Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/usage.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand All @@ -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-の優先順位))。
Expand Down
4 changes: 3 additions & 1 deletion docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand All @@ -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)):
Expand Down
123 changes: 120 additions & 3 deletions src/package_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 の処理。
Expand Down Expand Up @@ -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]
Expand Down
17 changes: 17 additions & 0 deletions tests/fixtures/go-install/README.md
Original file line number Diff line number Diff line change
@@ -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.
Binary file added tests/fixtures/go-install/direct-v1.0.0.zip
Binary file not shown.
Binary file added tests/fixtures/go-install/direct-v1.1.0.zip
Binary file not shown.
39 changes: 39 additions & 0 deletions tests/fixtures/go-install/generate.py
Original file line number Diff line number Diff line change
@@ -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"))
Binary file added tests/fixtures/go-install/indirect-v1.0.0.zip
Binary file not shown.
Loading