blob: 93fe3cd9b1dca997bd7c5927f164f0aef0dc9dc5 [file] [log] [blame]
// Copyright 2015 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.devtools.build.lib.runtime.mobileinstall;
import static com.google.devtools.build.lib.analysis.OutputGroupInfo.INTERNAL_SUFFIX;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.devtools.build.lib.analysis.ConfiguredTarget;
import com.google.devtools.build.lib.analysis.config.BuildConfiguration;
import com.google.devtools.build.lib.analysis.configuredtargets.AbstractConfiguredTarget;
import com.google.devtools.build.lib.analysis.test.TestConfiguration.TestOptions;
import com.google.devtools.build.lib.buildtool.BuildRequest;
import com.google.devtools.build.lib.buildtool.BuildResult;
import com.google.devtools.build.lib.buildtool.BuildTool;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.rules.AliasConfiguredTarget;
import com.google.devtools.build.lib.rules.android.WriteAdbArgsAction;
import com.google.devtools.build.lib.rules.android.WriteAdbArgsAction.StartType;
import com.google.devtools.build.lib.runtime.BlazeCommand;
import com.google.devtools.build.lib.runtime.BlazeCommandResult;
import com.google.devtools.build.lib.runtime.Command;
import com.google.devtools.build.lib.runtime.CommandEnvironment;
import com.google.devtools.build.lib.runtime.CommonCommandOptions;
import com.google.devtools.build.lib.runtime.commands.BuildCommand;
import com.google.devtools.build.lib.runtime.commands.ProjectFileSupport;
import com.google.devtools.build.lib.shell.AbnormalTerminationException;
import com.google.devtools.build.lib.shell.BadExitStatusException;
import com.google.devtools.build.lib.shell.CommandException;
import com.google.devtools.build.lib.util.CommandBuilder;
import com.google.devtools.build.lib.util.DetailedExitCode;
import com.google.devtools.build.lib.util.ExitCode;
import com.google.devtools.build.lib.util.io.OutErr;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.common.options.Converters;
import com.google.devtools.common.options.EnumConverter;
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.OptionMetadataTag;
import com.google.devtools.common.options.OptionPriority.PriorityCategory;
import com.google.devtools.common.options.OptionsBase;
import com.google.devtools.common.options.OptionsParser;
import com.google.devtools.common.options.OptionsParsingException;
import com.google.devtools.common.options.OptionsParsingResult;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/** Implementation of the 'mobile-install' command. */
@Command(
name = "mobile-install",
builds = true,
options = {MobileInstallCommand.Options.class, WriteAdbArgsAction.Options.class},
inherits = {BuildCommand.class},
shortDescription = "Installs targets to mobile devices.",
completion = "label",
allowResidue = true,
help = "resource:mobile-install.txt"
)
public class MobileInstallCommand implements BlazeCommand {
/**
* An enumeration of all the modes that mobile-install supports.
*/
public enum Mode { CLASSIC, CLASSIC_INTERNAL_TEST_DO_NOT_USE, SKYLARK }
/**
* Converter for the --mode option.
*/
public static class ModeConverter extends EnumConverter<Mode> {
public ModeConverter() {
super(Mode.class, "mode");
}
}
/**
* Command line options for the 'mobile-install' command.
*/
public static final class Options extends OptionsBase {
@Option(
name = "split_apks",
defaultValue = "false",
documentationCategory = OptionDocumentationCategory.OUTPUT_SELECTION,
effectTags = {OptionEffectTag.LOADING_AND_ANALYSIS, OptionEffectTag.AFFECTS_OUTPUTS},
help =
"Whether to use split apks to install and update the "
+ "application on the device. Works only with devices with "
+ "Marshmallow or later"
)
public boolean splitApks;
@Option(
name = "incremental",
defaultValue = "false",
documentationCategory = OptionDocumentationCategory.OUTPUT_SELECTION,
effectTags = OptionEffectTag.LOADING_AND_ANALYSIS,
help =
"Whether to do an incremental install. If true, try to avoid unnecessary additional "
+ "work by reading the state of the device the code is to be installed on and using "
+ "that information to avoid unnecessary work. If false (the default), always do a "
+ "full install."
)
public boolean incremental;
@Option(
name = "mode",
defaultValue = "classic",
converter = ModeConverter.class,
documentationCategory = OptionDocumentationCategory.EXECUTION_STRATEGY,
effectTags = {OptionEffectTag.LOADING_AND_ANALYSIS, OptionEffectTag.EXECUTION},
metadataTags = {OptionMetadataTag.INCOMPATIBLE_CHANGE},
help =
"Select how to run mobile-install. \"classic\" runs the current version of "
+ "mobile-install. \"skylark\" uses the new skylark version, which has support for "
+ "android_test."
)
public Mode mode;
@Option(
name = "mobile_install_aspect",
defaultValue = "@android_test_support//tools/android/mobile_install:mobile-install.bzl",
documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
effectTags = {OptionEffectTag.LOADING_AND_ANALYSIS, OptionEffectTag.CHANGES_INPUTS},
help = "The aspect to use for mobile-install."
)
public String mobileInstallAspect;
@Option(
name = "mobile_install_supported_rules",
defaultValue = "",
converter = Converters.CommaSeparatedOptionListConverter.class,
documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
effectTags = {OptionEffectTag.LOADING_AND_ANALYSIS},
help = "The supported rules for mobile-install.")
public List<String> mobileInstallSupportedRules;
}
private static final String SINGLE_TARGET_MESSAGE =
"Can only run a single target. Do not use wildcards that match more than one target";
private static final String NO_TARGET_MESSAGE = "No targets found to run";
@Override
public BlazeCommandResult exec(CommandEnvironment env, OptionsParsingResult options) {
Options mobileInstallOptions = options.getOptions(Options.class);
WriteAdbArgsAction.Options adbOptions = options.getOptions(WriteAdbArgsAction.Options.class);
if (mobileInstallOptions.mode == Mode.CLASSIC
|| mobileInstallOptions.mode == Mode.CLASSIC_INTERNAL_TEST_DO_NOT_USE) {
// Notify internal users that classic mode is no longer supported.
if (mobileInstallOptions.mode == Mode.CLASSIC
&& !mobileInstallOptions.mobileInstallAspect.startsWith("@")) {
env.getReporter().handle(Event.error(
"mobile-install --mode=classic is no longer supported"));
return BlazeCommandResult.exitCode(ExitCode.COMMAND_LINE_ERROR);
}
if (adbOptions.start == StartType.WARM && !mobileInstallOptions.incremental) {
env.getReporter().handle(Event.warn(
"Warm start is enabled, but will have no effect on a non-incremental build"));
}
List<String> targets =
ProjectFileSupport.getTargets(env.getRuntime().getProjectFileProvider(), options);
BuildRequest request =
BuildRequest.create(
this.getClass().getAnnotation(Command.class).name(),
options,
env.getRuntime().getStartupOptionsProvider(),
targets,
env.getReporter().getOutErr(),
env.getCommandId(),
env.getCommandStartTime());
DetailedExitCode detailedExitCode =
new BuildTool(env).processRequest(request, null).getDetailedExitCode();
return BlazeCommandResult.detailedExitCode(detailedExitCode);
}
// This list should look like: ["//executable:target", "arg1", "arg2"]
List<String> targetAndArgs = options.getResidue();
// The user must at least specify an executable target.
if (targetAndArgs.isEmpty()) {
env.getReporter().handle(Event.error("Must specify a target to run"));
return BlazeCommandResult.exitCode(ExitCode.COMMAND_LINE_ERROR);
}
List<String> targets = ImmutableList.of(targetAndArgs.get(0));
List<String> runTargetArgs = targetAndArgs.subList(1, targetAndArgs.size());
OutErr outErr = env.getReporter().getOutErr();
BuildRequest request =
BuildRequest.create(
this.getClass().getAnnotation(Command.class).name(),
options,
env.getRuntime().getStartupOptionsProvider(),
targets,
outErr,
env.getCommandId(),
env.getCommandStartTime());
BuildResult result = new BuildTool(env).processRequest(request, null);
if (!result.getSuccess()) {
env.getReporter().handle(Event.error("Build failed. Not running target"));
return BlazeCommandResult.detailedExitCode(result.getDetailedExitCode());
}
Collection<ConfiguredTarget> targetsBuilt = result.getSuccessfulTargets();
if (targetsBuilt == null) {
env.getReporter().handle(Event.warn(NO_TARGET_MESSAGE));
return BlazeCommandResult.exitCode(ExitCode.SUCCESS);
}
if (targetsBuilt.size() != 1) {
env.getReporter().handle(Event.error(SINGLE_TARGET_MESSAGE));
return BlazeCommandResult.exitCode(ExitCode.COMMAND_LINE_ERROR);
}
ConfiguredTarget targetToRun = Iterables.getOnlyElement(targetsBuilt);
if (!mobileInstallOptions.mobileInstallSupportedRules.isEmpty()) {
if (!isTargetSupported(env, targetToRun, mobileInstallOptions.mobileInstallSupportedRules)) {
return BlazeCommandResult.exitCode(ExitCode.RUN_FAILURE);
}
}
List<String> cmdLine = new ArrayList<>();
// TODO(bazel-team): Get the executable path from the filesToRun provider from the aspect.
BuildConfiguration configuration =
env.getSkyframeExecutor()
.getConfiguration(env.getReporter(), targetToRun.getConfigurationKey());
cmdLine.add(
configuration.getBinFragment().getPathString()
+ "/"
+ targetToRun.getLabel().toPathFragment().getPathString()
+ "_mi/launcher");
cmdLine.addAll(runTargetArgs);
cmdLine.add("--build_id=" + env.getCommandId());
// Collect relevant common command options
CommonCommandOptions commonCommandOptions = options.getOptions(CommonCommandOptions.class);
if (!commonCommandOptions.toolTag.isEmpty()) {
cmdLine.add("--tool_tag=" + commonCommandOptions.toolTag);
}
// Collect relevant adb options
cmdLine.add("--start=" + adbOptions.start);
if (!adbOptions.adb.isEmpty()) {
cmdLine.add("--adb=" + adbOptions.adb);
}
for (String adbArg : adbOptions.adbArgs) {
if (!adbArg.isEmpty()) {
cmdLine.add("--adb_arg=" + adbArg);
}
}
if (!adbOptions.device.isEmpty()) {
cmdLine.add("--device=" + adbOptions.device);
}
// Collect relevant test options
TestOptions testOptions = options.getOptions(TestOptions.class);
// Default value of testFilter is null.
if (!Strings.isNullOrEmpty(testOptions.testFilter)){
cmdLine.add("--test_filter=" + testOptions.testFilter);
}
for (String arg : testOptions.testArguments) {
if (!arg.isEmpty()) {
cmdLine.add("--test_arg=" + arg);
}
}
Path workingDir =
env.getDirectories().getOutputPath(env.getWorkspaceName()).getParentDirectory();
com.google.devtools.build.lib.shell.Command command =
new CommandBuilder()
.addArgs(cmdLine)
.setEnv(env.getClientEnv())
.setWorkingDir(workingDir)
.build();
try {
// Restore a raw EventHandler if it is registered. This allows for blaze run to produce the
// actual output of the command being run even if --color=no is specified.
env.getReporter().switchToAnsiAllowingHandler();
// The command API is a little strange in that the following statement will return normally
// only if the program exits with exit code 0. If it ends with any other code, we have to
// catch BadExitStatusException.
command
.execute(outErr.getOutputStream(), outErr.getErrorStream())
.getTerminationStatus()
.getExitCode();
return BlazeCommandResult.exitCode(ExitCode.SUCCESS);
} catch (BadExitStatusException e) {
String message =
"Non-zero return code '"
+ e.getResult().getTerminationStatus().getExitCode()
+ "' from command: "
+ e.getMessage();
env.getReporter().handle(Event.error(message));
return BlazeCommandResult.exitCode(ExitCode.RUN_FAILURE);
} catch (AbnormalTerminationException e) {
// The process was likely terminated by a signal in this case.
return BlazeCommandResult.exitCode(ExitCode.INTERRUPTED);
} catch (CommandException e) {
env.getReporter().handle(Event.error("Error running program: " + e.getMessage()));
return BlazeCommandResult.exitCode(ExitCode.RUN_FAILURE);
}
}
@Override
public void editOptions(OptionsParser optionsParser) {
Options options = optionsParser.getOptions(Options.class);
try {
if (options.mode == Mode.CLASSIC || options.mode == Mode.CLASSIC_INTERNAL_TEST_DO_NOT_USE) {
String outputGroup =
options.splitApks
? "mobile_install_split" + INTERNAL_SUFFIX
: options.incremental
? "mobile_install_incremental" + INTERNAL_SUFFIX
: "mobile_install_full" + INTERNAL_SUFFIX;
optionsParser.parse(
PriorityCategory.COMMAND_LINE,
"Options required by the mobile-install command",
ImmutableList.of("--output_groups=" + outputGroup));
} else {
optionsParser.parse(
PriorityCategory.COMMAND_LINE,
"Options required by the skylark implementation of mobile-install command",
ImmutableList.of(
"--aspects=" + options.mobileInstallAspect + "%MIASPECT",
"--output_groups=mobile_install" + INTERNAL_SUFFIX,
"--output_groups=mobile_install_launcher" + INTERNAL_SUFFIX));
}
} catch (OptionsParsingException e) {
throw new IllegalStateException(e);
}
}
private boolean isTargetSupported(
CommandEnvironment env, ConfiguredTarget target, List<String> mobileInstallSupportedRules) {
while (target instanceof AliasConfiguredTarget) {
target = ((AliasConfiguredTarget) target).getActual();
}
if (target instanceof AbstractConfiguredTarget) {
String ruleType = ((AbstractConfiguredTarget) target).getRuleClassString();
return isRuleSupported(env, mobileInstallSupportedRules, ruleType);
}
return false;
}
private boolean isRuleSupported(
CommandEnvironment env, List<String> mobileInstallSupportedRules, String ruleType) {
if (!mobileInstallSupportedRules.contains(ruleType)) {
env.getReporter()
.handle(
Event.error(
String.format(
"mobile-install can only be run on %s targets. Got: %s",
mobileInstallSupportedRules, ruleType)));
return false;
} else {
return true;
}
}
}