blob: d0956b8757fd84214ae1c602c7416146cae74e48 [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.
package com.google.devtools.build.lib.rules.android;
import static com.google.common.collect.MoreCollectors.onlyElement;
import static java.util.stream.Collectors.joining;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Streams;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.MutableActionGraph.ActionConflictException;
import com.google.devtools.build.lib.actions.ResourceSet;
import com.google.devtools.build.lib.analysis.ConfiguredTarget;
import com.google.devtools.build.lib.analysis.FileProvider;
import com.google.devtools.build.lib.analysis.FilesToRunProvider;
import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder;
import com.google.devtools.build.lib.analysis.RuleConfiguredTargetFactory;
import com.google.devtools.build.lib.analysis.RuleContext;
import com.google.devtools.build.lib.analysis.Runfiles;
import com.google.devtools.build.lib.analysis.RunfilesProvider;
import com.google.devtools.build.lib.analysis.RunfilesSupport;
import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
import com.google.devtools.build.lib.analysis.Whitelist;
import com.google.devtools.build.lib.analysis.actions.CustomCommandLine;
import com.google.devtools.build.lib.analysis.actions.SpawnAction;
import com.google.devtools.build.lib.analysis.actions.Substitution;
import com.google.devtools.build.lib.analysis.actions.Template;
import com.google.devtools.build.lib.analysis.actions.TemplateExpansionAction;
import com.google.devtools.build.lib.analysis.configuredtargets.RuleConfiguredTarget.Mode;
import com.google.devtools.build.lib.analysis.test.ExecutionInfo;
import com.google.devtools.build.lib.collect.nestedset.NestedSet;
import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
import com.google.devtools.build.lib.collect.nestedset.Order;
import com.google.devtools.build.lib.packages.TargetUtils;
import com.google.devtools.build.lib.packages.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/** An implementation for the "android_device" rule. */
public class AndroidDevice implements RuleConfiguredTargetFactory {
private static final Template STUB_SCRIPT =
Template.forResource(AndroidDevice.class, "android_device_stub_template.txt");
private static final String DEVICE_BROKER_TYPE = "WRAPPED_EMULATOR";
static final String WHITELIST_NAME = "android_device";
// Min resolution
private static final int MIN_HORIZONTAL = 240;
private static final int MIN_VERTICAL = 240;
private static final int MIN_RAM = 64;
private static final int MAX_RAM = 4096;
private static final int MIN_VM_HEAP = 16;
private static final int MIN_CACHE = 16;
// http://en.wikipedia.org/wiki/List_of_displays_by_pixel_density
// this is a much lower pixels-per-inch then even some of the oldest phones.
private static final int MIN_LCD_DENSITY = 30;
private static final Predicate<Artifact> SOURCE_PROPERTIES_SELECTOR =
(Artifact artifact) -> "source.properties".equals(artifact.getExecPath().getBaseName());
private static final Predicate<Artifact> SOURCE_PROPERTIES_FILTER =
Predicates.not(SOURCE_PROPERTIES_SELECTOR);
private final AndroidSemantics androidSemantics;
protected AndroidDevice(AndroidSemantics androidSemantics) {
this.androidSemantics = androidSemantics;
}
@Override
public ConfiguredTarget create(RuleContext ruleContext)
throws InterruptedException, RuleErrorException, ActionConflictException {
androidSemantics.checkForMigrationTag(ruleContext);
checkWhitelist(ruleContext);
Artifact executable = ruleContext.createOutputArtifact();
Artifact metadata =
ruleContext.getImplicitOutputArtifact(AndroidRuleClasses.ANDROID_DEVICE_EMULATOR_METADATA);
Artifact images =
ruleContext.getImplicitOutputArtifact(AndroidRuleClasses.ANDROID_DEVICE_USERDATA_IMAGES);
NestedSetBuilder<Artifact> filesBuilder = NestedSetBuilder.stableOrder();
filesBuilder.add(executable);
filesBuilder.add(metadata);
filesBuilder.add(images);
NestedSet<Artifact> filesToBuild = filesBuilder.build();
Map<String, String> executionInfo = TargetUtils.getExecutionInfo(ruleContext.getRule());
AndroidDeviceRuleAttributes deviceAttributes =
new AndroidDeviceRuleAttributes(ruleContext, ImmutableMap.copyOf(executionInfo));
if (ruleContext.hasErrors()) {
return null;
}
// dependencies needed for the runfiles collector.
Iterable<Artifact> commonDependencyArtifacts = deviceAttributes.getCommonDependencies();
deviceAttributes.createStubScriptAction(metadata, images, executable, ruleContext);
deviceAttributes.createBootAction(metadata, images);
FilesToRunProvider unifiedLauncher = deviceAttributes.getUnifiedLauncher();
Runfiles.Builder runfilesBuilder =
new Runfiles.Builder(ruleContext.getWorkspaceName())
.addTransitiveArtifacts(filesToBuild)
.addArtifacts(commonDependencyArtifacts)
.addRunfiles(ruleContext, RunfilesProvider.DEFAULT_RUNFILES);
if (unifiedLauncher.getRunfilesSupport() != null) {
runfilesBuilder
.merge(unifiedLauncher.getRunfilesSupport().getRunfiles())
.addLegacyExtraMiddleman(unifiedLauncher.getRunfilesSupport().getRunfilesMiddleman());
} else {
runfilesBuilder.addTransitiveArtifacts(unifiedLauncher.getFilesToRun());
}
Runfiles runfiles = runfilesBuilder.build();
RunfilesSupport runfilesSupport =
RunfilesSupport.withExecutable(ruleContext, runfiles, executable);
NestedSet<Artifact> extraFilesToRun =
NestedSetBuilder.create(Order.STABLE_ORDER, runfilesSupport.getRunfilesMiddleman());
boolean dex2OatEnabled =
ruleContext.attributes().get("pregenerate_oat_files_for_tests", Type.BOOLEAN);
return new RuleConfiguredTargetBuilder(ruleContext)
.setFilesToBuild(filesToBuild)
.addProvider(RunfilesProvider.class, RunfilesProvider.simple(runfiles))
.setRunfilesSupport(runfilesSupport, executable)
.addFilesToRun(extraFilesToRun)
.addNativeDeclaredProvider(new ExecutionInfo(executionInfo))
.addNativeDeclaredProvider(new AndroidDeviceBrokerInfo(DEVICE_BROKER_TYPE))
.addNativeDeclaredProvider(
new AndroidDex2OatInfo(
dex2OatEnabled,
false /* executeDex2OatOnHost */,
null /* deviceForPregeneratingOatFilesForTests */,
null /* framework */,
null /* dalvikCache */,
null /* deviceProps */))
.build();
}
private static void checkWhitelist(RuleContext ruleContext) throws RuleErrorException {
if (!Whitelist.isAvailable(ruleContext, WHITELIST_NAME)) {
ruleContext.throwWithRuleError("The android_device rule may not be used in this package");
}
}
/**
* Handles initialization phase validation of attributes, collection of dependencies and creation
* of actions.
*/
private static class AndroidDeviceRuleAttributes {
private final RuleContext ruleContext;
private final ImmutableMap<String, String> constraints;
private final Artifact adb;
private final Artifact emulatorArm;
private final Artifact emulatorX86;
private final Artifact adbStatic;
private final ImmutableList<Artifact> emulatorX86Bios;
private final ImmutableList<Artifact> xvfbSupportFiles;
private final Artifact mksdcard;
private final Artifact snapshotFs;
private final FilesToRunProvider unifiedLauncher;
private final Artifact androidRuntest;
private final ImmutableList<Artifact> androidRuntestDeps;
private final Artifact testingShbase;
private final ImmutableList<Artifact> testingShbaseDeps;
private final Optional<Artifact> defaultProperties;
private final Iterable<Artifact> platformApks;
private final Artifact sdkPath;
private final int ram;
private final int cache;
private final int vmHeap;
private final int density;
private final int horizontalResolution;
private final int verticalResolution;
// These can only be null if there was an error during creation.
private Iterable<Artifact> systemImages;
private Artifact sourcePropertiesFile;
private ImmutableList<Artifact> commonDependencies;
private AndroidDeviceRuleAttributes(
RuleContext ruleContext, ImmutableMap<String, String> executionInfo) {
this.ruleContext = ruleContext;
this.constraints = executionInfo;
horizontalResolution = ruleContext.attributes().get("horizontal_resolution", Type.INTEGER);
verticalResolution = ruleContext.attributes().get("vertical_resolution", Type.INTEGER);
ram = ruleContext.attributes().get("ram", Type.INTEGER);
density = ruleContext.attributes().get("screen_density", Type.INTEGER);
cache = ruleContext.attributes().get("cache", Type.INTEGER);
vmHeap = ruleContext.attributes().get("vm_heap", Type.INTEGER);
defaultProperties =
Optional.fromNullable(
ruleContext.getPrerequisiteArtifact("default_properties", Mode.HOST));
adb = ruleContext.getPrerequisiteArtifact("$adb", Mode.HOST);
emulatorArm = ruleContext.getPrerequisiteArtifact("$emulator_arm", Mode.HOST);
emulatorX86 = ruleContext.getPrerequisiteArtifact("$emulator_x86", Mode.HOST);
adbStatic = ruleContext.getPrerequisiteArtifact("$adb_static", Mode.HOST);
emulatorX86Bios =
ruleContext.getPrerequisiteArtifacts("$emulator_x86_bios", Mode.HOST).list();
xvfbSupportFiles = ruleContext.getPrerequisiteArtifacts("$xvfb_support", Mode.HOST).list();
mksdcard = ruleContext.getPrerequisiteArtifact("$mksd", Mode.HOST);
snapshotFs = ruleContext.getPrerequisiteArtifact("$empty_snapshot_fs", Mode.HOST);
unifiedLauncher = ruleContext.getExecutablePrerequisite("$unified_launcher", Mode.HOST);
androidRuntestDeps =
ruleContext.getPrerequisiteArtifacts("$android_runtest", Mode.HOST).list();
androidRuntest =
androidRuntestDeps.stream().filter(Artifact::isSourceArtifact).collect(onlyElement());
testingShbaseDeps = ruleContext.getPrerequisiteArtifacts("$testing_shbase", Mode.HOST).list();
testingShbase =
testingShbaseDeps
.stream()
.filter(
(Artifact artifact) -> "googletest.sh".equals(artifact.getPath().getBaseName()))
.collect(onlyElement());
// may be empty
platformApks = ruleContext.getPrerequisiteArtifacts("platform_apks", Mode.TARGET).list();
sdkPath = ruleContext.getPrerequisiteArtifact("$sdk_path", Mode.HOST);
TransitiveInfoCollection systemImagesAndSourceProperties =
ruleContext.getPrerequisite("system_image", Mode.TARGET);
if (ruleContext.hasErrors()) {
return;
}
List<Artifact> files =
systemImagesAndSourceProperties
.getProvider(FileProvider.class)
.getFilesToBuild()
.toList();
sourcePropertiesFile = Iterables.tryFind(files, SOURCE_PROPERTIES_SELECTOR).orNull();
systemImages = Iterables.filter(files, SOURCE_PROPERTIES_FILTER);
validateAttributes();
if (sourcePropertiesFile == null) {
ruleContext.attributeError(
"system_image",
"No source.properties files exist in this "
+ "filegroup ("
+ systemImagesAndSourceProperties.getLabel()
+ ")");
}
int numberOfSourceProperties = files.size() - Iterables.size(systemImages);
if (numberOfSourceProperties > 1) {
ruleContext.attributeError(
"system_image",
"Multiple source.properties files exist in "
+ "this filegroup ("
+ systemImagesAndSourceProperties.getLabel()
+ ")");
}
if (ruleContext.hasErrors()) {
return;
}
commonDependencies =
ImmutableList.<Artifact>builder()
.add(adb)
.add(sourcePropertiesFile)
.addAll(systemImages)
.add(emulatorArm)
.add(emulatorX86)
.add(adbStatic)
.addAll(emulatorX86Bios)
.addAll(xvfbSupportFiles)
.add(mksdcard)
.add(snapshotFs)
.addAll(androidRuntestDeps)
.addAll(testingShbaseDeps)
.addAll(platformApks)
.build();
}
/*
* The stub script will find the workspace directory of its runfiles tree and then execute
* from there.
* The stub script gets run via blaze run, blaze-bin or as a part of a test.
*/
private void createStubScriptAction(
Artifact metadata, Artifact images, Artifact executable, RuleContext ruleContext) {
List<Substitution> arguments = new ArrayList<>();
arguments.add(Substitution.of("%workspace%", ruleContext.getWorkspaceName()));
arguments.add(
Substitution.of(
"%unified_launcher%", unifiedLauncher.getExecutable().getRunfilesPathString()));
arguments.add(Substitution.of("%adb%", adb.getRunfilesPathString()));
arguments.add(Substitution.of("%adb_static%", adbStatic.getRunfilesPathString()));
arguments.add(Substitution.of("%emulator_x86%", emulatorX86.getRunfilesPathString()));
arguments.add(Substitution.of("%emulator_arm%", emulatorArm.getRunfilesPathString()));
arguments.add(Substitution.of("%mksdcard%", mksdcard.getRunfilesPathString()));
arguments.add(Substitution.of("%empty_snapshot_fs%", snapshotFs.getRunfilesPathString()));
arguments.add(
Substitution.of(
"%system_images%",
Streams.stream(systemImages)
.map(Artifact::getRunfilesPathString)
.collect(joining(" "))));
arguments.add(
Substitution.of(
"%bios_files%",
emulatorX86Bios.stream().map(Artifact::getRunfilesPathString).collect(joining(" "))));
arguments.add(
Substitution.of(
"%source_properties_file%", sourcePropertiesFile.getRunfilesPathString()));
arguments.add(Substitution.of("%image_input_file%", images.getRunfilesPathString()));
arguments.add(Substitution.of("%emulator_metadata_path%", metadata.getRunfilesPathString()));
arguments.add(Substitution.of("%android_runtest%", androidRuntest.getRunfilesPathString()));
arguments.add(Substitution.of("%testing_shbase%", testingShbase.getRunfilesPathString()));
arguments.add(Substitution.of("%sdk_path%", sdkPath.getRunfilesPathString()));
ruleContext.registerAction(
new TemplateExpansionAction(
ruleContext.getActionOwner(), executable, STUB_SCRIPT, arguments, true));
}
public FilesToRunProvider getUnifiedLauncher() {
return unifiedLauncher;
}
public void createBootAction(Artifact metadata, Artifact images) {
// the boot action will run during the build so use execpath
// strings to find all dependent artifacts (there is no nicely created runfiles
// folder we're executing in).
SpawnAction.Builder spawnBuilder =
new SpawnAction.Builder()
.addOutput(metadata)
.addOutput(images)
.addInputs(commonDependencies)
.setMnemonic("AndroidDeviceBoot")
.setProgressMessage("Creating Android image for %s", ruleContext.getLabel())
.setExecutionInfo(constraints)
.setExecutable(unifiedLauncher)
// Boot resource estimation:
// RAM: the emulator will use as much ram as has been requested in the device rule
// (there is a slight overhead for qemu's internals, but this is miniscule).
// CPU: 100% - the emulator will peg a single cpu during boot because it's a very
// computation intensive part of the lifecycle.
.setResources(ResourceSet.createWithRamCpu(ram, 1))
.addExecutableArguments(
"--action=boot",
"--density=" + density,
"--memory=" + ram,
"--cache=" + cache,
"--vm_size=" + vmHeap,
"--generate_output_dir="
+ images.getExecPath().getParentDirectory().getPathString(),
"--skin=" + getScreenSize(),
"--source_properties_file=" + sourcePropertiesFile.getExecPathString(),
"--system_images=" + Artifact.joinExecPaths(" ", systemImages),
"--flag_configured_android_tools",
"--adb=" + adb.getExecPathString(),
"--emulator_x86=" + emulatorX86.getExecPathString(),
"--emulator_arm=" + emulatorArm.getExecPathString(),
"--adb_static=" + adbStatic.getExecPathString(),
"--mksdcard=" + mksdcard.getExecPathString(),
"--empty_snapshot_fs=" + snapshotFs.getExecPathString(),
"--bios_files=" + Artifact.joinExecPaths(",", emulatorX86Bios),
"--nocopy_system_images",
"--single_image_file",
"--android_sdk_path=" + sdkPath.getExecPathString(),
"--platform_apks=" + Artifact.joinExecPaths(",", platformApks));
CustomCommandLine.Builder commandLine = CustomCommandLine.builder();
if (defaultProperties.isPresent()) {
spawnBuilder.addInput(defaultProperties.get());
commandLine.addPrefixedExecPath("--default_properties_file=", defaultProperties.get());
}
spawnBuilder.addCommandLine(commandLine.build());
ruleContext.registerAction(spawnBuilder.build(ruleContext));
}
public ImmutableList<Artifact> getCommonDependencies() {
return commonDependencies;
}
private void validateAttributes() {
if (horizontalResolution < MIN_HORIZONTAL) {
ruleContext.attributeError(
"horizontal_resolution", "horizontal must be at least: " + MIN_HORIZONTAL);
}
if (verticalResolution < MIN_VERTICAL) {
ruleContext.attributeError(
"vertical_resolution", "vertical must be at least: " + MIN_VERTICAL);
}
if (ram < MIN_RAM) {
ruleContext.attributeError("ram", "ram must be at least: " + MIN_RAM);
}
if (ram > MAX_RAM) {
ruleContext.attributeError("ram", "ram cannot be greater than: " + MAX_RAM);
}
if (density < MIN_LCD_DENSITY) {
ruleContext.attributeError(
"screen_density", "density must be at least: " + MIN_LCD_DENSITY);
}
if (cache < MIN_CACHE) {
ruleContext.attributeError("cache", "cache must be at least: " + MIN_CACHE);
}
if (vmHeap < MIN_VM_HEAP) {
ruleContext.attributeError("vm_heap", "vm heap must be at least: " + MIN_VM_HEAP);
}
}
private String getScreenSize() {
return horizontalResolution + "x" + verticalResolution;
}
}
}