| // Copyright 2015 The Bazel Authors. All rights reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package com.google.devtools.build.lib.rules.objc; |
| |
| import static com.google.common.base.CaseFormat.LOWER_UNDERSCORE; |
| import static com.google.common.base.CaseFormat.UPPER_CAMEL; |
| import static com.google.devtools.build.lib.packages.BuildType.LABEL; |
| import static com.google.devtools.build.lib.packages.BuildType.LABEL_LIST; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.common.base.CharMatcher; |
| import com.google.common.base.Optional; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.devtools.build.lib.actions.Artifact; |
| import com.google.devtools.build.lib.analysis.AbstractConfiguredTarget; |
| import com.google.devtools.build.lib.analysis.PrerequisiteArtifacts; |
| import com.google.devtools.build.lib.analysis.RuleConfiguredTarget.Mode; |
| import com.google.devtools.build.lib.analysis.RuleContext; |
| import com.google.devtools.build.lib.analysis.TransitiveInfoCollection; |
| import com.google.devtools.build.lib.collect.nestedset.NestedSet; |
| import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder; |
| import com.google.devtools.build.lib.packages.RuleClass.ConfiguredTargetFactory.RuleErrorException; |
| import com.google.devtools.build.lib.rules.proto.ProtoSourceFileBlacklist; |
| import com.google.devtools.build.lib.rules.proto.ProtoSourcesProvider; |
| import com.google.devtools.build.lib.syntax.Type; |
| import com.google.devtools.build.lib.util.FileType; |
| import java.util.ArrayList; |
| |
| /** Common rule attributes used by an objc_proto_library. */ |
| final class ProtoAttributes { |
| |
| /** |
| * List of file name segments that should be upper cased when being generated. More information |
| * available in the generateProtobufFilename() method. |
| */ |
| private static final ImmutableSet<String> UPPERCASE_SEGMENTS = |
| ImmutableSet.of("url", "http", "https"); |
| |
| @VisibleForTesting |
| static final String FILES_NOT_ALLOWED_ERROR = |
| "Using files and filegroups in objc_proto_library with portable_proto_filters is not " |
| + "allowed. Please wrap the files inside a proto_library target."; |
| |
| @VisibleForTesting |
| static final String FILES_DEPRECATED_WARNING = |
| "Using files and filegroups in objc_proto_library is deprecated"; |
| |
| @VisibleForTesting |
| static final String NO_PROTOS_ERROR = |
| "no protos to compile - a non-empty deps attribute is required"; |
| |
| @VisibleForTesting |
| static final String PORTABLE_PROTO_FILTERS_NOT_EXCLUSIVE_ERROR = |
| "The portable_proto_filters attribute is incompatible with the options_file, output_cpp, " |
| + "per_proto_includes and use_objc_header_names attributes."; |
| |
| @VisibleForTesting |
| static final String PORTABLE_PROTO_FILTERS_EMPTY_ERROR = |
| "The portable_proto_filters attribute can't be empty"; |
| |
| @VisibleForTesting |
| static final String OBJC_PROTO_LIB_DEP_IN_PROTOCOL_BUFFERS2_DEPS_ERROR = |
| "Protocol Buffers 2 objc_proto_library targets can't depend on other objc_proto_library " |
| + "targets. Please migrate your Protocol Buffers 2 objc_proto_library targets to use the " |
| + "portable_proto_filters attribute."; |
| |
| @VisibleForTesting |
| static final String PROTOCOL_BUFFERS2_IN_PROTOBUF_DEPS_ERROR = |
| "Protobuf objc_proto_library targets can't depend on Protocol Buffers 2 objc_proto_library " |
| + "targets. Please migrate your Protocol Buffers 2 objc_proto_library targets to use the " |
| + "portable_proto_filters attribute."; |
| |
| private final RuleContext ruleContext; |
| |
| /** |
| * Creates a new ProtoAttributes object that wraps over objc_proto_library's attributes. |
| * |
| * @param ruleContext context of the objc_proto_library to wrap |
| */ |
| ProtoAttributes(RuleContext ruleContext) { |
| this.ruleContext = ruleContext; |
| } |
| |
| /** |
| * Validates the proto attributes for this target. |
| * |
| * <ul> |
| * <li>Validates that there are protos specified to be compiled. |
| * <li>Validates that, when enabling the open source protobuf library, the options for the PB2 are |
| * not specified also. |
| * <li>Validates that, when enabling the open source protobuf library, the rule specifies at least |
| * one portable proto filter file. |
| * </ul> |
| */ |
| public void validate() throws RuleErrorException { |
| PrerequisiteArtifacts prerequisiteArtifacts = |
| ruleContext.getPrerequisiteArtifacts("deps", Mode.TARGET); |
| ImmutableList<Artifact> protos = prerequisiteArtifacts.filter(FileType.of(".proto")).list(); |
| |
| if (ruleContext |
| .attributes() |
| .isAttributeValueExplicitlySpecified(ObjcProtoLibraryRule.PORTABLE_PROTO_FILTERS_ATTR)) { |
| if (!protos.isEmpty()) { |
| ruleContext.throwWithAttributeError("deps", FILES_NOT_ALLOWED_ERROR); |
| } |
| if (getProtoFiles().isEmpty() && !hasObjcProtoLibraryDependencies()) { |
| ruleContext.throwWithRuleError(NO_PROTOS_ERROR); |
| } |
| |
| if (getPortableProtoFilters().isEmpty()) { |
| ruleContext.throwWithRuleError(PORTABLE_PROTO_FILTERS_EMPTY_ERROR); |
| } |
| |
| if (usesObjcHeaderNames() |
| || needsPerProtoIncludes() |
| || getOptionsFile().isPresent()) { |
| ruleContext.throwWithRuleError(PORTABLE_PROTO_FILTERS_NOT_EXCLUSIVE_ERROR); |
| } |
| if (hasPB2Dependencies()) { |
| ruleContext.throwWithRuleError(PROTOCOL_BUFFERS2_IN_PROTOBUF_DEPS_ERROR); |
| } |
| |
| } else { |
| if (!protos.isEmpty()) { |
| ruleContext.attributeWarning("deps", FILES_DEPRECATED_WARNING); |
| } |
| if (getProtoFiles().isEmpty()) { |
| ruleContext.throwWithRuleError(NO_PROTOS_ERROR); |
| } |
| |
| if (!usesObjcHeaderNames()) { |
| ruleContext.ruleWarning( |
| "As part of the migration process, it is recommended to enable " |
| + "use_objc_header_names. Please refer to b/29368416 for more information."); |
| } |
| if (hasObjcProtoLibraryDependencies()) { |
| ruleContext.throwWithRuleError(OBJC_PROTO_LIB_DEP_IN_PROTOCOL_BUFFERS2_DEPS_ERROR); |
| } |
| } |
| } |
| |
| /** Returns whether the generated header files should have be of type pb.h or pbobjc.h. */ |
| boolean usesObjcHeaderNames() { |
| if (ruleContext |
| .attributes() |
| .has(ObjcProtoLibraryRule.USE_OBJC_HEADER_NAMES_ATTR, Type.BOOLEAN)) { |
| return ruleContext |
| .attributes() |
| .get(ObjcProtoLibraryRule.USE_OBJC_HEADER_NAMES_ATTR, Type.BOOLEAN); |
| } |
| return false; |
| } |
| |
| /** Returns whether the includes should include each of the proto generated headers. */ |
| boolean needsPerProtoIncludes() { |
| if (ruleContext.attributes().has(ObjcProtoLibraryRule.PER_PROTO_INCLUDES_ATTR, Type.BOOLEAN)) { |
| return ruleContext |
| .attributes() |
| .get(ObjcProtoLibraryRule.PER_PROTO_INCLUDES_ATTR, Type.BOOLEAN); |
| } |
| return false; |
| } |
| |
| /** Returns whether to use the protobuf library instead of the PB2 library. */ |
| boolean hasPortableProtoFilters() { |
| return ruleContext |
| .attributes() |
| .isAttributeValueExplicitlySpecified(ObjcProtoLibraryRule.PORTABLE_PROTO_FILTERS_ATTR); |
| } |
| |
| /** Returns the list of portable proto filters. */ |
| ImmutableList<Artifact> getPortableProtoFilters() { |
| if (ruleContext |
| .attributes() |
| .has(ObjcProtoLibraryRule.PORTABLE_PROTO_FILTERS_ATTR, LABEL_LIST)) { |
| return ruleContext |
| .getPrerequisiteArtifacts(ObjcProtoLibraryRule.PORTABLE_PROTO_FILTERS_ATTR, Mode.HOST) |
| .list(); |
| } |
| return ImmutableList.of(); |
| } |
| |
| /** Returns the list of well known type protos. */ |
| ImmutableList<Artifact> getWellKnownTypeProtos() { |
| return ruleContext |
| .getPrerequisiteArtifacts(ObjcRuleClasses.PROTOBUF_WELL_KNOWN_TYPES, Mode.HOST) |
| .list(); |
| } |
| |
| /** Returns the options file, or {@link Optional#absent} if it was not specified. */ |
| Optional<Artifact> getOptionsFile() { |
| if (ruleContext.attributes().has(ObjcProtoLibraryRule.OPTIONS_FILE_ATTR, LABEL)) { |
| return Optional.fromNullable( |
| ruleContext.getPrerequisiteArtifact(ObjcProtoLibraryRule.OPTIONS_FILE_ATTR, Mode.HOST)); |
| } |
| return Optional.absent(); |
| } |
| |
| /** Returns the list of proto files to compile. */ |
| NestedSet<Artifact> getProtoFiles() { |
| return NestedSetBuilder.<Artifact>stableOrder() |
| .addAll(getProtoDepsFiles()) |
| .addTransitive(getProtoDepsSources()) |
| .build(); |
| } |
| |
| /** Returns the proto compiler to be used. */ |
| Artifact getProtoCompiler() { |
| return ruleContext.getPrerequisiteArtifact(ObjcRuleClasses.PROTO_COMPILER_ATTR, Mode.HOST); |
| } |
| |
| /** Returns the list of files needed by the proto compiler. */ |
| Iterable<Artifact> getProtoCompilerSupport() { |
| return ruleContext |
| .getPrerequisiteArtifacts(ObjcRuleClasses.PROTO_COMPILER_SUPPORT_ATTR, Mode.HOST) |
| .list(); |
| } |
| |
| /** |
| * Filters the well known protos from the given list of proto files. This should be used to |
| * prevent the well known protos from being generated as they are already generated in the runtime |
| * library. |
| */ |
| Iterable<Artifact> filterWellKnownProtos(Iterable<Artifact> protoFiles) { |
| ProtoSourceFileBlacklist wellKnownProtoBlacklist = |
| new ProtoSourceFileBlacklist(ruleContext, getWellKnownTypeProtos()); |
| return wellKnownProtoBlacklist.filter(protoFiles); |
| } |
| |
| /** Returns whether the given proto is a well known proto or not. */ |
| boolean isProtoWellKnown(Artifact protoFile) { |
| ProtoSourceFileBlacklist wellKnownProtoBlacklist = |
| new ProtoSourceFileBlacklist(ruleContext, getWellKnownTypeProtos()); |
| return wellKnownProtoBlacklist.isBlacklisted(protoFile); |
| } |
| |
| /** |
| * Processes the case of the proto file name in the same fashion as the objective_c generator's |
| * UnderscoresToCamelCase function. This converts snake case to camel case by splitting words |
| * by non alphabetic characters. This also treats the URL, HTTP and HTTPS as special words that |
| * need to be completely uppercase. |
| * |
| * Examples: |
| * - j2objc_descriptor -> J2ObjcDescriptor (notice that O is uppercase after the 2) |
| * - my_http_url_array -> MyHTTPURLArray |
| * - proto-descriptor -> ProtoDescriptor |
| * |
| * Original code reference: |
| * <p>https://github.com/google/protobuf/blob/master/src/google/protobuf/compiler/objectivec/objectivec_helpers.cc |
| */ |
| String getGeneratedProtoFilename(String protoFilename, boolean upcaseReservedWords) { |
| boolean lastCharWasDigit = false; |
| boolean lastCharWasUpper = false; |
| boolean lastCharWasLower = false; |
| |
| StringBuilder currentSegment = new StringBuilder(); |
| |
| ArrayList<String> segments = new ArrayList<>(); |
| |
| for (int i = 0; i < protoFilename.length(); i++) { |
| char currentChar = protoFilename.charAt(i); |
| if (CharMatcher.javaDigit().matches(currentChar)) { |
| if (!lastCharWasDigit) { |
| segments.add(currentSegment.toString()); |
| currentSegment = new StringBuilder(); |
| } |
| currentSegment.append(currentChar); |
| lastCharWasDigit = true; |
| lastCharWasUpper = false; |
| lastCharWasLower = false; |
| } else if (CharMatcher.javaLowerCase().matches(currentChar)) { |
| if (!lastCharWasLower && !lastCharWasUpper) { |
| segments.add(currentSegment.toString()); |
| currentSegment = new StringBuilder(); |
| } |
| currentSegment.append(currentChar); |
| lastCharWasDigit = false; |
| lastCharWasUpper = false; |
| lastCharWasLower = true; |
| } else if (CharMatcher.javaUpperCase().matches(currentChar)) { |
| if (!lastCharWasUpper) { |
| segments.add(currentSegment.toString()); |
| currentSegment = new StringBuilder(); |
| } |
| currentSegment.append(Character.toLowerCase(currentChar)); |
| lastCharWasDigit = false; |
| lastCharWasUpper = true; |
| lastCharWasLower = false; |
| } else { |
| lastCharWasDigit = false; |
| lastCharWasUpper = false; |
| lastCharWasLower = false; |
| } |
| } |
| |
| segments.add(currentSegment.toString()); |
| |
| StringBuilder casedSegments = new StringBuilder(); |
| for (String segment : segments) { |
| if (upcaseReservedWords && UPPERCASE_SEGMENTS.contains(segment)) { |
| casedSegments.append(segment.toUpperCase()); |
| } else { |
| casedSegments.append(LOWER_UNDERSCORE.to(UPPER_CAMEL, segment)); |
| } |
| } |
| return casedSegments.toString(); |
| } |
| |
| /** Returns the sets of proto files that were added using proto_library dependencies. */ |
| private NestedSet<Artifact> getProtoDepsSources() { |
| NestedSetBuilder<Artifact> artifacts = NestedSetBuilder.stableOrder(); |
| Iterable<ProtoSourcesProvider> providers = |
| ruleContext.getPrerequisites("deps", Mode.TARGET, ProtoSourcesProvider.class); |
| for (ProtoSourcesProvider provider : providers) { |
| artifacts.addTransitive(provider.getTransitiveProtoSources()); |
| } |
| return artifacts.build(); |
| } |
| |
| /** |
| * Returns the list of proto files that were added directly into the deps attributes. This way of |
| * specifying the protos is deprecated, and displays a warning when the target does so. |
| */ |
| private ImmutableList<Artifact> getProtoDepsFiles() { |
| PrerequisiteArtifacts prerequisiteArtifacts = |
| ruleContext.getPrerequisiteArtifacts("deps", Mode.TARGET); |
| return prerequisiteArtifacts.filter(FileType.of(".proto")).list(); |
| } |
| |
| private boolean hasPB2Dependencies() { |
| for (TransitiveInfoCollection dep : ruleContext.getPrerequisites("deps", Mode.TARGET)) { |
| if (isObjcProtoLibrary(dep) && !hasProtobufProvider(dep)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private boolean hasObjcProtoLibraryDependencies() { |
| for (TransitiveInfoCollection dep : ruleContext.getPrerequisites("deps", Mode.TARGET)) { |
| if (isObjcProtoLibrary(dep)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private boolean isObjcProtoLibrary(TransitiveInfoCollection dependency) { |
| try { |
| AbstractConfiguredTarget target = (AbstractConfiguredTarget) dependency; |
| String targetName = target.getTarget().getTargetKind(); |
| return targetName.equals("objc_proto_library rule"); |
| } catch (Exception e) { |
| return false; |
| } |
| } |
| |
| private boolean hasProtobufProvider(TransitiveInfoCollection dependency) { |
| return dependency.getProvider(ObjcProtoProvider.class) != null; |
| } |
| } |