blob: 461a7b2d43f4c640ffead2ef8b259ec213023d74 [file] [log] [blame]
// Copyright (c) 2024, 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:io';
import 'package:dart_model/dart_model.dart';
import 'src/bootstrap.dart';
/// Builds macros.
///
/// TODO(davidmorgan): add a way to clean up generated files and built output.
class MacroBuilder {
/// Builds an executable from user-written macro code.
///
/// Each `QualifiedName` in [macroImplementations] must point to a class that
/// implements `Macro` from `package:macro`.
///
/// The [packageConfig] must include the macros and all their deps.
///
/// TODO(davidmorgan): figure out builder lifecycle: is it one builder per
/// host, one per workspace, one per build?
/// TODO(davidmorgan): replace `File` packageConfig with a concept of version
/// solve and workspace.
/// TODO(davidmorgan): support for multi-root workspaces.
/// TODO(davidmorgan): support (or decide not to support) in-memory overlay
/// filesystems.
Future<BuiltMacroBundle> build(
Uri packageConfig,
Iterable<QualifiedName> macroImplementations,
) async {
final script = createBootstrap(macroImplementations.toList());
return await MacroBuild(packageConfig, script).build();
}
}
/// A bundle of one or more macros that's ready to execute.
class BuiltMacroBundle {
// TODO(davidmorgan): other formats besides executable.
final String executablePath;
BuiltMacroBundle(this.executablePath);
}
/// A single build.
///
/// TODO(davidmorgan): split to interface+implementations as we add different
/// ways to build.
class MacroBuild {
final Uri packageConfig;
final String script;
final Directory workspace = Directory.systemTemp.createTempSync(
'macro_builder',
);
/// Creates a build for [script] with [packageConfig], which must have all
/// the needed deps.
MacroBuild(this.packageConfig, this.script);
/// Runs the build.
///
/// Throws on failure to build.
Future<BuiltMacroBundle> build() async {
final scriptFile = File.fromUri(workspace.uri.resolve('bin/main.dart'));
await scriptFile.create(recursive: true);
await scriptFile.writeAsString(script.toString());
final targetPackageConfig = File.fromUri(
workspace.uri.resolve('.dart_tool/package_config.json'),
);
targetPackageConfig.parent.createSync(recursive: true);
targetPackageConfig.writeAsStringSync(
_makePackageConfigAbsolute(packageConfig),
);
// See package:analyzer/src/summary2/kernel_compilation_service.dart for an
// example of compiling macros using the frontend server.
//
// For now just use the command line.
final result = Process.runSync(
// TODO(davidmorgan): this is wrong if run from an AOT-compiled
// executable.
Platform.resolvedExecutable,
['compile', 'exe', 'bin/main.dart', '--output=bin/main.exe'],
workingDirectory: workspace.path,
);
if (result.exitCode != 0) {
throw StateError('Compile failed: ${result.stderr}');
}
return BuiltMacroBundle(
File.fromUri(scriptFile.parent.uri.resolve('main.exe')).path,
);
}
/// Returns the contents of [packageConfig] with relative paths replaced to
/// absolute paths, so the pubspec will work from any location.
String _makePackageConfigAbsolute(Uri packageConfig) {
final file = File.fromUri(packageConfig);
final root = file.parent.parent.absolute.uri;
return file.readAsStringSync().replaceAll(
'"rootUri": "../',
'"rootUri": "$root',
);
}
}