Skip to content

fix(flutter): pin SDK package deps to the Flutter workspace lock - #10

Merged
SpencerC merged 1 commit into
mainfrom
strange-hopper-75d832
Aug 28, 2026
Merged

fix(flutter): pin SDK package deps to the Flutter workspace lock#10
SpencerC merged 1 commit into
mainfrom
strange-hopper-75d832

Conversation

@SpencerC

Copy link
Copy Markdown
Owner

What & why

Cold CI builds of a flutter_library that runs build_runner started failing in the FlutterPrepareDeps action with:

Unhandled exception:
Bad state: Dependency process of BuildPackage(
  name: fuchsia_remote_debug_protocol,
  path: .../flutter/packages/fuchsia_remote_debug_protocol,
  dependencies: {meta, process, vm_service},
) not present, please run `dart pub get` or `flutter pub get` to fetch dependencies.
  #0  new BuildPackages.compute (package:build_runner/src/build_plan/build_packages.dart:98:11)

Root cause. Flutter >= 3.35 resolves its SDK packages as a pub workspace: each package pubspec carries any constraints (process: any) and one checked-in lockfile at the SDK root, flutter/pubspec.lock, pins the whole monorepo. _sanitize_published_pubspec strips the resolution: workspace marker and _ensure_pub_deps then solves each package standalone and unpinned, so the vendored .pub_cache took 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 own pub_deps.json, PACKAGE_CONFIG_FROM_PUB_DEPS_PY silently skipped the package (an os.path.isdir miss) 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 vendored process-5.0.6 while consumers pinned 5.0.5 — which is also what flutter/pubspec.lock pins. 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. aquery confirms FlutterPrepareDeps declares the assembled lib_pub_cache as an input and FlutterAssemblePubCache takes every SDK package's *_pub_cache tree.

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 the sdks: block would make pub discard the lockfile for a package whose environment: 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.json pins, and notes packages nothing supplies at all. The reconciliation lives in one shared constant so the two near-identical package_config writers cannot diverge.

Verification

  • Reproduced the bug: re-running the exact fetch-time solve against Flutter 3.38.4 vendors process-5.0.6, meta-1.19.0, vm_service-15.3.0, while flutter/pubspec.lock pins 5.0.5 / 1.17.0 / 15.0.2.
  • Live cold 3.38.4 fetch with this change: vendors 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 is json_annotation inside flutter_tools, which no consumer depends on.
  • Linux (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.5 is not read as a version of path, 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/smoke to cover the SDK-package graph (integration_testflutter_driverfuchsia_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: adding integration_test and regenerating codegen_app/pub_deps.json bumps 11 unrelated packages, six of which conflict with flutter_app's pins and would fail the pub extension, forcing a broad fixture regeneration unrelated to this bug. Happy to do that as a separate change.

The MODULE.bazel.lock updates are the bzlTransitiveDigest refresh for the flutter/pub extensions; the e2e/smoke one also picks up integrity_arm64 from 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_tests passes (24/24)
  • cd e2e/smoke && bazel test //:integration_tests passes (21/21, full re-execution)
  • Ran bazel run //docs:update if any rule/macro API changed — no public rule/macro API changed
  • pre-commit run --all-files (buildifier + prettier) is clean
  • Updated docs/README for user-facing changes — CHANGELOG entry added; no user-facing API change

🤖 Generated with Claude Code

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>
@SpencerC
SpencerC merged commit c93fd7d into main Aug 28, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant