| // 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})>; |