| // Copyright 2018 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // TODO(https://fxbug.dev/84961): Fix null safety and remove this language version. |
| // @dart=2.9 |
| |
| import 'dart:io'; |
| |
| import 'package:path/path.dart' as path; |
| import 'package:protoc_plugin/protoc.dart'; |
| |
| /// This is the alternate plugin driver for dart protobuf bindings. This |
| /// plugin driver is suppled into the `protoc` (or protoc_wrapper.py). This |
| /// driver is used alongside GN, takes in a mapping of dart package name -> dart |
| /// package paths, and uses these mappings to transform strictly file-based |
| /// imports to package-based imports. |
| /// |
| /// The plugin option passed to `protoc` end up here. The option format is: |
| /// A semicolon-separated list of package name -> package directory mappings of |
| /// the form `package_name|input_root`. `input_root` designates the directory |
| /// relative to which input protos are located -- typically the root of the GN |
| /// package, where the `BUILD.gn` file is located. |
| /// |
| /// The options above are used in the following way: |
| /// * Given a file-based import path in the .proto file, the generated dart |
| /// import path is rebased to a package path if possible. |
| /// * See [DartImportMappingOutputConfiguration.resolveImport] below. |
| class DartImportMappingOptionParser extends SingleOptionParser { |
| /// This is the option key we pass to protoc; |
| static const String kImportOptionKey = 'PackagePaths'; |
| |
| /// Output map of: package input root => package name |
| final Map<String, String> packageMapping; |
| |
| DartImportMappingOptionParser(this.packageMapping); |
| |
| @override |
| void parse(String name, String value, onError(String message)) { |
| if (value == null) { |
| onError('Invalid $kImportOptionKey option. Expected a non-empty value.'); |
| return; |
| } |
| |
| for (var entry in value.split(';')) { |
| var fields = entry.split('|'); |
| if (fields.length != 2) { |
| onError('ERROR: expected package_name|input_root. Got: $entry'); |
| continue; |
| } |
| if (packageMapping.containsKey(fields[1])) { |
| onError('ERROR: ${fields[1]} already exists.'); |
| continue; |
| } |
| packageMapping[fields[1]] = fields[0]; |
| } |
| } |
| } |
| |
| class DartImportMappingOutputConfiguration extends DefaultOutputConfiguration { |
| /// Output map (package root) => package name |
| final Map<String, String> packageMapping; |
| |
| DartImportMappingOutputConfiguration(this.packageMapping); |
| |
| /// Given a file-based import path ("a/b/c/d.proto"), this routine looks |
| /// through the [packageMapping] to find a package that this import may fall |
| /// under. If it can find one, it rebases the import path to the package |
| /// input root in the generated import string. |
| @override |
| Uri resolveImport(Uri target, Uri source, String extension) { |
| for (int i = target.pathSegments.length; i > 0; i--) { |
| var candidatePath = target.pathSegments.sublist(0, i).join('/'); |
| if (packageMapping.containsKey(candidatePath)) { |
| var packageName = packageMapping[candidatePath]; |
| var importPath = target.pathSegments |
| .sublist(i, target.pathSegments.length) |
| .join('/'); |
| var genImportPath = '${path.withoutExtension(importPath)}.pb.dart'; |
| return Uri.parse('package:$packageName/$genImportPath'); |
| } |
| } |
| return super.resolveImport(target, source, extension); |
| } |
| } |
| |
| void main() { |
| var packageMapping = <String, String>{}; |
| CodeGenerator(stdin, stdout).generate( |
| optionParsers: { |
| DartImportMappingOptionParser.kImportOptionKey: |
| DartImportMappingOptionParser(packageMapping), |
| }, |
| config: DartImportMappingOutputConfiguration(packageMapping), |
| ); |
| } |