blob: 79ed3b6ecb0d9f36ac6573df7d787e7f6b3203e5 [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:dartdev/src/native_assets_bundling.dart';
import 'package:dartdev/src/sdk.dart';
import 'package:dartdev/src/utils.dart';
import 'package:data_assets/data_assets.dart';
import 'package:file/local.dart';
import 'package:hooks/hooks.dart';
import 'package:hooks_runner/hooks_runner.dart';
import 'package:logging/logging.dart';
import 'package:package_config/package_config.dart' as package_config;
import 'package:yaml/yaml.dart' show loadYaml;
import 'core.dart';
class DartNativeAssetsBuilder {
final Uri? pubspecUri;
final Uri packageConfigUri;
final package_config.PackageConfig packageConfig;
final String runPackageName;
final bool includeDevDependencies;
final bool verbose;
static const _fileSystem = LocalFileSystem();
late final Future<PackageLayout> _packageLayout = () async {
return PackageLayout.fromPackageConfig(
_fileSystem,
packageConfig,
packageConfigUri,
runPackageName,
includeDevDependencies: includeDevDependencies,
);
}();
late 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.WARNING.value ||
verbose && levelValue >= Level.INFO.value) {
log.stdout(record.message);
} else {
// Note, this is ignored by default.
log.trace(record.message);
}
});
late final Future<NativeAssetsBuildRunner> _nativeAssetsBuildRunner =
() async {
return NativeAssetsBuildRunner(
// This always runs in JIT mode.
dartExecutable: Uri.file(sdk.dart),
logger: _logger,
fileSystem: const LocalFileSystem(),
packageLayout: await _packageLayout,
userDefines: UserDefines(workspacePubspec: pubspecUri),
);
}();
DartNativeAssetsBuilder({
this.pubspecUri,
required this.packageConfigUri,
required this.packageConfig,
required this.runPackageName,
required this.includeDevDependencies,
required this.verbose,
Target? target,
}) : target = target ?? Target.current;
/// Compiles all native assets for host OS in JIT mode.
///
/// If provided, only native assets of all transitive dependencies of
/// [runPackageName] are built.
Future<List<EncodedAsset>?> compileNativeAssetsJit() async {
final buildResult = await _buildNativeAssetsShared(linkingEnabled: false);
if (buildResult == null) return null;
return buildResult.encodedAssets;
}
/// Compiles all native assets for host OS in JIT mode, and creates the
/// native assets yaml file.
///
/// If provided, only native assets of all transitive dependencies of
/// [runPackageName] are built.
///
/// Used in `dart run` and `dart test`.
Future<Uri?> compileNativeAssetsJitYamlFile() async {
final assets = await compileNativeAssetsJit();
if (assets == null) return null;
final dartToolUri = Directory.current.uri.resolve('.dart_tool/');
final outputUri = dartToolUri.resolve('native_assets/');
await Directory.fromUri(outputUri).create(recursive: true);
final kernelAssets = await bundleNativeAssets(
assets,
target,
outputUri,
relocatable: false,
);
return await writeNativeAssetsYaml(
kernelAssets,
dartToolUri,
header: '''# Native assets mapping for host OS in JIT mode.
# Generated by dartdev and package:hooks_runner.
''',
);
}
Future<bool> warnOnNativeAssets() async {
final builder = await _nativeAssetsBuildRunner;
final packageNames = await builder.packagesWithBuildHooks();
if (packageNames.isEmpty) return false;
log.stderr(
'Package(s) $packageNames require the native assets feature to be enabled. '
'Enable native assets with `--enable-experiment=native-assets`.',
);
return true;
}
late final _extensions = [
CodeAssetExtension(
targetOS: target.os,
linkModePreference: LinkModePreference.dynamic,
targetArchitecture: target.architecture,
macOS: _macOSConfig,
cCompiler: _cCompilerConfig,
),
// TODO(dacoharkes,mosum): This should be gated behind a data-assets
// experiment flag.
DataAssetsExtension(),
];
Future<BuildResult?> _buildNativeAssetsShared({
required bool linkingEnabled,
}) async {
final builder = await _nativeAssetsBuildRunner;
final buildResult = await builder.build(
extensions: _extensions,
linkingEnabled: linkingEnabled,
);
if (buildResult.isFailure) return null;
return buildResult.success;
}
Future<BuildResult?> buildNativeAssetsAOT() {
return _buildNativeAssetsShared(linkingEnabled: true);
}
Future<LinkResult?> linkNativeAssetsAOT({
required String? recordedUsagesPath,
required BuildResult buildResult,
}) async {
final builder = await _nativeAssetsBuildRunner;
final linkResult = await builder.link(
extensions: _extensions,
resourceIdentifiers:
recordedUsagesPath != null ? Uri.file(recordedUsagesPath) : null,
buildResult: buildResult,
);
if (linkResult.isFailure) return null;
return linkResult.success;
}
final Target target;
late final _macOSConfig = target.os == OS.macOS
? MacOSCodeConfig(targetVersion: minimumSupportedMacOSVersion)
: null;
late final _cCompilerConfig = _getCCompilerConfig(target.os);
CCompilerConfig? _getCCompilerConfig(OS targetOS) {
// Specifically for running our tests on Dart CI with the test runner, we
// recognize specific variables to setup the C Compiler configuration.
final env = Platform.environment;
final cc = env['DART_HOOK_TESTING_C_COMPILER__CC'];
final ar = env['DART_HOOK_TESTING_C_COMPILER__AR'];
final ld = env['DART_HOOK_TESTING_C_COMPILER__LD'];
final envScript = env['DART_HOOK_TESTING_C_COMPILER__ENV_SCRIPT'];
final envScriptArgs =
env['DART_HOOK_TESTING_C_COMPILER__ENV_SCRIPT_ARGUMENTS']
?.split(' ')
.map((arg) => arg.trim())
.where((arg) => arg.isNotEmpty)
.toList();
if (cc != null && ar != null && ld != null) {
return CCompilerConfig(
archiver: Uri.file(ar),
compiler: Uri.file(cc),
linker: Uri.file(ld),
windows: targetOS == OS.windows
? WindowsCCompilerConfig(
developerCommandPrompt: envScript == null
? null
: DeveloperCommandPrompt(
script: Uri.file(envScript),
arguments: envScriptArgs ?? [],
),
)
: null,
);
}
return null;
}
/// Runs `pub get` if no package config can be found.
///
/// Returns `null` if no package config can be found, even after pub get.
static Future<Uri?> ensurePackageConfig(Uri uri) async {
var packageConfig = await _findPackageConfigUri(uri);
// TODO(https://github.com/dart-lang/package_config/issues/126): Use
// package config resolution from package:package_config.
if (packageConfig == null) {
final pubspecMaybe = await findPubspec(uri);
if (pubspecMaybe != null) {
// Silently run `pub get`, this is what would happen in
// `getExecutableForCommand` later.
final result = await Process.run(sdk.dart, ['pub', 'get']);
if (result.exitCode != 0) {
return null;
}
packageConfig = await _findPackageConfigUri(uri);
} else {
return null;
}
}
return packageConfig;
}
/// Tries to load the package config.
///
/// Returns null and writes to stderr if the package config is malformed.
static Future<package_config.PackageConfig?> loadPackageConfig(
Uri packageConfigUri) async {
try {
return await package_config.loadPackageConfigUri(packageConfigUri);
} on FormatException catch (e) {
// This can be thrown if the package_config.json is malformed or has
// duplicate entries.
log.stderr(
'Error encountered while parsing '
'${packageConfigUri.toFilePath()}: ${e.message}.',
);
return null;
}
}
/// Finds the package config uri.
///
/// Returns `null` if no package config can be found.
// TODO(https://github.com/dart-lang/package_config/issues/126): Expose this
// logic in package:package_config.
static Future<Uri?> _findPackageConfigUri(Uri uri) async {
while (true) {
final packageConfig =
File.fromUri(uri.resolve('.dart_tool/package_config.json'));
final packageGraph =
File.fromUri(uri.resolve('.dart_tool/package_graph.json'));
if (await packageConfig.exists() && await packageGraph.exists()) {
return packageConfig.uri;
}
final parent = uri.resolve('..');
if (parent == uri) {
return null;
}
uri = parent;
}
}
static Future<Uri?> findPubspec(Uri uri) async {
while (true) {
final candidate = uri.resolve('pubspec.yaml');
if (await File.fromUri(candidate).exists()) {
return candidate;
}
final parent = uri.resolve('..');
if (parent == uri) {
return null;
}
uri = parent;
}
}
static Future<Uri?> findWorkspacePubspec(Uri? workspacePackageConfig) async {
if (workspacePackageConfig == null) {
return null;
}
final candidate = workspacePackageConfig.resolve('../pubspec.yaml');
if (File.fromUri(candidate).existsSync()) {
return candidate;
}
return null;
}
/// Tries to find the package name that [uri] is in.
///
/// Returns `null` if package cannnot be determined.
static Future<String?> findRootPackageName(Uri uri) async {
final pubspecUri = await findPubspec(uri);
if (pubspecUri == null) {
return null;
}
final pubspecFile = File.fromUri(pubspecUri);
final contents = await pubspecFile.readAsString();
final pubspec = loadYaml(contents);
return pubspec['name'];
}
}