From 4209720e9087b218f3aea606f0d29a961e83f406 Mon Sep 17 00:00:00 2001 From: owa <20108042+owayo@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:42:01 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20Go=E3=81=AEinstall=E3=81=A7=E6=9C=AC?= =?UTF-8?q?=E4=BD=93=E3=83=81=E3=82=A7=E3=83=83=E3=82=AF=E3=82=B5=E3=83=A0?= =?UTF-8?q?=E3=81=A8=E9=96=93=E6=8E=A5=E4=BE=9D=E5=AD=98=E3=82=92=E6=8F=83?= =?UTF-8?q?=E3=81=88=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 引数なしのgo mod downloadはGo 1.17以降でモジュール本体のハッシュを go.sumへ記録せず、更新直後のreadonly buildが失敗していた。 go mod tidyでgo.modとgo.sumをソースに合わせて同期する。 ローカルのGo proxyを使った回帰テストで旧処理の失敗、新しい間接依存と 本体ハッシュの追加、readonly build/testの成功を検証する。 英語と日本語の文書に依存整理とvendorの更新手順を記載する。 --- docs/usage.ja.md | 4 +- docs/usage.md | 4 +- src/package_manager.rs | 123 +++++++++++++++++- tests/fixtures/go-install/README.md | 17 +++ tests/fixtures/go-install/direct-v1.0.0.zip | Bin 0 -> 399 bytes tests/fixtures/go-install/direct-v1.1.0.zip | Bin 0 -> 479 bytes tests/fixtures/go-install/generate.py | 39 ++++++ tests/fixtures/go-install/indirect-v1.0.0.zip | Bin 0 -> 411 bytes 8 files changed, 182 insertions(+), 5 deletions(-) create mode 100644 tests/fixtures/go-install/README.md create mode 100644 tests/fixtures/go-install/direct-v1.0.0.zip create mode 100644 tests/fixtures/go-install/direct-v1.1.0.zip create mode 100644 tests/fixtures/go-install/generate.py create mode 100644 tests/fixtures/go-install/indirect-v1.0.0.zip diff --git a/docs/usage.ja.md b/docs/usage.ja.md index ca335fd..89157c6 100644 --- a/docs/usage.ja.md +++ b/docs/usage.ja.md @@ -353,7 +353,7 @@ install が失敗しても、残りの install は続けます。失敗したコ | | `Pipfile.lock` | `pipenv install` | | | `pyproject.toml` / `requirements.txt` | `pip install -e .` | | Rust | `Cargo.toml`、または Tauri プロジェクトの `src-tauri/Cargo.toml`(このときは `src-tauri/` で実行) | `cargo update` | -| Go | `go.mod` | `go mod download` | +| Go | `go.mod` | `go mod tidy` | | Ruby | `Gemfile` | `bundle install` | | PHP | `composer.json` | `composer update` | | Java | `gradlew` | `./gradlew dependencies` | @@ -365,6 +365,8 @@ install が失敗しても、残りの install は続けます。失敗したコ PHP プロジェクトを `--install` で処理するとき、depup は `composer install` ではなく `composer update` を実行します。`composer install` は既存のロックファイルを再利用するため、depup が直前に `composer.json` へ書いた制約を反映できません。`composer update` で制約を解決し直し、`composer.lock` を更新します。 +Go では `go mod tidy` で、テストやプラットフォーム固有の import も含めて、ソースコードと `go.mod`・`go.sum` を揃えます。`go.mod` のハッシュに加えてモジュール本体のハッシュも記録し、後続のビルドに必要なチェックサムを揃えます。引数なしの `go mod download` と異なり、不足する間接依存を追加し、使われなくなった依存やハッシュを削除します。`// indirect` コメントが変わるほか、依存先が新しい Go を要求すると `go`・`toolchain` ディレクティブが変わる場合もあります。`vendor/` をコミットするプロジェクトでは、その後に `go mod vendor` で更新してください。tidy が失敗した場合は install 失敗として報告します。 + ### 推移的依存と age フィルター age フィルターは、depup がマニフェストに書き込むバージョンを決めるものです。`--install` で解決される推移的依存にも効くかどうかは、パッケージマネージャーによって違います。複数のマニフェストをまとめて解決する install には、更新がなかったメンバーも含めて最も厳しい設定を渡します([適用される age の優先順位](#適用される-age-の優先順位))。 diff --git a/docs/usage.md b/docs/usage.md index b3a43d9..951110f 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -352,7 +352,7 @@ The package manager is detected from files in the directory where the install ru | | `Pipfile.lock` | `pipenv install` | | | `pyproject.toml` / `requirements.txt` | `pip install -e .` | | Rust | `Cargo.toml`, or `src-tauri/Cargo.toml` in a Tauri project (`cargo update` then runs in `src-tauri/`) | `cargo update` | -| Go | `go.mod` | `go mod download` | +| Go | `go.mod` | `go mod tidy` | | Ruby | `Gemfile` | `bundle install` | | PHP | `composer.json` | `composer update` | | Java | `gradlew` | `./gradlew dependencies` | @@ -364,6 +364,8 @@ If none of the listed files is present, no install runs for that language, and n When `--install` processes a PHP project, depup runs `composer update` rather than `composer install`. `composer install` reuses the existing lock file and cannot reflect constraints that depup has just changed in `composer.json`; `composer update` resolves those constraints and refreshes `composer.lock`. +For Go, `go mod tidy` synchronizes `go.mod` and `go.sum` with the source code, including tests and platform-specific imports. It records module content checksums as well as `go.mod` checksums so that subsequent builds have the required sums. Unlike argumentless `go mod download`, it also adds missing indirect requirements and removes unused requirements and sums. This may change `// indirect` comments and, when dependencies require a newer Go release, the `go` or `toolchain` directives. Projects that commit `vendor/` must run `go mod vendor` afterwards to refresh it. A tidy failure is reported as an install failure. + ### Transitive Dependencies and the Age Filter The age filter decides which versions depup writes into manifests. Whether it also reaches transitive dependencies during `--install` depends on the package manager. An install that resolves multiple manifests uses the strictest policy of those manifests, including members with no updates ([Resolution Priority](#resolution-priority)): diff --git a/src/package_manager.rs b/src/package_manager.rs index ffd026e..c2111e7 100644 --- a/src/package_manager.rs +++ b/src/package_manager.rs @@ -277,8 +277,9 @@ impl SystemPackageManager { "pipenv" => vec!["pipenv", "install"], // Rust の処理 "cargo" => vec!["cargo", "update"], - // Go 言語 - "go" => vec!["go", "mod", "download"], + // Go 1.17 以降の引数なし download は本体のハッシュを go.sum に保存しない。 + // tidy でハッシュと、新版が必要とする間接依存の require を揃える。 + "go" => vec!["go", "mod", "tidy"], // Ruby の処理 "bundle" => vec!["bundle", "install"], // PHP の処理。 @@ -617,7 +618,123 @@ mod tests { fn test_get_install_command_go() { let pm = SystemPackageManager::new(); let cmd = pm.get_install_command("go"); - assert_eq!(cmd, vec!["go", "mod", "download"]); + assert_eq!(cmd, vec!["go", "mod", "tidy"]); + } + + #[test] + fn go_install_completes_checksums_and_indirect_requirements() { + use std::fs; + + if which::which("go").is_err() { + eprintln!("Skipping Go install regression: go is not installed"); + return; + } + + let temp = tempfile::tempdir().unwrap(); + let project = temp.path().join("project"); + let proxy = temp.path().join("proxy"); + fs::create_dir_all(&project).unwrap(); + for (name, version, archive, manifest) in [ + ( + "direct", + "v1.0.0", + include_bytes!("../tests/fixtures/go-install/direct-v1.0.0.zip").as_slice(), + "module example.com/direct\n\ngo 1.17\n", + ), + ( + "direct", + "v1.1.0", + include_bytes!("../tests/fixtures/go-install/direct-v1.1.0.zip").as_slice(), + "module example.com/direct\n\ngo 1.17\n\nrequire example.com/indirect v1.0.0\n", + ), + ( + "indirect", + "v1.0.0", + include_bytes!("../tests/fixtures/go-install/indirect-v1.0.0.zip").as_slice(), + "module example.com/indirect\n\ngo 1.17\n", + ), + ] { + let versions = proxy.join(format!("example.com/{name}/@v")); + fs::create_dir_all(&versions).unwrap(); + fs::write(versions.join(format!("{version}.zip")), archive).unwrap(); + fs::write(versions.join(format!("{version}.mod")), manifest).unwrap(); + fs::write( + versions.join(format!("{version}.info")), + format!(r#"{{"Version":"{version}","Time":"2020-01-01T00:00:00Z"}}"#), + ) + .unwrap(); + } + + let env = vec![ + ( + "GOPROXY".into(), + reqwest::Url::from_directory_path(&proxy) + .unwrap() + .to_string(), + ), + ("GOSUMDB".into(), "off".into()), + ("GOPRIVATE".into(), String::new()), + ("GONOPROXY".into(), "none".into()), + ("GOENV".into(), "off".into()), + ("GOFLAGS".into(), String::new()), + ("GO111MODULE".into(), "on".into()), + ("GOWORK".into(), "off".into()), + ("GOTOOLCHAIN".into(), "local".into()), + ( + "GOMODCACHE".into(), + temp.path().join("modcache").to_str().unwrap().into(), + ), + ( + "GOCACHE".into(), + temp.path().join("buildcache").to_str().unwrap().into(), + ), + ]; + let pm = SystemPackageManager::new(); + let run = |args: &[&str]| pm.run_command(args, &project, &env).unwrap(); + let assert_success = |output: Output| { + assert!( + output.status.success(), + "Go command failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + }; + fs::write( + project.join("go.mod"), + "module example.com/app\n\ngo 1.17\n\nrequire example.com/direct v1.0.0\n", + ) + .unwrap(); + fs::write( + project.join("main.go"), + "package main\nimport \"example.com/direct\"\nfunc main() { println(direct.Message()) }\n", + ) + .unwrap(); + assert_success(run(&["go", "mod", "tidy"])); + let manifest_path = project.join("go.mod"); + let manifest = fs::read_to_string(&manifest_path).unwrap(); + fs::write(&manifest_path, manifest.replace("v1.0.0", "v1.1.0")).unwrap(); + + // 旧コマンドは成功しても本体のハッシュを保存せず、readonly build は失敗する。 + assert_success(run(&["go", "mod", "download"])); + let sums = fs::read_to_string(project.join("go.sum")).unwrap(); + assert!(sums.contains("example.com/direct v1.1.0/go.mod h1:")); + assert!(!sums.contains("example.com/direct v1.1.0 h1:")); + assert!( + !run(&["go", "build", "-mod=readonly", "./..."]) + .status + .success() + ); + + assert_success(run(&pm.get_install_command("go"))); + let sums = fs::read_to_string(project.join("go.sum")).unwrap(); + assert!(sums.contains("example.com/direct v1.1.0 h1:")); + assert!(sums.contains("example.com/indirect v1.0.0 h1:")); + assert!( + fs::read_to_string(&manifest_path) + .unwrap() + .contains("example.com/indirect v1.0.0 // indirect") + ); + assert_success(run(&["go", "build", "-mod=readonly", "./..."])); + assert_success(run(&["go", "test", "-mod=readonly", "./..."])); } #[test] diff --git a/tests/fixtures/go-install/README.md b/tests/fixtures/go-install/README.md new file mode 100644 index 0000000..f423ee5 --- /dev/null +++ b/tests/fixtures/go-install/README.md @@ -0,0 +1,17 @@ +# Offline Go install fixtures + +These archives contain fictional modules for the Go install regression test in +`src/package_manager.rs`. Version `v1.1.0` of `example.com/direct` imports a new +transitive dependency, `example.com/indirect v1.0.0`; version `v1.0.0` does not. + +Regenerate the deterministic, uncompressed ZIP archives with: + +```sh +python3 tests/fixtures/go-install/generate.py +``` + +The Rust test creates the proxy metadata and a sample application in a temporary +directory. It uses this file proxy with the checksum server disabled and isolated +module/build caches, so it needs Go 1.17 or later but no network access. +The test is skipped when Go is unavailable. Python is only needed to regenerate +the committed archives. diff --git a/tests/fixtures/go-install/direct-v1.0.0.zip b/tests/fixtures/go-install/direct-v1.0.0.zip new file mode 100644 index 0000000000000000000000000000000000000000..f27925cc361c74a1f00bef9cdb2144c27100f916 GIT binary patch literal 399 zcmWIWW@Zs#fB;1X_ZQdxDg!wntN_HR6^Xe8IjMTd`MLTjnMJ9|B@ShVdIow1`sw+4 zx%nwTRGO2jfT@g&D?MMqP|whuD*(lUYYfY;nE?$3VHLa<0$!H1wi^Hh{FH?6<%V< literal 0 HcmV?d00001 diff --git a/tests/fixtures/go-install/direct-v1.1.0.zip b/tests/fixtures/go-install/direct-v1.1.0.zip new file mode 100644 index 0000000000000000000000000000000000000000..105f8b9de2691b73bb2b35eb9d493f25ba93d3ee GIT binary patch literal 479 zcmWIWW@Zs#fB;2?Uh(uF9zYHVD*$n7MPhD2PO4sVey)B>W>IQ#i9?y8o}r$BetN!M zZhi_7mFA=>U@GI{O3zmSsx;@~DoQOZ1!_f6l$i%HO#x_{ft~?Z0E#PKZdbpS2(%r9 zRq(nZH?_DpF+Ei;J-;9^IUC4QfSAjbnOl%wRHC4S>@t|8N?d8BdC3aC5WO0j3dJQw znR)37)e1$aC8b4q3NVFw2qm>ZCo(dLFyjs?pgX}}Nh63twijE#p__*8Wr$e}3`-i@ Y@S27QfB4zW=0{~7@W=#M9 literal 0 HcmV?d00001