Skip to content

Commit 3aba894

Browse files
authored
[native_assets_builder] Report dart sources as dependencies of hooks (#1780)
Closes: #1770 This PR adds the Dart sources to the dependencies in `HookResult`. It also fixes the dep-file parsing w.r.t. to escapes. ### Implementation details We're passing around `HookOutput`s which is the deserialized `output.json`. We could add the Dart sources as dependencies to it _after_ deserializing, but then the correspondence to the json is lost. So instead I've added an extra return value to the places where we pass `HookOutput` around.
1 parent 0caab92 commit 3aba894

File tree

9 files changed

+146
-64
lines changed

9 files changed

+146
-64
lines changed

.github/workflows/native.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ permissions: read-all
77

88
on:
99
pull_request:
10-
branches: [main]
10+
# No `branches:` to enable stacked PRs on GitHub.
1111
paths:
1212
- ".github/workflows/native.yaml"
1313
- "pkgs/native_assets_builder/**"

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

Lines changed: 84 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ class NativeAssetsBuildRunner {
157157
'Build configuration for ${package.name} contains errors', errors);
158158
}
159159

160-
final hookOutput = await _runHookForPackageCached(
160+
final result = await _runHookForPackageCached(
161161
Hook.build,
162162
config,
163163
(config, output) =>
@@ -167,8 +167,9 @@ class NativeAssetsBuildRunner {
167167
null,
168168
packageLayout,
169169
);
170-
if (hookOutput == null) return null;
171-
hookResult = hookResult.copyAdd(hookOutput);
170+
if (result == null) return null;
171+
final (hookOutput, hookDeps) = result;
172+
hookResult = hookResult.copyAdd(hookOutput, hookDeps);
172173
globalMetadata[package.name] = (hookOutput as BuildOutput).metadata;
173174
}
174175

@@ -259,7 +260,7 @@ class NativeAssetsBuildRunner {
259260
'Link configuration for ${package.name} contains errors', errors);
260261
}
261262

262-
final hookOutput = await _runHookForPackageCached(
263+
final result = await _runHookForPackageCached(
263264
Hook.link,
264265
config,
265266
(config, output) =>
@@ -269,8 +270,9 @@ class NativeAssetsBuildRunner {
269270
resourceIdentifiers,
270271
packageLayout,
271272
);
272-
if (hookOutput == null) return null;
273-
hookResult = hookResult.copyAdd(hookOutput);
273+
if (result == null) return null;
274+
final (hookOutput, hookDeps) = result;
275+
hookResult = hookResult.copyAdd(hookOutput, hookDeps);
274276
}
275277

276278
final errors = await applicationAssetValidator(hookResult.encodedAssets);
@@ -403,18 +405,15 @@ class NativeAssetsBuildRunner {
403405

404406
final config = BuildConfig(configBuilder.json);
405407
final packageConfigUri = packageLayout.packageConfigUri;
406-
final (
407-
compileSuccess,
408-
hookKernelFile,
409-
_,
410-
) = await _compileHookForPackageCached(
408+
final hookCompileResult = await _compileHookForPackageCached(
411409
config.packageName,
412410
config.outputDirectory,
413411
config.packageRoot.resolve('hook/${hook.scriptName}'),
414412
packageConfigUri,
415413
workingDirectory,
416414
);
417-
if (!compileSuccess) return null;
415+
if (hookCompileResult == null) return null;
416+
final (hookKernelFile, _) = hookCompileResult;
418417

419418
// TODO(https://github.com/dart-lang/native/issues/1321): Should dry runs be cached?
420419
final buildOutput = await runUnderDirectoriesLock(
@@ -437,12 +436,12 @@ class NativeAssetsBuildRunner {
437436
),
438437
);
439438
if (buildOutput == null) return null;
440-
hookResult = hookResult.copyAdd(buildOutput);
439+
hookResult = hookResult.copyAdd(buildOutput, [/*dry run is not cached*/]);
441440
}
442441
return hookResult;
443442
}
444443

445-
Future<HookOutput?> _runHookForPackageCached(
444+
Future<(HookOutput, List<Uri>)?> _runHookForPackageCached(
446445
Hook hook,
447446
HookConfig config,
448447
_HookValidator validator,
@@ -461,20 +460,17 @@ class NativeAssetsBuildRunner {
461460
timeout: singleHookTimeout,
462461
logger: logger,
463462
() async {
464-
final (
465-
compileSuccess,
466-
hookKernelFile,
467-
hookHashesFile,
468-
) = await _compileHookForPackageCached(
463+
final hookCompileResult = await _compileHookForPackageCached(
469464
config.packageName,
470465
config.outputDirectory,
471466
config.packageRoot.resolve('hook/${hook.scriptName}'),
472467
packageConfigUri,
473468
workingDirectory,
474469
);
475-
if (!compileSuccess) {
470+
if (hookCompileResult == null) {
476471
return null;
477472
}
473+
final (hookKernelFile, hookHashes) = hookCompileResult;
478474

479475
final buildOutputFile =
480476
File.fromUri(config.outputDirectory.resolve(hook.outputName));
@@ -510,7 +506,7 @@ ${e.message}
510506
);
511507
// All build flags go into [outDir]. Therefore we do not have to
512508
// check here whether the config is equal.
513-
return output;
509+
return (output, hookHashes.fileSystemEntities);
514510
}
515511
logger.info(
516512
'Rerunning ${hook.name} for ${config.packageName}'
@@ -533,12 +529,13 @@ ${e.message}
533529
if (await dependenciesHashFile.exists()) {
534530
await dependenciesHashFile.delete();
535531
}
532+
return null;
536533
} else {
537534
final modifiedDuringBuild = await dependenciesHashes.hashDependencies(
538535
[
539536
...result.dependencies,
540537
// Also depend on the hook source code.
541-
hookHashesFile.uri,
538+
hookHashes.file.uri,
542539
],
543540
lastModifiedCutoffTime,
544541
environment,
@@ -547,7 +544,7 @@ ${e.message}
547544
logger.severe('File modified during build. Build must be rerun.');
548545
}
549546
}
550-
return result;
547+
return (result, hookHashes.fileSystemEntities);
551548
},
552549
);
553550
}
@@ -685,7 +682,7 @@ ${e.message}
685682
///
686683
/// TODO(https://github.com/dart-lang/native/issues/1578): Compile only once
687684
/// instead of per config. This requires more locking.
688-
Future<(bool success, File kernelFile, File cacheFile)>
685+
Future<(File kernelFile, DependenciesHashFile cacheFile)?>
689686
_compileHookForPackageCached(
690687
String packageName,
691688
Uri outputDirectory,
@@ -721,7 +718,7 @@ ${e.message}
721718
}
722719

723720
if (!mustCompile) {
724-
return (true, kernelFile, dependenciesHashFile);
721+
return (kernelFile, dependenciesHashes);
725722
}
726723

727724
final success = await _compileHookForPackage(
@@ -733,8 +730,7 @@ ${e.message}
733730
depFile,
734731
);
735732
if (!success) {
736-
await dependenciesHashFile.delete();
737-
return (success, kernelFile, dependenciesHashFile);
733+
return null;
738734
}
739735

740736
final dartSources = await _readDepFile(depFile);
@@ -751,19 +747,7 @@ ${e.message}
751747
logger.severe('File modified during build. Build must be rerun.');
752748
}
753749

754-
return (success, kernelFile, dependenciesHashFile);
755-
}
756-
757-
Future<List<Uri>> _readDepFile(File depFile) async {
758-
// Format: `path/to/my.dill: path/to/my.dart, path/to/more.dart`
759-
final depFileContents = await depFile.readAsString();
760-
final dartSources = depFileContents
761-
.trim()
762-
.split(' ')
763-
.skip(1) // '<kernel file>:'
764-
.map(Uri.file)
765-
.toList();
766-
return dartSources;
750+
return (kernelFile, dependenciesHashes);
767751
}
768752

769753
Future<bool> _compileHookForPackage(
@@ -811,6 +795,12 @@ ${compileResult.stdout}
811795
''',
812796
);
813797
success = false;
798+
if (await depFile.exists()) {
799+
await depFile.delete();
800+
}
801+
if (await kernelFile.exists()) {
802+
await kernelFile.delete();
803+
}
814804
}
815805
return success;
816806
}
@@ -927,3 +917,57 @@ ${compileResult.stdout}
927917
extension on Uri {
928918
Uri get parent => File(toFilePath()).parent.uri;
929919
}
920+
921+
/// Parses depfile contents.
922+
///
923+
/// Format: `path/to/my.dill: path/to/my.dart, path/to/more.dart`
924+
///
925+
/// However, the spaces in paths are escaped with backslashes, and the
926+
/// backslashes are escaped with backslashes:
927+
///
928+
/// ```dart
929+
/// String _escapePath(String path) {
930+
/// return path.replaceAll('\\', '\\\\').replaceAll(' ', '\\ ');
931+
/// }
932+
/// ```
933+
List<String> parseDepFileInputs(String contents) {
934+
final output = contents.substring(0, contents.indexOf(': '));
935+
contents = contents.substring(output.length + ': '.length).trim();
936+
final pathsEscaped = _splitOnNonEscapedSpaces(contents);
937+
return pathsEscaped.map(_unescapeDepsPath).toList();
938+
}
939+
940+
String _unescapeDepsPath(String path) =>
941+
path.replaceAll(r'\ ', ' ').replaceAll(r'\\', r'\');
942+
943+
List<String> _splitOnNonEscapedSpaces(String contents) {
944+
var index = 0;
945+
final result = <String>[];
946+
while (index < contents.length) {
947+
final start = index;
948+
while (index < contents.length) {
949+
final u = contents.codeUnitAt(index);
950+
if (u == ' '.codeUnitAt(0)) {
951+
break;
952+
}
953+
if (u == r'\'.codeUnitAt(0)) {
954+
index++;
955+
if (index == contents.length) {
956+
throw const FormatException('malformed, ending with backslash');
957+
}
958+
final v = contents.codeUnitAt(index);
959+
assert(v == ' '.codeUnitAt(0) || v == r'\'.codeUnitAt(0));
960+
}
961+
index++;
962+
}
963+
result.add(contents.substring(start, index));
964+
index++;
965+
}
966+
return result;
967+
}
968+
969+
Future<List<Uri>> _readDepFile(File depFile) async {
970+
final depFileContents = await depFile.readAsString();
971+
final dartSources = parseDepFileInputs(depFileContents);
972+
return dartSources.map(Uri.file).toList();
973+
}

pkgs/native_assets_builder/lib/src/dependencies_hash_file/dependencies_hash_file.dart

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,21 @@ import '../utils/uri.dart';
1313

1414
class DependenciesHashFile {
1515
DependenciesHashFile({
16-
required File file,
17-
}) : _file = file;
16+
required this.file,
17+
});
1818

19-
final File _file;
19+
final File file;
2020
FileSystemHashes _hashes = FileSystemHashes();
2121

22+
List<Uri> get fileSystemEntities => _hashes.files.map((e) => e.path).toList();
23+
2224
Future<void> _readFile() async {
23-
if (!await _file.exists()) {
25+
if (!await file.exists()) {
2426
_hashes = FileSystemHashes();
2527
return;
2628
}
2729
final jsonObject =
28-
(json.decode(utf8.decode(await _file.readAsBytes())) as Map)
30+
(json.decode(utf8.decode(await file.readAsBytes())) as Map)
2931
.cast<String, Object>();
3032
_hashes = FileSystemHashes.fromJson(jsonObject);
3133
}
@@ -70,7 +72,7 @@ class DependenciesHashFile {
7072
return modifiedAfterTimeStamp;
7173
}
7274

73-
Future<void> _persist() => _file.writeAsString(json.encode(_hashes.toJson()));
75+
Future<void> _persist() => file.writeAsString(json.encode(_hashes.toJson()));
7476

7577
/// Reads the file with hashes and reports if there is an outdated file,
7678
/// directory or environment variable.

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ final class HookResult implements BuildResult, BuildDryRunResult, LinkResult {
3939
dependencies: dependencies ?? [],
4040
);
4141

42-
HookResult copyAdd(HookOutput hookOutput) {
42+
HookResult copyAdd(HookOutput hookOutput, List<Uri> hookDependencies) {
4343
final mergedMaps = mergeMaps(
4444
encodedAssetsForLinking,
4545
hookOutput is BuildOutput
@@ -61,6 +61,7 @@ final class HookResult implements BuildResult, BuildDryRunResult, LinkResult {
6161
dependencies: [
6262
...dependencies,
6363
...hookOutput.dependencies,
64+
...hookDependencies,
6465
]..sort(_uriCompare),
6566
);
6667
}

pkgs/native_assets_builder/test/build_runner/build_dependencies_test.dart

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,28 @@ void main() async {
5050
expect(result.encodedAssets.length, 2);
5151
expect(
5252
result.dependencies,
53-
[
54-
tempUri.resolve('native_add/').resolve('src/native_add.c'),
55-
tempUri
56-
.resolve('native_subtract/')
57-
.resolve('src/native_subtract.c'),
58-
],
53+
containsAll([
54+
tempUri.resolve('native_add/src/native_add.c'),
55+
tempUri.resolve('native_subtract/src/native_subtract.c'),
56+
if (!Platform.isWindows) ...[
57+
tempUri.resolve('native_add/hook/build.dart'),
58+
tempUri.resolve('native_subtract/hook/build.dart'),
59+
],
60+
]),
5961
);
62+
if (Platform.isWindows) {
63+
expect(
64+
// https://github.com/dart-lang/sdk/issues/59657
65+
// Deps file on windows sometimes have lowercase drive letters.
66+
// File.exists will work, but Uri equality doesn't.
67+
result.dependencies
68+
.map((e) => Uri.file(e.toFilePath().toLowerCase())),
69+
containsAll([
70+
tempUri.resolve('native_add/hook/build.dart'),
71+
tempUri.resolve('native_subtract/hook/build.dart'),
72+
].map((e) => Uri.file(e.toFilePath().toLowerCase()))),
73+
);
74+
}
6075
}
6176
});
6277
});

pkgs/native_assets_builder/test/build_runner/build_runner_caching_test.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ void main() async {
4444
);
4545
expect(
4646
result.dependencies,
47-
[
47+
contains(
4848
packageUri.resolve('src/native_add.c'),
49-
],
49+
),
5050
);
5151
}
5252

@@ -80,9 +80,9 @@ void main() async {
8080
);
8181
expect(
8282
result.dependencies,
83-
[
83+
contains(
8484
packageUri.resolve('src/native_add.c'),
85-
],
85+
),
8686
);
8787
}
8888
});

0 commit comments

Comments
 (0)