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