Java tutorial
// 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.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.skylarkinterface.Param; import com.google.devtools.build.lib.skylarkinterface.SkylarkCallable; import com.google.devtools.build.lib.skylarkinterface.SkylarkModule; 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). */ @SkylarkModule(name = "android_data", doc = "Utilities for working with Android data (manifests, resources, and assets). " + "This API is non-final and subject to change without warning; do not rely on it.") public abstract class AndroidSkylarkData { public abstract AndroidSemantics getAndroidSemantics(); @SkylarkCallable(name = "assets_from_deps", parameters = { @Param(name = "deps", defaultValue = "[]", type = SkylarkList.class, generic1 = AndroidAssetsInfo.class, positional = false, named = true, doc = "Dependencies to inherit assets from"), @Param(name = "neverlink", defaultValue = "False", type = Boolean.class, positional = false, named = true, doc = "Defaults to False. If true, assets will not be exposed to targets that depend on" + " them.") }, useEnvironment = true, doc = "Creates an AndroidAssetsInfo from this target's asset dependencies, ignoring local" + " assets. No processing will be done. This method is deprecated and exposed only" + " for backwards-compatibility with existing Native behavior.") public static AndroidAssetsInfo assetsFromDeps(SkylarkList<AndroidAssetsInfo> deps, boolean neverlink, Environment env) { return AssetDependencies.fromProviders(deps, neverlink).toInfo(env.getCallerLabel()); } @SkylarkCallable(name = "resources_from_deps", mandatoryPositionals = 1, // context parameters = { @Param(name = "deps", defaultValue = "[]", type = SkylarkList.class, generic1 = AndroidResourcesInfo.class, positional = false, named = true, doc = "Dependencies to inherit resources from"), @Param(name = "neverlink", defaultValue = "False", type = Boolean.class, positional = false, named = true, doc = "Defaults to False. If true, resources will not be exposed to targets that depend" + " on them."), @Param(name = "custom_package", positional = false, defaultValue = "None", type = String.class, noneable = true, named = true, doc = "The Android application package to stamp the manifest with. If not provided, the" + " current Java package, derived from the location of this target's BUILD" + " file, will be used. For example, given a BUILD file in" + " 'java/com/foo/bar/BUILD', the package would be 'com.foo.bar'."), }, useLocation = true, useEnvironment = true, doc = "Creates an AndroidResourcesInfo from this target's resource dependencies, ignoring local" + " resources. Only processing of deps will be done. This method is deprecated and" + " exposed only for backwards-compatibility with existing Native behavior. An empty" + " manifest will be generated and included in the provider - this path should not" + " be used when an explicit manifest is specified.") public static AndroidResourcesInfo resourcesFromDeps(AndroidDataContext ctx, SkylarkList<AndroidResourcesInfo> 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(ctx.getActionConstructionContext(), errorReporter); } return ResourceApk .processFromTransitiveLibraryData(ctx, ResourceDependencies.fromProviders(deps, /* neverlink = */ neverlink), AssetDependencies.empty(), StampedAndroidManifest .createEmpty(ctx.getActionConstructionContext(), pkg, /* exported = */ false)) .toResourceInfo(ctx.getLabel()); } } @SkylarkCallable(name = "stamp_manifest", mandatoryPositionals = 1, // AndroidDataContext ctx is mandatory parameters = { @Param(name = "manifest", positional = false, defaultValue = "None", type = Artifact.class, noneable = true, named = true, doc = "The manifest to stamp. If not passed, a dummy manifest will be generated"), @Param(name = "custom_package", positional = false, defaultValue = "None", type = String.class, noneable = true, named = true, doc = "The Android application package to stamp the manifest with. If not provided, the" + " current Java package, derived from the location of this target's BUILD" + " file, will be used. For example, given a BUILD file in" + " 'java/com/foo/bar/BUILD', the package would be 'com.foo.bar'."), @Param(name = "exports_manifest", positional = false, defaultValue = "False", type = Boolean.class, named = true, doc = "Defaults to False. If passed as True, this manifest will be exported to and" + " eventually merged into targets that depend on it. Otherwise, it won't be" + " inherited."), }, useLocation = true, useEnvironment = true, doc = "Stamps a manifest with package information.") 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)) { if (pkg == null) { pkg = AndroidManifest.getDefaultPackage(ctx.getActionConstructionContext(), errorReporter); } } Artifact primaryManifest = fromNoneable(manifest, Artifact.class); if (primaryManifest == null) { return StampedAndroidManifest.createEmpty(ctx.getActionConstructionContext(), pkg, exported) .toProvider(); } // If needed, rename the manifest to "AndroidManifest.xml", which aapt expects. Artifact renamedManifest = getAndroidSemantics().renameManifest(ctx, primaryManifest); return new AndroidManifest(renamedManifest, pkg, exported).stamp(ctx).toProvider(); } @SkylarkCallable(name = "merge_assets", mandatoryPositionals = 1, // context parameters = { @Param(name = "assets", positional = false, defaultValue = "None", type = SkylarkList.class, generic1 = ConfiguredTarget.class, noneable = true, named = true, doc = "Targets containing raw assets for this target. If passed, 'assets_dir' must also" + " be passed."), @Param(name = "assets_dir", positional = false, defaultValue = "None", type = String.class, noneable = true, named = true, doc = "Directory the assets are contained in. Must be passed if and only if 'assets' is" + " passed. This path will be split off of the asset paths on the device."), @Param(name = "deps", positional = false, defaultValue = "[]", type = SkylarkList.class, generic1 = AndroidAssetsInfo.class, named = true, doc = "Providers containing assets from dependencies. These assets will be merged" + " together with each other and this target's assets."), @Param(name = "neverlink", positional = false, defaultValue = "False", type = Boolean.class, named = true, doc = "Defaults to False. If passed as True, these assets will not be inherited by" + " targets that depend on this one.") }, useLocation = true, useEnvironment = true, doc = "Merges this target's assets together with assets inherited from dependencies. Note that," + " by default, actions for validating the merge are created but may not be called." + " You may want to force these actions to be called - see the 'validation_result'" + " field in AndroidAssetsInfo") public AndroidAssetsInfo mergeAssets(AndroidDataContext ctx, Object assets, Object assetsDir, SkylarkList<AndroidAssetsInfo> 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.getImmutableList(), neverlink)).toProvider(); } catch (RuleErrorException e) { throw new EvalException(Location.BUILTIN, e); } } @SkylarkCallable(name = "merge_resources", mandatoryPositionals = 2, // context, manifest parameters = { @Param(name = "resources", positional = false, defaultValue = "[]", type = SkylarkList.class, generic1 = FileProvider.class, named = true, doc = "Providers of this target's resources"), @Param(name = "deps", positional = false, defaultValue = "[]", type = SkylarkList.class, generic1 = AndroidResourcesInfo.class, named = true, doc = "Targets containing raw resources from dependencies. These resources will be merged" + " together with each other and this target's resources."), @Param(name = "neverlink", positional = false, defaultValue = "False", type = Boolean.class, named = true, doc = "Defaults to False. If passed as True, these resources will not be inherited by" + " targets that depend on this one."), @Param(name = "enable_data_binding", positional = false, defaultValue = "False", type = Boolean.class, named = true, doc = "Defaults to False. If True, processes data binding expressions in layout" + " resources."), }, useLocation = true, useEnvironment = true, doc = "Merges this target's resources together with resources inherited from dependencies." + " Returns a dict of provider type to actual info, with elements for" + " AndroidResourcesInfo (various resource information) and JavaInfo (wrapping the" + " R.class jar, for use in Java compilation). The passed manifest provider is used" + " to get Android package information and to validate that all resources it refers" + " to are available. Note that this method might do additional processing to this" + " manifest, so in the future, you may want to use the manifest contained in this" + " method's output instead of this one.") public SkylarkDict<Provider, NativeInfo> mergeResources(AndroidDataContext ctx, AndroidManifestInfo manifest, SkylarkList<ConfiguredTarget> resources, SkylarkList<AndroidResourcesInfo> 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), enableDataBinding, 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); } } @SkylarkCallable(name = "make_aar", mandatoryPositionals = 4, // context, resource info, asset info, and library class jar parameters = { @Param(name = "proguard_specs", type = SkylarkList.class, generic1 = ConfiguredTarget.class, defaultValue = "[]", positional = false, named = true, doc = "Files to be used as Proguard specification for this target, which will be" + " inherited in the top-level target"), @Param(name = "deps", type = SkylarkList.class, generic1 = AndroidLibraryAarInfo.class, defaultValue = "[]", positional = false, named = true, doc = "Dependant AAR providers used to build this AAR."), @Param(name = "neverlink", type = Boolean.class, defaultValue = "False", positional = false, named = true, doc = "Defaults to False. If true, this target's Aar will not be generated or propagated" + " to targets that depend upon it."), }, doc = "Builds an AAR and corresponding provider for this target. The resource and asset" + " providers from this same target must both be passed, as must the class JAR output" + " of building the Android Java library.") public AndroidLibraryAarInfo makeAar(AndroidDataContext ctx, AndroidResourcesInfo resourcesInfo, AndroidAssetsInfo assetsInfo, Artifact libraryClassJar, SkylarkList<ConfiguredTarget> proguardSpecs, SkylarkList<AndroidLibraryAarInfo> 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) { ValidatedAndroidData validatedAndroidData = resourcesInfo.getDirectAndroidResources().toList().get(0); if (validatedAndroidData.getLabel().equals(ctx.getLabel())) { // TODO(b/77574966): Remove this cast once we get rid of ResourceContainer and can guarantee // that only properly processed resources are passed into this object. if (!(validatedAndroidData instanceof ValidatedAndroidResources)) { throw new EvalException(Location.BUILTIN, "Old data processing pipeline does not support the Skylark API"); } resources = (ValidatedAndroidResources) validatedAndroidData; } else { definesLocalResources = false; } } // Get the target's local assets, if defined, from the provider boolean definesLocalAssets = assetsInfo.getDirectParsedAssets().isSingleton(); AndroidAssets assets = AndroidAssets.empty(); if (definesLocalAssets) { ParsedAndroidAssets parsed = assetsInfo.getDirectParsedAssets().toList().get(0); if (parsed.getLabel().equals(ctx.getLabel())) { assets = parsed; } else { definesLocalAssets = false; } } 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); } @SkylarkCallable(name = "process_library_data", mandatoryPositionals = 2, // ctx and libraryClassJar are required parameters = { @Param(name = "manifest", positional = false, type = Artifact.class, defaultValue = "None", named = true, noneable = true, doc = "If passed, the manifest to use for this target. Otherwise, a dummy manifest will" + " be generated."), @Param(name = "resources", positional = false, defaultValue = "None", type = SkylarkList.class, generic1 = FileProvider.class, named = true, noneable = true, doc = "Providers of this target's resources"), @Param(name = "assets", positional = false, defaultValue = "None", type = SkylarkList.class, generic1 = ConfiguredTarget.class, noneable = true, named = true, doc = "Targets containing raw assets for this target. If passed, 'assets_dir' must also" + " be passed."), @Param(name = "assets_dir", positional = false, defaultValue = "None", type = String.class, noneable = true, named = true, doc = "Directory the assets are contained in. Must be passed if and only if 'assets' is" + " passed. This path will be split off of the asset paths on the device."), @Param(name = "exports_manifest", positional = false, defaultValue = "None", type = Boolean.class, named = true, noneable = true, doc = "Defaults to False. If passed as True, this manifest will be exported to and" + " eventually merged into targets that depend on it. Otherwise, it won't be" + " inherited."), @Param(name = "custom_package", positional = false, defaultValue = "None", type = String.class, noneable = true, named = true, doc = "The Android application package to stamp the manifest with. If not provided, the" + " current Java package, derived from the location of this target's BUILD" + " file, will be used. For example, given a BUILD file in" + " 'java/com/foo/bar/BUILD', the package would be 'com.foo.bar'."), @Param(name = "neverlink", positional = false, defaultValue = "False", type = Boolean.class, named = true, doc = "Defaults to False. If passed as True, these resources and assets will not be" + " inherited by targets that depend on this one."), @Param(name = "enable_data_binding", positional = false, defaultValue = "False", type = Boolean.class, named = true, doc = "Defaults to False. If True, processes data binding expressions in layout" + " resources."), @Param(name = "proguard_specs", type = SkylarkList.class, generic1 = ConfiguredTarget.class, defaultValue = "[]", positional = false, named = true, doc = "Files to be used as Proguard specification for this target, which will be" + " inherited in the top-level target"), @Param(name = "deps", positional = false, defaultValue = "[]", type = SkylarkList.class, generic1 = AndroidAssetsInfo.class, named = true, doc = "Dependency targets. Providers will be extracted from these dependencies for each" + " type of data."), }, useLocation = true, useEnvironment = true, doc = "Performs full processing of data for android_library or similar rules. Returns a dict" + " from provider type to providers for the target.") public SkylarkDict<Provider, NativeInfo> processLibraryData(AndroidDataContext ctx, Artifact libraryClassJar, Object manifest, Object resources, Object assets, Object assetsDir, Object exportsManifest, Object customPackage, boolean neverlink, boolean enableDataBinding, SkylarkList<ConfiguredTarget> proguardSpecs, SkylarkList<ConfiguredTarget> deps, Location location, Environment env) throws InterruptedException, EvalException { SkylarkList<AndroidResourcesInfo> resourceDeps = getProviders(deps, AndroidResourcesInfo.PROVIDER); SkylarkList<AndroidAssetsInfo> assetDeps = getProviders(deps, AndroidAssetsInfo.PROVIDER); ImmutableMap.Builder<Provider, NativeInfo> 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<Provider, NativeInfo> 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()); } @SkylarkCallable(name = "process_aar_import_data", // context, resource and asset TreeArtifacts, and manifest artifact are all mandatory mandatoryPositionals = 4, parameters = { @Param(name = "deps", type = SkylarkList.class, generic1 = ConfiguredTarget.class, named = true, positional = false, defaultValue = "[]", doc = "Targets to inherit asset and resource dependencies from.") }, doc = "Processes assets, resources, and manifest for aar_import targets") public SkylarkDict<Provider, NativeInfo> processAarImportData(AndroidDataContext ctx, SpecialArtifact resources, SpecialArtifact assets, Artifact androidManifestArtifact, SkylarkList<ConfiguredTarget> 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), /* enableDataBinding = */ false, aaptVersion); MergedAndroidAssets mergedAssets = AndroidAssets.forAarImport(assets).process(ctx, AssetDependencies .fromProviders(getProviders(deps, AndroidAssetsInfo.PROVIDER), /* neverlink = */ false)); ResourceApk resourceApk = ResourceApk.of(validatedResources, mergedAssets, null, null); return getNativeInfosFrom(resourceApk, ctx.getLabel()); } @SkylarkCallable(name = "process_local_test_data", mandatoryPositionals = 1, // context is mandatory parameters = { @Param(name = "manifest", positional = false, type = Artifact.class, defaultValue = "None", named = true, noneable = true, doc = "If passed, the manifest to use for this target. Otherwise, a dummy manifest will" + " be generated."), @Param(name = "resources", positional = false, defaultValue = "[]", type = SkylarkList.class, generic1 = FileProvider.class, named = true, doc = "Providers of this target's resources"), @Param(name = "assets", positional = false, defaultValue = "None", type = SkylarkList.class, generic1 = ConfiguredTarget.class, noneable = true, named = true, doc = "Targets containing raw assets for this target. If passed, 'assets_dir' must also" + " be passed."), @Param(name = "assets_dir", positional = false, defaultValue = "None", type = String.class, noneable = true, named = true, doc = "Directory the assets are contained in. Must be passed if and only if 'assets' is" + " passed. This path will be split off of the asset paths on the device."), @Param(name = "custom_package", positional = false, defaultValue = "None", type = String.class, noneable = true, named = true, doc = "The Android application package to stamp the manifest with. If not provided, the" + " current Java package, derived from the location of this target's BUILD" + " file, will be used. For example, given a BUILD file in" + " 'java/com/foo/bar/BUILD', the package would be 'com.foo.bar'."), @Param(name = "aapt_version", positional = false, defaultValue = "'auto'", type = String.class, named = true, doc = "The version of aapt to use. Defaults to 'auto'. 'aapt' and 'aapt2' are also" + " supported."), @Param(name = "manifest_values", positional = false, defaultValue = "{}", type = SkylarkDict.class, generic1 = String.class, named = true, doc = "A dictionary of values to be overridden in the manifest. You must expand any" + " templates in these values before they are passed to this function."), @Param(name = "deps", positional = false, defaultValue = "[]", type = SkylarkList.class, generic1 = AndroidAssetsInfo.class, named = true, doc = "Dependency targets. Providers will be extracted from these dependencies for each" + " type of data."), }, useLocation = true, useEnvironment = true, doc = "Processes resources, assets, and manifests for android_local_test and returns a dict" + " from provider type to the appropriate provider.") public SkylarkDict<Provider, NativeInfo> processLocalTestData(AndroidDataContext ctx, Object manifest, SkylarkList<ConfiguredTarget> resources, Object assets, Object assetsDir, Object customPackage, String aaptVersionString, SkylarkDict<String, String> manifestValues, SkylarkList<ConfiguredTarget> 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(), 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); } } @SkylarkCallable(name = "make_binary_settings", mandatoryPositionals = 1, // AndroidDataContext is mandatory parameters = { @Param(name = "shrink_resources", positional = false, noneable = true, defaultValue = "None", type = Boolean.class, named = true, doc = "Whether to shrink resources. Defaults to the value used in Android" + " configuration."), @Param(name = "resource_configuration_filters", positional = false, defaultValue = "[]", type = SkylarkList.class, generic1 = String.class, named = true, doc = "A list of resource configuration filters, such 'en' that will limit the resources" + " in the apk to only the ones in the 'en' configuration."), @Param(name = "densities", positional = false, defaultValue = "[]", type = SkylarkList.class, generic1 = String.class, named = true, doc = "Densities to filter for when building the apk. A corresponding compatible-screens" + " section will also be added to the manifest if it does not already contain a" + " superset listing."), @Param(name = "nocompress_extensions", positional = false, defaultValue = "[]", type = SkylarkList.class, generic1 = String.class, named = true, doc = "A list of file extension to leave uncompressed in apk. Templates must be expanded" + " before passing this value in."), @Param(name = "aapt_version", positional = false, defaultValue = "'auto'", type = String.class, named = true, doc = "The version of aapt to use. Defaults to 'auto'. 'aapt' and 'aapt2' are also" + " supported."), }, useLocation = true, useEnvironment = true, doc = "Returns a wrapper object containing various settings shared across multiple methods for" + " processing binary data.") public BinaryDataSettings makeBinarySettings(AndroidDataContext ctx, Object shrinkResources, SkylarkList<String> resourceConfigurationFilters, SkylarkList<String> densities, SkylarkList<String> 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); } @SkylarkModule(name = "AndroidBinaryDataSettings", doc = "Wraps common settings for working with android_binary assets, resources, and manifest") private static class BinaryDataSettings { private final AndroidAaptVersion aaptVersion; private final boolean shrinkResources; private final ResourceFilterFactory resourceFilterFactory; private final ImmutableList<String> noCompressExtensions; private BinaryDataSettings(AndroidAaptVersion aaptVersion, boolean shrinkResources, ResourceFilterFactory resourceFilterFactory, ImmutableList<String> noCompressExtensions) { this.aaptVersion = aaptVersion; this.shrinkResources = shrinkResources; this.resourceFilterFactory = resourceFilterFactory; this.noCompressExtensions = noCompressExtensions; } } @SkylarkCallable(name = "process_binary_data", mandatoryPositionals = 1, // AndroidDataContext is mandatory parameters = { @Param(name = "resources", positional = false, defaultValue = "[]", type = SkylarkList.class, generic1 = FileProvider.class, named = true, doc = "Providers of this target's resources"), @Param(name = "assets", positional = false, defaultValue = "None", type = SkylarkList.class, generic1 = ConfiguredTarget.class, noneable = true, named = true, doc = "Targets containing raw assets for this target. If passed, 'assets_dir' must also" + " be passed."), @Param(name = "assets_dir", positional = false, defaultValue = "None", type = String.class, noneable = true, named = true, doc = "Directory the assets are contained in. Must be passed if and only if 'assets' is" + " passed. This path will be split off of the asset paths on the device."), @Param(name = "manifest", positional = false, type = Artifact.class, defaultValue = "None", named = true, noneable = true, doc = "If passed, the manifest to use for this target. Otherwise, a dummy manifest will" + " be generated."), @Param(name = "custom_package", positional = false, defaultValue = "None", type = String.class, noneable = true, named = true, doc = "The Android application package to stamp the manifest with. If not provided, the" + " current Java package, derived from the location of this target's BUILD" + " file, will be used. For example, given a BUILD file in" + " 'java/com/foo/bar/BUILD', the package would be 'com.foo.bar'."), @Param(name = "manifest_values", positional = false, defaultValue = "{}", type = SkylarkDict.class, generic1 = String.class, named = true, doc = "A dictionary of values to be overridden in the manifest. You must expand any" + " templates in the values before calling this function."), @Param(name = "deps", positional = false, defaultValue = "[]", type = SkylarkList.class, generic1 = ConfiguredTarget.class, named = true, doc = "Dependency targets. Providers will be extracted from these dependencies for each" + " type of data."), @Param(name = "manifest_merger", type = String.class, defaultValue = "'auto'", positional = false, named = true, doc = "The manifest merger to use. Defaults to 'auto', but 'android' and 'legacy' are" + " also supported."), @Param(name = "binary_settings", type = BinaryDataSettings.class, noneable = true, defaultValue = "None", positional = false, named = true, doc = "Settings common to various binary processing methods, created with" + " make_binary_data_settings"), @Param(name = "crunch_png", positional = false, defaultValue = "True", type = Boolean.class, named = true, doc = "Whether PNG crunching should be done. Defaults to True."), @Param(name = "enable_data_binding", positional = false, defaultValue = "False", type = Boolean.class, named = true, doc = "Defaults to False. If True, processes data binding expressions in layout" + " resources."), }, useLocation = true, useEnvironment = true, doc = "Processes resources, assets, and manifests for android_binary and returns the" + " appropriate providers.") public AndroidBinaryDataInfo processBinaryData(AndroidDataContext ctx, SkylarkList<ConfiguredTarget> resources, Object assets, Object assetsDir, Object manifest, Object customPackage, SkylarkDict<String, String> manifestValues, SkylarkList<ConfiguredTarget> 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(), resourceDeps, manifestValues, ApplicationManifest.useLegacyMerging(errorReporter, ctx.getAndroidConfig(), 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, dataBindingEnabled, /* featureOf = */ null, /* featureAfter = */ null).generateRClass(ctx, settings.aaptVersion); return AndroidBinaryDataInfo.of(resourceApk.getArtifact(), resourceApk.getResourceProguardConfig(), resourceApk.toResourceInfo(ctx.getLabel()), resourceApk.toAssetsInfo(ctx.getLabel()).get(), resourceApk.toManifestInfo().get()); } catch (RuleErrorException e) { throw new EvalException(location, e); } } @SkylarkCallable(name = "shrink_data_apk", // Required: AndroidDataContext, AndroidBinaryDataInfo to shrink, and two proguard outputs mandatoryPositionals = 4, parameters = { @Param(name = "binary_settings", type = BinaryDataSettings.class, noneable = true, defaultValue = "None", positional = false, named = true, doc = "Settings common to various binary processing methods, created with" + " make_binary_data_settings"), @Param(name = "deps", positional = false, defaultValue = "[]", type = SkylarkList.class, generic1 = ConfiguredTarget.class, named = true, doc = "Dependency targets. Providers will be extracted from these dependencies for each" + " type of data."), @Param(name = "proguard_specs", type = SkylarkList.class, generic1 = ConfiguredTarget.class, defaultValue = "[]", positional = false, named = true, doc = "Files to be used as Proguard specification for this target, which will be" + " inherited in the top-level target"), @Param(name = "extra_proguard_specs,", type = SkylarkList.class, generic1 = ConfiguredTarget.class, defaultValue = "[]", positional = false, named = true, doc = "Additional proguard specs that should be added for top-level targets. This value" + " is controlled by Java configuration."), }, useLocation = true, useEnvironment = true, doc = "Possibly shrinks the data APK by removing resources that were marked as unused during" + " proguarding.") public AndroidBinaryDataInfo shrinkDataApk(AndroidDataContext ctx, AndroidBinaryDataInfo binaryDataInfo, Artifact proguardOutputJar, Artifact proguardMapping, Object maybeSettings, SkylarkList<ConfiguredTarget> deps, SkylarkList<ConfiguredTarget> localProguardSpecs, SkylarkList<ConfiguredTarget> extraProguardSpecs, Location location, Environment env) throws EvalException, InterruptedException { BinaryDataSettings settings = fromNoneableOrDefault(maybeSettings, BinaryDataSettings.class, defaultBinaryDataSettings(ctx, location, env)); if (!settings.shrinkResources) { return binaryDataInfo; } ImmutableList<Artifact> proguardSpecs = AndroidBinary.getProguardSpecs(ctx, getAndroidSemantics(), binaryDataInfo.getResourceProguardConfig(), binaryDataInfo.getManifestInfo().getManifest(), filesFromConfiguredTargets(localProguardSpecs), filesFromConfiguredTargets(extraProguardSpecs), getProviders(deps, ProguardSpecProvider.class)); // 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<Artifact> 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<Provider, NativeInfo> getNativeInfosFrom(ResourceApk resourceApk, Label label) { ImmutableMap.Builder<Provider, NativeInfo> builder = ImmutableMap.builder(); builder.put(AndroidResourcesInfo.PROVIDER, resourceApk.toResourceInfo(label)); resourceApk.toAssetsInfo(label).ifPresent(info -> builder.put(AndroidAssetsInfo.PROVIDER, info)); 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. * * <p>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 <T> the type to cast to * @return {@code null}, if the noneable argument was None, or the cast object, otherwise. */ @Nullable public static <T> T fromNoneable(Object object, Class<T> clazz) { if (isNone(object)) { return null; } return clazz.cast(object); } public static <T> T fromNoneableOrDefault(Object object, Class<T> 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. * * <p>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 <T> List<T> listFromNoneable(Object object, Class<T> clazz) throws EvalException { SkylarkList<?> asList = fromNoneable(object, SkylarkList.class); if (asList == null) { return null; } return SkylarkList.castList(asList, clazz, null); } private static ImmutableList<Artifact> filesFromConfiguredTargets(SkylarkList<ConfiguredTarget> targets) { ImmutableList.Builder<Artifact> builder = ImmutableList.builder(); for (FileProvider provider : getFileProviders(targets)) { builder.addAll(provider.getFilesToBuild()); } return builder.build(); } private static ImmutableList<FileProvider> getFileProviders(SkylarkList<ConfiguredTarget> targets) { return getProviders(targets, FileProvider.class); } private static <T extends TransitiveInfoProvider> ImmutableList<T> getProviders( SkylarkList<ConfiguredTarget> targets, Class<T> clazz) { return targets.stream().map(target -> target.getProvider(clazz)).filter(Objects::nonNull) .collect(ImmutableList.toImmutableList()); } public static <T extends NativeInfo> SkylarkList<T> getProviders(SkylarkList<ConfiguredTarget> targets, NativeProvider<T> provider) { return SkylarkList.createImmutable(targets.stream().map(target -> target.get(provider)) .filter(Objects::nonNull).collect(ImmutableList.toImmutableList())); } private static <T> SkylarkList<T> listFromNoneableOrEmpty(Object object, Class<T> clazz) throws EvalException { List<T> value = listFromNoneable(object, clazz); if (value == null) { return SkylarkList.createImmutable(ImmutableList.of()); } return SkylarkList.createImmutable(value); } }