feat(shorebird_cli): shorebird patch ios-alpha generates link file (#1511)

This commit is contained in:
Felix Angelov
2023-11-20 15:27:34 -06:00
committed by GitHub
parent 4c9c1d77ff
commit c3dc6f5e2c
7 changed files with 271 additions and 9 deletions
@@ -28,6 +28,7 @@ Future<void> main(List<String> args) async {
adbRef,
androidSdkRef,
androidStudioRef,
aotToolsRef,
artifactManagerRef,
authRef,
bundletoolRef,
@@ -11,6 +11,7 @@ import 'package:shorebird_cli/src/command.dart';
import 'package:shorebird_cli/src/config/config.dart';
import 'package:shorebird_cli/src/deployment_track.dart';
import 'package:shorebird_cli/src/doctor.dart';
import 'package:shorebird_cli/src/executables/executables.dart';
import 'package:shorebird_cli/src/formatters/file_size_formatter.dart';
import 'package:shorebird_cli/src/ios.dart';
import 'package:shorebird_cli/src/logger.dart';
@@ -220,6 +221,56 @@ Current Flutter Revision: $originalFlutterRevision
return ExitCode.software.code;
}
final appDirectory = getAppDirectory();
if (appDirectory == null) {
logger.err('Unable to find .app directory within .xcarchive.');
return ExitCode.software.code;
}
final base = File(
p.join(
appDirectory.path,
'Frameworks',
'App.framework',
'App',
),
);
if (!base.existsSync()) {
logger.err('Unable to find base AOT file at ${base.path}');
return ExitCode.software.code;
}
final patch = File(_aotOutputPath);
if (!patch.existsSync()) {
logger.err('Unable to find patch AOT file at ${patch.path}');
return ExitCode.software.code;
}
final analyzeSnapshot = shorebirdEnv.analyzeSnapshotFile;
if (!analyzeSnapshot.existsSync()) {
logger.err('Unable to find analyze_snapshot at ${analyzeSnapshot.path}');
return ExitCode.software.code;
}
final linkProgress = logger.progress('Linking AOT files');
try {
await aotTools.link(
base: base.path,
patch: patch.path,
analyzeSnapshot: analyzeSnapshot.path,
workingDirectory: _buildDirectory,
);
} catch (error) {
linkProgress.fail('Failed to link AOT files: $error');
return ExitCode.software.code;
}
linkProgress.complete();
if (dryRun) {
logger
..info('No issues detected.')
@@ -227,14 +278,14 @@ Current Flutter Revision: $originalFlutterRevision
return ExitCode.success.code;
}
final aotFile = File(_aotOutputPath);
final aotFileSize = aotFile.statSync().size;
final patchFile = File(_vmcodeOutputPath);
final patchFileSize = patchFile.statSync().size;
final summary = [
'''📱 App: ${lightCyan.wrap(app.displayName)} ${lightCyan.wrap('($appId)')}''',
if (flavor != null) '🍧 Flavor: ${lightCyan.wrap(flavor)}',
'📦 Release Version: ${lightCyan.wrap(releaseVersion)}',
'''🕹️ Platform: ${lightCyan.wrap(releasePlatform.name)} ${lightCyan.wrap('[$arch (${formatBytes(aotFileSize)})]')}''',
'''🕹️ Platform: ${lightCyan.wrap(releasePlatform.name)} ${lightCyan.wrap('[$arch (${formatBytes(patchFileSize)})]')}''',
if (isStaging)
'🟠 Track: ${lightCyan.wrap('Staging')}'
else
@@ -268,9 +319,9 @@ ${summary.join('\n')}
patchArtifactBundles: {
Arch.arm64: PatchArtifactBundle(
arch: arch,
path: aotFile.path,
hash: _hashFn(aotFile.readAsBytesSync()),
size: aotFileSize,
path: patchFile.path,
hash: _hashFn(patchFile.readAsBytesSync()),
size: patchFileSize,
),
},
);
@@ -278,12 +329,21 @@ ${summary.join('\n')}
return ExitCode.success.code;
}
String get _aotOutputPath => p.join(
String get _buildDirectory => p.join(
shorebirdEnv.getShorebirdProjectRoot()!.path,
'build',
);
String get _aotOutputPath => p.join(
_buildDirectory,
'out.aot',
);
String get _vmcodeOutputPath => p.join(
_buildDirectory,
'out.vmcode',
);
Future<void> _buildPatch() async {
final target = results['target'] as String?;
final flavor = results['flavor'] as String?;
@@ -1,7 +1,14 @@
import 'package:path/path.dart' as p;
import 'package:scoped/scoped.dart';
import 'package:shorebird_cli/src/cache.dart';
import 'package:shorebird_cli/src/process.dart';
/// A reference to a [AotTools] instance.
final aotToolsRef = create(AotTools.new);
/// The [AotTools] instance available in the current zone.
AotTools get aotTools => read(aotToolsRef);
/// Wrapper around the shorebird `aot-tools` executable.
class AotTools {
static const executableName = 'aot-tools';
@@ -92,6 +92,20 @@ class ShorebirdEnv {
);
}
File get analyzeSnapshotFile {
return File(
p.join(
flutterDirectory.path,
'bin',
'cache',
'artifacts',
'engine',
'ios-release',
'analyze_snapshot_arm64',
),
);
}
/// The `shorebird.yaml` file for this project.
File getShorebirdYamlFile({required Directory cwd}) {
return File(p.join(cwd.path, 'shorebird.yaml'));
@@ -15,6 +15,7 @@ import 'package:shorebird_cli/src/commands/patch/patch.dart';
import 'package:shorebird_cli/src/config/config.dart';
import 'package:shorebird_cli/src/deployment_track.dart';
import 'package:shorebird_cli/src/doctor.dart';
import 'package:shorebird_cli/src/executables/aot_tools.dart';
import 'package:shorebird_cli/src/logger.dart';
import 'package:shorebird_cli/src/os/operating_system_interface.dart';
import 'package:shorebird_cli/src/patch_diff_checker.dart';
@@ -44,6 +45,7 @@ void main() {
const releasePlatform = ReleasePlatform.ios;
const platformName = 'ios';
const elfAotSnapshotFileName = 'out.aot';
const linkFileName = 'out.vmcode';
const ipaPath = 'build/ios/ipa/Runner.ipa';
const infoPlistContent = '''
<?xml version="1.0" encoding="UTF-8"?>
@@ -122,12 +124,14 @@ flutter:
group(PatchIosCommand, () {
late ArgResults argResults;
late AotTools aotTools;
late Auth auth;
late CodePushClientWrapper codePushClientWrapper;
late Directory flutterDirectory;
late Directory shorebirdRoot;
late Directory projectRoot;
late File genSnapshotFile;
late File analyzeSnapshotFile;
late Doctor doctor;
late IosArchiveDiffer archiveDiffer;
late Progress progress;
@@ -150,6 +154,7 @@ flutter:
return runScoped(
body,
values: {
aotToolsRef.overrideWith(() => aotTools),
authRef.overrideWith(() => auth),
codePushClientWrapperRef.overrideWith(() => codePushClientWrapper),
doctorRef.overrideWith(() => doctor),
@@ -184,6 +189,21 @@ flutter:
)
..createSync(recursive: true)
..writeAsStringSync(infoPlistContent);
File(
p.join(
projectRoot.path,
'build',
'ios',
'archive',
'Runner.xcarchive',
'Products',
'Applications',
'Runner.app',
'Frameworks',
'App.framework',
'App',
),
).createSync(recursive: true);
File(p.join(projectRoot.path, ipaPath)).createSync(recursive: true);
}
@@ -204,6 +224,9 @@ flutter:
File(
p.join(projectRoot.path, 'build', elfAotSnapshotFileName),
).createSync(recursive: true);
File(
p.join(projectRoot.path, 'build', linkFileName),
).createSync(recursive: true);
}
setUpAll(() {
@@ -219,6 +242,7 @@ flutter:
setUp(() {
argResults = MockArgResults();
aotTools = MockAotTools();
auth = MockAuth();
codePushClientWrapper = MockCodePushClientWrapper();
doctor = MockDoctor();
@@ -238,6 +262,18 @@ flutter:
'gen_snapshot_arm64',
),
);
analyzeSnapshotFile = File(
p.join(
flutterDirectory.path,
'bin',
'cache',
'artifacts',
'engine',
'android-arm-release',
'darwin-x64',
'analyze_snapshot',
),
)..createSync(recursive: true);
archiveDiffer = MockIosArchiveDiffer();
progress = MockProgress();
logger = MockLogger();
@@ -260,6 +296,14 @@ flutter:
when(() => argResults['codesign']).thenReturn(true);
when(() => argResults['staging']).thenReturn(false);
when(() => argResults.rest).thenReturn([]);
when(
() => aotTools.link(
base: any(named: 'base'),
patch: any(named: 'patch'),
analyzeSnapshot: any(named: 'analyzeSnapshot'),
workingDirectory: any(named: 'workingDirectory'),
),
).thenAnswer((_) async {});
when(() => auth.isAuthenticated).thenReturn(true);
when(() => auth.client).thenReturn(httpClient);
when(
@@ -292,8 +336,9 @@ flutter:
when(flutterValidator.validate).thenAnswer((_) async => []);
when(() => logger.confirm(any())).thenReturn(true);
when(() => logger.progress(any())).thenReturn(progress);
when(() => operatingSystemInterface.which('flutter'))
.thenReturn('/path/to/flutter');
when(
() => operatingSystemInterface.which('flutter'),
).thenReturn('/path/to/flutter');
when(() => platform.operatingSystem).thenReturn(Platform.macOS);
when(() => platform.environment).thenReturn({});
when(() => platform.script).thenReturn(shorebirdRoot.uri);
@@ -304,6 +349,9 @@ flutter:
).thenReturn(projectRoot);
when(() => shorebirdEnv.flutterDirectory).thenReturn(flutterDirectory);
when(() => shorebirdEnv.genSnapshotFile).thenReturn(genSnapshotFile);
when(
() => shorebirdEnv.analyzeSnapshotFile,
).thenReturn(analyzeSnapshotFile);
when(() => shorebirdEnv.flutterRevision).thenReturn(flutterRevision);
when(() => shorebirdEnv.isRunningOnCI).thenReturn(false);
when(() => shorebirdFlutter.useRevision(revision: any(named: 'revision')))
@@ -819,6 +867,113 @@ Please re-run the release command for this version or create a new release.'''),
);
});
test('exits with code 70 if appDirectory is not found', () async {
setUpProjectRoot();
setUpProjectRootArtifacts();
File(
p.join(
projectRoot.path,
'build',
'ios',
'archive',
'Runner.xcarchive',
'Products',
'Applications',
'Runner.app',
),
).deleteSync(recursive: true);
final exitCode = await runWithOverrides(command.run);
expect(exitCode, equals(ExitCode.software.code));
verify(
() => logger.err('Unable to find .app directory within .xcarchive.'),
).called(1);
});
test('exits with code 70 if base app is not found', () async {
setUpProjectRoot();
setUpProjectRootArtifacts();
final base = File(
p.join(
projectRoot.path,
'build',
'ios',
'archive',
'Runner.xcarchive',
'Products',
'Applications',
'Runner.app',
'Frameworks',
'App.framework',
'App',
),
)..deleteSync(recursive: true);
final exitCode = await runWithOverrides(command.run);
expect(exitCode, equals(ExitCode.software.code));
verify(
() => logger.err('Unable to find base AOT file at ${base.path}'),
).called(1);
});
test('exits with code 70 if patch AOT file is not found', () async {
setUpProjectRoot();
setUpProjectRootArtifacts();
final patch = File(
p.join(projectRoot.path, 'build', elfAotSnapshotFileName),
)..deleteSync(recursive: true);
final exitCode = await runWithOverrides(command.run);
expect(exitCode, equals(ExitCode.software.code));
verify(
() => logger.err('Unable to find patch AOT file at ${patch.path}'),
).called(1);
});
test('exits with code 70 if analyze snapshot is not found', () async {
setUpProjectRoot();
setUpProjectRootArtifacts();
analyzeSnapshotFile.deleteSync(recursive: true);
final exitCode = await runWithOverrides(command.run);
expect(exitCode, equals(ExitCode.software.code));
verify(
() => logger.err(
'Unable to find analyze_snapshot at ${analyzeSnapshotFile.path}',
),
).called(1);
});
test('exits with code 70 if linking fails', () async {
final exception = Exception('oops');
when(
() => aotTools.link(
base: any(named: 'base'),
patch: any(named: 'patch'),
analyzeSnapshot: any(named: 'analyzeSnapshot'),
workingDirectory: any(named: 'workingDirectory'),
),
).thenThrow(exception);
setUpProjectRoot();
setUpProjectRootArtifacts();
final exitCode = await runWithOverrides(command.run);
expect(exitCode, equals(ExitCode.software.code));
verify(
() => progress.fail('Failed to link AOT files: $exception'),
).called(1);
});
test('does not create patch on --dry-run', () async {
when(() => argResults['dry-run']).thenReturn(true);
setUpProjectRoot();
@@ -38,6 +38,8 @@ class MockAndroidSdk extends Mock implements AndroidSdk {}
class MockAndroidStudio extends Mock implements AndroidStudio {}
class MockAotTools extends Mock implements AotTools {}
class MockAppMetadata extends Mock implements AppMetadata {}
class MockAppleDevice extends Mock implements AppleDevice {}
@@ -184,6 +184,29 @@ void main() {
});
});
group('analyzeSnapshotFile', () {
test('returns correct path', () {
expect(
runWithOverrides(() => shorebirdEnv.analyzeSnapshotFile.path),
equals(
p.join(
shorebirdRoot.path,
'bin',
'cache',
'flutter',
flutterRevision,
'bin',
'cache',
'artifacts',
'engine',
'ios-release',
'analyze_snapshot_arm64',
),
),
);
});
});
group('getPubspecYamlFile', () {
test('returns correct file', () {
final tempDir = Directory.systemTemp.createTempSync();