Unverified Commit 379a2d56 authored by Chris Bracken's avatar Chris Bracken Committed by GitHub

Push /usr/bin to front of PATH for ios-deploy runs (#19281)

ios-deploy relies on LLDB.framework, which relies on /usr/bin/python and
the 'six' module that's installed on the system. However, it appears to
use the first version of Python on PATH, rather than explicitly
specifying the system install.  If a user has a custom install of Python
(e.g., via Homebrew or MacPorts) ahead of the system Python on their
PATH, LLDB.framework will pick up that version instead. If the user
hasn't installed the 'six' module, ios-deploy will fail with a
relatively cryptic error message.

This patch pushes /usr/bin to the front of PATH for the duration of the
ios-deploy run to avoid this scenario.

This patch also removes checks for package six.

Neither Flutter nor any of its direct dependencies/tooling relies on
package six. ios-deploy depends on LLDB.framework (included with Xcode),
which relies on a Python script that imports this package but uses
whichever Python is at the front of the path. Flutter now invokes
ios-deploy with a PATH with /usr/bin forced to the front in order to
avoid this problem.

We could have retained the check out of paranoia, but this seems
unnecessary since it's entirely possible LLDB.framework may one day drop
this dependency, in which case I'd expect the base system install of
Python would likely drop it as well.
parent 2c00e5f2
......@@ -5,6 +5,8 @@
import 'dart:async';
import 'dart:convert';
import 'package:meta/meta.dart';
import '../application_package.dart';
import '../base/file_system.dart';
import '../base/io.dart';
......@@ -26,6 +28,77 @@ const String _kIdeviceinstallerInstructions =
const Duration kPortForwardTimeout = const Duration(seconds: 10);
class IOSDeploy {
const IOSDeploy();
/// Installs and runs the specified app bundle using ios-deploy, then returns
/// the exit code.
Future<int> runApp({
@required String deviceId,
@required String bundlePath,
@required List<String> launchArguments,
}) async {
final List<String> launchCommand = <String>[
'/usr/bin/env',
'ios-deploy',
'--id',
deviceId,
'--bundle',
bundlePath,
'--no-wifi',
'--justlaunch',
];
if (launchArguments.isNotEmpty) {
launchCommand.add('--args');
launchCommand.add('${launchArguments.join(" ")}');
}
// Push /usr/bin to the front of PATH to pick up default system python, package 'six'.
//
// ios-deploy transitively depends on LLDB.framework, which invokes a
// Python script that uses package 'six'. LLDB.framework relies on the
// python at the front of the path, which may not include package 'six'.
// Ensure that we pick up the system install of python, which does include
// it.
final Map<String, String> iosDeployEnv = new Map<String, String>.from(platform.environment);
iosDeployEnv['PATH'] = '/usr/bin:${iosDeployEnv['PATH']}';
return await runCommandAndStreamOutput(
launchCommand,
mapFunction: _monitorInstallationFailure,
trace: true,
environment: iosDeployEnv,
);
}
// Maps stdout line stream. Must return original line.
String _monitorInstallationFailure(String stdout) {
// Installation issues.
if (stdout.contains('Error 0xe8008015') || stdout.contains('Error 0xe8000067')) {
printError(noProvisioningProfileInstruction, emphasis: true);
// Launch issues.
} else if (stdout.contains('e80000e2')) {
printError('''
═══════════════════════════════════════════════════════════════════════════════════
Your device is locked. Unlock your device first before running.
═══════════════════════════════════════════════════════════════════════════════════''',
emphasis: true);
} else if (stdout.contains('Error 0xe8000022')) {
printError('''
═══════════════════════════════════════════════════════════════════════════════════
Error launching app. Try launching from within Xcode via:
open ios/Runner.xcworkspace
Your Xcode version may be too old for your iOS version.
═══════════════════════════════════════════════════════════════════════════════════''',
emphasis: true);
}
return stdout;
}
}
class IOSDevices extends PollingDeviceDiscovery {
IOSDevices() : super('iOS devices');
......@@ -213,34 +286,18 @@ class IOSDevice extends Device {
if (platformArgs['trace-startup'] ?? false)
launchArguments.add('--trace-startup');
final List<String> launchCommand = <String>[
'/usr/bin/env',
'ios-deploy',
'--id',
id,
'--bundle',
bundle.path,
'--no-wifi',
'--justlaunch',
];
if (launchArguments.isNotEmpty) {
launchCommand.add('--args');
launchCommand.add('${launchArguments.join(" ")}');
}
int installationResult = -1;
Uri localObservatoryUri;
final Status installStatus =
logger.startProgress('Installing and launching...', expectSlowOperation: true);
final Status installStatus = logger.startProgress('Installing and launching...', expectSlowOperation: true);
if (!debuggingOptions.debuggingEnabled) {
// If debugging is not enabled, just launch the application and continue.
printTrace('Debugging is not enabled');
installationResult = await runCommandAndStreamOutput(
launchCommand,
mapFunction: monitorInstallationFailure,
trace: true,
installationResult = await const IOSDeploy().runApp(
deviceId: id,
bundlePath: bundle.path,
launchArguments: launchArguments,
);
installStatus.stop();
} else {
......@@ -258,10 +315,10 @@ class IOSDevice extends Device {
final Future<Uri> forwardObservatoryUri = observatoryDiscovery.uri;
final Future<int> launch = runCommandAndStreamOutput(
launchCommand,
mapFunction: monitorInstallationFailure,
trace: true,
final Future<int> launch = const IOSDeploy().runApp(
deviceId: id,
bundlePath: bundle.path,
launchArguments: launchArguments,
);
localObservatoryUri = await launch.then<Uri>((int result) async {
......@@ -321,33 +378,6 @@ class IOSDevice extends Device {
@override
Future<Null> takeScreenshot(File outputFile) => iMobileDevice.takeScreenshot(outputFile);
// Maps stdout line stream. Must return original line.
String monitorInstallationFailure(String stdout) {
// Installation issues.
if (stdout.contains('Error 0xe8008015') || stdout.contains('Error 0xe8000067')) {
printError(noProvisioningProfileInstruction, emphasis: true);
// Launch issues.
} else if (stdout.contains('e80000e2')) {
printError('''
═══════════════════════════════════════════════════════════════════════════════════
Your device is locked. Unlock your device first before running.
═══════════════════════════════════════════════════════════════════════════════════''',
emphasis: true);
} else if (stdout.contains('Error 0xe8000022')) {
printError('''
═══════════════════════════════════════════════════════════════════════════════════
Error launching app. Try launching from within Xcode via:
open ios/Runner.xcworkspace
Your Xcode version may be too old for your iOS version.
═══════════════════════════════════════════════════════════════════════════════════''',
emphasis: true);
}
return stdout;
}
}
/// Decodes an encoded syslog string to a UTF-8 representation.
......
......@@ -48,8 +48,6 @@ class IOSWorkflow extends DoctorValidator implements Workflow {
bool get hasHomebrew => os.which('brew') != null;
bool get hasPythonSixModule => kPythonSix.isInstalled;
Future<String> get macDevMode async => (await runAsync(<String>['DevToolsSecurity', '-status'])).processResult.stdout;
Future<bool> get _iosDeployIsInstalledAndMeetsVersionCheck async {
......@@ -67,7 +65,6 @@ class IOSWorkflow extends DoctorValidator implements Workflow {
Future<ValidationResult> validate() async {
final List<ValidationMessage> messages = <ValidationMessage>[];
ValidationType xcodeStatus = ValidationType.missing;
ValidationType pythonStatus = ValidationType.missing;
ValidationType brewStatus = ValidationType.missing;
String xcodeVersionInfo;
......@@ -121,14 +118,6 @@ class IOSWorkflow extends DoctorValidator implements Workflow {
}
}
// Python dependencies installed
if (hasPythonSixModule) {
pythonStatus = ValidationType.installed;
} else {
pythonStatus = ValidationType.missing;
messages.add(new ValidationMessage.error(kPythonSix.errorMessage));
}
// brew installed
if (hasHomebrew) {
brewStatus = ValidationType.installed;
......@@ -221,7 +210,7 @@ class IOSWorkflow extends DoctorValidator implements Workflow {
}
return new ValidationResult(
<ValidationType>[xcodeStatus, pythonStatus, brewStatus].reduce(_mergeValidationTypes),
<ValidationType>[xcodeStatus, brewStatus].reduce(_mergeValidationTypes),
messages,
statusInfo: xcodeVersionInfo
);
......
......@@ -31,27 +31,10 @@ import 'xcodeproj.dart';
const int kXcodeRequiredVersionMajor = 9;
const int kXcodeRequiredVersionMinor = 0;
// The Python `six` module is a dependency for Xcode builds, and installed by
// default, but may not be present in custom Python installs; e.g., via
// Homebrew.
const PythonModule kPythonSix = const PythonModule('six');
IMobileDevice get iMobileDevice => context[IMobileDevice];
Xcode get xcode => context[Xcode];
class PythonModule {
const PythonModule(this.name);
final String name;
bool get isInstalled => exitsHappy(<String>['python', '-c', 'import $name']);
String get errorMessage =>
'Missing Xcode dependency: Python module "$name".\n'
'Install via \'pip install $name\' or \'sudo easy_install $name\'.';
}
class IMobileDevice {
const IMobileDevice();
......@@ -200,11 +183,6 @@ Future<XcodeBuildResult> buildXcodeProject({
if (!_checkXcodeVersion())
return new XcodeBuildResult(success: false);
if (!kPythonSix.isInstalled) {
printError(kPythonSix.errorMessage);
return new XcodeBuildResult(success: false);
}
final XcodeProjectInfo projectInfo = xcodeProjectInterpreter.getInfo(app.appDirectory);
if (!projectInfo.targets.contains('Runner')) {
printError('The Xcode project does not define target "Runner" which is needed by Flutter tooling.');
......
......@@ -43,7 +43,6 @@ void main() {
when(xcode.isInstalled).thenReturn(false);
when(xcode.xcodeSelectPath).thenReturn(null);
final IOSWorkflowTestTarget workflow = new IOSWorkflowTestTarget(
hasPythonSixModule: false,
hasHomebrew: false,
hasIosDeploy: false,
);
......@@ -111,22 +110,6 @@ void main() {
CocoaPods: () => cocoaPods,
});
testUsingContext('Emits partial status when python six not installed', () async {
when(xcode.isInstalled).thenReturn(true);
when(xcode.versionText)
.thenReturn('Xcode 8.2.1\nBuild version 8C1002\n');
when(xcode.isInstalledAndMeetsVersionCheck).thenReturn(true);
when(xcode.eulaSigned).thenReturn(true);
when(xcode.isSimctlInstalled).thenReturn(true);
final IOSWorkflowTestTarget workflow = new IOSWorkflowTestTarget(hasPythonSixModule: false);
final ValidationResult result = await workflow.validate();
expect(result.type, ValidationType.partial);
}, overrides: <Type, Generator>{
IMobileDevice: () => iMobileDevice,
Xcode: () => xcode,
CocoaPods: () => cocoaPods,
});
testUsingContext('Emits partial status when homebrew not installed', () async {
when(xcode.isInstalled).thenReturn(true);
when(xcode.versionText)
......@@ -327,7 +310,6 @@ class MockCocoaPods extends Mock implements CocoaPods {}
class IOSWorkflowTestTarget extends IOSWorkflow {
IOSWorkflowTestTarget({
this.hasPythonSixModule = true,
this.hasHomebrew = true,
bool hasIosDeploy = true,
String iosDeployVersionText = '1.9.2',
......@@ -336,9 +318,6 @@ class IOSWorkflowTestTarget extends IOSWorkflow {
iosDeployVersionText = new Future<String>.value(iosDeployVersionText),
hasIDeviceInstaller = new Future<bool>.value(hasIDeviceInstaller);
@override
final bool hasPythonSixModule;
@override
final bool hasHomebrew;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment