From 102ab1e6d989de57019e0b1ee003ccb311ca3095 Mon Sep 17 00:00:00 2001 From: Christopher Fujino Date: Mon, 15 Jul 2019 09:22:29 -0700 Subject: [PATCH] Reland bundle ios deps (#36093) This updates the flutter tool cache to download binary files for ideviceinstaller, ios-deploy, libimobiledevice, and dynamically linked dependencies from Flutter's GCP bucket. --- .cirrus.yml | 4 +- bin/internal/ideviceinstaller.version | 1 + bin/internal/ios-deploy.version | 1 + bin/internal/libimobiledevice.version | 1 + bin/internal/libplist.version | 1 + bin/internal/openssl.version | 1 + bin/internal/usbmuxd.version | 1 + dev/devicelab/lib/framework/adb.dart | 27 ++- packages/flutter_tools/lib/src/artifacts.dart | 82 ++++++-- .../flutter_tools/lib/src/base/process.dart | 14 +- .../lib/src/base/user_messages.dart | 41 ---- packages/flutter_tools/lib/src/cache.dart | 57 +++++- .../flutter_tools/lib/src/context_runner.dart | 3 +- packages/flutter_tools/lib/src/doctor.dart | 2 - .../flutter_tools/lib/src/ios/devices.dart | 64 ++++--- .../lib/src/ios/ios_workflow.dart | 91 --------- packages/flutter_tools/lib/src/ios/mac.dart | 127 +++++++++++-- .../lib/src/macos/cocoapods.dart | 4 +- .../lib/src/macos/cocoapods_validator.dart | 10 +- .../test/general.shard/cache_test.dart | 16 ++ .../general.shard/ios/code_signing_test.dart | 2 +- .../general.shard/ios/ios_workflow_test.dart | 178 ------------------ .../test/general.shard/ios/mac_test.dart | 73 +++++-- .../macos/cocoapods_validator_test.dart | 27 +-- 24 files changed, 397 insertions(+), 431 deletions(-) create mode 100644 bin/internal/ideviceinstaller.version create mode 100644 bin/internal/ios-deploy.version create mode 100644 bin/internal/libimobiledevice.version create mode 100644 bin/internal/libplist.version create mode 100644 bin/internal/openssl.version create mode 100644 bin/internal/usbmuxd.version delete mode 100644 packages/flutter_tools/test/general.shard/ios/ios_workflow_test.dart diff --git a/.cirrus.yml b/.cirrus.yml index 204112cec3..6674fcc1d8 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -21,7 +21,7 @@ task: fingerprint_script: echo $OS; cat bin/internal/engine.version artifacts_cache: folder: bin/cache/artifacts - fingerprint_script: echo $OS; cat bin/internal/engine.version + fingerprint_script: echo $OS; cat bin/internal/*.version setup_script: ./dev/bots/cirrus_setup.sh matrix: - name: docs @@ -298,7 +298,7 @@ task: fingerprint_script: echo %OS% & type bin\internal\engine.version artifacts_cache: folder: bin\cache\artifacts - fingerprint_script: echo %OS% & type bin\internal\engine.version + fingerprint_script: echo %OS% & type bin\internal\*.version setup_script: - flutter config --no-analytics - flutter doctor -v diff --git a/bin/internal/ideviceinstaller.version b/bin/internal/ideviceinstaller.version new file mode 100644 index 0000000000..90f1492fb9 --- /dev/null +++ b/bin/internal/ideviceinstaller.version @@ -0,0 +1 @@ +ab9352110092cf651b5602301371cd00691c7e13 diff --git a/bin/internal/ios-deploy.version b/bin/internal/ios-deploy.version new file mode 100644 index 0000000000..768361896d --- /dev/null +++ b/bin/internal/ios-deploy.version @@ -0,0 +1 @@ +ea5583388ac0ca035f6b991fd7955bea6492c68c diff --git a/bin/internal/libimobiledevice.version b/bin/internal/libimobiledevice.version new file mode 100644 index 0000000000..0fc801bd3c --- /dev/null +++ b/bin/internal/libimobiledevice.version @@ -0,0 +1 @@ +398c1208731cb887c64a31c2ae111048b079f80d diff --git a/bin/internal/libplist.version b/bin/internal/libplist.version new file mode 100644 index 0000000000..973064be2b --- /dev/null +++ b/bin/internal/libplist.version @@ -0,0 +1 @@ +17546f53ac1377b0d4f45a800aaec7366ba5b6a0 diff --git a/bin/internal/openssl.version b/bin/internal/openssl.version new file mode 100644 index 0000000000..400e36a8e9 --- /dev/null +++ b/bin/internal/openssl.version @@ -0,0 +1 @@ +03da376ff7504c63a1d00d57cf41bd7b7e93ff65 diff --git a/bin/internal/usbmuxd.version b/bin/internal/usbmuxd.version new file mode 100644 index 0000000000..d77b5cf4e9 --- /dev/null +++ b/bin/internal/usbmuxd.version @@ -0,0 +1 @@ +60109fdef47dfe0badfb558a6a2105e8fb23660a diff --git a/dev/devicelab/lib/framework/adb.dart b/dev/devicelab/lib/framework/adb.dart index e6765c6ab2..f6db339c35 100644 --- a/dev/devicelab/lib/framework/adb.dart +++ b/dev/devicelab/lib/framework/adb.dart @@ -392,9 +392,34 @@ class IosDeviceDiscovery implements DeviceDiscovery { _workingDevice = allDevices[math.Random().nextInt(allDevices.length)]; } + // Returns the path to cached binaries relative to devicelab directory + String get _artifactDirPath { + return path.normalize( + path.join( + path.current, + '../../bin/cache/artifacts', + ) + ); + } + + // Returns a colon-separated environment variable that contains the paths + // of linked libraries for idevice_id + Map get _ideviceIdEnvironment { + final String libPath = const [ + 'libimobiledevice', + 'usbmuxd', + 'libplist', + 'openssl', + 'ideviceinstaller', + 'ios-deploy', + ].map((String packageName) => path.join(_artifactDirPath, packageName)).join(':'); + return {'DYLD_LIBRARY_PATH': libPath}; + } + @override Future> discoverDevices() async { - final List iosDeviceIDs = LineSplitter.split(await eval('idevice_id', ['-l'])) + final String ideviceIdPath = path.join(_artifactDirPath, 'libimobiledevice', 'idevice_id'); + final List iosDeviceIDs = LineSplitter.split(await eval(ideviceIdPath, ['-l'], environment: _ideviceIdEnvironment)) .map((String line) => line.trim()) .where((String line) => line.isNotEmpty) .toList(); diff --git a/packages/flutter_tools/lib/src/artifacts.dart b/packages/flutter_tools/lib/src/artifacts.dart index 65a8b630f6..191fd15671 100644 --- a/packages/flutter_tools/lib/src/artifacts.dart +++ b/packages/flutter_tools/lib/src/artifacts.dart @@ -40,6 +40,14 @@ enum Artifact { kernelWorkerSnapshot, /// The root of the web implementation of the dart SDK. flutterWebSdk, + iosDeploy, + ideviceinfo, + ideviceId, + idevicename, + idevicesyslog, + idevicescreenshot, + ideviceinstaller, + iproxy, /// The root of the Linux desktop sources. linuxDesktopPath, /// The root of the Windows desktop sources. @@ -93,6 +101,22 @@ String _artifactToFileName(Artifact artifact, [ TargetPlatform platform, BuildMo return 'dartdevc.dart.snapshot'; case Artifact.kernelWorkerSnapshot: return 'kernel_worker.dart.snapshot'; + case Artifact.iosDeploy: + return 'ios-deploy'; + case Artifact.ideviceinfo: + return 'ideviceinfo'; + case Artifact.ideviceId: + return 'idevice_id'; + case Artifact.idevicename: + return 'idevicename'; + case Artifact.idevicesyslog: + return 'idevicesyslog'; + case Artifact.idevicescreenshot: + return 'idevicescreenshot'; + case Artifact.ideviceinstaller: + return 'ideviceinstaller'; + case Artifact.iproxy: + return 'iproxy'; case Artifact.linuxDesktopPath: if (platform != TargetPlatform.linux_x64) { throw Exception('${getNameForTargetPlatform(platform)} does not support' @@ -187,13 +211,26 @@ class CachedArtifacts extends Artifacts { } String _getIosArtifactPath(Artifact artifact, TargetPlatform platform, BuildMode mode) { - final String engineDir = _getEngineArtifactsPath(platform, mode); + final String artifactFileName = _artifactToFileName(artifact); switch (artifact) { case Artifact.genSnapshot: case Artifact.snapshotDart: case Artifact.flutterFramework: case Artifact.frontendServerSnapshotForEngineDartSdk: - return fs.path.join(engineDir, _artifactToFileName(artifact)); + final String engineDir = _getEngineArtifactsPath(platform, mode); + return fs.path.join(engineDir, artifactFileName); + case Artifact.ideviceId: + case Artifact.ideviceinfo: + case Artifact.idevicescreenshot: + case Artifact.idevicesyslog: + case Artifact.idevicename: + return cache.getArtifactDirectory('libimobiledevice').childFile(artifactFileName).path; + case Artifact.iosDeploy: + return cache.getArtifactDirectory('ios-deploy').childFile(artifactFileName).path; + case Artifact.ideviceinstaller: + return cache.getArtifactDirectory('ideviceinstaller').childFile(artifactFileName).path; + case Artifact.iproxy: + return cache.getArtifactDirectory('usbmuxd').childFile(artifactFileName).path; default: assert(false, 'Artifact $artifact not available for platform $platform.'); return null; @@ -302,24 +339,25 @@ class LocalEngineArtifacts extends Artifacts { @override String getArtifactPath(Artifact artifact, { TargetPlatform platform, BuildMode mode }) { + final String artifactFileName = _artifactToFileName(artifact); switch (artifact) { case Artifact.snapshotDart: - return fs.path.join(_engineSrcPath, 'flutter', 'lib', 'snapshot', _artifactToFileName(artifact)); + return fs.path.join(_engineSrcPath, 'flutter', 'lib', 'snapshot', artifactFileName); case Artifact.genSnapshot: return _genSnapshotPath(); case Artifact.flutterTester: return _flutterTesterPath(platform); case Artifact.isolateSnapshotData: case Artifact.vmSnapshotData: - return fs.path.join(engineOutPath, 'gen', 'flutter', 'lib', 'snapshot', _artifactToFileName(artifact)); + return fs.path.join(engineOutPath, 'gen', 'flutter', 'lib', 'snapshot', artifactFileName); case Artifact.platformKernelDill: - return fs.path.join(_getFlutterPatchedSdkPath(mode), _artifactToFileName(artifact)); + return fs.path.join(_getFlutterPatchedSdkPath(mode), artifactFileName); case Artifact.platformLibrariesJson: - return fs.path.join(_getFlutterPatchedSdkPath(mode), 'lib', _artifactToFileName(artifact)); + return fs.path.join(_getFlutterPatchedSdkPath(mode), 'lib', artifactFileName); case Artifact.flutterFramework: - return fs.path.join(engineOutPath, _artifactToFileName(artifact)); + return fs.path.join(engineOutPath, artifactFileName); case Artifact.flutterMacOSFramework: - return fs.path.join(engineOutPath, _artifactToFileName(artifact)); + return fs.path.join(engineOutPath, artifactFileName); case Artifact.flutterPatchedSdkPath: // When using local engine always use [BuildMode.debug] regardless of // what was specified in [mode] argument because local engine will @@ -329,23 +367,35 @@ class LocalEngineArtifacts extends Artifacts { case Artifact.flutterWebSdk: return _getFlutterWebSdkPath(); case Artifact.frontendServerSnapshotForEngineDartSdk: - return fs.path.join(_hostEngineOutPath, 'gen', _artifactToFileName(artifact)); + return fs.path.join(_hostEngineOutPath, 'gen', artifactFileName); case Artifact.engineDartSdkPath: return fs.path.join(_hostEngineOutPath, 'dart-sdk'); case Artifact.engineDartBinary: - return fs.path.join(_hostEngineOutPath, 'dart-sdk', 'bin', _artifactToFileName(artifact)); + return fs.path.join(_hostEngineOutPath, 'dart-sdk', 'bin', artifactFileName); case Artifact.dart2jsSnapshot: - return fs.path.join(_hostEngineOutPath, 'dart-sdk', 'bin', 'snapshots', _artifactToFileName(artifact)); + return fs.path.join(_hostEngineOutPath, 'dart-sdk', 'bin', 'snapshots', artifactFileName); case Artifact.dartdevcSnapshot: - return fs.path.join(dartSdkPath, 'bin', 'snapshots', _artifactToFileName(artifact)); + return fs.path.join(dartSdkPath, 'bin', 'snapshots', artifactFileName); case Artifact.kernelWorkerSnapshot: - return fs.path.join(_hostEngineOutPath, 'dart-sdk', 'bin', 'snapshots', _artifactToFileName(artifact)); + return fs.path.join(_hostEngineOutPath, 'dart-sdk', 'bin', 'snapshots', artifactFileName); + case Artifact.ideviceId: + case Artifact.ideviceinfo: + case Artifact.idevicename: + case Artifact.idevicescreenshot: + case Artifact.idevicesyslog: + return cache.getArtifactDirectory('libimobiledevice').childFile(artifactFileName).path; + case Artifact.ideviceinstaller: + return cache.getArtifactDirectory('ideviceinstaller').childFile(artifactFileName).path; + case Artifact.iosDeploy: + return cache.getArtifactDirectory('ios-deploy').childFile(artifactFileName).path; + case Artifact.iproxy: + return cache.getArtifactDirectory('usbmuxd').childFile(artifactFileName).path; case Artifact.linuxDesktopPath: - return fs.path.join(_hostEngineOutPath, _artifactToFileName(artifact)); + return fs.path.join(_hostEngineOutPath, artifactFileName); case Artifact.windowsDesktopPath: - return fs.path.join(_hostEngineOutPath, _artifactToFileName(artifact)); + return fs.path.join(_hostEngineOutPath, artifactFileName); case Artifact.skyEnginePath: - return fs.path.join(_hostEngineOutPath, 'gen', 'dart-pkg', _artifactToFileName(artifact)); + return fs.path.join(_hostEngineOutPath, 'gen', 'dart-pkg', artifactFileName); } assert(false, 'Invalid artifact $artifact.'); return null; diff --git a/packages/flutter_tools/lib/src/base/process.dart b/packages/flutter_tools/lib/src/base/process.dart index 47b2949a17..1dd5765821 100644 --- a/packages/flutter_tools/lib/src/base/process.dart +++ b/packages/flutter_tools/lib/src/base/process.dart @@ -263,20 +263,26 @@ Future runCheckedAsync( return result; } -bool exitsHappy(List cli) { +bool exitsHappy( + List cli, { + Map environment, +}) { _traceCommand(cli); try { - return processManager.runSync(cli).exitCode == 0; + return processManager.runSync(cli, environment: environment).exitCode == 0; } catch (error) { printTrace('$cli failed with $error'); return false; } } -Future exitsHappyAsync(List cli) async { +Future exitsHappyAsync( + List cli, { + Map environment, +}) async { _traceCommand(cli); try { - return (await processManager.run(cli)).exitCode == 0; + return (await processManager.run(cli, environment: environment)).exitCode == 0; } catch (error) { printTrace('$cli failed with $error'); return false; diff --git a/packages/flutter_tools/lib/src/base/user_messages.dart b/packages/flutter_tools/lib/src/base/user_messages.dart index 09af158d94..0f359b58be 100644 --- a/packages/flutter_tools/lib/src/base/user_messages.dart +++ b/packages/flutter_tools/lib/src/base/user_messages.dart @@ -145,44 +145,6 @@ class UserMessages { 'Once installed, run:\n' ' sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer'; - // Messages used in IOSValidator - String get iOSIMobileDeviceMissing => - 'libimobiledevice and ideviceinstaller are not installed. To install with Brew, run:\n' - ' brew update\n' - ' brew install --HEAD usbmuxd\n' - ' brew link usbmuxd\n' - ' brew install --HEAD libimobiledevice\n' - ' brew install ideviceinstaller'; - String get iOSIMobileDeviceBroken => - 'Verify that all connected devices have been paired with this computer in Xcode.\n' - 'If all devices have been paired, libimobiledevice and ideviceinstaller may require updating.\n' - 'To update with Brew, run:\n' - ' brew update\n' - ' brew uninstall --ignore-dependencies libimobiledevice\n' - ' brew uninstall --ignore-dependencies usbmuxd\n' - ' brew install --HEAD usbmuxd\n' - ' brew unlink usbmuxd\n' - ' brew link usbmuxd\n' - ' brew install --HEAD libimobiledevice\n' - ' brew install ideviceinstaller'; - String get iOSDeviceInstallerMissing => - 'ideviceinstaller is not installed; this is used to discover connected iOS devices.\n' - 'To install with Brew, run:\n' - ' brew install --HEAD usbmuxd\n' - ' brew link usbmuxd\n' - ' brew install --HEAD libimobiledevice\n' - ' brew install ideviceinstaller'; - String iOSDeployVersion(String version) => 'ios-deploy $version'; - String iOSDeployOutdated(String minVersion) => - 'ios-deploy out of date ($minVersion is required). To upgrade with Brew:\n' - ' brew upgrade ios-deploy'; - String get iOSDeployMissing => - 'ios-deploy not installed. To install:\n' - ' brew install ios-deploy'; - String get iOSBrewMissing => - 'Brew can be used to install tools for iOS device development.\n' - 'Download brew at https://brew.sh/.'; - // Messages used in CocoaPodsValidator String cocoaPodsVersion(String version) => 'CocoaPods version $version'; String cocoaPodsUninitialized(String consequence) => @@ -206,9 +168,6 @@ class UserMessages { '$consequence\n' 'To upgrade:\n' '$upgradeInstructions'; - String get cocoaPodsBrewMissing => - 'Brew can be used to install CocoaPods.\n' - 'Download brew at https://brew.sh/.'; // Messages used in VsCodeValidator String vsCodeVersion(String version) => 'version $version'; diff --git a/packages/flutter_tools/lib/src/cache.dart b/packages/flutter_tools/lib/src/cache.dart index 6d2d13e023..cd16d3ea9f 100644 --- a/packages/flutter_tools/lib/src/cache.dart +++ b/packages/flutter_tools/lib/src/cache.dart @@ -53,7 +53,7 @@ class DevelopmentArtifact { /// Artifacts required by all developments. static const DevelopmentArtifact universal = DevelopmentArtifact._('universal'); - /// The vaulues of DevelopmentArtifacts. + /// The values of DevelopmentArtifacts. static final List values = [ android, iOS, @@ -83,6 +83,9 @@ class Cache { _artifacts.add(LinuxEngineArtifacts(this)); _artifacts.add(LinuxFuchsiaSDKArtifacts(this)); _artifacts.add(MacOSFuchsiaSDKArtifacts(this)); + for (String artifactName in IosUsbArtifacts.artifactNames) { + _artifacts.add(IosUsbArtifacts(artifactName, this)); + } } else { _artifacts.addAll(artifacts); } @@ -221,6 +224,22 @@ class Cache { return getCacheArtifacts().childDirectory(name); } + MapEntry get dyLdLibEntry { + if (_dyLdLibEntry != null) { + return _dyLdLibEntry; + } + final List paths = []; + for (CachedArtifact artifact in _artifacts) { + final String currentPath = artifact.dyLdLibPath; + if (currentPath.isNotEmpty) { + paths.add(currentPath); + } + } + _dyLdLibEntry = MapEntry('DYLD_LIBRARY_PATH', paths.join(':')); + return _dyLdLibEntry; + } + MapEntry _dyLdLibEntry; + /// The web sdk has to be co-located with the dart-sdk so that they can share source /// code. Directory getWebSdkDirectory() { @@ -328,6 +347,9 @@ abstract class CachedArtifact { // artifact name. String get stampName => name; + /// Returns a string to be set as environment DYLD_LIBARY_PATH variable + String get dyLdLibPath => ''; + /// All development artifacts this cache provides. final Set developmentArtifacts; @@ -890,6 +912,39 @@ class MacOSFuchsiaSDKArtifacts extends _FuchsiaSDKArtifacts { } } +/// Cached iOS/USB binary artifacts. +class IosUsbArtifacts extends CachedArtifact { + IosUsbArtifacts(String name, Cache cache) : super( + name, + cache, + // This is universal to ensure every command checks for them first + const { DevelopmentArtifact.universal }, + ); + + static const List artifactNames = [ + 'libimobiledevice', + 'usbmuxd', + 'libplist', + 'openssl', + 'ideviceinstaller', + 'ios-deploy', + ]; + + @override + String get dyLdLibPath { + return cache.getArtifactDirectory(name).path; + } + + @override + Future updateInner() { + if (!platform.isMacOS) { + return Future.value(); + } + final Uri archiveUri = Uri.parse('$_storageBaseUrl/flutter_infra/ios-usb-dependencies/$name/$version/$name.zip'); + return _downloadZipArchive('Downloading $name...', archiveUri, location); + } +} + // Many characters are problematic in filenames, especially on Windows. final Map> _flattenNameSubstitutions = >{ r'@'.codeUnitAt(0): '@@'.codeUnits, diff --git a/packages/flutter_tools/lib/src/context_runner.dart b/packages/flutter_tools/lib/src/context_runner.dart index 533cfc6fbd..ba5d910ad5 100644 --- a/packages/flutter_tools/lib/src/context_runner.dart +++ b/packages/flutter_tools/lib/src/context_runner.dart @@ -87,9 +87,8 @@ Future runInContext( FuchsiaWorkflow: () => FuchsiaWorkflow(), GenSnapshot: () => const GenSnapshot(), HotRunnerConfig: () => HotRunnerConfig(), - IMobileDevice: () => const IMobileDevice(), + IMobileDevice: () => IMobileDevice(), IOSSimulatorUtils: () => IOSSimulatorUtils(), - IOSValidator: () => const IOSValidator(), IOSWorkflow: () => const IOSWorkflow(), KernelCompilerFactory: () => const KernelCompilerFactory(), LinuxWorkflow: () => const LinuxWorkflow(), diff --git a/packages/flutter_tools/lib/src/doctor.dart b/packages/flutter_tools/lib/src/doctor.dart index 58ba4de796..c4aa3a67bc 100644 --- a/packages/flutter_tools/lib/src/doctor.dart +++ b/packages/flutter_tools/lib/src/doctor.dart @@ -72,8 +72,6 @@ class _DefaultDoctorValidatorsProvider implements DoctorValidatorsProvider { GroupedValidator([androidValidator, androidLicenseValidator]), if (iosWorkflow.appliesToHostPlatform || macOSWorkflow.appliesToHostPlatform) GroupedValidator([xcodeValidator, cocoapodsValidator]), - if (iosWorkflow.appliesToHostPlatform) - iosValidator, if (webWorkflow.appliesToHostPlatform) const WebValidator(), // Add desktop doctors to workflow if the flag is enabled. diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart index 3ac80c57f4..33e520decf 100644 --- a/packages/flutter_tools/lib/src/ios/devices.dart +++ b/packages/flutter_tools/lib/src/ios/devices.dart @@ -7,6 +7,7 @@ import 'dart:async'; import 'package:meta/meta.dart'; import '../application_package.dart'; +import '../artifacts.dart'; import '../base/file_system.dart'; import '../base/io.dart'; import '../base/logger.dart'; @@ -23,10 +24,6 @@ import 'code_signing.dart'; import 'ios_workflow.dart'; import 'mac.dart'; -const String _kIdeviceinstallerInstructions = - 'To work with iOS devices, please install ideviceinstaller. To install, run:\n' - 'brew install ideviceinstaller.'; - class IOSDeploy { const IOSDeploy(); @@ -37,9 +34,15 @@ class IOSDeploy { @required String bundlePath, @required List launchArguments, }) async { - final List launchCommand = [ + final String iosDeployPath = artifacts.getArtifactPath(Artifact.iosDeploy, platform: TargetPlatform.ios); + // TODO(fujino): remove fallback once g3 updated + const List fallbackIosDeployPath = [ '/usr/bin/env', 'ios-deploy', + ]; + final List commandList = iosDeployPath != null ? [iosDeployPath] : fallbackIosDeployPath; + final List launchCommand = [ + ...commandList, '--id', deviceId, '--bundle', @@ -61,6 +64,7 @@ class IOSDeploy { // it. final Map iosDeployEnv = Map.from(platform.environment); iosDeployEnv['PATH'] = '/usr/bin:${iosDeployEnv['PATH']}'; + iosDeployEnv.addEntries(>[cache.dyLdLibEntry]); return await runCommandAndStreamOutput( launchCommand, @@ -120,8 +124,20 @@ class IOSDevice extends Device { platformType: PlatformType.ios, ephemeral: true, ) { - _installerPath = _checkForCommand('ideviceinstaller'); - _iproxyPath = _checkForCommand('iproxy'); + if (!platform.isMacOS) { + printError('Cannot control iOS devices or simulators. ideviceinstaller and iproxy are not available on your platform.'); + _installerPath = null; + _iproxyPath = null; + return; + } + _installerPath = artifacts.getArtifactPath( + Artifact.ideviceinstaller, + platform: TargetPlatform.ios + ) ?? 'ideviceinstaller'; // TODO(fujino): remove fallback once g3 updated + _iproxyPath = artifacts.getArtifactPath( + Artifact.iproxy, + platform: TargetPlatform.ios + ) ?? 'iproxy'; // TODO(fujino): remove fallback once g3 updated } String _installerPath; @@ -173,23 +189,6 @@ class IOSDevice extends Device { return devices; } - static String _checkForCommand( - String command, [ - String macInstructions = _kIdeviceinstallerInstructions, - ]) { - try { - command = runCheckedSync(['which', command]).trim(); - } catch (e) { - if (platform.isMacOS) { - printError('$command not found. $macInstructions'); - } else { - printError('Cannot control iOS devices or simulators. $command is not available on your platform.'); - } - return null; - } - return command; - } - @override Future isAppInstalled(ApplicationPackage app) async { try { @@ -589,12 +588,17 @@ class _IOSDevicePortForwarder extends DevicePortForwarder { while (!connected) { printTrace('attempting to forward device port $devicePort to host port $hostPort'); // Usage: iproxy LOCAL_TCP_PORT DEVICE_TCP_PORT UDID - process = await runCommand([ - device._iproxyPath, - hostPort.toString(), - devicePort.toString(), - device.id, - ]); + process = await runCommand( + [ + device._iproxyPath, + hostPort.toString(), + devicePort.toString(), + device.id, + ], + environment: Map.fromEntries( + >[cache.dyLdLibEntry], + ), + ); // TODO(ianh): This is a flakey race condition, https://github.com/libimobiledevice/libimobiledevice/issues/674 connected = !await process.stdout.isEmpty.timeout(_kiProxyPortForwardTimeout, onTimeout: () => false); if (!connected) { diff --git a/packages/flutter_tools/lib/src/ios/ios_workflow.dart b/packages/flutter_tools/lib/src/ios/ios_workflow.dart index cf95c30c8d..f21dffd814 100644 --- a/packages/flutter_tools/lib/src/ios/ios_workflow.dart +++ b/packages/flutter_tools/lib/src/ios/ios_workflow.dart @@ -2,21 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; - import '../base/context.dart'; -import '../base/os.dart'; import '../base/platform.dart'; -import '../base/process.dart'; -import '../base/user_messages.dart'; -import '../base/version.dart'; import '../doctor.dart'; import '../macos/xcode.dart'; -import 'mac.dart'; import 'plist_utils.dart' as plist; IOSWorkflow get iosWorkflow => context.get(); -IOSValidator get iosValidator => context.get(); class IOSWorkflow implements Workflow { const IOSWorkflow(); @@ -40,86 +32,3 @@ class IOSWorkflow implements Workflow { return plist.getValueFromFile(path, key); } } - -class IOSValidator extends DoctorValidator { - - const IOSValidator() : super('iOS tools - develop for iOS devices'); - - Future get hasIDeviceInstaller => exitsHappyAsync(['ideviceinstaller', '-h']); - - Future get hasIosDeploy => exitsHappyAsync(['ios-deploy', '--version']); - - String get iosDeployMinimumVersion => '1.9.4'; - - // ios-deploy <= v1.9.3 declares itself as v2.0.0 - List get iosDeployBadVersions => ['2.0.0']; - - Future get iosDeployVersionText async => (await runAsync(['ios-deploy', '--version'])).processResult.stdout.replaceAll('\n', ''); - - bool get hasHomebrew => os.which('brew') != null; - - Future get macDevMode async => (await runAsync(['DevToolsSecurity', '-status'])).processResult.stdout; - - Future get _iosDeployIsInstalledAndMeetsVersionCheck async { - if (!await hasIosDeploy) - return false; - try { - final Version version = Version.parse(await iosDeployVersionText); - return version >= Version.parse(iosDeployMinimumVersion) - && !iosDeployBadVersions.map((String v) => Version.parse(v)).contains(version); - } on FormatException catch (_) { - return false; - } - } - - // Change this value if the number of checks for packages needed for installation changes - static const int totalChecks = 4; - - @override - Future validate() async { - final List messages = []; - ValidationType packageManagerStatus = ValidationType.installed; - - int checksFailed = 0; - - if (!iMobileDevice.isInstalled) { - checksFailed += 3; - packageManagerStatus = ValidationType.partial; - messages.add(ValidationMessage.error(userMessages.iOSIMobileDeviceMissing)); - } else if (!await iMobileDevice.isWorking) { - checksFailed += 2; - packageManagerStatus = ValidationType.partial; - messages.add(ValidationMessage.error(userMessages.iOSIMobileDeviceBroken)); - } else if (!await hasIDeviceInstaller) { - checksFailed += 1; - packageManagerStatus = ValidationType.partial; - messages.add(ValidationMessage.error(userMessages.iOSDeviceInstallerMissing)); - } - - final bool iHasIosDeploy = await hasIosDeploy; - - // Check ios-deploy is installed at meets version requirements. - if (iHasIosDeploy) { - messages.add(ValidationMessage(userMessages.iOSDeployVersion(await iosDeployVersionText))); - } - if (!await _iosDeployIsInstalledAndMeetsVersionCheck) { - packageManagerStatus = ValidationType.partial; - if (iHasIosDeploy) { - messages.add(ValidationMessage.error(userMessages.iOSDeployOutdated(iosDeployMinimumVersion))); - } else { - checksFailed += 1; - messages.add(ValidationMessage.error(userMessages.iOSDeployMissing)); - } - } - - // If one of the checks for the packages failed, we may need brew so that we can install - // the necessary packages. If they're all there, however, we don't even need it. - if (checksFailed == totalChecks) - packageManagerStatus = ValidationType.missing; - if (checksFailed > 0 && !hasHomebrew) { - messages.add(ValidationMessage.hint(userMessages.iOSBrewMissing)); - } - - return ValidationResult(packageManagerStatus, messages); - } -} diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart index dcc7f5acdc..2bcdd59463 100644 --- a/packages/flutter_tools/lib/src/ios/mac.dart +++ b/packages/flutter_tools/lib/src/ios/mac.dart @@ -7,6 +7,7 @@ import 'dart:async'; import 'package:meta/meta.dart'; import '../application_package.dart'; +import '../artifacts.dart'; import '../base/common.dart'; import '../base/context.dart'; import '../base/file_system.dart'; @@ -42,35 +43,99 @@ class IOSDeviceNotFoundError implements Exception { } class IMobileDevice { - const IMobileDevice(); + IMobileDevice() + : _ideviceIdPath = artifacts.getArtifactPath(Artifact.ideviceId, platform: TargetPlatform.ios) + ?? 'idevice_id', // TODO(fujino): remove fallback once g3 updated + _ideviceinfoPath = artifacts.getArtifactPath(Artifact.ideviceinfo, platform: TargetPlatform.ios) + ?? 'ideviceinfo', // TODO(fujino): remove fallback once g3 updated + _idevicenamePath = artifacts.getArtifactPath(Artifact.idevicename, platform: TargetPlatform.ios) + ?? 'idevicename', // TODO(fujino): remove fallback once g3 updated + _idevicesyslogPath = artifacts.getArtifactPath(Artifact.idevicesyslog, platform: TargetPlatform.ios) + ?? 'idevicesyslog', // TODO(fujino): remove fallback once g3 updated + _idevicescreenshotPath = artifacts.getArtifactPath(Artifact.idevicescreenshot, platform: TargetPlatform.ios) + ?? 'idevicescreenshot' { // TODO(fujino): remove fallback once g3 updated + } + final String _ideviceIdPath; + final String _ideviceinfoPath; + final String _idevicenamePath; + final String _idevicesyslogPath; + final String _idevicescreenshotPath; - bool get isInstalled => exitsHappy(['idevice_id', '-h']); + bool get isInstalled { + _isInstalled ??= exitsHappy( + [ + _ideviceIdPath, + '-h' + ], + environment: Map.fromEntries( + >[cache.dyLdLibEntry] + ), + ); + return _isInstalled; + } + bool _isInstalled; /// Returns true if libimobiledevice is installed and working as expected. /// /// Older releases of libimobiledevice fail to work with iOS 10.3 and above. Future get isWorking async { - if (!isInstalled) - return false; + if (_isWorking != null) { + return _isWorking; + } + if (!isInstalled) { + _isWorking = false; + return _isWorking; + } // If usage info is printed in a hyphenated id, we need to update. const String fakeIphoneId = '00008020-001C2D903C42002E'; - final ProcessResult ideviceResult = (await runAsync(['ideviceinfo', '-u', fakeIphoneId])).processResult; + final Map executionEnv = Map.fromEntries( + >[cache.dyLdLibEntry] + ); + final ProcessResult ideviceResult = (await runAsync( + [ + _ideviceinfoPath, + '-u', + fakeIphoneId + ], + environment: executionEnv, + )).processResult; if (ideviceResult.stdout.contains('Usage: ideviceinfo')) { - return false; + _isWorking = false; + return _isWorking; } // If no device is attached, we're unable to detect any problems. Assume all is well. - final ProcessResult result = (await runAsync(['idevice_id', '-l'])).processResult; - if (result.exitCode == 0 && result.stdout.isEmpty) - return true; - - // Check that we can look up the names of any attached devices. - return await exitsHappyAsync(['idevicename']); + final ProcessResult result = (await runAsync( + [ + _ideviceIdPath, + '-l', + ], + environment: executionEnv, + )).processResult; + if (result.exitCode == 0 && result.stdout.isEmpty) { + _isWorking = true; + } else { + // Check that we can look up the names of any attached devices. + _isWorking = await exitsHappyAsync( + [_idevicenamePath], + environment: executionEnv, + ); + } + return _isWorking; } + bool _isWorking; Future getAvailableDeviceIDs() async { try { - final ProcessResult result = await processManager.run(['idevice_id', '-l']); + final ProcessResult result = await processManager.run( + [ + _ideviceIdPath, + '-l' + ], + environment: Map.fromEntries( + >[cache.dyLdLibEntry] + ), + ); if (result.exitCode != 0) throw ToolExit('idevice_id returned an error:\n${result.stderr}'); return result.stdout; @@ -81,7 +146,18 @@ class IMobileDevice { Future getInfoForDevice(String deviceID, String key) async { try { - final ProcessResult result = await processManager.run(['ideviceinfo', '-u', deviceID, '-k', key]); + final ProcessResult result = await processManager.run( + [ + _ideviceinfoPath, + '-u', + deviceID, + '-k', + key + ], + environment: Map.fromEntries( + >[cache.dyLdLibEntry] + ), + ); if (result.exitCode == 255 && result.stdout != null && result.stdout.contains('No device found')) throw IOSDeviceNotFoundError('ideviceinfo could not find device:\n${result.stdout}'); if (result.exitCode != 0) @@ -93,11 +169,30 @@ class IMobileDevice { } /// Starts `idevicesyslog` and returns the running process. - Future startLogger(String deviceID) => runCommand(['idevicesyslog', '-u', deviceID]); + Future startLogger(String deviceID) { + return runCommand( + [ + _idevicesyslogPath, + '-u', + deviceID, + ], + environment: Map.fromEntries( + >[cache.dyLdLibEntry] + ), + ); + } /// Captures a screenshot to the specified outputFile. Future takeScreenshot(File outputFile) { - return runCheckedAsync(['idevicescreenshot', outputFile.path]); + return runCheckedAsync( + [ + _idevicescreenshotPath, + outputFile.path + ], + environment: Map.fromEntries( + >[cache.dyLdLibEntry] + ), + ); } } diff --git a/packages/flutter_tools/lib/src/macos/cocoapods.dart b/packages/flutter_tools/lib/src/macos/cocoapods.dart index 9462395b72..9729387c9c 100644 --- a/packages/flutter_tools/lib/src/macos/cocoapods.dart +++ b/packages/flutter_tools/lib/src/macos/cocoapods.dart @@ -30,11 +30,11 @@ const String unknownCocoaPodsConsequence = ''' Ensure that the output of 'pod --version' contains only digits and . to be recognized by Flutter.'''; const String cocoaPodsInstallInstructions = ''' - brew install cocoapods + sudo gem install cocoapods pod setup'''; const String cocoaPodsUpgradeInstructions = ''' - brew upgrade cocoapods + sudo gem install cocoapods pod setup'''; CocoaPods get cocoaPods => context.get(); diff --git a/packages/flutter_tools/lib/src/macos/cocoapods_validator.dart b/packages/flutter_tools/lib/src/macos/cocoapods_validator.dart index 615c3f54b3..aaee745572 100644 --- a/packages/flutter_tools/lib/src/macos/cocoapods_validator.dart +++ b/packages/flutter_tools/lib/src/macos/cocoapods_validator.dart @@ -5,7 +5,6 @@ import 'dart:async'; import '../base/context.dart'; -import '../base/os.dart'; import '../base/user_messages.dart'; import '../doctor.dart'; import 'cocoapods.dart'; @@ -15,8 +14,6 @@ CocoaPodsValidator get cocoapodsValidator => context.get(); class CocoaPodsValidator extends DoctorValidator { const CocoaPodsValidator() : super('CocoaPods subvalidator'); - bool get hasHomebrew => os.which('brew') != null; - @override Future validate() async { final List messages = []; @@ -48,11 +45,6 @@ class CocoaPodsValidator extends DoctorValidator { } } - // Only check/report homebrew status if CocoaPods isn't installed. - if (status == ValidationType.missing && !hasHomebrew) { - messages.add(ValidationMessage.hint(userMessages.cocoaPodsBrewMissing)); - } - return ValidationResult(status, messages); } -} \ No newline at end of file +} diff --git a/packages/flutter_tools/test/general.shard/cache_test.dart b/packages/flutter_tools/test/general.shard/cache_test.dart index 18b97b6768..ca29c695e2 100644 --- a/packages/flutter_tools/test/general.shard/cache_test.dart +++ b/packages/flutter_tools/test/general.shard/cache_test.dart @@ -107,6 +107,22 @@ void main() { verifyNever(artifact1.update({})); verify(artifact2.update({})); }); + testUsingContext('getter dyLdLibEntry concatenates the output of each artifact\'s dyLdLibEntry getter', () async { + final CachedArtifact artifact1 = MockCachedArtifact(); + final CachedArtifact artifact2 = MockCachedArtifact(); + final CachedArtifact artifact3 = MockCachedArtifact(); + when(artifact1.dyLdLibPath).thenReturn('/path/to/alpha:/path/to/beta'); + when(artifact2.dyLdLibPath).thenReturn('/path/to/gamma:/path/to/delta:/path/to/epsilon'); + when(artifact3.dyLdLibPath).thenReturn(''); // Empty output + final Cache cache = Cache(artifacts: [artifact1, artifact2, artifact3]); + expect(cache.dyLdLibEntry.key, 'DYLD_LIBRARY_PATH'); + expect( + cache.dyLdLibEntry.value, + '/path/to/alpha:/path/to/beta:/path/to/gamma:/path/to/delta:/path/to/epsilon', + ); + }, overrides: { + Cache: ()=> mockCache, + }); testUsingContext('failed storage.googleapis.com download shows China warning', () async { final CachedArtifact artifact1 = MockCachedArtifact(); final CachedArtifact artifact2 = MockCachedArtifact(); diff --git a/packages/flutter_tools/test/general.shard/ios/code_signing_test.dart b/packages/flutter_tools/test/general.shard/ios/code_signing_test.dart index c9a54b351d..57a22423e8 100644 --- a/packages/flutter_tools/test/general.shard/ios/code_signing_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/code_signing_test.dart @@ -425,7 +425,7 @@ void main() { final Map signingConfigs = await getCodeSigningIdentityDevelopmentTeam(iosApp: app); expect( - testLogger.errorText, + testLogger.errorText.replaceAll('\n', ' '), contains('Saved signing certificate "iPhone Developer: Invalid Profile" is not a valid development certificate'), ); expect( diff --git a/packages/flutter_tools/test/general.shard/ios/ios_workflow_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_workflow_test.dart deleted file mode 100644 index 2ac101980f..0000000000 --- a/packages/flutter_tools/test/general.shard/ios/ios_workflow_test.dart +++ /dev/null @@ -1,178 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:file/memory.dart'; -import 'package:flutter_tools/src/base/file_system.dart'; -import 'package:flutter_tools/src/base/io.dart'; -import 'package:flutter_tools/src/doctor.dart'; -import 'package:flutter_tools/src/ios/ios_workflow.dart'; -import 'package:flutter_tools/src/ios/mac.dart'; -import 'package:mockito/mockito.dart'; -import 'package:process/process.dart'; - -import '../../src/common.dart'; -import '../../src/context.dart'; - -void main() { - group('iOS Workflow validation', () { - MockIMobileDevice iMobileDevice; - MockIMobileDevice iMobileDeviceUninstalled; - MockProcessManager processManager; - FileSystem fs; - - setUp(() { - iMobileDevice = MockIMobileDevice(); - iMobileDeviceUninstalled = MockIMobileDevice(isInstalled: false); - processManager = MockProcessManager(); - fs = MemoryFileSystem(); - }); - - testUsingContext('Emit missing status when nothing is installed', () async { - final IOSWorkflowTestTarget workflow = IOSWorkflowTestTarget( - hasHomebrew: false, - hasIosDeploy: false, - hasIDeviceInstaller: false, - iosDeployVersionText: '0.0.0', - ); - final ValidationResult result = await workflow.validate(); - expect(result.type, ValidationType.missing); - }, overrides: { - IMobileDevice: () => iMobileDeviceUninstalled, - }); - - testUsingContext('Emits installed status when homebrew not installed, but not needed', () async { - final IOSWorkflowTestTarget workflow = IOSWorkflowTestTarget(hasHomebrew: false); - final ValidationResult result = await workflow.validate(); - expect(result.type, ValidationType.installed); - }, overrides: { - IMobileDevice: () => iMobileDevice, - }); - - testUsingContext('Emits partial status when libimobiledevice is not installed', () async { - final IOSWorkflowTestTarget workflow = IOSWorkflowTestTarget(); - final ValidationResult result = await workflow.validate(); - expect(result.type, ValidationType.partial); - }, overrides: { - IMobileDevice: () => MockIMobileDevice(isInstalled: false, isWorking: false), - }); - - testUsingContext('Emits partial status when libimobiledevice is installed but not working', () async { - final IOSWorkflowTestTarget workflow = IOSWorkflowTestTarget(); - final ValidationResult result = await workflow.validate(); - expect(result.type, ValidationType.partial); - }, overrides: { - IMobileDevice: () => MockIMobileDevice(isWorking: false), - }); - - testUsingContext('Emits partial status when libimobiledevice is installed but not working', () async { - when(processManager.run( - ['ideviceinfo', '-u', '00008020-001C2D903C42002E'], - workingDirectory: anyNamed('workingDirectory'), - environment: anyNamed('environment')), - ).thenAnswer((Invocation _) async { - final MockProcessResult result = MockProcessResult(); - when(result.stdout).thenReturn(r''' -Usage: ideviceinfo [OPTIONS] -Show information about a connected device. - - -d, --debug enable communication debugging - -s, --simple use a simple connection to avoid auto-pairing with the device - -u, --udid UDID target specific device by its 40-digit device UDID - -q, --domain NAME set domain of query to NAME. Default: None - -k, --key NAME only query key specified by NAME. Default: All keys. - -x, --xml output information as xml plist instead of key/value pairs - -h, --help prints usage information - '''); - return null; - }); - final IOSWorkflowTestTarget workflow = IOSWorkflowTestTarget(); - final ValidationResult result = await workflow.validate(); - expect(result.type, ValidationType.partial); - }, overrides: { - ProcessManager: () => processManager, - }); - - - testUsingContext('Emits partial status when ios-deploy is not installed', () async { - final IOSWorkflowTestTarget workflow = IOSWorkflowTestTarget(hasIosDeploy: false); - final ValidationResult result = await workflow.validate(); - expect(result.type, ValidationType.partial); - }, overrides: { - IMobileDevice: () => iMobileDevice, - }); - - testUsingContext('Emits partial status when ios-deploy version is too low', () async { - final IOSWorkflowTestTarget workflow = IOSWorkflowTestTarget(iosDeployVersionText: '1.8.0'); - final ValidationResult result = await workflow.validate(); - expect(result.type, ValidationType.partial); - }, overrides: { - IMobileDevice: () => iMobileDevice, - }); - - testUsingContext('Emits partial status when ios-deploy version is a known bad version', () async { - final IOSWorkflowTestTarget workflow = IOSWorkflowTestTarget(iosDeployVersionText: '2.0.0'); - final ValidationResult result = await workflow.validate(); - expect(result.type, ValidationType.partial); - }, overrides: { - IMobileDevice: () => iMobileDevice, - }); - - testUsingContext('Succeeds when all checks pass', () async { - final ValidationResult result = await IOSWorkflowTestTarget().validate(); - expect(result.type, ValidationType.installed); - }, overrides: { - FileSystem: () => fs, - IMobileDevice: () => iMobileDevice, - ProcessManager: () => processManager, - }); - }); -} - -final ProcessResult exitsHappy = ProcessResult( - 1, // pid - 0, // exitCode - '', // stdout - '', // stderr -); - -class MockIMobileDevice extends IMobileDevice { - MockIMobileDevice({ - this.isInstalled = true, - bool isWorking = true, - }) : isWorking = Future.value(isWorking); - - @override - final bool isInstalled; - - @override - final Future isWorking; -} - -class MockProcessManager extends Mock implements ProcessManager {} -class MockProcessResult extends Mock implements ProcessResult {} - -class IOSWorkflowTestTarget extends IOSValidator { - IOSWorkflowTestTarget({ - this.hasHomebrew = true, - bool hasIosDeploy = true, - String iosDeployVersionText = '1.9.4', - bool hasIDeviceInstaller = true, - }) : hasIosDeploy = Future.value(hasIosDeploy), - iosDeployVersionText = Future.value(iosDeployVersionText), - hasIDeviceInstaller = Future.value(hasIDeviceInstaller); - - @override - final bool hasHomebrew; - - @override - final Future hasIosDeploy; - - @override - final Future iosDeployVersionText; - - @override - final Future hasIDeviceInstaller; -} diff --git a/packages/flutter_tools/test/general.shard/ios/mac_test.dart b/packages/flutter_tools/test/general.shard/ios/mac_test.dart index e9f88c799b..8ffc0f0445 100644 --- a/packages/flutter_tools/test/general.shard/ios/mac_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/mac_test.dart @@ -9,6 +9,8 @@ import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/io.dart' show ProcessException, ProcessResult; import 'package:flutter_tools/src/ios/mac.dart'; import 'package:flutter_tools/src/ios/xcodeproj.dart'; +import 'package:flutter_tools/src/artifacts.dart'; +import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/project.dart'; import 'package:mockito/mockito.dart'; import 'package:platform/platform.dart'; @@ -22,6 +24,8 @@ final Map noColorTerminalOverride = { Platform: _kNoColorTerminalPlatform, }; +class MockArtifacts extends Mock implements Artifacts {} +class MockCache extends Mock implements Cache {} class MockProcessManager extends Mock implements ProcessManager {} class MockFile extends Mock implements File {} class MockXcodeProjectInterpreter extends Mock implements XcodeProjectInterpreter {} @@ -32,41 +36,80 @@ void main() { final FakePlatform osx = FakePlatform.fromPlatform(const LocalPlatform()) ..operatingSystem = 'macos'; MockProcessManager mockProcessManager; + final String libimobiledevicePath = fs.path.join('bin', 'cache', 'artifacts', 'libimobiledevice'); + final String ideviceIdPath = fs.path.join(libimobiledevicePath, 'idevice_id'); + final String ideviceInfoPath = fs.path.join(libimobiledevicePath, 'ideviceinfo'); + final String idevicescreenshotPath = fs.path.join(libimobiledevicePath, 'idevicescreenshot'); + MockArtifacts mockArtifacts; + MockCache mockCache; setUp(() { mockProcessManager = MockProcessManager(); + mockCache = MockCache(); + mockArtifacts = MockArtifacts(); + when(mockArtifacts.getArtifactPath(Artifact.ideviceId, platform: anyNamed('platform'))).thenReturn(ideviceIdPath); + when(mockCache.dyLdLibEntry).thenReturn( + MapEntry('DYLD_LIBRARY_PATH', libimobiledevicePath) + ); + }); + + testUsingContext('isWorking returns false if libimobiledevice is not installed', () async { + when(mockProcessManager.runSync( + [ideviceIdPath, '-h'], environment: anyNamed('environment') + )).thenReturn(ProcessResult(123, 1, '', '')); + expect(await iMobileDevice.isWorking, false); + }, overrides: { + ProcessManager: () => mockProcessManager, + Artifacts: () => mockArtifacts, }); testUsingContext('getAvailableDeviceIDs throws ToolExit when libimobiledevice is not installed', () async { - when(mockProcessManager.run(['idevice_id', '-l'])) - .thenThrow(const ProcessException('idevice_id', ['-l'])); + when(mockProcessManager.run( + [ideviceIdPath, '-l'], + environment: {'DYLD_LIBRARY_PATH': libimobiledevicePath}, + )).thenThrow(ProcessException(ideviceIdPath, ['-l'])); expect(() async => await iMobileDevice.getAvailableDeviceIDs(), throwsToolExit()); }, overrides: { ProcessManager: () => mockProcessManager, + Cache: () => mockCache, + Artifacts: () => mockArtifacts, }); testUsingContext('getAvailableDeviceIDs throws ToolExit when idevice_id returns non-zero', () async { - when(mockProcessManager.run(['idevice_id', '-l'])) - .thenAnswer((_) => Future.value(ProcessResult(1, 1, '', 'Sad today'))); + when(mockProcessManager.run( + [ideviceIdPath, '-l'], + environment: {'DYLD_LIBRARY_PATH': libimobiledevicePath}, + )).thenAnswer((_) => Future.value(ProcessResult(1, 1, '', 'Sad today'))); expect(() async => await iMobileDevice.getAvailableDeviceIDs(), throwsToolExit()); }, overrides: { ProcessManager: () => mockProcessManager, + Cache: () => mockCache, + Artifacts: () => mockArtifacts, }); testUsingContext('getAvailableDeviceIDs returns idevice_id output when installed', () async { - when(mockProcessManager.run(['idevice_id', '-l'])) - .thenAnswer((_) => Future.value(ProcessResult(1, 0, 'foo', ''))); + when(mockProcessManager.run( + [ideviceIdPath, '-l'], + environment: {'DYLD_LIBRARY_PATH': libimobiledevicePath}, + )).thenAnswer((_) => Future.value(ProcessResult(1, 0, 'foo', ''))); expect(await iMobileDevice.getAvailableDeviceIDs(), 'foo'); }, overrides: { ProcessManager: () => mockProcessManager, + Cache: () => mockCache, + Artifacts: () => mockArtifacts, }); testUsingContext('getInfoForDevice throws IOSDeviceNotFoundError when ideviceinfo returns specific error code and message', () async { - when(mockProcessManager.run(['ideviceinfo', '-u', 'foo', '-k', 'bar'])) - .thenAnswer((_) => Future.value(ProcessResult(1, 255, 'No device found with udid foo, is it plugged in?', ''))); + when(mockArtifacts.getArtifactPath(Artifact.ideviceinfo, platform: anyNamed('platform'))).thenReturn(ideviceInfoPath); + when(mockProcessManager.run( + [ideviceInfoPath, '-u', 'foo', '-k', 'bar'], + environment: {'DYLD_LIBRARY_PATH': libimobiledevicePath}, + )).thenAnswer((_) => Future.value(ProcessResult(1, 255, 'No device found with udid foo, is it plugged in?', ''))); expect(() async => await iMobileDevice.getInfoForDevice('foo', 'bar'), throwsA(isInstanceOf())); }, overrides: { ProcessManager: () => mockProcessManager, + Cache: () => mockCache, + Artifacts: () => mockArtifacts, }); group('screenshot', () { @@ -77,14 +120,15 @@ void main() { setUp(() { mockProcessManager = MockProcessManager(); mockOutputFile = MockFile(); + when(mockArtifacts.getArtifactPath(Artifact.idevicescreenshot, platform: anyNamed('platform'))).thenReturn(idevicescreenshotPath); }); testUsingContext('error if idevicescreenshot is not installed', () async { when(mockOutputFile.path).thenReturn(outputPath); // Let `idevicescreenshot` fail with exit code 1. - when(mockProcessManager.run(['idevicescreenshot', outputPath], - environment: null, + when(mockProcessManager.run([idevicescreenshotPath, outputPath], + environment: {'DYLD_LIBRARY_PATH': libimobiledevicePath}, workingDirectory: null, )).thenAnswer((_) => Future.value(ProcessResult(4, 1, '', ''))); @@ -92,20 +136,23 @@ void main() { }, overrides: { ProcessManager: () => mockProcessManager, Platform: () => osx, + Cache: () => mockCache, }); testUsingContext('idevicescreenshot captures and returns screenshot', () async { when(mockOutputFile.path).thenReturn(outputPath); - when(mockProcessManager.run(any, environment: null, workingDirectory: null)).thenAnswer( + when(mockProcessManager.run(any, environment: anyNamed('environment'), workingDirectory: null)).thenAnswer( (Invocation invocation) => Future.value(ProcessResult(4, 0, '', ''))); await iMobileDevice.takeScreenshot(mockOutputFile); - verify(mockProcessManager.run(['idevicescreenshot', outputPath], - environment: null, + verify(mockProcessManager.run([idevicescreenshotPath, outputPath], + environment: {'DYLD_LIBRARY_PATH': libimobiledevicePath}, workingDirectory: null, )); }, overrides: { ProcessManager: () => mockProcessManager, + Cache: () => mockCache, + Artifacts: () => mockArtifacts, }); }); }); diff --git a/packages/flutter_tools/test/general.shard/macos/cocoapods_validator_test.dart b/packages/flutter_tools/test/general.shard/macos/cocoapods_validator_test.dart index 2dc88945b4..59c838ac84 100644 --- a/packages/flutter_tools/test/general.shard/macos/cocoapods_validator_test.dart +++ b/packages/flutter_tools/test/general.shard/macos/cocoapods_validator_test.dart @@ -23,7 +23,7 @@ void main() { }); testUsingContext('Emits installed status when CocoaPods is installed', () async { - final CocoaPodsTestTarget workflow = CocoaPodsTestTarget(); + const CocoaPodsValidator workflow = CocoaPodsValidator(); final ValidationResult result = await workflow.validate(); expect(result.type, ValidationType.installed); }, overrides: { @@ -33,7 +33,7 @@ void main() { testUsingContext('Emits missing status when CocoaPods is not installed', () async { when(cocoaPods.evaluateCocoaPodsInstallation) .thenAnswer((_) async => CocoaPodsStatus.notInstalled); - final CocoaPodsTestTarget workflow = CocoaPodsTestTarget(); + const CocoaPodsValidator workflow = CocoaPodsValidator(); final ValidationResult result = await workflow.validate(); expect(result.type, ValidationType.missing); }, overrides: { @@ -43,7 +43,7 @@ void main() { testUsingContext('Emits partial status when CocoaPods is installed with unknown version', () async { when(cocoaPods.evaluateCocoaPodsInstallation) .thenAnswer((_) async => CocoaPodsStatus.unknownVersion); - final CocoaPodsTestTarget workflow = CocoaPodsTestTarget(); + const CocoaPodsValidator workflow = CocoaPodsValidator(); final ValidationResult result = await workflow.validate(); expect(result.type, ValidationType.partial); }, overrides: { @@ -52,7 +52,7 @@ void main() { testUsingContext('Emits partial status when CocoaPods is not initialized', () async { when(cocoaPods.isCocoaPodsInitialized).thenAnswer((_) async => false); - final CocoaPodsTestTarget workflow = CocoaPodsTestTarget(); + const CocoaPodsValidator workflow = CocoaPodsValidator(); final ValidationResult result = await workflow.validate(); expect(result.type, ValidationType.partial); }, overrides: { @@ -62,30 +62,13 @@ void main() { testUsingContext('Emits partial status when CocoaPods version is too low', () async { when(cocoaPods.evaluateCocoaPodsInstallation) .thenAnswer((_) async => CocoaPodsStatus.belowRecommendedVersion); - final CocoaPodsTestTarget workflow = CocoaPodsTestTarget(); + const CocoaPodsValidator workflow = CocoaPodsValidator(); final ValidationResult result = await workflow.validate(); expect(result.type, ValidationType.partial); }, overrides: { CocoaPods: () => cocoaPods, }); - - testUsingContext('Emits installed status when homebrew not installed, but not needed', () async { - final CocoaPodsTestTarget workflow = CocoaPodsTestTarget(hasHomebrew: false); - final ValidationResult result = await workflow.validate(); - expect(result.type, ValidationType.installed); - }, overrides: { - CocoaPods: () => cocoaPods, - }); }); } class MockCocoaPods extends Mock implements CocoaPods {} - -class CocoaPodsTestTarget extends CocoaPodsValidator { - CocoaPodsTestTarget({ - this.hasHomebrew = true, - }); - - @override - final bool hasHomebrew; -}