Add support for Bazel
When generating Dart proto in Bazel, source protos are located relative
to their Bazel package and the `.pb.dart` output is emitted to an
equivalent path under the lib dir. e.g., `//a/b/c:d/e/f.proto` will
generate an output at `//a/b/c:lib/d/e/f.pb.dart`.
diff --git a/bin/protoc_plugin_bazel.dart b/bin/protoc_plugin_bazel.dart
new file mode 100755
index 0000000..4ab1b79
--- /dev/null
+++ b/bin/protoc_plugin_bazel.dart
@@ -0,0 +1,15 @@
+#!/usr/bin/env dart
+// Copyright (c) 2016, 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:protoc_plugin/bazel.dart';
+import 'package:protoc_plugin/protoc.dart';
+
+void main() {
+ var packages = {};
+ new CodeGenerator(stdin, stdout).generate(
+ optionParsers: {bazelOptionId: new BazelOptionParser(packages)},
+ config: new BazelOutputConfiguration(packages));
+}
diff --git a/lib/bazel.dart b/lib/bazel.dart
new file mode 100644
index 0000000..6bfe90a
--- /dev/null
+++ b/lib/bazel.dart
@@ -0,0 +1,146 @@
+// Copyright (c) 2016, 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.
+
+/// Bazel support for protoc_plugin.
+library protoc_bazel;
+
+import 'package:path/path.dart' as p;
+import 'protoc.dart' show SingleOptionParser, DefaultOutputConfiguration;
+
+/// Dart protoc plugin option for Bazel packages.
+///
+/// This option takes a semicolon-separated list of Bazel package metadata in
+/// `package_name|input_root|output_root` form. `input_root` designates the
+/// directory relative to which input protos are located -- typically the root
+/// of the Bazel package, where the `BUILD` file is located. `output_root`
+/// designates the directory relative to which generated `.pb.dart` outputs are
+/// emitted -- typically the package's `lib/` directory under the genfiles
+/// directory specified by `genfiles_dir` in the Bazel configuration. Generated
+/// outputs are emitted at the same path relative to `output_root` as the input
+/// proto is found relative to `input_root`.
+///
+/// For example, using `foo.bar|foo/bar|foo/bar/lib`:
+/// * `foo/bar/baz.proto` will generate `foo/bar/lib/baz.pb.dart`
+/// * `foo/bar/a/b/baz.proto` will generate `foo/bar/lib/a/b/baz.pb.dart`
+const bazelOptionId = 'BazelPackages';
+
+class BazelPackage {
+ final String name;
+ final String input_root;
+ final String output_root;
+
+ BazelPackage(this.name, String input_root, String output_root)
+ : input_root = p.normalize(input_root),
+ output_root = p.normalize(output_root);
+}
+
+/// Parser for the `BazelPackages` option.
+class BazelOptionParser implements SingleOptionParser {
+ /// Output map of package input_root to package.
+ final Map<String, BazelPackage> output;
+
+ BazelOptionParser(this.output);
+
+ @override
+ void parse(String name, String value, onError(String message)) {
+ if (value == null) {
+ onError('Invalid $bazelOptionId option. Expected a non-empty value.');
+ return;
+ }
+
+ for (var entry in value.split(';')) {
+ var fields = entry.split('|');
+ if (fields.length != 3) {
+ onError(
+ 'ERROR: expected package_name|input_root|output_root. Got: $entry');
+ continue;
+ }
+ var pkg = new BazelPackage(fields[0], fields[1], fields[2]);
+ if (!output.containsKey(pkg.input_root)) {
+ output[pkg.input_root] = pkg;
+ } else {
+ var prev = output[pkg.input_root];
+ if (pkg.name != prev.name) {
+ onError('ERROR: multiple packages with input_root ${pkg.input_root}: '
+ '${prev.name} and ${pkg.name}');
+ continue;
+ }
+ if (pkg.output_root != prev.output_root) {
+ onError('ERROR: conflicting output_roots for package ${pkg.name}: '
+ '${prev.output_root} and ${pkg.output_root}');
+ continue;
+ }
+ }
+ }
+ }
+}
+
+/// A Dart `package:` URI with package name and path components.
+class _PackageUri {
+ final String packageName;
+ final String path;
+ Uri get uri => Uri.parse('package:$packageName/$path');
+
+ _PackageUri(this.packageName, this.path);
+}
+
+/// [OutputConfiguration] that uses Bazel layout information to resolve output
+/// locations and imports.
+class BazelOutputConfiguration extends DefaultOutputConfiguration {
+ final Map<String, BazelPackage> packages;
+
+ BazelOutputConfiguration(this.packages);
+
+ /// Search for the most specific Bazel package above [searchPath].
+ BazelPackage _findPackage(String searchPath) {
+ var index = searchPath.lastIndexOf('/');
+ while (index > 0) {
+ searchPath = searchPath.substring(0, index);
+ var pkg = packages[searchPath];
+ if (pkg != null) return pkg;
+ index = searchPath.lastIndexOf('/');
+ }
+ return null;
+ }
+
+ @override
+ Uri outputPathFor(Uri input) {
+ var pkg = _findPackage(input.path);
+ if (pkg == null) {
+ throw new ArgumentError('Unable to locate package for input $input.');
+ }
+
+ // Bazel package-relative paths.
+ var relativeInput = input.path.substring('${pkg.input_root}/'.length);
+ var relativeOutput = replacePathExtension(relativeInput);
+ var outputPath = p.join(pkg.output_root, relativeOutput);
+ return new Uri.file(outputPath);
+ }
+
+ @override
+ Uri resolveImport(Uri target, Uri source) {
+ var targetUri = _packageUriFor(replacePathExtension(target.path));
+ var sourceUri = _packageUriFor(source.path);
+
+ if (targetUri == null && sourceUri != null) {
+ // We can't reach outside of the lib/ directory of a package without
+ // using a package: import. Using a relative import for [target] could
+ // break anyone who uses a package: import to load [source].
+ throw 'ERROR: cannot generate import for $target from $source.';
+ }
+
+ if (targetUri != null && sourceUri?.packageName != targetUri.packageName) {
+ return targetUri.uri;
+ }
+
+ return super.resolveImport(target, source);
+ }
+
+ _PackageUri _packageUriFor(String target) {
+ var pkg = _findPackage(target);
+ if (pkg == null) return null;
+ var relPath = target.substring(pkg.input_root.length + 1);
+ return new _PackageUri(pkg.name, relPath);
+ }
+}
diff --git a/test/all_tests.dart b/test/all_tests.dart
index f4b2b52..c0e1e9d 100755
--- a/test/all_tests.dart
+++ b/test/all_tests.dart
@@ -5,6 +5,7 @@
library protoc_plugin_all_tests;
+import 'bazel_test.dart' as bazel;
import 'client_generator_test.dart' as client_generator;
import 'const_generator_test.dart' as const_generator;
import 'enum_generator_test.dart' as enum_generator;
@@ -26,6 +27,7 @@
import 'wire_format_test.dart' as wire_format;
void main() {
+ bazel.main();
client_generator.main();
const_generator.main();
enum_generator.main();
diff --git a/test/bazel_test.dart b/test/bazel_test.dart
new file mode 100644
index 0000000..af28b49
--- /dev/null
+++ b/test/bazel_test.dart
@@ -0,0 +1,199 @@
+// Copyright (c) 2016, 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.
+
+library bazel_test;
+
+import 'package:protoc_plugin/bazel.dart';
+import 'package:test/test.dart';
+
+void main() {
+ group('BazelOptionParser', () {
+ var optionParser;
+ var packages;
+ var errors;
+
+ setUp(() {
+ packages = {};
+ optionParser = new BazelOptionParser(packages);
+ errors = [];
+ });
+
+ _onError(String message) {
+ errors.add(message);
+ }
+
+ test('should call onError for null values', () {
+ optionParser.parse(null, null, _onError);
+ expect(errors, isNotEmpty);
+ });
+
+ test('should call onError for empty values', () {
+ optionParser.parse(null, '', _onError);
+ expect(errors, isNotEmpty);
+ });
+
+ test('should call onError for malformed entries', () {
+ optionParser.parse(null, 'foo', _onError);
+ optionParser.parse(null, 'foo|bar', _onError);
+ optionParser.parse(null, 'foo|bar|baz|quux', _onError);
+ expect(errors.length, 3);
+ expect(packages, isEmpty);
+ });
+
+ test('should handle a single package|path entry', () {
+ optionParser.parse(null, 'foo|bar/baz|wibble/wobble', _onError);
+ expect(errors, isEmpty);
+ expect(packages.length, 1);
+ expect(packages['bar/baz'].name, 'foo');
+ expect(packages['bar/baz'].input_root, 'bar/baz');
+ expect(packages['bar/baz'].output_root, 'wibble/wobble');
+ });
+
+ test('should handle multiple package|path entries', () {
+ optionParser.parse(
+ null,
+ 'foo|bar/baz|wibble/wobble;a|b/c/d|e/f;one.two|three|four/five',
+ _onError);
+ expect(errors, isEmpty);
+ expect(packages.length, 3);
+ expect(packages['bar/baz'].name, 'foo');
+ expect(packages['bar/baz'].input_root, 'bar/baz');
+ expect(packages['bar/baz'].output_root, 'wibble/wobble');
+ expect(packages['b/c/d'].name, 'a');
+ expect(packages['b/c/d'].input_root, 'b/c/d');
+ expect(packages['b/c/d'].output_root, 'e/f');
+ expect(packages['three'].name, 'one.two');
+ expect(packages['three'].input_root, 'three');
+ expect(packages['three'].output_root, 'four/five');
+ });
+
+ test('should skip and continue past malformed entries', () {
+ optionParser.parse(null,
+ 'foo|bar/baz|wibble/wobble;fizz;a.b|c/d|e/f;x|y|zz|y', _onError);
+ expect(errors.length, 2);
+ expect(packages.length, 2);
+ expect(packages['bar/baz'].name, 'foo');
+ expect(packages['c/d'].name, 'a.b');
+ });
+
+ test('should emit error for conflicting package names', () {
+ optionParser.parse(null,
+ 'foo|bar/baz|wibble/wobble;flob|bar/baz|wibble/wobble', _onError);
+ expect(errors.length, 1);
+ expect(packages.length, 1);
+ expect(packages['bar/baz'].name, 'foo');
+ });
+
+ test('should emit error for conflicting output_roots', () {
+ optionParser.parse(null,
+ 'foo|bar/baz|wibble/wobble;foo|bar/baz|womble/wumble', _onError);
+ expect(errors.length, 1);
+ expect(packages.length, 1);
+ expect(packages['bar/baz'].output_root, 'wibble/wobble');
+ });
+
+ test('should normalize paths', () {
+ optionParser.parse(
+ null, 'foo|bar//baz/|quux/;a|b/|c;c|d//e/f///|g//h//', _onError);
+ expect(errors, isEmpty);
+ expect(packages.length, 3);
+ expect(packages['bar/baz'].name, 'foo');
+ expect(packages['bar/baz'].input_root, 'bar/baz');
+ expect(packages['bar/baz'].output_root, 'quux');
+ expect(packages['b'].name, 'a');
+ expect(packages['b'].input_root, 'b');
+ expect(packages['b'].output_root, 'c');
+ expect(packages['d/e/f'].name, 'c');
+ expect(packages['d/e/f'].input_root, 'd/e/f');
+ expect(packages['d/e/f'].output_root, 'g/h');
+ });
+ });
+
+ group('BazelOutputConfiguration', () {
+ var packages;
+ var config;
+
+ setUp(() {
+ packages = {
+ 'foo/bar': new BazelPackage('a.b.c', 'foo/bar', 'baz/flob'),
+ 'foo/bar/baz': new BazelPackage('d.e.f', 'foo/bar/baz', 'baz/flob/foo'),
+ 'wibble/wobble':
+ new BazelPackage('wibble.wobble', 'wibble/wobble', 'womble/wumble'),
+ };
+ config = new BazelOutputConfiguration(packages);
+ });
+
+ group('outputPathForUri', () {
+ test('should handle files at package root', () {
+ var p = config.outputPathFor(Uri.parse('foo/bar/quux.proto'));
+ expect(p.path, 'baz/flob/quux.pb.dart');
+ });
+
+ test('should handle files below package root', () {
+ var p = config.outputPathFor(Uri.parse('foo/bar/a/b/quux.proto'));
+ expect(p.path, 'baz/flob/a/b/quux.pb.dart');
+ });
+
+ test('should handle files in a nested package root', () {
+ var p = config.outputPathFor(Uri.parse('foo/bar/baz/quux.proto'));
+ expect(p.path, 'baz/flob/foo/quux.pb.dart');
+ });
+
+ test('should handle files below a nested package root', () {
+ var p = config.outputPathFor(Uri.parse('foo/bar/baz/a/b/quux.proto'));
+ expect(p.path, 'baz/flob/foo/a/b/quux.pb.dart');
+ });
+
+ test('should throw if unable to locate the package for an input', () {
+ expect(
+ () => config.outputPathFor(Uri.parse('a/b/c/quux.proto')), throws);
+ });
+ });
+
+ group('resolveImport', () {
+ test('should emit relative import if in same package', () {
+ var target = Uri.parse('foo/bar/quux.proto');
+ var source = Uri.parse('foo/bar/baz.proto');
+ var uri = config.resolveImport(target, source);
+ expect(uri.path, 'quux.pb.dart');
+ });
+
+ test('should emit relative import if in subdir of same package', () {
+ var target = Uri.parse('foo/bar/a/b/quux.proto');
+ var source = Uri.parse('foo/bar/baz.proto');
+ var uri = config.resolveImport(target, source);
+ expect(uri.path, 'a/b/quux.pb.dart');
+ });
+
+ test('should emit relative import if in parent dir in same package', () {
+ var target = Uri.parse('foo/bar/quux.proto');
+ var source = Uri.parse('foo/bar/a/b/baz.proto');
+ var uri = config.resolveImport(target, source);
+ expect(uri.path, '../../quux.pb.dart');
+ });
+
+ test('should emit package: import if in different package', () {
+ var target = Uri.parse('wibble/wobble/quux.proto');
+ var source = Uri.parse('foo/bar/baz.proto');
+ var uri = config.resolveImport(target, source);
+ expect(uri.scheme, 'package');
+ expect(uri.path, 'wibble.wobble/quux.pb.dart');
+ });
+
+ test('should emit package: import if in subdir of different package', () {
+ var target = Uri.parse('wibble/wobble/foo/bar/quux.proto');
+ var source = Uri.parse('foo/bar/baz.proto');
+ var uri = config.resolveImport(target, source);
+ expect(uri.scheme, 'package');
+ expect(uri.path, 'wibble.wobble/foo/bar/quux.pb.dart');
+ });
+
+ test('should throw if target is in unknown package', () {
+ var target = Uri.parse('flob/flub/quux.proto');
+ var source = Uri.parse('foo/bar/baz.proto');
+ expect(() => config.resolveImport(target, source), throws);
+ });
+ });
+ });
+}