blob: 351be14d45f499300adf89183b1b4a8bcb362714 [file] [log] [blame] [edit]
// 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:code_assets/code_assets.dart';
import 'package:dart2native/generate.dart';
import 'package:dartdev/src/commands/compile.dart';
import 'package:dartdev/src/experiments.dart';
import 'package:dartdev/src/native_assets_bundling.dart';
import 'package:dartdev/src/native_assets_macos.dart';
import 'package:dartdev/src/sdk.dart';
import 'package:front_end/src/api_prototype/compiler_options.dart'
show Verbosity;
import 'package:hooks_runner/hooks_runner.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';
static const int genericErrorExitCode = 255;
final bool recordUseEnabled;
BuildCommand({bool verbose = false, required this.recordUseEnabled})
: super(cmdName, 'Build a Dart application including native assets.',
verbose) {
addSubcommand(BuildCliSubcommand(
verbose: verbose,
recordUseEnabled: recordUseEnabled,
));
}
@override
CommandCategory get commandCategory => CommandCategory.project;
}
/// Subcommand for `dart build cli`.
///
/// Expects [Directory.current] to contain a Dart project with a bin/ directory.
class BuildCliSubcommand extends CompileSubcommandCommand {
final bool recordUseEnabled;
static const String cmdName = 'cli';
static final OS targetOS = OS.current;
late final List<File> entryPoints;
BuildCliSubcommand({bool verbose = false, required this.recordUseEnabled})
: super(
cmdName,
'''Build a Dart application with a command line interface (CLI).
The resulting CLI app bundle is structured in the following manner:
bundle/
bin/
<executable>
lib/
<dynamic libraries>
''',
verbose) {
final binDirectory =
Directory.fromUri(Directory.current.uri.resolve('bin/'));
final outputDirectoryDefault = Directory.fromUri(Directory.current.uri
.resolve('build/cli/${OS.current}_${Architecture.current}/'));
entryPoints = binDirectory.existsSync()
? binDirectory
.listSync()
.whereType<File>()
.where((e) => e.path.endsWith('dart'))
.toList()
: [];
argParser
..addOption(
'output',
abbr: 'o',
help: '''
Write the output to <output>/bundle/.
This can be an absolute or relative path.
''',
valueHelp: 'path',
defaultsTo: path
.relative(outputDirectoryDefault.path, from: Directory.current.path)
.makeFolder(),
)
..addOption(
'target',
abbr: 't',
help: '''The main entry-point file of the command-line application.
Must be a Dart file in the bin/ directory.
If the "--target" option is omitted, and there is a single Dart file in bin/,
then that is used instead.''',
valueHelp: 'path',
defaultsTo: entryPoints.length == 1
? path.relative(entryPoints.single.path,
from: Directory.current.path)
: null,
)
..addOption(
'verbosity',
help: 'Sets the verbosity level of the compilation.',
valueHelp: 'level',
defaultsTo: Verbosity.defaultValue,
allowed: Verbosity.allowedValues,
allowedHelp: Verbosity.allowedValuesHelp,
)
..addExperimentalFlags(verbose: verbose);
}
@override
Future<int> run() async {
if (!checkArtifactExists(sdk.genKernelSnapshot) ||
!checkArtifactExists(sdk.genSnapshot) ||
!checkArtifactExists(sdk.dartAotRuntime) ||
!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!;
var target = args.option('target');
if (target == null) {
stderr.write(
'There are multiple possible targets in the `bin/` directory, '
"and the 'target' argument wasn't specified.",
);
return 255;
}
final sourceUri =
File.fromUri(Uri.file(target).normalizePath()).absolute.uri;
if (!checkFile(sourceUri.toFilePath())) {
return genericErrorExitCode;
}
final outputUri = Uri.directory(
args.option('output')?.normalizeCanonicalizePath().makeFolder() ??
sourceUri.toFilePath().removeDotDart().makeFolder(),
);
if (await File.fromUri(outputUri.resolve('pubspec.yaml')).exists()) {
stderr.writeln("'dart build' refuses to delete your project.");
stderr.writeln('Requested output directory: ${outputUri.toFilePath()}');
return 128;
}
final verbosity = args.option('verbosity')!;
final enabledExperiments = args.enabledExperiments;
stdout.writeln('''The `dart build cli` command is in preview at the moment.
See documentation on https://dart.dev/interop/c-interop#native-assets.
''');
final packageConfigUri = await DartNativeAssetsBuilder.ensurePackageConfig(
sourceUri,
);
final pubspecUri =
await DartNativeAssetsBuilder.findWorkspacePubspec(packageConfigUri);
final executableName = path.basenameWithoutExtension(sourceUri.path);
return await doBuild(
executables: [(name: executableName, sourceEntryPoint: sourceUri)],
enabledExperiments: enabledExperiments,
outputUri: outputUri,
packageConfigUri: packageConfigUri!,
pubspecUri: pubspecUri,
recordUseEnabled: recordUseEnabled,
verbose: verbose,
verbosity: verbosity,
);
}
static Future<int> doBuild({
required DartBuildExecutables executables,
required Uri outputUri,
required Uri packageConfigUri,
required Uri? pubspecUri,
required bool recordUseEnabled,
required List<String> enabledExperiments,
required bool verbose,
required String verbosity,
}) async {
if (executables.length >= 2 && recordUseEnabled) {
// Multiple entry points can lead to multiple different tree-shakings.
// We either need to generate a new entry point that combines all entry
// points and combine that into a single executable and have wrappers
// around that executable. Or, we need to merge the recorded uses for the
// various entrypoints. The former will lead to smaller bundle-size
// overall.
stderr.writeln(
'Multiple executables together with record use is not yet supported.',
);
return 255;
}
final outputDir = Directory.fromUri(outputUri);
if (await outputDir.exists()) {
stdout.writeln('Deleting output directory: ${outputUri.toFilePath()}.');
try {
await outputDir.delete(recursive: true);
} on PathAccessException {
stderr.writeln(
'Failed to delete: ${outputUri.toFilePath()}. '
'The application might be in use.',
);
return 255;
}
}
// Place the bundle in a subdir so that we can potentially put debug symbols
// next to it.
final bundleDirectory = Directory.fromUri(outputUri.resolve('bundle/'));
final binDirectory = Directory.fromUri(bundleDirectory.uri.resolve('bin/'));
await binDirectory.create(recursive: true);
stdout.writeln('Building native assets.');
final packageConfig =
await DartNativeAssetsBuilder.loadPackageConfig(packageConfigUri);
if (packageConfig == null) {
return compileErrorExitCode;
}
final runPackageName = await DartNativeAssetsBuilder.findRootPackageName(
executables.first.sourceEntryPoint,
);
pubspecUri ??=
await DartNativeAssetsBuilder.findWorkspacePubspec(packageConfigUri);
final builder = DartNativeAssetsBuilder(
pubspecUri: pubspecUri,
packageConfigUri: packageConfigUri,
packageConfig: packageConfig,
runPackageName: runPackageName!,
includeDevDependencies: false,
verbose: verbose,
);
final buildResult = await builder.buildNativeAssetsAOT();
if (buildResult == null) {
stderr.writeln('Native assets build failed.');
return 255;
}
final tempDir = Directory.systemTemp.createTempSync();
try {
var first = true;
Uri? nativeAssetsYamlUri;
LinkResult? linkResult;
for (final e in executables) {
String? recordedUsagesPath;
if (recordUseEnabled) {
recordedUsagesPath = path.join(tempDir.path, 'recorded_usages.json');
}
final outputExeUri = binDirectory.uri.resolve(
targetOS.executableFileName(e.name),
);
final generator = KernelGenerator(
genSnapshot: sdk.genSnapshot,
targetDartAotRuntime: sdk.dartAotRuntime,
kind: Kind.exe,
sourceFile: e.sourceEntryPoint.toFilePath(),
outputFile: outputExeUri.toFilePath(),
verbose: verbose,
verbosity: verbosity,
defines: [],
packages: packageConfigUri.toFilePath(),
targetOS: targetOS,
enableExperiment: enabledExperiments.join(','),
tempDir: tempDir,
);
final snapshotGenerator = await generator.generate(
recordedUsagesFile: recordedUsagesPath,
);
if (first) {
// Multiple executables are only supported with recorded uses
// disabled, so don't re-invoke link hooks.
linkResult = await builder.linkNativeAssetsAOT(
recordedUsagesPath: recordedUsagesPath,
buildResult: buildResult,
);
}
if (linkResult == null) {
stderr.writeln('Native assets link failed.');
return 255;
}
final allAssets = [
...buildResult.encodedAssets,
...linkResult.encodedAssets
];
final staticAssets = allAssets
.where((e) => e.isCodeAsset)
.map(CodeAsset.fromEncoded)
.where((e) => e.linkMode == StaticLinking());
if (staticAssets.isNotEmpty) {
stderr.write(
"""'dart build' does not yet support CodeAssets with static linking.
Use linkMode as dynamic library instead.""");
return 255;
}
if (allAssets.isNotEmpty && first) {
// Without tree-shaking, the assets after linking must be identical
// for all entry points.
final kernelAssets = await bundleNativeAssets(
allAssets,
builder.target,
binDirectory.uri,
relocatable: true,
verbose: true,
);
nativeAssetsYamlUri =
await writeNativeAssetsYaml(kernelAssets, tempDir.uri);
}
await snapshotGenerator.generate(
nativeAssets: nativeAssetsYamlUri?.toFilePath(),
);
if (targetOS == OS.macOS) {
// The dylibs are opened with a relative path to the executable.
// MacOS prevents opening dylibs that are not on the include path.
await rewriteInstallPath(outputExeUri);
}
first = false;
}
} finally {
await tempDir.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$'), '');
}
/// The executables to build in a `dart build cli` app bundle.
///
/// All entry points must be in the same package.
///
/// The names are typically taken from the `executables` section of the
/// `pubspec.yaml` file.
///
/// Recorded usages and multiple executables are not supported yet.
typedef DartBuildExecutables = List<({String name, Uri sourceEntryPoint})>;