From b31ffc5261e60dfc0e81a5e1cd2d57ed3e4d89ef Mon Sep 17 00:00:00 2001 From: Kamil Bukum Date: Mon, 31 Aug 2026 16:23:32 -0500 Subject: [PATCH] Implement lockstep update for shared dependencies in Cargo - Enhance lockfile updater to force precise updates for dependencies requiring coordinated version changes. - Add tests to validate behavior for shared dependency families in various workspace scenarios. - Introduce necessary fixture files for testing lockstep updates across multiple crates. --- .../cargo/file_updater/lockfile_updater.rb | 106 ++++- .../file_updater/lockfile_updater_spec.rb | 426 ++++++++++++++++++ .../spec/fixtures/lockfiles/futures_lockstep | 144 ++++++ .../lockfiles/futures_lockstep_workspace | 190 ++++++++ .../spec/fixtures/manifests/futures_lockstep | 7 + .../futures_lockstep_workspace_consumer | 7 + .../futures_lockstep_workspace_locker | 7 + .../manifests/futures_lockstep_workspace_root | 6 + 8 files changed, 892 insertions(+), 1 deletion(-) create mode 100644 cargo/spec/fixtures/lockfiles/futures_lockstep create mode 100644 cargo/spec/fixtures/lockfiles/futures_lockstep_workspace create mode 100644 cargo/spec/fixtures/manifests/futures_lockstep create mode 100644 cargo/spec/fixtures/manifests/futures_lockstep_workspace_consumer create mode 100644 cargo/spec/fixtures/manifests/futures_lockstep_workspace_locker create mode 100644 cargo/spec/fixtures/manifests/futures_lockstep_workspace_root diff --git a/cargo/lib/dependabot/cargo/file_updater/lockfile_updater.rb b/cargo/lib/dependabot/cargo/file_updater/lockfile_updater.rb index 92a2fe2e669..b79b10d5bee 100644 --- a/cargo/lib/dependabot/cargo/file_updater/lockfile_updater.rb +++ b/cargo/lib/dependabot/cargo/file_updater/lockfile_updater.rb @@ -13,7 +13,7 @@ module Dependabot module Cargo class FileUpdater - # rubocop:disable-next Metrics/ClassLength + # rubocop:disable Metrics/ClassLength class LockfileUpdater extend T::Sig @@ -86,14 +86,55 @@ def run_updates @current_dependency = dependency_to_update next if previous_line_already_replaced? + lockfile_before = File.read("Cargo.lock") run_cargo_command( "cargo update -p #{dependency_spec}", fingerprint: "cargo update -p " ) + + force_precise_update if precise_update_needed?(lockfile_before) end end end + sig { void } + def force_precise_update + run_cargo_command( + "cargo update -p #{dependency_spec} --precise #{dependency.version}", + fingerprint: "cargo update -p --precise " + ) + end + + # `cargo update -p name:version` won't move a package whose bump + # requires lockstep updates to sibling crates shared with other members + # (e.g. the `futures` family: 0.3.34 needs `futures-*` at `^0.3.34`). + # Cargo leaves the line unchanged and a later validation fails, so we + # retry with `--precise` to force the exact target and cascade the + # siblings. + # + # Only retry on a genuine no-op. Comparing the dependency's entries + # *and* its incoming edges before/after the plain command distinguishes + # a stuck line from one Cargo did resolve — including an edge repointed + # onto an already-present target entry, where forcing `--precise` would + # wrongly fail. + sig { params(lockfile_before: String).returns(T::Boolean) } + def precise_update_needed?(lockfile_before) + return false if git_dependency? + + version = dependency.version + return false unless version && version_class.correct?(version) + + previous_version = dependency.previous_version + return false unless previous_version && version_class.correct?(previous_version) + return false if previous_version == version + + lockfile_after = File.read("Cargo.lock") + return false if dependency_move_signature(lockfile_before, dependency) != + dependency_move_signature(lockfile_after, dependency) + + package_version_count(lockfile_after, dependency, previous_version).positive? + end + # An earlier command in this run may already have resolved this # dependency's line, in which case `cargo update -p name:version` # would match no package and fail hard. Skip the command when the @@ -710,6 +751,68 @@ def dependency_lockfile_entries(lockfile_content, dependency) entries end + # A "did this dependency move?" signature: the dependency's own package + # identity plus its incoming edges. An edge repointed onto an + # already-present target entry moves the dependency without changing any + # `[[package]]` block, so identity alone is not a reliable signal. + sig do + params(lockfile_content: String, dependency: Dependabot::Dependency) + .returns(T::Array[String]) + end + def dependency_move_signature(lockfile_content, dependency) + dependency_identity_signature(lockfile_content, dependency) + + dependency_reference_edges(lockfile_content, dependency) + end + + # Identity of each of the dependency's own `[[package]]` blocks, reduced + # to the fields that define which crate instance is present: `name`, + # `version` and `source`. + # + # This deliberately excludes the block's outgoing `dependencies` array + # (and checksum): during a grouped update Cargo may add or drop version + # qualifiers on this crate's own outgoing edges when a sibling starts or + # stops coexisting, even though this crate itself did not move. Folding + # the whole block in would misread that as movement and wrongly suppress + # the `--precise` fallback, leaving the requested version stuck. + sig do + params(lockfile_content: String, dependency: Dependabot::Dependency) + .returns(T::Array[String]) + end + def dependency_identity_signature(lockfile_content, dependency) + dependency_lockfile_entries(lockfile_content, dependency).map do |entry| + entry.lines.filter_map do |line| + stripped = line.strip + stripped if stripped.match?(/\A(?:name|version|source) = /) + end.join("\n") + end.sort + end + + # Incoming edges to the dependency (`"futures"` or `"futures 0.3.33"` + # inside other packages' `dependencies` arrays), each qualified by the + # parent package that owns it. Cargo only appends the version when + # several versions of the crate coexist, so a repointed edge is visible + # here even when both entries stay in place. Qualifying by parent means a + # pair of edges swapping targets between two parents (`foo 1` -> `foo 2` + # in one, `foo 2` -> `foo 1` in another) is still detected as movement + # rather than cancelling out in a globally-sorted list. + sig do + params(lockfile_content: String, dependency: Dependabot::Dependency) + .returns(T::Array[String]) + end + def dependency_reference_edges(lockfile_content, dependency) + edge_regex = /\A"#{Regexp.escape(dependency.name)}( [^"]+)?",?\z/ + edges = T.let([], T::Array[String]) + lockfile_content.scan(LOCKFILE_ENTRY_REGEX) do + block = Regexp.last_match.to_s + parent_id = [block[/^name = "[^"]+"$/], block[/^version = "[^"]+"$/]].compact.join(" ") + block.lines.each do |line| + stripped = line.strip + edges << "#{parent_id} => #{stripped}" if stripped.match?(edge_regex) + end + end + edges.sort + end + # A git dependency can legitimately resolve to a different commit than # the checker expected (e.g. the tracked branch advanced between the # check and this update). Accept the update when the dependency's git @@ -794,6 +897,7 @@ def package_version_count(lockfile_content, dependency, version) end end end + # rubocop:enable Metrics/ClassLength end end end diff --git a/cargo/spec/dependabot/cargo/file_updater/lockfile_updater_spec.rb b/cargo/spec/dependabot/cargo/file_updater/lockfile_updater_spec.rb index 13879ae8ce2..bd2dc6f4ad7 100644 --- a/cargo/spec/dependabot/cargo/file_updater/lockfile_updater_spec.rb +++ b/cargo/spec/dependabot/cargo/file_updater/lockfile_updater_spec.rb @@ -370,6 +370,432 @@ end end + context "when a shared dependency family requires a lockstep update" do + let(:manifest_fixture_name) { "futures_lockstep" } + let(:lockfile_fixture_name) { "futures_lockstep" } + let(:dependency_name) { "futures" } + let(:dependency_version) { "0.3.34" } + let(:dependency_previous_version) { "0.3.33" } + let(:requirements) do + [{ file: "Cargo.toml", requirement: "0.3", groups: [], source: nil }] + end + let(:previous_requirements) { requirements } + let(:futures_family) do + %w(futures futures-channel futures-core futures-executor futures-io + futures-macro futures-sink futures-task futures-util) + end + let(:commands) { [] } + + # Simulate Cargo's real behaviour for this workspace shape: a plain + # `cargo update -p futures:0.3.33` cannot bump the shared `futures-*` + # siblings and leaves the line unchanged, while the `--precise` retry + # forces the coordinated 0.3.34 update across the whole family. + before do + allow(updater).to receive(:run_cargo_command) do |command, **| + commands << command + next unless command.include?("--precise") + + content = File.read("Cargo.lock") + futures_family.each do |crate| + content = content.sub( + %(name = "#{crate}"\nversion = "0.3.33"), + %(name = "#{crate}"\nversion = "0.3.34") + ) + end + File.write("Cargo.lock", content) + end + end + + it "retries with --precise to force the coordinated update" do + expect(updated_lockfile_content).to include(%(name = "futures"\nversion = "0.3.34")) + expect(updated_lockfile_content).to include(%(name = "futures-util"\nversion = "0.3.34")) + expect(commands).to eq( + [ + "cargo update -p futures:0.3.33", + "cargo update -p futures:0.3.33 --precise 0.3.34" + ] + ) + end + + context "when the plain update already moves the dependency" do + before do + allow(updater).to receive(:run_cargo_command) do |command, **| + commands << command + + content = File.read("Cargo.lock") + futures_family.each do |crate| + content = content.sub( + %(name = "#{crate}"\nversion = "0.3.33"), + %(name = "#{crate}"\nversion = "0.3.34") + ) + end + File.write("Cargo.lock", content) + end + end + + it "does not issue a redundant --precise retry" do + expect(updated_lockfile_content).to include(%(name = "futures"\nversion = "0.3.34")) + expect(commands).to eq(["cargo update -p futures:0.3.33"]) + end + end + + context "when Cargo resolves to a different valid version" do + before do + allow(updater).to receive(:run_cargo_command) do |command, **| + commands << command + + content = File.read("Cargo.lock") + futures_family.each do |crate| + content = content.sub( + %(name = "#{crate}"\nversion = "0.3.33"), + %(name = "#{crate}"\nversion = "0.3.35") + ) + end + File.write("Cargo.lock", content) + end + end + + it "keeps Cargo's choice without forcing --precise" do + expect(updated_lockfile_content).to include(%(name = "futures"\nversion = "0.3.35")) + expect(commands).to eq(["cargo update -p futures:0.3.33"]) + end + end + + context "when a newer entry already coexists and the plain update is a no-op" do + # The lockfile already contains an unrelated `futures` 0.3.35 entry + # (pulled by another dependent) alongside the stuck 0.3.33 line. The + # plain update is a no-op for our edge, so the fallback must still + # fire: the decision depends on whether *this* command moved the + # dependency, not on whether some higher version merely exists. + let(:lockfile_body) do + fixture("lockfiles", lockfile_fixture_name) + <<~COEXISTING + [[package]] + name = "futures" + version = "0.3.35" + source = "registry+https://github.com/rust-lang/crates.io-index" + checksum = "0000000000000000000000000000000000000000000000000000000000000000" + COEXISTING + end + + it "still forces --precise for the stuck previous-version line" do + expect(updated_lockfile_content).to include(%(name = "futures"\nversion = "0.3.34")) + expect(commands).to eq( + [ + "cargo update -p futures:0.3.33", + "cargo update -p futures:0.3.33 --precise 0.3.34" + ] + ) + end + end + end + + context "when a shared dependency family in a real workspace needs a lockstep update" do + # Unstubbed, end-to-end regression for issue #16092. This mirrors the + # reporter's workspace: `futures = "0.3"` lives in + # `[workspace.dependencies]`, one member consumes it via + # `futures.workspace = true`, and a second member pulls `tower` (which + # depends directly on `futures-util`). That direct dependent pins the + # shared `futures-*` siblings, so a plain `cargo update -p futures:0.3.33` + # is a genuine no-op (Cargo will not bump the shared siblings), while the + # `--precise` retry forces the coordinated 0.3.34 update. The lockfile is + # a real all-0.3.33 family with genuine checksums, so this exercises the + # actual Cargo resolution rather than a stubbed substitution. + let(:manifest_fixture_name) { "futures_lockstep_workspace_root" } + let(:lockfile_fixture_name) { "futures_lockstep_workspace" } + let(:consumer_manifest) do + Dependabot::DependencyFile.new( + name: "consumer/Cargo.toml", + content: fixture("manifests", "futures_lockstep_workspace_consumer") + ) + end + let(:locker_manifest) do + Dependabot::DependencyFile.new( + name: "locker/Cargo.toml", + content: fixture("manifests", "futures_lockstep_workspace_locker") + ) + end + let(:dependency_files) { [manifest, lockfile, consumer_manifest, locker_manifest] } + let(:dependency_name) { "futures" } + let(:dependency_version) { "0.3.34" } + let(:dependency_previous_version) { "0.3.33" } + let(:requirements) do + [{ file: "Cargo.toml", requirement: "0.3", groups: ["workspace.dependencies"], source: nil }] + end + let(:previous_requirements) { requirements } + + it "forces --precise so the whole futures family moves to 0.3.34" do + # `futures` itself is pinned exactly by `--precise`, so assert its + # exact version and checksum. The siblings are pulled via `^0.3.34` + # requirements and may legitimately resolve to a newer 0.3.x from the + # live registry, so only assert that none of them are left at 0.3.33. + expect(updated_lockfile_content).to include(%(name = "futures"\nversion = "0.3.34")) + %w(futures futures-channel futures-core futures-executor futures-io + futures-macro futures-sink futures-task futures-util).each do |crate| + expect(updated_lockfile_content).not_to include(%(name = "#{crate}"\nversion = "0.3.33")) + end + # 0.3.34 `futures` checksum, proving a real crates.io resolve rather + # than a stubbed version substitution. + expect(updated_lockfile_content).to include( + "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" + ) + end + end + + context "when the target already coexists and the plain update repoints an edge" do + # The lockfile already carries both `previous_version` (kept by a + # legitimate transitive consumer that pins the old major) and the + # target version (pulled by another consumer). The plain update simply + # repoints our consumer's edge onto the already-present target entry: + # the `[[package]]` entries are byte-identical before and after, but the + # incoming edge moved. The fallback must NOT fire here — forcing + # `cargo update -p foo:1.0.0 --precise 2.0.0` would fail because the + # retained old major cannot move. + let(:manifest_body) do + <<~TOML + [package] + name = "app" + version = "0.1.0" + + [dependencies] + foo = "2.0.0" + TOML + end + let(:lockfile_body) do + <<~LOCK + # This file is automatically @generated by Cargo. + # It is not intended for manual editing. + version = 4 + + [[package]] + name = "app" + version = "0.1.0" + dependencies = [ + "foo 1.0.0", + ] + + [[package]] + name = "keeper" + version = "0.1.0" + dependencies = [ + "foo 1.0.0", + ] + + [[package]] + name = "other" + version = "0.1.0" + dependencies = [ + "foo 2.0.0", + ] + + [[package]] + name = "foo" + version = "1.0.0" + source = "registry+https://github.com/rust-lang/crates.io-index" + checksum = "1111111111111111111111111111111111111111111111111111111111111111" + + [[package]] + name = "foo" + version = "2.0.0" + source = "registry+https://github.com/rust-lang/crates.io-index" + checksum = "2222222222222222222222222222222222222222222222222222222222222222" + LOCK + end + let(:dependency_name) { "foo" } + let(:dependency_version) { "2.0.0" } + let(:dependency_previous_version) { "1.0.0" } + let(:requirements) do + [{ file: "Cargo.toml", requirement: "2.0.0", groups: ["dependencies"], source: nil }] + end + let(:previous_requirements) { requirements } + let(:commands) { [] } + + before do + allow(updater).to receive(:run_cargo_command) do |command, **| + commands << command + next if command.include?("--precise") + + # Plain update repoints `app`'s edge onto the pre-existing 2.0.0 + # entry; `keeper` keeps `foo 1.0.0`, so both entries survive. + content = File.read("Cargo.lock") + content = content.sub( + %(name = "app"\nversion = "0.1.0"\ndependencies = [\n "foo 1.0.0",\n]), + %(name = "app"\nversion = "0.1.0"\ndependencies = [\n "foo 2.0.0",\n]) + ) + File.write("Cargo.lock", content) + end + end + + it "respects the repointed edge and does not force a doomed --precise" do + expect(updated_lockfile_content) + .to include(%(name = "app"\nversion = "0.1.0"\ndependencies = [\n "foo 2.0.0",)) + expect(commands).to eq(["cargo update -p foo:1.0.0"]) + end + end + + context "when the plain update only re-renders the dependency's outgoing edges" do + # During a grouped resolution Cargo can add a version qualifier to this + # crate's *own* outgoing sibling edge (`"futures-util"` -> + # `"futures-util 0.3.33"`) because another version of the sibling starts + # coexisting, all while leaving `futures` itself stuck at 0.3.33. The + # crate did not move, so the fallback must still fire: the move signature + # must ignore the outgoing `dependencies` array and compare identity + # only. + let(:manifest_body) do + <<~TOML + [package] + name = "app" + version = "0.1.0" + + [dependencies] + futures = "0.3" + TOML + end + let(:lockfile_body) do + <<~LOCK + # This file is automatically @generated by Cargo. + # It is not intended for manual editing. + version = 4 + + [[package]] + name = "app" + version = "0.1.0" + dependencies = [ + "futures", + ] + + [[package]] + name = "futures" + version = "0.3.33" + source = "registry+https://github.com/rust-lang/crates.io-index" + checksum = "1111111111111111111111111111111111111111111111111111111111111111" + dependencies = [ + "futures-util", + ] + + [[package]] + name = "futures-util" + version = "0.3.33" + source = "registry+https://github.com/rust-lang/crates.io-index" + checksum = "2222222222222222222222222222222222222222222222222222222222222222" + LOCK + end + let(:dependency_name) { "futures" } + let(:dependency_version) { "0.3.34" } + let(:dependency_previous_version) { "0.3.33" } + let(:requirements) do + [{ file: "Cargo.toml", requirement: "0.3", groups: ["dependencies"], source: nil }] + end + let(:previous_requirements) { requirements } + let(:commands) { [] } + + before do + allow(updater).to receive(:run_cargo_command) do |command, **| + commands << command + content = File.read("Cargo.lock") + + content = + if command.include?("--precise") + content.gsub(%(version = "0.3.33"), %(version = "0.3.34")) + else + # Plain update only qualifies the outgoing sibling edge; `futures` + # stays at 0.3.33. + content.sub(%( "futures-util",), %( "futures-util 0.3.33",)) + end + + File.write("Cargo.lock", content) + end + end + + it "still forces --precise because the crate's identity did not move" do + expect(updated_lockfile_content).to include(%(name = "futures"\nversion = "0.3.34")) + expect(commands).to eq( + [ + "cargo update -p futures:0.3.33", + "cargo update -p futures:0.3.33 --precise 0.3.34" + ] + ) + end + end + + context "when two parents swap the dependency versions between them" do + # One parent moves `foo 1.0.0` -> `foo 2.0.0` while another moves + # `foo 2.0.0` -> `foo 1.0.0`. The globally-sorted set of edge strings is + # unchanged, but an edge genuinely moved, so the fallback must NOT fire. + # This is caught only because each edge is qualified by its parent. + let(:manifest_body) do + <<~TOML + [package] + name = "app" + version = "0.1.0" + + [dependencies] + foo = "2.0.0" + TOML + end + let(:lockfile_body) do + <<~LOCK + # This file is automatically @generated by Cargo. + # It is not intended for manual editing. + version = 4 + + [[package]] + name = "a" + version = "0.1.0" + dependencies = [ + "foo 1.0.0", + ] + + [[package]] + name = "b" + version = "0.1.0" + dependencies = [ + "foo 2.0.0", + ] + + [[package]] + name = "foo" + version = "1.0.0" + source = "registry+https://github.com/rust-lang/crates.io-index" + checksum = "1111111111111111111111111111111111111111111111111111111111111111" + + [[package]] + name = "foo" + version = "2.0.0" + source = "registry+https://github.com/rust-lang/crates.io-index" + checksum = "2222222222222222222222222222222222222222222222222222222222222222" + LOCK + end + let(:dependency_name) { "foo" } + let(:dependency_version) { "2.0.0" } + let(:dependency_previous_version) { "1.0.0" } + let(:requirements) do + [{ file: "Cargo.toml", requirement: "2.0.0", groups: ["dependencies"], source: nil }] + end + let(:previous_requirements) { requirements } + let(:commands) { [] } + + before do + allow(updater).to receive(:run_cargo_command) do |command, **| + commands << command + next if command.include?("--precise") + + content = File.read("Cargo.lock") + content = content + .sub(%(name = "a"\nversion = "0.1.0"\ndependencies = [\n "foo 1.0.0",), + %(name = "a"\nversion = "0.1.0"\ndependencies = [\n "foo 2.0.0",)) + .sub(%(name = "b"\nversion = "0.1.0"\ndependencies = [\n "foo 2.0.0",), + %(name = "b"\nversion = "0.1.0"\ndependencies = [\n "foo 1.0.0",)) + File.write("Cargo.lock", content) + end + end + + it "detects the per-parent edge movement and does not force --precise" do + expect(updated_lockfile_content) + .to include(%(name = "a"\nversion = "0.1.0"\ndependencies = [\n "foo 2.0.0",)) + expect(commands).to eq(["cargo update -p foo:1.0.0"]) + end + end + context "when the previous version also exists from another source" do let(:manifest_fixture_name) { "duplicate_source_versions" } let(:lockfile_fixture_name) { "duplicate_source_versions" } diff --git a/cargo/spec/fixtures/lockfiles/futures_lockstep b/cargo/spec/fixtures/lockfiles/futures_lockstep new file mode 100644 index 00000000000..80144e29dc3 --- /dev/null +++ b/cargo/spec/fixtures/lockfiles/futures_lockstep @@ -0,0 +1,144 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "dependabot" +version = "0.1.0" +dependencies = [ + "futures", +] + +[[package]] +name = "futures" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-executor" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f8d4e9d3a2c6b1e8b6de3d3e1eeffce4c37c8b6e5f5b0a4e2e0c8b6b6b6b6b6" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a35a5c5b9b9c8d2e3e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e" + +[[package]] +name = "futures-macro" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c5e6e6e6e6e6e6e6e6e6e6e6e6e6e6e6e6e6e6e6e6e6e6e6e6e6e6e6e6e6e6e6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7" + +[[package]] +name = "futures-task" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-util" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" diff --git a/cargo/spec/fixtures/lockfiles/futures_lockstep_workspace b/cargo/spec/fixtures/lockfiles/futures_lockstep_workspace new file mode 100644 index 00000000000..c2ffa1dcaa1 --- /dev/null +++ b/cargo/spec/fixtures/lockfiles/futures_lockstep_workspace @@ -0,0 +1,190 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "consumer" +version = "0.1.0" +dependencies = [ + "futures", +] + +[[package]] +name = "futures" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-executor" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" + +[[package]] +name = "futures-macro" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" + +[[package]] +name = "futures-task" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-util" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "locker" +version = "0.1.0" +dependencies = [ + "tower", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "syn" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" diff --git a/cargo/spec/fixtures/manifests/futures_lockstep b/cargo/spec/fixtures/manifests/futures_lockstep new file mode 100644 index 00000000000..3bfddfa98b3 --- /dev/null +++ b/cargo/spec/fixtures/manifests/futures_lockstep @@ -0,0 +1,7 @@ +[package] +name = "dependabot" +version = "0.1.0" +authors = ["support@dependabot.com"] + +[dependencies] +futures = "0.3" diff --git a/cargo/spec/fixtures/manifests/futures_lockstep_workspace_consumer b/cargo/spec/fixtures/manifests/futures_lockstep_workspace_consumer new file mode 100644 index 00000000000..9771798d490 --- /dev/null +++ b/cargo/spec/fixtures/manifests/futures_lockstep_workspace_consumer @@ -0,0 +1,7 @@ +[package] +name = "consumer" +version = "0.1.0" +edition = "2021" + +[dependencies] +futures.workspace = true diff --git a/cargo/spec/fixtures/manifests/futures_lockstep_workspace_locker b/cargo/spec/fixtures/manifests/futures_lockstep_workspace_locker new file mode 100644 index 00000000000..bb6ff0e0dd0 --- /dev/null +++ b/cargo/spec/fixtures/manifests/futures_lockstep_workspace_locker @@ -0,0 +1,7 @@ +[package] +name = "locker" +version = "0.1.0" +edition = "2021" + +[dependencies] +tower = { version = "0.5", features = ["util"] } diff --git a/cargo/spec/fixtures/manifests/futures_lockstep_workspace_root b/cargo/spec/fixtures/manifests/futures_lockstep_workspace_root new file mode 100644 index 00000000000..478c6685564 --- /dev/null +++ b/cargo/spec/fixtures/manifests/futures_lockstep_workspace_root @@ -0,0 +1,6 @@ +[workspace] +resolver = "2" +members = ["consumer", "locker"] + +[workspace.dependencies] +futures = "0.3"