// 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 static com.google.devtools.build.lib.syntax.Type.BOOLEAN; 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 PROTOBUF_ATTRIBUTES_NOT_EXCLUSIVE_ERROR = "The portable_proto_filters and uses_protobuf attributes are 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."; @VisibleForTesting static final String USES_PROTOBUF_CANT_BE_SPECIFIED_AS_FALSE = "objc_proto_library targets can't specify the uses_protobuf attribute to false when also " + "specifying portable_proto_filters."; 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> protoFiles = prerequisiteArtifacts.filter(FileType.of(".proto")).list(); if (hasPortableProtoFilters() && ruleContext .attributes() .isAttributeValueExplicitlySpecified(ObjcProtoLibraryRule.USES_PROTOBUF_ATTR) && !ruleContext.attributes().get(ObjcProtoLibraryRule.USES_PROTOBUF_ATTR, BOOLEAN)) { ruleContext.throwWithAttributeError( ObjcProtoLibraryRule.USES_PROTOBUF_ATTR, USES_PROTOBUF_CANT_BE_SPECIFIED_AS_FALSE); } if (requiresProtobuf()) { if (!protoFiles.isEmpty()) { ruleContext.throwWithAttributeError("deps", FILES_NOT_ALLOWED_ERROR); } if (getProtoFiles().isEmpty() && !hasObjcProtoLibraryDependencies()) { ruleContext.throwWithAttributeError("deps", NO_PROTOS_ERROR); } if (getPortableProtoFilters().isEmpty() && !usesProtobuf()) { ruleContext.throwWithAttributeError( ObjcProtoLibraryRule.PORTABLE_PROTO_FILTERS_ATTR, PORTABLE_PROTO_FILTERS_EMPTY_ERROR); } if (usesObjcHeaderNames() || needsPerProtoIncludes() || getOptionsFile().isPresent()) { ruleContext.throwWithRuleError(PROTOBUF_ATTRIBUTES_NOT_EXCLUSIVE_ERROR); } if (hasPB2Dependencies()) { ruleContext.throwWithAttributeError("deps", PROTOCOL_BUFFERS2_IN_PROTOBUF_DEPS_ERROR); } } else { if (!protoFiles.isEmpty()) { ruleContext.attributeWarning("deps", FILES_DEPRECATED_WARNING); } if (getProtoFiles().isEmpty()) { ruleContext.throwWithAttributeError("deps", NO_PROTOS_ERROR); } if (hasObjcProtoLibraryDependencies()) { ruleContext.throwWithAttributeError( "deps", 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 this target configures the uses_protobuf attribute. */ boolean usesProtobuf() { if (ruleContext.attributes().has(ObjcProtoLibraryRule.USES_PROTOBUF_ATTR, Type.BOOLEAN)) { return ruleContext.attributes().get(ObjcProtoLibraryRule.USES_PROTOBUF_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 whether the target requires the use of protobuf or not. This is a convenience method to * check that either the {@code portable_proto_filters} or the {@code uses_protobuf} attributes * were passed. */ boolean requiresProtobuf() { return hasPortableProtoFilters() || usesProtobuf(); } /** 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; } }