| // 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, String extension) { |
| 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 base = p.withoutExtension(relativeInput); |
| var outputPath = p.join(pkg.output_root, "$base$extension"); |
| return new Uri.file(outputPath); |
| } |
| |
| @override |
| Uri resolveImport(Uri target, Uri source, String extension) { |
| var targetBase = p.withoutExtension(target.path); |
| var targetUri = _packageUriFor("$targetBase$extension"); |
| 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, extension); |
| } |
| |
| _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); |
| } |
| } |