Skip to content
Draft
2 changes: 1 addition & 1 deletion DEPS
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
17 changes: 13 additions & 4 deletions engine/src/flutter/shell/common/shorebird/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -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),
]
Expand Down Expand Up @@ -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" ]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
20 changes: 17 additions & 3 deletions engine/src/flutter/shell/common/shorebird/build_rust_updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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 = [
Expand All @@ -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('')
Expand Down
175 changes: 174 additions & 1 deletion packages/flutter_tools/lib/src/base/build.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -95,6 +97,7 @@ class AOTSnapshotter {
}) : _logger = logger,
_fileSystem = fileSystem,
_xcode = xcode,
_processManager = processManager,
_genSnapshot = GenSnapshot(
artifacts: artifacts,
processManager: processManager,
Expand All @@ -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<int> build({
required TargetPlatform platform,
Expand Down Expand Up @@ -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 = <String>[
...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) {
Expand All @@ -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<int> _computeDDTable({
required SnapshotType snapshotType,
required DarwinArch darwinArch,
required String mainPath,
required Directory outputDir,
required List<String> 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 = <String>[
'--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, <String>[
'--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, <String>[
'--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<int> _runProcess(String executable, List<String> args) async {
_logger.printTrace('Running: $executable ${args.join(' ')}');
final io.ProcessResult result = await _processManager.run(
<String>[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<int> _buildFramework({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
}
Loading
Loading