blob: d93848153514366cdcf2ce7e53d276773eaf71f0 [file] [log] [blame]
// 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:dartdev/src/utils.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:native_assets_cli/native_assets_cli_internal.dart';
import 'package:path/path.dart' as path;
import 'package:vm/target_os.dart'; // For possible --target-os values.
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';
static const String formatOptionName = 'format';
static const int genericErrorExitCode = 255;
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('target-os',
help: 'Compile to a specific target operating system.',
allowed: TargetOS.names)
..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 genericErrorExitCode;
}
final outputUri = Uri.directory(
args.option(outputOptionName)?.normalizeCanonicalizePath().makeFolder() ??
sourceUri.toFilePath().removeDotDart().makeFolder(),
);
final format = Kind.values.byName(args.option(formatOptionName)!);
final outputExeUri = outputUri.resolve(
format.appendFileExtension(
sourceUri.pathSegments.last.split('.').first,
),
);
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
// targetOS is allowed to be null for a platform-independent snapshot
// or a different platform than the host.
} else if (targetOS == null) {
targetOS = Platform.operatingSystem;
} else if (targetOS != Platform.operatingSystem) {
stderr.writeln(
"'dart build -f ${format.name}' does not support cross-OS compilation.");
stderr.writeln('Host OS: ${Platform.operatingSystem}');
stderr.writeln('Target OS: $targetOS');
return 128;
}
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);
// Start native asset generation here.
stdout.writeln('Building native assets.');
final workingDirectory = Directory.current.uri;
final target = Target.current;
final targetMacOSVersion =
target.os == OS.macOS ? minimumSupportedMacOSVersion : null;
final nativeAssetsBuildRunner = NativeAssetsBuildRunner(
dartExecutable: Uri.file(sdk.dart),
logger: logger(verbose),
);
final buildResult = await nativeAssetsBuildRunner.build(
workingDirectory: workingDirectory,
target: target,
linkModePreference: LinkModePreferenceImpl.dynamic,
buildMode: BuildModeImpl.release,
includeParentEnvironment: true,
supportedAssetTypes: [
NativeCodeAsset.type,
],
targetMacOSVersion: targetMacOSVersion,
);
if (!buildResult.success) {
stderr.writeln('Native assets build failed.');
return 255;
}
// End native asset generation here.
final tempDir = Directory.systemTemp.createTempSync();
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,
linkModePreference: LinkModePreferenceImpl.dynamic,
buildMode: BuildModeImpl.release,
includeParentEnvironment: true,
buildResult: buildResult,
targetMacOSVersion: targetMacOSVersion,
supportedAssetTypes: [
NativeCodeAsset.type,
],
);
if (!linkResult.success) {
stderr.writeln('Native assets link failed.');
return 255;
}
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);
}
}
await snapshotGenerator.generate(
nativeAssets: assetsDartUri?.toFilePath(),
);
// End linking here.
} finally {
await tempDir.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 {
String normalizeCanonicalizePath() => path.canonicalize(path.normalize(this));
String makeFolder() => endsWith('\\') || endsWith('/') ? this : '$this/';
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 {
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;
}
}