diff --git a/DEPS b/DEPS index 0a222ea156b76..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": "667fc4ab87165889877a529f72c17bdb3b36738d", + "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('') diff --git a/packages/flutter_tools/lib/src/base/build.dart b/packages/flutter_tools/lib/src/base/build.dart index 008b93da357d2..a2277acbd95ee 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,23 @@ class AOTSnapshotter { final Logger _logger; final FileSystem _fileSystem; final Xcode _xcode; + final ProcessManager _processManager; final GenSnapshot _genSnapshot; + /// 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. + /// + /// 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({ required TargetPlatform platform, @@ -230,9 +248,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 +311,126 @@ 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...'); + + // 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'); + 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( + snapshotType: snapshotType, + additionalArgs: elfArgs, + darwinArch: darwinArch, + ); + if (elfExitCode != 0) { + _logger.printError('DD analysis: gen_snapshot (ELF pass) failed with exit code $elfExitCode'); + return elfExitCode; + } + + // 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); + _cleanupFile(ddIdentityPath); + return computeTableResult; + } + + // 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; + } + + /// 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'); } } 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(