Unverified Commit 08b225e0 authored by Jenn Magder's avatar Jenn Magder Committed by GitHub

Implement iOS app install deltas (#77756)

parent 047dcc70
...@@ -414,6 +414,10 @@ abstract class IOSApp extends ApplicationPackage { ...@@ -414,6 +414,10 @@ abstract class IOSApp extends ApplicationPackage {
String get simulatorBundlePath; String get simulatorBundlePath;
String get deviceBundlePath; String get deviceBundlePath;
/// Directory used by ios-deploy to store incremental installation metadata for
/// faster second installs.
Directory get appDeltaDirectory;
} }
class BuildableIOSApp extends IOSApp { class BuildableIOSApp extends IOSApp {
...@@ -440,6 +444,9 @@ class BuildableIOSApp extends IOSApp { ...@@ -440,6 +444,9 @@ class BuildableIOSApp extends IOSApp {
@override @override
String get deviceBundlePath => _buildAppPath('iphoneos'); String get deviceBundlePath => _buildAppPath('iphoneos');
@override
Directory get appDeltaDirectory => globals.fs.directory(globals.fs.path.join(getIosBuildDirectory(), 'app-delta'));
// Xcode uses this path for the final archive bundle location, // Xcode uses this path for the final archive bundle location,
// not a top-level output directory. // not a top-level output directory.
// Specifying `build/ios/archive/Runner` will result in `build/ios/archive/Runner.xcarchive`. // Specifying `build/ios/archive/Runner` will result in `build/ios/archive/Runner.xcarchive`.
...@@ -468,6 +475,9 @@ class PrebuiltIOSApp extends IOSApp { ...@@ -468,6 +475,9 @@ class PrebuiltIOSApp extends IOSApp {
final Directory bundleDir; final Directory bundleDir;
final String bundleName; final String bundleName;
@override
final Directory appDeltaDirectory = null;
@override @override
String get name => bundleName; String get name => bundleName;
......
...@@ -258,6 +258,7 @@ class IOSDevice extends Device { ...@@ -258,6 +258,7 @@ class IOSDevice extends Device {
installationResult = await _iosDeploy.installApp( installationResult = await _iosDeploy.installApp(
deviceId: id, deviceId: id,
bundlePath: bundle.path, bundlePath: bundle.path,
appDeltaDirectory: app.appDeltaDirectory,
launchArguments: <String>[], launchArguments: <String>[],
interfaceType: interfaceType, interfaceType: interfaceType,
); );
...@@ -384,6 +385,7 @@ class IOSDevice extends Device { ...@@ -384,6 +385,7 @@ class IOSDevice extends Device {
iosDeployDebugger = _iosDeploy.prepareDebuggerForLaunch( iosDeployDebugger = _iosDeploy.prepareDebuggerForLaunch(
deviceId: id, deviceId: id,
bundlePath: bundle.path, bundlePath: bundle.path,
appDeltaDirectory: package.appDeltaDirectory,
launchArguments: launchArguments, launchArguments: launchArguments,
interfaceType: interfaceType, interfaceType: interfaceType,
); );
...@@ -404,6 +406,7 @@ class IOSDevice extends Device { ...@@ -404,6 +406,7 @@ class IOSDevice extends Device {
installationResult = await _iosDeploy.launchApp( installationResult = await _iosDeploy.launchApp(
deviceId: id, deviceId: id,
bundlePath: bundle.path, bundlePath: bundle.path,
appDeltaDirectory: package.appDeltaDirectory,
launchArguments: launchArguments, launchArguments: launchArguments,
interfaceType: interfaceType, interfaceType: interfaceType,
); );
......
...@@ -11,6 +11,7 @@ import 'package:process/process.dart'; ...@@ -11,6 +11,7 @@ import 'package:process/process.dart';
import '../artifacts.dart'; import '../artifacts.dart';
import '../base/common.dart'; import '../base/common.dart';
import '../base/file_system.dart';
import '../base/io.dart'; import '../base/io.dart';
import '../base/logger.dart'; import '../base/logger.dart';
import '../base/platform.dart'; import '../base/platform.dart';
...@@ -89,15 +90,21 @@ class IOSDeploy { ...@@ -89,15 +90,21 @@ class IOSDeploy {
Future<int> installApp({ Future<int> installApp({
@required String deviceId, @required String deviceId,
@required String bundlePath, @required String bundlePath,
@required Directory appDeltaDirectory,
@required List<String>launchArguments, @required List<String>launchArguments,
@required IOSDeviceInterface interfaceType, @required IOSDeviceInterface interfaceType,
}) async { }) async {
appDeltaDirectory?.createSync(recursive: true);
final List<String> launchCommand = <String>[ final List<String> launchCommand = <String>[
_binaryPath, _binaryPath,
'--id', '--id',
deviceId, deviceId,
'--bundle', '--bundle',
bundlePath, bundlePath,
if (appDeltaDirectory != null) ...<String>[
'--app_deltas',
appDeltaDirectory.path,
],
if (interfaceType != IOSDeviceInterface.network) if (interfaceType != IOSDeviceInterface.network)
'--no-wifi', '--no-wifi',
if (launchArguments.isNotEmpty) ...<String>[ if (launchArguments.isNotEmpty) ...<String>[
...@@ -121,9 +128,11 @@ class IOSDeploy { ...@@ -121,9 +128,11 @@ class IOSDeploy {
IOSDeployDebugger prepareDebuggerForLaunch({ IOSDeployDebugger prepareDebuggerForLaunch({
@required String deviceId, @required String deviceId,
@required String bundlePath, @required String bundlePath,
@required Directory appDeltaDirectory,
@required List<String> launchArguments, @required List<String> launchArguments,
@required IOSDeviceInterface interfaceType, @required IOSDeviceInterface interfaceType,
}) { }) {
appDeltaDirectory?.createSync(recursive: true);
// Interactive debug session to support sending the lldb detach command. // Interactive debug session to support sending the lldb detach command.
final List<String> launchCommand = <String>[ final List<String> launchCommand = <String>[
'script', 'script',
...@@ -135,6 +144,10 @@ class IOSDeploy { ...@@ -135,6 +144,10 @@ class IOSDeploy {
deviceId, deviceId,
'--bundle', '--bundle',
bundlePath, bundlePath,
if (appDeltaDirectory != null) ...<String>[
'--app_deltas',
appDeltaDirectory.path,
],
'--debug', '--debug',
if (interfaceType != IOSDeviceInterface.network) if (interfaceType != IOSDeviceInterface.network)
'--no-wifi', '--no-wifi',
...@@ -157,15 +170,21 @@ class IOSDeploy { ...@@ -157,15 +170,21 @@ class IOSDeploy {
Future<int> launchApp({ Future<int> launchApp({
@required String deviceId, @required String deviceId,
@required String bundlePath, @required String bundlePath,
@required Directory appDeltaDirectory,
@required List<String> launchArguments, @required List<String> launchArguments,
@required IOSDeviceInterface interfaceType, @required IOSDeviceInterface interfaceType,
}) async { }) async {
appDeltaDirectory?.createSync(recursive: true);
final List<String> launchCommand = <String>[ final List<String> launchCommand = <String>[
_binaryPath, _binaryPath,
'--id', '--id',
deviceId, deviceId,
'--bundle', '--bundle',
bundlePath, bundlePath,
if (appDeltaDirectory != null) ...<String>[
'--app_deltas',
appDeltaDirectory.path,
],
if (interfaceType != IOSDeviceInterface.network) if (interfaceType != IOSDeviceInterface.network)
'--no-wifi', '--no-wifi',
'--justlaunch', '--justlaunch',
......
...@@ -426,18 +426,24 @@ Future<XcodeBuildResult> buildXcodeProject({ ...@@ -426,18 +426,24 @@ Future<XcodeBuildResult> buildXcodeProject({
targetBuildDir, targetBuildDir,
buildSettings['WRAPPER_NAME'], buildSettings['WRAPPER_NAME'],
); );
if (globals.fs.isDirectorySync(expectedOutputDirectory)) { if (globals.fs.directory(expectedOutputDirectory).existsSync()) {
// Copy app folder to a place where other tools can find it without knowing // Copy app folder to a place where other tools can find it without knowing
// the BuildInfo. // the BuildInfo.
outputDir = expectedOutputDirectory.replaceFirst('/$configuration-', '/'); outputDir = targetBuildDir.replaceFirst('/$configuration-', '/');
if (globals.fs.isDirectorySync(outputDir)) { globals.fs.directory(outputDir).createSync(recursive: true);
// Previous output directory might have incompatible artifacts
// (for example, kernel binary files produced from previous run). // rsync instead of copy to maintain timestamps to support incremental
globals.fs.directory(outputDir).deleteSync(recursive: true); // app install deltas. Use --delete to remove incompatible artifacts
} // (for example, kernel binary files produced from previous run).
copyDirectory( await globals.processUtils.run(
globals.fs.directory(expectedOutputDirectory), <String>[
globals.fs.directory(outputDir), 'rsync',
'-av',
'--delete',
expectedOutputDirectory,
outputDir,
],
throwOnError: true,
); );
} else { } else {
globals.printError('Build succeeded but the expected app at $expectedOutputDirectory not found'); globals.printError('Build succeeded but the expected app at $expectedOutputDirectory not found');
......
...@@ -7,6 +7,8 @@ ...@@ -7,6 +7,8 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:file/memory.dart';
import 'package:file_testing/file_testing.dart';
import 'package:flutter_tools/src/artifacts.dart'; import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/logger.dart';
...@@ -23,10 +25,12 @@ import '../../src/fakes.dart'; ...@@ -23,10 +25,12 @@ import '../../src/fakes.dart';
void main () { void main () {
Artifacts artifacts; Artifacts artifacts;
String iosDeployPath; String iosDeployPath;
FileSystem fileSystem;
setUp(() { setUp(() {
artifacts = Artifacts.test(); artifacts = Artifacts.test();
iosDeployPath = artifacts.getArtifactPath(Artifact.iosDeploy, platform: TargetPlatform.ios); iosDeployPath = artifacts.getArtifactPath(Artifact.iosDeploy, platform: TargetPlatform.ios);
fileSystem = MemoryFileSystem.test();
}); });
testWithoutContext('IOSDeploy.iosDeployEnv returns path with /usr/bin first', () { testWithoutContext('IOSDeploy.iosDeployEnv returns path with /usr/bin first', () {
...@@ -50,6 +54,8 @@ void main () { ...@@ -50,6 +54,8 @@ void main () {
'123', '123',
'--bundle', '--bundle',
'/', '/',
'--app_deltas',
'app-delta',
'--debug', '--debug',
'--args', '--args',
<String>[ <String>[
...@@ -62,10 +68,12 @@ void main () { ...@@ -62,10 +68,12 @@ void main () {
stdout: '(lldb) run\nsuccess\nDid finish launching.', stdout: '(lldb) run\nsuccess\nDid finish launching.',
), ),
]); ]);
final Directory appDeltaDirectory = fileSystem.directory('app-delta');
final IOSDeploy iosDeploy = setUpIOSDeploy(processManager, artifacts: artifacts); final IOSDeploy iosDeploy = setUpIOSDeploy(processManager, artifacts: artifacts);
final IOSDeployDebugger iosDeployDebugger = iosDeploy.prepareDebuggerForLaunch( final IOSDeployDebugger iosDeployDebugger = iosDeploy.prepareDebuggerForLaunch(
deviceId: '123', deviceId: '123',
bundlePath: '/', bundlePath: '/',
appDeltaDirectory: appDeltaDirectory,
launchArguments: <String>['--enable-dart-profiling'], launchArguments: <String>['--enable-dart-profiling'],
interfaceType: IOSDeviceInterface.network, interfaceType: IOSDeviceInterface.network,
); );
...@@ -73,6 +81,7 @@ void main () { ...@@ -73,6 +81,7 @@ void main () {
expect(await iosDeployDebugger.launchAndAttach(), isTrue); expect(await iosDeployDebugger.launchAndAttach(), isTrue);
expect(await iosDeployDebugger.logLines.toList(), <String>['Did finish launching.']); expect(await iosDeployDebugger.logLines.toList(), <String>['Did finish launching.']);
expect(processManager.hasRemainingExpectations, false); expect(processManager.hasRemainingExpectations, false);
expect(appDeltaDirectory, exists);
}); });
}); });
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
// @dart = 2.8 // @dart = 2.8
import 'package:file/memory.dart'; import 'package:file/memory.dart';
import 'package:file_testing/file_testing.dart';
import 'package:flutter_tools/src/application_package.dart'; import 'package:flutter_tools/src/application_package.dart';
import 'package:flutter_tools/src/artifacts.dart'; import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/file_system.dart';
...@@ -119,10 +120,22 @@ void main() { ...@@ -119,10 +120,22 @@ void main() {
setUpIOSProject(fileSystem); setUpIOSProject(fileSystem);
final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory); final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app'); final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app');
fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true);
processManager.addCommand(FakeCommand(command: _xattrArgs(flutterProject))); processManager.addCommand(FakeCommand(command: _xattrArgs(flutterProject)));
processManager.addCommand(const FakeCommand(command: kRunReleaseArgs)); processManager.addCommand(const FakeCommand(command: kRunReleaseArgs));
processManager.addCommand(const FakeCommand(command: <String>[...kRunReleaseArgs, '-showBuildSettings'])); processManager.addCommand(const FakeCommand(command: <String>[...kRunReleaseArgs, '-showBuildSettings'], stdout: r'''
TARGET_BUILD_DIR=build/ios/Release-iphoneos
WRAPPER_NAME=My Super Awesome App.app
'''
));
processManager.addCommand(const FakeCommand(command: <String>[
'rsync',
'-av',
'--delete',
'build/ios/Release-iphoneos/My Super Awesome App.app',
'build/ios/iphoneos',
]));
processManager.addCommand(FakeCommand( processManager.addCommand(FakeCommand(
command: <String>[ command: <String>[
iosDeployPath, iosDeployPath,
...@@ -130,6 +143,8 @@ void main() { ...@@ -130,6 +143,8 @@ void main() {
'123', '123',
'--bundle', '--bundle',
'build/ios/iphoneos/My Super Awesome App.app', 'build/ios/iphoneos/My Super Awesome App.app',
'--app_deltas',
'build/ios/app-delta',
'--no-wifi', '--no-wifi',
'--justlaunch', '--justlaunch',
'--args', '--args',
...@@ -146,6 +161,7 @@ void main() { ...@@ -146,6 +161,7 @@ void main() {
platformArgs: <String, Object>{}, platformArgs: <String, Object>{},
); );
expect(fileSystem.directory('build/ios/iphoneos'), exists);
expect(launchResult.started, true); expect(launchResult.started, true);
expect(processManager.hasRemainingExpectations, false); expect(processManager.hasRemainingExpectations, false);
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
...@@ -169,6 +185,7 @@ void main() { ...@@ -169,6 +185,7 @@ void main() {
setUpIOSProject(fileSystem); setUpIOSProject(fileSystem);
final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory); final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app'); final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app');
fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true);
processManager.addCommand(FakeCommand(command: _xattrArgs(flutterProject))); processManager.addCommand(FakeCommand(command: _xattrArgs(flutterProject)));
processManager.addCommand(const FakeCommand(command: kRunReleaseArgs)); processManager.addCommand(const FakeCommand(command: kRunReleaseArgs));
...@@ -183,7 +200,18 @@ void main() { ...@@ -183,7 +200,18 @@ void main() {
const FakeCommand( const FakeCommand(
command: <String>[...kRunReleaseArgs, '-showBuildSettings'], command: <String>[...kRunReleaseArgs, '-showBuildSettings'],
exitCode: 0, exitCode: 0,
stdout: r'''
TARGET_BUILD_DIR=build/ios/Release-iphoneos
WRAPPER_NAME=My Super Awesome App.app
'''
)); ));
processManager.addCommand(const FakeCommand(command: <String>[
'rsync',
'-av',
'--delete',
'build/ios/Release-iphoneos/My Super Awesome App.app',
'build/ios/iphoneos',
]));
processManager.addCommand(FakeCommand( processManager.addCommand(FakeCommand(
command: <String>[ command: <String>[
iosDeployPath, iosDeployPath,
...@@ -191,6 +219,8 @@ void main() { ...@@ -191,6 +219,8 @@ void main() {
'123', '123',
'--bundle', '--bundle',
'build/ios/iphoneos/My Super Awesome App.app', 'build/ios/iphoneos/My Super Awesome App.app',
'--app_deltas',
'build/ios/app-delta',
'--no-wifi', '--no-wifi',
'--justlaunch', '--justlaunch',
'--args', '--args',
...@@ -221,6 +251,7 @@ void main() { ...@@ -221,6 +251,7 @@ void main() {
}); });
expect(launchResult?.started, true); expect(launchResult?.started, true);
expect(fileSystem.directory('build/ios/iphoneos'), exists);
expect(processManager.hasRemainingExpectations, false); expect(processManager.hasRemainingExpectations, false);
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
ProcessManager: () => processManager, ProcessManager: () => processManager,
......
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