// Copyright 2018 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.android; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.devtools.build.lib.actions.Artifact; import com.google.devtools.build.lib.actions.Artifact.SpecialArtifact; import com.google.devtools.build.lib.analysis.ConfiguredTarget; import com.google.devtools.build.lib.analysis.FileProvider; import com.google.devtools.build.lib.analysis.TransitiveInfoProvider; import com.google.devtools.build.lib.analysis.skylark.SkylarkErrorReporter; import com.google.devtools.build.lib.cmdline.Label; import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder; import com.google.devtools.build.lib.collect.nestedset.Order; import com.google.devtools.build.lib.events.Location; import com.google.devtools.build.lib.packages.BuiltinProvider; import com.google.devtools.build.lib.packages.NativeInfo; import com.google.devtools.build.lib.packages.NativeProvider; import com.google.devtools.build.lib.packages.Provider; import com.google.devtools.build.lib.packages.RuleClass.ConfiguredTargetFactory.RuleErrorException; import com.google.devtools.build.lib.rules.android.AndroidConfiguration.AndroidAaptVersion; import com.google.devtools.build.lib.rules.android.AndroidLibraryAarInfo.Aar; import com.google.devtools.build.lib.rules.java.JavaCompilationInfoProvider; import com.google.devtools.build.lib.rules.java.JavaInfo; import com.google.devtools.build.lib.rules.java.ProguardSpecProvider; import com.google.devtools.build.lib.skylarkbuildapi.android.AndroidBinaryDataSettingsApi; import com.google.devtools.build.lib.skylarkbuildapi.android.AndroidDataProcessingApi; import com.google.devtools.build.lib.syntax.Environment; import com.google.devtools.build.lib.syntax.EvalException; import com.google.devtools.build.lib.syntax.Runtime; import com.google.devtools.build.lib.syntax.SkylarkDict; import com.google.devtools.build.lib.syntax.SkylarkList; import com.google.devtools.build.lib.vfs.PathFragment; import java.util.List; import java.util.Objects; import java.util.Optional; import javax.annotation.Nullable; /** Skylark-visible methods for working with Android data (manifests, resources, and assets). */ public abstract class AndroidSkylarkData implements AndroidDataProcessingApi< AndroidDataContext, ConfiguredTarget, Artifact, SpecialArtifact, AndroidAssetsInfo, AndroidResourcesInfo, AndroidManifestInfo, AndroidLibraryAarInfo, AndroidBinaryDataInfo> { public abstract AndroidSemantics getAndroidSemantics(); @Override public AndroidAssetsInfo assetsFromDeps( SkylarkList deps, boolean neverlink, Environment env) { return AssetDependencies.fromProviders(deps, neverlink).toInfo(env.getCallerLabel()); } @Override public AndroidResourcesInfo resourcesFromDeps( AndroidDataContext ctx, SkylarkList deps, boolean neverlink, Object customPackage, Location location, Environment env) throws InterruptedException, EvalException { try (SkylarkErrorReporter errorReporter = SkylarkErrorReporter.from(ctx.getActionConstructionContext(), location, env)) { String pkg = fromNoneable(customPackage, String.class); if (pkg == null) { pkg = AndroidManifest.getDefaultPackage( env.getCallerLabel(), ctx.getActionConstructionContext(), errorReporter); } return ResourceApk.processFromTransitiveLibraryData( ctx, DataBinding.asDisabledDataBindingContext(), ResourceDependencies.fromProviders(deps, /* neverlink = */ neverlink), AssetDependencies.empty(), StampedAndroidManifest.createEmpty( ctx.getActionConstructionContext(), pkg, /* exported = */ false)) .toResourceInfo(ctx.getLabel()); } } @Override public AndroidManifestInfo stampAndroidManifest( AndroidDataContext ctx, Object manifest, Object customPackage, boolean exported, Location location, Environment env) throws InterruptedException, EvalException { String pkg = fromNoneable(customPackage, String.class); try (SkylarkErrorReporter errorReporter = SkylarkErrorReporter.from(ctx.getActionConstructionContext(), location, env)) { return AndroidManifest.from( ctx, errorReporter, fromNoneable(manifest, Artifact.class), getAndroidSemantics(), pkg, exported) .stamp(ctx) .toProvider(); } } @Override public AndroidAssetsInfo mergeAssets( AndroidDataContext ctx, Object assets, Object assetsDir, SkylarkList deps, boolean neverlink, Location location, Environment env) throws EvalException, InterruptedException { try (SkylarkErrorReporter errorReporter = SkylarkErrorReporter.from(ctx.getActionConstructionContext(), location, env)) { return AndroidAssets.from( errorReporter, listFromNoneable(assets, ConfiguredTarget.class), isNone(assetsDir) ? null : PathFragment.create(fromNoneable(assetsDir, String.class))) .process( ctx, AssetDependencies.fromProviders(deps, neverlink), ctx.getAndroidConfig().getAndroidAaptVersion()) .toProvider(); } catch (RuleErrorException e) { throw new EvalException(Location.BUILTIN, e); } } @Override public SkylarkDict mergeResources( AndroidDataContext ctx, AndroidManifestInfo manifest, SkylarkList resources, SkylarkList deps, boolean neverlink, boolean enableDataBinding, Location location, Environment env) throws EvalException, InterruptedException { try (SkylarkErrorReporter errorReporter = SkylarkErrorReporter.from(ctx.getActionConstructionContext(), location, env)) { AndroidAaptVersion aaptVersion = ctx.getAndroidConfig().getAndroidAaptVersion(); ValidatedAndroidResources validated = AndroidResources.from(errorReporter, getFileProviders(resources), "resources") .process( ctx, manifest.asStampedManifest(), ResourceDependencies.fromProviders(deps, neverlink), DataBinding.contextFrom( enableDataBinding, ctx.getActionConstructionContext(), ctx.getAndroidConfig()), aaptVersion); JavaInfo javaInfo = getJavaInfoForRClassJar(validated.getClassJar()); return SkylarkDict.of( /* env = */ null, AndroidResourcesInfo.PROVIDER, validated.toProvider(), JavaInfo.PROVIDER, javaInfo); } catch (RuleErrorException e) { throw new EvalException(Location.BUILTIN, e); } } @Override public AndroidLibraryAarInfo makeAar( AndroidDataContext ctx, AndroidResourcesInfo resourcesInfo, AndroidAssetsInfo assetsInfo, Artifact libraryClassJar, SkylarkList proguardSpecs, SkylarkList deps, boolean neverlink) throws EvalException, InterruptedException { if (neverlink) { return AndroidLibraryAarInfo.create( null, NestedSetBuilder.emptySet(Order.NAIVE_LINK_ORDER), NestedSetBuilder.emptySet(Order.NAIVE_LINK_ORDER)); } // Get the target's local resources, if defined, from the provider boolean definesLocalResources = resourcesInfo.getDirectAndroidResources().isSingleton(); AndroidResources resources = AndroidResources.empty(); if (definesLocalResources) { ValidatedAndroidResources validatedAndroidResources = resourcesInfo.getDirectAndroidResources().toList().get(0); if (validatedAndroidResources.getLabel().equals(ctx.getLabel())) { resources = validatedAndroidResources; } else { definesLocalResources = false; } } // Get the target's local assets, if defined, from the provider boolean definesLocalAssets = false; AndroidAssets assets = AndroidAssets.empty(); if (assetsInfo.getDirectParsedAssets().isSingleton()) { ParsedAndroidAssets parsed = assetsInfo.getDirectParsedAssets().toList().get(0); if (parsed.getLabel().equals(ctx.getLabel())) { assets = parsed; definesLocalAssets = true; } } if (!definesLocalAssets) { // The target might still define an empty list of assets, in which case its information is not // propagated for efficiency. If this is the case, we will still have an artifact for the // merging output. definesLocalAssets = assetsInfo.getValidationResult() != null; } if (definesLocalResources != definesLocalAssets) { throw new EvalException( Location.BUILTIN, "Must define either both or none of assets and resources. Use the merge_assets and" + " merge_resources methods to define them, or assets_from_deps and" + " resources_from_deps to inherit without defining them."); } return Aar.makeAar( ctx, resources, assets, resourcesInfo.getManifest(), resourcesInfo.getRTxt(), libraryClassJar, filesFromConfiguredTargets(proguardSpecs)) .toProvider(deps, definesLocalResources); } @Override public SkylarkDict processLibraryData( AndroidDataContext ctx, Artifact libraryClassJar, Object manifest, Object resources, Object assets, Object assetsDir, Object exportsManifest, Object customPackage, boolean neverlink, boolean enableDataBinding, SkylarkList proguardSpecs, SkylarkList deps, Location location, Environment env) throws InterruptedException, EvalException { SkylarkList resourceDeps = getProviders(deps, AndroidResourcesInfo.PROVIDER); SkylarkList assetDeps = getProviders(deps, AndroidAssetsInfo.PROVIDER); ImmutableMap.Builder infoBuilder = ImmutableMap.builder(); AndroidResourcesInfo resourcesInfo; AndroidAssetsInfo assetsInfo; if (isNone(manifest) && isNone(resources) && isNone(assets) && isNone(assetsDir) && isNone(exportsManifest)) { // If none of these parameters were specified, for backwards compatibility, do not trigger // data processing. resourcesInfo = resourcesFromDeps(ctx, resourceDeps, neverlink, customPackage, location, env); assetsInfo = assetsFromDeps(assetDeps, neverlink, env); infoBuilder.put(AndroidResourcesInfo.PROVIDER, resourcesInfo); } else { AndroidManifestInfo baseManifest = stampAndroidManifest( ctx, manifest, customPackage, fromNoneableOrDefault(exportsManifest, Boolean.class, false), location, env); SkylarkDict resourceOutput = mergeResources( ctx, baseManifest, listFromNoneableOrEmpty(resources, ConfiguredTarget.class), resourceDeps, neverlink, enableDataBinding, location, env); resourcesInfo = (AndroidResourcesInfo) resourceOutput.get(AndroidResourcesInfo.PROVIDER); assetsInfo = mergeAssets(ctx, assets, assetsDir, assetDeps, neverlink, location, env); infoBuilder.putAll(resourceOutput); } AndroidLibraryAarInfo aarInfo = makeAar( ctx, resourcesInfo, assetsInfo, libraryClassJar, proguardSpecs, getProviders(deps, AndroidLibraryAarInfo.PROVIDER), neverlink); // Only expose the aar provider in non-neverlinked actions if (!neverlink) { infoBuilder.put(AndroidLibraryAarInfo.PROVIDER, aarInfo); } // Expose the updated manifest that was changed by resource processing // TODO(b/30817309): Use the base manifest once manifests are no longer changed in resource // processing AndroidManifestInfo manifestInfo = resourcesInfo.getManifest().toProvider(); return SkylarkDict.copyOf( /* env = */ null, infoBuilder .put(AndroidAssetsInfo.PROVIDER, assetsInfo) .put(AndroidManifestInfo.PROVIDER, manifestInfo) .build()); } @Override public SkylarkDict processAarImportData( AndroidDataContext ctx, SpecialArtifact resources, SpecialArtifact assets, Artifact androidManifestArtifact, SkylarkList deps) throws InterruptedException { AndroidAaptVersion aaptVersion = ctx.getAndroidConfig().getAndroidAaptVersion(); ValidatedAndroidResources validatedResources = AndroidResources.forAarImport(resources) .process( ctx, AndroidManifest.forAarImport(androidManifestArtifact), ResourceDependencies.fromProviders( getProviders(deps, AndroidResourcesInfo.PROVIDER), /* neverlink = */ false), DataBinding.asDisabledDataBindingContext(), aaptVersion); MergedAndroidAssets mergedAssets = AndroidAssets.forAarImport(assets) .process( ctx, AssetDependencies.fromProviders( getProviders(deps, AndroidAssetsInfo.PROVIDER), /* neverlink = */ false), aaptVersion); ResourceApk resourceApk = ResourceApk.of(validatedResources, mergedAssets, null, null); return getNativeInfosFrom(resourceApk, ctx.getLabel()); } @Override public SkylarkDict processLocalTestData( AndroidDataContext ctx, Object manifest, SkylarkList resources, Object assets, Object assetsDir, Object customPackage, String aaptVersionString, SkylarkDict manifestValues, SkylarkList deps, Location location, Environment env) throws InterruptedException, EvalException { try (SkylarkErrorReporter errorReporter = SkylarkErrorReporter.from(ctx.getActionConstructionContext(), location, env)) { AndroidManifest rawManifest = AndroidManifest.from( ctx, errorReporter, fromNoneable(manifest, Artifact.class), fromNoneable(customPackage, String.class), /* exportsManifest = */ false); ResourceApk resourceApk = AndroidLocalTestBase.buildResourceApk( ctx, getAndroidSemantics(), errorReporter, DataBinding.asDisabledDataBindingContext(), rawManifest, AndroidResources.from(errorReporter, getFileProviders(resources), "resource_files"), AndroidAssets.from( errorReporter, listFromNoneable(assets, ConfiguredTarget.class), isNone(assetsDir) ? null : PathFragment.create(fromNoneable(assetsDir, String.class))), ResourceDependencies.fromProviders( getProviders(deps, AndroidResourcesInfo.PROVIDER), /* neverlink = */ false), AssetDependencies.fromProviders( getProviders(deps, AndroidAssetsInfo.PROVIDER), /* neverlink = */ false), manifestValues, AndroidAaptVersion.chooseTargetAaptVersion(ctx, errorReporter, aaptVersionString)); return getNativeInfosFrom(resourceApk, ctx.getLabel()); } catch (RuleErrorException e) { throw new EvalException(Location.BUILTIN, e); } } @Override public BinaryDataSettings makeBinarySettings( AndroidDataContext ctx, Object shrinkResources, SkylarkList resourceConfigurationFilters, SkylarkList densities, SkylarkList noCompressExtensions, String aaptVersionString, Location location, Environment env) throws EvalException { AndroidAaptVersion aaptVersion; try (SkylarkErrorReporter errorReporter = SkylarkErrorReporter.from(ctx.getActionConstructionContext(), location, env)) { aaptVersion = AndroidAaptVersion.chooseTargetAaptVersion(ctx, errorReporter, aaptVersionString); } catch (RuleErrorException e) { throw new EvalException(Location.BUILTIN, e); } return new BinaryDataSettings( aaptVersion, fromNoneableOrDefault( shrinkResources, Boolean.class, ctx.getAndroidConfig().useAndroidResourceShrinking()), ResourceFilterFactory.from(aaptVersion, resourceConfigurationFilters, densities), noCompressExtensions.getImmutableList()); } /** * Helper method to get default {@link * com.google.devtools.build.lib.rules.android.AndroidSkylarkData.BinaryDataSettings}. */ private BinaryDataSettings defaultBinaryDataSettings( AndroidDataContext ctx, Location location, Environment env) throws EvalException { return makeBinarySettings( ctx, Runtime.NONE, SkylarkList.createImmutable(ImmutableList.of()), SkylarkList.createImmutable(ImmutableList.of()), SkylarkList.createImmutable(ImmutableList.of()), "auto", location, env); } private static class BinaryDataSettings implements AndroidBinaryDataSettingsApi { private final AndroidAaptVersion aaptVersion; private final boolean shrinkResources; private final ResourceFilterFactory resourceFilterFactory; private final ImmutableList noCompressExtensions; private BinaryDataSettings( AndroidAaptVersion aaptVersion, boolean shrinkResources, ResourceFilterFactory resourceFilterFactory, ImmutableList noCompressExtensions) { this.aaptVersion = aaptVersion; this.shrinkResources = shrinkResources; this.resourceFilterFactory = resourceFilterFactory; this.noCompressExtensions = noCompressExtensions; } } @Override public AndroidBinaryDataInfo processBinaryData( AndroidDataContext ctx, SkylarkList resources, Object assets, Object assetsDir, Object manifest, Object customPackage, SkylarkDict manifestValues, SkylarkList deps, String manifestMerger, Object maybeSettings, boolean crunchPng, boolean dataBindingEnabled, Location location, Environment env) throws InterruptedException, EvalException { try (SkylarkErrorReporter errorReporter = SkylarkErrorReporter.from(ctx.getActionConstructionContext(), location, env)) { BinaryDataSettings settings = fromNoneableOrDefault( maybeSettings, BinaryDataSettings.class, defaultBinaryDataSettings(ctx, location, env)); AndroidManifest rawManifest = AndroidManifest.from( ctx, errorReporter, fromNoneable(manifest, Artifact.class), getAndroidSemantics(), fromNoneable(customPackage, String.class), /* exportsManifest = */ false); ResourceDependencies resourceDeps = ResourceDependencies.fromProviders( getProviders(deps, AndroidResourcesInfo.PROVIDER), /* neverlink = */ false); StampedAndroidManifest stampedManifest = rawManifest.mergeWithDeps( ctx, getAndroidSemantics(), errorReporter, resourceDeps, manifestValues, manifestMerger); ResourceApk resourceApk = ProcessedAndroidData.processBinaryDataFrom( ctx, errorReporter, stampedManifest, AndroidBinary.shouldShrinkResourceCycles( ctx.getAndroidConfig(), errorReporter, settings.shrinkResources), manifestValues, settings.aaptVersion, AndroidResources.from( errorReporter, getFileProviders(resources), "resource_files"), AndroidAssets.from( errorReporter, listFromNoneable(assets, ConfiguredTarget.class), isNone(assetsDir) ? null : PathFragment.create(fromNoneable(assetsDir, String.class))), resourceDeps, AssetDependencies.fromProviders( getProviders(deps, AndroidAssetsInfo.PROVIDER), /* neverlink = */ false), settings.resourceFilterFactory, settings.noCompressExtensions, crunchPng, /* featureOf = */ null, /* featureAfter = */ null, DataBinding.contextFrom( dataBindingEnabled, ctx.getActionConstructionContext(), ctx.getAndroidConfig())) .generateRClass(ctx, settings.aaptVersion); return AndroidBinaryDataInfo.of( resourceApk.getArtifact(), resourceApk.getResourceProguardConfig(), resourceApk.toResourceInfo(ctx.getLabel()), resourceApk.toAssetsInfo(ctx.getLabel()), resourceApk.toManifestInfo().get()); } catch (RuleErrorException e) { throw new EvalException(location, e); } } @Override public AndroidBinaryDataInfo shrinkDataApk( AndroidDataContext ctx, AndroidBinaryDataInfo binaryDataInfo, Artifact proguardOutputJar, Artifact proguardMapping, Object maybeSettings, SkylarkList deps, SkylarkList localProguardSpecs, SkylarkList extraProguardSpecs, Location location, Environment env) throws EvalException, InterruptedException { BinaryDataSettings settings = fromNoneableOrDefault( maybeSettings, BinaryDataSettings.class, defaultBinaryDataSettings(ctx, location, env)); if (!settings.shrinkResources) { return binaryDataInfo; } ImmutableList proguardSpecs = AndroidBinary.getProguardSpecs( ctx, getAndroidSemantics(), binaryDataInfo.getResourceProguardConfig(), binaryDataInfo.getManifestInfo().getManifest(), filesFromConfiguredTargets(localProguardSpecs), filesFromConfiguredTargets(extraProguardSpecs), getProviders(deps, ProguardSpecProvider.PROVIDER)); // TODO(asteinb): There should never be more than one direct resource exposed in the provider. // Can we adjust its structure to take this into account? if (!binaryDataInfo.getResourcesInfo().getDirectAndroidResources().isSingleton()) { throw new EvalException( Location.BUILTIN, "Expected exactly 1 direct android resource container, but found: " + binaryDataInfo.getResourcesInfo().getDirectAndroidResources()); } Optional maybeShrunkApk = AndroidBinary.maybeShrinkResources( ctx, binaryDataInfo.getResourcesInfo().getDirectAndroidResources().toList().get(0), ResourceDependencies.fromProviders( getProviders(deps, AndroidResourcesInfo.PROVIDER), /* neverlink = */ false), proguardSpecs, proguardOutputJar, proguardMapping, settings.aaptVersion, settings.resourceFilterFactory, settings.noCompressExtensions); return maybeShrunkApk.map(binaryDataInfo::withShrunkApk).orElse(binaryDataInfo); } public static SkylarkDict getNativeInfosFrom( ResourceApk resourceApk, Label label) { ImmutableMap.Builder builder = ImmutableMap.builder(); builder .put(AndroidResourcesInfo.PROVIDER, resourceApk.toResourceInfo(label)) .put(AndroidAssetsInfo.PROVIDER, resourceApk.toAssetsInfo(label)); resourceApk.toManifestInfo().ifPresent(info -> builder.put(AndroidManifestInfo.PROVIDER, info)); builder.put(JavaInfo.PROVIDER, getJavaInfoForRClassJar(resourceApk.getResourceJavaClassJar())); return SkylarkDict.copyOf(/* env = */ null, builder.build()); } private static JavaInfo getJavaInfoForRClassJar(Artifact rClassJar) { return JavaInfo.Builder.create() .setNeverlink(true) .addProvider( JavaCompilationInfoProvider.class, new JavaCompilationInfoProvider.Builder() .setCompilationClasspath(NestedSetBuilder.create(Order.NAIVE_LINK_ORDER, rClassJar)) .build()) .build(); } /** Checks if a "Noneable" object passed by Skylark is "None", which Java should treat as null. */ public static boolean isNone(Object object) { return object == Runtime.NONE; } /** * Converts a "Noneable" Object passed by Skylark to an nullable object of the appropriate type. * *

Skylark "Noneable" types are passed in as an Object that may be either the correct type or a * Runtime.NONE object. Skylark will handle type checking, based on the appropriate @param * annotation, but we still need to do the actual cast (or conversion to null) ourselves. * * @param object the Noneable object * @param clazz the correct class, as defined in the @Param annotation * @param the type to cast to * @return {@code null}, if the noneable argument was None, or the cast object, otherwise. */ @Nullable public static T fromNoneable(Object object, Class clazz) { if (isNone(object)) { return null; } return clazz.cast(object); } public static T fromNoneableOrDefault(Object object, Class clazz, T defaultValue) { T value = fromNoneable(object, clazz); if (value == null) { return defaultValue; } return value; } /** * Converts a "Noneable" Object passed by Skylark to a List of the appropriate type. * *

This first calls {@link #fromNoneable(Object, Class)} to get a SkylarkList, then safely * casts it to a list with the appropriate generic. */ @Nullable public static List listFromNoneable(Object object, Class clazz) throws EvalException { SkylarkList asList = fromNoneable(object, SkylarkList.class); if (asList == null) { return null; } return SkylarkList.castList(asList, clazz, null); } private static ImmutableList filesFromConfiguredTargets( SkylarkList targets) { ImmutableList.Builder builder = ImmutableList.builder(); for (FileProvider provider : getFileProviders(targets)) { builder.addAll(provider.getFilesToBuild()); } return builder.build(); } private static ImmutableList getFileProviders( SkylarkList targets) { return getProviders(targets, FileProvider.class); } private static ImmutableList getProviders( SkylarkList targets, Class clazz) { return targets .stream() .map(target -> target.getProvider(clazz)) .filter(Objects::nonNull) .collect(ImmutableList.toImmutableList()); } public static SkylarkList getProviders( SkylarkList targets, NativeProvider provider) { return SkylarkList.createImmutable( targets .stream() .map(target -> target.get(provider)) .filter(Objects::nonNull) .collect(ImmutableList.toImmutableList())); } protected static SkylarkList getProviders( SkylarkList targets, BuiltinProvider provider) { return SkylarkList.createImmutable( targets .stream() .map(target -> target.get(provider)) .filter(Objects::nonNull) .collect(ImmutableList.toImmutableList())); } private static SkylarkList listFromNoneableOrEmpty(Object object, Class clazz) throws EvalException { List value = listFromNoneable(object, clazz); if (value == null) { return SkylarkList.createImmutable(ImmutableList.of()); } return SkylarkList.createImmutable(value); } }