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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ make ci # CI と同じ検査 (fmt-check + lint + test、書き換
- age が有効な場合 (CLI `--age` / プロジェクト `minimumReleaseAge` / グローバル設定 / 組み込みデフォルト 1w のいずれか) の transitive 依存への適用方法は PM ごとに異なる。install フェーズも judge と同じ解決済み age を使うため、CLI `--age` を明示しなくてもプロジェクト設定やデフォルト 1w が transitive へ反映される:
- **Rust (cargo)**: `cargo update` 後に Cargo.lock を走査し、age 違反の crate を差し戻す (post-install audit。対象・差し戻し先・差し戻し方・表示は下の小項目)。cargo の再解決で連鎖する新たな違反に備えて最大 5 回まで反復する。**監査対象は install 前後で lock が変わったエントリだけに絞る** (`changed_entries`) — install 前の Cargo.lock を控えておき、同じバージョンのまま残っているものは depup が入れたものではないので照会しない。crates.io は crawler policy により 1 リクエスト/秒へ直列化されるため、lock 全体 (depup 自身なら 249 件) を舐めると監査だけで 4〜9 分かかり、進捗表示のない無音待ちがハングと区別できなかった (実測で監査対象 249 件 → 24 件)。install 前に Cargo.lock が無ければ baseline は空 = 全エントリが新規なので従来どおり全件監査になる。安全弁として監査には `LOCK_AGE_AUDIT_BUDGET` (180 秒) の時間予算があり、Cargo.lock の有無に関係なく常に効く (タイマーは lock ディレクトリごとに監査開始時から数えるので、lock が複数あれば各 180 秒)。超過時は監査済みの差し戻しを保持したまま打ち切り、未検証件数を stderr に警告する (exit code には影響しない)。install が 1 件でも失敗した場合、監査自体を実行しない。差し戻しで起動する `cargo update -p --precise` は 1 回 `CARGO_UPDATE_TIMEOUT` (120 秒) で打ち切る (`kill_on_drop` 付き)
- **監査するのは crates.io の crate だけ**。私設 registry の crate は crates.io の公開日で判定できないので対象外にする。以前は私設 registry の crate も同名の crates.io の crate の公開日で照会しており、別物の公開日で差し戻すかどうかを決めていた
- **差し戻し先**: depup がこの実行で更新した直接依存は、画面に出した更新先の版 (age を満たし、OSV の脆弱性チェックと `--max-change` も通った版) を優先する (`rollback_target` / `PreferredVersions`)。同じ semver 系列で今の版より古く age を満たし、install 前の lock にあった同じ系列の版以上のものに限る (manifest の版要求が lock より古いまま `--max-change` で古い版を選んだ場合に、install 前より古い版まで下げて既存コードが使っていた API を消さないため)。それ以外は、age を満たす安定版 (プレリリースを除く) のうち今の版より古い最新 (`pick_older_within_age`)。judge の版を優先しないと、OSV で退けた版を差し戻しで選び直してしまう。どちらの場合も install 前の lock の同系列の版 (`version_floor`) より古くはせず、age を満たす版がそれより古いものしか無ければ install 前の版へ戻す (install で入った変更だけを取り消す)。戻した install 前の版も期間内なら `LockAgeStatus::Restored` とし、「satisfy --age」の一覧とは別の行 (`returned to the version locked before the install, which is also newer than --age`) で黄色く出す (満たしたと読ませない)。まとめ解きでも同じ下限を `RollbackCandidate::minimum` として範囲固定の下端と結果の検査に使う
- **差し戻し先**: depup がこの実行で更新した直接依存は、画面に出した更新先の版 (age を満たし、OSV の脆弱性チェックと `--max-change` も通った版) を優先する (`rollback_target` / `PreferredVersions`)。同じ semver 系列で今の版より古く age を満たし、install 前の lock にあった同じ系列の版以上のものに限る (manifest の版要求が lock より古いまま `--max-change` で古い版を選んだ場合に、install 前より古い版まで下げて既存コードが使っていた API を消さないため)。それ以外は、同じ semver 系列で age を満たす安定版 (プレリリースを除く) のうち今の版より古い最新 (`pick_older_within_age`)。judge の版を優先しないと、OSV で退けた版を差し戻しで選び直してしまう。どちらの場合も install 前の lock の同系列の利用可能な版 (`version_floor`) より古くはせず、age を満たす版がそれより古いものしか無ければ install 前の版へ戻す (install で入った変更だけを取り消す)。crates.io adapter が yank 済み・公開日不明として取得一覧から外した版は、下限にも戻し先にも使わない。下限が無効なら同じ semver 系列の有効な age 条件内の版を選び、候補が無ければ `NoOlderCandidate` として未解消を報告する。戻した install 前の版も期間内なら `LockAgeStatus::Restored` とし、「satisfy --age」の一覧とは別の行 (`returned to the version locked before the install, which is also newer than --age`) で黄色く出す (満たしたと読ませない)。まとめ解きでも同じ下限を `RollbackCandidate::minimum` として範囲固定の下端と結果の検査に使う
- **1 件ずつの `--precise` が衝突する仕組み**: `cc` / `syn` のように 1 件ずつで済む crate は、従来どおり `cargo update -p <name>@<current> --precise <older>` で差し戻す。ただし `--precise` は対象の依存元 (対象に依存している crate) を lock の版に据え置く。互いを `=` で固定し合う crate 一族 (wasm-bindgen / js-sys / web-sys / wasm-bindgen-futures / wasm-bindgen-test など) では、据え置かれた依存元の `=` 要件が対象の古い版を許さない。一族の中に依存元を持たない crate が 1 つだけなら、それを動かせば依存先も連れて動く (`-p` で指定した crate の依存先は、必要なら更新される) が、2 つ以上あると互いを据え置き合い、どれから試しても全件が衝突して失敗する。以前はこの 1 件ずつの差し戻ししか無く、失敗は `--verbose` のときにしか出さなかったため、既定の出力には差し戻せた分だけが並んで `--age` が効いたように見えていた
- **まとめ解き** (`cargo_rollback/batch.rs`): 1 件ずつの差し戻しで衝突した crate は、まとめて解き直す。利用者の Cargo.toml は一切書き換えない。workspace の依存解決に必要な部分 (root と member の Cargo.toml、空の target ファイル、Cargo.lock) だけを一時ディレクトリに写し (`scratch.rs`)、その写しに一時的な固定用の別名依存 (`depup-age-pin-N = { package = ..., version = ..., default-features = false }`) を足して、対象の crate を `-p <name>@<ver>` に並べてまとめて `cargo update` する。直接依存でない crate も固定できるように、既存の依存の版要求を書き換えるのではなく別名の依存を足す (推移依存だけの一族も救える)。対象は最初から lock の依存辺でつながる組 (連結成分) に分け、大きい組から解く (辺の無い組どうしは互いの固定に縛られないので、まとめて試す利点が無く、解けない 1 件が一族を巻き添えにして予算も食うだけになる。予算が尽きたときに戻す crate の多い一族を先に済ませておける)。組ごとに試す順は「全員を `=目標` で固定 → 一族の頂点 (一族内に依存元を持たない crate) だけを `=` で固定 → 全員を同じ系列で目標以下 (`>=max(系列の下限, install 前の版), <=目標`) に固定」。範囲固定は judge が一族の 1 つだけ古い版を選んだ場合など目標同士が噛み合わないときのためで、選ばれた版は次のパスで公開日を監査し直す。Cargo.toml の直接の版要求が目標を許さない crate は、まとめ解きの前に外して `BlockedByManifest` として報告する (解けない 1 件が一族のまとめ解きを巻き添えにしない)。時間切れは cargo の失敗と型で区別し (`CargoError::TimedOut` / `RunError::OutOfTime`)、別の手を試し続けずに残りを `NotAttempted` にする。元の Cargo.lock を置き換える前にも検証の分の時間が残っているかを確かめる (置き換えてから時間切れになると、戻すだけの無駄になり、理由も「Cargo.toml が受け入れない」と取り違える)。まとめ解きが 1 件も戻せなかったときは、lock の内容と候補の集合の組 (`together_fingerprint`) を覚えておき、同じ条件では試し直さない (lock だけを鍵にすると、新しい候補が加わった組を試さず、無関係な差し戻しのたびに同じ組を丸ごと試し直していた)。固定を外して lock を整えてから元の Cargo.lock を置き換え、元の Cargo.toml がその lock をそのまま受け入れること (`cargo update --workspace --locked`) を確かめる。受け入れられなければ、置き換える前の Cargo.lock に戻す。作業中に元の Cargo.toml / Cargo.lock が書き換わったら、そちらを優先して手を引く。cargo は元のディレクトリで起動する — cargo の設定ファイル (`.cargo/config.toml`) も rustup の toolchain 指定 (`rust-toolchain.toml` 等) も起動ディレクトリから上へたどって探すので、一時ディレクトリで起動すると、元のプロジェクトの source replacement や toolchain の固定が効かないまま解いてしまう。まとめ解きで新しく Cargo.lock に入った crate も同じく監査して差し戻し、lock から外れた crate は `<name> <ver> → removed` と表示して差し戻せた件数に含める
- **差し戻しの成否は lock を読み直して判定する**: `cargo update --precise` の exit 0 だけで成功にしない (directory source への置き換えでは `--precise` が何もせず exit 0 になり、以前は「差し戻した」と誤報告したうえ試行済みとして二度と扱わなかった)。成功後に lock を読み直して今の版が外れたことを確かめ、外れていなければ失敗としてまとめ解きへ回す。監査の最後には `AdjustmentLog::reconcile_with_lock` で「差し戻した」記録を最終の lock と突き合わせ (元の版が残っていれば失敗に直し、後の差し戻しでさらに動いた行き先に合わせる)、最終の lock に残る install で変わった版を、監査中に取得した公開日 (キャッシュのみ。通信しない) で判定し直す。公開日を取得していない版は未検証 (`unchecked`) として数える。まとめ解きの検証に失敗して元の Cargo.lock を戻せなかったときは、元の内容を OS の一時ディレクトリへ退避し、その場所を `problems` として `--verbose` なしで表示する。確認から書き戻しまでの間に他のプロセスが Cargo.lock を書いた変更までは守れない (cargo 自身のコマンドも Cargo.lock を排他せずに書くので同じ条件)
Expand Down
6 changes: 4 additions & 2 deletions docs/usage.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -394,9 +394,11 @@ Rust では、install の前後で `Cargo.lock` のバージョンが変わっ
差し戻し先のバージョンは次のとおりです。

- depup がこの実行で更新した直接依存は、更新結果に表示したバージョンを優先します。このバージョンは、age フィルター・脆弱性チェック・`--max-change` をすでに通っています。
- それ以外のクレートは、age フィルターの条件を満たし、ロックされた版より古い安定版(プレリリースを除く)のうち最新のものです。
- それ以外のクレートは、同じ semver 系列で age フィルターの条件を満たし、ロックされた版より古い安定版(プレリリースを除く)のうち最新のものです。

どちらの場合も、install 前の `Cargo.lock` にあった同じ semver 系列のバージョンより古くはしません。条件を満たすバージョンがそれより古いものしかなければ、install 前のバージョンに戻します。install で入った変更だけを取り消すためです。戻した install 前のバージョンも age フィルターの条件を満たさない場合は、差し戻した一覧とは別の黄色い行で表示します。
どちらの場合も、install 前の `Cargo.lock` にあった同じ semver 系列の利用可能なバージョンより古くはしません。yank 済み・公開日不明などで crates.io の利用可能な版情報に含まれないバージョンは、この下限にも戻し先にも使いません。その場合は、互換性があり age フィルターを満たす古い版を選び、有効な候補がなければ未解消の違反として報告します。

条件を満たすバージョンが利用可能な下限より古いものしかなければ、install 前のバージョンに戻します。install で入った変更だけを取り消すためです。戻した install 前のバージョンも age フィルターの条件を満たさない場合は、差し戻した一覧とは別の黄色い行で表示します。

```text
. — 1 crate(s) returned to the version locked before the install, which is also newer than --age:
Expand Down
6 changes: 4 additions & 2 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -393,9 +393,11 @@ Only changed entries are audited because depup limits crates.io requests to one
A crate is rolled back to the following version:

- For a direct dependency that depup updated in this run, the version shown in its update line is preferred. That version has already passed the age filter, the OSV check, and `--max-change`.
- For any other crate, the newest stable (non-prerelease) version that satisfies the age filter and is older than the locked one.
- For any other crate, the newest stable (non-prerelease) version from the same semver series that satisfies the age filter and is older than the locked one.

In both cases, a rollback never goes below the version of the same semver series that `Cargo.lock` held before the install. If only older versions satisfy the age filter, the crate goes back to its pre-install version, so depup undoes only the change the install made. When that pre-install version is itself newer than the age filter allows, it is reported on a separate yellow line instead of as rolled back:
In both cases, a rollback never goes below an available pre-install version from the same semver series. Pre-install versions absent from usable crates.io metadata, including yanked versions and versions with unreadable release dates, are excluded from both this minimum and restoration. In that case, depup chooses an older compatible version that satisfies the age filter, or reports an unresolved violation if none exists.

If only versions below an available minimum satisfy the age filter, the crate goes back to that pre-install version, so depup undoes only the change the install made. When that pre-install version is itself newer than the age filter allows, it is reported on a separate yellow line instead of as rolled back:

```text
. — 1 crate(s) returned to the version locked before the install, which is also newer than --age:
Expand Down
30 changes: 23 additions & 7 deletions src/cargo_rollback/audit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -314,9 +314,10 @@ impl AdjustmentLog {
/// judge がこの実行で選んだ版 (`preferred`) のうち、期間を満たし、今の版より古く、
/// 同じ semver 系列にあるものがあれば、その最大を採る。表示した更新先と lock の版が
/// 一致し、judge が OSV や `--max-change` で退けた版を差し戻しで選び直すこともない。
/// 該当が無ければ、期間を満たす版のうち今の版より古い最新 ([`pick_older_within_age`])。
/// 該当が無ければ、同じ semver 系列で期間を満たす版のうち今の版より古い最新
/// ([`pick_older_within_age`])。
///
/// どちらの場合も、install 前の lock (`before`) にあった同じ系列の版 ([`version_floor`])
/// どちらの場合も、install 前の lock (`before`) にあった利用可能な同系列の版 ([`version_floor`])
/// より古くはしない。manifest の版要求が lock より古いまま (`^1.40.0` で lock は 1.50.0)
/// judge が `--max-change` で 1.40.x を選んだ場合や、install 前から期間内の版が入っていた
/// 場合に、install 前より古い版まで下げると、それまで使えていた API が消えてビルドが壊れる。
Expand All @@ -329,7 +330,7 @@ pub(crate) fn rollback_target(
before: Option<&[String]>,
exemptions: &crate::update::AgeExemptions,
) -> Option<String> {
let floor = version_floor(before, current);
let floor = version_floor(available, before, current);
let at_least_floor = |version: &str| {
floor
.as_deref()
Expand All @@ -352,20 +353,31 @@ pub(crate) fn rollback_target(
return Some(version.clone());
}
match pick_older_within_age(available, current, cutoff, exemptions) {
Some(version) if at_least_floor(&version) => Some(version),
Some(version) if same_series(&version, current) && at_least_floor(&version) => {
Some(version)
}
// 期間を満たす版が下限より古い (または無い) なら、install 前の版に戻す
_ => floor,
}
}

/// install 前の lock (`before`) にあった、`current` と同じ semver 系列で `current` より古い版の
/// うち最新。差し戻しでこれより古くしない下限
fn version_floor(before: Option<&[String]>, current: &str) -> Option<String> {
/// うち利用可能な最新。yank 済み・公開日不明などで取得一覧にない版は、下限にも戻し先にも使わない。
fn version_floor(
available: &[VersionInfo],
before: Option<&[String]>,
current: &str,
) -> Option<String> {
before
.unwrap_or_default()
.iter()
.filter(|version| same_series(version, current))
.filter(|version| compare_versions(version, current) == std::cmp::Ordering::Less)
.filter(|version| {
available
.iter()
.any(|info| compare_versions(&info.version, version) == std::cmp::Ordering::Equal)
})
.max_by(|a, b| compare_versions(a, b))
.cloned()
}
Expand Down Expand Up @@ -714,7 +726,11 @@ impl<'a> LockAgeAuditor<'a> {
name: name.clone(),
current: current.clone(),
target: target.clone(),
minimum: version_floor(baseline.get(name).map(Vec::as_slice), current),
minimum: version_floor(
&all_versions,
baseline.get(name).map(Vec::as_slice),
current,
),
};

if verify_only {
Expand Down
Loading
Loading