Skip to content

Commit

Permalink
Build macros.
Browse files Browse the repository at this point in the history
  • Loading branch information
davidmorgan committed Jul 22, 2024
1 parent afefe34 commit fc0a4a9
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 14 deletions.
90 changes: 83 additions & 7 deletions pkgs/_macro_builder/lib/macro_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,98 @@
// 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';

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.
Future<BuiltMacroBundle> build(
Iterable<QualifiedName> macroImplementations) async {
// TODO(davidmorgan): implement.
// Generated entrypoint will instantiate all the `Macro` instances pointed
// to by `macroImplementations` then pass them to `MacroClient.run` in
// `package:_macro_client`.
return BuiltMacroBundle();
File packageConfig, Iterable<QualifiedName> macroImplementations) async {
final script = createBootstrap(macroImplementations.toList());

return await MacroBuild(packageConfig, script).build();
}

static String createBootstrap(List<QualifiedName> macros) {
final script = StringBuffer();
for (var i = 0; i != macros.length; ++i) {
final macro = macros[i];
script.writeln("import '${macro.uri}' as m$i;");
}
script.write('''
import 'dart:convert';
import 'package:_macro_client/macro_client.dart';
import 'package:macro_service/macro_service.dart';
void main(List<String> arguments) {
MacroClient().run(
HostEndpoint.fromJson(json.decode(arguments[0])),
[''');
for (var i = 0; i != macros.length; ++i) {
final macro = macros[i];
script.write('m$i.${macro.name}()');
if (i != macros.length - 1) script.write(', ');
}
script.writeln(']);');
script.writeln('}');
return script.toString();
}
}

/// A bundle of one or more macros that's ready to execute.
class BuiltMacroBundle {}
class BuiltMacroBundle {
// TODO(davidmorgan): other formats besides executable.
final String executablePath;

BuiltMacroBundle(this.executablePath);
}

/// A single build.
class MacroBuild {
final File packageConfig;
final String script;
final Directory workspace =
Directory.systemTemp.createTempSync('macro_builder');

MacroBuild(this.packageConfig, this.script);

Future<BuiltMacroBundle> build() async {
final scriptFile = File.fromUri(workspace.uri.resolve('bin/main.dart'));
scriptFile.parent.createSync(recursive: true);
scriptFile.writeAsStringSync(script.toString());

// TODO(davidmorgan): replace relative paths to absolute.
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('dart', ['compile', 'exe', 'bin/main.dart'],
workingDirectory: workspace.path);
if (result.exitCode != 0) {
throw StateError('Compile failed: ${result.stderr}');
}

return BuiltMacroBundle(scriptFile.parent.uri.resolve('main.exe').path);
}

String _makePackageConfigAbsolute(File pubspec) {
final root = pubspec.parent.parent.absolute.uri;
return pubspec
.readAsStringSync()
.replaceAll('"rootUri": "../', '"rootUri": "$root');
}
}
5 changes: 5 additions & 0 deletions pkgs/_macro_builder/mono_pkg.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,8 @@ stages:
- format:
sdk:
- dev
- unit_test:
- test: --test-randomize-ordering-seed=random
os:
- linux
- windows
1 change: 1 addition & 0 deletions pkgs/_macro_builder/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ dependencies:

dev_dependencies:
dart_flutter_team_lints: ^3.0.0
test: ^1.25.0
50 changes: 50 additions & 0 deletions pkgs/_macro_builder/test/macro_builder_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// 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:_macro_builder/macro_builder.dart';
import 'package:dart_model/dart_model.dart';
import 'package:test/test.dart';

void main() {
group(MacroBuilder, () {
test('bootstrap matches golden', () async {
final script = MacroBuilder.createBootstrap([
QualifiedName('package:_test_macros/declare_x_macro.dart#DeclareX'),
QualifiedName('package:_test_macros/declare_y_macro.dart#DeclareY'),
QualifiedName(
'package:_more_macros/other_macro.dart#OtherMacroImplementation')
]);

expect(script, '''
import 'package:_test_macros/declare_x_macro.dart' as m0;
import 'package:_test_macros/declare_y_macro.dart' as m1;
import 'package:_more_macros/other_macro.dart' as m2;
import 'dart:convert';
import 'package:_macro_client/macro_client.dart';
import 'package:macro_service/macro_service.dart';
void main(List<String> arguments) {
MacroClient().run(
HostEndpoint.fromJson(json.decode(arguments[0])),
[m0.DeclareX(), m1.DeclareY(), m2.OtherMacroImplementation()]);
}
''');
});

test('builds macros', () async {
final builder = MacroBuilder();

final bundle = await builder.build(
File.fromUri(Uri.parse('../../.dart_tool/package_config.json')), [
QualifiedName(
'package:_test_macros/declare_x_macro.dart#DeclareXImplementation')
]);

expect(File(bundle.executablePath).existsSync(), true);
});
});
}
6 changes: 3 additions & 3 deletions pkgs/dart_model/lib/src/dart_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import 'dart_model.g.dart';

export 'dart_model.g.dart';

// TODO(davidmorgan): remove example when we have an actual extension method.
extension ModelExtension on Model {
String get exampleGetter => 'exampleValue';
extension QualifiedNameExtension on QualifiedName {
String get uri => string.substring(0, string.indexOf('#'));
String get name => string.substring(string.indexOf('#') + 1);
}
15 changes: 11 additions & 4 deletions pkgs/dart_model/test/model_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,6 @@ void main() {
}
};

test('has extension methods', () {
expect(model.exampleGetter, 'exampleValue');
});

test('maps to JSON', () {
expect(model as Map, expected);
});
Expand Down Expand Up @@ -104,4 +100,15 @@ void main() {
'JsonData']!['members']);
});
});

group(QualifiedName, () {
test('has uri', () {
expect(QualifiedName('package:foo/foo.dart#Foo').uri,
'package:foo/foo.dart');
});

test('has name', () {
expect(QualifiedName('package:foo/foo.dart#Foo').uri, 'Foo');
});
});
}

0 comments on commit fc0a4a9

Please sign in to comment.