fix(flutter): pin SDK package deps to the Flutter workspace lock - #10
Merged
Conversation
Flutter >= 3.35 resolves its SDK packages as a pub workspace: each package pubspec carries `any` constraints and one checked-in lockfile at the SDK root (`flutter/pubspec.lock`) pins the whole monorepo. The SDK repository rule strips the `resolution: workspace` marker and then solves every package standalone, so those `any` constraints resolved against pub.dev live -- the vendored `.pub_cache` took whatever was latest on the day of the fetch. Nothing else supplies those packages: SDK BUILD files are generated with `include_hosted_deps = False`, so the pinned `@pub_*` repository the pub extension creates for them is unreachable from the Bazel graph. When the vendored version drifted away from what a consumer pinned in its own pub_deps.json, `PACKAGE_CONFIG_FROM_PUB_DEPS_PY` silently skipped the package (`os.path.isdir` miss) and the build died much later inside build_runner's bootstrap with an unexplained `Bad state: Dependency <name> of BuildPackage(...) not present`, naming a package nobody had written down. Observed with `process`: pub.dev published 5.0.6, so every cold fetch vendored `process-5.0.6` while consumers pinned 5.0.5 (the version `flutter/pubspec.lock` also pins). Only cold builds were affected -- a warm repository or action cache still carried 5.0.5. Seed each SDK package's solve with the workspace lock, filtered to its hosted entries; the git and `sdk:` entries name monorepo-internal packages a standalone solve never resolves, and the `sdks:` block would make pub discard the lockfile for a package whose `environment:` is narrower. A cold 3.38.4 fetch now vendors 230 of 231 package-versions exactly as Flutter pinned them. Releases before the pub-workspace migration pin exact versions in every package pubspec and ship no workspace lock, so they are untouched. Also stop dropping packages silently: preparing a library now fails, naming the package and both versions, when the assembled cache holds a version other than the one pub_deps.json pins, and notes packages nothing supplies. The reconciliation is one shared constant so the two near-identical package_config writers cannot diverge. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Cold CI builds of a
flutter_librarythat runsbuild_runnerstarted failing in theFlutterPrepareDepsaction with:Root cause. Flutter >= 3.35 resolves its SDK packages as a pub workspace: each package pubspec carries
anyconstraints (process: any) and one checked-in lockfile at the SDK root,flutter/pubspec.lock, pins the whole monorepo._sanitize_published_pubspecstrips theresolution: workspacemarker and_ensure_pub_depsthen solves each package standalone and unpinned, so the vendored.pub_cachetook whatever pub.dev served as latest on the day of the fetch.Nothing else supplies those packages: SDK BUILD files are generated with
include_hosted_deps = False, so the pinned@pub_<name>repository the pub extension creates is unreachable from the Bazel graph. When the vendored version drifted away from the version a consumer pinned in its ownpub_deps.json,PACKAGE_CONFIG_FROM_PUB_DEPS_PYsilently skipped the package (anos.path.isdirmiss) and the build died much later inside build_runner's bootstrap, naming a package nobody had written down.Concretely: pub.dev published
process 5.0.6, so every cold fetch vendoredprocess-5.0.6while consumers pinned5.0.5— which is also whatflutter/pubspec.lockpins. Only cold builds were affected; a warm repository or action cache still carried 5.0.5, which is why it looked platform-specific.This is not a missing input edge.
aqueryconfirmsFlutterPrepareDepsdeclares the assembledlib_pub_cacheas an input andFlutterAssemblePubCachetakes every SDK package's*_pub_cachetree.Fix. Seed each SDK package's solve with the SDK's own workspace lock, filtered to its hosted entries. The git and
sdk:entries name monorepo-internal packages (dev/integration_tests/...) that a standalone solve never resolves, and thesdks:block would make pub discard the lockfile for a package whoseenvironment:is narrower. Releases before the pub-workspace migration pin exact versions in every package pubspec and ship no workspace lock, so they are untouched.Second, stop dropping packages silently. Preparing a library now fails — naming the package and both versions — when the assembled cache holds a version other than the one
pub_deps.jsonpins, and notes packages nothing supplies at all. The reconciliation lives in one shared constant so the two near-identicalpackage_configwriters cannot diverge.Verification
process-5.0.6,meta-1.19.0,vm_service-15.3.0, whileflutter/pubspec.lockpins 5.0.5 / 1.17.0 / 15.0.2.process-5.0.5,meta-1.17.0,vm_service-15.0.2. 230 of 231 vendored package-versions now match the workspace lock exactly; the one exception isjson_annotationinsideflutter_tools, which no consumer depends on.linux/amd64, Dart 3.13.2): unpinned solve →process-5.0.6; seeded solve →process-5.0.5.Tests
flutter/tests/sdk_lock_test.bzl— three unit tests for the lock filter: hosted entries survive intact (sha256 and url included, or pub re-resolves), git/sdk:/sdks:are dropped, and a lock with nothing hosted yields no seed.flutter/tests/package_config_test.bzl— five tests that execute the reconciliation for real against a fixture cache: version drift fails with the offending versions, an unsupplied package stays a note,path_provider-2.1.5is not read as a version ofpath, an SDK-sourced package is not matched against a same-named hosted directory, and a healthy cache stays silent.Notes for reviewers
I did not extend
e2e/smoketo cover the SDK-package graph (integration_test→flutter_driver→fuchsia_remote_debug_protocol). That workspace pins Flutter 3.24.0, which predates pub workspaces and pins exactly per pubspec, so the drift provably cannot occur there. I also trial-ran it: addingintegration_testand regeneratingcodegen_app/pub_deps.jsonbumps 11 unrelated packages, six of which conflict withflutter_app's pins and wouldfailthe pub extension, forcing a broad fixture regeneration unrelated to this bug. Happy to do that as a separate change.The
MODULE.bazel.lockupdates are thebzlTransitiveDigestrefresh for the flutter/pub extensions; thee2e/smokeone also picks upintegrity_arm64from the previous commit.Downstream consumers need to repoint at a release containing this commit for the SDK repositories to refetch.
Checklist
bazel test //flutter/tests:all_tests //docs:update_testspasses (24/24)cd e2e/smoke && bazel test //:integration_testspasses (21/21, full re-execution)bazel run //docs:updateif any rule/macro API changed — no public rule/macro API changedpre-commit run --all-files(buildifier + prettier) is clean🤖 Generated with Claude Code