swift-algorithms

Activity, over time

Daily observations from the public event archive.

Attention

Daily stars & forks

Loading attention chart

Star accumulation

Cumulative stars

Loading star accumulation chart

Pull requests

Daily opens & merges

Loading pull requests chart

Conversations

Daily issues & comments

Loading conversations chart

Community

Daily accounts & reviews

Loading community chart

Releases

Daily releases

Loading releases chart

Mentions

1post · 30d
Nov 30 posts
  1. Zenn · · @zunda_pixel

    Package.resolved の originHash は何のためにあるのか Renovate が依存を上げた PR をマージしたあと、tuist install を流すと Package.resolved の 1 行だけが書き換わるようになりました。 Package.resolved { - "originHash" : "b51d86e0d60dc9c07d7da15f1bb995c680b75f60f09f0ee586aea8f14852849d", - "originHash" : "9ea100c33d0673a7cbf638a61115d34ce142720e95d1761c390aa31ae6ec796b", "pins" : [ この originHash が何のための値なのかを、SwiftPM のソースを読み、実際に計測して確かめました。分かったことは次の 4 つです。 originHash は「この Package.resolved がどの Package.swift に対して作られたか」の目印で、改ざん検知の値ではない 一致していると、SwiftPM は依存のバージョンを選び直す計算を省ける。差が出るのは checkout が足りないとき(clone 直後や .build をキャッシュしない CI)で、今回の環境(pin 60 個)では約 20 秒だった SwiftPM 自身は、pin が変わらない限り originHash を書き直さない。だから古いまま残りやすい 実際に解決しないまま originHash だけを今の値にするのは危ない。Tuist は一致した hash を信じて推移的な依存の要求を確かめず、要求を満たさない pin をそのまま使う(再現して確かめた) 後半では、依存を自動で上げる Renovate と Dependabot で、この値の扱いがどう違うかも比べます。調べる途中で見立てを 3 回間違えたので、その話も最後に書きます。 ! SwiftPM のコードは 2026 年 10 月時点の main(7137197)、計測は macOS・Xcode 27.2(Swift 6.4.2)で行いました。 originHash はルートの manifest の SHA-256 originHash は、Package.resolved の書式のバージョン 3 で入った値です。SwiftPM は、ルートの Package.swift の tools-version が 5.10 以上のときにこの書式で書き出します。 中身は、SwiftPM の computeResolvedFileOriginHash を読むと分かります。 Sources/Workspace/Workspace+Dependencies.swift private func computeResolvedFileOriginHash(root: PackageGraphRootInput) throws -> String { var content = try root.packages.reduce(into: "") { partial, element in let path = try ManifestLoader.findManifest( packagePath: element, fileSystem: self.fileSystem, currentToolsVersion: self.currentToolsVersion ) try partial.append(self.fileSystem.readFileContents(path)) } content += root.dependencies.reduce(into: "") { partial, element in partial += element.locationString } return content.sha256Checksum } ルートパッケージの manifest のファイル内容をそのままつなげ、ルートに追加の依存があればその URL を足して、SHA-256 をとっています。ふつうのパッケージでは追加の依存は無いので、Package.swift のファイル内容の SHA-256 そのものになります。手元でも確かめられます。 $ grep originHash Package.resolved "originHash" : "9ea100c33d0673a7cbf638a61115d34ce142720e95d1761c390aa31ae6ec796b", $ shasum -a 256 Package.swift 9ea100c33d0673a7cbf638a61115d34ce142720e95d1761c390aa31ae6ec796b Package.swift ここから 3 つのことが言えます。 pin(各依存のバージョンとコミット)は計算に入っていない。Package.resolved の中身が書き換えられても、originHash では気づけない コメントを 1 文字足しただけでも値が変わる Package@swift-6.0.swift のような Swift のバージョンごとの manifest があると、使っている toolchain が選んだファイルの hash になる 一致すると、SwiftPM はバージョンの選び直しを省く originHash を入れた SwiftPM の PR(swiftlang/swift-package-manager#6698、2023 年 8 月)には、狙いがこう書かれています。 when a package.resolved file exists the dependencies resolution should stick to it, unless requested otherwise 実装では、swift package resolve やビルドが使う標準の解決(コード上は .bestEffort)の中で、この hash を比べて分岐しています(抜粋して簡略化しています)。 let currentHash = try self.computeResolvedFileOriginHash(root: root) guard storedHash == currentHash else { // 'Package.resolved' origin hash does do not match manifest dependencies. resolving and updating accordingly return try await resolveAndUpdateResolvedFile(forceResolution: false) } // 'Package.resolved' origin hash matches manifest dependencies, attempting resolution based on this file let (manifests, precomputationResult) = try await self.tryResolveBasedOnResolvedVersionsFile(...) コメントにしたメッセージは、--very-verbose を付けると実際にログに出ます(「does do not」は原文のままです)。 originHash 通る経路 やること 一致 tryResolveBasedOnResolvedVersionsFile pin のコミットで checkout し、pin が今の要求を満たすかを確かめる 不一致 resolveAndUpdateResolvedFile 足りない checkout があれば resolver を走らせる(今の pin を優先して選ぶ)。揃っていれば pin が要求を満たすかを確かめるだけ どちらの経路でも、pin が今の要求を満たすかは確かめられ(precomputeResolution か resolver)、満たさなければ解決し直します。今の pin が要求を満たしていれば、どちらの経路でもその pin がそのまま使われます。違うのは、そこにたどり着くまでの手間です。後で書く計測でも、hash が一致していてもずれていても、最終的な pin は同じでした。 なお、Package.resolved を正として扱う --force-resolved-versions と、swift package update は originHash を読みません。update は値を計算して書き込むだけです。 差が出るのは checkout が足りないとき 手間の違いがどれくらいかを、仕事で開発しているアプリの依存で測りました。Package.swift に直接書いている依存が 25 個、推移的な依存も含めた pin が 60 個あり、すべて git のリポジトリです。 まず、.build に checkout がある状態です。5 回ずつ流しました。 originHash swift package resolve 一致 0.46〜0.51 秒 古い 0.43〜0.51 秒 差はありませんでした。前の節の分岐のとおり、checkout が揃っていれば、hash がずれていても resolver は走らないからです。checkout が揃った手元の開発では、originHash が古くても遅くなりません。 次に、.build を空にした状態です。git のリポジトリは SwiftPM のキャッシュ(--cache-path)に入っているので、clone の時間はほぼ含みません。順番を入れ替えて 2 回ずつ流しました。 originHash 1 巡目 2 巡目 ログの「Computing version for」 一致 66 秒 29 秒 0 件 古い 87 秒 53 秒 60 件 一致していると、SwiftPM は pin のコミットでそのまま作業コピーを作ります。古いと resolver が走り、60 パッケージすべてについて、git のタグの一覧と、候補になるバージョンの Package.swift を読みます。ログの「Computing version for」がその作業で、これが 2 回とも約 20 秒の差になりました。clone 直後の手元や、.build をキャッシュしない CI は、毎回この分だけ遅くなります。 Tuist ではもっと差が開きます。Tuist の依存解決(SwifterPM)は、hash が一致すると swift package resolve を起動せず、自前のキャッシュから戻すだけで済ませるからです。.build が空の状態で 1 回ずつ測ると、一致で 7.4 秒、古いと 134 秒でした。 ! ネットワークから clone し直す場合は測っていません。その場合は clone の時間が大きく、差の割合は小さくなると考えています。 計測の手順と条件 コマンドは swift package --package-path <dir> --scratch-path <dir>/.build --cache-path <cache> --disable-scm-to-registry-transformation resolve で、時間は /usr/bin/time -p で測りました。 hash を一致させるときは、Package.resolved の originHash を shasum -a 256 Package.swift の値に書き換えました。古い hash は、Package.swift を変える前の値です。pin はどちらも同じです。 checkout がある状態は、同じ .build で続けて 5 回ずつ流しました。 checkout が無い状態は、毎回新しいディレクトリを作り、--scratch-path を空にしました。--cache-path は共通なので、git のリポジトリはキャッシュから取ります。1 巡目は一致→古い、2 巡目は古い→一致の順です。 Tuist の数字は、Tuist の依存解決部分を swifterpm コマンドとしてビルドし、空の --scratch-path で 1 回ずつ測りました。 回数が少なく、同じ条件でも 29 秒と 66 秒のように大きくぶれています。絶対値はマシンや負荷で変わるので、2 巡とも約 20 秒の差が出た、という点だけを読み取ってください。 SwiftPM は pin が変わらないと originHash を書き直さない これだけ効く値なのに、古いまま残りやすい理由は、SwiftPM の保存処理にあります。saveResolvedFile は、保存済みの pin と今の pin を比べて、同じなら何もせずに抜けます(抜粋です。比べている部分は日本語のコメントに置き換えました)。 Sources/Workspace/Workspace+ResolvedPackages.swift // try to load the Package.resolved store from disk so we can compare for any changes // this is needed as we want to avoid re-writing the resolved files unless absolutely necessary var needsUpdate = false if let persistedResolvedPackagesStore = try? self.resolvedPackagesStore.load() { // pin の数と、各 pin の状態を比べる(originHash は比べない) } // exist early is there is nothing to do if !needsUpdate { return } 比べているのは pin だけなので、Package.swift を変えても pin が変わらなければ、originHash は前の値のまま残ります。小さなパッケージで再現できます。 mkdir -p repro/Sources/Repro && cd repro cat > Package.swift <<'EOF' // swift-tools-version: 6.0 import PackageDescription let package = Package( name: "Repro", dependencies: [ .package(url: "apple/swift-algorithms", from: "1.1.0"), ], targets: [ .target(name: "Repro", dependencies: [.product(name: "Algorithms", package: "swift-algorithms")]), ] ) EOF echo 'import Algorithms' > Sources/Repro/Repro.swift swift package resolve grep originHash Package.resolved # shasum -a 256 Package.swift と同じ値 要求だけを変える(今の pin 1.2.1 はどちらでも満たす) sed -i '' 's/from: "1.1.0"/from: "1.2.0"/' Package.swift swift package resolve --very-verbose 2>&1 | grep "origin hash" debug: 'Package.resolved' origin hash does do not match manifest dependencies. resolving and updating accordingly grep originHash Package.resolved # 前の値のまま。何回流しても変わらない swift package update でも、pin が変わらなければ書き直しません。同じ不満は SwiftPM の Issue(#7644「V3 originHash should update more eagerly」、2024 年 6 月)にも出ていて、まだ直っていません。 依存を自動で上げるツールを使っていると、この状態にすぐなります。Renovate の Swift 対応は、Package.swift の要求を上げるときに Package.resolved の該当する pin も書き換えます。ただし originHash には触りません。このことは renovatebot/renovate#41534 に既知の制約として書かれています。マージした後に SwiftPM が解決しても、pin はすでに新しいので、originHash は古いまま残ります。 Dependabot は事情が違います。Dependabot の Swift 対応は、書き換えた Package.swift を作業用のディレクトリに置き、swift package resolve <依存> --version <バージョン> を実際に走らせて、その結果の Package.resolved を commit します(lockfile_updater.rb)。originHash も SwiftPM が計算するので、正しい値になります。以前 Dependabot を使っていたときの commit を見返すと、Package.swift を書き換えた PR でも、originHash は Package.swift の SHA-256 と一致していました。 冒頭の 1 行の diff は、ここに Tuist が絡んで出ていました。Tuist は tuist install で解決するたびに、自分で計算した originHash でファイルを書き直します。SwiftPM と違って値を最新に保つので、Renovate の PR の後に古い値が見つかると、書き直して diff を出すわけです。 解決しないまま originHash だけを直してはいけない この値がただの SHA-256 なら、Renovate が Package.swift を書き換えるついでに計算すればよさそうに見えます。私も最初はそう考えて、Renovate に提案しました(後で取り下げています)。ところが、これは Tuist を使っていると危ない直し方です。 Tuist の依存解決は、hash が一致すると swift package resolve を起動せずに pin をそのまま使います。この速い経路で確かめているのはミラーとローカルパッケージの条件だけで、推移的な依存の要求は確かめません。 一方、Renovate が書き換えるのは、上げた依存の pin だけです。たとえば GoogleSignIn-iOS を 10.0.0 に上げたとします。10.0.0 は AppAuth-iOS に from: "3.0.0" を要求しますが、Package.resolved には AppAuth-iOS 2.1.0 の pin が残ります。実際に私たちのリポジトリでも、この状態の PR がマージされていました。 この状態で originHash だけを今の値にするとどうなるかを、小さな Tuist のプロジェクトで試しました。GoogleSignIn-iOS を from: "9.2.0" で入れた状態から、Renovate と同じように Package.swift の要求と GoogleSignIn-iOS の pin だけを 10.0.0 に書き換えました。そのうえで、Tuist 4.212.0 の tuist install を流しています。 originHash tuist install checkout された AppAuth-iOS GTMAppAuth 古いまま 29 秒、成功 3.0.0 6.0.0 今の値に書き換え 3 秒、成功 2.1.0 5.0.0 古いままだと、Tuist は SwiftPM に解決を任せ、SwiftPM が AppAuth-iOS を 3.0.0 に直しました。今の値にしてあると、Tuist は速い経路で済ませました。checkout されたのは、GoogleSignIn-iOS 10.0.0 の要求(from: "3.0.0")を満たさない AppAuth-iOS 2.1.0 で、エラーも出ませんでした。古い originHash が、ちょうど「確かめ直せ」という合図になっていたわけです。 SwiftPM 自身は、どちらの経路でも pin を確かめます。同じ Package.resolved を素の swift package resolve で解決すると、hash が一致していても(ログは「origin hash matches」)、AppAuth-iOS は 3.0.0 に直りました。 再現の手順 Tuist.swift と、Tuist/Package.swift に .package(url: "google/GoogleSignIn-iOS", from: "9.2.0") だけを書いたプロジェクトで tuist install する。GoogleSignIn-iOS 9.2.0、AppAuth-iOS 2.1.0、GTMAppAuth 5.0.0 が pin される 別のプロジェクトで from: "10.0.0" にして tuist install し、10.0.0 系の依存をキャッシュに入れておく 1 の Package.resolved の GoogleSignIn-iOS の pin だけを、2 のもの(10.0.0)に書き換える。Package.swift は from: "10.0.0" にする originHash を 1 の値のまま残したものと、shasum -a 256 Tuist/Package.swift の値に書き換えたものを用意し、それぞれ空の Tuist/.build で tuist install する 結果の Package.resolved と Tuist/.build/workspace-state.json で、checkout されたバージョンを確かめる 同じ更新を Dependabot がすると、SwiftPM が解決するので AppAuth-iOS も 3.0.0 に上がり、originHash も正しい値になります。pin と originHash を一緒に、実際の解決結果で書き換えるので、この問題は起きません。 ほかにも、自動で上げる PR がそれぞれ originHash を書き換えると、どの PR も Package.resolved の 2 行目を別の値に変えることになります。そのため、1 本マージするたびに残りの PR が conflict します。 originHash を変えてよいのは、その Package.swift で実際に解決して、pin と一緒に書き込むときだけです。 どう付き合うか 使っている道具ごとに、現実的な扱い方が違います。 SwiftPM と Xcode だけを使っている場合は、古い originHash でも結果は変わらず、checkout が無いときに遅くなるだけです。ただ、今の SwiftPM には、pin を変えずに originHash だけを更新するコマンドがありません。Package.resolved を消して作り直せば hash は新しくなりますが、範囲内の新しいバージョンに pin が上がることがあります。 Tuist を使っている場合は、tuist install が書き直した 1 行の diff を、そのまま commit するのがよいと考えています。見た目だけの差分に見えますが、これを入れておくと、clone 直後や CI の install が速い経路に戻ります。 Renovate と Dependabot の違い 依存を自動で上げているなら、どちらのツールかで扱いが変わります。 Renovate Dependabot 方式 SwiftPM を動かさず、pin の文字列を書き換える Linux の上で SwiftPM を実際に動かす originHash 古いまま残る SwiftPM が計算した正しい値になる 推移的な依存の pin 取り残される SwiftPM が揃える Linux でだけ使う依存 入らない 入る PR 同士の conflict originHash の行ではぶつからない Package.swift を変える PR 同士が originHash の行でぶつかる ※ Package.swift を持つパッケージを更新する場合の比較です。Xcode プロジェクトの Package.resolved については後で書きます。 Dependabot の方が Package.resolved を正しく保てますが、代わりに Linux で解決した結果になります。Dependabot は Ubuntu の上の Swift 6.3.1 で解決します(Dockerfile)。Linux で解決するので、Apple のプラットフォームでは使わない依存まで pin に入ることがあります。 私たちのアプリでは OpenCombine がそうでした。依存している combine-schedulers は、Darwin 以外でだけ OpenCombine を使う trait を既定で有効にしています(Package@swift-6.1.swift)。macOS で解決すると OpenCombine は pin に入りませんが、Dependabot の PR には入ってきて、手で消していました。この食い違いを避けるなら、使わない OpenCombine をあえてルートの依存に書いておく手もあります。macOS で解決しても pin に入るので、Linux の結果と揃います(target を持たない manifest で確かめました)。 Dependabot の Swift も、手元の Xcode より古くなりがちです。依存に Package@swift-6.4.swift のような新しい Swift 向けの manifest が増えると、Linux と手元で読むファイルが変わり、結果がずれる可能性があります。 なお、Package.swift を持たない Xcode プロジェクトの Package.resolved を書き換える経路(xcode_lockfile_updater.rb)は、SwiftPM を動かさず、originHash を扱うコードもありません。こちらは Renovate と同じく、pin だけを書き換えます。 どちらのツールでも、いちばんきれいなのは、その PR の中で macOS で実際に解決して、結果を commit することです。たとえば Renovate のブランチで macOS の CI に tuist install を流させ、書き換わった Package.resolved を bot が PR に commit・push します。こうすると originHash だけでなく、取り残された推移的な依存の pin も一緒に直り、Linux でだけ入る依存も混ざりません。ただし swift package resolve は pin が変わらなければ originHash を書き直さないので、Tuist を使わない場合は、この方法でも hash が古いまま残ることがあります。 SwiftPM 側の修正 根本を直すため、SwiftPM に PR(swiftlang/swift-package-manager#10718)を出しました。pin が要求を満たすと確かめた後で、originHash がずれていたら hash だけを書き直す変更です。入れば Xcode でも originHash が最新に保たれるようになります。ただし代わりに、Package.swift を変えた後に 1 行の diff が出るようになり、Package.swift を触ったブランチ同士はこの行でぶつかりやすくなります。checkout が無いときの解決の速さと conflict のどちらを取るか、というトレードオフです。 見立てを 3 回間違えた話 最初は「Tuist が余計な diff を出している」と思いました。調べると、Tuist の値は SwiftPM の定義どおりの SHA-256 で、古い値を残していたのは Renovate の側でした。 次に「Renovate が originHash を計算し直せばいい」と考えました。これは前の節のとおり、Tuist では壊れた pin を通してしまうので取り下げました。 最後に「SwiftPM と Xcode では古い originHash でも遅くならない。書き直して diff を出す Tuist の方を SwiftPM に合わせればいい」と考えて、Tuist に PR(tuist/tuist#14475)を出しました。根拠は checkout がある状態の計測だけでした。checkout が無い状態を測ってみると、Tuist の書き直しこそが速さを保っていたと分かり、PR は閉じました。 近道が効くのは、ふだんの手元ではなく clone 直後や CI です。性能を確かめるときは、効いてほしい状態で測るべきだった、というのがいちばんの反省です。 originHash は「近道を使ってよいか」の目印 originHash は、Package.resolved が今の Package.swift 用かどうかを SwiftPM に伝える目印です。値が正しければ、clone 直後や CI で依存のバージョンの選び直しを省けます。ただ、SwiftPM 自身は pin が変わるまで更新しないので、古くなりがちです(#7644)。 古い値に気づいたら、実際に解決した結果と一緒に commit する。解決しないまま値だけを書き換えない。この 2 つを守れば、diff に紛れ込むこの 1 行が、クリーンな環境での解決を速くしてくれます。依存を自動で上げるなら、Dependabot は実際に解決して書き換えるので前者に当たります。ただし Linux で解決した結果になる点には気をつけてください。

Latest stargazers

The last 10 distinct accounts we observed this year.

Most active participants

PR, issue, comment & review events in the past year.

Packages

No packages found.

README badges

Embed a badge in your README or website.

  • apple/swift-algorithms reached #1 in daily stars on gitwal · Oct 8, 2020
Live mentions · last 30 days
apple/swift-algorithms mentions in the last 30 days