Skip to content
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

perf(mason): compile hooks #613

Merged
merged 24 commits into from Nov 12, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/mason_cli.yaml
Expand Up @@ -93,4 +93,4 @@ jobs:
dart pub global activate pana

- name: Verify Pub Score
run: ../../tool/verify_pub_score.sh
run: ../../tool/verify_pub_score.sh 100
Copy link
Collaborator

Choose a reason for hiding this comment

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

👀 on purpose?

Copy link
Owner Author

Choose a reason for hiding this comment

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

Yeah until mason is published because pana doesn’t take into account dependency overrides

23 changes: 23 additions & 0 deletions packages/mason/lib/src/generator.dart
Expand Up @@ -640,3 +640,26 @@ extension on String {
}
}
}

extension on HookFile {
String get fileHash => sha1.convert(content).toString();

Directory get cacheDirectory {
return Directory(
p.join(
Directory(p.join(Directory.systemTemp.path, '.mason')).path,
sha1.convert(utf8.encode(File(path).parent.absolute.path)).toString(),
),
);
}

Directory get buildDirectory {
return Directory(
p.join(cacheDirectory.path, 'build', p.basenameWithoutExtension(path)),
);
}

File get module {
return File(p.join(buildDirectory.path, '.$fileHash.dill'));
}
}
191 changes: 113 additions & 78 deletions packages/mason/lib/src/hooks.dart
Expand Up @@ -45,15 +45,15 @@ Ensure the hook contains a 'run' method:
);
}

/// {@template hook_run_exception}
/// Thrown when an error occurs when trying to run hook.
/// {@template hook_compile_exception}
/// Thrown when an error occurs when trying to compile a hook.
/// {@endtemplate}
class HookRunException extends MasonException {
/// {@macro hook_run_exception}
HookRunException(String path, String error)
class HookCompileException extends MasonException {
/// {@macro hook_compile_exception}
HookCompileException(String path, String error)
: super(
'''
Unable to execute hook: $path.
Unable to compile hook: $path.
Error: $error''',
);
}
Expand Down Expand Up @@ -202,6 +202,7 @@ class GeneratorHooks {
Map<String, dynamic> vars = const <String, dynamic>{},
String? workingDirectory,
void Function(Map<String, dynamic> vars)? onVarsChanged,
Logger? logger,
}) async {
final preGenHook = this.preGenHook;
if (preGenHook != null && pubspec != null) {
Expand All @@ -210,6 +211,7 @@ class GeneratorHooks {
vars: vars,
workingDirectory: workingDirectory,
onVarsChanged: onVarsChanged,
logger: logger,
);
}
}
Expand All @@ -220,6 +222,7 @@ class GeneratorHooks {
Map<String, dynamic> vars = const <String, dynamic>{},
String? workingDirectory,
void Function(Map<String, dynamic> vars)? onVarsChanged,
Logger? logger,
}) async {
final postGenHook = this.postGenHook;
if (postGenHook != null && pubspec != null) {
Expand All @@ -228,59 +231,49 @@ class GeneratorHooks {
vars: vars,
workingDirectory: workingDirectory,
onVarsChanged: onVarsChanged,
logger: logger,
);
}
}

/// Runs the provided [hook] with the specified [vars].
/// An optional [workingDirectory] can also be specified.
Future<void> _runHook({
required HookFile hook,
Map<String, dynamic> vars = const <String, dynamic>{},
void Function(Map<String, dynamic> vars)? onVarsChanged,
String? workingDirectory,
}) async {
final pubspec = this.pubspec;
final subscriptions = <StreamSubscription>[];
final messagePort = ReceivePort();
final errorPort = ReceivePort();
final exitPort = ReceivePort();
/// Compile all hooks into modules for faster execution.
/// Hooks are compiled lazily by default but calling [compile]
/// can be used to compile hooks ahead of time.
Future<void> compile({Logger? logger}) async {
await _installDependencies();

dynamic hookError;
subscriptions.add(errorPort.listen((dynamic error) => hookError = error));
if (preGenHook != null && !preGenHook!.module.existsSync()) {
await _compile(hook: preGenHook!, logger: logger);
}

if (onVarsChanged != null) {
subscriptions.add(
messagePort.listen((dynamic message) {
if (message is String) {
onVarsChanged(
json.decode(message) as Map<String, dynamic>,
);
}
}),
);
if (postGenHook != null && !postGenHook!.module.existsSync()) {
await _compile(hook: postGenHook!, logger: logger);
}
}

Future<void> _dartPubGet({required String workingDirectory}) async {
final result = await Process.run(
'dart',
['pub', 'get'],
workingDirectory: workingDirectory,
runInShell: true,
Future<void> _dartPubGet({required String workingDirectory}) async {
final result = await Process.run(
'dart',
['pub', 'get'],
workingDirectory: workingDirectory,
runInShell: true,
);
if (result.exitCode != ExitCode.success.code) {
throw HookDependencyInstallFailure(
workingDirectory,
'${result.stderr}',
);
if (result.exitCode != ExitCode.success.code) {
throw HookDependencyInstallFailure(hook.path, '${result.stderr}');
}
}
}

Future<void> _installDependencies() async {
final hook = preGenHook ?? postGenHook;
if (hook == null) return;

final hookCacheDir = hook.cacheDirectory;
final pubspec = this.pubspec;

var dependenciesInstalled = false;
Directory? hookCacheDir;
Uri? packageConfigUri;
if (pubspec != null) {
final directoryHash = sha1.convert(pubspec).toString();
hookCacheDir = Directory(
p.join(Directory.systemTemp.path, '.mason', directoryHash),
);
final packageConfigFile = File(
p.join(hookCacheDir.path, '.dart_tool', 'package_config.json'),
);
Expand All @@ -291,11 +284,18 @@ class GeneratorHooks {
p.join(hookCacheDir.path, 'pubspec.yaml'),
).writeAsBytes(pubspec);
await _dartPubGet(workingDirectory: hookCacheDir.path);
dependenciesInstalled = true;
}

packageConfigUri = packageConfigFile.uri;
}
}

Future<void> _compile({required HookFile hook, Logger? logger}) async {
final hookCacheDir = hook.cacheDirectory;
final hookBuildDir = hook.buildDirectory;
final hookHash = hook.fileHash;

try {
await hookBuildDir.delete(recursive: true);
} catch (_) {}

Uri? uri;
try {
Expand All @@ -307,6 +307,67 @@ class GeneratorHooks {

if (uri == null) throw HookMissingRunException(hook.path);

final hookFile = File(p.join(hookBuildDir.path, '.$hookHash.dart'))
..createSync(recursive: true)
..writeAsBytesSync(uri.data!.contentAsBytes());

final progress = logger?.progress('Compiling ${p.basename(hook.path)}');
final result = await Process.run(
'dart',
['compile', 'kernel', hookFile.path],
workingDirectory: hookCacheDir.path,
runInShell: true,
);

if (result.exitCode != ExitCode.success.code) {
final error = result.stderr.toString();
progress?.fail(error);
throw HookCompileException(hook.path, error);
}

progress?.complete('Compiled ${p.basename(hook.path)}');
}

/// Runs the provided [hook] with the specified [vars].
/// An optional [workingDirectory] can also be specified.
Future<void> _runHook({
required HookFile hook,
Map<String, dynamic> vars = const <String, dynamic>{},
void Function(Map<String, dynamic> vars)? onVarsChanged,
String? workingDirectory,
Logger? logger,
}) async {
final subscriptions = <StreamSubscription>[];
final messagePort = ReceivePort();
final errorPort = ReceivePort();
final exitPort = ReceivePort();

dynamic hookError;
subscriptions.add(errorPort.listen((dynamic error) => hookError = error));

if (onVarsChanged != null) {
subscriptions.add(
messagePort.listen((dynamic message) {
if (message is String) {
onVarsChanged(
json.decode(message) as Map<String, dynamic>,
);
}
}),
);
}

await _installDependencies();
final hookCacheDir = hook.cacheDirectory;
final packageConfigUri = File(
p.join(hookCacheDir.path, '.dart_tool', 'package_config.json'),
).uri;
final module = hook.module;

if (!module.existsSync()) {
await _compile(hook: hook, logger: logger);
}

Future<Isolate> spawnIsolate(Uri uri) {
return Isolate.spawnUri(
uri,
Expand All @@ -318,35 +379,9 @@ class GeneratorHooks {
}

final cwd = Directory.current;
Isolate? isolate;
try {
if (workingDirectory != null) Directory.current = workingDirectory;
isolate = await spawnIsolate(uri);
} on IsolateSpawnException catch (error) {
Never throwHookRunException(IsolateSpawnException error) {
Directory.current = cwd;
final msg = error.message;
final content =
msg.contains('Error: ') ? msg.split('Error: ').last : msg;
throw HookRunException(hook.path, content.trim());
}

final shouldRetry = !dependenciesInstalled && hookCacheDir != null;
if (workingDirectory != null) Directory.current = workingDirectory;

if (!shouldRetry) throwHookRunException(error);

// Failure to spawn the isolate could be due to changes in the pub cache.
// We attempt to reinstall hook dependencies.
await _dartPubGet(workingDirectory: hookCacheDir.path);

// Retry spawning the isolate if the hook dependencies
// have been successfully reinstalled.
try {
isolate = await spawnIsolate(uri);
} on IsolateSpawnException catch (error) {
throwHookRunException(error);
}
}
final isolate = await spawnIsolate(module.uri);

isolate
..addErrorListener(errorPort.sendPort)
Expand Down
3 changes: 3 additions & 0 deletions packages/mason/test/fixtures/basic/hooks/post_gen.dart
@@ -0,0 +1,3 @@
import 'package:mason/mason.dart';

void run(HookContext context) {}