blob: 6388d0012e3b841a8a4bf259c15bce6acff41ce4 [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:logging/logging.dart';
import 'package:native_assets_cli/native_assets_cli.dart';
import '../package_layout/package_layout.dart';
import '../utils/run_process.dart';
import 'build_planner.dart';
typedef DependencyMetadata = Map<String, Metadata>;
/// The programmatic API to be used by Dart launchers to invoke native builds.
///
/// These methods are invoked by launchers such as dartdev (for `dart run`)
/// and flutter_tools (for `flutter run` and `flutter build`).
class NativeAssetsBuildRunner {
final Logger logger;
final Uri dartExecutable;
NativeAssetsBuildRunner({
required this.logger,
required this.dartExecutable,
});
final _metadata = <Target, DependencyMetadata>{};
/// [workingDirectory] is expected to contain `.dart_tool`.
///
/// This method is invoked by launchers such as dartdev (for `dart run`) and
/// flutter_tools (for `flutter run` and `flutter build`).
///
/// Completes the future with an error if the build fails.
Future<List<Asset>> build({
required LinkModePreference linkModePreference,
required Target target,
required Uri workingDirectory,
CCompilerConfig? cCompilerConfig,
IOSSdk? targetIOSSdk,
required bool includeParentEnvironment,
}) async {
assert(_metadata.isEmpty);
final packageLayout =
await PackageLayout.fromRootPackageRoot(workingDirectory);
final packagesWithNativeAssets =
await packageLayout.packagesWithNativeAssets;
final planner = await NativeAssetsBuildPlanner.fromRootPackageRoot(
rootPackageRoot: packageLayout.rootPackageRoot,
packagesWithNativeAssets: packagesWithNativeAssets,
dartExecutable: Uri.file(Platform.resolvedExecutable),
);
final plan = planner.plan();
final assetList = <Asset>[];
for (final package in plan) {
final dependencyMetadata = _metadataForPackage(
packageGraph: planner.packageGraph,
packageName: package.name,
targetMetadata: _metadata[target],
);
final config = await _cliConfig(
packageName: package.name,
packageRoot: packageLayout.packageRoot(package.name),
target: target,
linkMode: linkModePreference,
buildParentDir: packageLayout.dartToolNativeAssetsBuilder,
dependencyMetadata: dependencyMetadata,
cCompilerConfig: cCompilerConfig,
targetIOSSdk: targetIOSSdk,
);
final assets = await _buildPackageCached(
config,
packageLayout.packageConfigUri,
workingDirectory,
includeParentEnvironment,
);
assetList.addAll(assets);
}
return assetList;
}
Future<List<Asset>> _buildPackageCached(
BuildConfig config,
Uri packageConfigUri,
Uri workingDirectory,
bool includeParentEnvironment,
) async {
final packageName = config.packageName;
final outDir = config.outDir;
if (!await Directory.fromUri(outDir).exists()) {
await Directory.fromUri(outDir).create(recursive: true);
}
final buildOutput = await BuildOutput.readFromFile(outDir: outDir);
final lastBuilt = buildOutput?.timestamp.roundDownToSeconds() ??
DateTime.fromMillisecondsSinceEpoch(0);
final dependencies = buildOutput?.dependencies;
final lastChange = await dependencies?.lastModified() ?? DateTime.now();
if (lastBuilt.isAfter(lastChange)) {
logger.info('Skipping build for $packageName in $outDir. '
'Last build on $lastBuilt, last input change on $lastChange.');
// All build flags go into [outDir]. Therefore we do not have to check
// here whether the config is equal.
setMetadata(config.target, packageName, buildOutput?.metadata);
return buildOutput!.assets;
}
return _buildPackage(
config,
packageConfigUri,
workingDirectory,
includeParentEnvironment,
);
}
Future<List<Asset>> _buildPackage(
BuildConfig config,
Uri packageConfigUri,
Uri workingDirectory,
bool includeParentEnvironment,
) async {
final outDir = config.outDir;
final configFile = outDir.resolve('config.yaml');
final buildDotDart = config.packageRoot.resolve('build.dart');
final configFileContents = config.toYamlString();
logger.info('config.yaml contents: $configFileContents');
await File.fromUri(configFile).writeAsString(configFileContents);
final buildOutputFile = File.fromUri(outDir.resolve(BuildOutput.fileName));
if (await buildOutputFile.exists()) {
// Ensure we'll never read outdated build results.
await buildOutputFile.delete();
}
await runProcess(
workingDirectory: workingDirectory,
executable: dartExecutable,
arguments: [
'--packages=${packageConfigUri.toFilePath()}',
buildDotDart.toFilePath(),
'--config=${configFile.toFilePath()}',
],
logger: logger,
includeParentEnvironment: includeParentEnvironment,
expectedExitCode: 0,
throwOnUnexpectedExitCode: true,
);
final buildOutput = await BuildOutput.readFromFile(outDir: outDir);
setMetadata(config.target, config.packageName, buildOutput?.metadata);
return buildOutput?.assets ?? [];
}
void setMetadata(Target target, String packageName, Metadata? metadata) {
if (metadata == null) {
return;
}
_metadata[target] ??= {};
_metadata[target]![packageName] = metadata;
}
static Future<BuildConfig> _cliConfig({
required String packageName,
required Uri packageRoot,
required Target target,
IOSSdk? targetIOSSdk,
required LinkModePreference linkMode,
required Uri buildParentDir,
CCompilerConfig? cCompilerConfig,
DependencyMetadata? dependencyMetadata,
}) async {
final buildDirName = BuildConfig.checksum(
packageRoot: packageRoot,
target: target,
linkModePreference: linkMode,
targetIOSSdk: targetIOSSdk,
cCompiler: cCompilerConfig,
dependencyMetadata: dependencyMetadata,
);
final outDirUri = buildParentDir.resolve('$buildDirName/');
final outDir = Directory.fromUri(outDirUri);
if (!await outDir.exists()) {
// TODO(https://dartbug.com/50565): Purge old or unused folders.
await outDir.create(recursive: true);
}
return BuildConfig(
outDir: outDirUri,
packageRoot: packageRoot,
target: target,
linkModePreference: linkMode,
targetIOSSdk: targetIOSSdk,
cCompiler: cCompilerConfig,
dependencyMetadata: dependencyMetadata,
);
}
DependencyMetadata? _metadataForPackage({
required PackageGraph packageGraph,
required String packageName,
DependencyMetadata? targetMetadata,
}) {
if (targetMetadata == null) {
return null;
}
final dependencies = packageGraph.neighborsOf(packageName).toSet();
return {
for (final entry in targetMetadata.entries)
if (dependencies.contains(entry.key)) entry.key: entry.value,
};
}
}
extension on DateTime {
DateTime roundDownToSeconds() =>
DateTime.fromMillisecondsSinceEpoch(millisecondsSinceEpoch -
millisecondsSinceEpoch % Duration(seconds: 1).inMilliseconds);
}
extension on BuildConfig {
String get packageName =>
packageRoot.pathSegments.lastWhere((e) => e.isNotEmpty);
}