blob: d463586df9b2bfae4097f6c36656f4f9fcb1fffd [file] [log] [blame]
// Copyright 2017 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.
// Copyright 2017 The Bazel Authors. All rights reserved.
package com.google.devtools.build.android;
import com.android.aapt.Resources.Reference;
import com.android.aapt.Resources.XmlNode;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.android.aapt2.Aapt2ConfigOptions;
import com.google.devtools.build.android.aapt2.CompiledResources;
import com.google.devtools.build.android.aapt2.ResourceLinker;
import com.google.devtools.build.android.aapt2.StaticLibrary;
import com.google.devtools.build.android.resources.Visibility;
import com.google.devtools.build.android.xml.ProtoXmlUtils;
import com.google.devtools.common.options.Option;
import com.google.devtools.common.options.OptionDocumentationCategory;
import com.google.devtools.common.options.OptionEffectTag;
import com.google.devtools.common.options.OptionsBase;
import com.google.devtools.common.options.OptionsParser;
import com.google.devtools.common.options.ShellQuotedParamsFilePreProcessor;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/** Performs resource validation and static linking for compiled android resources. */
public class ValidateAndLinkResourcesAction {
/** Action configuration options. */
public static class Options extends OptionsBase {
@Option(
name = "compiled",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.UNKNOWN},
defaultValue = "null",
converter = Converters.ExistingPathConverter.class,
category = "input",
help = "Compiled resources to link.",
deprecationWarning = "Use --resources.")
// TODO(b/64570523): Still used by blaze. Will be removed as part of the command line cleanup.
@Deprecated
public Path compiled;
@Option(
name = "compiledDep",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.UNKNOWN},
defaultValue = "null",
converter = Converters.PathListConverter.class,
category = "input",
allowMultiple = true,
help = "Compiled resource dependencies to link.")
public List<Path> compiledDeps;
@Option(
name = "manifest",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.UNKNOWN},
defaultValue = "null",
converter = Converters.ExistingPathConverter.class,
category = "input",
help = "Manifest for the library.",
deprecationWarning = "Use --resources.")
// TODO(b/64570523): Still used by blaze. Will be removed as part of the command line cleanup.
@Deprecated
public Path manifest;
@Option(
name = "resources",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.UNKNOWN},
defaultValue = "null",
converter = Converters.CompiledResourcesConverter.class,
category = "input",
help = "Compiled resources to link.")
public CompiledResources resources;
// TODO(b/64570523): remove this flag when it is no longer used.
@Option(
name = "libraries",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.UNKNOWN},
defaultValue = "null",
converter = Converters.StaticLibraryListConverter.class,
category = "input",
help = "Static libraries to link against. Deprecated, use --library")
public List<StaticLibrary> deprecatedLibraries;
@Option(
name = "library",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.UNKNOWN},
defaultValue = "null",
converter = Converters.StaticLibraryConverter.class,
category = "input",
allowMultiple = true,
help = "Static libraries to link against.")
public List<StaticLibrary> libraries;
@Option(
name = "packageForR",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.UNKNOWN},
defaultValue = "null",
category = "input",
help = "Package for the resources.")
public String packageForR;
@Option(
name = "staticLibraryOut",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.UNKNOWN},
defaultValue = "null",
converter = Converters.PathConverter.class,
category = "output",
help = "Static library produced.")
public Path staticLibraryOut;
@Option(
name = "rTxtOut",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.UNKNOWN},
defaultValue = "null",
converter = Converters.PathConverter.class,
category = "output",
help = "R.txt out.")
public Path rTxtOut;
@Option(
name = "sourceJarOut",
documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
effectTags = {OptionEffectTag.UNKNOWN},
converter = Converters.PathConverter.class,
defaultValue = "null",
category = "output",
help = "Generated java classes from the resources.")
public Path sourceJarOut;
}
public static void main(String[] args) throws Exception {
OptionsParser optionsParser =
OptionsParser.builder()
.optionsClasses(
Options.class, Aapt2ConfigOptions.class, ResourceProcessorCommonOptions.class)
.argsPreProcessor(new ShellQuotedParamsFilePreProcessor(FileSystems.getDefault()))
.build();
optionsParser.parse(args);
Options options = optionsParser.getOptions(Options.class);
final Aapt2ConfigOptions aapt2Options = optionsParser.getOptions(Aapt2ConfigOptions.class);
final Profiler profiler = LoggingProfiler.createAndStart("manifest");
try (ScopedTemporaryDirectory scopedTmp =
new ScopedTemporaryDirectory("android_resources_tmp");
ExecutorServiceCloser executorService = ExecutorServiceCloser.createWithFixedPoolOf(15)) {
CompiledResources resources =
// TODO(b/64570523): Remove when the flags are standardized.
Optional.ofNullable(options.resources)
.orElseGet(
() ->
CompiledResources.from(
Preconditions.checkNotNull(options.compiled),
Preconditions.checkNotNull(options.manifest)))
// We need to make the manifest aapt safe (w.r.t., placeholders). For now, just stub
// it out.
.processManifest(
manifest ->
AndroidManifest.parseFrom(manifest)
.writeDummyManifestForAapt(
scopedTmp.getPath().resolve("manifest-aapt-dummy"),
options.packageForR));
List<CompiledResources> deps =
options.compiledDeps.stream()
.map(CompiledResources::from)
.collect(ImmutableList.toImmutableList());
profiler.recordEndOf("manifest").startTask("validate");
// TODO(b/146663858): distinguish direct/transitive deps for "strict deps".
// TODO(b/128711690): validate AndroidManifest.xml
checkVisibilityOfResourceReferences(
/*androidManifest=*/ XmlNode.getDefaultInstance(), resources, deps);
profiler.recordEndOf("validate").startTask("link");
ResourceLinker.create(aapt2Options.aapt2, executorService, scopedTmp.getPath())
.profileUsing(profiler)
// NB: these names are really confusing.
// .dependencies is meant for linking in android.jar
// .include is meant for regular dependencies
.dependencies(Optional.ofNullable(options.deprecatedLibraries).orElse(options.libraries))
.include(deps)
.buildVersion(aapt2Options.buildToolsVersion)
.outputAsProto(aapt2Options.resourceTableAsProto)
.linkStatically(resources)
.copyLibraryTo(options.staticLibraryOut)
.copySourceJarTo(options.sourceJarOut)
.copyRTxtTo(options.rTxtOut);
profiler.recordEndOf("link");
}
}
/**
* Validates that resources referenced from AndroidManifest.xml and res/ are visible.
*
* @param androidManifest AndroidManifest in protobuf format; while {@link CompiledResources} also
* contains a manifest, aapt2 requires it to be regular XML and only converts it to protobuf
* after "linking".
* @param compiled resources of a compilation unit (i.e. an android_library rule)
* @param deps resources from the transitive closure of the rule's "deps" attribute
*/
static void checkVisibilityOfResourceReferences(
XmlNode androidManifest, CompiledResources compiled, List<CompiledResources> deps) {
ImmutableSet<String> privateResourceNames =
deps.stream()
.flatMap(
cr ->
AndroidCompiledDataDeserializer.create(
/*includeFileContentsForValidation=*/ false)
.read(DependencyInfo.UNKNOWN, cr.getZip())
.entrySet()
.stream())
.filter(entry -> entry.getValue().getVisibility() == Visibility.PRIVATE)
.map(entry -> ((FullyQualifiedName) entry.getKey()).asQualifiedReference())
.collect(ImmutableSet.toImmutableSet());
StringBuilder errorMessage = new StringBuilder();
{
List<String> referencedPrivateResources =
ProtoXmlUtils.getAllResourceReferences(androidManifest).stream()
.map(Reference::getName)
.filter(privateResourceNames::contains)
.collect(ImmutableList.toImmutableList());
if (!referencedPrivateResources.isEmpty()) {
errorMessage
.append("AndroidManifest.xml references external private resources ")
.append(referencedPrivateResources)
.append('\n');
}
}
for (Map.Entry<DataKey, DataResource> resource :
AndroidCompiledDataDeserializer.create(/*includeFileContentsForValidation=*/ true)
.read(DependencyInfo.UNKNOWN, compiled.getZip())
.entrySet()) {
List<String> referencedPrivateResources =
resource.getValue().getReferencedResources().stream()
.map(Reference::getName)
.filter(privateResourceNames::contains)
.collect(ImmutableList.toImmutableList());
if (!referencedPrivateResources.isEmpty()) {
errorMessage
.append(resource.getKey().toPrettyString())
.append(" (defined in ")
.append(resource.getValue().source().getPath())
.append(") references external private resources ")
.append(referencedPrivateResources)
.append('\n');
}
}
if (errorMessage.length() != 0) {
throw new UserException(errorMessage.toString());
}
}
}