Implement iOS app install deltas (#77756)
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user