/*
* Copyright 2012-present Facebook, Inc.
*
* 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.facebook.buck.cli;
import com.facebook.buck.android.AdbHelper;
import com.facebook.buck.android.HasInstallableApk;
import com.facebook.buck.apple.AppleBundle;
import com.facebook.buck.apple.AppleBundleDescription;
import com.facebook.buck.apple.AppleConfig;
import com.facebook.buck.apple.AppleInfoPlistParsing;
import com.facebook.buck.apple.ApplePlatform;
import com.facebook.buck.apple.device.AppleDeviceHelper;
import com.facebook.buck.apple.simulator.AppleCoreSimulatorServiceController;
import com.facebook.buck.apple.simulator.AppleSimulator;
import com.facebook.buck.apple.simulator.AppleSimulatorController;
import com.facebook.buck.apple.simulator.AppleSimulatorDiscovery;
import com.facebook.buck.cli.UninstallCommand.UninstallOptions;
import com.facebook.buck.command.Build;
import com.facebook.buck.event.ConsoleEvent;
import com.facebook.buck.event.InstallEvent;
import com.facebook.buck.io.ProjectFilesystem;
import com.facebook.buck.json.BuildFileParseException;
import com.facebook.buck.log.Logger;
import com.facebook.buck.model.BuildTarget;
import com.facebook.buck.model.BuildTargetException;
import com.facebook.buck.model.Flavor;
import com.facebook.buck.parser.NoSuchBuildTargetException;
import com.facebook.buck.parser.ParserConfig;
import com.facebook.buck.parser.SpeculativeParsing;
import com.facebook.buck.parser.TargetNodeSpec;
import com.facebook.buck.rules.BuildRule;
import com.facebook.buck.rules.BuildRuleResolver;
import com.facebook.buck.rules.Description;
import com.facebook.buck.rules.SourcePath;
import com.facebook.buck.rules.SourcePathResolver;
import com.facebook.buck.rules.SourcePathRuleFinder;
import com.facebook.buck.rules.TargetNode;
import com.facebook.buck.step.AdbOptions;
import com.facebook.buck.step.ExecutionContext;
import com.facebook.buck.step.TargetDeviceOptions;
import com.facebook.buck.util.HumanReadableException;
import com.facebook.buck.util.MoreExceptions;
import com.facebook.buck.util.Optionals;
import com.facebook.buck.util.ProcessExecutor;
import com.facebook.buck.util.UnixUserIdFetcher;
import com.facebook.infer.annotation.Assertions;
import com.facebook.infer.annotation.SuppressFieldNotInitialized;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Functions;
import com.google.common.base.Preconditions;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.util.concurrent.ListeningExecutorService;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import javax.annotation.Nullable;
import org.kohsuke.args4j.Option;
/** Command so a user can build and install an APK. */
public class InstallCommand extends BuildCommand {
private static final Logger LOG = Logger.get(InstallCommand.class);
private static final long APPLE_SIMULATOR_WAIT_MILLIS = 20000;
private static final ImmutableList<String> APPLE_SIMULATOR_APPS =
ImmutableList.of("Simulator.app", "iOS Simulator.app");
private static final String DEFAULT_APPLE_SIMULATOR_NAME = "iPhone 5s";
private static final InstallResult FAILURE = InstallResult.builder().setExitCode(1).build();
@VisibleForTesting static final String RUN_LONG_ARG = "--run";
@VisibleForTesting static final String RUN_SHORT_ARG = "-r";
@VisibleForTesting static final String WAIT_FOR_DEBUGGER_LONG_ARG = "--wait-for-debugger";
@VisibleForTesting static final String WAIT_FOR_DEBUGGER_SHORT_ARG = "-w";
@VisibleForTesting static final String INSTALL_VIA_SD_LONG_ARG = "--via-sd";
@VisibleForTesting static final String INSTALL_VIA_SD_SHORT_ARG = "-S";
@VisibleForTesting static final String ACTIVITY_LONG_ARG = "--activity";
@VisibleForTesting static final String ACTIVITY_SHORT_ARG = "-a";
@VisibleForTesting static final String UNINSTALL_LONG_ARG = "--uninstall";
@VisibleForTesting static final String UNINSTALL_SHORT_ARG = "-u";
@Option(
name = UNINSTALL_LONG_ARG,
aliases = {UNINSTALL_SHORT_ARG},
usage = "Uninstall the existing version before installing."
)
private boolean uninstallFirst = false;
@AdditionalOptions @SuppressFieldNotInitialized private UninstallOptions uninstallOptions;
@AdditionalOptions @SuppressFieldNotInitialized private AdbCommandLineOptions adbOptions;
@AdditionalOptions @SuppressFieldNotInitialized
private TargetDeviceCommandLineOptions deviceOptions;
@Option(
name = "--",
usage = "Arguments passed when running with -r. Only valid for Apple targets.",
handler = ConsumeAllOptionsHandler.class,
depends = "-r"
)
private List<String> runArgs = new ArrayList<>();
@Option(
name = RUN_LONG_ARG,
aliases = {RUN_SHORT_ARG},
usage = "Run an activity (the default activity for package unless -a is specified)."
)
private boolean run = false;
@Option(
name = WAIT_FOR_DEBUGGER_LONG_ARG,
aliases = {WAIT_FOR_DEBUGGER_SHORT_ARG},
usage = "Have the launched process wait for the debugger"
)
private boolean waitForDebugger = false;
@Option(
name = INSTALL_VIA_SD_LONG_ARG,
aliases = {INSTALL_VIA_SD_SHORT_ARG},
usage = "Copy package to external storage (SD) instead of /data/local/tmp before installing."
)
private boolean installViaSd = false;
@Option(
name = ACTIVITY_LONG_ARG,
aliases = {ACTIVITY_SHORT_ARG},
metaVar = "<pkg/activity>",
usage = "Activity to launch e.g. com.facebook.katana/.LoginActivity. Implies -r."
)
@Nullable
private String activity = null;
public AdbOptions adbOptions(BuckConfig buckConfig) {
return adbOptions.getAdbOptions(buckConfig);
}
public TargetDeviceOptions targetDeviceOptions() {
return deviceOptions.getTargetDeviceOptions();
}
public UninstallOptions uninstallOptions() {
return uninstallOptions;
}
public boolean shouldUninstallFirst() {
return uninstallFirst;
}
public boolean shouldStartActivity() {
return (activity != null) || run;
}
public boolean shouldInstallViaSd() {
return installViaSd;
}
@Nullable
public String getActivityToStart() {
return activity;
}
@Override
public int runWithoutHelp(CommandRunnerParams params) throws IOException, InterruptedException {
int exitCode = checkArguments(params);
if (exitCode != 0) {
return exitCode;
}
try (CommandThreadManager pool =
new CommandThreadManager("Install", getConcurrencyLimit(params.getBuckConfig()))) {
// Get the helper targets if present
ImmutableSet<String> installHelperTargets;
try {
installHelperTargets = getInstallHelperTargets(params, pool.getExecutor());
} catch (BuildTargetException | BuildFileParseException e) {
params
.getBuckEventBus()
.post(ConsoleEvent.severe(MoreExceptions.getHumanReadableOrLocalizedMessage(e)));
return 1;
}
// Build the targets
exitCode = super.run(params, pool.getExecutor(), installHelperTargets);
if (exitCode != 0) {
return exitCode;
}
}
// Install the targets
try {
exitCode = install(params);
} catch (NoSuchBuildTargetException e) {
throw new HumanReadableException(e.getHumanReadableErrorMessage());
}
if (exitCode != 0) {
return exitCode;
}
return exitCode;
}
private int install(CommandRunnerParams params)
throws IOException, InterruptedException, NoSuchBuildTargetException {
Build build = super.getBuild();
int exitCode = 0;
for (BuildTarget buildTarget : getBuildTargets()) {
BuildRule buildRule = build.getRuleResolver().requireRule(buildTarget);
SourcePathResolver pathResolver =
new SourcePathResolver(new SourcePathRuleFinder(build.getRuleResolver()));
if (buildRule instanceof HasInstallableApk) {
ExecutionContext executionContext =
ExecutionContext.builder()
.from(build.getExecutionContext())
.setAdbOptions(Optional.of(adbOptions(params.getBuckConfig())))
.setTargetDeviceOptions(Optional.of(targetDeviceOptions()))
.setExecutors(params.getExecutors())
.setCellPathResolver(params.getCell().getCellPathResolver())
.build();
exitCode =
installApk(params, (HasInstallableApk) buildRule, executionContext, pathResolver);
if (exitCode != 0) {
return exitCode;
}
} else if (buildRule instanceof AppleBundle) {
AppleBundle appleBundle = (AppleBundle) buildRule;
InstallEvent.Started started = InstallEvent.started(appleBundle.getBuildTarget());
params.getBuckEventBus().post(started);
InstallResult installResult =
installAppleBundle(
params,
appleBundle,
appleBundle.getProjectFilesystem(),
build.getExecutionContext().getProcessExecutor(),
pathResolver);
params
.getBuckEventBus()
.post(
InstallEvent.finished(
started,
installResult.getExitCode() == 0,
installResult.getLaunchedPid(),
Optional.empty()));
exitCode = installResult.getExitCode();
if (exitCode != 0) {
return exitCode;
}
} else {
params
.getBuckEventBus()
.post(
ConsoleEvent.severe(
String.format(
"Specified rule %s must be of type android_binary() or apk_genrule() or "
+ "apple_bundle() but was %s().\n",
buildRule.getFullyQualifiedName(), buildRule.getType())));
return 1;
}
}
return exitCode;
}
private ImmutableSet<String> getInstallHelperTargets(
CommandRunnerParams params, ListeningExecutorService executor)
throws IOException, InterruptedException, BuildTargetException, BuildFileParseException {
ParserConfig parserConfig = params.getBuckConfig().getView(ParserConfig.class);
ImmutableSet.Builder<String> installHelperTargets = ImmutableSet.builder();
for (int index = 0; index < getArguments().size(); index++) {
// TODO(markwang): Cache argument parsing
TargetNodeSpec spec =
parseArgumentsAsTargetNodeSpecs(params.getBuckConfig(), getArguments()).get(index);
BuildTarget target =
FluentIterable.from(
params
.getParser()
.resolveTargetSpecs(
params.getBuckEventBus(),
params.getCell(),
getEnableParserProfiling(),
executor,
ImmutableList.of(spec),
SpeculativeParsing.of(false),
parserConfig.getDefaultFlavorsMode()))
.transformAndConcat(Functions.identity())
.first()
.get();
TargetNode<?, ?> node =
params
.getParser()
.getTargetNode(
params.getBuckEventBus(),
params.getCell(),
getEnableParserProfiling(),
executor,
target);
if (node != null
&& Description.getBuildRuleType(node.getDescription())
.equals(Description.getBuildRuleType(AppleBundleDescription.class))) {
for (Flavor flavor : node.getBuildTarget().getFlavors()) {
if (ApplePlatform.needsInstallHelper(flavor.getName())) {
AppleConfig appleConfig = params.getBuckConfig().getView(AppleConfig.class);
Optional<BuildTarget> deviceHelperTarget = appleConfig.getAppleDeviceHelperTarget();
Optionals.addIfPresent(
Optionals.bind(
deviceHelperTarget,
input ->
!input.toString().isEmpty()
? Optional.of(input.toString())
: Optional.empty()),
installHelperTargets);
}
}
}
}
return installHelperTargets.build();
}
private int installApk(
CommandRunnerParams params,
HasInstallableApk hasInstallableApk,
ExecutionContext executionContext,
SourcePathResolver pathResolver)
throws IOException, InterruptedException {
final AdbHelper adbHelper =
AdbHelper.get(executionContext, params.getBuckConfig().getRestartAdbOnFailure());
// Uninstall the app first, if requested.
if (shouldUninstallFirst()) {
String packageName =
AdbHelper.tryToExtractPackageNameFromManifest(
pathResolver, hasInstallableApk.getApkInfo());
adbHelper.uninstallApp(packageName, uninstallOptions().shouldKeepUserData());
// Perhaps the app wasn't installed to begin with, shouldn't stop us.
}
if (!adbHelper.installApk(pathResolver, hasInstallableApk, shouldInstallViaSd(), false)) {
return 1;
}
// We've installed the application successfully.
// Is either of --activity or --run present?
if (shouldStartActivity()) {
int exitCode =
adbHelper.startActivity(
pathResolver, hasInstallableApk, getActivityToStart(), waitForDebugger);
if (exitCode != 0) {
return exitCode;
}
}
return 0;
}
private InstallResult installAppleBundle(
CommandRunnerParams params,
AppleBundle appleBundle,
ProjectFilesystem projectFilesystem,
ProcessExecutor processExecutor,
SourcePathResolver pathResolver)
throws IOException, InterruptedException, NoSuchBuildTargetException {
if (appleBundle.getPlatformName().equals(ApplePlatform.IPHONESIMULATOR.getName())) {
return installAppleBundleForSimulator(
params, appleBundle, pathResolver, projectFilesystem, processExecutor);
}
if (appleBundle.getPlatformName().equals(ApplePlatform.IPHONEOS.getName())) {
return installAppleBundleForDevice(
params, appleBundle, projectFilesystem, processExecutor, pathResolver);
}
params
.getConsole()
.printBuildFailure(
"Install not yet supported for platform " + appleBundle.getPlatformName() + ".");
return FAILURE;
}
private InstallResult installAppleBundleForDevice(
CommandRunnerParams params,
AppleBundle appleBundle,
ProjectFilesystem projectFilesystem,
ProcessExecutor processExecutor,
SourcePathResolver pathResolver)
throws IOException, NoSuchBuildTargetException {
AppleConfig appleConfig = params.getBuckConfig().getView(AppleConfig.class);
final Path helperPath;
Optional<BuildTarget> helperTarget = appleConfig.getAppleDeviceHelperTarget();
if (helperTarget.isPresent()) {
BuildRuleResolver resolver = super.getBuild().getRuleResolver();
BuildRule buildRule = resolver.requireRule(helperTarget.get());
if (buildRule == null) {
params
.getConsole()
.printBuildFailure(
String.format(
"Cannot install %s (could not resolve build rule for device helper target %s)",
appleBundle.getFullyQualifiedName(), helperTarget.get().getBaseName()));
return FAILURE;
}
SourcePath buildRuleOutputPath = buildRule.getSourcePathToOutput();
if (buildRuleOutputPath == null) {
params
.getConsole()
.printBuildFailure(
String.format(
"Cannot install %s (device helper target %s does not specify an output)",
appleBundle.getFullyQualifiedName(), helperTarget.get().getBaseName()));
return FAILURE;
}
helperPath = pathResolver.getAbsolutePath(buildRuleOutputPath);
} else {
Optional<Path> helperOverridePath = appleConfig.getAppleDeviceHelperAbsolutePath();
if (helperOverridePath.isPresent()) {
helperPath = helperOverridePath.get();
} else {
params
.getConsole()
.printBuildFailure(
String.format(
"Cannot install %s (could not find path to device install helper tool)",
appleBundle.getFullyQualifiedName()));
return FAILURE;
}
}
AppleDeviceHelper helper = new AppleDeviceHelper(processExecutor, helperPath);
ImmutableMap<String, String> connectedDevices = helper.getConnectedDevices();
if (connectedDevices.size() == 0) {
params
.getConsole()
.printBuildFailure(
String.format(
"Cannot install %s (no connected devices found)",
appleBundle.getFullyQualifiedName()));
return FAILURE;
}
String selectedUdid = null;
if (targetDeviceOptions().getSerialNumber().isPresent()) {
String udidPrefix =
Assertions.assertNotNull(targetDeviceOptions().getSerialNumber().get()).toLowerCase();
for (String udid : connectedDevices.keySet()) {
if (udid.startsWith(udidPrefix)) {
selectedUdid = udid;
break;
}
}
if (selectedUdid == null) {
params
.getConsole()
.printBuildFailure(
String.format(
"Cannot install %s to the device %s (no connected devices with that UDID/prefix)",
appleBundle.getFullyQualifiedName(), udidPrefix));
return FAILURE;
}
} else {
if (connectedDevices.size() > 1) {
LOG.warn(
"More than one connected device found, and no device ID specified. A device will be"
+ " arbitrarily picked.");
}
selectedUdid = connectedDevices.keySet().iterator().next();
}
LOG.info(
"Installing "
+ appleBundle.getFullyQualifiedName()
+ " to device "
+ selectedUdid
+ " ("
+ connectedDevices.get(selectedUdid)
+ ")");
if (helper.installBundleOnDevice(
selectedUdid,
pathResolver.getAbsolutePath(
Preconditions.checkNotNull(appleBundle.getSourcePathToOutput())))) {
params
.getConsole()
.printSuccess(
"Installed "
+ appleBundle.getFullyQualifiedName()
+ " to device "
+ selectedUdid
+ " ("
+ connectedDevices.get(selectedUdid)
+ ")");
if (run) {
Optional<String> appleBundleId;
try (InputStream bundlePlistStream =
projectFilesystem.getInputStreamForRelativePath(appleBundle.getInfoPlistPath())) {
appleBundleId = AppleInfoPlistParsing.getBundleIdFromPlistStream(bundlePlistStream);
}
if (!appleBundleId.isPresent()) {
params
.getConsole()
.printBuildFailure(
String.format(
"Cannot run %s (could not get bundle ID from %s)",
appleBundle.getFullyQualifiedName(), appleBundle.getInfoPlistPath()));
return FAILURE;
}
if (waitForDebugger) {
LOG.warn(WAIT_FOR_DEBUGGER_LONG_ARG + " not yet implemented for devices.");
}
if (helper.runBundleOnDevice(selectedUdid, appleBundleId.get())) {
return InstallResult.builder().setExitCode(0).build();
} else {
params
.getConsole()
.printBuildFailure(
"Failed to run "
+ appleBundle.getFullyQualifiedName()
+ " on device "
+ selectedUdid
+ " ("
+ connectedDevices.get(selectedUdid)
+ ")");
return FAILURE;
}
} else {
return InstallResult.builder().setExitCode(0).build();
}
} else {
params
.getConsole()
.printBuildFailure(
"Failed to install "
+ appleBundle.getFullyQualifiedName()
+ " to device "
+ selectedUdid
+ " ("
+ connectedDevices.get(selectedUdid)
+ ")");
return FAILURE;
}
}
private InstallResult installAppleBundleForSimulator(
CommandRunnerParams params,
AppleBundle appleBundle,
SourcePathResolver pathResolver,
ProjectFilesystem projectFilesystem,
ProcessExecutor processExecutor)
throws IOException, InterruptedException {
AppleConfig appleConfig = params.getBuckConfig().getView(AppleConfig.class);
Optional<Path> xcodeDeveloperPath =
appleConfig.getAppleDeveloperDirectorySupplier(processExecutor).get();
if (!xcodeDeveloperPath.isPresent()) {
params
.getConsole()
.printBuildFailure(
String.format(
"Cannot install %s (Xcode not found)", appleBundle.getFullyQualifiedName()));
return FAILURE;
}
UnixUserIdFetcher userIdFetcher = new UnixUserIdFetcher();
AppleCoreSimulatorServiceController appleCoreSimulatorServiceController =
new AppleCoreSimulatorServiceController(processExecutor);
Optional<Path> coreSimulatorServicePath =
appleCoreSimulatorServiceController.getCoreSimulatorServicePath(userIdFetcher);
boolean shouldWaitForSimulatorsToShutdown = false;
if (!coreSimulatorServicePath.isPresent()
|| !coreSimulatorServicePath
.get()
.toRealPath()
.startsWith(xcodeDeveloperPath.get().toRealPath())) {
LOG.warn(
"Core simulator service path %s does not match developer directory %s, "
+ "killing all simulators.",
coreSimulatorServicePath, xcodeDeveloperPath.get());
if (!appleCoreSimulatorServiceController.killSimulatorProcesses()) {
params.getConsole().printBuildFailure("Could not kill running simulator processes.");
return FAILURE;
}
shouldWaitForSimulatorsToShutdown = true;
}
Path simctlPath = xcodeDeveloperPath.get().resolve("usr/bin/simctl");
Optional<AppleSimulator> appleSimulator =
getAppleSimulatorForBundle(appleBundle, processExecutor, simctlPath);
if (!appleSimulator.isPresent()) {
params
.getConsole()
.printBuildFailure(
String.format(
"Cannot install %s (no appropriate simulator found)",
appleBundle.getFullyQualifiedName()));
return FAILURE;
}
Path iosSimulatorPath = null;
Path xcodeApplicationsPath = xcodeDeveloperPath.get().resolve("Applications");
for (String simulatorApp : APPLE_SIMULATOR_APPS) {
Path resolvedSimulatorPath = xcodeApplicationsPath.resolve(simulatorApp);
if (projectFilesystem.isDirectory(resolvedSimulatorPath)) {
iosSimulatorPath = resolvedSimulatorPath;
break;
}
}
if (iosSimulatorPath == null) {
params
.getConsole()
.printBuildFailure(
String.format(
"Cannot install %s (could not find simulator under %s, checked %s)",
appleBundle.getFullyQualifiedName(),
xcodeApplicationsPath,
APPLE_SIMULATOR_APPS));
return FAILURE;
}
AppleSimulatorController appleSimulatorController =
new AppleSimulatorController(processExecutor, simctlPath, iosSimulatorPath);
if (!appleSimulatorController.canStartSimulator(appleSimulator.get().getUdid())) {
LOG.warn("Cannot start simulator %s, killing simulators and trying again.");
if (!appleCoreSimulatorServiceController.killSimulatorProcesses()) {
params.getConsole().printBuildFailure("Could not kill running simulator processes.");
return FAILURE;
}
shouldWaitForSimulatorsToShutdown = true;
// Killing the simulator can cause the UDIDs to change, so we need to fetch them again.
appleSimulator = getAppleSimulatorForBundle(appleBundle, processExecutor, simctlPath);
if (!appleSimulator.isPresent()) {
params
.getConsole()
.printBuildFailure(
String.format(
"Cannot install %s (no appropriate simulator found)",
appleBundle.getFullyQualifiedName()));
return FAILURE;
}
}
long remainingMillis = APPLE_SIMULATOR_WAIT_MILLIS;
if (shouldWaitForSimulatorsToShutdown) {
Optional<Long> shutdownMillis =
appleSimulatorController.waitForSimulatorsToShutdown(remainingMillis);
if (!shutdownMillis.isPresent()) {
params
.getConsole()
.printBuildFailure(
String.format(
"Cannot install %s (simulators did not shut down within %d ms).",
appleBundle.getFullyQualifiedName(), APPLE_SIMULATOR_WAIT_MILLIS));
return FAILURE;
}
LOG.debug("Simulators shut down in %d millis.", shutdownMillis.get());
remainingMillis -= shutdownMillis.get();
}
LOG.debug("Starting up simulator %s", appleSimulator.get());
Optional<Long> startMillis =
appleSimulatorController.startSimulator(appleSimulator.get().getUdid(), remainingMillis);
if (!startMillis.isPresent()) {
params
.getConsole()
.printBuildFailure(
String.format(
"Cannot install %s (could not start simulator %s within %d ms)",
appleBundle.getFullyQualifiedName(),
appleSimulator.get().getName(),
APPLE_SIMULATOR_WAIT_MILLIS));
return FAILURE;
}
LOG.debug(
"Simulator started in %d ms. Installing Apple bundle %s in simulator %s",
startMillis.get(), appleBundle, appleSimulator.get());
if (!appleSimulatorController.installBundleInSimulator(
appleSimulator.get().getUdid(),
pathResolver.getAbsolutePath(
Preconditions.checkNotNull(appleBundle.getSourcePathToOutput())))) {
params
.getConsole()
.printBuildFailure(
String.format(
"Cannot install %s (could not install bundle %s in simulator %s)",
appleBundle.getFullyQualifiedName(),
pathResolver.getAbsolutePath(appleBundle.getSourcePathToOutput()),
appleSimulator.get().getName()));
return FAILURE;
}
if (run) {
return launchAppleBundle(
params, appleBundle, appleSimulatorController, projectFilesystem, appleSimulator.get());
} else {
params
.getBuckEventBus()
.post(
ConsoleEvent.info(
params
.getConsole()
.getAnsi()
.asHighlightedSuccessText(
"Successfully installed %s. (Use `buck install -r %s` to run.)"),
getArguments().get(0),
getArguments().get(0)));
return InstallResult.builder().setExitCode(0).build();
}
}
private InstallResult launchAppleBundle(
CommandRunnerParams params,
AppleBundle appleBundle,
AppleSimulatorController appleSimulatorController,
ProjectFilesystem projectFilesystem,
AppleSimulator appleSimulator)
throws IOException, InterruptedException {
LOG.debug("Launching Apple bundle %s in simulator %s", appleBundle, appleSimulator);
Optional<String> appleBundleId;
try (InputStream bundlePlistStream =
projectFilesystem.getInputStreamForRelativePath(appleBundle.getInfoPlistPath())) {
appleBundleId = AppleInfoPlistParsing.getBundleIdFromPlistStream(bundlePlistStream);
}
if (!appleBundleId.isPresent()) {
params
.getConsole()
.printBuildFailure(
String.format(
"Cannot install %s (could not get bundle ID from %s)",
appleBundle.getFullyQualifiedName(), appleBundle.getInfoPlistPath()));
return FAILURE;
}
Optional<Long> launchedPid =
appleSimulatorController.launchInstalledBundleInSimulator(
appleSimulator.getUdid(),
appleBundleId.get(),
waitForDebugger
? AppleSimulatorController.LaunchBehavior.WAIT_FOR_DEBUGGER
: AppleSimulatorController.LaunchBehavior.DO_NOT_WAIT_FOR_DEBUGGER,
runArgs);
if (!launchedPid.isPresent()) {
params
.getConsole()
.printBuildFailure(
String.format(
"Cannot launch %s (failed to launch bundle ID %s)",
appleBundle.getFullyQualifiedName(), appleBundleId.get()));
return FAILURE;
}
params
.getBuckEventBus()
.post(
ConsoleEvent.info(
params
.getConsole()
.getAnsi()
.asHighlightedSuccessText(
"Successfully launched %s%s. To debug, run: lldb -p %d"),
getArguments().get(0),
waitForDebugger ? " (waiting for debugger)" : "",
launchedPid.get()));
return InstallResult.builder().setExitCode(0).setLaunchedPid(launchedPid.get()).build();
}
private Optional<AppleSimulator> getAppleSimulatorForBundle(
AppleBundle appleBundle, ProcessExecutor processExecutor, Path simctlPath)
throws IOException, InterruptedException {
LOG.debug("Choosing simulator for %s", appleBundle);
Optional<AppleSimulator> simulatorByUdid = Optional.empty();
Optional<AppleSimulator> simulatorByName = Optional.empty();
Optional<AppleSimulator> defaultSimulator = Optional.empty();
boolean wantUdid = deviceOptions.getSerialNumber().isPresent();
boolean wantName = deviceOptions.getSimulatorName().isPresent();
for (AppleSimulator simulator :
AppleSimulatorDiscovery.discoverAppleSimulators(processExecutor, simctlPath)) {
if (wantUdid
&& deviceOptions
.getSerialNumber()
.get()
.toLowerCase(Locale.US)
.equals(simulator.getUdid().toLowerCase(Locale.US))) {
LOG.debug("Got UDID match (%s): %s", deviceOptions.getSerialNumber().get(), simulator);
simulatorByUdid = Optional.of(simulator);
// We shouldn't need to keep looking.
break;
} else if (wantName
&& deviceOptions
.getSimulatorName()
.get()
.toLowerCase(Locale.US)
.equals(simulator.getName().toLowerCase(Locale.US))) {
LOG.debug("Got name match (%s): %s", simulator.getName(), simulator);
simulatorByName = Optional.of(simulator);
// We assume the simulators are sorted by OS version, so we'll keep
// looking for a more recent simulator with this name.
} else if (simulator.getName().equals(DEFAULT_APPLE_SIMULATOR_NAME)) {
LOG.debug("Got default match (%s): %s", DEFAULT_APPLE_SIMULATOR_NAME, simulator);
defaultSimulator = Optional.of(simulator);
}
}
if (wantUdid) {
if (simulatorByUdid.isPresent()) {
return simulatorByUdid;
} else {
LOG.warn(
"Asked to find simulator with UDID %s, but couldn't find one.",
deviceOptions.getSerialNumber().get());
return Optional.empty();
}
} else if (wantName) {
if (simulatorByName.isPresent()) {
return simulatorByName;
} else {
LOG.warn(
"Asked to find simulator with name %s, but couldn't find one.",
deviceOptions.getSimulatorName().get());
return Optional.empty();
}
} else {
return defaultSimulator;
}
}
@Override
public String getShortDescription() {
return "builds and installs an application";
}
@Override
public boolean isReadOnly() {
return false;
}
}