Skip to content

[native_assets_builder] Report dart sources as dependencies of hooks #1780

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Dec 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/native.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ permissions: read-all

on:
pull_request:
branches: [main]
# No `branches:` to enable stacked PRs on GitHub.
paths:
- ".github/workflows/native.yaml"
- "pkgs/native_assets_builder/**"
Expand Down
124 changes: 84 additions & 40 deletions pkgs/native_assets_builder/lib/src/build_runner/build_runner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ class NativeAssetsBuildRunner {
'Build configuration for ${package.name} contains errors', errors);
}

final hookOutput = await _runHookForPackageCached(
final result = await _runHookForPackageCached(
Hook.build,
config,
(config, output) =>
Expand All @@ -167,8 +167,9 @@ class NativeAssetsBuildRunner {
null,
packageLayout,
);
if (hookOutput == null) return null;
hookResult = hookResult.copyAdd(hookOutput);
if (result == null) return null;
final (hookOutput, hookDeps) = result;
hookResult = hookResult.copyAdd(hookOutput, hookDeps);
globalMetadata[package.name] = (hookOutput as BuildOutput).metadata;
}

Expand Down Expand Up @@ -259,7 +260,7 @@ class NativeAssetsBuildRunner {
'Link configuration for ${package.name} contains errors', errors);
}

final hookOutput = await _runHookForPackageCached(
final result = await _runHookForPackageCached(
Hook.link,
config,
(config, output) =>
Expand All @@ -269,8 +270,9 @@ class NativeAssetsBuildRunner {
resourceIdentifiers,
packageLayout,
);
if (hookOutput == null) return null;
hookResult = hookResult.copyAdd(hookOutput);
if (result == null) return null;
final (hookOutput, hookDeps) = result;
hookResult = hookResult.copyAdd(hookOutput, hookDeps);
}

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

final config = BuildConfig(configBuilder.json);
final packageConfigUri = packageLayout.packageConfigUri;
final (
compileSuccess,
hookKernelFile,
_,
) = await _compileHookForPackageCached(
final hookCompileResult = await _compileHookForPackageCached(
config.packageName,
config.outputDirectory,
config.packageRoot.resolve('hook/${hook.scriptName}'),
packageConfigUri,
workingDirectory,
);
if (!compileSuccess) return null;
if (hookCompileResult == null) return null;
final (hookKernelFile, _) = hookCompileResult;

// TODO(https://github.com/dart-lang/native/issues/1321): Should dry runs be cached?
final buildOutput = await runUnderDirectoriesLock(
Expand All @@ -437,12 +436,12 @@ class NativeAssetsBuildRunner {
),
);
if (buildOutput == null) return null;
hookResult = hookResult.copyAdd(buildOutput);
hookResult = hookResult.copyAdd(buildOutput, [/*dry run is not cached*/]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't you say dry run was finally purged from existence?
Why is this code still here?

}
return hookResult;
}

Future<HookOutput?> _runHookForPackageCached(
Future<(HookOutput, List<Uri>)?> _runHookForPackageCached(
Hook hook,
HookConfig config,
_HookValidator validator,
Expand All @@ -461,20 +460,17 @@ class NativeAssetsBuildRunner {
timeout: singleHookTimeout,
logger: logger,
() async {
final (
compileSuccess,
hookKernelFile,
hookHashesFile,
) = await _compileHookForPackageCached(
final hookCompileResult = await _compileHookForPackageCached(
config.packageName,
config.outputDirectory,
config.packageRoot.resolve('hook/${hook.scriptName}'),
packageConfigUri,
workingDirectory,
);
if (!compileSuccess) {
if (hookCompileResult == null) {
return null;
}
final (hookKernelFile, hookHashes) = hookCompileResult;

final buildOutputFile =
File.fromUri(config.outputDirectory.resolve(hook.outputName));
Expand Down Expand Up @@ -510,7 +506,7 @@ ${e.message}
);
// All build flags go into [outDir]. Therefore we do not have to
// check here whether the config is equal.
return output;
return (output, hookHashes.fileSystemEntities);
}
logger.info(
'Rerunning ${hook.name} for ${config.packageName}'
Expand All @@ -533,12 +529,13 @@ ${e.message}
if (await dependenciesHashFile.exists()) {
await dependenciesHashFile.delete();
}
return null;
} else {
final modifiedDuringBuild = await dependenciesHashes.hashDependencies(
[
...result.dependencies,
// Also depend on the hook source code.
hookHashesFile.uri,
hookHashes.file.uri,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's assume we did one build. Now I modify hook/build.dart by adding a few comments.

Next time we run a build, the system sees that hook sources changed and it will re-compile the hook, giving us a new dill file.

Now, since we only changed comments in the hook/build.dart, we will get an identical .dill file.

So the next step is running the hook. Though we can skip running the hook if none of the hook-reported deps changed and the hook.dill didn't change (which it didn't)
=> In this scenario we could avoid running the hook.

Though this code looks like we're going to re-run the hook, because we add the hooks.d file here (where hashes of sources changed, we we added some comments).

(One could argue this redundant run is ok because it's very uncommon, but from conceptual point we should only re-run things when needed)

],
lastModifiedCutoffTime,
environment,
Expand All @@ -547,7 +544,7 @@ ${e.message}
logger.severe('File modified during build. Build must be rerun.');
}
}
return result;
return (result, hookHashes.fileSystemEntities);
},
);
}
Expand Down Expand Up @@ -685,7 +682,7 @@ ${e.message}
///
/// TODO(https://github.com/dart-lang/native/issues/1578): Compile only once
/// instead of per config. This requires more locking.
Future<(bool success, File kernelFile, File cacheFile)>
Future<(File kernelFile, DependenciesHashFile cacheFile)?>
_compileHookForPackageCached(
String packageName,
Uri outputDirectory,
Expand Down Expand Up @@ -721,7 +718,7 @@ ${e.message}
}

if (!mustCompile) {
return (true, kernelFile, dependenciesHashFile);
return (kernelFile, dependenciesHashes);
}

final success = await _compileHookForPackage(
Expand All @@ -733,8 +730,7 @@ ${e.message}
depFile,
);
if (!success) {
await dependenciesHashFile.delete();
return (success, kernelFile, dependenciesHashFile);
return null;
}

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

return (success, kernelFile, dependenciesHashFile);
}

Future<List<Uri>> _readDepFile(File depFile) async {
// Format: `path/to/my.dill: path/to/my.dart, path/to/more.dart`
final depFileContents = await depFile.readAsString();
final dartSources = depFileContents
.trim()
.split(' ')
.skip(1) // '<kernel file>:'
.map(Uri.file)
.toList();
return dartSources;
return (kernelFile, dependenciesHashes);
}

Future<bool> _compileHookForPackage(
Expand Down Expand Up @@ -811,6 +795,12 @@ ${compileResult.stdout}
''',
);
success = false;
if (await depFile.exists()) {
await depFile.delete();
}
if (await kernelFile.exists()) {
await kernelFile.delete();
}
}
return success;
}
Expand Down Expand Up @@ -927,3 +917,57 @@ ${compileResult.stdout}
extension on Uri {
Uri get parent => File(toFilePath()).parent.uri;
}

/// Parses depfile contents.
///
/// Format: `path/to/my.dill: path/to/my.dart, path/to/more.dart`
///
/// However, the spaces in paths are escaped with backslashes, and the
/// backslashes are escaped with backslashes:
///
/// ```dart
/// String _escapePath(String path) {
/// return path.replaceAll('\\', '\\\\').replaceAll(' ', '\\ ');
/// }
/// ```
List<String> parseDepFileInputs(String contents) {
final output = contents.substring(0, contents.indexOf(': '));
contents = contents.substring(output.length + ': '.length).trim();
final pathsEscaped = _splitOnNonEscapedSpaces(contents);
return pathsEscaped.map(_unescapeDepsPath).toList();
}

String _unescapeDepsPath(String path) =>
path.replaceAll(r'\ ', ' ').replaceAll(r'\\', r'\');

List<String> _splitOnNonEscapedSpaces(String contents) {
var index = 0;
final result = <String>[];
while (index < contents.length) {
final start = index;
while (index < contents.length) {
final u = contents.codeUnitAt(index);
if (u == ' '.codeUnitAt(0)) {
break;
}
if (u == r'\'.codeUnitAt(0)) {
index++;
if (index == contents.length) {
throw const FormatException('malformed, ending with backslash');
}
final v = contents.codeUnitAt(index);
assert(v == ' '.codeUnitAt(0) || v == r'\'.codeUnitAt(0));
}
index++;
}
result.add(contents.substring(start, index));
index++;
}
return result;
}

Future<List<Uri>> _readDepFile(File depFile) async {
final depFileContents = await depFile.readAsString();
final dartSources = parseDepFileInputs(depFileContents);
return dartSources.map(Uri.file).toList();
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,21 @@ import '../utils/uri.dart';

class DependenciesHashFile {
DependenciesHashFile({
required File file,
}) : _file = file;
required this.file,
});

final File _file;
final File file;
FileSystemHashes _hashes = FileSystemHashes();

List<Uri> get fileSystemEntities => _hashes.files.map((e) => e.path).toList();

Future<void> _readFile() async {
if (!await _file.exists()) {
if (!await file.exists()) {
_hashes = FileSystemHashes();
return;
}
final jsonObject =
(json.decode(utf8.decode(await _file.readAsBytes())) as Map)
(json.decode(utf8.decode(await file.readAsBytes())) as Map)
.cast<String, Object>();
_hashes = FileSystemHashes.fromJson(jsonObject);
}
Expand Down Expand Up @@ -70,7 +72,7 @@ class DependenciesHashFile {
return modifiedAfterTimeStamp;
}

Future<void> _persist() => _file.writeAsString(json.encode(_hashes.toJson()));
Future<void> _persist() => file.writeAsString(json.encode(_hashes.toJson()));

/// Reads the file with hashes and reports if there is an outdated file,
/// directory or environment variable.
Expand Down
3 changes: 2 additions & 1 deletion pkgs/native_assets_builder/lib/src/model/hook_result.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ final class HookResult implements BuildResult, BuildDryRunResult, LinkResult {
dependencies: dependencies ?? [],
);

HookResult copyAdd(HookOutput hookOutput) {
HookResult copyAdd(HookOutput hookOutput, List<Uri> hookDependencies) {
final mergedMaps = mergeMaps(
encodedAssetsForLinking,
hookOutput is BuildOutput
Expand All @@ -61,6 +61,7 @@ final class HookResult implements BuildResult, BuildDryRunResult, LinkResult {
dependencies: [
...dependencies,
...hookOutput.dependencies,
...hookDependencies,
]..sort(_uriCompare),
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,28 @@ void main() async {
expect(result.encodedAssets.length, 2);
expect(
result.dependencies,
[
tempUri.resolve('native_add/').resolve('src/native_add.c'),
tempUri
.resolve('native_subtract/')
.resolve('src/native_subtract.c'),
],
containsAll([
tempUri.resolve('native_add/src/native_add.c'),
tempUri.resolve('native_subtract/src/native_subtract.c'),
if (!Platform.isWindows) ...[
tempUri.resolve('native_add/hook/build.dart'),
tempUri.resolve('native_subtract/hook/build.dart'),
],
]),
);
if (Platform.isWindows) {
expect(
// https://github.com/dart-lang/sdk/issues/59657
// Deps file on windows sometimes have lowercase drive letters.
// File.exists will work, but Uri equality doesn't.
result.dependencies
.map((e) => Uri.file(e.toFilePath().toLowerCase())),
containsAll([
tempUri.resolve('native_add/hook/build.dart'),
tempUri.resolve('native_subtract/hook/build.dart'),
].map((e) => Uri.file(e.toFilePath().toLowerCase()))),
);
}
}
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ void main() async {
);
expect(
result.dependencies,
[
contains(
packageUri.resolve('src/native_add.c'),
],
),
);
}

Expand Down Expand Up @@ -80,9 +80,9 @@ void main() async {
);
expect(
result.dependencies,
[
contains(
packageUri.resolve('src/native_add.c'),
],
),
);
}
});
Expand Down
Loading
Loading