Skip to content

add a protoc_plugin:generate CLI #1034

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

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 3 additions & 23 deletions benchmarks/tool/compile_protos.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
7 changes: 0 additions & 7 deletions benchmarks/tool/run_protoc_plugin.sh

This file was deleted.

12 changes: 4 additions & 8 deletions protoc_plugin/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
14 changes: 14 additions & 0 deletions protoc_plugin/bin/generate.dart
Original file line number Diff line number Diff line change
@@ -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<String> args) {
final generator = ProtoGen();
generator.generate(args);
}
15 changes: 13 additions & 2 deletions protoc_plugin/bin/protoc_plugin.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
5 changes: 3 additions & 2 deletions protoc_plugin/bin/protoc_plugin_bazel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <String, BazelPackage>{};
CodeGenerator(stdin, stdout).generate(
final generator = CodeGenerator(stdin, stdout);
await generator.generate(
optionParsers: {bazelOptionId: BazelOptionParser(packages)},
config: BazelOutputConfiguration(packages),
);
Expand Down
220 changes: 220 additions & 0 deletions protoc_plugin/lib/generate.dart
Original file line number Diff line number Diff line change
@@ -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<String> 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<File>()
.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<String> protoPaths,
required List<String> protos,
}) async {
final execScriptHelper = TempExecScriptHelper();
final execScript = execScriptHelper.execScript;

final args = <String>[];
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] <proto files>',
);
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;
}
}
66 changes: 66 additions & 0 deletions protoc_plugin/lib/src/generate_utils.dart
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading
Loading