Add link.dart hook

Adding a linking script support, see go/resource-shaking. The goal is to run a `link.dart` script after kernel compilation complimentary to `build.dart` running before.

Change-Id: Iadc8648ae5fa2e823b6541c5bc08617bb860a017
Cq-Include-Trybots: luci.dart.try:pkg-linux-debug-try,pkg-linux-release-arm64-try,pkg-linux-release-try,pkg-mac-release-arm64-try,pkg-mac-release-try,pkg-win-release-arm64-try,pkg-win-release-try
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/338380
Reviewed-by: Daco Harkes <dacoharkes@google.com>
Commit-Queue: Moritz Sümmermann <mosum@google.com>
diff --git a/DEPS b/DEPS
index 95cf9e2..7db601a 100644
--- a/DEPS
+++ b/DEPS
@@ -164,7 +164,7 @@
   "material_color_utilities_rev": "799b6ba2f3f1c28c67cc7e0b4f18e0c7d7f3c03e",
   "mime_rev": "b01c9a24e0991da479bd405138be3b3e403ff456",
   "mockito_rev": "81ecb88b631a9c1e677852a33ad916238dee0ef2",
-  "native_rev": "a152cfa21129510e9227962a01ec12918e192ecc", # mosum@ and dacoharkes@ are rolling breaking changes manually while the assets features are in experimental.
+  "native_rev": "1a6faf502c01c598ce8ed6c77ea22c29774dbf34", # mosum@ and dacoharkes@ are rolling breaking changes manually while the assets features are in experimental.
   "package_config_rev": "a36e496d61937a800c22f68035dc98bf8ead7fb2",
   "path_rev": "f411b96a2598d3911b485d606430fd775fd9e41a",
   "pool_rev": "e6df05a287a2193c4c566608f9cd12e8689d3264",
diff --git a/pkg/dart2native/lib/generate.dart b/pkg/dart2native/lib/generate.dart
index 0fb6d69..7f1f6d1 100644
--- a/pkg/dart2native/lib/generate.dart
+++ b/pkg/dart2native/lib/generate.dart
@@ -16,7 +16,7 @@
   'dartaotruntime$executableSuffix',
 );
 
-/// The kinds of native executables supported by [generateNative].
+/// The kinds of native executables supported by [KernelGenerator].
 enum Kind {
   aot,
   exe;
@@ -29,98 +29,176 @@
   }
 }
 
+/// First step of generating a snapshot, generating a kernel.
+///
+/// See also the docs for [_Generator].
+extension type KernelGenerator._(_Generator _generator) {
+  KernelGenerator({
+    required String sourceFile,
+    required List<String> defines,
+    Kind kind = Kind.exe,
+    String? outputFile,
+    String? debugFile,
+    String? packages,
+    String? targetOS,
+    String enableExperiment = '',
+    bool enableAsserts = false,
+    bool verbose = false,
+    String verbosity = 'all',
+    required Directory tempDir,
+  }) : _generator = _Generator(
+          sourceFile: sourceFile,
+          defines: defines,
+          tempDir: tempDir,
+          debugFile: debugFile,
+          enableAsserts: enableAsserts,
+          enableExperiment: enableExperiment,
+          kind: kind,
+          outputFile: outputFile,
+          packages: packages,
+          targetOS: targetOS,
+          verbose: verbose,
+          verbosity: verbosity,
+        );
+
+  /// Generate a kernel file,
+  ///
+  /// [resourcesFile] is the path to `resources.json`, where the tree-shaking
+  /// information collected during kernel compilation is stored.
+  Future<SnapshotGenerator> generate({String? resourcesFile}) =>
+      _generator.generateKernel(resourcesFile: resourcesFile);
+}
+
+/// Second step of generating a snapshot is generating the snapshot itself.
+///
+/// See also the docs for [_Generator].
+extension type SnapshotGenerator._(_Generator _generator) {
+  /// Generate a snapshot or executable.
+  ///
+  /// This means concatenating the list of assets to the kernel and then calling
+  /// `genSnapshot`. [nativeAssets] is the path to `native_assets.yaml`, and
+  /// [extraOptions] is a set of extra options to be passed to `genSnapshot`.
+  Future<void> generate({
+    String? nativeAssets,
+    List<String> extraOptions = const [],
+  }) =>
+      _generator.generateSnapshotWithAssets(
+        nativeAssets: nativeAssets,
+        extraOptions: extraOptions,
+      );
+}
+
 /// Generates a self-contained executable or AOT snapshot.
 ///
-/// [sourceFile] can be the path to either a Dart source file containing `main`
-/// or a kernel file generated with `--link-platform`.
+/// This is a two-step process. First, a kernel is generated. Then, if present,
+/// the list of assets is concatenated to the kernel as a library. In a final
+/// step, the snapshot or executable itself is generated.
 ///
-/// [defines] is the list of Dart defines to be set in the compiled program.
-///
-/// [kind] is the type of executable to be generated ([Kind.exe] or [Kind.aot]).
-///
-/// [outputFile] is the location the generated output will be written. If null,
-/// the generated output will be written adjacent to [sourceFile] with the file
-/// extension matching the executable type specified by [kind].
-///
-/// [debugFile] specifies the file debugging information should be written to.
-///
-/// [packages] is the path to the `.dart_tool/package_config.json`.
-///
-/// [targetOS] specifies the operating system the executable is being generated
-/// for. This must be provided when [kind] is [Kind.exe], and it must match the
-/// current operating system.
-///
-/// [nativeAssets] is the path to `native_assets.yaml`.
-///
-/// [resourcesFile] is the path to `resources.json`.
-///
-/// [enableExperiment] is a comma separated list of language experiments to be
-/// enabled.
-///
-/// [verbosity] specifies the logging verbosity of the CFE.
-///
-/// [extraOptions] is a set of extra options to be passed to `genSnapshot`.
-Future<void> generateNative({
-  required String sourceFile,
-  required List<String> defines,
-  Kind kind = Kind.exe,
-  String? outputFile,
-  String? debugFile,
-  String? packages,
-  String? targetOS,
-  String? nativeAssets,
-  String? resourcesFile,
-  String enableExperiment = '',
-  bool enableAsserts = false,
-  bool verbose = false,
-  String verbosity = 'all',
-  List<String> extraOptions = const [],
-}) async {
-  final tempDir = Directory.systemTemp.createTempSync();
-  final programKernelFile = path.join(tempDir.path, 'program.dill');
+/// To reduce possible errors in calling order of the steps, this class is only
+/// exposed through [KernelGenerator] and [SnapshotGenerator], which make it
+/// impossible to call steps out of order.
+class _Generator {
+  /// The list of Dart defines to be set in the compiled program.
+  final List<String> _defines;
 
-  final sourcePath = _normalize(sourceFile)!;
-  final sourceWithoutDartOrDill = sourcePath.replaceFirst(
-    RegExp(r'\.(dart|dill)$'),
-    '',
-  );
-  final outputPath = _normalize(
-    outputFile ?? kind.appendFileExtension(sourceWithoutDartOrDill),
-  )!;
-  final debugPath = _normalize(debugFile);
-  packages = _normalize(packages);
+  /// The type of executable to be generated, either [Kind.exe] or [Kind.aot].
+  final Kind _kind;
 
-  if (kind == Kind.exe) {
-    if (targetOS == null) {
-      throw ArgumentError('targetOS must be specified for executables.');
-    } else if (targetOS != Platform.operatingSystem) {
-      throw UnsupportedError(
-          'Cross compilation not supported for executables.');
+  /// The location the generated output will be written. If null the generated
+  /// output will be written adjacent to [_sourcePath] with the file extension
+  /// matching the executable type specified by [_kind].
+  final String? _outputFile;
+
+  /// Specifies the file debugging information should be written to.
+  final String? _debugFile;
+
+  /// Specifies the operating system the executable is being generated for. This
+  /// must be provided when [_kind] is [Kind.exe], and it must match the current
+  /// operating system.
+  final String? _targetOS;
+
+  /// A comma separated list of language experiments to be enabled.
+  final String _enableExperiment;
+
+  ///
+  final bool _enableAsserts;
+  final bool _verbose;
+
+  /// Specifies the logging verbosity of the CFE.
+  final String _verbosity;
+
+  /// A temporary directory specified by the caller, who also has to clean it
+  /// up.
+  final Directory _tempDir;
+
+  /// The location of the compiled kernel file, which will be written on a call
+  /// to [generateKernel].
+  final String _programKernelFile;
+
+  /// The path to either a Dart source file containing `main` or a kernel file
+  /// generated with `--link-platform`.
+  final String _sourcePath;
+
+  /// The path to the `.dart_tool/package_config.json`.
+  final String? _packages;
+
+  _Generator({
+    required String sourceFile,
+    required List<String> defines,
+    required Kind kind,
+    String? outputFile,
+    String? debugFile,
+    String? packages,
+    String? targetOS,
+    required String enableExperiment,
+    required bool enableAsserts,
+    required bool verbose,
+    required String verbosity,
+    required Directory tempDir,
+  })  : _kind = kind,
+        _verbose = verbose,
+        _tempDir = tempDir,
+        _verbosity = verbosity,
+        _enableAsserts = enableAsserts,
+        _enableExperiment = enableExperiment,
+        _targetOS = targetOS,
+        _debugFile = debugFile,
+        _outputFile = outputFile,
+        _defines = defines,
+        _programKernelFile = path.join(tempDir.path, 'program.dill'),
+        _sourcePath = _normalize(sourceFile)!,
+        _packages = _normalize(packages) {
+    if (_kind == Kind.exe) {
+      if (_targetOS == null) {
+        throw ArgumentError('targetOS must be specified for executables.');
+      } else if (_targetOS != Platform.operatingSystem) {
+        throw UnsupportedError(
+            'Cross compilation not supported for executables.');
+      }
     }
   }
 
-  if (verbose) {
-    if (targetOS != null) {
-      print('Specializing Platform getters for target OS $targetOS.');
+  Future<SnapshotGenerator> generateKernel({String? resourcesFile}) async {
+    if (_verbose) {
+      if (_targetOS != null) {
+        print('Specializing Platform getters for target OS $_targetOS.');
+      }
+      print('Generating AOT kernel dill.');
     }
-    print('Compiling $sourcePath to $outputPath using format $kind:');
-    print('Generating AOT kernel dill.');
-  }
 
-  try {
     final kernelResult = await generateKernelHelper(
       dartaotruntime: dartaotruntime,
-      sourceFile: sourcePath,
-      kernelFile: programKernelFile,
-      packages: packages,
-      defines: defines,
-      fromDill: await isKernelFile(sourcePath),
-      enableAsserts: enableAsserts,
-      enableExperiment: enableExperiment,
-      targetOS: targetOS,
+      sourceFile: _sourcePath,
+      kernelFile: _programKernelFile,
+      packages: _packages,
+      defines: _defines,
+      fromDill: await isKernelFile(_sourcePath),
+      enableAsserts: _enableAsserts,
+      enableExperiment: _enableExperiment,
+      targetOS: _targetOS,
       extraGenKernelOptions: [
         '--invocation-modes=compile',
-        '--verbosity=$verbosity',
+        '--verbosity=$_verbosity',
       ],
       resourcesFile: resourcesFile,
       aot: true,
@@ -129,25 +207,88 @@
     if (kernelResult.exitCode != 0) {
       throw StateError('Generating AOT kernel dill failed!');
     }
-    String kernelFile;
+    return SnapshotGenerator._(this);
+  }
+
+  Future<void> generateSnapshotWithAssets({
+    String? nativeAssets,
+    required List<String> extraOptions,
+  }) async {
+    final kernelFile = await _concatenateAssetsToKernel(nativeAssets);
+    await _generateSnapshot(extraOptions, kernelFile);
+  }
+
+  Future<void> _generateSnapshot(
+    List<String> extraOptions,
+    String kernelFile,
+  ) async {
+    final sourceWithoutDartOrDill = _sourcePath.replaceFirst(
+      RegExp(r'\.(dart|dill)$'),
+      '',
+    );
+    final outputPath = _normalize(
+      _outputFile ?? _kind.appendFileExtension(sourceWithoutDartOrDill),
+    )!;
+    final debugPath = _normalize(_debugFile);
+
+    if (_verbose) {
+      print('Compiling $_sourcePath to $outputPath using format $_kind:');
+      print('Generating AOT snapshot. $genSnapshot $extraOptions');
+    }
+    final snapshotFile = _kind == Kind.aot
+        ? outputPath
+        : path.join(_tempDir.path, 'snapshot.aot');
+    final snapshotResult = await generateAotSnapshotHelper(
+      kernelFile,
+      snapshotFile,
+      debugPath,
+      _enableAsserts,
+      extraOptions,
+    );
+
+    if (_verbose || snapshotResult.exitCode != 0) {
+      await _forwardOutput(snapshotResult);
+    }
+    if (snapshotResult.exitCode != 0) {
+      throw StateError('Generating AOT snapshot failed!');
+    }
+
+    if (_kind == Kind.exe) {
+      if (_verbose) {
+        print('Generating executable.');
+      }
+      await writeAppendedExecutable(dartaotruntime, snapshotFile, outputPath);
+
+      if (Platform.isLinux || Platform.isMacOS) {
+        if (_verbose) {
+          print('Marking binary executable.');
+        }
+        await markExecutable(outputPath);
+      }
+    }
+
+    print('Generated: $outputPath');
+  }
+
+  Future<String> _concatenateAssetsToKernel(String? nativeAssets) async {
     if (nativeAssets == null) {
-      kernelFile = programKernelFile;
+      return _programKernelFile;
     } else {
       // TODO(dacoharkes): This method will need to be split in two parts. Then
       // the link hooks can be run in between those two parts.
       final nativeAssetsDillFile =
-          path.join(tempDir.path, 'native_assets.dill');
+          path.join(_tempDir.path, 'native_assets.dill');
       final kernelResult = await generateKernelHelper(
         dartaotruntime: dartaotruntime,
         kernelFile: nativeAssetsDillFile,
-        packages: packages,
-        defines: defines,
-        enableAsserts: enableAsserts,
-        enableExperiment: enableExperiment,
-        targetOS: targetOS,
+        packages: _packages,
+        defines: _defines,
+        enableAsserts: _enableAsserts,
+        enableExperiment: _enableExperiment,
+        targetOS: _targetOS,
         extraGenKernelOptions: [
           '--invocation-modes=compile',
-          '--verbosity=$verbosity',
+          '--verbosity=$_verbosity',
         ],
         nativeAssets: nativeAssets,
         aot: true,
@@ -156,8 +297,8 @@
       if (kernelResult.exitCode != 0) {
         throw StateError('Generating AOT kernel dill failed!');
       }
-      kernelFile = path.join(tempDir.path, 'kernel.dill');
-      final programKernelBytes = await File(programKernelFile).readAsBytes();
+      final kernelFile = path.join(_tempDir.path, 'kernel.dill');
+      final programKernelBytes = await File(_programKernelFile).readAsBytes();
       final nativeAssetKernelBytes =
           await File(nativeAssetsDillFile).readAsBytes();
       await File(kernelFile).writeAsBytes(
@@ -167,45 +308,8 @@
         ],
         flush: true,
       );
+      return kernelFile;
     }
-
-    if (verbose) {
-      print('Generating AOT snapshot. $genSnapshot $extraOptions');
-    }
-    final snapshotFile =
-        kind == Kind.aot ? outputPath : path.join(tempDir.path, 'snapshot.aot');
-    final snapshotResult = await generateAotSnapshotHelper(
-      kernelFile,
-      snapshotFile,
-      debugPath,
-      enableAsserts,
-      extraOptions,
-    );
-
-    if (verbose || snapshotResult.exitCode != 0) {
-      await _forwardOutput(snapshotResult);
-    }
-    if (snapshotResult.exitCode != 0) {
-      throw StateError('Generating AOT snapshot failed!');
-    }
-
-    if (kind == Kind.exe) {
-      if (verbose) {
-        print('Generating executable.');
-      }
-      await writeAppendedExecutable(dartaotruntime, snapshotFile, outputPath);
-
-      if (Platform.isLinux || Platform.isMacOS) {
-        if (verbose) {
-          print('Marking binary executable.');
-        }
-        await markExecutable(outputPath);
-      }
-    }
-
-    print('Generated: $outputPath');
-  } finally {
-    tempDir.deleteSync(recursive: true);
   }
 }
 
diff --git a/pkg/dartdev/lib/src/commands/build.dart b/pkg/dartdev/lib/src/commands/build.dart
index fec6afe..5fcf734 100644
--- a/pkg/dartdev/lib/src/commands/build.dart
+++ b/pkg/dartdev/lib/src/commands/build.dart
@@ -20,6 +20,9 @@
 import '../core.dart';
 import '../native_assets.dart';
 
+const _libOutputDirectory = 'lib';
+const _dataOutputDirectory = 'assets';
+
 class BuildCommand extends DartdevCommand {
   static const String cmdName = 'build';
   static const String outputOptionName = 'output';
@@ -99,7 +102,7 @@
         sourceUri.pathSegments.last.split('.').first,
       ),
     );
-    String? targetOS = args.option('target-os');
+    String? targetOS = args['target-os'];
     if (format != Kind.exe) {
       assert(format == Kind.aot);
       // If we're generating an AOT snapshot and not an executable, then
@@ -122,13 +125,15 @@
     }
     await outputDir.create(recursive: true);
 
+    // Start native asset generation here.
     stdout.writeln('Building native assets.');
     final workingDirectory = Directory.current.uri;
     final target = Target.current;
-    final buildResult = await NativeAssetsBuildRunner(
+    final nativeAssetsBuildRunner = NativeAssetsBuildRunner(
       dartExecutable: Uri.file(sdk.dart),
       logger: logger(verbose),
-    ).build(
+    );
+    final buildResult = await nativeAssetsBuildRunner.build(
       workingDirectory: workingDirectory,
       target: target,
       linkModePreference: LinkModePreferenceImpl.dynamic,
@@ -139,93 +144,119 @@
       ],
     );
     if (!buildResult.success) {
-      stderr.write('Native assets build failed.');
+      stderr.writeln('Native assets build failed.');
       return 255;
     }
-    final assets = buildResult.assets;
-    final nativeAssets = assets.whereType<NativeCodeAssetImpl>();
-    final staticAssets =
-        nativeAssets.where((e) => e.linkMode == StaticLinkingImpl());
-    if (staticAssets.isNotEmpty) {
-      stderr.write(
-          """'dart build' does not yet support NativeCodeAssets with static linking.
-Use linkMode as dynamic library instead.""");
-      return 255;
-    }
+    // End native asset generation here.
 
-    Uri? tempUri;
-    Uri? nativeAssetsDartUri;
     final tempDir = Directory.systemTemp.createTempSync();
-    if (nativeAssets.isNotEmpty) {
-      stdout.writeln('Copying native assets.');
-      KernelAsset targetLocation(NativeCodeAssetImpl asset) {
-        final linkMode = asset.linkMode;
-        final KernelAssetPath kernelAssetPath;
-        switch (linkMode) {
-          case DynamicLoadingSystemImpl _:
-            kernelAssetPath = KernelAssetSystemPath(linkMode.uri);
-          case LookupInExecutableImpl _:
-            kernelAssetPath = KernelAssetInExecutable();
-          case LookupInProcessImpl _:
-            kernelAssetPath = KernelAssetInProcess();
-          case DynamicLoadingBundledImpl _:
-            kernelAssetPath = KernelAssetRelativePath(
-              Uri(path: asset.file!.pathSegments.last),
-            );
-          default:
-            throw Exception(
-              'Unsupported NativeCodeAsset linkMode ${linkMode.runtimeType} in asset $asset',
-            );
-        }
-        return KernelAsset(
-          id: asset.id,
-          target: target,
-          path: kernelAssetPath,
-        );
+    try {
+      final packageConfig = await packageConfigUri(sourceUri);
+      final resources = path.join(tempDir.path, 'resources.json');
+      final generator = KernelGenerator(
+        kind: format,
+        sourceFile: sourceUri.toFilePath(),
+        outputFile: outputExeUri.toFilePath(),
+        verbose: verbose,
+        verbosity: args.option('verbosity')!,
+        defines: [],
+        packages: packageConfig?.toFilePath(),
+        targetOS: targetOS,
+        enableExperiment: args.enabledExperiments.join(','),
+        tempDir: tempDir,
+      );
+
+      final snapshotGenerator = await generator.generate(
+        resourcesFile: resources,
+      );
+
+      // Start linking here.
+      final linkResult = await nativeAssetsBuildRunner.link(
+        resourceIdentifiers: Uri.file(resources),
+        workingDirectory: workingDirectory,
+        target: target,
+        buildMode: BuildModeImpl.release,
+        includeParentEnvironment: true,
+        buildResult: buildResult,
+      );
+
+      if (!linkResult.success) {
+        stderr.writeln('Native assets link failed.');
+        return 255;
       }
 
-      final assetTargetLocations = {
-        for (final asset in nativeAssets) asset: targetLocation(asset),
-      };
-      final copiedFiles = await Future.wait([
-        for (final assetMapping in assetTargetLocations.entries)
-          if (assetMapping.value.path is KernelAssetRelativePath)
-            File.fromUri(assetMapping.key.file!).copy(outputUri
-                .resolveUri(
-                    (assetMapping.value.path as KernelAssetRelativePath).uri)
-                .toFilePath())
-      ]);
-      stdout.writeln('Copied ${copiedFiles.length} native assets.');
+      final tempUri = tempDir.uri;
+      Uri? assetsDartUri;
+      final allAssets = [...buildResult.assets, ...linkResult.assets];
+      final staticAssets = allAssets
+          .whereType<NativeCodeAssetImpl>()
+          .where((e) => e.linkMode == StaticLinkingImpl());
+      if (staticAssets.isNotEmpty) {
+        stderr.write(
+            """'dart build' does not yet support NativeCodeAssets with static linking.
+Use linkMode as dynamic library instead.""");
+        return 255;
+      }
+      if (allAssets.isNotEmpty) {
+        final targetMapping = _targetMapping(allAssets, target);
+        assetsDartUri = await _writeAssetsYaml(
+          targetMapping.map((e) => e.target).toList(),
+          assetsDartUri,
+          tempUri,
+        );
+        if (allAssets.isNotEmpty) {
+          stdout.writeln(
+              'Copying ${allAssets.length} build assets: ${allAssets.map((e) => e.id)}');
+          _copyAssets(targetMapping, outputUri);
+        }
+      }
 
-      tempUri = tempDir.uri;
-      nativeAssetsDartUri = tempUri.resolve('native_assets.yaml');
-      final assetsContent = KernelAssets(assetTargetLocations.values.toList())
-          .toNativeAssetsFile();
-      await Directory.fromUri(nativeAssetsDartUri.resolve('.')).create();
-      await File(nativeAssetsDartUri.toFilePath()).writeAsString(assetsContent);
+      await snapshotGenerator.generate(
+        nativeAssets: assetsDartUri?.toFilePath(),
+      );
+
+      // End linking here.
+    } finally {
+      await tempDir.delete(recursive: true);
     }
-    final packageConfig = await packageConfigUri(sourceUri);
-
-    await generateNative(
-      kind: format,
-      sourceFile: sourceUri.toFilePath(),
-      outputFile: outputExeUri.toFilePath(),
-      verbose: verbose,
-      verbosity: args.option('verbosity')!,
-      defines: [],
-      nativeAssets: nativeAssetsDartUri?.toFilePath(),
-      packages: packageConfig?.toFilePath(),
-      targetOS: targetOS,
-      enableExperiment: args.enabledExperiments.join(','),
-      resourcesFile: path.join(tempDir.path, 'resources.json'),
-    );
-
-    if (tempUri != null) {
-      await Directory.fromUri(tempUri).delete(recursive: true);
-    }
-
     return 0;
   }
+
+  List<({AssetImpl asset, KernelAsset target})> _targetMapping(
+    Iterable<AssetImpl> assets,
+    Target target,
+  ) {
+    return [
+      for (final asset in assets)
+        (asset: asset, target: asset.targetLocation(target)),
+    ];
+  }
+
+  void _copyAssets(
+    List<({AssetImpl asset, KernelAsset target})> assetTargetLocations,
+    Uri output,
+  ) {
+    for (final (asset: asset, target: target) in assetTargetLocations) {
+      final targetPath = target.path;
+      if (targetPath is KernelAssetRelativePath) {
+        asset.file!.copyTo(targetPath, output);
+      }
+    }
+  }
+
+  Future<Uri> _writeAssetsYaml(
+    List<KernelAsset> assetTargetLocations,
+    Uri? nativeAssetsDartUri,
+    Uri tempUri,
+  ) async {
+    stdout.writeln('Writing native_assets.yaml.');
+    nativeAssetsDartUri = tempUri.resolve('native_assets.yaml');
+    final assetsContent =
+        KernelAssets(assetTargetLocations).toNativeAssetsFile();
+    await Directory.fromUri(nativeAssetsDartUri.resolve('.')).create();
+    await File(nativeAssetsDartUri.toFilePath()).writeAsString(assetsContent);
+    return nativeAssetsDartUri;
+  }
 }
 
 extension on String {
@@ -234,6 +265,67 @@
   String removeDotDart() => replaceFirst(RegExp(r'\.dart$'), '');
 }
 
+extension on Uri {
+  void copyTo(KernelAssetRelativePath target, Uri outputUri) {
+    if (this != target.uri) {
+      final targetUri = outputUri.resolveUri(target.uri);
+      File.fromUri(targetUri).createSync(
+        recursive: true,
+        exclusive: true,
+      );
+      File.fromUri(this).copySync(targetUri.toFilePath());
+    }
+  }
+}
+
+extension on AssetImpl {
+  KernelAsset targetLocation(Target target) {
+    return switch (this) {
+      NativeCodeAssetImpl nativeAsset => nativeAsset.targetLocation(target),
+      DataAssetImpl dataAsset => dataAsset.targetLocation(target),
+      AssetImpl() => throw UnimplementedError(),
+    };
+  }
+}
+
+extension on NativeCodeAssetImpl {
+  KernelAsset targetLocation(Target target) {
+    final KernelAssetPath kernelAssetPath;
+    switch (linkMode) {
+      case DynamicLoadingSystemImpl dynamicLoading:
+        kernelAssetPath = KernelAssetSystemPath(dynamicLoading.uri);
+      case LookupInExecutableImpl _:
+        kernelAssetPath = KernelAssetInExecutable();
+      case LookupInProcessImpl _:
+        kernelAssetPath = KernelAssetInProcess();
+      case DynamicLoadingBundledImpl _:
+        kernelAssetPath = KernelAssetRelativePath(
+          Uri(path: path.join(_libOutputDirectory, file!.pathSegments.last)),
+        );
+      default:
+        throw Exception(
+          'Unsupported NativeCodeAsset linkMode ${linkMode.runtimeType} in asset $this',
+        );
+    }
+    return KernelAsset(
+      id: id,
+      target: target,
+      path: kernelAssetPath,
+    );
+  }
+}
+
+extension on DataAssetImpl {
+  KernelAsset targetLocation(Target target) {
+    return KernelAsset(
+      id: id,
+      target: target,
+      path: KernelAssetRelativePath(
+          Uri(path: path.join(_dataOutputDirectory, file.pathSegments.last))),
+    );
+  }
+}
+
 // TODO(https://github.com/dart-lang/package_config/issues/126): Expose this
 // logic in package:package_config.
 Future<Uri?> packageConfigUri(Uri uri) async {
diff --git a/pkg/dartdev/lib/src/commands/compile.dart b/pkg/dartdev/lib/src/commands/compile.dart
index d48ce44..86ad166 100644
--- a/pkg/dartdev/lib/src/commands/compile.dart
+++ b/pkg/dartdev/lib/src/commands/compile.dart
@@ -507,9 +507,9 @@
       stderr.writeln('Target OS: $targetOS');
       return 128;
     }
-
+    final tempDir = Directory.systemTemp.createTempSync();
     try {
-      await generateNative(
+      final kernelGenerator = KernelGenerator(
         kind: format,
         sourceFile: sourcePath,
         outputFile: args.option('output'),
@@ -520,8 +520,12 @@
         debugFile: args.option('save-debugging-info'),
         verbose: verbose,
         verbosity: args.option('verbosity')!,
-        extraOptions: args.multiOption('extra-gen-snapshot-options'),
         targetOS: targetOS,
+        tempDir: tempDir,
+      );
+      final snapshotGenerator = await kernelGenerator.generate();
+      await snapshotGenerator.generate(
+        extraOptions: args.multiOption('extra-gen-snapshot-options'),
       );
       return 0;
     } catch (e, st) {
@@ -531,6 +535,8 @@
         log.stderr(st.toString());
       }
       return compileErrorExitCode;
+    } finally {
+      await tempDir.delete(recursive: true);
     }
   }
 }
diff --git a/pkg/dartdev/lib/src/native_assets.dart b/pkg/dartdev/lib/src/native_assets.dart
index 1dbb803..6d47db8 100644
--- a/pkg/dartdev/lib/src/native_assets.dart
+++ b/pkg/dartdev/lib/src/native_assets.dart
@@ -29,11 +29,12 @@
       .exists()) {
     return (true, <AssetImpl>[]);
   }
-  final buildResult = await NativeAssetsBuildRunner(
+  final nativeAssetsBuildRunner = NativeAssetsBuildRunner(
     // This always runs in JIT mode.
     dartExecutable: Uri.file(sdk.dart),
     logger: logger(verbose),
-  ).build(
+  );
+  final buildResult = await nativeAssetsBuildRunner.build(
     workingDirectory: workingDirectory,
     // When running in JIT mode, only the host OS needs to be build.
     target: Target.current,
@@ -45,9 +46,22 @@
     runPackageName: runPackageName,
     supportedAssetTypes: [
       NativeCodeAsset.type,
+      DataAsset.type,
     ],
   );
-  return (buildResult.success, buildResult.assets);
+
+  final linkResult = await nativeAssetsBuildRunner.link(
+    workingDirectory: workingDirectory,
+    target: Target.current,
+    buildMode: BuildModeImpl.release,
+    includeParentEnvironment: true,
+    buildResult: buildResult,
+  );
+
+  return (
+    buildResult.success && linkResult.success,
+    [...buildResult.assets, ...linkResult.assets],
+  );
 }
 
 /// Compiles all native assets for host OS in JIT mode, and creates the
@@ -69,8 +83,14 @@
     return (false, null);
   }
   final kernelAssets = KernelAssets([
-    for (final asset in assets.whereType<NativeCodeAssetImpl>())
-      _targetLocation(asset),
+    ...[
+      for (final asset in assets.whereType<NativeCodeAssetImpl>())
+        _targetLocation(asset),
+    ],
+    ...[
+      for (final asset in assets.whereType<DataAssetImpl>())
+        _dataTargetLocation(asset),
+    ]
   ]);
 
   final workingDirectory = Directory.current.uri;
@@ -107,6 +127,14 @@
   );
 }
 
+KernelAsset _dataTargetLocation(DataAssetImpl asset) {
+  return KernelAsset(
+    id: asset.id,
+    target: Target.current,
+    path: KernelAssetAbsolutePath(asset.file),
+  );
+}
+
 Future<bool> warnOnNativeAssets() async {
   final workingDirectory = Directory.current.uri;
   if (!await File.fromUri(
@@ -118,8 +146,10 @@
   try {
     final packageLayout =
         await PackageLayout.fromRootPackageRoot(workingDirectory);
-    final packagesWithNativeAssets =
-        await packageLayout.packagesWithNativeAssets;
+    final packagesWithNativeAssets = [
+      ...await packageLayout.packagesWithAssets(Hook.build),
+      ...await packageLayout.packagesWithAssets(Hook.link)
+    ];
     if (packagesWithNativeAssets.isEmpty) {
       return false;
     }
diff --git a/pkg/dartdev/test/native_assets/build_test.dart b/pkg/dartdev/test/native_assets/build_test.dart
index 33810b4..b97cf6e 100644
--- a/pkg/dartdev/test/native_assets/build_test.dart
+++ b/pkg/dartdev/test/native_assets/build_test.dart
@@ -6,6 +6,7 @@
 
 import 'dart:io';
 
+import 'package:native_assets_cli/native_assets_cli_internal.dart';
 import 'package:test/test.dart';
 
 import '../utils.dart';
@@ -116,4 +117,63 @@
       expect(result.exitCode, 255);
     });
   });
+
+  test('dart link assets', timeout: longTimeout, () async {
+    await nativeAssetsTest('drop_dylib_link', (dartAppUri) async {
+      final result = await runDart(
+        arguments: [
+          '--enable-experiment=native-assets',
+          'build',
+          'bin/drop_dylib_link.dart',
+        ],
+        workingDirectory: dartAppUri,
+        logger: logger,
+        expectExitCodeZero: false,
+      );
+      expect(result.exitCode, 0);
+
+      // Check that the build directory exists
+      final directory =
+          Directory.fromUri(dartAppUri.resolve('bin/drop_dylib_link'));
+      expect(directory.existsSync(), true);
+
+      // Check that only one dylib is in the final application package
+      final buildFiles = directory.listSync(recursive: true);
+      expect(
+        buildFiles.where((file) => file.path.contains('add')),
+        isNotEmpty,
+      );
+      expect(
+        buildFiles.where((file) => file.path.contains('multiply')),
+        isEmpty,
+      );
+    });
+  });
+
+  test('dart link assets', timeout: longTimeout, () async {
+    await nativeAssetsTest('add_asset_link', (dartAppUri) async {
+      final result = await runDart(
+        arguments: [
+          '--enable-experiment=native-assets',
+          'build',
+          'bin/add_asset_link.dart',
+        ],
+        workingDirectory: dartAppUri,
+        logger: logger,
+        expectExitCodeZero: false,
+      );
+      expect(result.exitCode, 0);
+
+      // Check that the build directory exists
+      final directory =
+          Directory.fromUri(dartAppUri.resolve('bin/add_asset_link'));
+      expect(directory.existsSync(), true);
+      final dylib =
+          OSImpl.current.libraryFileName('add', DynamicLoadingBundledImpl());
+      expect(
+        File.fromUri(directory.uri.resolve('lib/$dylib')).existsSync(),
+        true,
+      );
+    });
+  });
 }
diff --git a/pkg/dartdev/test/native_assets/helpers.dart b/pkg/dartdev/test/native_assets/helpers.dart
index a536b59..65264f5 100644
--- a/pkg/dartdev/test/native_assets/helpers.dart
+++ b/pkg/dartdev/test/native_assets/helpers.dart
@@ -161,6 +161,8 @@
   assert(const [
     'dart_app',
     'native_add',
+    'drop_dylib_link',
+    'add_asset_link',
   ].contains(packageUnderTest));
   return await inTempDir((tempUri) async {
     await copyTestProjects(tempUri, logger);
diff --git a/pkg/dartdev/test/native_assets/run_test.dart b/pkg/dartdev/test/native_assets/run_test.dart
index c4906bd1..df72734 100644
--- a/pkg/dartdev/test/native_assets/run_test.dart
+++ b/pkg/dartdev/test/native_assets/run_test.dart
@@ -91,4 +91,64 @@
       expect(result.stdout, isNot(contains('build.dart')));
     });
   });
+
+  test('dart link assets succeeds', timeout: longTimeout, () async {
+    await nativeAssetsTest('drop_dylib_link', (dartAppUri) async {
+      final result = await runDart(
+        arguments: [
+          '--enable-experiment=native-assets',
+          'run',
+          'bin/drop_dylib_link.dart',
+          'add'
+        ],
+        workingDirectory: dartAppUri,
+        logger: logger,
+        expectExitCodeZero: false,
+      );
+      expect(result.exitCode, 0);
+    });
+  });
+
+  test('dart link assets doesnt have treeshaken asset', timeout: longTimeout,
+      () async {
+    await nativeAssetsTest('drop_dylib_link', (dartAppUri) async {
+      try {
+        await runDart(
+          arguments: [
+            '--enable-experiment=native-assets',
+            'run',
+            'bin/drop_dylib_link.dart',
+            'multiply'
+          ],
+          workingDirectory: dartAppUri,
+          logger: logger,
+          expectExitCodeZero: false,
+        );
+      } catch (e) {
+        expect(e, e is ArgumentError);
+        expect(
+          (e as ArgumentError).message.toString(),
+          contains('''
+Couldn't resolve native function 'multiply' in 'package:drop_dylib_link/dylib_multiply' : No asset with id 'package:drop_dylib_link/dylib_multiply' found. Available native assets: package:drop_dylib_link/dylib_add.
+'''),
+        );
+      }
+    });
+  });
+
+  test('dart add asset in linking', timeout: longTimeout, () async {
+    await nativeAssetsTest('add_asset_link', (dartAppUri) async {
+      final result = await runDart(
+        arguments: [
+          '--enable-experiment=native-assets',
+          'run',
+          'bin/add_asset_link.dart',
+        ],
+        workingDirectory: dartAppUri,
+        logger: logger,
+        expectExitCodeZero: false,
+      );
+      expect(result.exitCode, 0);
+    });
+  });
 }