diff --git a/benchmarks/tool/compile_protos.sh b/benchmarks/tool/compile_protos.sh index 1b05e61f7..193d90a98 100755 --- a/benchmarks/tool/compile_protos.sh +++ b/benchmarks/tool/compile_protos.sh @@ -4,26 +4,6 @@ # 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. -SCRIPT_DIR=$(dirname "${BASH_SOURCE}") -BENCHMARK_DIR=$SCRIPT_DIR/.. - -# These protos don't have any imports -SIMPLE_PROTOS=( - "protos/google_message1_proto2.proto" - "protos/google_message1_proto3.proto" - "protos/google_message2.proto" - "protos/packed_fields.proto" -) - -set -x -set -e - -mkdir -p lib/generated - -protoc --dart_out=lib/generated --plugin=protoc-gen-dart=tool/run_protoc_plugin.sh \ - -I$BENCHMARK_DIR/protos \ - "${SIMPLE_PROTOS[@]/#/$BENCHMARK_DIR/}" - -protoc --dart_out=lib/generated --plugin=protoc-gen-dart=tool/run_protoc_plugin.sh \ - -I$BENCHMARK_DIR/protos/query_benchmark \ - $BENCHMARK_DIR/protos/query_benchmark/*.proto +dart run protoc_plugin:generate \ + --proto-path=protos/query_benchmark \ + --out=lib/generated diff --git a/benchmarks/tool/run_protoc_plugin.sh b/benchmarks/tool/run_protoc_plugin.sh deleted file mode 100755 index e32cf9880..000000000 --- a/benchmarks/tool/run_protoc_plugin.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -# Copyright (c) 2022, 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. - -dart run protoc_plugin protoc-gen-dart diff --git a/protoc_plugin/Makefile b/protoc_plugin/Makefile index 217fdfd84..da7ea56fd 100644 --- a/protoc_plugin/Makefile +++ b/protoc_plugin/Makefile @@ -82,17 +82,13 @@ PREGENERATED_SRCS := $(shell find protos -name '*.proto') $(TEST_PROTO_LIBS): $(PLUGIN_SRC) $(TEST_PROTO_SRCS) mkdir -p $(TEST_PROTO_DIR) - protoc\ - --dart_out="$(TEST_PROTO_DIR)"\ - -I$(TEST_PROTO_SRC_DIR)\ - -Iprotos\ - --plugin=protoc-gen-dart=$(realpath $(PLUGIN_PATH))\ + dart run protoc_plugin:generate \ + --out="$(TEST_PROTO_DIR)" \ + --proto-path=$(TEST_PROTO_SRC_DIR) \ $(TEST_PROTO_SRCS) update-pregenerated: $(PLUGIN_PATH) $(PREGENERATED_SRCS) - protoc --dart_out=lib/src/gen \ - -Iprotos \ - --plugin=protoc-gen-dart=$(realpath $(PLUGIN_PATH)) $(PREGENERATED_SRCS) + dart run protoc_plugin:generate find lib/src/gen -name '*.pbjson.dart' -delete protos: $(PLUGIN_PATH) $(TEST_PROTO_LIBS) diff --git a/protoc_plugin/bin/generate.dart b/protoc_plugin/bin/generate.dart new file mode 100644 index 000000000..ec534e016 --- /dev/null +++ b/protoc_plugin/bin/generate.dart @@ -0,0 +1,14 @@ +#!/usr/bin/env dart +// Copyright (c) 2025, 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. + +/// A command line utility to allow easy generation of protobuf files. +library; + +import 'package:protoc_plugin/generate.dart'; + +void main(List args) { + final generator = ProtoGen(); + generator.generate(args); +} diff --git a/protoc_plugin/bin/protoc_plugin.dart b/protoc_plugin/bin/protoc_plugin.dart index 54da34c64..d9668068d 100755 --- a/protoc_plugin/bin/protoc_plugin.dart +++ b/protoc_plugin/bin/protoc_plugin.dart @@ -3,10 +3,21 @@ // 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. +/// This entry-point is expected to be called from the `protoc` command-line +/// tool. +/// +/// It will read it's binary, protobuf encoded input from stdin and write its +/// cooresponding output to stdout. +/// +/// See https://protobuf.dev/reference/other/ for more information about +/// Protobuf compiler plugins. +library; + import 'dart:io'; import 'package:protoc_plugin/protoc.dart'; -void main() { - CodeGenerator(stdin, stdout).generate(); +void main() async { + final generator = CodeGenerator(stdin, stdout); + await generator.generate(); } diff --git a/protoc_plugin/bin/protoc_plugin_bazel.dart b/protoc_plugin/bin/protoc_plugin_bazel.dart index c6dd2a6ba..637121cca 100755 --- a/protoc_plugin/bin/protoc_plugin_bazel.dart +++ b/protoc_plugin/bin/protoc_plugin_bazel.dart @@ -8,9 +8,10 @@ import 'dart:io'; import 'package:protoc_plugin/bazel.dart'; import 'package:protoc_plugin/protoc.dart'; -void main() { +void main() async { final packages = {}; - CodeGenerator(stdin, stdout).generate( + final generator = CodeGenerator(stdin, stdout); + await generator.generate( optionParsers: {bazelOptionId: BazelOptionParser(packages)}, config: BazelOutputConfiguration(packages), ); diff --git a/protoc_plugin/lib/generate.dart b/protoc_plugin/lib/generate.dart new file mode 100644 index 000000000..b392a7600 --- /dev/null +++ b/protoc_plugin/lib/generate.dart @@ -0,0 +1,220 @@ +#!/usr/bin/env dart +// Copyright (c) 2025, 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. + +/// A command line utility to allow easy generation of protobuf files. +library; + +import 'dart:convert'; +import 'dart:io'; + +import 'package:args/args.dart'; + +import 'src/generate_utils.dart'; + +// TODO: update readme docs + +// TODO: review the UI here + +// TODO: have an option to not include defaults + +// TODO: generate grpc by default? + +// TODO: add tests + +class ProtoGen { + void generate(List args) { + final parser = _createArgParser(); + + ArgResults results; + + try { + results = parser.parse(args); + } on ArgParserException catch (e) { + _usage(e.message, parser, isFailure: true); + } + + if (results.flag('help')) { + _usage( + 'A wrapper around protoc to make it easier to generate Dart code from ' + 'protobuf definitions.', + parser, + ); + } + + final out = results.option('out'); + final version = results.flag('version'); + final grpc = results.flag('grpc'); + final protoPaths = results.multiOption('proto-path'); + var protos = results.rest; + + if (out == null) { + _usage( + 'The output directory option (--out) is required.', + parser, + isFailure: true, + ); + } else { + final outDir = Directory(out); + if (!outDir.existsSync()) { + outDir.createSync(recursive: true); + } + } + + final protoDir = Directory('protos'); + + // paths + + for (final dir in protoPaths) { + if (!Directory(dir).existsSync()) { + _usage( + "The --proto-path '$dir' does not exist.", + parser, + isFailure: true, + ); + } + } + + // auto-setup protoPaths + if (protoDir.existsSync() && !protoPaths.contains(protoDir.path)) { + protoPaths.add(protoDir.path); + } + + final defaultProtosPath = calculateDefaultProtosPath(); + if (defaultProtosPath != null && !protoPaths.contains(defaultProtosPath)) { + protoPaths.add(defaultProtosPath); + } + + // files + + for (final file in protos) { + if (!File(file).existsSync()) { + _usage( + "The proto file '$file' does not exist.", + parser, + isFailure: true, + ); + } + } + + // We either need an explicit list of files to compile, or that a 'protos' + // directory exists. + if (protos.isEmpty) { + if (protoDir.existsSync()) { + final protoFiles = protoDir + .listSync(recursive: true) + .whereType() + .where((file) => file.path.endsWith('.proto')); + protos = protoFiles.map((file) => file.path).toList(); + } else { + _usage('No proto files passed.', parser, isFailure: true); + } + } + + // detect protoc ('protoc --version') + if (_protocVersion() == null) { + _usage( + ''' +'protoc' not found on the path. + +See installation options at: https://protobuf.dev/installation/. + ''', + parser, + isFailure: true, + ); + } + + // handle --version + if (version) { + print(_protocVersion()); + exit(0); + } + + protoc(out: out, grpc: grpc, protoPaths: protoPaths, protos: protos); + } + + void protoc({ + required String out, + required bool grpc, + required List protoPaths, + required List protos, + }) async { + final execScriptHelper = TempExecScriptHelper(); + final execScript = execScriptHelper.execScript; + + final args = []; + if (grpc) { + args.add('--dart_out=grpc:$out'); + } else { + args.add('--dart_out=$out'); + } + args.add('--plugin=${execScript.absolute.path}'); + args.addAll([...(protoPaths.map((p) => '--proto_path=$p'))]); + args.addAll(protos); + + // todo: + print('protoc ${args.join(' ')}'); + print(''); + + final process = await Process.start('protoc', args); + process.stdout.transform(const Utf8Decoder()).listen((line) { + stdout.writeln(line); + }); + process.stderr.transform(const Utf8Decoder()).listen((line) { + stderr.writeln(line); + }); + exitCode = await process.exitCode; + + execScriptHelper.dispose(); + } + + static ArgParser _createArgParser() { + final parser = ArgParser(); + + parser.addFlag( + 'help', + abbr: 'h', + negatable: false, + help: 'Print this usage information.', + ); + parser.addFlag( + 'version', + negatable: false, + help: 'Display the protoc version in use.', + ); + parser.addMultiOption( + 'proto-path', + valueHelp: 'directory', + help: 'Specify the directories in which to search for imported protos.', + ); + parser.addOption( + 'out', + defaultsTo: 'lib/src/gen', + valueHelp: 'directory', + help: 'The output directory for the generated code.', + ); + parser.addFlag( + 'grpc', + negatable: false, + help: 'Generate gRPC service classes.', + ); + + return parser; + } + + Never _usage(String message, ArgParser parser, {bool isFailure = false}) { + stderr.writeln(message); + stderr.writeln(); + stderr.writeln( + 'usage: dart run protoc_plugin:generate [options] ', + ); + stderr.writeln(parser.usage); + exit(isFailure ? 64 : 0); + } + + static String? _protocVersion() { + final result = Process.runSync('protoc', ['--version']); + return result.exitCode == 0 ? (result.stdout as String).trim() : null; + } +} diff --git a/protoc_plugin/lib/src/generate_utils.dart b/protoc_plugin/lib/src/generate_utils.dart new file mode 100644 index 000000000..f751b03cc --- /dev/null +++ b/protoc_plugin/lib/src/generate_utils.dart @@ -0,0 +1,66 @@ +// Copyright (c) 2025, 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:path/path.dart' as path; + +import 'shared.dart'; + +String? calculateDefaultProtosPath() { + var protocPath = which('protoc'); + if (protocPath == null) { + return null; + } + + // resolve any symlink + if (FileSystemEntity.isLinkSync(protocPath)) { + final link = Link(protocPath); + protocPath = link.resolveSymbolicLinksSync(); + } + + // return the include dir + // "29.3/bin/protoc-29.3.0" ==> "29.3/include" + final rootDir = path.dirname(path.dirname(protocPath)); + final includeDir = Directory(path.join(rootDir, 'include')); + return includeDir.existsSync() ? includeDir.path : null; +} + +/// todo: doc +class TempExecScriptHelper { + late final Directory _tempDir; + late final File execScript; + + TempExecScriptHelper() { + _tempDir = Directory.systemTemp.createTempSync(); + execScript = _createTempExecScript(_tempDir); + } + + void dispose() { + if (_tempDir.existsSync()) { + _tempDir.deleteSync(recursive: true); + } + } + + static File _createTempExecScript(Directory parentDir) { + final name = Platform.isWindows ? 'protoc-gen-dart.bat' : 'protoc-gen-dart'; + final file = File(path.join(parentDir.path, name)); + + if (Platform.isWindows) { + file.writeAsStringSync(r''' +@echo off +dart run protoc_plugin -c "$@" +'''); + } else { + file.writeAsStringSync(r''' +#!/bin/bash +dart run protoc_plugin -c "$@" +'''); + + Process.runSync('chmod', ['+x', file.absolute.path]); + } + + return file; + } +} diff --git a/protoc_plugin/lib/src/shared.dart b/protoc_plugin/lib/src/shared.dart index 6730c761a..f8138d8bd 100644 --- a/protoc_plugin/lib/src/shared.dart +++ b/protoc_plugin/lib/src/shared.dart @@ -102,3 +102,10 @@ String? toDartComment(String value) { } final _leadingSpaces = RegExp('^ +'); + +/// Use the platform dependent 'which' command to locate a binary. +String? which(String commandName) { + final cmd = Platform.isWindows ? 'where' : 'which'; + final result = Process.runSync(cmd, [commandName]); + return result.exitCode == 0 ? (result.stdout as String).trim() : null; +} diff --git a/protoc_plugin/pubspec.yaml b/protoc_plugin/pubspec.yaml index 3f6fa2609..a5781df48 100644 --- a/protoc_plugin/pubspec.yaml +++ b/protoc_plugin/pubspec.yaml @@ -10,6 +10,7 @@ environment: resolution: workspace dependencies: + args: ^2.7.0 collection: ^1.15.0 dart_style: ^3.0.0 fixnum: ^1.0.0 @@ -22,4 +23,5 @@ dev_dependencies: test: ^1.16.0 executables: + generate: protoc-gen-dart: protoc_plugin diff --git a/protoc_plugin/test/generate_utils_test.dart b/protoc_plugin/test/generate_utils_test.dart new file mode 100644 index 000000000..35a3012d3 --- /dev/null +++ b/protoc_plugin/test/generate_utils_test.dart @@ -0,0 +1,45 @@ +// Copyright (c) 2025, 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. + +@TestOn('vm') +library; + +import 'package:protoc_plugin/src/generate_utils.dart'; +import 'package:protoc_plugin/src/shared.dart'; +import 'package:test/test.dart'; + +void main() { + test('calculateDefaultProtosPath', () { + final actual = calculateDefaultProtosPath(); + expect(actual, isNotNull); + }, skip: noProtoc()); + + group('TempExecScriptHelper', () { + TempExecScriptHelper? helper; + + tearDown(() { + helper?.dispose(); + }); + + test('create', () { + helper = TempExecScriptHelper(); + + expect(helper!.execScript.existsSync(), true); + expect( + helper!.execScript.readAsStringSync(), + contains('dart run protoc_plugin'), + ); + }); + + test('dispose', () { + helper = TempExecScriptHelper(); + + expect(helper!.execScript.existsSync(), true); + helper!.dispose(); + expect(helper!.execScript.existsSync(), false); + }); + }); +} + +String? noProtoc() => which('protoc') == null ? 'protoc not found' : null; diff --git a/protoc_plugin/test/shared_test.dart b/protoc_plugin/test/shared_test.dart index a8397505d..f7296e8ab 100644 --- a/protoc_plugin/test/shared_test.dart +++ b/protoc_plugin/test/shared_test.dart @@ -2,6 +2,9 @@ // 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. +@TestOn('vm') +library; + import 'package:protoc_plugin/src/shared.dart'; import 'package:test/test.dart'; @@ -45,4 +48,16 @@ void main() { ); }); }); + + group('which', () { + test('can locate a command', () { + final actual = which('dart'); + expect(actual, isNotNull); + }); + + test('missing command returns null', () { + final actual = which('foo-bar-command'); + expect(actual, isNull); + }); + }); }