Skip to content

[WIP] fix(modpack): 将 CurseForge API Key 改为编译期可选常量#118

Merged
HsiangNianian merged 3 commits intoHydroRoll-Team:feature/multi-instancesfrom
BegoniaHe:fix/curseforge-api-key
Mar 18, 2026
Merged

[WIP] fix(modpack): 将 CurseForge API Key 改为编译期可选常量#118
HsiangNianian merged 3 commits intoHydroRoll-Team:feature/multi-instancesfrom
BegoniaHe:fix/curseforge-api-key

Conversation

@BegoniaHe
Copy link
Copy Markdown
Contributor

fix(modpack): 将 CurseForge API Key 改为编译期可选常量

修复 env!() 宏在开发者本地无 CURSEFORGE_API_KEY 时导致编译失败的问题,改用 option_env!() + build.rs 中的 dotenvy 读取 .env 文件,实现编译期嵌入、缺失时优雅降级。

更改类型

  • Bug 修复(修复问题的非破坏性更改)
  • 新功能(添加功能的非破坏性更改)
  • 破坏性更改(会导致现有功能无法正常工作的修复或功能)
  • 文档更新
  • UI/UX 改进
  • 性能优化
  • 代码重构(无功能性更改)
  • 配置更改
  • 测试添加或更新

LLM 生成代码声明

  • 此 PR 不包含 LLM 生成的代码,我提供质量担保

相关 Issue

相关 #110 #117

更改内容

后端 (Rust)

  • modpack.rs:将 env!("CURSEFORGE_API_KEY") 替换为 const CURSEFORGE_API_KEY: Option<&str> = option_env!("CURSEFORGE_API_KEY"),key 不存在时编译为 None,调用 CurseForge 功能时返回友好错误而非 panic
  • build.rs:添加 dotenvy::dotenv() 调用,允许通过 .env 文件在编译期注入 key,并注册 cargo:rerun-if-changed / cargo:rerun-if-env-changed 确保增量构建正确

前端 (Svelte)

配置

  • Cargo.toml:在 [build-dependencies] 中添加 dotenvy = { version = "0.15", default-features = false }
  • .gitignore:添加 .env / .env.local 忽略规则,防止 key 被意外提交
  • .env.example:新增示例文件,说明可选配置项及获取方式

测试

测试环境

  • 操作系统:Fedora Linux 6.19.6-300.fc44.x86_64 x86_64
  • DropOut 版本:0.2.0-alpha.5
  • 测试的 Minecraft 版本:N/A
  • Mod 加载器:N/A

测试用例

  • 已在 Windows 上测试
  • 已在 macOS 上测试
  • 已在 Linux 上测试
  • 已测试原版 Minecraft
  • 已测试 Fabric
  • 已测试 Forge
  • 已测试游戏启动
  • 已测试登录流程
  • 已测试 Java 检测/下载

测试步骤

  1. 不设置 CURSEFORGE_API_KEY,不创建 .env 文件,直接执行 cargo check → 应编译通过(无报错)
  2. 创建 .env 文件并写入 CURSEFORGE_API_KEY=test_key,执行 cargo check → 应编译通过,key 被嵌入二进制
  3. 不含 key 的构建中触发 CurseForge modpack 导入 → 应返回友好错误提示而非 panic

检查清单

代码质量

  • 我的代码遵循项目的代码风格指南
  • 我已对自己的代码进行了自审
  • 我已对难以理解的区域添加了注释
  • 我的更改没有产生新的警告或错误

测试验证

  • 我已在本地测试了我的更改
  • 我已添加测试来证明我的修复有效或功能正常工作
  • 新的和现有的单元测试在本地通过
  • 我至少在一个目标平台上进行了测试

文档更新

  • 我已相应地更新了文档
  • 如有需要,我已更新 README
  • 我已在必要处添加/更新代码注释

依赖项

  • 我已检查没有添加不必要的依赖项
  • 所有新依赖项都已正确记录
  • Cargo.lock 已更新

附加说明

dotenvy 仅作为 build-dependency,不会进入最终二进制。官方发布构建通过 CI 环境变量注入 key,普通开发者无需任何操作即可正常编译和运行。

Copilot AI review requested due to automatic review settings March 12, 2026 08:55
@vercel
Copy link
Copy Markdown

vercel bot commented Mar 12, 2026

@BegoniaHe is attempting to deploy a commit to the retrofor Team on Vercel.

A member of the Team first needs to authorize it.

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @BegoniaHe, your pull request is larger than the review limit of 150000 diff characters

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR aims to make the CurseForge API key optional at compile time to avoid local build failures when CURSEFORGE_API_KEY is unset, but it also includes substantial additional backend/frontend functionality changes (game lifecycle, instance import/export/repair, Java module refactors, dev server/CSP tweaks).

Changes:

  • Make CurseForge API key compile-time optional via option_env!() and add .env handling in the build script.
  • Add game lifecycle tracking (start/stop + game-exited payload), instance operations locking, and instance import/export/repair APIs.
  • Refactor parts of the Java subsystem (provider retries/bounded concurrency, persistence/cache, detection split) and update the React UI to use the new lifecycle/events and instance tooling.

Reviewed changes

Copilot reviewed 33 out of 34 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
src-tauri/build.rs Attempts to load .env during build to support optional build-time vars.
src-tauri/Cargo.toml Adds dotenvy as a build-dependency.
src-tauri/src/core/modpack.rs Switches CurseForge key to option_env!() and errors gracefully when missing.
src-tauri/tauri.conf.json Tweaks dev URL and dev CSP configuration.
src-tauri/capabilities/default.json Allows dev-server remote URLs for Tauri capabilities.
src-tauri/src/utils/zip.rs Adds path sanitization for archive extraction to prevent traversal.
src-tauri/src/main.rs Introduces GameProcessState, stop_game, path resolution changes, and new instance commands.
src-tauri/src/core/instance.rs Adds per-instance operation locking, shared-cache path resolution, and import/export/repair features.
src-tauri/src/core/downloader.rs Hardens Java download queue persistence with a global file lock + atomic writes + helper APIs.
src-tauri/src/core/java/mod.rs Adds provider-pluggable APIs and new resume result types; changes TS export paths.
src-tauri/src/core/java/error.rs Adds structured resume-failure reason enum + tests; changes TS export paths.
src-tauri/src/core/java/cache.rs Moves Java catalog cache logic into a dedicated module with atomic writes.
src-tauri/src/core/java/persistence.rs Uses atomic write strategy for Java config persistence; adds load_java_config_result.
src-tauri/src/core/java/providers/adoptium.rs Adds bounded concurrency and retry/backoff for catalog requests + tests.
src-tauri/src/core/java/validation.rs Reads Java version from stdout when stderr is empty.
src-tauri/src/core/java/install.rs Extracts Java download+install flow into a dedicated module.
src-tauri/src/core/java/detection/* Splits Java detection into common/linux/macos/windows/unix modules (replacing monolith).
packages/ui/vite.config.ts Pins dev server to 127.0.0.1:5173 with strict port.
packages/ui/src/client.ts Adds new instance APIs and stop-game; (still types resumeJavaDownloads as old return type).
packages/ui/src/stores/game-store.ts Adds lifecycle listener + start/stop integration and per-instance running/launching state.
packages/ui/src/pages/index.tsx Initializes game lifecycle listener on app load.
packages/ui/src/pages/instances-view.tsx Adds start/stop controls per instance + import/export/repair UI actions.
packages/ui/src/components/bottom-bar.tsx Migrates start/stop behavior to the centralized game store and instance selection UI.
packages/ui/src/models/instance.ts Adds import/export/repair helpers and more robust active-instance handling.
packages/ui/src/models/auth.ts Improves login error handling and polling interval safety.
.gitignore Ignores .env / .env.local and adds .vscode/.
.env.example Documents optional CURSEFORGE_API_KEY setup.
.github/copilot-instructions.md Updates contributor instructions (currently references non-existent packages/ui-new).
Comments suppressed due to low confidence (1)

src-tauri/src/core/instance.rs:353

  • delete_instance acquires an operation lock via begin_operation, but there are multiple early-? returns (e.g. instance not found, save() failure, directory deletion failure) before end_operation is called. That can permanently “busy-lock” an instance. Ensure end_operation is always executed (e.g. RAII guard / drop guard pattern, or structured match/finally-style cleanup).
    pub fn delete_instance(&self, id: &str) -> Result<(), String> {
        self.begin_operation(id, InstanceOperation::Delete)?;
        let mut config = self.instances.lock().unwrap();

        // Find the instance
        let instance_index = config
            .instances
            .iter()
            .position(|i| i.id == id)
            .ok_or_else(|| format!("Instance {} not found", id))?;


You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +39 to +42
#[ts(
export,
export_to = "../../packages/ui-new/src/types/bindings/java/index.ts"
)]
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ResumeJavaDownloadFailureReason is exported via ts-rs to ../../packages/ui-new/..., but this repo uses TS_RS_EXPORT_DIR=./packages/ui/src/types/bindings and doesn’t have packages/ui-new. This will break/fragment the generated bindings. Export it relative to the configured bindings dir instead.

Suggested change
#[ts(
export,
export_to = "../../packages/ui-new/src/types/bindings/java/index.ts"
)]
#[ts(export, export_to = "java/index.ts")]

Copilot uses AI. Check for mistakes.
Comment on lines 385 to 387
export function resumeJavaDownloads(): Promise<JavaInstallation[]> {
return invoke<JavaInstallation[]>("resume_java_downloads");
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resume_java_downloads now returns ResumeJavaDownloadsResult on the Rust side, but the TS client still types it as Promise<JavaInstallation[]>. This will cause a type mismatch and likely runtime breakage for callers. Update the client type (and any consumers) to match the new return shape.

Copilot uses AI. Check for mistakes.
Comment on lines +501 to +505
pub fn export_instance(&self, id: &str, archive_path: &Path) -> Result<PathBuf, String> {
self.begin_operation(id, InstanceOperation::ImportExport)?;
let instance = self
.get_instance(id)
.ok_or_else(|| format!("Instance {} not found", id))?;
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

export_instance takes an operation lock (begin_operation) but only releases it at the very end. Any error during file creation, zip writing, or directory traversal will return early and keep the lock held. Ensure end_operation runs on all paths (RAII guard / scope guard).

Copilot uses AI. Check for mistakes.
Comment on lines +573 to +577
let imported = self.create_instance(final_name, app_handle)?;
self.begin_operation(&imported.id, InstanceOperation::ImportExport)?;

for index in 0..archive.len() {
let mut entry = archive.by_index(index).map_err(|e| e.to_string())?;
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import_instance calls begin_operation(&imported.id, ...) but does not guarantee end_operation on error paths (zip iteration/extraction, update_instance failure, etc.). If any of those ? return, the instance can remain locked as “busy”. Use a scope guard to always release the operation lock.

Copilot uses AI. Check for mistakes.
Comment on lines +7 to +65
@@ -33,59 +34,80 @@ src-tauri/ # Rust backend
version_merge.rs # Parent version inheritance merging
utils/
zip.rs # Native library extraction
ui/ # Svelte 5 frontend
src/
App.svelte # Main app component, enforces dark mode
stores/ # Svelte 5 runes state management ($state, $effect)
auth.svelte.ts # Authentication state with device code polling
game.svelte.ts # Game state (running, logs)
settings.svelte.ts # Settings + Java detection
ui.svelte.ts # UI state (toasts, modals, active view)
components/ # UI components (HomeView, VersionsView, SettingsView, etc.)
lib/ # Reusable components (DownloadMonitor, GameConsole)
packages/
ui-new/ # React frontend used by Tauri
src/
main.tsx # React Router setup (hash routing)
pages/ # Route views (Home, Versions, Settings, ...)
stores/ # Zustand stores
components/ # UI components
ui/ # Legacy Svelte 5 frontend
src/
App.svelte # Main app component, enforces dark mode
stores/ # Svelte 5 runes state management ($state, $effect)
auth.svelte.ts # Authentication state with device code polling
game.svelte.ts # Game state (running, logs)
settings.svelte.ts # Settings + Java detection
ui.svelte.ts # UI state (toasts, modals, active view)
components/ # UI components (HomeView, VersionsView, SettingsView, etc.)
lib/ # Reusable components (DownloadMonitor, GameConsole)
```

## Critical Development Workflows

### Development Mode

```bash
cargo tauri dev # Starts frontend dev server (Vite on :5173) + Tauri window
cargo tauri dev # Runs ui-new dev server (Vite on :5173) + Tauri window
```
- Frontend uses **Rolldown-based Vite fork** (`npm:rolldown-vite@7.2.5`) with hot reload

- `src-tauri/tauri.conf.json` runs `pnpm --filter @dropout/ui-new dev`
- Frontend uses **Rolldown-based Vite fork** (`npm:rolldown-vite@^7`) with hot reload
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.github/copilot-instructions.md now references packages/ui-new as the active React UI and updates commands (pnpm --filter @dropout/ui-new ...), but this repo only contains packages/ui (no ui-new). This makes the contributor instructions incorrect/misleading; update the paths/commands to match the actual workspace layout.

Copilot uses AI. Check for mistakes.
if let Ok(path) = dotenvy::dotenv() {
// Tell Cargo to re-run this script if .env changes
println!("cargo:rerun-if-changed={}", path.display());
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dotenvy::dotenv() in a build script only affects the build-script process; it does not automatically make CURSEFORGE_API_KEY visible to the crate compilation where option_env!() is evaluated. As written, values from .env likely won’t be embedded, contradicting the comment and PR description. If you want .env to inject the key at compile time, emit cargo:rustc-env=CURSEFORGE_API_KEY=... when the var is present (and keep the rerun-if-* lines).

Suggested change
}
}
// If CURSEFORGE_API_KEY is set (from .env or the environment), forward it to rustc
if let Ok(value) = std::env::var("CURSEFORGE_API_KEY") {
println!("cargo:rustc-env=CURSEFORGE_API_KEY={}", value);
}

Copilot uses AI. Check for mistakes.
Comment on lines +47 to +79
struct RunningGameProcess {
child: Child,
instance_id: String,
version_id: String,
}

pub struct GameProcessState {
running_game: AsyncMutex<Option<RunningGameProcess>>,
}

impl Default for GameProcessState {
fn default() -> Self {
Self::new()
}
}

impl GameProcessState {
pub fn new() -> Self {
Self {
running_game: AsyncMutex::new(None),
}
}
}

#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct GameExitedEvent {
instance_id: String,
version_id: String,
exit_code: Option<i32>,
was_stopped: bool,
}

Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR metadata/description focuses on making CURSEFORGE_API_KEY optional at build time, but this PR also adds game lifecycle management, instance import/export/repair commands, Java module refactors, CSP/dev server changes, etc. That scope mismatch makes it hard to review/land safely; consider splitting into focused PRs (API key fix vs lifecycle/instance/java changes).

Copilot uses AI. Check for mistakes.
Comment on lines +451 to +456
self.begin_operation(id, InstanceOperation::ImportExport)?;
let source_instance = self
.get_instance(id)
.ok_or_else(|| format!("Instance {} not found", id))?;

{
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

duplicate_instance calls begin_operation but only calls end_operation on the success path. Any error before line 496 (missing source instance, invalid name, copy failure, insert/save failure) will leave the operation lock held and block future operations on that instance. Add guaranteed cleanup for the lock.

Copilot uses AI. Check for mistakes.
Comment on lines +722 to +723
let data = fs::read(&path).map_err(|e| e.to_string())?;
writer.write_all(&data).map_err(|e| e.to_string())?;
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

append_directory_to_zip reads each file fully into memory (fs::read) before writing to the zip. Large instance directories (world saves, logs, mods) can cause high peak memory usage or OOM. Prefer streaming via std::io::copy from a File into the zip writer, or chunked reads.

Suggested change
let data = fs::read(&path).map_err(|e| e.to_string())?;
writer.write_all(&data).map_err(|e| e.to_string())?;
let mut file = fs::File::open(&path).map_err(|e| e.to_string())?;
std::io::copy(&mut file, writer).map_err(|e| e.to_string())?;

Copilot uses AI. Check for mistakes.
Comment on lines +68 to +97
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(
export,
export_to = "../../packages/ui-new/src/types/bindings/java/index.ts"
)]
pub struct ResumeJavaDownloadFailure {
pub major_version: u32,
pub image_type: String,
pub install_path: String,
pub reason: ResumeJavaDownloadFailureReason,
pub error: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(
export,
export_to = "../../packages/ui-new/src/types/bindings/java/index.ts"
)]
pub struct ResumeJavaDownloadsResult {
pub successful_installations: Vec<JavaInstallation>,
pub failed_downloads: Vec<ResumeJavaDownloadFailure>,
pub total_pending: usize,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)]
#[serde(rename_all = "lowercase")]
#[ts(
export,
export_to = "../../packages/ui-new/src/types/bindings/java/index.ts"
)]
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The #[ts(export_to = "../../packages/ui-new/..." )] paths point to a non-existent packages/ui-new directory in this repo and also bypass the existing TS_RS_EXPORT_DIR configuration (see .cargo/config.toml). This will break ts-rs type generation and leave the frontend bindings out of sync. Please export relative to the configured bindings dir (e.g. java/index.ts under packages/ui/src/types/bindings) and keep all exports consistent.

Copilot uses AI. Check for mistakes.
Replace env!("CURSEFORGE_API_KEY") with option_env!() so the build never
fails when the key is absent. dotenvy in build.rs reads an optional .env
file, letting maintainers supply the key locally without touching system
environment variables. .gitignore updated to exclude .env files.

Reviewed-by: Claude Sonnet 4.6
@BegoniaHe BegoniaHe force-pushed the fix/curseforge-api-key branch from 02bbbae to 41163b9 Compare March 12, 2026 09:09
@HsiangNianian HsiangNianian self-assigned this Mar 18, 2026
@HsiangNianian HsiangNianian changed the base branch from main to feature/multi-instances March 18, 2026 03:54
@HsiangNianian HsiangNianian merged commit 8c913f2 into HydroRoll-Team:feature/multi-instances Mar 18, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants