Skip to content

[CI-only] Validate 3.44.2-rc0 rebase — DO NOT MERGE#159

Closed
bdero wants to merge 82 commits into
ci-base/3.44.2from
flutter_release_ex/3.44.2-rc0
Closed

[CI-only] Validate 3.44.2-rc0 rebase — DO NOT MERGE#159
bdero wants to merge 82 commits into
ci-base/3.44.2from
flutter_release_ex/3.44.2-rc0

Conversation

@bdero

@bdero bdero commented Jun 12, 2026

Copy link
Copy Markdown
Member

Runs shorebird_ci on the rebased shorebird/dev tip (45755df) for the Flutter 3.44.2 release.

Force-push to the protected default branch shorebird/dev is blocked, so this PR exists only to exercise CI on the exact rebased tip. Base ci-base/3.44.2 is the 3.44.2 tag (an ancestor of the head), so the PR merge ref equals the rebased tip — CI tests it cleanly.

Do not merge. shorebird/dev will be hard-reset to temp/shorebird-dev-3.44.2 separately. This PR + ci-base/3.44.2 are throwaway.

eseidel and others added 30 commits June 11, 2026 21:58
)

* fix: stop using globals for patch data

* chore: run et format

* chore: add missing files

* test: add unittest

* chore: run et format

* chore: move elf_cache down into runtime

* chore: rename elf* to patch*

* chore: clean up logs

* chore: clean up comments

* chore: use Shorebird dart

* chore: small cleanup
…ASE_URL (#97)

* fix: make dart/flutter work without FLUTTER_STORAGE_BASE_URL

* feat: shorebird flutter should work without setting FLUTTER_STORAGE_BASE_URL

* fix: flutter_tools test fixes

* fix: enable running flutter_tools tests

* chore: remove unnecessary workflow
* chore: move build_engine scripts into this repo

* chore: fix path of content_aware_hash.sh
* chore: roll dart to 6a78a2deaee05bc74775fcfa2ff27aa53c96efca

* wip

* chore: run et format

* chore: attempt to clean up shorebird.cc

* chore: fix build

* chore: remove FLUTTER_STORAGE_BASE_URL override
* feat: allow patch_verification_mode

* test: update tests

* chore: rename to patch_verification
* es/report_start_fix

* fix: second callsite
* chore: add a C++ interface onto the updater

* chore: centralize SHOREBIRD_PLATFORM_SUPPORTED

* test: fix tests
Previously we stopped reporting start on android by accident.
This fixes that.  I also removed the once-per-process guard since
it's not necessary.  This should be correctly reporting
once-per-shell and let the rust code only handle the first of the
calls.

Fixes shorebirdtech/shorebird#3488
As part of our previous fix for FlutterEngineGroup, we introduced
a new bug whereby report_launch_start could be called more than once
in a multi-engine scenerio.  That would cause confusion about what
the current boot patch is, since the current patch is updated as
part of report_launch_start.  report_launch_start should only be
called once per processs, which this change fixes.

We still need more end-to-end testing at this layer to prevent bugs
like this from sneaking in.
…#108)

- Create a template Flutter project once in setUpAll and copy it per
  test, avoiding repeated `flutter create` calls
- Run a warm-up `flutter build apk` in setUpAll (outside per-test
  timeout) to prime the Gradle cache
- Add actions/cache for ~/.gradle so subsequent CI runs start warm
- Add VERBOSE env var and failure output logging from #107
* chore: split CI into parallel jobs

Split the single CI job into three parallel jobs:
1. flutter-tools-tests: Runs on ubuntu + macOS (unchanged)
2. shorebird-android-tests: Runs on Ubuntu only (faster runners)
3. shorebird-ios-tests: Runs on macOS only (requires Xcode)

This improves CI performance by:
- Running all jobs in parallel instead of sequentially
- Moving Android tests off macOS to faster Ubuntu runners
- Removing Windows from the matrix (nothing was running there anyway)

Expected speedup on macOS: ~5 minutes (no longer runs Android tests)

* Add Android smoke test on macOS

Run a single "can build an apk" test on macOS to catch any
platform-specific issues with Android builds on macOS.

* Add comment explaining why Windows is excluded
* feat: add sharded CI build runner

Adds a Dart-based shard runner for parallel engine builds:
- JSON configs for Linux, macOS, Windows shards
- run_shard.dart: executes gn/ninja/rust builds
- compose.dart: assembles iOS/macOS frameworks
- GCS upload/download for artifact staging

* refactor: parse compose.json into typed objects

Move from reading compose.json directly as Map<String, dynamic> to
using proper ComposeConfig and ComposeDef model classes. This follows
a more idiomatic Dart pattern.

* feat: add finalize.dart for manifest generation and uploads

Implements the finalize job logic that:
- Downloads artifacts from GCS staging
- Generates artifacts_manifest.yaml
- Uploads to production GCS bucket (download.shorebird.dev)

Ports upload logic from linux_upload.sh, mac_upload.sh, and
generate_manifest.sh into a single Dart script.

* fix: correct cargo-ndk invocation for Android Rust builds

- Use --target flag (not -t) with Rust target triples
- Set ANDROID_NDK_HOME to engine's hermetic NDK
- Build all Android targets in a single cargo ndk command
- Remove incorrect _androidArch helper function

This matches the behavior of linux_build.sh.

* test: add unit tests for config parsing

Tests cover:
- PlatformConfig: single-step shards, multi-step shards, compose_input
- BuildStep: gn_ninja and rust step types
- ComposeConfig: compose definitions, requires, script, args
- Error handling for unknown shards/compose names

* feat: add artifacts field to shard configs

Define artifacts declaratively in JSON configs instead of hardcoding
upload paths in Dart. Each artifact specifies:
- src: source path relative to out/
- dst: destination path with $engine placeholder
- zip: whether to zip before upload (for directories like dart-sdk)
- content_hash: whether to also upload to content-hash path (for Dart SDK)

This makes the config self-describing and aligns with Flutter's
ci/builders/*.json pattern of explicit artifact declarations.

* refactor: read shard names from JSON configs instead of hardcoding

Load PlatformConfig for each platform to get shard names dynamically,
rather than maintaining a duplicate list in finalize.dart.

* feat(ci): add manifest generation and bucket configuration

- Extract generateManifest to lib/manifest.dart with tests
- Refactor finalize.dart to use artifacts from JSON configs
- Add --bucket flag for test uploads to alternate buckets
- Add compare_buckets.dart for validating uploads against production

* chore: add pubspec.lock for shard_runner

* chore: allow shard_runner pubspec.lock in gitignore

* chore: use local .gitignore for shard_runner pubspec.lock

* refactor: load manifest from template file

Move manifest content to artifacts_manifest.template.yaml and update
generateManifest to load from template file with sync IO.

* fix: fail finalize on download errors instead of continuing

A missing shard download means incomplete artifacts. Better to fail
loudly than silently upload an incomplete build.

* fix: fail on gsutil/zip errors instead of warning

Upload and zip failures should halt the build, not silently continue
with missing artifacts.

* refactor: clean up shard_runner CLI and config parsing

- Add shared runChecked() helper to eliminate duplicated Process.run
  + exit code check patterns across config.dart, gcs.dart, finalize.dart,
  and compose.dart
- Add @immutable annotations to all data classes (via package:meta)
- Remove implicit single-step shard shorthand; all shards now use
  explicit steps arrays in JSON configs
- Convert all async file IO to sync equivalents (existsSync, etc.)
- Make --engine-src and --run-id mandatory CLI args, removing hidden
  defaults and GITHUB_RUN_ID env var fallback
- Restructure compose.json to use explicit flags/path_args instead of
  a single args list that guessed flag vs path semantics
- Collect outDirs from config upfront rather than accumulating during
  execution

* ci: add shard_runner tests to shorebird_ci workflow

- Add analysis_options.yaml (package:lints/recommended with strict mode)
- Add shard-runner-tests job with format, analyze, and test steps
- Fix stale await on sync PlatformConfig.load in compare_buckets.dart
- Reformat all files to Dart standard (80 char width)
Each shard runs on a separate machine, so it needs its own Rust build
step for the updater library. Previously only the host/android shards
had Rust steps, but all shards that build libflutter need libupdater.a
for their specific target triple.
On Windows, gcloud SDK tools like gsutil are installed as .cmd files.
When Dart's Process.run is called without runInShell, it doesn't
resolve these .cmd extensions. This adds a helper that explicitly
checks for .cmd versions in PATH on Windows.
eseidel and others added 26 commits June 11, 2026 21:59
…ifact (#146)

* ci(shorebird): add dst + artifacts to compose.json; add mac framework artifact

Two changes that pair with shorebirdtech/_build_engine#209:

1. compose.json now declares output directory + production artifact list
   per compose, matching the model shards/*.json already uses. Without
   this finalize uploads only shard outputs to prod and the iOS
   XCFramework, FlutterMacOS framework + dSYM, and gen_snapshot zips
   never leave the staging bucket.

   - ios-framework: artifacts.zip + Flutter.framework.dSYM.zip (legacy
     mac_upload.sh equivalent)
   - macos-framework: FlutterMacOS.framework.zip + .dSYM.zip
   - macos-gen-snapshot: gen_snapshot.zip, with a dst override of
     "release/snapshot" so create_macos_gen_snapshots.py's --zip flag
     (which zips the entire dst directory) doesn't scoop up sibling
     composes' outputs

2. mac-arm64 shard's artifacts list gains
   mac_release_arm64/zip_archives/darwin-arm64-release/artifacts.zip
   uploaded to flutter_infra_release/.../darwin-x64-release/artifacts.zip
   (intentional cross-arch path mirroring legacy mac_upload.sh:
   "arm macs use darwin-x64-release and we currently only support those").

Both fields are optional in the runner's parser (defaults: dst="release",
artifacts=[]), so older / mid-rollout flutter SHAs continue to work with
the new build_engine code; the finalize prod-upload set just shrinks.

* ci(shorebird): prefix android-shard artifact srcs with out_dir

The macOS android shard's artifact srcs include the shard's out_dir
prefix (e.g. "android_release_arm64/zip_archives/...") so that finalize,
which joins src against engine/src/out, finds the file. The Linux and
Windows android-* shards' srcs had no such prefix
("zip_archives/android-arm64-release/...") and finalize skipped every
artifact as not-found.

Verified by the upload log of run 25561356686 — only Linux/host and
Windows/host shard artifacts (which had correct prefixes) actually
landed in the bucket; every Linux/Windows android-* artifact was
[Skip] (not found).

Prefix every artifact src in:
- linux/android-arm64 with android_release_arm64/
- linux/android-arm32 with android_release/
- linux/android-x64 with android_release_x64/
- windows/android-arm64 with android_release_arm64/
- windows/android-arm32 with android_release/
- windows/android-x64 with android_release_x64/
…#147)

Both files are listed as shard artifacts in the new sharded build but
neither is actually produced — they were built outside the gn+ninja
graph in the legacy shell scripts (mac_build.sh / linux_build.sh) and
the sharded pipeline only knew about gn_ninja and rust step types.

shorebirdtech/_build_engine#209 adds a `shell` step type to
shard_runner. This PR uses it:

- mac-x64: new shell step runs `flutter/bin/et stamp` (writes
  out/engine_stamp.json) and copies the result into
  out/mac_release/engine_stamp.json so it ends up in the shard's
  tarball. The matching artifact src updates from "engine_stamp.json"
  to "mac_release/engine_stamp.json".

- linux/host: new shell step runs the host-built dart against
  flutter/third_party/dart/pkg/aot_tools to produce
  out/host_release/aot_tools/aot-tools.dill, matching what
  linux_build.sh:78-90 does. The existing artifact src
  ("host_release/aot_tools/aot-tools.dill") is already correct.

After this lands and an engine bump produces a SHA carrying it,
finalize will upload these two missing files and the sharded build's
prod artifact set should match the legacy build's exactly (modulo
iOS XCFramework signing).
The legacy linux_build.sh used `pub get --offline` because gclient
hooks pre-populated ~/.pub-cache (run_pub_get.py runs as part of
gclient sync). The new sharded path runs run_pub_get.py only in the
sync-linux job; per-shard runners have their own ephemeral
~/.pub-cache, so by the time the shell step runs in linux/host the
aot_tools deps (e.g. very_good_analysis) aren't cached:

  Because aot_tools depends on very_good_analysis any which doesn't
  exist (could not find package very_good_analysis in cache),
  version solving failed.

Drop --offline. pub get will fetch from pub.dev in a few seconds —
trivial overhead vs pre-populating the cache or sharing pub cache via
namespace cache.

Verified failure in run 25571102939 (linux/host).
…149)

Restores the legacy `pub get --offline` form. The original failure
(missing very_good_analysis in shard pub cache) was a shard-environment
problem — sync.yaml ran tools/run_pub_get.py to populate the cache, but
shards' pub cache is per-runner ephemeral. Fixed properly in
shorebirdtech/_build_engine#209 (shard-build.yaml now also runs
run_pub_get.py post-sync), so shell steps can rely on --offline matching
legacy behavior. The JSON config shouldn't carry the env-prep detail.

(See FIXME in build_engine's shard-build.yaml: real fix is to pull
aot_tools deps via gclient like every other in-tree Dart package.)
create_macos_framework.py outputs FlutterMacOS.framework.zip locally,
but legacy uploads it to .../darwin-x64-release/framework.zip in the
public bucket. Sharded was uploading it as FlutterMacOS.framework.zip,
so the legacy/sharded bucket compare showed framework.zip only-in-
legacy and FlutterMacOS.framework.zip only-in-sharded for the same
artifact.

src stays as FlutterMacOS.framework.zip (the actual local filename);
only the published dst path changes.
… not single-arch) (#151)

create_macos_framework.py with --zip produces two outputs in dst/:
- FlutterMacOS.framework.zip — single-arch, double-zipped framework
  (~17 MB)
- framework.zip — the xcframework (both arm64 + x64) plus codesign
  config files (entitlements.txt, without_entitlements.txt,
  unsigned_binaries.txt) (~122 MB)

Legacy mac_upload.sh uploads framework.zip (the xcframework). Compose
was uploading the single-arch FlutterMacOS.framework.zip and renaming
it to framework.zip in the bucket via the dst path — same name on
disk, completely different bytes.

Verified by bucket-comparing engine 93865d6 (sharded, post-#150)
against legacy 25c9b21 at the same darwin-x64-release/framework.zip
path: 17,237,143 bytes vs 121,918,469 bytes. After this lands, the
sharded artifact at that path should be the xcframework matching
legacy in size and contents.
)

Removes the three -ext shards (ios-release-ext, ios-sim-x64-ext,
ios-sim-arm64-ext) from macos.json and from ios-framework's `requires`.
Adds --no-extension-safe-frameworks to the compose flags so
create_ios_framework.py doesn't try to read the now-missing
*_extension_safe out_dirs.

## Why this is safe

In upstream Flutter PR flutter/engine#165346 ("Make iOS Flutter
framework extension-safe", March 2025), Victoria Ashworth removed the
two-framework design. Before that PR, BUILD.gn gated
`-fapplication-extension` and `APPLICATION_EXTENSION_API_ONLY=1` on
`if (darwin_extension_safe)`. After it, the `ios_application_extension`
gn config is applied unconditionally to every iOS Flutter framework
target. Non-extension-safe API calls are guarded at runtime with
`NS_EXTENSION_UNAVAILABLE_IOS` annotations.

In our current engine (post-PR-165346), the gn arg `darwin_extension_safe`
is set when `--darwin-extension-safe` is passed but read by zero
BUILD.gn files. Verified:

    $ grep -rn darwin_extension_safe engine/src
    engine/src/flutter/tools/gn:62:  if args.darwin_extension_safe:
    engine/src/flutter/tools/gn:839:  if args.darwin_extension_safe:
    engine/src/flutter/tools/gn:840:    gn_args['darwin_extension_safe'] = True

The only thing the flag still does is append `_extension_safe` to the
out_dir. The binaries produced in `ios_release/` and
`ios_release_extension_safe/` are byte-identical modulo embedded paths.

Upstream Flutter's CI still builds both (see mac_ios_engine.json) and
`create_ios_framework.py` still bundles the duplicate into the shipped
xcframework by default — but that's dead infrastructure nobody cleaned
up. flutter/engine#168955 added `--no-extension-safe-frameworks` as an
opt-out for DDM builds; we now use it for all our iOS composes.

## What clients see

Customers who manually integrate Flutter.xcframework into an Xcode
project and pinned `extension_safe/Flutter.xcframework/...` in their
project files will need to drop the `extension_safe/` segment and point
at `Flutter.xcframework/...` instead. Same bytes; just one less path
indirection. We expect this audience is small (most Shorebird users go
through `flutter` + `shorebird` CLI which only references the top-level
`Flutter.xcframework`).

## If that breaks more people than expected

We can restore source-level backwards compatibility with a single
symlink in `create_ios_framework.py`:

    if args.include_extension_safe_frameworks:
      os.makedirs(os.path.join(dst, 'extension_safe'), exist_ok=True)
      os.symlink('../Flutter.xcframework',
                 os.path.join(dst, 'extension_safe', 'Flutter.xcframework'))

`sky_utils.create_zip` already uses `zip -y` (symlink-preserving) and
macOS `unzip` preserves them on extract. Since iOS artifacts are only
ever pulled on macOS hosts, cross-platform symlink portability isn't
a concern. We deliberately picked the simpler path of dropping the
duplicate entirely rather than maintaining a fork patch; this comment
is here so we can revisit if needed.

## CI impact

Drops 3 of the 9 macOS shards (~50 min of CPU per engine build saved)
and ships an `ios-release/artifacts.zip` without the duplicate
`extension_safe/Flutter.xcframework` directory (~half the iOS Flutter
binary's bytes per release zip). The shorebird artifact_proxy needs no
change: it operates on whole-zip URLs, not on paths inside the zip, so
the absence of `extension_safe/` in the unzipped tree never reaches it.

Coordinated with shorebirdtech/_build_engine matrix change (drops the
three -ext entries from the build-macos strategy matrix).

Refs:
- flutter/engine#165346 (Make iOS Flutter framework extension-safe)
- flutter/engine#168955 (Add --no-extension-safe-frameworks flag)
- flutter/engine#142531 (original "nesting bundles disallowed" issue)
* Add 2-pass release build for Dynamic Dispatch table

When building for arm64 Apple platforms with the linker enabled, run
gen_snapshot twice: first in ELF mode to produce a temporary snapshot
for analyze_snapshot to compute the DD table manifest, caller links,
and slot mapping, then in assembly mode with --dd_slot_mapping to
produce the final snapshot with indirect calls wired up.

The DD table files (App.dd.link, App.dd_callers.link) are copied into
the shorebird supplement directory alongside the existing link files
so they can be bundled with releases and used during patch builds.

* Add DD analysis gen_snapshot command to test expectations

The 2-pass DD table build runs gen_snapshot in ELF mode before the
main assembly pass. Update tests to expect this additional command.

* Fix macOS universal binary test command order for concurrent DD build

The arm64 DD analysis pass delays the arm64 assembly, so x86_64
(which skips DD) completes its build first when both run concurrently
via Future.wait.

* Skip DD table computation when analyze_snapshot is absent

Move the analyze_snapshot existence check before the gen_snapshot ELF
pass so the entire DD computation is a no-op on standard Flutter SDKs
that don't ship analyze_snapshot. This fixes iOS smoke test failures
where gen_snapshot was being invoked unnecessarily in ELF mode.

Also reverts test changes that are no longer needed since DD commands
won't appear when analyze_snapshot doesn't exist in the test filesystem.

* Fix macOS universal binary test command order for DD table async

The arm64 build's async _computeDDTable() check (even a no-op when
analyze_snapshot is absent) introduces an await that lets x86_64
reach gen_snapshot first in Future.wait. Reorder test expectations
to match the actual interleaving: x86_64 before arm64 at each step.

* fix: pass DD function identity file in base build pipeline

The DD slot mapping now uses kernel_offset-based function matching
(instead of function names). The base build must export an identity
side file during gen_snapshot pass 1 and pass it to
analyze_snapshot --compute_dd_slot_mapping.

Without this, the DDSlotMapping has empty kernel_offset_to_slot and
FinalizeIndirectStaticCallTable can't assign any DD slots, resulting
in an empty DD table in the base snapshot.

* feat: make DD table max bytes configurable via environment variable

Read SHOREBIRD_DD_MAX_BYTES from the environment to allow overriding
the cascade limiter threshold. Defaults to 10000 if not set.

An environment variable is used (rather than a command-line flag) so
that older Flutter builds without DD table support silently ignore it.

* chore: bump dart_sdk_revision to cascade-limiter

Updates dart_sdk_revision to include SIMARM64 simulator fixes
(DoRedirectedCall, ClobberVolatileRegisters, Execute reason param)
needed for ios_debug engine builds.

* chore: checkpoint current DD table base build changes

Snapshot of work in progress on the base build's DD pipeline:
- debug print in AOTSnapshotter.build for usesDDTable diagnosis
- BUILD.gn change to place analyze_snapshot in universal/ next to
  gen_snapshot so flutter_tools can resolve it by path substitution

Committing to a clean base before restructuring the pipeline to
support the pre-DD optimized pass added in the dart-sdk patch flow.

* fix: probe gen_snapshot for DD flag support before running DD pipeline

When shorebird_flutter ships with the DD table 2-pass release build
enabled but the underlying engine's gen_snapshot binary predates the
DD table work (e.g. a user has downgraded their engine cache, or is
running against a Shorebird release from before DD landed),
gen_snapshot hard-errors on the ELF pass with "Setting VM flags
failed: Unrecognized flags: print_dd_function_identity_to" and the
entire release build fails. Older engines simply don't know about
`--print_dd_function_identity_to`, `--dd_slot_mapping`, or any of the
DD table flag family.

Add a capability probe in _computeDDTable that runs gen_snapshot once
with the DD flag plus a bogus kernel input. Two possible failure
modes distinguish support:

  - Flag recognized → flag parsing passes, kernel load fails:
    "Can't load Kernel binary: File size is too small to be a valid
    kernel file."

  - Flag not recognized → VM init fails at flag parsing:
    "Setting VM flags failed: Unrecognized flags:
    print_dd_function_identity_to"

If stderr contains the "Unrecognized flags" token, skip the entire
DD pipeline and fall back to a plain single-pass Shorebird release
build. The cascade-limiter linker (in aot_tools) independently
handles the no-DD case by falling back to the CT pass's op.link for
final pass OP alignment (see dart-sdk commit 139dd8c864d), so the
patch side of the pipeline keeps working too.

Probe strategies that don't work:

  - `gen_snapshot --help` doesn't list individual flags.
  - `gen_snapshot --print_flags` exits early on "At least one input
    is required" before dumping any flag info.
  - Passing the flag without a snapshot kind and kernel silently
    ignores it regardless of support.

The flag+bogus-kernel+snapshot-kind combination is the only
invocation that reliably distinguishes recognized-but-failed-later
from outright-rejected, on both DD-aware and pre-DD engines.

Result is cached per gen_snapshot path so multi-arch release builds
don't pay the probe cost more than once.

* feat: DD 2-pass release build for cascade limiter

When SHOREBIRD_DD_MAX_BYTES is set, AOTSnapshotter now performs a
2-pass build:

1. Pass 1: gen_snapshot produces an ELF for analysis + DD identity file
2. analyze_snapshot computes DD table + caller links + slot mapping
3. Pass 2: gen_snapshot rebuilds with --dd_slot_mapping for DD-enabled code

This produces DD-aware release snapshots where high-fanout cascade
functions are routed through the indirect static call table, enabling
the cascade limiter's link percentage benefit.

Also adds DD supplement files (App.dd.link, App.dd_callers.link,
App.dd_identity.link, App.dd_slots.link) to the LinkSupplement copy
list so they're propagated to the Shorebird CLI's supplement directory.

* fix: address flutter_tools test failures

- build_test.dart: GenSnapshot now requires a `fileSystem` parameter (added by
  this branch for the analyze_snapshot probe path); pass `MemoryFileSystem.test()`
  to the test constructor.
- macos_test.dart: the DD pass is gated on SHOREBIRD_DD_MAX_BYTES, which isn't
  set in the test environment, so neither arm64 nor x86_64 has an extra async
  hop before gen_snapshot. With concurrent Future.wait the archs now reach
  gen_snapshot in iteration order (arm64 first, x86_64 second). Update the
  expected command sequence and the explanatory comment to match.

* review: address build.dart feedback (dead field, fallback, helper extraction)

Three changes from Eric's review:

1. Drop the unused `reportTimings` field on AOTSnapshotter — added in
   81304a9 but never read anywhere.
2. Replace the analyze_snapshot fallback iteration with explicit
   arch-based naming, mirroring how GenSnapshot.run picks its binary.
3. Extract the inlined DD 2-pass logic out of build() into
   _runDdAnalysisPass and _readDdMaxBytes, and collapse the five
   identical path joins into a local linkPath() helper.

* revert: drop universal/ analyze_snapshot move; probe parent dir instead

Reverts the snapshot BUILD.gn change that moved the lipo'd
analyze_snapshot binary into ${root_out_dir}/universal/. That move
broke create_ios_framework.py:96, which still reads analyze_snapshot
from the build-dir root, and was only needed to make the
flutter_tools getAnalyzeSnapshotPath helper find the binary in
local-engine mode (where gen_snapshot resolves via .../universal/...).

Instead, getAnalyzeSnapshotPath now probes both dirname(genSnapshot)
and dirname(genSnapshot)/.., which finds:
  * cached SDK layout: analyze_snapshot alongside gen_snapshot (probe 1)
  * local-engine layout: analyze_snapshot one level up at the build-dir
    root (probe 2)

Keeps engine packaging untouched and avoids coupling this PR to the
Shorebird CLI's view of the published artifact layout.

* chore: bump dart_sdk_revision to 59680e070c3 (cascade-limiter HEAD)

* chore: bump dart_sdk_revision to f9f552a6e77 (DD VERIFY all-slots fix)

* chore: bump dart_sdk_revision to 109ff541113 (DD sentinel-fill)

* chore: bump dart_sdk_revision to afa77ceb273 (DD sentinel-fill amend)

* fix: harden DD analysis pass — version-match analyze_snapshot, fail loudly

Two related hardenings to prevent silent DD-disable regressions like the
one we just chased on Wonderous (link percentage cratered from 90% to
22.6% because a stale universal/analyze_snapshot binary from a prior
BUILD.gn revision was selected, analyze_snapshot rejected gen_snapshot's
output with "Wrong full snapshot version", and the DD pass swallowed the
failure):

1. getAnalyzeSnapshotPath now verifies the candidate analyze_snapshot's
   --sdk_version output matches gen_snapshot's --version output. The
   strings include the build timestamp, so two binaries from the same
   build agree exactly. A stale binary left behind by a previous build
   configuration prints a different version line and is skipped. If the
   version probe itself fails (e.g. binary won't run), fall back to the
   exists-only check rather than fail-closed.

2. _runDdAnalysisPass now checks exit codes from both
   analyze_snapshot --compute_dd_table and --compute_dd_slot_mapping
   invocations, and from the post-conditions on the produced files,
   surfacing each failure with a concrete printError + non-zero return.
   Previously the calls used await without an exit-code check, so any
   failure (snapshot version skew, missing input, OOM, etc.) silently
   left App.dd.link unwritten and the build proceeded to gen_snapshot
   pass 2 with no --dd_slot_mapping. The release would publish without
   DD activation; subsequent patches would fall back to on-the-fly DD
   computation against a no-DD base and produce structurally divergent
   patches with devastating link percentages.

Also prefer aborting early when getAnalyzeSnapshotPath returns null
(versus silently shipping no DD), with a message that explains the
patching consequence.

* chore: bump dart_sdk_revision to 49005fce564 (May 7 review fixes)

* chore: bump dart_sdk_revision to 93ded8b64ac (review #5/#7/#9/#10/#11)

* chore: bump dart_sdk_revision to c1f23751d53 (post-rebase)

* feat(flutter_tools): pass --print_dd_resolution_to and copy into supplement

Wire gen_snapshot's new --print_dd_resolution_to flag (dart-sdk
companion commit) through AOTSnapshotter._runDdAnalysisPass: the
2-pass DD release build now writes App.dd_resolution.tsv next to the
other DD link files, and LinkSupplement.create copies it into
build/<platform>/shorebird/ so it gets bundled into the release
supplement upload (and downstream into the patch debug zip).

The file is a TSV diagnostic dump of per-slot DD table resolution
outcomes — useful for figuring out which specific slots ended up
dropped during DD resolution when investigating patch link
percentage regressions. Inert when SHOREBIRD_DD_MAX_BYTES is unset
(2-pass DD build doesn't run, no file produced).

* chore: dart format build.dart (CI format check)

* chore: bump dart_sdk_revision to f3656bfc678 (post-merge of flutter#785)
Picks up the runtime_offsets_extracted.h regen on dart-sdk
flutter_release_ex/3.41.9-rc3 needed after PR flutter#785 added
Thread.indirect_static_call_table_offset and shifted offsets.
Without this bump, gen_snapshot fails CheckOffsets at engine build
time with "Thread::indirect_static_call_table_offset() got 2576,
expected 2600".
Picks up the host-portable placement of Thread.indirect_static_call_table_
(dart-sdk #move-to-portable-section) and the matching regen of
runtime_offsets_extracted.h. After this bump, gen_snapshot's CheckOffsets
passes on both Linux-x64 hosts (cross-building for android-arm64) and
macOS-arm64 hosts.
Picks up shorebirdtech/updater#358 (fix(inflate): preserve real upstream
cause + add diagnostic byte counters). The on-device Update failed log
will now carry patch_file / base_size / decompressed_into_pipe /
decompressed_out_of_pipe / base_read / base_last_seek_pos / base_seeks
/ output_written counters in its anyhow context chain, instead of the
misleading "Decompression of patch failed" message that was masking
the real upstream cause.
Investigating an in-the-wild iOS standalone patch-apply failure where
the on-device synthesized base stream is ~903 KB shorter than the
host-side `aot_tools dump_blobs` extraction. The patch is generated
against the host bytes; bipatch then reads past EOF on the device and
errors out (currently visible via the diagnostic counters in
shorebirdtech/updater#358 -- base_size=1864048b on device vs 2767106b
host-side).

This logs each of the 4 mappings' sizes separately when
`createForSnapshots` runs, so the next failure report directly names
which `Dart_SnapshotDataSize` / `Dart_SnapshotInstrSize` call returned
the wrong value. Same `FML_LOG(INFO)` level the rest of the
shorebird-side engine logs use; one extra line per app launch.

To be reverted after the root cause is identified.
Moves the diagnostic FML_LOG so it fires at every app boot (in
SetBaseSnapshot) regardless of whether inflate() is ever reached.
This way we capture per-mapping sizes for both the failing iOS
standalone variant and the passing iOS add-to-app variant for
side-by-side comparison while investigating the on-device base
size mismatch.
… observability

Two correctness/observability improvements to the iOS shorebird base
reader (SHOREBIRD_USE_INTERPRETER). Neither changes patch-application
behavior; both improve debuggability of future on-device issues.

== 1. Seek now reports absolute stream position ==

SnapshotsDataHandle::Seek was returning current_index_.offset (the
offset within the current blob) rather than the absolute position from
the start of the concatenated stream. POSIX lseek and Rust io::Seek
both contract for the new absolute position.

Every current consumer happens to ignore the returned u64:
  - bipatch v1.0.0's Read state machine calls
    `self.old.seek(SeekFrom::Current(seek))?` at lib.rs:149 and
    discards the position value — the `?` only ?-propagates I/O errors.
  - The updater's inflate() captures Seek(End, 0) once as
    `base_total_size`, but uses it solely for diagnostic display
    (the `base_size=` field in error/info lines). No control flow,
    validation, or bounds-check depends on it.

So this is a long-standing API-contract bug with no observable
patch-correctness impact — verified empirically by reproducing the
pre-fix behavior in a wrapper around Cursor and confirming patches
still apply correctly. Fixing it brings the implementation in line
with consumer expectations and makes the updater's `base_size=`
diagnostic field report the true stream length, which would have
shortened the recent rc3 investigation considerably.

Existing SnapshotsDataHandle unit tests assert on byte content read
after seeks, not on Seek's return value, so the contract change is
compatible with the test suite.

== 2. Per-mapping / per-blob FML_LOG(INFO) lines ==

  - shell/common/shorebird/shorebird.cc::SetBaseSnapshot logs the
    four mapping sizes (vm_data, isolate_data, vm_instructions,
    isolate_instructions) once per app boot.
  - shell/common/shorebird/snapshots_data_handle.cc::createForSnapshots
    logs the same per-blob sizes once per patch-apply attempt.

Both lines are INFO-level, fire at most once per significant event
(not in any hot path), and let customer syslogs include the exact
sizes the on-device base reader exposes — directly comparable to
host-side `aot_tools dump_blobs` output. Cheap, durable observability
for a code path that's previously been very hard to diagnose remotely.
The windows/host shard runs three serial steps inside one matrix job:
rust → gn_ninja host_release → gn_ninja host_debug. On the most recent
12x28 build run, those ninja phases took ~22m and ~18m respectively —
the shard's total wall clock was ~31m and it was the long pole on the
Windows critical path.

Split into two parallel sub-shards, each carrying rust + its own ninja
step + the artifacts that ninja produces:

  host-release  → rust + gn_ninja host_release
                  artifacts: dart-sdk-windows-x64.zip,
                             windows-x64-flutter.zip
  host-debug    → rust + gn_ninja host_debug
                  artifact:  artifacts.zip

Rust gets duplicated across both shards (it builds the updater rlib
which both ninja steps link). The duplication costs ~1m per shard but
runs in parallel; net wall clock for the longer sub-shard should be
roughly half the original. Setup cost (gclient sync, depot_tools,
gcloud auth) is also duplicated but small thanks to the Windows
source cache landing in _build_engine#231.

Windows has slot headroom (4 shards on a pool with more), so adding
one shard doesn't queue. Expected windows/host critical path drops
from ~31m to ~20m.

No _build_engine code change needed — shard_runner reads shard
configs JSON-key-agnostically and the matrix is dynamic via
discover-shards.
3989e7a01cd is the tip of dart-sdk's bdero/fix-indirect-call-catch-entry-moves
branch (unmerged). This is a one-off customer build, not a standard RC — the
purpose is to package the fix for the affected customer to test.
…44 fix)

Per shorebirdtech/_shorebird#2150 item 1 Part 1: Upstream Flutter PR
flutter#181275 (merged 2026-01-26, in 3.44) inverted libapp.so strip
responsibility — Flutter stopped stripping it itself, AGP is expected
to, and a new post-build check in
packages/flutter_tools/lib/src/android/gradle.dart fatal-errors if
libapp.so.sym/dbg is absent from the AAB.

This breaks two Shorebird flows:
1. Existing apps with the legacy keepDebugSymbols.add("**/libapp.so")
   line in their build.gradle.kts (AGP skips stripping → no .sym)
2. Obfuscated builds where Shorebird CLI passes
   --extra-gen-snapshot-options=--strip (gen_snapshot pre-strips → AGP
   has nothing to strip → no .sym)

Both produce valid AABs that run correctly, just without Dart-code
crash symbols in Play Console. Demoting the check to a warning lets
existing users upgrade to 3.44 without their first build failing, and
unblocks the obfuscated CI/release flow. Users get a clear message
explaining the trade-off and how to opt into AGP-side stripping.
The DD 2-pass code in AOTSnapshotter assumed iOS-style single-arch
gen_snapshot invocations. macOS breaks two of its preconditions:

1. The shipped `analyze_snapshot` for macOS is a single host-arch
   binary (no `_arm64`/`_x64` suffix). When targeting x64 from an
   arm64 host, gen_snapshot_x64 reports `macos_simx64` while
   analyze_snapshot reports `macos_arm64`, and the strict
   --sdk_version equality check in getAnalyzeSnapshotPath rejects
   it -> null -> "could not find an analyze_snapshot binary whose
   --sdk_version matches gen_snapshot" -> abort.
2. Flutter's compile_macos_framework target spawns gen_snapshot_arm64
   and gen_snapshot_x64 in parallel via Future.wait. Both DD pass 1
   invocations write to the same `App_dd_analysis.so` path under
   `.dart_tool/flutter_build/<hash>/`, so they race; even if (1) were
   fixed, the analyze_snapshot --compute_dd_table call would see a
   half-written or already-deleted ELF.

Properly fixing both means per-arch DD output paths (so the two
gen_snapshot invocations don't collide), a relaxed version check
(so a single analyze_snapshot can serve both archs when the Dart
SDK matches), or shipping arch-specific analyze_snapshot binaries
from the engine build. That's tracked separately.

For now, gate `usesLinker && ddMaxBytes > 0` on `platform !=
TargetPlatform.darwin`. macOS releases will ship without DD
activation; patches against them will compute DD on the fly when
applied -- the pre-1.6.99 behavior, slightly worse link percentage
but functional. iOS / Android / Linux / Windows are unaffected.

Reproduced on a Shorebird macOS standalone release after this
landed, where the error chain was:
  DD pass: could not find an analyze_snapshot binary whose
  --sdk_version matches gen_snapshot. ... Aborting the build instead.
  Snapshot file does not exist
  DD pass: analyze_snapshot --compute_dd_table failed with exit code 255.
  Target compile_macos_framework failed: PathNotFoundException

With this fix:
- release: succeeds (37s)
- patch: succeeds (32s)
- baseline XCUITest counter assertion fails -- separate
  accessibility/click-routing issue, unrelated to DD.
freeze.yml synced from upstream in a broken state: line 68's
`run: echo "...override: code freeze..."` makes YAML read the colon as a
mapping key, so the workflow fails to parse and reports a failed run on
every push to shorebird/dev. We never modified the file.

Replace it with upstream flutter/flutter's current version, which fixed the
issue (dropped the colon-bearing label + the offending steps). Still gated
to `github.repository == 'flutter/flutter'`, so it remains a no-op on our
fork -- this just stops the per-push red and keeps the file aligned with
upstream (no sync conflict).
…ng it) (#155)

* shorebird: consume our published Dart SDK on mac-arm64, skip rebuilding it

Point the macos-arm64 dart-sdk prebuilt at Shorebird's own published SDK
(gs://shorebird-dart-sdk-prebuilt, dep_type: gcs) instead of Google's CIPD,
and drop --no-prebuilt-dart-sdk from the mac-arm64 engine shard.

Effect: the mac-arm64 shard stops building the Dart SDK from source (the
dart_sdk gn target was a full from-source compile). With the prebuilt it
becomes copy_dart_sdk -- a ~1.5s copy of our SDK -- so the engine builds
against our Dart fork end to end and the mac-arm64 build is significantly
shorter. The published dart-sdk-darwin-arm64.zip artifact is now our SDK.

Verified locally: gn --mac-cpu=arm64 --prebuilt-dart-sdk + ninja dart_sdk
copies our SDK (revision 3bc26d9, executable) in ~1.5s.

Bot auth to the private bucket is keyless WIF via shorebird-dart-sdk-reader
(shorebirdtech/_build_engine sync.yaml). mac-x64/linux/windows are
unchanged (still build Dart from source / use Google CIPD) pending their
own published prebuilts.

* shorebird: key the dart-sdk prebuilt object on Var('dart_sdk_revision')

Use the existing dart_sdk_revision var for the macos-arm64 gcs prebuilt
object name instead of hardcoding the sha, so the source clone and the
prebuilt always reference the same fork revision. Bump dart_sdk_revision to
the published sha (3bc26d9); a published prebuilt must exist for whatever
this var points at.

Not dart_revision: that's Google's upstream revision and is load-bearing
for the linux/windows/mac-x64 CIPD entries; our GCS objects are keyed by
our fork's sha.

gclient validate passes; object resolves to
gs://shorebird-dart-sdk-prebuilt/<dart_sdk_revision>/dart-sdk-darwin-arm64.tar.gz.
Points dart_sdk_revision at the flutter_release_ex/3.44.1-rc1 dart-sdk
tip. Includes the arm64 FFI callback exit-path ABI fixes for
async/group-bound callbacks, the stack-arg delta / owned-isolate exit
fix, and the ZoneHandle fix for the DD call's arguments descriptor.
Also updates the macos-arm64 prebuilt GCS pins (sha256/size/generation)
to the published artifact for this revision.

Updater unchanged at a591b7f6b96 (same as 3.44.1-rc0).
The 3.44.1 rebase bumped source to upstream but left several tool test
files on the old (~3.41) base and dropped the test-side of shorebird's own
source divergences, so `tool_tests/general` was red on shorebird/dev (and
on every PR into it).

Reconcile the affected test files with the current source in this checkout
(3-way merge of shorebird's changes onto 3.44.1 where applicable, plus
expectation updates to match what the source actually emits):

- macos_test.dart: pick up upstream's getInfo `buildDirectory` param + new
  imports; align iOS gen_snapshot to the assembly→cc/clang→strip flow this
  fork uses (not upstream's macho-dylib path).
- common_test.dart / android_test.dart: expect `--strip` (shorebird
  strip-by-default, build.dart) on AOT ELF gen_snapshot invocations.
- cache_test.dart: match the displayName/downloadCount artifact API
  (FakeSecondaryCachedArtifact gains the missing overrides) and the
  "Web SDK"/"Engine Information" download-status messages.

Tests only; no lib/ changes, nothing skipped. All four files pass locally
(macos 21, common 22, android 13, cache 58 + 2 pre-existing skips).
Partial revert of #155 for the 3.44.1-rc1 release. With the flag
dropped, the kernel-compiling host steps (const_finder.dart.snapshot,
flutter_patched_sdk dills) resolved the upstream CIPD Dart prebuilt
(dart_revision fc3da898ea1) instead of our fork, stamping kernels with
an SDK hash our VM (0e6acdc83e) refuses to load:
'ConstFinder failure: Can't load Kernel binary: Invalid SDK hash' on
every release-mode build.

The DEPS macos-arm64 GCS prebuilt dep stays (inert with the flag
restored). #155 can return once every kernel-producing step is covered
by a fork prebuilt and the engine build verifies artifact kernel
hashes against the shipped VM.
* Bump flutter_flavorizr to published 2.5.0 (fix flavor APK build)

The Shorebird Android Tests' flavor builds fail on 3.44.1:

    Product Flavor playStore contains custom resource values,
    but the feature is disabled.

The test scaffolds flavors with flutter_flavorizr pinned to a fork at a
Flutter-3.29-era ref (AngeloAvv/flutter_flavorizr#291, which was never
merged). The AGP that ships with 3.44.1 disables resValues by default, and
the 3.29-era generator doesn't enable the build feature, so the generated
project won't configure.

Move to the published flutter_flavorizr ^2.5.0, which has moved well past
3.29 and generates AGP-8-compatible flavor config. Removes the stale fork
pin + felangel TODO.

* Pass -f to flutter_flavorizr (skip interactive prompt under CI)

flutter_flavorizr 2.5.0 added an interactive 'Do you want to proceed?
(Y/n)' confirmation, which throws 'No terminal attached to stdout' in CI
(both Android Tests and the Smoke build). The -f/--force flag runs it
non-interactively.

* Pin flutter_flavorizr to 2.4.2 (Ruby xcodeproj, fixes iOS flavor archive)

2.5.0 replaced Ruby xcodeproj with dart_xcodeproj; its generated .pbxproj
breaks 'flutter build ipa --no-codesign --flavor' (unsigned flavor archive
demands a Development Team). All versions emit identical visible signing
settings, so it's the dart_xcodeproj generation, not config — and the old
fork worked precisely because it used Ruby xcodeproj.

2.4.2 is the last Ruby-xcodeproj release and still carries the 3.29/AGP-8
resValues fix that the 3.29-era fork lacked, so it should green both the
Android (resValues) and Smoke (iOS flavor) builds.

* Enable resValues build feature after flavorizr (fix flavored APK)

There is no single flutter_flavorizr version that greens both checks:
2.5.0 fixes the AGP-8 resValues issue but its dart_xcodeproj rewrite breaks
the unsigned iOS flavor archive; 2.4.2 (last Ruby-xcodeproj release) fixes
iOS but predates the resValues fix.

So pin 2.4.2 (iOS works) and post-fix the one thing it misses: append
android.defaults.buildfeatures.resvalues=true to the generated
gradle.properties so the flavored 'flutter build apk' configures under AGP 8.
Wraps flavorizr rather than forking it.
Built on dart-sdk db98bdaa9d8 from flutter_release_ex/3.44.2-rc0. Rebase of
shorebird/dev onto Flutter 3.44.2 / Dart 3.12.2. No new fork fixes since
1.6.107 (3.44.1-rc1); engine built clean across all 20 shards.
@bdero

bdero commented Jun 12, 2026

Copy link
Copy Markdown
Member Author

shorebird/dev has been reset to the release tip (65b1e43); this CI-only PR is no longer needed.

@bdero bdero closed this Jun 12, 2026
@bdero bdero deleted the flutter_release_ex/3.44.2-rc0 branch June 12, 2026 22:32
@bdero bdero restored the flutter_release_ex/3.44.2-rc0 branch June 12, 2026 22:33
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.

3 participants