blob: f66cb65d7e1a0bc0ae5138cab1bdf3a9cdba5817 [file] [log] [blame]
// 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);
}
}