From 7f52df0948e372bc580555da2912608be1aa492b Mon Sep 17 00:00:00 2001 From: Brandon DeRosier Date: Mon, 30 Mar 2026 10:24:29 -0700 Subject: [PATCH 1/9] 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. --- .../flutter_tools/lib/src/base/build.dart | 155 +++++++++++++++++- .../lib/src/build_system/targets/common.dart | 3 + 2 files changed, 157 insertions(+), 1 deletion(-) diff --git a/packages/flutter_tools/lib/src/base/build.dart b/packages/flutter_tools/lib/src/base/build.dart index 008b93da357d2..35b7ca5211cb8 100644 --- a/packages/flutter_tools/lib/src/base/build.dart +++ b/packages/flutter_tools/lib/src/base/build.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:io' as io; + import 'package:process/process.dart'; import '../artifacts.dart'; @@ -95,6 +97,7 @@ class AOTSnapshotter { }) : _logger = logger, _fileSystem = fileSystem, _xcode = xcode, + _processManager = processManager, _genSnapshot = GenSnapshot( artifacts: artifacts, processManager: processManager, @@ -104,8 +107,14 @@ class AOTSnapshotter { final Logger _logger; final FileSystem _fileSystem; final Xcode _xcode; + final ProcessManager _processManager; final GenSnapshot _genSnapshot; + /// The maximum cascade byte threshold for the DD table cascade limiter. + /// Functions whose transitive caller tree exceeds this many compiled code + /// bytes are routed through the indirect dispatch table. + static const int _ddMaxBytes = 10000; + /// Builds an architecture-specific ahead-of-time compiled snapshot of the specified script. Future build({ required TargetPlatform platform, @@ -230,9 +239,44 @@ class AOTSnapshotter { genSnapshotArgs.add(mainPath); final snapshotType = SnapshotType(platform, buildMode); + + // Dynamic Dispatch (DD) table: for arm64 Apple platforms with the linker + // enabled, we do a 2-pass build. Pass 1 produces a temp ELF for + // analyze_snapshot to compute the DD table and slot mapping. Pass 2 + // produces the final assembly snapshot with indirect calls wired up. + final bool usesDDTable = usesLinker && darwinArch == DarwinArch.arm64; + String? ddSlotMappingPath; + + if (usesDDTable) { + final ddResult = await _computeDDTable( + snapshotType: snapshotType, + darwinArch: darwinArch!, + mainPath: mainPath, + outputDir: outputDir, + genSnapshotArgs: genSnapshotArgs, + ); + if (ddResult != 0) { + return ddResult; + } + final slotMappingFile = _fileSystem.file( + _fileSystem.path.join(outputDir.parent.path, 'App.dd_slots.link'), + ); + if (slotMappingFile.existsSync()) { + ddSlotMappingPath = slotMappingFile.path; + } + } + + // Insert DD slot mapping arg before mainPath (the last arg). + final finalGenSnapshotArgs = [ + ...genSnapshotArgs.take(genSnapshotArgs.length - 1), + if (ddSlotMappingPath != null) + '--dd_slot_mapping=$ddSlotMappingPath', + genSnapshotArgs.last, // mainPath + ]; + final int genSnapshotExitCode = await _genSnapshot.run( snapshotType: snapshotType, - additionalArgs: genSnapshotArgs, + additionalArgs: finalGenSnapshotArgs, darwinArch: darwinArch, ); if (genSnapshotExitCode != 0) { @@ -258,6 +302,115 @@ class AOTSnapshotter { } } + /// Computes the Dynamic Dispatch (DD) table for the release snapshot. + /// + /// Runs gen_snapshot in ELF mode to produce a temporary snapshot, then uses + /// analyze_snapshot to compute the DD table manifest, caller links, and slot + /// mapping. The DD files are written next to the other link files in + /// [outputDir]'s parent. + /// + /// Returns 0 on success, non-zero on failure. + Future _computeDDTable({ + required SnapshotType snapshotType, + required DarwinArch darwinArch, + required String mainPath, + required Directory outputDir, + required List genSnapshotArgs, + }) async { + _logger.printTrace('Computing DD table for release snapshot...'); + + final String linkDir = outputDir.parent.path; + final String tempElfPath = _fileSystem.path.join(outputDir.path, '_dd_analysis.elf'); + final String ddTablePath = _fileSystem.path.join(linkDir, 'App.dd.link'); + final String ddCallerLinksPath = _fileSystem.path.join(linkDir, 'App.dd_callers.link'); + final String ddSlotMappingPath = _fileSystem.path.join(linkDir, 'App.dd_slots.link'); + + // Build a temporary ELF snapshot (no DD) for analyze_snapshot. + // Strip out assembly/link-dump args — we only need a bare ELF. + final elfArgs = [ + '--deterministic', + '--snapshot_kind=app-aot-elf', + '--elf=$tempElfPath', + mainPath, + ]; + final int elfExitCode = await _genSnapshot.run( + snapshotType: snapshotType, + additionalArgs: elfArgs, + darwinArch: darwinArch, + ); + if (elfExitCode != 0) { + _logger.printError('DD analysis: gen_snapshot (ELF pass) failed with exit code $elfExitCode'); + return elfExitCode; + } + + // Derive analyze_snapshot path from gen_snapshot path. + final genSnapshotArtifact = darwinArch == DarwinArch.arm64 + ? Artifact.genSnapshotArm64 + : Artifact.genSnapshotX64; + final genSnapshotPath = _genSnapshot.getSnapshotterPath(snapshotType, genSnapshotArtifact); + final analyzeSnapshotPath = _fileSystem.path.join( + _fileSystem.path.dirname(genSnapshotPath), + _fileSystem.path.basename(genSnapshotPath).replaceFirst('gen_snapshot', 'analyze_snapshot'), + ); + + if (!_fileSystem.file(analyzeSnapshotPath).existsSync()) { + _logger.printTrace('analyze_snapshot not found at $analyzeSnapshotPath, skipping DD table.'); + _cleanupFile(tempElfPath); + return 0; + } + + // Step 1: Compute DD table + caller links. + final int computeTableResult = await _runProcess(analyzeSnapshotPath, [ + '--compute_dd_table=$ddTablePath', + '--dd_caller_links=$ddCallerLinksPath', + '--dd_max_bytes=$_ddMaxBytes', + tempElfPath, + ]); + if (computeTableResult != 0) { + _logger.printError('DD analysis: compute_dd_table failed with exit code $computeTableResult'); + _cleanupFile(tempElfPath); + return computeTableResult; + } + + // Step 2: Compute DD slot mapping. + final int computeMappingResult = await _runProcess(analyzeSnapshotPath, [ + '--compute_dd_slot_mapping=$ddSlotMappingPath', + '--dd_table_data=$ddTablePath', + '--dd_caller_links=$ddCallerLinksPath', + tempElfPath, + ]); + if (computeMappingResult != 0) { + _logger.printError('DD analysis: compute_dd_slot_mapping failed with exit code $computeMappingResult'); + _cleanupFile(tempElfPath); + return computeMappingResult; + } + + _logger.printTrace('DD table computed successfully.'); + _cleanupFile(tempElfPath); + return 0; + } + + /// Runs a process and returns the exit code. + Future _runProcess(String executable, List args) async { + _logger.printTrace('Running: $executable ${args.join(' ')}'); + final io.ProcessResult result = await _processManager.run( + [executable, ...args], + ); + if (result.exitCode != 0) { + _logger.printTrace('stdout: ${result.stdout}'); + _logger.printTrace('stderr: ${result.stderr}'); + } + return result.exitCode; + } + + /// Deletes a file if it exists. + void _cleanupFile(String path) { + final file = _fileSystem.file(path); + if (file.existsSync()) { + file.deleteSync(); + } + } + /// Builds an iOS or macOS framework at [outputPath]/App.framework from the assembly /// source at [assemblyPath]. Future _buildFramework({ diff --git a/packages/flutter_tools/lib/src/build_system/targets/common.dart b/packages/flutter_tools/lib/src/build_system/targets/common.dart index e67e646bebf1b..9a9768e37de42 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/common.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/common.dart @@ -537,5 +537,8 @@ abstract final class LinkSupplement { maybeCopy('App.dispatch_table.json'); maybeCopy('App.ft.link'); maybeCopy('App.field_table.json'); + // DD table files (generated by analyze_snapshot during 2-pass release build). + maybeCopy('App.dd.link'); + maybeCopy('App.dd_callers.link'); } } From 2d4910805f6c8cc80a5ae0076624ea961d2f11f0 Mon Sep 17 00:00:00 2001 From: Brandon DeRosier Date: Tue, 31 Mar 2026 15:43:08 -0700 Subject: [PATCH 2/9] 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. --- .../test/general.shard/base/build_test.dart | 30 +++++++++++++++++++ .../build_system/targets/common_test.dart | 10 +++++++ .../build_system/targets/macos_test.dart | 10 +++++++ 3 files changed, 50 insertions(+) diff --git a/packages/flutter_tools/test/general.shard/base/build_test.dart b/packages/flutter_tools/test/general.shard/base/build_test.dart index 90cc0c26ac1db..90a926f70ec4e 100644 --- a/packages/flutter_tools/test/general.shard/base/build_test.dart +++ b/packages/flutter_tools/test/general.shard/base/build_test.dart @@ -210,6 +210,16 @@ void main() { mode: BuildMode.profile, ); processManager.addCommands([ + // DD analysis: gen_snapshot ELF pass for DD table computation. + FakeCommand( + command: [ + genSnapshotPath, + '--deterministic', + '--snapshot_kind=app-aot-elf', + '--elf=${fileSystem.path.join(outputPath, '_dd_analysis.elf')}', + 'main.dill', + ], + ), FakeCommand( command: [ genSnapshotPath, @@ -286,6 +296,16 @@ void main() { mode: BuildMode.profile, ); processManager.addCommands([ + // DD analysis: gen_snapshot ELF pass for DD table computation. + FakeCommand( + command: [ + genSnapshotPath, + '--deterministic', + '--snapshot_kind=app-aot-elf', + '--elf=${fileSystem.path.join(outputPath, '_dd_analysis.elf')}', + 'main.dill', + ], + ), FakeCommand( command: [ genSnapshotPath, @@ -358,6 +378,16 @@ void main() { mode: BuildMode.release, ); processManager.addCommands([ + // DD analysis: gen_snapshot ELF pass for DD table computation. + FakeCommand( + command: [ + genSnapshotPath, + '--deterministic', + '--snapshot_kind=app-aot-elf', + '--elf=${fileSystem.path.join(outputPath, '_dd_analysis.elf')}', + 'main.dill', + ], + ), FakeCommand( command: [ genSnapshotPath, diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/common_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/common_test.dart index c0dda6b07591e..34ecc13e58479 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/common_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/common_test.dart @@ -809,6 +809,16 @@ void main() { iosEnvironment.defines[kSdkRoot] = 'path/to/iPhoneOS.sdk'; final String build = iosEnvironment.buildDir.path; processManager.addCommands([ + // DD analysis: gen_snapshot ELF pass for DD table computation. + FakeCommand( + command: [ + 'Artifact.genSnapshotArm64.TargetPlatform.ios.profile', + '--deterministic', + kElfAot, + '--elf=$build/arm64/_dd_analysis.elf', + '$build/app.dill', + ], + ), FakeCommand( command: [ // This path is not known by the cache due to the iOS gen_snapshot split. diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart index d57c76c444c7f..e697c87ae6a30 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart @@ -946,6 +946,16 @@ flavors: final build = environment.buildDir.path; processManager.addCommands([ + // DD analysis: gen_snapshot ELF pass for DD table computation (arm64 only). + FakeCommand( + command: [ + 'Artifact.genSnapshotArm64.TargetPlatform.darwin.release', + '--deterministic', + '--snapshot_kind=app-aot-elf', + '--elf=${environment.buildDir.childFile('arm64/_dd_analysis.elf').path}', + environment.buildDir.childFile('app.dill').path, + ], + ), FakeCommand( command: [ 'Artifact.genSnapshotArm64.TargetPlatform.darwin.release', From 27e03b7b6a32b23b7835fa1c001bb8e90ba4214e Mon Sep 17 00:00:00 2001 From: Brandon DeRosier Date: Tue, 31 Mar 2026 16:00:29 -0700 Subject: [PATCH 3/9] 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. --- .../build_system/targets/macos_test.dart | 53 ++++++++++--------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart index e697c87ae6a30..fc88c6fa61748 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart @@ -947,6 +947,9 @@ flavors: final build = environment.buildDir.path; processManager.addCommands([ // DD analysis: gen_snapshot ELF pass for DD table computation (arm64 only). + // arm64 and x86_64 builds run concurrently via Future.wait. arm64 starts + // the DD ELF pass first, then x86_64 (which skips DD) starts its assembly + // pass before arm64's DD ELF completes and proceeds to its assembly pass. FakeCommand( command: [ 'Artifact.genSnapshotArm64.TargetPlatform.darwin.release', @@ -958,34 +961,36 @@ flavors: ), FakeCommand( command: [ - 'Artifact.genSnapshotArm64.TargetPlatform.darwin.release', + 'Artifact.genSnapshotX64.TargetPlatform.darwin.release', '--deterministic', ...linkInfoArgsFor(build), '--snapshot_kind=app-aot-assembly', - '--assembly=${environment.buildDir.childFile('arm64/snapshot_assembly.S').path}', + '--assembly=${environment.buildDir.childFile('x86_64/snapshot_assembly.S').path}', environment.buildDir.childFile('app.dill').path, ], ), FakeCommand( command: [ - 'Artifact.genSnapshotX64.TargetPlatform.darwin.release', + 'Artifact.genSnapshotArm64.TargetPlatform.darwin.release', '--deterministic', ...linkInfoArgsFor(build), '--snapshot_kind=app-aot-assembly', - '--assembly=${environment.buildDir.childFile('x86_64/snapshot_assembly.S').path}', + '--assembly=${environment.buildDir.childFile('arm64/snapshot_assembly.S').path}', environment.buildDir.childFile('app.dill').path, ], ), + // x86_64 framework build completes before arm64 because arm64's DD + // analysis pass delays it. FakeCommand( command: [ 'xcrun', 'cc', '-arch', - 'arm64', + 'x86_64', '-c', - environment.buildDir.childFile('arm64/snapshot_assembly.S').path, + environment.buildDir.childFile('x86_64/snapshot_assembly.S').path, '-o', - environment.buildDir.childFile('arm64/snapshot_assembly.o').path, + environment.buildDir.childFile('x86_64/snapshot_assembly.o').path, ], ), FakeCommand( @@ -993,11 +998,11 @@ flavors: 'xcrun', 'cc', '-arch', - 'x86_64', + 'arm64', '-c', - environment.buildDir.childFile('x86_64/snapshot_assembly.S').path, + environment.buildDir.childFile('arm64/snapshot_assembly.S').path, '-o', - environment.buildDir.childFile('x86_64/snapshot_assembly.o').path, + environment.buildDir.childFile('arm64/snapshot_assembly.o').path, ], ), FakeCommand( @@ -1005,7 +1010,7 @@ flavors: 'xcrun', 'clang', '-arch', - 'arm64', + 'x86_64', '-dynamiclib', '-Xlinker', '-rpath', @@ -1019,8 +1024,8 @@ flavors: '-install_name', '@rpath/App.framework/App', '-o', - environment.buildDir.childFile('arm64/App.framework/App').path, - environment.buildDir.childFile('arm64/snapshot_assembly.o').path, + environment.buildDir.childFile('x86_64/App.framework/App').path, + environment.buildDir.childFile('x86_64/snapshot_assembly.o').path, ], ), FakeCommand( @@ -1028,7 +1033,7 @@ flavors: 'xcrun', 'clang', '-arch', - 'x86_64', + 'arm64', '-dynamiclib', '-Xlinker', '-rpath', @@ -1042,8 +1047,8 @@ flavors: '-install_name', '@rpath/App.framework/App', '-o', - environment.buildDir.childFile('x86_64/App.framework/App').path, - environment.buildDir.childFile('x86_64/snapshot_assembly.o').path, + environment.buildDir.childFile('arm64/App.framework/App').path, + environment.buildDir.childFile('arm64/snapshot_assembly.o').path, ], ), FakeCommand( @@ -1051,8 +1056,8 @@ flavors: 'xcrun', 'dsymutil', '-o', - environment.buildDir.childFile('arm64/App.framework.dSYM').path, - environment.buildDir.childFile('arm64/App.framework/App').path, + environment.buildDir.childFile('x86_64/App.framework.dSYM').path, + environment.buildDir.childFile('x86_64/App.framework/App').path, ], ), FakeCommand( @@ -1060,8 +1065,8 @@ flavors: 'xcrun', 'dsymutil', '-o', - environment.buildDir.childFile('x86_64/App.framework.dSYM').path, - environment.buildDir.childFile('x86_64/App.framework/App').path, + environment.buildDir.childFile('arm64/App.framework.dSYM').path, + environment.buildDir.childFile('arm64/App.framework/App').path, ], ), FakeCommand( @@ -1069,9 +1074,9 @@ flavors: 'xcrun', 'strip', '-x', - environment.buildDir.childFile('arm64/App.framework/App').path, + environment.buildDir.childFile('x86_64/App.framework/App').path, '-o', - environment.buildDir.childFile('arm64/App.framework/App').path, + environment.buildDir.childFile('x86_64/App.framework/App').path, ], ), FakeCommand( @@ -1079,9 +1084,9 @@ flavors: 'xcrun', 'strip', '-x', - environment.buildDir.childFile('x86_64/App.framework/App').path, + environment.buildDir.childFile('arm64/App.framework/App').path, '-o', - environment.buildDir.childFile('x86_64/App.framework/App').path, + environment.buildDir.childFile('arm64/App.framework/App').path, ], ), FakeCommand( From 71e58c6ca174cebb243ee7cfc1b3bd37f929ed87 Mon Sep 17 00:00:00 2001 From: Brandon DeRosier Date: Tue, 31 Mar 2026 17:23:33 -0700 Subject: [PATCH 4/9] 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. --- .../flutter_tools/lib/src/base/build.dart | 34 ++++++----- .../test/general.shard/base/build_test.dart | 30 --------- .../build_system/targets/common_test.dart | 10 --- .../build_system/targets/macos_test.dart | 61 +++++++------------ 4 files changed, 41 insertions(+), 94 deletions(-) diff --git a/packages/flutter_tools/lib/src/base/build.dart b/packages/flutter_tools/lib/src/base/build.dart index 35b7ca5211cb8..3b66f37027836 100644 --- a/packages/flutter_tools/lib/src/base/build.dart +++ b/packages/flutter_tools/lib/src/base/build.dart @@ -319,6 +319,24 @@ class AOTSnapshotter { }) async { _logger.printTrace('Computing DD table for release snapshot...'); + // Derive analyze_snapshot path from gen_snapshot path. + // Check for it early so we can skip the entire DD computation (including + // the gen_snapshot ELF pass) when using a standard Flutter SDK that doesn't + // ship analyze_snapshot. + final genSnapshotArtifact = darwinArch == DarwinArch.arm64 + ? Artifact.genSnapshotArm64 + : Artifact.genSnapshotX64; + final genSnapshotPath = _genSnapshot.getSnapshotterPath(snapshotType, genSnapshotArtifact); + final analyzeSnapshotPath = _fileSystem.path.join( + _fileSystem.path.dirname(genSnapshotPath), + _fileSystem.path.basename(genSnapshotPath).replaceFirst('gen_snapshot', 'analyze_snapshot'), + ); + + if (!_fileSystem.file(analyzeSnapshotPath).existsSync()) { + _logger.printTrace('analyze_snapshot not found at $analyzeSnapshotPath, skipping DD table.'); + return 0; + } + final String linkDir = outputDir.parent.path; final String tempElfPath = _fileSystem.path.join(outputDir.path, '_dd_analysis.elf'); final String ddTablePath = _fileSystem.path.join(linkDir, 'App.dd.link'); @@ -343,22 +361,6 @@ class AOTSnapshotter { return elfExitCode; } - // Derive analyze_snapshot path from gen_snapshot path. - final genSnapshotArtifact = darwinArch == DarwinArch.arm64 - ? Artifact.genSnapshotArm64 - : Artifact.genSnapshotX64; - final genSnapshotPath = _genSnapshot.getSnapshotterPath(snapshotType, genSnapshotArtifact); - final analyzeSnapshotPath = _fileSystem.path.join( - _fileSystem.path.dirname(genSnapshotPath), - _fileSystem.path.basename(genSnapshotPath).replaceFirst('gen_snapshot', 'analyze_snapshot'), - ); - - if (!_fileSystem.file(analyzeSnapshotPath).existsSync()) { - _logger.printTrace('analyze_snapshot not found at $analyzeSnapshotPath, skipping DD table.'); - _cleanupFile(tempElfPath); - return 0; - } - // Step 1: Compute DD table + caller links. final int computeTableResult = await _runProcess(analyzeSnapshotPath, [ '--compute_dd_table=$ddTablePath', diff --git a/packages/flutter_tools/test/general.shard/base/build_test.dart b/packages/flutter_tools/test/general.shard/base/build_test.dart index 90a926f70ec4e..90cc0c26ac1db 100644 --- a/packages/flutter_tools/test/general.shard/base/build_test.dart +++ b/packages/flutter_tools/test/general.shard/base/build_test.dart @@ -210,16 +210,6 @@ void main() { mode: BuildMode.profile, ); processManager.addCommands([ - // DD analysis: gen_snapshot ELF pass for DD table computation. - FakeCommand( - command: [ - genSnapshotPath, - '--deterministic', - '--snapshot_kind=app-aot-elf', - '--elf=${fileSystem.path.join(outputPath, '_dd_analysis.elf')}', - 'main.dill', - ], - ), FakeCommand( command: [ genSnapshotPath, @@ -296,16 +286,6 @@ void main() { mode: BuildMode.profile, ); processManager.addCommands([ - // DD analysis: gen_snapshot ELF pass for DD table computation. - FakeCommand( - command: [ - genSnapshotPath, - '--deterministic', - '--snapshot_kind=app-aot-elf', - '--elf=${fileSystem.path.join(outputPath, '_dd_analysis.elf')}', - 'main.dill', - ], - ), FakeCommand( command: [ genSnapshotPath, @@ -378,16 +358,6 @@ void main() { mode: BuildMode.release, ); processManager.addCommands([ - // DD analysis: gen_snapshot ELF pass for DD table computation. - FakeCommand( - command: [ - genSnapshotPath, - '--deterministic', - '--snapshot_kind=app-aot-elf', - '--elf=${fileSystem.path.join(outputPath, '_dd_analysis.elf')}', - 'main.dill', - ], - ), FakeCommand( command: [ genSnapshotPath, diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/common_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/common_test.dart index 34ecc13e58479..c0dda6b07591e 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/common_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/common_test.dart @@ -809,16 +809,6 @@ void main() { iosEnvironment.defines[kSdkRoot] = 'path/to/iPhoneOS.sdk'; final String build = iosEnvironment.buildDir.path; processManager.addCommands([ - // DD analysis: gen_snapshot ELF pass for DD table computation. - FakeCommand( - command: [ - 'Artifact.genSnapshotArm64.TargetPlatform.ios.profile', - '--deterministic', - kElfAot, - '--elf=$build/arm64/_dd_analysis.elf', - '$build/app.dill', - ], - ), FakeCommand( command: [ // This path is not known by the cache due to the iOS gen_snapshot split. diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart index fc88c6fa61748..d57c76c444c7f 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart @@ -946,51 +946,36 @@ flavors: final build = environment.buildDir.path; processManager.addCommands([ - // DD analysis: gen_snapshot ELF pass for DD table computation (arm64 only). - // arm64 and x86_64 builds run concurrently via Future.wait. arm64 starts - // the DD ELF pass first, then x86_64 (which skips DD) starts its assembly - // pass before arm64's DD ELF completes and proceeds to its assembly pass. FakeCommand( command: [ 'Artifact.genSnapshotArm64.TargetPlatform.darwin.release', '--deterministic', - '--snapshot_kind=app-aot-elf', - '--elf=${environment.buildDir.childFile('arm64/_dd_analysis.elf').path}', - environment.buildDir.childFile('app.dill').path, - ], - ), - FakeCommand( - command: [ - 'Artifact.genSnapshotX64.TargetPlatform.darwin.release', - '--deterministic', ...linkInfoArgsFor(build), '--snapshot_kind=app-aot-assembly', - '--assembly=${environment.buildDir.childFile('x86_64/snapshot_assembly.S').path}', + '--assembly=${environment.buildDir.childFile('arm64/snapshot_assembly.S').path}', environment.buildDir.childFile('app.dill').path, ], ), FakeCommand( command: [ - 'Artifact.genSnapshotArm64.TargetPlatform.darwin.release', + 'Artifact.genSnapshotX64.TargetPlatform.darwin.release', '--deterministic', ...linkInfoArgsFor(build), '--snapshot_kind=app-aot-assembly', - '--assembly=${environment.buildDir.childFile('arm64/snapshot_assembly.S').path}', + '--assembly=${environment.buildDir.childFile('x86_64/snapshot_assembly.S').path}', environment.buildDir.childFile('app.dill').path, ], ), - // x86_64 framework build completes before arm64 because arm64's DD - // analysis pass delays it. FakeCommand( command: [ 'xcrun', 'cc', '-arch', - 'x86_64', + 'arm64', '-c', - environment.buildDir.childFile('x86_64/snapshot_assembly.S').path, + environment.buildDir.childFile('arm64/snapshot_assembly.S').path, '-o', - environment.buildDir.childFile('x86_64/snapshot_assembly.o').path, + environment.buildDir.childFile('arm64/snapshot_assembly.o').path, ], ), FakeCommand( @@ -998,11 +983,11 @@ flavors: 'xcrun', 'cc', '-arch', - 'arm64', + 'x86_64', '-c', - environment.buildDir.childFile('arm64/snapshot_assembly.S').path, + environment.buildDir.childFile('x86_64/snapshot_assembly.S').path, '-o', - environment.buildDir.childFile('arm64/snapshot_assembly.o').path, + environment.buildDir.childFile('x86_64/snapshot_assembly.o').path, ], ), FakeCommand( @@ -1010,7 +995,7 @@ flavors: 'xcrun', 'clang', '-arch', - 'x86_64', + 'arm64', '-dynamiclib', '-Xlinker', '-rpath', @@ -1024,8 +1009,8 @@ flavors: '-install_name', '@rpath/App.framework/App', '-o', - environment.buildDir.childFile('x86_64/App.framework/App').path, - environment.buildDir.childFile('x86_64/snapshot_assembly.o').path, + environment.buildDir.childFile('arm64/App.framework/App').path, + environment.buildDir.childFile('arm64/snapshot_assembly.o').path, ], ), FakeCommand( @@ -1033,7 +1018,7 @@ flavors: 'xcrun', 'clang', '-arch', - 'arm64', + 'x86_64', '-dynamiclib', '-Xlinker', '-rpath', @@ -1047,8 +1032,8 @@ flavors: '-install_name', '@rpath/App.framework/App', '-o', - environment.buildDir.childFile('arm64/App.framework/App').path, - environment.buildDir.childFile('arm64/snapshot_assembly.o').path, + environment.buildDir.childFile('x86_64/App.framework/App').path, + environment.buildDir.childFile('x86_64/snapshot_assembly.o').path, ], ), FakeCommand( @@ -1056,8 +1041,8 @@ flavors: 'xcrun', 'dsymutil', '-o', - environment.buildDir.childFile('x86_64/App.framework.dSYM').path, - environment.buildDir.childFile('x86_64/App.framework/App').path, + environment.buildDir.childFile('arm64/App.framework.dSYM').path, + environment.buildDir.childFile('arm64/App.framework/App').path, ], ), FakeCommand( @@ -1065,8 +1050,8 @@ flavors: 'xcrun', 'dsymutil', '-o', - environment.buildDir.childFile('arm64/App.framework.dSYM').path, - environment.buildDir.childFile('arm64/App.framework/App').path, + environment.buildDir.childFile('x86_64/App.framework.dSYM').path, + environment.buildDir.childFile('x86_64/App.framework/App').path, ], ), FakeCommand( @@ -1074,9 +1059,9 @@ flavors: 'xcrun', 'strip', '-x', - environment.buildDir.childFile('x86_64/App.framework/App').path, + environment.buildDir.childFile('arm64/App.framework/App').path, '-o', - environment.buildDir.childFile('x86_64/App.framework/App').path, + environment.buildDir.childFile('arm64/App.framework/App').path, ], ), FakeCommand( @@ -1084,9 +1069,9 @@ flavors: 'xcrun', 'strip', '-x', - environment.buildDir.childFile('arm64/App.framework/App').path, + environment.buildDir.childFile('x86_64/App.framework/App').path, '-o', - environment.buildDir.childFile('arm64/App.framework/App').path, + environment.buildDir.childFile('x86_64/App.framework/App').path, ], ), FakeCommand( From c1cfbf9acc11ac08b2bb0e20e9e18bf27fba51ac Mon Sep 17 00:00:00 2001 From: Brandon DeRosier Date: Tue, 31 Mar 2026 17:35:33 -0700 Subject: [PATCH 5/9] 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. --- .../build_system/targets/macos_test.dart | 55 +++++++++++-------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart b/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart index d57c76c444c7f..9216b5a1e5af7 100644 --- a/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart +++ b/packages/flutter_tools/test/general.shard/build_system/targets/macos_test.dart @@ -945,37 +945,44 @@ flavors: .createSync(recursive: true); final build = environment.buildDir.path; + // Because arm64 goes through an async _computeDDTable() check (even + // though it returns immediately when analyze_snapshot is absent), x86_64 + // reaches gen_snapshot first when both run concurrently via Future.wait. + // After that first yield, the two builds interleave at each await point. processManager.addCommands([ + // x86_64 gen_snapshot runs first (arm64 is still in _computeDDTable). FakeCommand( command: [ - 'Artifact.genSnapshotArm64.TargetPlatform.darwin.release', + 'Artifact.genSnapshotX64.TargetPlatform.darwin.release', '--deterministic', ...linkInfoArgsFor(build), '--snapshot_kind=app-aot-assembly', - '--assembly=${environment.buildDir.childFile('arm64/snapshot_assembly.S').path}', + '--assembly=${environment.buildDir.childFile('x86_64/snapshot_assembly.S').path}', environment.buildDir.childFile('app.dill').path, ], ), + // arm64 gen_snapshot runs next. FakeCommand( command: [ - 'Artifact.genSnapshotX64.TargetPlatform.darwin.release', + 'Artifact.genSnapshotArm64.TargetPlatform.darwin.release', '--deterministic', ...linkInfoArgsFor(build), '--snapshot_kind=app-aot-assembly', - '--assembly=${environment.buildDir.childFile('x86_64/snapshot_assembly.S').path}', + '--assembly=${environment.buildDir.childFile('arm64/snapshot_assembly.S').path}', environment.buildDir.childFile('app.dill').path, ], ), + // From here on the two builds interleave: x86_64 then arm64 at each step. FakeCommand( command: [ 'xcrun', 'cc', '-arch', - 'arm64', + 'x86_64', '-c', - environment.buildDir.childFile('arm64/snapshot_assembly.S').path, + environment.buildDir.childFile('x86_64/snapshot_assembly.S').path, '-o', - environment.buildDir.childFile('arm64/snapshot_assembly.o').path, + environment.buildDir.childFile('x86_64/snapshot_assembly.o').path, ], ), FakeCommand( @@ -983,11 +990,11 @@ flavors: 'xcrun', 'cc', '-arch', - 'x86_64', + 'arm64', '-c', - environment.buildDir.childFile('x86_64/snapshot_assembly.S').path, + environment.buildDir.childFile('arm64/snapshot_assembly.S').path, '-o', - environment.buildDir.childFile('x86_64/snapshot_assembly.o').path, + environment.buildDir.childFile('arm64/snapshot_assembly.o').path, ], ), FakeCommand( @@ -995,7 +1002,7 @@ flavors: 'xcrun', 'clang', '-arch', - 'arm64', + 'x86_64', '-dynamiclib', '-Xlinker', '-rpath', @@ -1009,8 +1016,8 @@ flavors: '-install_name', '@rpath/App.framework/App', '-o', - environment.buildDir.childFile('arm64/App.framework/App').path, - environment.buildDir.childFile('arm64/snapshot_assembly.o').path, + environment.buildDir.childFile('x86_64/App.framework/App').path, + environment.buildDir.childFile('x86_64/snapshot_assembly.o').path, ], ), FakeCommand( @@ -1018,7 +1025,7 @@ flavors: 'xcrun', 'clang', '-arch', - 'x86_64', + 'arm64', '-dynamiclib', '-Xlinker', '-rpath', @@ -1032,8 +1039,8 @@ flavors: '-install_name', '@rpath/App.framework/App', '-o', - environment.buildDir.childFile('x86_64/App.framework/App').path, - environment.buildDir.childFile('x86_64/snapshot_assembly.o').path, + environment.buildDir.childFile('arm64/App.framework/App').path, + environment.buildDir.childFile('arm64/snapshot_assembly.o').path, ], ), FakeCommand( @@ -1041,8 +1048,8 @@ flavors: 'xcrun', 'dsymutil', '-o', - environment.buildDir.childFile('arm64/App.framework.dSYM').path, - environment.buildDir.childFile('arm64/App.framework/App').path, + environment.buildDir.childFile('x86_64/App.framework.dSYM').path, + environment.buildDir.childFile('x86_64/App.framework/App').path, ], ), FakeCommand( @@ -1050,8 +1057,8 @@ flavors: 'xcrun', 'dsymutil', '-o', - environment.buildDir.childFile('x86_64/App.framework.dSYM').path, - environment.buildDir.childFile('x86_64/App.framework/App').path, + environment.buildDir.childFile('arm64/App.framework.dSYM').path, + environment.buildDir.childFile('arm64/App.framework/App').path, ], ), FakeCommand( @@ -1059,9 +1066,9 @@ flavors: 'xcrun', 'strip', '-x', - environment.buildDir.childFile('arm64/App.framework/App').path, + environment.buildDir.childFile('x86_64/App.framework/App').path, '-o', - environment.buildDir.childFile('arm64/App.framework/App').path, + environment.buildDir.childFile('x86_64/App.framework/App').path, ], ), FakeCommand( @@ -1069,9 +1076,9 @@ flavors: 'xcrun', 'strip', '-x', - environment.buildDir.childFile('x86_64/App.framework/App').path, + environment.buildDir.childFile('arm64/App.framework/App').path, '-o', - environment.buildDir.childFile('x86_64/App.framework/App').path, + environment.buildDir.childFile('arm64/App.framework/App').path, ], ), FakeCommand( From b74213d458d492ce53a7ad74e48149719259933f Mon Sep 17 00:00:00 2001 From: Brandon DeRosier Date: Wed, 1 Apr 2026 05:55:48 -0700 Subject: [PATCH 6/9] 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. --- packages/flutter_tools/lib/src/base/build.dart | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/flutter_tools/lib/src/base/build.dart b/packages/flutter_tools/lib/src/base/build.dart index 3b66f37027836..7f699abe25ce0 100644 --- a/packages/flutter_tools/lib/src/base/build.dart +++ b/packages/flutter_tools/lib/src/base/build.dart @@ -342,13 +342,17 @@ class AOTSnapshotter { final String ddTablePath = _fileSystem.path.join(linkDir, 'App.dd.link'); final String ddCallerLinksPath = _fileSystem.path.join(linkDir, 'App.dd_callers.link'); final String ddSlotMappingPath = _fileSystem.path.join(linkDir, 'App.dd_slots.link'); + final String ddIdentityPath = _fileSystem.path.join(linkDir, 'App.dd_identity.link'); // Build a temporary ELF snapshot (no DD) for analyze_snapshot. // Strip out assembly/link-dump args — we only need a bare ELF. + // Export DD function identity (InstructionsTable index → kernel_offset) + // so the slot mapping can use kernel_offset-based function matching. final elfArgs = [ '--deterministic', '--snapshot_kind=app-aot-elf', '--elf=$tempElfPath', + '--print_dd_function_identity_to=$ddIdentityPath', mainPath, ]; final int elfExitCode = await _genSnapshot.run( @@ -371,24 +375,29 @@ class AOTSnapshotter { if (computeTableResult != 0) { _logger.printError('DD analysis: compute_dd_table failed with exit code $computeTableResult'); _cleanupFile(tempElfPath); + _cleanupFile(ddIdentityPath); return computeTableResult; } - // Step 2: Compute DD slot mapping. + // Step 2: Compute DD slot mapping using identity file for + // kernel_offset-based function matching. final int computeMappingResult = await _runProcess(analyzeSnapshotPath, [ '--compute_dd_slot_mapping=$ddSlotMappingPath', '--dd_table_data=$ddTablePath', '--dd_caller_links=$ddCallerLinksPath', + '--dd_function_identity=$ddIdentityPath', tempElfPath, ]); if (computeMappingResult != 0) { _logger.printError('DD analysis: compute_dd_slot_mapping failed with exit code $computeMappingResult'); _cleanupFile(tempElfPath); + _cleanupFile(ddIdentityPath); return computeMappingResult; } _logger.printTrace('DD table computed successfully.'); _cleanupFile(tempElfPath); + _cleanupFile(ddIdentityPath); return 0; } From fa050e0a0b3e8902ec853fedbc9aa38cccfd8e34 Mon Sep 17 00:00:00 2001 From: Brandon DeRosier Date: Thu, 2 Apr 2026 16:47:02 -0700 Subject: [PATCH 7/9] 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. --- packages/flutter_tools/lib/src/base/build.dart | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/flutter_tools/lib/src/base/build.dart b/packages/flutter_tools/lib/src/base/build.dart index 7f699abe25ce0..a2277acbd95ee 100644 --- a/packages/flutter_tools/lib/src/base/build.dart +++ b/packages/flutter_tools/lib/src/base/build.dart @@ -110,10 +110,19 @@ class AOTSnapshotter { final ProcessManager _processManager; final GenSnapshot _genSnapshot; - /// The maximum cascade byte threshold for the DD table cascade limiter. + /// The default cascade byte threshold for the DD table cascade limiter. + static const int _ddMaxBytesDefault = 10000; + + /// The cascade byte threshold for the DD table cascade limiter. /// Functions whose transitive caller tree exceeds this many compiled code /// bytes are routed through the indirect dispatch table. - static const int _ddMaxBytes = 10000; + /// + /// Overridable via the SHOREBIRD_DD_MAX_BYTES environment variable. An + /// environment variable is used (rather than a command-line flag) so that + /// older Flutter builds without DD table support silently ignore it. + static int get _ddMaxBytes => + int.tryParse(io.Platform.environment['SHOREBIRD_DD_MAX_BYTES'] ?? '') ?? + _ddMaxBytesDefault; /// Builds an architecture-specific ahead-of-time compiled snapshot of the specified script. Future build({ From 4259c50eeec66731e04caf095df2bff3c38f0a05 Mon Sep 17 00:00:00 2001 From: Brandon DeRosier Date: Thu, 2 Apr 2026 22:31:47 -0700 Subject: [PATCH 8/9] 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. --- DEPS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPS b/DEPS index 0a222ea156b76..19af2c91f4174 100644 --- a/DEPS +++ b/DEPS @@ -16,7 +16,7 @@ vars = { 'skia_git': 'https://skia.googlesource.com', 'llvm_git': 'https://llvm.googlesource.com', 'skia_revision': 'a183ded9ad67d998a5b0fe4cd86d3ef5402ffb45', - "dart_sdk_revision": "667fc4ab87165889877a529f72c17bdb3b36738d", + "dart_sdk_revision": "c7c0514cb1cf73a921eff762195f5210740aafef", "dart_sdk_git": "git@github.com:shorebirdtech/dart-sdk.git", "updater_git": "https://github.com/shorebirdtech/updater.git", "updater_rev": "dc2cd0a86a89e53cd5c0f87efe4ccea93fae9eae", From 5572ff22855e405c4c4e5477706670e63c72ab5f Mon Sep 17 00:00:00 2001 From: Brandon DeRosier Date: Thu, 2 Apr 2026 22:32:00 -0700 Subject: [PATCH 9/9] fix: copy Rust updater library to build output dir for Ninja tracking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The GN action for building the Rust updater via cargo declared only a stamp file as output. The library path referenced by libs[] was in the source tree, so Ninja couldn't map it to any build rule — causing "missing and no known rule to make it" for targets where the pre-built library didn't already exist (e.g. host_debug on macOS). Fix: the build_rust_updater.py script now copies the cargo-built library from the source tree to target_gen_dir, which is declared as an action output. This lets Ninja properly order the link step after the cargo build. --- DEPS | 2 +- .../flutter/shell/common/shorebird/BUILD.gn | 17 ++++++++++++---- .../common/shorebird/build_rust_updater.gni | 7 ++++++- .../common/shorebird/build_rust_updater.py | 20 ++++++++++++++++--- 4 files changed, 37 insertions(+), 9 deletions(-) diff --git a/DEPS b/DEPS index 19af2c91f4174..3525ae20966fe 100644 --- a/DEPS +++ b/DEPS @@ -16,7 +16,7 @@ vars = { 'skia_git': 'https://skia.googlesource.com', 'llvm_git': 'https://llvm.googlesource.com', 'skia_revision': 'a183ded9ad67d998a5b0fe4cd86d3ef5402ffb45', - "dart_sdk_revision": "c7c0514cb1cf73a921eff762195f5210740aafef", + "dart_sdk_revision": "4452ab217de", "dart_sdk_git": "git@github.com:shorebirdtech/dart-sdk.git", "updater_git": "https://github.com/shorebirdtech/updater.git", "updater_rev": "dc2cd0a86a89e53cd5c0f87efe4ccea93fae9eae", diff --git a/engine/src/flutter/shell/common/shorebird/BUILD.gn b/engine/src/flutter/shell/common/shorebird/BUILD.gn index 66695791bd402..ca18086a67b31 100644 --- a/engine/src/flutter/shell/common/shorebird/BUILD.gn +++ b/engine/src/flutter/shell/common/shorebird/BUILD.gn @@ -17,21 +17,30 @@ source_set("snapshots_data_handle") { } if (shorebird_updater_supported) { + # The library is copied from cargo's source-tree output into the build + # output dir so GN/Ninja can track it as an action output and properly + # order the link step after the cargo build. + _updater_output_lib = "$target_gen_dir/$shorebird_updater_lib_name" + action("build_rust_updater") { script = "//flutter/shell/common/shorebird/build_rust_updater.py" - # The stamp file is the declared output for Ninja dependency tracking. _stamp = "$target_gen_dir/rust_updater_${shorebird_updater_rust_target}.stamp" - outputs = [ _stamp ] + outputs = [ + _stamp, + _updater_output_lib, + ] args = [ "--rust-target", shorebird_updater_rust_target, "--manifest-dir", rebase_path("$shorebird_updater_dir", root_build_dir), + "--cargo-output-lib", + rebase_path("$shorebird_updater_cargo_lib", root_build_dir), "--output-lib", - rebase_path("$shorebird_updater_output_lib", root_build_dir), + rebase_path("$_updater_output_lib", root_build_dir), "--stamp", rebase_path(_stamp, root_build_dir), ] @@ -73,7 +82,7 @@ source_set("updater") { if (shorebird_updater_supported) { deps += [ ":build_rust_updater" ] - libs = [ shorebird_updater_output_lib ] + libs = [ _updater_output_lib ] if (is_win) { libs += [ "userenv.lib" ] diff --git a/engine/src/flutter/shell/common/shorebird/build_rust_updater.gni b/engine/src/flutter/shell/common/shorebird/build_rust_updater.gni index 147395685e35d..ef53ab6bd1dd8 100644 --- a/engine/src/flutter/shell/common/shorebird/build_rust_updater.gni +++ b/engine/src/flutter/shell/common/shorebird/build_rust_updater.gni @@ -60,7 +60,12 @@ if (shorebird_updater_supported) { _updater_lib_name = "libupdater.a" } - shorebird_updater_output_lib = "$shorebird_updater_dir/target/$shorebird_updater_rust_target/release/$_updater_lib_name" + # Path where cargo produces the library (in the source tree). + shorebird_updater_cargo_lib = "$shorebird_updater_dir/target/$shorebird_updater_rust_target/release/$_updater_lib_name" + + # The library filename, used by BUILD.gn to construct the output path + # in target_gen_dir (which is only available inside BUILD.gn targets). + shorebird_updater_lib_name = _updater_lib_name # Glob all .rs source files so the input list stays in sync automatically. shorebird_updater_rs_sources = diff --git a/engine/src/flutter/shell/common/shorebird/build_rust_updater.py b/engine/src/flutter/shell/common/shorebird/build_rust_updater.py index 7212e4ef21348..0effa29e118a6 100644 --- a/engine/src/flutter/shell/common/shorebird/build_rust_updater.py +++ b/engine/src/flutter/shell/common/shorebird/build_rust_updater.py @@ -19,7 +19,14 @@ def main(): parser.add_argument( '--manifest-dir', required=True, help='Directory containing the workspace Cargo.toml' ) - parser.add_argument('--output-lib', required=True, help='Expected output library path') + parser.add_argument( + '--cargo-output-lib', + required=True, + help='Path where cargo places the built library (in the source tree)' + ) + parser.add_argument( + '--output-lib', required=True, help='Path to copy the library to (in the build output dir)' + ) parser.add_argument('--stamp', required=True, help='Stamp file to write on success') parser.add_argument('--ndk-path', help='Path to the Android NDK (required for Android targets)') parser.add_argument( @@ -45,6 +52,7 @@ def main(): # Ninja runs the action). Resolve them to absolute paths so they work # regardless of cargo's working directory. manifest_path = os.path.abspath(os.path.join(args.manifest_dir, 'Cargo.toml')) + cargo_output_lib = os.path.abspath(args.cargo_output_lib) output_lib = os.path.abspath(args.output_lib) cmd = [ @@ -65,10 +73,16 @@ def main(): print(f'ERROR: cargo build failed with exit code {result.returncode}', file=sys.stderr) return result.returncode - if not os.path.exists(output_lib): - print(f'ERROR: Expected output library not found: {output_lib}', file=sys.stderr) + if not os.path.exists(cargo_output_lib): + print(f'ERROR: Cargo output library not found: {cargo_output_lib}', file=sys.stderr) return 1 + # Copy the library from the cargo output (source tree) to the build output + # dir so GN/Ninja can track it as an action output. + import shutil + os.makedirs(os.path.dirname(output_lib), exist_ok=True) + shutil.copy2(cargo_output_lib, output_lib) + # Write stamp file to signal success to Ninja. with open(args.stamp, 'w') as f: f.write('')