[vm/ffi] dartdev CLI native-assets
This CL introduces native assets suport for `dart run` and introduces
`dart build` which is similar to `dart compile` but outputs a folder
instead to that native assets can be bundled with an executable.
Change-Id: Ib6cfb95539f0adee46c99e531e440928c3f72f2b
Cq-Include-Trybots: luci.dart.try:pkg-linux-debug-try,pkg-linux-release-try,pkg-mac-release-arm64-try,pkg-mac-release-try,pkg-win-release-try
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/267340
Reviewed-by: Martin Kustermann <kustermann@google.com>
Reviewed-by: Ben Konyi <bkonyi@google.com>
Commit-Queue: Daco Harkes <dacoharkes@google.com>
diff --git a/pkg/_fe_analyzer_shared/lib/src/experiments/flags.dart b/pkg/_fe_analyzer_shared/lib/src/experiments/flags.dart
index 13e5da4..5bdc64c 100644
--- a/pkg/_fe_analyzer_shared/lib/src/experiments/flags.dart
+++ b/pkg/_fe_analyzer_shared/lib/src/experiments/flags.dart
@@ -107,6 +107,13 @@
experimentEnabledVersion: const Version(2, 17),
experimentReleasedVersion: const Version(2, 17)),
+ nativeAssets(
+ name: 'native-assets',
+ isEnabledByDefault: false,
+ isExpired: false,
+ experimentEnabledVersion: const Version(3, 1),
+ experimentReleasedVersion: const Version(3, 1)),
+
nonNullable(
name: 'non-nullable',
isEnabledByDefault: true,
diff --git a/pkg/analyzer/lib/src/dart/analysis/experiments.g.dart b/pkg/analyzer/lib/src/dart/analysis/experiments.g.dart
index dc6c8064..9ca8dc6 100644
--- a/pkg/analyzer/lib/src/dart/analysis/experiments.g.dart
+++ b/pkg/analyzer/lib/src/dart/analysis/experiments.g.dart
@@ -32,6 +32,7 @@
EnableString.macros: ExperimentalFeatures.macros,
EnableString.named_arguments_anywhere:
ExperimentalFeatures.named_arguments_anywhere,
+ EnableString.native_assets: ExperimentalFeatures.native_assets,
EnableString.non_nullable: ExperimentalFeatures.non_nullable,
EnableString.nonfunction_type_aliases:
ExperimentalFeatures.nonfunction_type_aliases,
@@ -93,6 +94,9 @@
/// String to enable the experiment "named-arguments-anywhere"
static const String named_arguments_anywhere = 'named-arguments-anywhere';
+ /// String to enable the experiment "native-assets"
+ static const String native_assets = 'native-assets';
+
/// String to enable the experiment "non-nullable"
static const String non_nullable = 'non-nullable';
@@ -278,8 +282,18 @@
releaseVersion: Version.parse('2.17.0'),
);
- static final non_nullable = ExperimentalFeature(
+ static final native_assets = ExperimentalFeature(
index: 14,
+ enableString: EnableString.native_assets,
+ isEnabledByDefault: IsEnabledByDefault.native_assets,
+ isExpired: IsExpired.native_assets,
+ documentation: 'Compile and bundle native assets.',
+ experimentalReleaseVersion: null,
+ releaseVersion: null,
+ );
+
+ static final non_nullable = ExperimentalFeature(
+ index: 15,
enableString: EnableString.non_nullable,
isEnabledByDefault: IsEnabledByDefault.non_nullable,
isExpired: IsExpired.non_nullable,
@@ -289,7 +303,7 @@
);
static final nonfunction_type_aliases = ExperimentalFeature(
- index: 15,
+ index: 16,
enableString: EnableString.nonfunction_type_aliases,
isEnabledByDefault: IsEnabledByDefault.nonfunction_type_aliases,
isExpired: IsExpired.nonfunction_type_aliases,
@@ -299,7 +313,7 @@
);
static final patterns = ExperimentalFeature(
- index: 16,
+ index: 17,
enableString: EnableString.patterns,
isEnabledByDefault: IsEnabledByDefault.patterns,
isExpired: IsExpired.patterns,
@@ -309,7 +323,7 @@
);
static final records = ExperimentalFeature(
- index: 17,
+ index: 18,
enableString: EnableString.records,
isEnabledByDefault: IsEnabledByDefault.records,
isExpired: IsExpired.records,
@@ -319,7 +333,7 @@
);
static final sealed_class = ExperimentalFeature(
- index: 18,
+ index: 19,
enableString: EnableString.sealed_class,
isEnabledByDefault: IsEnabledByDefault.sealed_class,
isExpired: IsExpired.sealed_class,
@@ -329,7 +343,7 @@
);
static final set_literals = ExperimentalFeature(
- index: 19,
+ index: 20,
enableString: EnableString.set_literals,
isEnabledByDefault: IsEnabledByDefault.set_literals,
isExpired: IsExpired.set_literals,
@@ -339,7 +353,7 @@
);
static final spread_collections = ExperimentalFeature(
- index: 20,
+ index: 21,
enableString: EnableString.spread_collections,
isEnabledByDefault: IsEnabledByDefault.spread_collections,
isExpired: IsExpired.spread_collections,
@@ -349,7 +363,7 @@
);
static final super_parameters = ExperimentalFeature(
- index: 21,
+ index: 22,
enableString: EnableString.super_parameters,
isEnabledByDefault: IsEnabledByDefault.super_parameters,
isExpired: IsExpired.super_parameters,
@@ -359,7 +373,7 @@
);
static final test_experiment = ExperimentalFeature(
- index: 22,
+ index: 23,
enableString: EnableString.test_experiment,
isEnabledByDefault: IsEnabledByDefault.test_experiment,
isExpired: IsExpired.test_experiment,
@@ -370,7 +384,7 @@
);
static final triple_shift = ExperimentalFeature(
- index: 23,
+ index: 24,
enableString: EnableString.triple_shift,
isEnabledByDefault: IsEnabledByDefault.triple_shift,
isExpired: IsExpired.triple_shift,
@@ -380,7 +394,7 @@
);
static final unnamed_libraries = ExperimentalFeature(
- index: 24,
+ index: 25,
enableString: EnableString.unnamed_libraries,
isEnabledByDefault: IsEnabledByDefault.unnamed_libraries,
isExpired: IsExpired.unnamed_libraries,
@@ -390,7 +404,7 @@
);
static final value_class = ExperimentalFeature(
- index: 25,
+ index: 26,
enableString: EnableString.value_class,
isEnabledByDefault: IsEnabledByDefault.value_class,
isExpired: IsExpired.value_class,
@@ -400,7 +414,7 @@
);
static final variance = ExperimentalFeature(
- index: 26,
+ index: 27,
enableString: EnableString.variance,
isEnabledByDefault: IsEnabledByDefault.variance,
isExpired: IsExpired.variance,
@@ -455,6 +469,9 @@
/// Default state of the experiment "named-arguments-anywhere"
static const bool named_arguments_anywhere = true;
+ /// Default state of the experiment "native-assets"
+ static const bool native_assets = false;
+
/// Default state of the experiment "non-nullable"
static const bool non_nullable = true;
@@ -541,6 +558,9 @@
/// Expiration status of the experiment "named-arguments-anywhere"
static const bool named_arguments_anywhere = true;
+ /// Expiration status of the experiment "native-assets"
+ static const bool native_assets = false;
+
/// Expiration status of the experiment "non-nullable"
static const bool non_nullable = true;
@@ -631,6 +651,9 @@
bool get named_arguments_anywhere =>
isEnabled(ExperimentalFeatures.named_arguments_anywhere);
+ /// Current state for the flag "native-assets"
+ bool get native_assets => isEnabled(ExperimentalFeatures.native_assets);
+
/// Current state for the flag "non-nullable"
bool get non_nullable => isEnabled(ExperimentalFeatures.non_nullable);
diff --git a/pkg/dart2native/lib/dart2native.dart b/pkg/dart2native/lib/dart2native.dart
index 75b0859..17f110d 100644
--- a/pkg/dart2native/lib/dart2native.dart
+++ b/pkg/dart2native/lib/dart2native.dart
@@ -50,16 +50,18 @@
}
Future<ProcessResult> generateAotKernel(
- String dart,
- String genKernel,
- String platformDill,
- String sourceFile,
- String kernelFile,
- String? packages,
- List<String> defines,
- {String enableExperiment = '',
- String? targetOS,
- List<String> extraGenKernelOptions = const []}) {
+ String dart,
+ String genKernel,
+ String platformDill,
+ String sourceFile,
+ String kernelFile,
+ String? packages,
+ List<String> defines, {
+ String enableExperiment = '',
+ String? targetOS,
+ List<String> extraGenKernelOptions = const [],
+ String? nativeAssets,
+}) {
return Process.run(dart, [
genKernel,
'--platform',
@@ -73,6 +75,7 @@
'-o',
kernelFile,
...extraGenKernelOptions,
+ if (nativeAssets != null) ...['--native-assets', nativeAssets],
sourceFile
]);
}
diff --git a/pkg/dart2native/lib/generate.dart b/pkg/dart2native/lib/generate.dart
index 0c6a9e0..d354237 100644
--- a/pkg/dart2native/lib/generate.dart
+++ b/pkg/dart2native/lib/generate.dart
@@ -33,6 +33,7 @@
bool verbose = false,
String verbosity = 'all',
List<String> extraOptions = const [],
+ String? nativeAssets,
}) async {
final Directory tempDir = Directory.systemTemp.createTempSync();
try {
@@ -62,15 +63,23 @@
}
final String kernelFile = path.join(tempDir.path, 'kernel.dill');
- final kernelResult = await generateAotKernel(Platform.executable, genKernel,
- productPlatformDill, sourcePath, kernelFile, packages, defines,
- enableExperiment: enableExperiment,
- targetOS: targetOS,
- extraGenKernelOptions: [
- '--invocation-modes=compile',
- '--verbosity=$verbosity',
- '--${soundNullSafety ? '' : 'no-'}sound-null-safety',
- ]);
+ final kernelResult = await generateAotKernel(
+ Platform.executable,
+ genKernel,
+ productPlatformDill,
+ sourcePath,
+ kernelFile,
+ packages,
+ defines,
+ enableExperiment: enableExperiment,
+ targetOS: targetOS,
+ extraGenKernelOptions: [
+ '--invocation-modes=compile',
+ '--verbosity=$verbosity',
+ '--${soundNullSafety ? '' : 'no-'}sound-null-safety',
+ ],
+ nativeAssets: nativeAssets,
+ );
await _forwardOutput(kernelResult);
if (kernelResult.exitCode != 0) {
throw 'Generating AOT kernel dill failed!';
diff --git a/pkg/dartdev/lib/dartdev.dart b/pkg/dartdev/lib/dartdev.dart
index dba71b9..59ddfc5 100644
--- a/pkg/dartdev/lib/dartdev.dart
+++ b/pkg/dartdev/lib/dartdev.dart
@@ -17,6 +17,7 @@
import 'src/analytics.dart';
import 'src/commands/analyze.dart';
+import 'src/commands/build.dart';
import 'src/commands/compilation_server.dart';
import 'src/commands/compile.dart';
import 'src/commands/create.dart';
@@ -54,7 +55,7 @@
}
// Finally, call the runner to execute the command; see DartdevRunner.
- final runner = DartdevRunner(args);
+ final runner = DartdevRunner(args, io.Platform.executableArguments);
exitCode = await runner.run(args);
} on UsageException catch (e) {
// TODO(sigurdm): It is unclear when a UsageException gets to here, and
@@ -86,15 +87,23 @@
final bool verbose;
+ final List<String> vmEnabledExperiments;
+
late Analytics _analytics;
- DartdevRunner(List<String> args)
+ DartdevRunner(List<String> args, [List<String> vmArgs = const []])
: verbose = args.contains('-v') || args.contains('--verbose'),
argParser = globalDartdevOptionsParser(
verbose: args.contains('-v') || args.contains('--verbose')),
+ vmEnabledExperiments = parseVmEnabledExperiments(vmArgs),
super('dart', '$dartdevDescription.') {
addCommand(AnalyzeCommand(verbose: verbose));
addCommand(CompilationServerCommand(verbose: verbose));
+ final nativeAssetsExperimentEnabled =
+ nativeAssetsEnabled(vmEnabledExperiments);
+ if (nativeAssetsExperimentEnabled) {
+ addCommand(BuildCommand(verbose: verbose));
+ }
addCommand(CompileCommand(verbose: verbose));
addCommand(CreateCommand(verbose: verbose));
addCommand(DebugAdapterCommand(verbose: verbose));
@@ -113,8 +122,13 @@
isVerbose: () => verbose,
),
);
- addCommand(RunCommand(verbose: verbose));
- addCommand(TestCommand());
+ addCommand(RunCommand(
+ verbose: verbose,
+ nativeAssetsExperimentEnabled: nativeAssetsExperimentEnabled,
+ ));
+ addCommand(TestCommand(
+ nativeAssetsExperimentEnabled: nativeAssetsExperimentEnabled,
+ ));
}
@visibleForTesting
diff --git a/pkg/dartdev/lib/src/commands/build.dart b/pkg/dartdev/lib/src/commands/build.dart
new file mode 100644
index 0000000..798dce3
--- /dev/null
+++ b/pkg/dartdev/lib/src/commands/build.dart
@@ -0,0 +1,207 @@
+// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:io';
+
+import 'package:dart2native/generate.dart';
+import 'package:dartdev/src/commands/compile.dart';
+import 'package:dartdev/src/experiments.dart';
+import 'package:dartdev/src/sdk.dart';
+import 'package:front_end/src/api_prototype/compiler_options.dart'
+ show Verbosity;
+import 'package:native_assets_builder/native_assets_builder.dart';
+import 'package:native_assets_cli/native_assets_cli.dart';
+import 'package:path/path.dart' as path;
+
+import '../core.dart';
+import '../native_assets.dart';
+
+class BuildCommand extends DartdevCommand {
+ static const String cmdName = 'build';
+ static const String outputOptionName = 'output';
+ static const String formatOptionName = 'format';
+
+ BuildCommand({bool verbose = false})
+ : super(cmdName, 'Build a Dart application including native assets.',
+ verbose) {
+ argParser
+ ..addOption(
+ outputOptionName,
+ abbr: 'o',
+ help: '''
+ Write the output to <folder name>.
+ This can be an absolute or relative path.
+ ''',
+ )
+ ..addOption(
+ formatOptionName,
+ abbr: 'f',
+ allowed: ['exe', 'aot'],
+ defaultsTo: 'exe',
+ )
+ ..addOption(
+ 'verbosity',
+ help: 'Sets the verbosity level of the compilation.',
+ defaultsTo: Verbosity.defaultValue,
+ allowed: Verbosity.allowedValues,
+ allowedHelp: Verbosity.allowedValuesHelp,
+ )
+ ..addExperimentalFlags(verbose: verbose);
+ }
+
+ @override
+ String get invocation => '${super.invocation} <dart entry point>';
+
+ @override
+ Future<int> run() async {
+ if (!Sdk.checkArtifactExists(genKernel) ||
+ !Sdk.checkArtifactExists(genSnapshot) ||
+ !Sdk.checkArtifactExists(sdk.dart)) {
+ return 255;
+ }
+ // AOT compilation isn't supported on ia32. Currently, generating an
+ // executable only supports AOT runtimes, so these commands are disabled.
+ if (Platform.version.contains('ia32')) {
+ stderr.write("'dart build' is not supported on x86 architectures");
+ return 64;
+ }
+ final args = argResults!;
+
+ // We expect a single rest argument; the dart entry point.
+ if (args.rest.length != 1) {
+ // This throws.
+ usageException('Missing Dart entry point.');
+ }
+
+ // TODO(https://dartbug.com/52458): Support `dart build <pkg>:<bin-script>`.
+ // Similar to Dart run. Possibly also in `dart compile`.
+ final sourceUri = Uri(path: args.rest[0].normalizeCanonicalizePath());
+ if (!checkFile(sourceUri.toFilePath())) {
+ return -1;
+ }
+
+ final outputUri = Uri.directory(
+ (args[outputOptionName] as String?)
+ ?.normalizeCanonicalizePath()
+ .makeFolder() ??
+ sourceUri.toFilePath().removeDotDart().makeFolder(),
+ );
+
+ final format = args[formatOptionName] as String;
+ final outputExeUri = outputUri
+ .resolve('${sourceUri.pathSegments.last.split('.').first}.$format');
+
+ final outputDir = Directory.fromUri(outputUri);
+ if (await outputDir.exists()) {
+ stdout.writeln('Deleting output directory: ${outputUri.toFilePath()}.');
+ await outputDir.delete(recursive: true);
+ }
+ await outputDir.create(recursive: true);
+
+ stdout.writeln('Building native assets.');
+ final workingDirectory = Directory.current.uri;
+ final target = Target.current;
+ final nativeAssets = await NativeAssetsBuildRunner(
+ dartExecutable: Uri.file(sdk.dart),
+ logger: logger,
+ ).build(
+ workingDirectory: workingDirectory,
+ target: target,
+ linkModePreference: LinkModePreference.dynamic,
+ includeParentEnvironment: true,
+ );
+ final staticAssets = nativeAssets.whereLinkMode(LinkMode.static);
+ if (staticAssets.isNotEmpty) {
+ stderr.write(
+ """'dart build' does not yet support native artifacts packaged as ${LinkMode.static.name}.
+Use linkMode as dynamic library instead.""");
+ return 255;
+ }
+
+ Uri? tempUri;
+ Uri? nativeAssetsDartUri;
+ if (nativeAssets.isNotEmpty) {
+ stdout.writeln('Copying native assets.');
+ Asset targetLocation(Asset asset) {
+ final path = asset.path;
+ switch (path.runtimeType) {
+ case AssetSystemPath:
+ case AssetInExecutable:
+ case AssetInProcess:
+ return asset;
+ case AssetAbsolutePath:
+ return asset.copyWith(
+ path: AssetRelativePath(
+ Uri(
+ path: (path as AssetAbsolutePath).uri.pathSegments.last,
+ ),
+ ),
+ );
+ }
+ throw 'Unsupported asset path type ${path.runtimeType} in asset $asset';
+ }
+
+ final assetTargetLocations = {
+ for (final asset in nativeAssets) asset: targetLocation(asset),
+ };
+ await Future.wait([
+ for (final assetMapping in assetTargetLocations.entries)
+ if (assetMapping.key != assetMapping.value)
+ File.fromUri((assetMapping.key.path as AssetAbsolutePath).uri).copy(
+ outputUri
+ .resolveUri(
+ (assetMapping.value.path as AssetRelativePath).uri)
+ .toFilePath())
+ ]);
+
+ tempUri = (await Directory.systemTemp.createTemp()).uri;
+ nativeAssetsDartUri = tempUri.resolve('native_assets.yaml');
+ final assetsContent =
+ assetTargetLocations.values.toList().toNativeAssetsFile();
+ await Directory.fromUri(nativeAssetsDartUri.resolve('.')).create();
+ await File(nativeAssetsDartUri.toFilePath()).writeAsString(assetsContent);
+ }
+ final packageConfig = await packageConfigUri(sourceUri);
+
+ await generateNative(
+ kind: format,
+ sourceFile: sourceUri.toFilePath(),
+ outputFile: outputExeUri.toFilePath(),
+ verbose: verbose,
+ verbosity: args['verbosity'],
+ defines: [],
+ nativeAssets: nativeAssetsDartUri?.toFilePath(),
+ packages: packageConfig?.toFilePath(),
+ );
+
+ if (tempUri != null) {
+ await Directory.fromUri(tempUri).delete(recursive: true);
+ }
+
+ return 0;
+ }
+}
+
+extension on String {
+ String normalizeCanonicalizePath() => path.canonicalize(path.normalize(this));
+ String makeFolder() => endsWith('\\') || endsWith('/') ? this : '$this/';
+ String removeDotDart() => replaceFirst(RegExp(r'\.dart$'), '');
+}
+
+// TODO(https://github.com/dart-lang/package_config/issues/126): Expose this
+// logic in package:package_config.
+Future<Uri?> packageConfigUri(Uri uri) async {
+ while (true) {
+ final candidate = uri.resolve('.dart_tool/package_config.json');
+ if (await File.fromUri(candidate).exists()) {
+ return candidate;
+ }
+ final parent = uri.resolve('..');
+ if (parent == uri) {
+ return null;
+ }
+ uri = parent;
+ }
+}
diff --git a/pkg/dartdev/lib/src/commands/compile.dart b/pkg/dartdev/lib/src/commands/compile.dart
index 691f924..f532d61 100644
--- a/pkg/dartdev/lib/src/commands/compile.dart
+++ b/pkg/dartdev/lib/src/commands/compile.dart
@@ -14,6 +14,7 @@
import '../core.dart';
import '../experiments.dart';
+import '../native_assets.dart';
import '../sdk.dart';
import '../utils.dart';
import '../vm_interop_handler.dart';
@@ -236,12 +237,14 @@
final String commandName;
final String format;
final String help;
+ final bool nativeAssetsExperimentEnabled;
CompileNativeCommand({
required this.commandName,
required this.format,
required this.help,
bool verbose = false,
+ this.nativeAssetsExperimentEnabled = false,
}) : super(commandName, 'Compile Dart $help', verbose) {
argParser
..addOption(
@@ -326,6 +329,15 @@
return compileErrorExitCode;
}
+ if (nativeAssetsExperimentEnabled) {
+ final assets = await compileNativeAssetsJit();
+ if (assets?.isNotEmpty ?? false) {
+ stderr.writeln(
+ "'dart compile' does currently not support native assets.");
+ return 255;
+ }
+ }
+
try {
await generateNative(
kind: format,
@@ -422,8 +434,10 @@
class CompileCommand extends DartdevCommand {
static const String cmdName = 'compile';
- CompileCommand({bool verbose = false})
- : super(cmdName, 'Compile Dart to various formats.', verbose) {
+ CompileCommand({
+ bool verbose = false,
+ bool nativeAssetsExperimentEnabled = false,
+ }) : super(cmdName, 'Compile Dart to various formats.', verbose) {
addSubcommand(CompileJSCommand(verbose: verbose));
addSubcommand(CompileSnapshotCommand(
commandName: CompileSnapshotCommand.jitSnapshotCmdName,
@@ -446,6 +460,7 @@
help: 'to a self-contained executable.',
format: 'exe',
verbose: verbose,
+ nativeAssetsExperimentEnabled: nativeAssetsExperimentEnabled,
));
addSubcommand(CompileNativeCommand(
commandName: CompileNativeCommand.aotSnapshotCmdName,
@@ -453,6 +468,7 @@
'To run the snapshot use: dartaotruntime <AOT snapshot file>',
format: 'aot',
verbose: verbose,
+ nativeAssetsExperimentEnabled: nativeAssetsExperimentEnabled,
));
}
}
diff --git a/pkg/dartdev/lib/src/commands/run.dart b/pkg/dartdev/lib/src/commands/run.dart
index 21d7de4..c0f53b8 100644
--- a/pkg/dartdev/lib/src/commands/run.dart
+++ b/pkg/dartdev/lib/src/commands/run.dart
@@ -16,6 +16,7 @@
import '../core.dart';
import '../experiments.dart';
import '../generate_kernel.dart';
+import '../native_assets.dart';
import '../resident_frontend_constants.dart';
import '../resident_frontend_utils.dart';
import '../sdk.dart';
@@ -42,8 +43,12 @@
);
}
- RunCommand({bool verbose = false})
- : super(
+ final bool nativeAssetsExperimentEnabled;
+
+ RunCommand({
+ bool verbose = false,
+ this.nativeAssetsExperimentEnabled = false,
+ }) : super(
cmdName,
'Run a Dart program.',
verbose,
@@ -295,6 +300,18 @@
}
}
+ String? nativeAssets;
+ if (nativeAssetsExperimentEnabled) {
+ try {
+ nativeAssets = (await compileNativeAssetsJitYamlFile())?.toFilePath();
+ } on Exception catch (e, stacktrace) {
+ log.stderr('Error: Compiling native assets failed.');
+ log.stderr(e.toString());
+ log.stderr(stacktrace.toString());
+ return errorExitCode;
+ }
+ }
+
final hasServerInfoOption = args.wasParsed(serverInfoOption);
final useResidentServer =
args.wasParsed(residentOption) || hasServerInfoOption;
@@ -304,6 +321,7 @@
executable = await getExecutableForCommand(
mainCommand,
allowSnapshot: !(useResidentServer || hasExperiments),
+ nativeAssets: nativeAssets,
);
} on CommandResolutionFailedException catch (e) {
log.stderr(e.message);
diff --git a/pkg/dartdev/lib/src/commands/test.dart b/pkg/dartdev/lib/src/commands/test.dart
index 0d594fc..d75007d 100644
--- a/pkg/dartdev/lib/src/commands/test.dart
+++ b/pkg/dartdev/lib/src/commands/test.dart
@@ -5,9 +5,11 @@
import 'dart:async';
import 'package:args/args.dart';
+import 'package:dartdev/src/experiments.dart';
import 'package:pub/pub.dart';
import '../core.dart';
+import '../native_assets.dart';
import '../vm_interop_handler.dart';
/// Implement `dart test`.
@@ -16,7 +18,10 @@
class TestCommand extends DartdevCommand {
static const String cmdName = 'test';
- TestCommand() : super(cmdName, 'Run tests for a project.', false);
+ final bool nativeAssetsExperimentEnabled;
+
+ TestCommand({this.nativeAssetsExperimentEnabled = false})
+ : super(cmdName, 'Run tests for a project.', false);
// This argument parser is here solely to ensure that VM specific flags are
// provided before any command and to provide a more consistent help message
@@ -39,10 +44,27 @@
@override
FutureOr<int> run() async {
final args = argResults!;
+
+ String? nativeAssets;
+ if (nativeAssetsExperimentEnabled) {
+ try {
+ nativeAssets = (await compileNativeAssetsJitYamlFile())?.toFilePath();
+ } on Exception catch (e, stacktrace) {
+ log.stderr('Error: Compiling native assets failed.');
+ log.stderr(e.toString());
+ log.stderr(stacktrace.toString());
+ return DartdevCommand.errorExitCode;
+ }
+ }
+
try {
- final testExecutable = await getExecutableForCommand('test:test');
- log.trace('dart $testExecutable ${args.rest.join(' ')}');
- VmInteropHandler.run(testExecutable.executable, args.rest,
+ final testExecutable = await getExecutableForCommand('test:test',
+ nativeAssets: nativeAssets);
+ final argsRestNoExperiment = args.rest
+ .where((e) => !e.startsWith('--$experimentFlagName='))
+ .toList();
+ log.trace('dart $testExecutable ${argsRestNoExperiment.join(' ')}');
+ VmInteropHandler.run(testExecutable.executable, argsRestNoExperiment,
packageConfigOverride: testExecutable.packageConfig!);
return 0;
} on CommandResolutionFailedException catch (e) {
diff --git a/pkg/dartdev/lib/src/experiments.dart b/pkg/dartdev/lib/src/experiments.dart
index 5a49e07..b31c2af 100644
--- a/pkg/dartdev/lib/src/experiments.dart
+++ b/pkg/dartdev/lib/src/experiments.dart
@@ -76,3 +76,25 @@
return enabledExperiments;
}
}
+
+List<String> parseVmEnabledExperiments(List<String> vmArgs) {
+ var experiments = <String>[];
+ var itr = vmArgs.iterator;
+ while (itr.moveNext()) {
+ var arg = itr.current;
+ if (arg == '--$experimentFlagName') {
+ if (!itr.moveNext()) break;
+ experiments.add(itr.current);
+ } else if (arg.startsWith('--$experimentFlagName=')) {
+ var parts = arg.split('=');
+ if (parts.length == 2) {
+ experiments.addAll(parts[1].split(','));
+ }
+ }
+ }
+ return experiments;
+}
+
+bool nativeAssetsEnabled(List<String> vmEnabledExperiments) =>
+ vmEnabledExperiments
+ .contains(ExperimentalFeatures.native_assets.enableString);
diff --git a/pkg/dartdev/lib/src/native_assets.dart b/pkg/dartdev/lib/src/native_assets.dart
new file mode 100644
index 0000000..1a3d874
--- /dev/null
+++ b/pkg/dartdev/lib/src/native_assets.dart
@@ -0,0 +1,72 @@
+// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:io';
+
+import 'package:dartdev/src/sdk.dart';
+import 'package:logging/logging.dart';
+import 'package:native_assets_builder/native_assets_builder.dart';
+import 'package:native_assets_cli/native_assets_cli.dart';
+
+import 'core.dart';
+
+final logger = Logger('')
+ ..onRecord.listen((LogRecord record) {
+ final levelValue = record.level.value;
+ if (levelValue >= Level.SEVERE.value) {
+ log.stderr(record.message);
+ } else if (levelValue >= Level.INFO.value) {
+ log.stdout(record.message);
+ } else {
+ log.trace(record.message);
+ }
+ });
+
+/// Compiles all native assets for host OS in JIT mode.
+///
+/// Returns `null` on missing package_config.json, failing gracefully.
+Future<List<Asset>?> compileNativeAssetsJit() async {
+ final workingDirectory = Directory.current.uri;
+ // TODO(https://github.com/dart-lang/package_config/issues/126): Use
+ // package config resolution from package:package_config.
+ if (!await File.fromUri(
+ workingDirectory.resolve('.dart_tool/package_config.json'))
+ .exists()) {
+ return null;
+ }
+ final assets = await NativeAssetsBuildRunner(
+ // This always runs in JIT mode.
+ dartExecutable: Uri.file(sdk.dart),
+ logger: logger,
+ ).build(
+ workingDirectory: workingDirectory,
+ // When running in JIT mode, only the host OS needs to be build.
+ target: Target.current,
+ // When running in JIT mode, only dynamic libraries are supported.
+ linkModePreference: LinkModePreference.dynamic,
+ includeParentEnvironment: true,
+ );
+ return assets;
+}
+
+/// Compiles all native assets for host OS in JIT mode, and creates the
+/// native assets yaml file.
+///
+/// Used in `dart run` and `dart test`.
+///
+/// Returns `null` on missing package_config.json, failing gracefully.
+Future<Uri?> compileNativeAssetsJitYamlFile() async {
+ final assets = await compileNativeAssetsJit();
+ if (assets == null) return null;
+
+ final workingDirectory = Directory.current.uri;
+ final assetsUri = workingDirectory.resolve('.dart_tool/native_assets.yaml');
+ final nativeAssetsYaml = '''# Native assets mapping for host OS in JIT mode.
+# Generated by dartdev and package:native.
+${assets.toNativeAssetsFile()}''';
+ final assetFile = File(assetsUri.toFilePath());
+ await assetFile.writeAsString(nativeAssetsYaml);
+ return assetsUri;
+}
diff --git a/pkg/dartdev/lib/src/vm_interop_handler.dart b/pkg/dartdev/lib/src/vm_interop_handler.dart
index 388d1bd..ab6f189 100644
--- a/pkg/dartdev/lib/src/vm_interop_handler.dart
+++ b/pkg/dartdev/lib/src/vm_interop_handler.dart
@@ -5,6 +5,8 @@
import 'dart:isolate';
/// Contains methods used to communicate DartDev results back to the VM.
+///
+/// Messages are received in runtime/bin/dartdev_isolate.cc.
abstract class VmInteropHandler {
/// Initializes [VmInteropHandler] to utilize [port] to communicate with the
/// VM.
diff --git a/pkg/dartdev/pubspec.yaml b/pkg/dartdev/pubspec.yaml
index 0b5636b..25f5eeb 100644
--- a/pkg/dartdev/pubspec.yaml
+++ b/pkg/dartdev/pubspec.yaml
@@ -19,7 +19,10 @@
dartdoc: any
dds: any
front_end: any
+ logging: any
meta: any
+ native_assets_builder: any
+ native_assets_cli: any
package_config: any
path: any
pub: any
diff --git a/pkg/dartdev/test/native_assets/build_test.dart b/pkg/dartdev/test/native_assets/build_test.dart
new file mode 100644
index 0000000..dc8ce07
--- /dev/null
+++ b/pkg/dartdev/test/native_assets/build_test.dart
@@ -0,0 +1,42 @@
+// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// @dart=2.18
+
+import 'dart:io';
+
+import 'package:test/test.dart';
+
+import '../utils.dart';
+import 'helpers.dart';
+
+void main(List<String> args) async {
+ final bool fromDartdevSource = args.contains('--source');
+
+ test('dart build', timeout: longTimeout, () async {
+ await nativeAssetsTest('dart_app', (dartAppUri) async {
+ await runDart(
+ arguments: [
+ '--enable-experiment=native-assets',
+ if (fromDartdevSource)
+ Platform.script.resolve('../../bin/dartdev.dart').toFilePath(),
+ 'build',
+ 'bin/dart_app.dart',
+ ],
+ workingDirectory: dartAppUri,
+ logger: logger,
+ );
+
+ final exeUri = dartAppUri.resolve('bin/dart_app/dart_app.exe');
+ expect(await File.fromUri(exeUri).exists(), true);
+ final result = await runProcess(
+ executable: exeUri,
+ arguments: [],
+ workingDirectory: dartAppUri,
+ logger: logger,
+ );
+ expectDartAppStdout(result.stdout);
+ });
+ });
+}
diff --git a/pkg/dartdev/test/native_assets/helpers.dart b/pkg/dartdev/test/native_assets/helpers.dart
new file mode 100644
index 0000000..36636e0
--- /dev/null
+++ b/pkg/dartdev/test/native_assets/helpers.dart
@@ -0,0 +1,179 @@
+// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:io';
+
+import 'package:logging/logging.dart';
+import 'package:native_assets_builder/src/utils/run_process.dart'
+ as run_process;
+import 'package:test/test.dart';
+import 'package:yaml/yaml.dart';
+
+extension UriExtension on Uri {
+ Uri get parent {
+ return File(toFilePath()).parent.uri;
+ }
+}
+
+const keepTempKey = 'KEEP_TEMPORARY_DIRECTORIES';
+
+Future<void> inTempDir(Future<void> Function(Uri tempUri) fun) async {
+ final tempDir = await Directory.systemTemp.createTemp();
+ try {
+ await fun(tempDir.uri);
+ } finally {
+ if (!Platform.environment.containsKey(keepTempKey) ||
+ Platform.environment[keepTempKey]!.isEmpty) {
+ await tempDir.delete(recursive: true);
+ }
+ }
+}
+
+/// Runs a [Process].
+///
+/// If [logger] is provided, stream stdout and stderr to it.
+///
+/// If [captureOutput], captures stdout and stderr.
+Future<run_process.RunProcessResult> runProcess({
+ required Uri executable,
+ List<String> arguments = const [],
+ Uri? workingDirectory,
+ Map<String, String>? environment,
+ bool includeParentEnvironment = true,
+ required Logger? logger,
+ bool captureOutput = true,
+ int expectedExitCode = 0,
+ bool throwOnUnexpectedExitCode = false,
+}) =>
+ run_process.runProcess(
+ executable: executable,
+ arguments: arguments,
+ workingDirectory: workingDirectory,
+ environment: environment,
+ includeParentEnvironment: includeParentEnvironment,
+ logger: logger,
+ captureOutput: captureOutput,
+ expectedExitCode: expectedExitCode,
+ throwOnUnexpectedExitCode: throwOnUnexpectedExitCode,
+ );
+
+Future<void> copyTestProjects(Uri copyTargetUri, Logger logger) async {
+ final pkgNativeAssetsBuilderUri =
+ Platform.script.resolve('../../../native_assets_builder/');
+ // Reuse the test projects from `pkg:native`.
+ final testProjectsUri =
+ pkgNativeAssetsBuilderUri.resolve('test/test_projects/');
+ final manifestUri = testProjectsUri.resolve('manifest.yaml');
+ final manifestFile = File.fromUri(manifestUri);
+ final manifestString = await manifestFile.readAsString();
+ final manifestYaml = loadYamlDocument(manifestString);
+ final manifest = [
+ for (final path in manifestYaml.contents as YamlList) Uri(path: path)
+ ];
+ final filesToCopy =
+ manifest.where((e) => e.pathSegments.last != 'pubspec.yaml').toList();
+ final filesToModify =
+ manifest.where((e) => e.pathSegments.last == 'pubspec.yaml').toList();
+
+ for (final pathToCopy in filesToCopy) {
+ final sourceFile = File.fromUri(testProjectsUri.resolveUri(pathToCopy));
+ final targetUri = copyTargetUri.resolveUri(pathToCopy);
+ final targetDirUri = targetUri.parent;
+ final targetDir = Directory.fromUri(targetDirUri);
+ if (!(await targetDir.exists())) {
+ await targetDir.create(recursive: true);
+ }
+ await sourceFile.copy(targetUri.toFilePath());
+ }
+ for (final pathToModify in filesToModify) {
+ final sourceFile = File.fromUri(testProjectsUri.resolveUri(pathToModify));
+ final targetUri = copyTargetUri.resolveUri(pathToModify);
+ final sourceString = await sourceFile.readAsString();
+ final modifiedString = sourceString.replaceAll(
+ 'path: ../../../',
+ 'path: ${pkgNativeAssetsBuilderUri.toFilePath().replaceAll('\\', '/')}',
+ );
+ await File.fromUri(targetUri).writeAsString(modifiedString);
+ }
+
+ // If we're copying `my_native_library/` we need to simulate that its
+ // native assets are pre-built
+ final myNativeLibraryUri = copyTargetUri.resolve('my_native_library/');
+ if (await Directory(myNativeLibraryUri.toFilePath()).exists()) {
+ await runPubGet(
+ workingDirectory: myNativeLibraryUri,
+ logger: logger,
+ );
+ await runDart(
+ arguments: ['tool/native.dart', 'build'],
+ workingDirectory: myNativeLibraryUri,
+ logger: logger,
+ );
+ }
+}
+
+Future<void> runPubGet({
+ required Uri workingDirectory,
+ required Logger logger,
+}) async {
+ final result = await runDart(
+ arguments: ['pub', 'get'],
+ workingDirectory: workingDirectory,
+ logger: logger,
+ );
+ expect(result.exitCode, 0);
+}
+
+void expectDartAppStdout(String stdout) {
+ expect(
+ stdout,
+ stringContainsInOrder(
+ [
+ 'add(5, 6) = 11',
+ 'subtract(5, 6) = -1',
+ ],
+ ),
+ );
+}
+
+/// Logger that outputs the full trace when a test fails.
+final logger = Logger('')
+ ..level = Level.ALL
+ ..onRecord.listen((record) {
+ printOnFailure('${record.level.name}: ${record.time}: ${record.message}');
+ });
+
+final dartExecutable = Uri.file(Platform.resolvedExecutable);
+
+Future<void> nativeAssetsTest(
+ String packageUnderTest,
+ Future<void> Function(Uri) fun,
+) async {
+ assert(const [
+ 'dart_app',
+ 'native_add',
+ ].contains(packageUnderTest));
+ return await inTempDir((tempUri) async {
+ await copyTestProjects(tempUri, logger);
+ final packageUri = tempUri.resolve('$packageUnderTest/');
+ await runPubGet(workingDirectory: packageUri, logger: logger);
+ return await fun(packageUri);
+ });
+}
+
+Future<run_process.RunProcessResult> runDart({
+ required List<String> arguments,
+ Uri? workingDirectory,
+ required Logger? logger,
+}) async {
+ final result = await runProcess(
+ executable: dartExecutable,
+ arguments: arguments,
+ workingDirectory: workingDirectory,
+ logger: logger,
+ );
+ expect(result.exitCode, 0);
+ return result;
+}
diff --git a/pkg/dartdev/test/native_assets/run_test.dart b/pkg/dartdev/test/native_assets/run_test.dart
new file mode 100644
index 0000000..beb3284
--- /dev/null
+++ b/pkg/dartdev/test/native_assets/run_test.dart
@@ -0,0 +1,52 @@
+// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// @dart=2.18
+
+import 'package:test/test.dart';
+
+import '../utils.dart';
+import 'helpers.dart';
+
+void main(List<String> args) async {
+ // No --source option, `dart run` from source does not output target program
+ // stdout.
+
+ test('dart run ', timeout: longTimeout, () async {
+ await nativeAssetsTest('dart_app', (dartAppUri) async {
+ final result = await runDart(
+ arguments: [
+ '--enable-experiment=native-assets',
+ 'run',
+ ],
+ workingDirectory: dartAppUri,
+ logger: logger,
+ );
+ expectDartAppStdout(result.stdout);
+ });
+ });
+
+ test('dart run test/xxx_test.dart', timeout: longTimeout, () async {
+ await nativeAssetsTest('native_add', (packageUri) async {
+ final result = await runDart(
+ arguments: [
+ '--enable-experiment=native-assets',
+ 'run',
+ 'test/native_add_test.dart',
+ ],
+ workingDirectory: packageUri,
+ logger: logger,
+ );
+ expect(
+ result.stdout,
+ stringContainsInOrder(
+ [
+ 'native add test',
+ 'All tests passed!',
+ ],
+ ),
+ );
+ });
+ });
+}
diff --git a/pkg/dartdev/test/native_assets/test_test.dart b/pkg/dartdev/test/native_assets/test_test.dart
new file mode 100644
index 0000000..23848d6
--- /dev/null
+++ b/pkg/dartdev/test/native_assets/test_test.dart
@@ -0,0 +1,60 @@
+// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// @dart=2.18
+
+import 'package:test/test.dart';
+
+import '../utils.dart';
+import 'helpers.dart';
+
+void main(List<String> args) async {
+ // No --source option, `dart run` from source does not output target program
+ // stdout.
+
+ test('dart test', timeout: longTimeout, () async {
+ await nativeAssetsTest('native_add', (packageUri) async {
+ final result = await runDart(
+ arguments: [
+ '--enable-experiment=native-assets',
+ 'test',
+ ],
+ workingDirectory: packageUri,
+ logger: logger,
+ );
+ expect(
+ result.stdout,
+ stringContainsInOrder(
+ [
+ 'native add test',
+ 'All tests passed!',
+ ],
+ ),
+ );
+ });
+ });
+
+ test('dart run test:test', timeout: longTimeout, () async {
+ await nativeAssetsTest('native_add', (packageUri) async {
+ final result = await runDart(
+ arguments: [
+ '--enable-experiment=native-assets',
+ 'run',
+ 'test:test',
+ ],
+ workingDirectory: packageUri,
+ logger: logger,
+ );
+ expect(
+ result.stdout,
+ stringContainsInOrder(
+ [
+ 'native add test',
+ 'All tests passed!',
+ ],
+ ),
+ );
+ });
+ });
+}
diff --git a/pkg/dartdev/test/utils.dart b/pkg/dartdev/test/utils.dart
index d562234..0c44883 100644
--- a/pkg/dartdev/test/utils.dart
+++ b/pkg/dartdev/test/utils.dart
@@ -223,7 +223,7 @@
///
/// Many of this package tests rely on having the SDK folder layout.
void ensureRunFromSdkBinDart() {
- final uri = Uri(path: Platform.resolvedExecutable);
+ final uri = Uri.file(Platform.resolvedExecutable);
final pathReversed = uri.pathSegments.reversed.toList();
if (!pathReversed[0].startsWith('dart')) {
throw StateError('Main executable is not Dart: ${uri.toFilePath()}.');
diff --git a/pkg/front_end/lib/src/api_prototype/experimental_flags_generated.dart b/pkg/front_end/lib/src/api_prototype/experimental_flags_generated.dart
index 5b660a1..28a2d1a1 100644
--- a/pkg/front_end/lib/src/api_prototype/experimental_flags_generated.dart
+++ b/pkg/front_end/lib/src/api_prototype/experimental_flags_generated.dart
@@ -171,6 +171,14 @@
experimentEnabledVersion: const Version(2, 17),
experimentReleasedVersion: const Version(2, 17));
+ static const ExperimentalFlag nativeAssets = const ExperimentalFlag(
+ name: 'native-assets',
+ isEnabledByDefault: false,
+ isExpired: false,
+ enabledVersion: const Version(3, 1),
+ experimentEnabledVersion: const Version(3, 1),
+ experimentReleasedVersion: const Version(3, 1));
+
static const ExperimentalFlag nonNullable = const ExperimentalFlag(
name: 'non-nullable',
isEnabledByDefault: true,
@@ -382,6 +390,10 @@
GlobalFeature get namedArgumentsAnywhere => _namedArgumentsAnywhere ??=
_computeGlobalFeature(ExperimentalFlag.namedArgumentsAnywhere);
+ GlobalFeature? _nativeAssets;
+ GlobalFeature get nativeAssets =>
+ _nativeAssets ??= _computeGlobalFeature(ExperimentalFlag.nativeAssets);
+
GlobalFeature? _nonNullable;
GlobalFeature get nonNullable =>
_nonNullable ??= _computeGlobalFeature(ExperimentalFlag.nonNullable);
@@ -526,6 +538,11 @@
canonicalUri,
libraryVersion);
+ LibraryFeature? _nativeAssets;
+ LibraryFeature get nativeAssets =>
+ _nativeAssets ??= globalFeatures._computeLibraryFeature(
+ ExperimentalFlag.nativeAssets, canonicalUri, libraryVersion);
+
LibraryFeature? _nonNullable;
LibraryFeature get nonNullable =>
_nonNullable ??= globalFeatures._computeLibraryFeature(
@@ -625,6 +642,8 @@
return macros;
case shared.ExperimentalFlag.namedArgumentsAnywhere:
return namedArgumentsAnywhere;
+ case shared.ExperimentalFlag.nativeAssets:
+ return nativeAssets;
case shared.ExperimentalFlag.nonNullable:
return nonNullable;
case shared.ExperimentalFlag.nonfunctionTypeAliases:
@@ -690,6 +709,8 @@
return ExperimentalFlag.macros;
case "named-arguments-anywhere":
return ExperimentalFlag.namedArgumentsAnywhere;
+ case "native-assets":
+ return ExperimentalFlag.nativeAssets;
case "non-nullable":
return ExperimentalFlag.nonNullable;
case "nonfunction-type-aliases":
@@ -749,6 +770,8 @@
ExperimentalFlag.macros: ExperimentalFlag.macros.isEnabledByDefault,
ExperimentalFlag.namedArgumentsAnywhere:
ExperimentalFlag.namedArgumentsAnywhere.isEnabledByDefault,
+ ExperimentalFlag.nativeAssets:
+ ExperimentalFlag.nativeAssets.isEnabledByDefault,
ExperimentalFlag.nonNullable: ExperimentalFlag.nonNullable.isEnabledByDefault,
ExperimentalFlag.nonfunctionTypeAliases:
ExperimentalFlag.nonfunctionTypeAliases.isEnabledByDefault,
@@ -974,6 +997,7 @@
shared.ExperimentalFlag.macros: ExperimentalFlag.macros,
shared.ExperimentalFlag.namedArgumentsAnywhere:
ExperimentalFlag.namedArgumentsAnywhere,
+ shared.ExperimentalFlag.nativeAssets: ExperimentalFlag.nativeAssets,
shared.ExperimentalFlag.nonNullable: ExperimentalFlag.nonNullable,
shared.ExperimentalFlag.nonfunctionTypeAliases:
ExperimentalFlag.nonfunctionTypeAliases,
diff --git a/pkg/native_assets_builder/lib/src/build_runner/build_runner.dart b/pkg/native_assets_builder/lib/src/build_runner/build_runner.dart
index 6e979c8..6388d00 100644
--- a/pkg/native_assets_builder/lib/src/build_runner/build_runner.dart
+++ b/pkg/native_assets_builder/lib/src/build_runner/build_runner.dart
@@ -137,7 +137,7 @@
}
await runProcess(
workingDirectory: workingDirectory,
- executable: dartExecutable.toFilePath(),
+ executable: dartExecutable,
arguments: [
'--packages=${packageConfigUri.toFilePath()}',
buildDotDart.toFilePath(),
@@ -145,6 +145,8 @@
],
logger: logger,
includeParentEnvironment: includeParentEnvironment,
+ expectedExitCode: 0,
+ throwOnUnexpectedExitCode: true,
);
final buildOutput = await BuildOutput.readFromFile(outDir: outDir);
setMetadata(config.target, config.packageName, buildOutput?.metadata);
diff --git a/pkg/native_assets_builder/lib/src/utils/run_process.dart b/pkg/native_assets_builder/lib/src/utils/run_process.dart
index b557bb5..6a34b3ff 100644
--- a/pkg/native_assets_builder/lib/src/utils/run_process.dart
+++ b/pkg/native_assets_builder/lib/src/utils/run_process.dart
@@ -8,17 +8,22 @@
import 'package:logging/logging.dart';
-/// Runs a process async and captures the exit code and standard out.
+/// Runs a [Process].
///
-/// Supports streaming output using a [Logger].
+/// If [logger] is provided, stream stdout and stderr to it.
+///
+/// If [captureOutput], captures stdout and stderr.
+// TODO(dacoharkes): Share between package:c_compiler and here.
Future<RunProcessResult> runProcess({
- required String executable,
- required List<String> arguments,
+ required Uri executable,
+ List<String> arguments = const [],
Uri? workingDirectory,
Map<String, String>? environment,
bool includeParentEnvironment = true,
- bool throwOnFailure = true,
- required Logger logger,
+ required Logger? logger,
+ bool captureOutput = true,
+ int expectedExitCode = 0,
+ bool throwOnUnexpectedExitCode = false,
}) async {
if (Platform.isWindows && !includeParentEnvironment) {
const winEnvKeys = [
@@ -39,18 +44,18 @@
final commandString = [
if (printWorkingDir) '(cd ${workingDirectory.toFilePath()};',
...?environment?.entries.map((entry) => '${entry.key}=${entry.value}'),
- executable,
+ executable.toFilePath(),
...arguments.map((a) => a.contains(' ') ? "'$a'" : a),
if (printWorkingDir) ')',
].join(' ');
- logger.info('Running `$commandString`.');
+ logger?.info('Running `$commandString`.');
- final stdoutBuffer = <String>[];
- final stderrBuffer = <String>[];
+ final stdoutBuffer = StringBuffer();
+ final stderrBuffer = StringBuffer();
final stdoutCompleter = Completer<Object?>();
final stderrCompleter = Completer<Object?>();
- final Process process = await Process.start(
- executable,
+ final process = await Process.start(
+ executable.toFilePath(),
arguments,
workingDirectory: workingDirectory?.toFilePath(),
environment: environment,
@@ -60,66 +65,60 @@
process.stdout.transform(utf8.decoder).transform(const LineSplitter()).listen(
(s) {
- logger.fine(s);
- stdoutBuffer.add(s);
+ logger?.fine(s);
+ if (captureOutput) stdoutBuffer.writeln(s);
},
onDone: stdoutCompleter.complete,
);
process.stderr.transform(utf8.decoder).transform(const LineSplitter()).listen(
(s) {
- logger.severe(s);
- stderrBuffer.add(s);
+ logger?.severe(s);
+ if (captureOutput) stderrBuffer.writeln(s);
},
onDone: stderrCompleter.complete,
);
- final int exitCode = await process.exitCode;
+ final exitCode = await process.exitCode;
await stdoutCompleter.future;
- final String stdout = stdoutBuffer.join();
await stderrCompleter.future;
- final String stderr = stderrBuffer.join();
final result = RunProcessResult(
pid: process.pid,
- command: '$executable ${arguments.join(' ')}',
+ command: commandString,
exitCode: exitCode,
- stdout: stdout,
- stderr: stderr,
+ stdout: stdoutBuffer.toString(),
+ stderr: stderrBuffer.toString(),
);
- if (throwOnFailure && result.exitCode != 0) {
- throw ProcessInvocationException(result);
+ if (throwOnUnexpectedExitCode && expectedExitCode != exitCode) {
+ throw ProcessException(
+ executable.toFilePath(),
+ arguments,
+ "Full command string: '$commandString'.\n"
+ "Exit code: '$exitCode'.\n"
+ 'For the output of the process check the logger output.',
+ );
}
return result;
}
-class RunProcessResult extends ProcessResult {
+/// Drop in replacement of [ProcessResult].
+class RunProcessResult {
+ final int pid;
+
final String command;
- final int _exitCode;
+ final int exitCode;
- // For some reason super.exitCode returns 0.
- @override
- int get exitCode => _exitCode;
+ final String stderr;
- final String _stderrString;
-
- @override
- String get stderr => _stderrString;
-
- final String _stdoutString;
-
- @override
- String get stdout => _stdoutString;
+ final String stdout;
RunProcessResult({
- required int pid,
+ required this.pid,
required this.command,
- required int exitCode,
- required String stderr,
- required String stdout,
- }) : _exitCode = exitCode,
- _stderrString = stderr,
- _stdoutString = stdout,
- super(pid, exitCode, stdout, stderr);
+ required this.exitCode,
+ required this.stderr,
+ required this.stdout,
+ });
@override
String toString() => '''command: $command
@@ -127,15 +126,3 @@
stdout: $stdout
stderr: $stderr''';
}
-
-class ProcessInvocationException implements Exception {
- final RunProcessResult runProcessResult;
-
- ProcessInvocationException(this.runProcessResult);
-
- String get message => '''A process run failed.
-$runProcessResult''';
-
- @override
- String toString() => message;
-}
diff --git a/pkg/native_assets_builder/pubspec.yaml b/pkg/native_assets_builder/pubspec.yaml
index eb130dd..449f602 100644
--- a/pkg/native_assets_builder/pubspec.yaml
+++ b/pkg/native_assets_builder/pubspec.yaml
@@ -5,7 +5,7 @@
publish_to: none
environment:
- sdk: ">=2.17.0 <3.0.0"
+ sdk: ">=3.0.0 <4.0.0"
# Use 'any' constraints here; we get our versions from the DEPS file.
dependencies:
diff --git a/pkg/native_assets_builder/test/build_runner/build_planner_test.dart b/pkg/native_assets_builder/test/build_runner/build_planner_test.dart
index 2af502a..b9b920d 100644
--- a/pkg/native_assets_builder/test/build_runner/build_planner_test.dart
+++ b/pkg/native_assets_builder/test/build_runner/build_planner_test.dart
@@ -21,7 +21,7 @@
await runPubGet(workingDirectory: nativeAddUri, logger: logger);
final result = await runProcess(
- executable: Platform.resolvedExecutable,
+ executable: Uri.file(Platform.resolvedExecutable),
arguments: [
'pub',
'deps',
diff --git a/pkg/native_assets_builder/test/build_runner/helpers.dart b/pkg/native_assets_builder/test/build_runner/helpers.dart
index 443966a..b0b2c03 100644
--- a/pkg/native_assets_builder/test/build_runner/helpers.dart
+++ b/pkg/native_assets_builder/test/build_runner/helpers.dart
@@ -17,7 +17,7 @@
required Logger logger,
}) async {
final result = await runProcess(
- executable: Platform.resolvedExecutable,
+ executable: Uri.file(Platform.resolvedExecutable),
arguments: ['pub', 'get'],
workingDirectory: workingDirectory,
logger: logger,
@@ -78,7 +78,7 @@
if (Platform.isLinux) {
final assetUri = (asset.path as AssetAbsolutePath).uri;
final nmResult = await runProcess(
- executable: 'nm',
+ executable: Uri(path: 'nm'),
arguments: [
'-D',
assetUri.toFilePath(),
diff --git a/pkg/native_assets_builder/test/helpers.dart b/pkg/native_assets_builder/test/helpers.dart
index 342bc4b..4168590 100644
--- a/pkg/native_assets_builder/test/helpers.dart
+++ b/pkg/native_assets_builder/test/helpers.dart
@@ -3,10 +3,11 @@
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
-import 'dart:convert';
import 'dart:io';
import 'package:logging/logging.dart';
+import 'package:native_assets_builder/src/utils/run_process.dart'
+ as run_process;
import 'package:test/test.dart';
import 'package:yaml/yaml.dart';
@@ -36,106 +37,33 @@
}
}
-/// Runs a process async and captures the exit code and standard out.
-Future<RunProcessResult> runProcess({
- required String executable,
- required List<String> arguments,
+/// Runs a [Process].
+///
+/// If [logger] is provided, stream stdout and stderr to it.
+///
+/// If [captureOutput], captures stdout and stderr.
+Future<run_process.RunProcessResult> runProcess({
+ required Uri executable,
+ List<String> arguments = const [],
Uri? workingDirectory,
Map<String, String>? environment,
- bool throwOnFailure = true,
- required Logger logger,
-}) async {
- final printWorkingDir =
- workingDirectory != null && workingDirectory != Directory.current.uri;
- final commandString = [
- if (printWorkingDir) '(cd ${workingDirectory.toFilePath()};',
- ...?environment?.entries.map((entry) => '${entry.key}=${entry.value}'),
- executable,
- ...arguments.map((a) => a.contains(' ') ? "'$a'" : a),
- if (printWorkingDir) ')',
- ].join(' ');
-
- logger.info('Running `$commandString`.');
-
- final stdoutBuffer = <String>[];
- final stderrBuffer = <String>[];
- final stdoutCompleter = Completer<Object?>();
- final stderrCompleter = Completer<Object?>();
- final Process process = await Process.start(
- executable,
- arguments,
- workingDirectory: workingDirectory?.toFilePath(),
- environment: environment,
- );
-
- process.stdout.transform(utf8.decoder).listen(
- (s) {
- logger.fine(' $s');
- stdoutBuffer.add(s);
- },
- onDone: stdoutCompleter.complete,
- );
- process.stderr.transform(utf8.decoder).listen(
- (s) {
- logger.shout(' $s');
- stderrBuffer.add(s);
- },
- onDone: stderrCompleter.complete,
- );
-
- final int exitCode = await process.exitCode;
- await stdoutCompleter.future;
- final String stdout = stdoutBuffer.join();
- await stderrCompleter.future;
- final String stderr = stderrBuffer.join();
- final result = RunProcessResult(
- pid: process.pid,
- command: '$executable ${arguments.join(' ')}',
- exitCode: exitCode,
- stdout: stdout,
- stderr: stderr,
- );
- if (throwOnFailure && result.exitCode != 0) {
- throw result;
- }
- return result;
-}
-
-class RunProcessResult extends ProcessResult {
- final String command;
-
- final int _exitCode;
-
- @override
- int get exitCode => _exitCode;
-
- final String _stderrString;
-
- @override
- String get stderr => _stderrString;
-
- final String _stdoutString;
-
- @override
- String get stdout => _stdoutString;
-
- RunProcessResult({
- required int pid,
- required this.command,
- required int exitCode,
- required String stderr,
- required String stdout,
- }) : _exitCode = exitCode,
- _stderrString = stderr,
- _stdoutString = stdout,
- super(pid, exitCode, stdout, stderr);
-
- @override
- String toString() => '''command: $command
-exitCode: $exitCode
-stdout: $stdout
-stderr: $stderr''';
-}
+ bool includeParentEnvironment = true,
+ required Logger? logger,
+ bool captureOutput = true,
+ int expectedExitCode = 0,
+ bool throwOnUnexpectedExitCode = false,
+}) =>
+ run_process.runProcess(
+ executable: executable,
+ arguments: arguments,
+ workingDirectory: workingDirectory,
+ environment: environment,
+ includeParentEnvironment: includeParentEnvironment,
+ logger: logger,
+ captureOutput: captureOutput,
+ expectedExitCode: expectedExitCode,
+ throwOnUnexpectedExitCode: throwOnUnexpectedExitCode,
+ );
final pkgNativeAssetsBuilderUri = Platform.script.resolve('../../');
final testProjectsUri =
diff --git a/tools/experimental_features.yaml b/tools/experimental_features.yaml
index a27cac0..d7c5fad 100644
--- a/tools/experimental_features.yaml
+++ b/tools/experimental_features.yaml
@@ -133,6 +133,9 @@
inline-class:
help: "Inline class"
+ native-assets:
+ help: "Compile and bundle native assets."
+
# Experiment flag only used for testing.
test-experiment:
help: >-