diff --git a/pkgs/_macro_builder/lib/macro_builder.dart b/pkgs/_macro_builder/lib/macro_builder.dart index 826b784b..18f62305 100644 --- a/pkgs/_macro_builder/lib/macro_builder.dart +++ b/pkgs/_macro_builder/lib/macro_builder.dart @@ -2,6 +2,8 @@ // 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 { @@ -9,15 +11,89 @@ class MacroBuilder { /// /// 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 build( - Iterable 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 macroImplementations) async { + final script = createBootstrap(macroImplementations.toList()); + + return await MacroBuild(packageConfig, script).build(); + } + + static String createBootstrap(List 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 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 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'); + } +} diff --git a/pkgs/_macro_builder/mono_pkg.yaml b/pkgs/_macro_builder/mono_pkg.yaml index 15378042..457cafcd 100644 --- a/pkgs/_macro_builder/mono_pkg.yaml +++ b/pkgs/_macro_builder/mono_pkg.yaml @@ -8,3 +8,8 @@ stages: - format: sdk: - dev +- unit_test: + - test: --test-randomize-ordering-seed=random + os: + - linux + - windows diff --git a/pkgs/_macro_builder/pubspec.yaml b/pkgs/_macro_builder/pubspec.yaml index fe8a6d2d..eb07ee96 100644 --- a/pkgs/_macro_builder/pubspec.yaml +++ b/pkgs/_macro_builder/pubspec.yaml @@ -13,3 +13,4 @@ dependencies: dev_dependencies: dart_flutter_team_lints: ^3.0.0 + test: ^1.25.0 diff --git a/pkgs/_macro_builder/test/macro_builder_test.dart b/pkgs/_macro_builder/test/macro_builder_test.dart new file mode 100644 index 00000000..10c85037 --- /dev/null +++ b/pkgs/_macro_builder/test/macro_builder_test.dart @@ -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 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); + }); + }); +} diff --git a/pkgs/dart_model/lib/src/dart_model.dart b/pkgs/dart_model/lib/src/dart_model.dart index d21294fe..923666c6 100644 --- a/pkgs/dart_model/lib/src/dart_model.dart +++ b/pkgs/dart_model/lib/src/dart_model.dart @@ -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); } diff --git a/pkgs/dart_model/test/model_test.dart b/pkgs/dart_model/test/model_test.dart index 4fde1815..086c38db 100644 --- a/pkgs/dart_model/test/model_test.dart +++ b/pkgs/dart_model/test/model_test.dart @@ -46,10 +46,6 @@ void main() { } }; - test('has extension methods', () { - expect(model.exampleGetter, 'exampleValue'); - }); - test('maps to JSON', () { expect(model as Map, expected); }); @@ -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'); + }); + }); }