Skip to content

Commit b01a3f3

Browse files
authored
[native_assets_builder] Automatically track all Dart sources as dependencies (#1322)
1 parent 952da66 commit b01a3f3

File tree

63 files changed

+460
-159
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

63 files changed

+460
-159
lines changed

.github/workflows/native.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ jobs:
4545
sdk: 3.3.0
4646
- os: windows
4747
sdk: stable
48+
# native_assets_builder uses `dart compile kernel --depfile` which is only available in 3.5.0.
49+
# We don't care too much about native_assets_builder on stable. It will be pulled into Dart and Flutter on last master/main.
50+
- sdk: stable
51+
package: native_assets_builder
52+
- sdk: 3.3.0
53+
package: native_assets_builder
4854

4955
runs-on: ${{ matrix.os }}-latest
5056

pkgs/native_assets_builder/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
## 0.8.1-wip
22

3+
- `BuildRunner` now automatically invokes build hooks again if any of their Dart
4+
sources changed.
35
- Add more data asset test files.
46

57
## 0.8.0

pkgs/native_assets_builder/lib/src/build_runner/build_runner.dart

Lines changed: 178 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'dart:async';
66
import 'dart:io';
77

88
import 'package:logging/logging.dart';
9+
import 'package:native_assets_cli/native_assets_cli.dart' as api;
910
import 'package:native_assets_cli/native_assets_cli_internal.dart';
1011
import 'package:package_config/package_config.dart';
1112

@@ -15,7 +16,9 @@ import '../model/hook_result.dart';
1516
import '../model/link_dry_run_result.dart';
1617
import '../model/link_result.dart';
1718
import '../package_layout/package_layout.dart';
19+
import '../utils/file.dart';
1820
import '../utils/run_process.dart';
21+
import '../utils/uri.dart';
1922
import 'build_planner.dart';
2023

2124
typedef DependencyMetadata = Map<String, Metadata>;
@@ -24,6 +27,10 @@ typedef DependencyMetadata = Map<String, Metadata>;
2427
///
2528
/// These methods are invoked by launchers such as dartdev (for `dart run`)
2629
/// and flutter_tools (for `flutter run` and `flutter build`).
30+
///
31+
/// The native assets build runner does not support reentrancy for identical
32+
/// [api.BuildConfig] and [api.LinkConfig]! For more info see:
33+
/// https://github.com/dart-lang/native/issues/1319
2734
class NativeAssetsBuildRunner {
2835
final Logger logger;
2936
final Uri dartExecutable;
@@ -40,6 +47,10 @@ class NativeAssetsBuildRunner {
4047
///
4148
/// If provided, only assets of all transitive dependencies of
4249
/// [runPackageName] are built.
50+
///
51+
/// The native assets build runner does not support reentrancy for identical
52+
/// [api.BuildConfig] and [api.LinkConfig]! For more info see:
53+
/// https://github.com/dart-lang/native/issues/1319
4354
Future<BuildResult> build({
4455
required LinkModePreferenceImpl linkModePreference,
4556
required Target target,
@@ -81,6 +92,10 @@ class NativeAssetsBuildRunner {
8192
///
8293
/// If provided, only assets of all transitive dependencies of
8394
/// [runPackageName] are linked.
95+
///
96+
/// The native assets build runner does not support reentrancy for identical
97+
/// [api.BuildConfig] and [api.LinkConfig]! For more info see:
98+
/// https://github.com/dart-lang/native/issues/1319
8499
Future<LinkResult> link({
85100
required LinkModePreferenceImpl linkModePreference,
86101
required Target target,
@@ -371,6 +386,7 @@ class NativeAssetsBuildRunner {
371386
var hookResult = HookResult();
372387
for (final package in buildPlan) {
373388
final config = await _cliConfigDryRun(
389+
package: package,
374390
packageName: package.name,
375391
packageRoot: packageLayout.packageRoot(package.name),
376392
targetOS: targetOS,
@@ -381,13 +397,30 @@ class NativeAssetsBuildRunner {
381397
buildDryRunResult: buildDryRunResult,
382398
linkingEnabled: linkingEnabled,
383399
);
400+
final packageConfigUri = packageLayout.packageConfigUri;
401+
final (
402+
compileSuccess,
403+
hookKernelFile,
404+
_,
405+
) = await _compileHookForPackageCached(
406+
config,
407+
packageConfigUri,
408+
workingDirectory,
409+
includeParentEnvironment,
410+
);
411+
if (!compileSuccess) {
412+
hookResult.copyAdd(HookOutputImpl(), false);
413+
continue;
414+
}
415+
// TODO(https://github.com/dart-lang/native/issues/1321): Should dry runs be cached?
384416
var (buildOutput, packageSuccess) = await _runHookForPackage(
385417
hook,
386418
config,
387-
packageLayout.packageConfigUri,
419+
packageConfigUri,
388420
workingDirectory,
389421
includeParentEnvironment,
390422
null,
423+
hookKernelFile,
391424
);
392425
buildOutput = _expandArchsNativeCodeAssets(buildOutput);
393426
hookResult = hookResult.copyAdd(buildOutput, packageSuccess);
@@ -427,16 +460,33 @@ class NativeAssetsBuildRunner {
427460
Uri? resources,
428461
) async {
429462
final outDir = config.outputDirectory;
463+
final (
464+
compileSuccess,
465+
hookKernelFile,
466+
hookLastSourceChange,
467+
) = await _compileHookForPackageCached(
468+
config,
469+
packageConfigUri,
470+
workingDirectory,
471+
includeParentEnvironment,
472+
);
473+
if (!compileSuccess) {
474+
return (HookOutputImpl(), false);
475+
}
430476

431477
final hookOutput = HookOutputImpl.readFromFile(file: config.outputFile);
432478
if (hookOutput != null) {
433479
final lastBuilt = hookOutput.timestamp.roundDownToSeconds();
434-
final lastChange = await hookOutput.dependenciesModel.lastModified();
435-
436-
if (lastBuilt.isAfter(lastChange)) {
437-
logger
438-
.info('Skipping ${hook.name} for ${config.packageName} in $outDir. '
439-
'Last build on $lastBuilt, last input change on $lastChange.');
480+
final dependenciesLastChange =
481+
await hookOutput.dependenciesModel.lastModified();
482+
if (lastBuilt.isAfter(dependenciesLastChange) &&
483+
lastBuilt.isAfter(hookLastSourceChange)) {
484+
logger.info(
485+
'Skipping ${hook.name} for ${config.packageName} in $outDir. '
486+
'Last build on $lastBuilt. '
487+
'Last dependencies change on $dependenciesLastChange. '
488+
'Last hook change on $hookLastSourceChange.',
489+
);
440490
// All build flags go into [outDir]. Therefore we do not have to check
441491
// here whether the config is equal.
442492
return (hookOutput, true);
@@ -450,6 +500,7 @@ class NativeAssetsBuildRunner {
450500
workingDirectory,
451501
includeParentEnvironment,
452502
resources,
503+
hookKernelFile,
453504
);
454505
}
455506

@@ -460,6 +511,7 @@ class NativeAssetsBuildRunner {
460511
Uri workingDirectory,
461512
bool includeParentEnvironment,
462513
Uri? resources,
514+
File hookKernelFile,
463515
) async {
464516
final configFile = config.outputDirectory.resolve('../config.json');
465517
final configFileContents = config.toJsonString();
@@ -473,7 +525,7 @@ class NativeAssetsBuildRunner {
473525

474526
final arguments = [
475527
'--packages=${packageConfigUri.toFilePath()}',
476-
config.script.toFilePath(),
528+
hookKernelFile.path,
477529
'--config=${configFile.toFilePath()}',
478530
if (resources != null) resources.toFilePath(),
479531
];
@@ -484,6 +536,7 @@ class NativeAssetsBuildRunner {
484536
logger: logger,
485537
includeParentEnvironment: includeParentEnvironment,
486538
);
539+
487540
var success = true;
488541
if (result.exitCode != 0) {
489542
final printWorkingDir = workingDirectory != Directory.current.uri;
@@ -542,7 +595,114 @@ ${e.message}
542595
}
543596
}
544597

598+
/// Compiles the hook to dill and caches the dill.
599+
///
600+
/// It does not reuse the cached dill for different [config]s, due to
601+
/// reentrancy requirements. For more info see:
602+
/// https://github.com/dart-lang/native/issues/1319
603+
Future<(bool success, File kernelFile, DateTime lastSourceChange)>
604+
_compileHookForPackageCached(
605+
HookConfigImpl config,
606+
Uri packageConfigUri,
607+
Uri workingDirectory,
608+
bool includeParentEnvironment,
609+
) async {
610+
final kernelFile = File.fromUri(
611+
config.outputDirectory.resolve('../hook.dill'),
612+
);
613+
final depFile = File.fromUri(
614+
config.outputDirectory.resolve('../hook.dill.d'),
615+
);
616+
final bool mustCompile;
617+
final DateTime sourceLastChange;
618+
if (!await depFile.exists()) {
619+
mustCompile = true;
620+
sourceLastChange = DateTime.now();
621+
} else {
622+
// Format: `path/to/my.dill: path/to/my.dart, path/to/more.dart`
623+
final depFileContents = await depFile.readAsString();
624+
final dartSourceFiles = depFileContents
625+
.trim()
626+
.split(' ')
627+
.skip(1) // '<dill>:'
628+
.map((u) => Uri.file(u).fileSystemEntity)
629+
.toList();
630+
final dartFilesLastChange = await dartSourceFiles.lastModified();
631+
final packageConfigLastChange =
632+
await packageConfigUri.fileSystemEntity.lastModified();
633+
sourceLastChange = packageConfigLastChange.isAfter(dartFilesLastChange)
634+
? packageConfigLastChange
635+
: dartFilesLastChange;
636+
final dillLastChange = await kernelFile.lastModified();
637+
mustCompile = sourceLastChange.isAfter(dillLastChange);
638+
}
639+
final bool success;
640+
if (!mustCompile) {
641+
success = true;
642+
} else {
643+
success = await _compileHookForPackage(
644+
config,
645+
packageConfigUri,
646+
workingDirectory,
647+
includeParentEnvironment,
648+
kernelFile,
649+
depFile,
650+
);
651+
}
652+
return (success, kernelFile, sourceLastChange);
653+
}
654+
655+
Future<bool> _compileHookForPackage(
656+
HookConfigImpl config,
657+
Uri packageConfigUri,
658+
Uri workingDirectory,
659+
bool includeParentEnvironment,
660+
File kernelFile,
661+
File depFile,
662+
) async {
663+
final compileArguments = [
664+
'compile',
665+
'kernel',
666+
'--packages=${packageConfigUri.toFilePath()}',
667+
'--output=${kernelFile.path}',
668+
'--depfile=${depFile.path}',
669+
config.script.toFilePath(),
670+
];
671+
final compileResult = await runProcess(
672+
workingDirectory: workingDirectory,
673+
executable: dartExecutable,
674+
arguments: compileArguments,
675+
logger: logger,
676+
includeParentEnvironment: includeParentEnvironment,
677+
);
678+
var success = true;
679+
if (compileResult.exitCode != 0) {
680+
final printWorkingDir = workingDirectory != Directory.current.uri;
681+
final commandString = [
682+
if (printWorkingDir) '(cd ${workingDirectory.toFilePath()};',
683+
dartExecutable.toFilePath(),
684+
...compileArguments.map((a) => a.contains(' ') ? "'$a'" : a),
685+
if (printWorkingDir) ')',
686+
].join(' ');
687+
logger.severe(
688+
'''
689+
Building native assets for package:${config.packageName} failed.
690+
Compilation of hook returned with exit code: ${compileResult.exitCode}.
691+
To reproduce run:
692+
$commandString
693+
stderr:
694+
${compileResult.stderr}
695+
stdout:
696+
${compileResult.stdout}
697+
''',
698+
);
699+
success = false;
700+
}
701+
return success;
702+
}
703+
545704
static Future<HookConfigImpl> _cliConfigDryRun({
705+
required Package package,
546706
required String packageName,
547707
required Uri packageRoot,
548708
required OSImpl targetOS,
@@ -553,8 +713,16 @@ ${e.message}
553713
Iterable<String>? supportedAssetTypes,
554714
required bool? linkingEnabled,
555715
}) async {
556-
final hookDirName = 'dry_run_${hook.name}_${targetOS}_$linkMode';
557-
final outDirUri = buildParentDir.resolve('$hookDirName/out/');
716+
final buildDirName = HookConfigImpl.checksumDryRun(
717+
packageName: package.name,
718+
packageRoot: package.root,
719+
targetOS: targetOS,
720+
linkModePreference: linkMode,
721+
supportedAssetTypes: supportedAssetTypes,
722+
hook: hook,
723+
linkingEnabled: linkingEnabled,
724+
);
725+
final outDirUri = buildParentDir.resolve('$buildDirName/out/');
558726
final outDir = Directory.fromUri(outDirUri);
559727
if (!await outDir.exists()) {
560728
await outDir.create(recursive: true);

pkgs/native_assets_builder/lib/src/model/hook_result.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ final class HookResult
6060
final oneInTwo = assets2.where((asset) => assets1.contains(asset));
6161
if (twoInOne.isNotEmpty || oneInTwo.isNotEmpty) {
6262
throw ArgumentError(
63-
'Found assets with same IDs, ${[...oneInTwo, ...twoInOne]}');
63+
'Found duplicate IDs, ${oneInTwo.map((e) => e.id).toList()}');
6464
}
6565
return [
6666
...assets1,
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:io';
6+
7+
extension FileSystemEntityExtension on FileSystemEntity {
8+
Future<DateTime> lastModified() async {
9+
final this_ = this;
10+
if (this_ is Link || await FileSystemEntity.isLink(this_.path)) {
11+
// Don't follow links.
12+
return DateTime.fromMicrosecondsSinceEpoch(0);
13+
}
14+
if (this_ is File) {
15+
if (!await this_.exists()) {
16+
// If the file was deleted, regard it is modified recently.
17+
return DateTime.now();
18+
}
19+
return await this_.lastModified();
20+
}
21+
assert(this_ is Directory);
22+
this_ as Directory;
23+
return await this_.lastModified();
24+
}
25+
}
26+
27+
extension FileSystemEntityIterable on Iterable<FileSystemEntity> {
28+
Future<DateTime> lastModified() async {
29+
var last = DateTime.fromMillisecondsSinceEpoch(0);
30+
for (final entity in this) {
31+
final entityTimestamp = await entity.lastModified();
32+
if (entityTimestamp.isAfter(last)) {
33+
// print([entity, entityTimestamp]);
34+
last = entityTimestamp;
35+
}
36+
}
37+
return last;
38+
}
39+
}
40+
41+
extension DirectoryExtension on Directory {
42+
Future<DateTime> lastModified() async {
43+
var last = DateTime.fromMillisecondsSinceEpoch(0);
44+
await for (final entity in list()) {
45+
final entityTimestamp = await entity.lastModified();
46+
if (entityTimestamp.isAfter(last)) {
47+
// print([this, entityTimestamp]);
48+
last = entityTimestamp;
49+
}
50+
}
51+
return last;
52+
}
53+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:io';
6+
7+
extension UriExtension on Uri {
8+
FileSystemEntity get fileSystemEntity {
9+
if (path.endsWith(Platform.pathSeparator) || path.endsWith('/')) {
10+
return Directory.fromUri(this);
11+
}
12+
return File.fromUri(this);
13+
}
14+
}

0 commit comments

Comments
 (0)