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 0000000..f27925c Binary files /dev/null and b/tests/fixtures/go-install/direct-v1.0.0.zip differ 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 0000000..105f8b9 Binary files /dev/null and b/tests/fixtures/go-install/direct-v1.1.0.zip differ diff --git a/tests/fixtures/go-install/generate.py b/tests/fixtures/go-install/generate.py new file mode 100644 index 0000000..0dba7e2 --- /dev/null +++ b/tests/fixtures/go-install/generate.py @@ -0,0 +1,39 @@ +"""Regenerate the offline Go proxy archives with Python's standard library.""" + +from pathlib import Path +from zipfile import ZIP_STORED, ZipFile, ZipInfo + +MODULES = [ + ( + "direct", + "v1.0.0", + "module example.com/direct\n\ngo 1.17\n", + 'package direct\nfunc Message() string { return "old" }\n', + ), + ( + "direct", + "v1.1.0", + "module example.com/direct\n\ngo 1.17\n\nrequire example.com/indirect v1.0.0\n", + ( + 'package direct\nimport "example.com/indirect"\n' + "func Message() string { return indirect.Message() }\n" + ), + ), + ( + "indirect", + "v1.0.0", + "module example.com/indirect\n\ngo 1.17\n", + 'package indirect\nfunc Message() string { return "new" }\n', + ), +] + + +if __name__ == "__main__": + for name, version, manifest, source in MODULES: + archive = Path(__file__).parent / f"{name}-{version}.zip" + with ZipFile(archive, "w", compression=ZIP_STORED) as output: + for filename, contents in [("go.mod", manifest), ("message.go", source)]: + entry = ZipInfo(f"example.com/{name}@{version}/{filename}") + entry.create_system = 3 + entry.external_attr = 0o100644 << 16 + output.writestr(entry, contents.encode("utf-8")) diff --git a/tests/fixtures/go-install/indirect-v1.0.0.zip b/tests/fixtures/go-install/indirect-v1.0.0.zip new file mode 100644 index 0000000..77955f1 Binary files /dev/null and b/tests/fixtures/go-install/indirect-v1.0.0.zip differ