refactor(shorebird_cli): Add IosFrameworkPatcher (#2036)
This commit is contained in:
@@ -6,6 +6,7 @@ import 'package:shorebird_cli/src/logger.dart';
|
||||
import 'package:shorebird_cli/src/os/operating_system_interface.dart';
|
||||
import 'package:shorebird_cli/src/platform/platform.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_android_artifacts.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_artifacts.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_env.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_process.dart';
|
||||
|
||||
@@ -321,4 +322,33 @@ Either run `flutter pub get` manually, or follow the steps in ${link(uri: Uri.pa
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an AOT snapshot of the given [appDillPath] at [outFilePath] and
|
||||
/// returns the resulting file.
|
||||
Future<File> buildElfAotSnapshot({
|
||||
required String appDillPath,
|
||||
required String outFilePath,
|
||||
}) async {
|
||||
final arguments = [
|
||||
'--deterministic',
|
||||
'--snapshot-kind=app-aot-elf',
|
||||
'--elf=$outFilePath',
|
||||
appDillPath,
|
||||
];
|
||||
|
||||
final result = await process.run(
|
||||
shorebirdArtifacts.getArtifactPath(
|
||||
artifact: ShorebirdArtifact.genSnapshot,
|
||||
),
|
||||
arguments,
|
||||
);
|
||||
|
||||
if (result.exitCode != ExitCode.success.code) {
|
||||
throw ArtifactBuildException(
|
||||
'Failed to create snapshot: ${result.stderr}',
|
||||
);
|
||||
}
|
||||
|
||||
return File(outFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,4 +270,27 @@ class ArtifactManager {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Finds the most recently-edited app.dill file in the .dart_tool directory.
|
||||
// TODO(bryanoltman): This is an enormous hack – we don't know that this is
|
||||
// the correct file.
|
||||
File newestAppDill() {
|
||||
final projectRoot = shorebirdEnv.getShorebirdProjectRoot()!;
|
||||
final dartToolBuildDir = Directory(
|
||||
p.join(
|
||||
projectRoot.path,
|
||||
'.dart_tool',
|
||||
'flutter_build',
|
||||
),
|
||||
);
|
||||
|
||||
return dartToolBuildDir
|
||||
.listSync(recursive: true)
|
||||
.whereType<File>()
|
||||
.where((f) => p.basename(f.path) == 'app.dill')
|
||||
.reduce(
|
||||
(a, b) =>
|
||||
a.statSync().modified.isAfter(b.statSync().modified) ? a : b,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:mason_logger/mason_logger.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:platform/platform.dart';
|
||||
import 'package:shorebird_cli/src/archive/directory_archive.dart';
|
||||
import 'package:shorebird_cli/src/archive_analysis/archive_differ.dart';
|
||||
import 'package:shorebird_cli/src/archive_analysis/ios_archive_differ.dart';
|
||||
import 'package:shorebird_cli/src/artifact_builder.dart';
|
||||
import 'package:shorebird_cli/src/artifact_manager.dart';
|
||||
import 'package:shorebird_cli/src/code_push_client_wrapper.dart';
|
||||
import 'package:shorebird_cli/src/commands/patch_new/patch_new.dart';
|
||||
import 'package:shorebird_cli/src/doctor.dart';
|
||||
import 'package:shorebird_cli/src/executables/aot_tools.dart';
|
||||
import 'package:shorebird_cli/src/executables/xcodebuild.dart';
|
||||
import 'package:shorebird_cli/src/logger.dart';
|
||||
import 'package:shorebird_cli/src/patch_diff_checker.dart';
|
||||
import 'package:shorebird_cli/src/platform.dart';
|
||||
import 'package:shorebird_cli/src/platform/platform.dart';
|
||||
import 'package:shorebird_cli/src/release_type.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_artifacts.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_env.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_flutter.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_validator.dart';
|
||||
import 'package:shorebird_cli/src/third_party/flutter_tools/lib/flutter_tools.dart';
|
||||
import 'package:shorebird_cli/src/version.dart';
|
||||
import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';
|
||||
|
||||
/// {@template ios_framework_patcher}
|
||||
/// Functions to patch an iOS Framework release.
|
||||
/// {@endtemplate}
|
||||
class IosFrameworkPatcher extends Patcher {
|
||||
/// {@macro ios_framework_patcher}
|
||||
IosFrameworkPatcher({
|
||||
required super.argResults,
|
||||
required super.flavor,
|
||||
required super.target,
|
||||
});
|
||||
|
||||
String get _buildDirectory => p.join(
|
||||
shorebirdEnv.getShorebirdProjectRoot()!.path,
|
||||
'build',
|
||||
);
|
||||
|
||||
String get _vmcodeOutputPath => p.join(
|
||||
_buildDirectory,
|
||||
'out.vmcode',
|
||||
);
|
||||
|
||||
@override
|
||||
ArchiveDiffer get archiveDiffer => IosArchiveDiffer();
|
||||
|
||||
@override
|
||||
String get primaryReleaseArtifactArch => 'xcframework';
|
||||
|
||||
@override
|
||||
ReleaseType get releaseType => ReleaseType.iosFramework;
|
||||
|
||||
@visibleForTesting
|
||||
double? lastBuildLinkPercentage;
|
||||
|
||||
@override
|
||||
Future<void> assertPreconditions() async {
|
||||
try {
|
||||
await shorebirdValidator.validatePreconditions(
|
||||
checkUserIsAuthenticated: true,
|
||||
checkShorebirdInitialized: true,
|
||||
validators: doctor.iosCommandValidators,
|
||||
supportedOperatingSystems: {Platform.macOS},
|
||||
);
|
||||
} on PreconditionFailedException catch (e) {
|
||||
exit(e.exitCode.code);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> assertArgsAreValid() async {
|
||||
if (!argResults.wasParsed('release-version')) {
|
||||
logger.err('Missing required argument: --release-version');
|
||||
exit(ExitCode.usage.code);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<File> buildPatchArtifact() async {
|
||||
final flutterVersionString = await shorebirdFlutter.getVersionAndRevision();
|
||||
final buildProgress = logger.progress(
|
||||
'Building patch with Flutter $flutterVersionString',
|
||||
);
|
||||
try {
|
||||
await artifactBuilder.buildIosFramework();
|
||||
} on ArtifactBuildException catch (error) {
|
||||
buildProgress.fail(error.message);
|
||||
exit(ExitCode.software.code);
|
||||
}
|
||||
try {
|
||||
final newestDillFile = artifactManager.newestAppDill();
|
||||
await artifactBuilder.buildElfAotSnapshot(
|
||||
appDillPath: newestDillFile.path,
|
||||
outFilePath: p.join(
|
||||
shorebirdEnv.getShorebirdProjectRoot()!.path,
|
||||
'build',
|
||||
'out.aot',
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
buildProgress.fail('$error');
|
||||
exit(ExitCode.software.code);
|
||||
}
|
||||
|
||||
buildProgress.complete();
|
||||
|
||||
return Directory(
|
||||
p.join(
|
||||
artifactManager.getAppXcframeworkDirectory().path,
|
||||
ArtifactManager.appXcframeworkName,
|
||||
),
|
||||
).zipToTempFile();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<Arch, PatchArtifactBundle>> createPatchArtifacts({
|
||||
required String appId,
|
||||
required int releaseId,
|
||||
}) async {
|
||||
final releaseArtifact = await codePushClientWrapper.getReleaseArtifact(
|
||||
appId: appId,
|
||||
releaseId: releaseId,
|
||||
arch: 'xcframework',
|
||||
platform: ReleasePlatform.ios,
|
||||
);
|
||||
|
||||
final downloadProgress = logger.progress('Downloading release artifact');
|
||||
final File releaseArtifactZipFile;
|
||||
try {
|
||||
releaseArtifactZipFile = await artifactManager.downloadFile(
|
||||
Uri.parse(releaseArtifact.url),
|
||||
);
|
||||
} catch (error) {
|
||||
downloadProgress.fail('$error');
|
||||
exit(ExitCode.software.code);
|
||||
}
|
||||
downloadProgress.complete();
|
||||
|
||||
final unzipProgress = logger.progress('Extracting release artifact');
|
||||
final tempDir = Directory.systemTemp.createTempSync();
|
||||
await artifactManager.extractZip(
|
||||
zipFile: releaseArtifactZipFile,
|
||||
outputDirectory: tempDir,
|
||||
);
|
||||
final releaseXcframeworkPath = tempDir.path;
|
||||
|
||||
unzipProgress
|
||||
.complete('Extracted release artifact to $releaseXcframeworkPath');
|
||||
final releaseArtifactFile = File(
|
||||
p.join(
|
||||
releaseXcframeworkPath,
|
||||
'ios-arm64',
|
||||
'App.framework',
|
||||
'App',
|
||||
),
|
||||
);
|
||||
|
||||
final aotSnapshotFile = File(
|
||||
p.join(
|
||||
shorebirdEnv.getShorebirdProjectRoot()!.path,
|
||||
'build',
|
||||
'out.aot',
|
||||
),
|
||||
);
|
||||
final useLinker = AotTools.usesLinker(shorebirdEnv.flutterRevision);
|
||||
if (useLinker) {
|
||||
await _runLinker(
|
||||
aotSnapshot: aotSnapshotFile,
|
||||
releaseArtifact: releaseArtifactFile,
|
||||
);
|
||||
}
|
||||
|
||||
final patchBuildFile =
|
||||
useLinker ? File(_vmcodeOutputPath) : aotSnapshotFile;
|
||||
final File patchFile;
|
||||
if (await aotTools.isGeneratePatchDiffBaseSupported()) {
|
||||
final patchBaseProgress = logger.progress('Generating patch diff base');
|
||||
final analyzeSnapshotPath = shorebirdArtifacts.getArtifactPath(
|
||||
artifact: ShorebirdArtifact.analyzeSnapshot,
|
||||
);
|
||||
|
||||
final File patchBaseFile;
|
||||
try {
|
||||
// If the aot_tools executable supports the dump_blobs command, we
|
||||
// can generate a stable diff base and use that to create a patch.
|
||||
patchBaseFile = await aotTools.generatePatchDiffBase(
|
||||
analyzeSnapshotPath: analyzeSnapshotPath,
|
||||
releaseSnapshot: releaseArtifactFile,
|
||||
);
|
||||
patchBaseProgress.complete();
|
||||
} catch (error) {
|
||||
patchBaseProgress.fail('$error');
|
||||
exit(ExitCode.software.code);
|
||||
}
|
||||
|
||||
patchFile = File(
|
||||
await artifactManager.createDiff(
|
||||
releaseArtifactPath: patchBaseFile.path,
|
||||
patchArtifactPath: patchBuildFile.path,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
patchFile = patchBuildFile;
|
||||
}
|
||||
|
||||
return {
|
||||
Arch.arm64: PatchArtifactBundle(
|
||||
arch: 'aarch64',
|
||||
path: patchFile.path,
|
||||
hash: sha256.convert(patchBuildFile.readAsBytesSync()).toString(),
|
||||
size: patchFile.statSync().size,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> extractReleaseVersionFromArtifact(File artifact) {
|
||||
// Not implemented - release verison must be specified by the user.
|
||||
throw UnimplementedError(
|
||||
'Release version must be specified using --release-version.',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<CreatePatchMetadata> createPatchMetadata(DiffStatus diffStatus) async {
|
||||
return CreatePatchMetadata(
|
||||
releasePlatform: releaseType.releasePlatform,
|
||||
usedIgnoreAssetChangesFlag: allowAssetDiffs,
|
||||
hasAssetChanges: diffStatus.hasAssetChanges,
|
||||
usedIgnoreNativeChangesFlag: allowNativeDiffs,
|
||||
hasNativeChanges: diffStatus.hasNativeChanges,
|
||||
linkPercentage: lastBuildLinkPercentage,
|
||||
environment: BuildEnvironmentMetadata(
|
||||
operatingSystem: platform.operatingSystem,
|
||||
operatingSystemVersion: platform.operatingSystemVersion,
|
||||
shorebirdVersion: packageVersion,
|
||||
xcodeVersion: await xcodeBuild.version(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _runLinker({
|
||||
required File aotSnapshot,
|
||||
required File releaseArtifact,
|
||||
}) async {
|
||||
if (!aotSnapshot.existsSync()) {
|
||||
logger.err('Unable to find patch AOT file at ${aotSnapshot.path}');
|
||||
exit(ExitCode.software.code);
|
||||
}
|
||||
|
||||
final analyzeSnapshot = File(
|
||||
shorebirdArtifacts.getArtifactPath(
|
||||
artifact: ShorebirdArtifact.analyzeSnapshot,
|
||||
),
|
||||
);
|
||||
|
||||
if (!analyzeSnapshot.existsSync()) {
|
||||
logger.err('Unable to find analyze_snapshot at ${analyzeSnapshot.path}');
|
||||
exit(ExitCode.software.code);
|
||||
}
|
||||
|
||||
final genSnapshot = shorebirdArtifacts.getArtifactPath(
|
||||
artifact: ShorebirdArtifact.genSnapshot,
|
||||
);
|
||||
|
||||
final linkProgress = logger.progress('Linking AOT files');
|
||||
try {
|
||||
lastBuildLinkPercentage = await aotTools.link(
|
||||
base: releaseArtifact.path,
|
||||
patch: aotSnapshot.path,
|
||||
analyzeSnapshot: analyzeSnapshot.path,
|
||||
genSnapshot: genSnapshot,
|
||||
kernel: artifactManager.newestAppDill().path,
|
||||
outputPath: _vmcodeOutputPath,
|
||||
workingDirectory: _buildDirectory,
|
||||
);
|
||||
} catch (error) {
|
||||
linkProgress.fail('Failed to link AOT files: $error');
|
||||
exit(ExitCode.software.code);
|
||||
}
|
||||
|
||||
linkProgress.complete();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
export 'aar_patcher.dart';
|
||||
export 'android_patcher.dart';
|
||||
export 'ios_framework_patcher.dart';
|
||||
export 'patch_new_command.dart';
|
||||
export 'patcher.dart';
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:meta/meta.dart';
|
||||
import 'package:scoped/scoped.dart';
|
||||
import 'package:shorebird_cli/src/archive_analysis/archive_differ.dart';
|
||||
import 'package:shorebird_cli/src/artifact_manager.dart';
|
||||
import 'package:shorebird_cli/src/cache.dart';
|
||||
import 'package:shorebird_cli/src/code_push_client_wrapper.dart';
|
||||
import 'package:shorebird_cli/src/command.dart';
|
||||
import 'package:shorebird_cli/src/commands/patch_new/patch_new.dart';
|
||||
@@ -137,7 +138,11 @@ NOTE: this is ${styleBold.wrap('not')} recommended. Asset changes cannot be incl
|
||||
case ReleaseType.ios:
|
||||
throw UnimplementedError();
|
||||
case ReleaseType.iosFramework:
|
||||
throw UnimplementedError();
|
||||
return IosFrameworkPatcher(
|
||||
argResults: results,
|
||||
flavor: flavor,
|
||||
target: target,
|
||||
);
|
||||
case ReleaseType.aar:
|
||||
return AarPatcher(
|
||||
argResults: results,
|
||||
@@ -157,6 +162,8 @@ NOTE: this is ${styleBold.wrap('not')} recommended. Asset changes cannot be incl
|
||||
await patcher.assertPreconditions();
|
||||
await patcher.assertArgsAreValid();
|
||||
|
||||
await cache.updateAll();
|
||||
|
||||
final app = await codePushClientWrapper.getApp(appId: appId);
|
||||
|
||||
File? patchArtifact;
|
||||
@@ -186,6 +193,8 @@ NOTE: this is ${styleBold.wrap('not')} recommended. Asset changes cannot be incl
|
||||
|
||||
return await runScoped(
|
||||
() async {
|
||||
await cache.updateAll();
|
||||
|
||||
// Don't built the patch artifact twice with the same Flutter revision.
|
||||
if (lastBuiltFlutterRevision != release.flutterRevision) {
|
||||
patchArtifact = await patcher.buildPatchArtifact();
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'package:mason_logger/mason_logger.dart';
|
||||
import 'package:shorebird_cli/src/artifact_builder.dart';
|
||||
import 'package:shorebird_cli/src/code_push_client_wrapper.dart';
|
||||
import 'package:shorebird_cli/src/commands/release_new/releaser.dart';
|
||||
import 'package:shorebird_cli/src/commands/release_new/release_new.dart';
|
||||
import 'package:shorebird_cli/src/commands/release_new/releaser.dart';
|
||||
import 'package:shorebird_cli/src/doctor.dart';
|
||||
import 'package:shorebird_cli/src/logger.dart';
|
||||
import 'package:shorebird_cli/src/platform.dart';
|
||||
|
||||
@@ -5,12 +5,12 @@ import 'package:platform/platform.dart';
|
||||
import 'package:shorebird_cli/src/artifact_builder.dart';
|
||||
import 'package:shorebird_cli/src/artifact_manager.dart';
|
||||
import 'package:shorebird_cli/src/code_push_client_wrapper.dart';
|
||||
import 'package:shorebird_cli/src/release_type.dart';
|
||||
import 'package:shorebird_cli/src/commands/release_new/releaser.dart';
|
||||
import 'package:shorebird_cli/src/doctor.dart';
|
||||
import 'package:shorebird_cli/src/executables/xcodebuild.dart';
|
||||
import 'package:shorebird_cli/src/logger.dart';
|
||||
import 'package:shorebird_cli/src/platform.dart';
|
||||
import 'package:shorebird_cli/src/release_type.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_env.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_flutter.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_validator.dart';
|
||||
@@ -92,6 +92,20 @@ class IosFrameworkReleaser extends Releaser {
|
||||
targetLibraryDirectory.path,
|
||||
);
|
||||
|
||||
// Rename Flutter.xcframework to ShorebirdFlutter.xcframework to avoid
|
||||
// Xcode warning users about the .xcframework signature changing.
|
||||
Directory(
|
||||
p.join(
|
||||
targetLibraryDirectory.path,
|
||||
'Flutter.xcframework',
|
||||
),
|
||||
).renameSync(
|
||||
p.join(
|
||||
targetLibraryDirectory.path,
|
||||
'ShorebirdFlutter.xcframework',
|
||||
),
|
||||
);
|
||||
|
||||
return targetLibraryDirectory;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:async';
|
||||
import 'package:mason_logger/mason_logger.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:scoped/scoped.dart';
|
||||
import 'package:shorebird_cli/src/cache.dart';
|
||||
import 'package:shorebird_cli/src/code_push_client_wrapper.dart';
|
||||
import 'package:shorebird_cli/src/command.dart';
|
||||
import 'package:shorebird_cli/src/commands/release_new/release_new.dart';
|
||||
@@ -178,6 +179,8 @@ of the iOS app that is using this module.''',
|
||||
await releaser.assertPreconditions();
|
||||
await releaser.assertArgsAreValid();
|
||||
|
||||
await cache.updateAll();
|
||||
|
||||
// This command handles logging, we don't need to provide our own
|
||||
// progress, error logs, etc.
|
||||
final app = await codePushClientWrapper.getApp(appId: appId);
|
||||
@@ -193,6 +196,8 @@ of the iOS app that is using this module.''',
|
||||
);
|
||||
return await runScoped(
|
||||
() async {
|
||||
await cache.updateAll();
|
||||
|
||||
final releaseArtifact = await releaser.buildReleaseArtifacts();
|
||||
final releaseVersion = await releaser.getReleaseVersion(
|
||||
releaseArtifactRoot: releaseArtifact,
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:shorebird_cli/src/logger.dart';
|
||||
import 'package:shorebird_cli/src/os/operating_system_interface.dart';
|
||||
import 'package:shorebird_cli/src/platform/platform.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_android_artifacts.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_artifacts.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_env.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_process.dart';
|
||||
import 'package:test/test.dart';
|
||||
@@ -22,6 +23,7 @@ void main() {
|
||||
late ShorebirdLogger logger;
|
||||
late OperatingSystemInterface operatingSystemInterface;
|
||||
late ShorebirdAndroidArtifacts shorebirdAndroidArtifacts;
|
||||
late ShorebirdArtifacts shorebirdArtifacts;
|
||||
late ShorebirdEnv shorebirdEnv;
|
||||
late ShorebirdProcess shorebirdProcess;
|
||||
late ShorebirdProcessResult buildProcessResult;
|
||||
@@ -36,6 +38,7 @@ void main() {
|
||||
loggerRef.overrideWith(() => logger),
|
||||
osInterfaceRef.overrideWith(() => operatingSystemInterface),
|
||||
processRef.overrideWith(() => shorebirdProcess),
|
||||
shorebirdArtifactsRef.overrideWith(() => shorebirdArtifacts),
|
||||
shorebirdEnvRef.overrideWith(() => shorebirdEnv),
|
||||
shorebirdAndroidArtifactsRef
|
||||
.overrideWith(() => shorebirdAndroidArtifacts),
|
||||
@@ -55,6 +58,7 @@ void main() {
|
||||
operatingSystemInterface = MockOperatingSystemInterface();
|
||||
pubGetProcessResult = MockProcessResult();
|
||||
shorebirdAndroidArtifacts = MockShorebirdAndroidArtifacts();
|
||||
shorebirdArtifacts = MockShorebirdArtifacts();
|
||||
shorebirdEnv = MockShorebirdEnv();
|
||||
shorebirdProcess = MockShorebirdProcess();
|
||||
|
||||
@@ -772,6 +776,48 @@ Failed to build:
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('buildElfAotSnapshot', () {
|
||||
setUp(() {
|
||||
when(
|
||||
() => shorebirdArtifacts.getArtifactPath(
|
||||
artifact: ShorebirdArtifact.genSnapshot,
|
||||
),
|
||||
).thenReturn('gen_snapshot');
|
||||
});
|
||||
|
||||
group('when build fails', () {
|
||||
setUp(() {
|
||||
when(() => buildProcessResult.exitCode)
|
||||
.thenReturn(ExitCode.software.code);
|
||||
});
|
||||
|
||||
test('throws ArtifactBuildException', () {
|
||||
expect(
|
||||
() => runWithOverrides(
|
||||
() => builder.buildElfAotSnapshot(
|
||||
appDillPath: 'asdf',
|
||||
outFilePath: 'asdf',
|
||||
),
|
||||
),
|
||||
throwsA(isA<ArtifactBuildException>()),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('when build succeeds', () {
|
||||
test('returns outFile', () async {
|
||||
final outFile = await runWithOverrides(
|
||||
() => builder.buildElfAotSnapshot(
|
||||
appDillPath: '/app/dill/path',
|
||||
outFilePath: '/path/to/out',
|
||||
),
|
||||
);
|
||||
|
||||
expect(outFile.path, '/path/to/out');
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
testOn: 'mac-os',
|
||||
);
|
||||
|
||||
@@ -592,5 +592,30 @@ void main() {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('newestAppDill', () {
|
||||
late File appDill2;
|
||||
|
||||
setUp(() {
|
||||
final tempDir = Directory.systemTemp.createTempSync();
|
||||
when(() => shorebirdEnv.getShorebirdProjectRoot()).thenReturn(tempDir);
|
||||
final flutterBuildDir = Directory(
|
||||
p.join(tempDir.path, '.dart_tool', 'flutter_build'),
|
||||
)..createSync(recursive: true);
|
||||
File(p.join(flutterBuildDir.path, 'app1', 'app.dill'))
|
||||
..createSync(recursive: true)
|
||||
..setLastModified(DateTime.now().subtract(const Duration(days: 1)));
|
||||
appDill2 = File(p.join(flutterBuildDir.path, 'app2', 'app.dill'))
|
||||
..createSync(recursive: true)
|
||||
..setLastModified(DateTime.now());
|
||||
});
|
||||
|
||||
test('selects the most recently edited .app.dill file', () {
|
||||
final result = runWithOverrides(artifactManager.newestAppDill);
|
||||
|
||||
expect(result, isNotNull);
|
||||
expect(result.path, equals(appDill2.path));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,816 @@
|
||||
import 'package:args/args.dart';
|
||||
import 'package:mason_logger/mason_logger.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:platform/platform.dart';
|
||||
import 'package:scoped/scoped.dart';
|
||||
import 'package:shorebird_cli/src/archive_analysis/archive_analysis.dart';
|
||||
import 'package:shorebird_cli/src/artifact_builder.dart';
|
||||
import 'package:shorebird_cli/src/artifact_manager.dart';
|
||||
import 'package:shorebird_cli/src/code_push_client_wrapper.dart';
|
||||
import 'package:shorebird_cli/src/commands/patch_new/patch_new.dart';
|
||||
import 'package:shorebird_cli/src/doctor.dart';
|
||||
import 'package:shorebird_cli/src/engine_config.dart';
|
||||
import 'package:shorebird_cli/src/executables/aot_tools.dart';
|
||||
import 'package:shorebird_cli/src/executables/xcodebuild.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';
|
||||
import 'package:shorebird_cli/src/platform.dart';
|
||||
import 'package:shorebird_cli/src/platform/platform.dart';
|
||||
import 'package:shorebird_cli/src/release_type.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_artifacts.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_env.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_flutter.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_process.dart';
|
||||
import 'package:shorebird_cli/src/shorebird_validator.dart';
|
||||
import 'package:shorebird_cli/src/third_party/flutter_tools/lib/flutter_tools.dart';
|
||||
import 'package:shorebird_cli/src/validators/validators.dart';
|
||||
import 'package:shorebird_cli/src/version.dart';
|
||||
import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../../matchers.dart';
|
||||
import '../../mocks.dart';
|
||||
|
||||
void main() {
|
||||
group(
|
||||
IosFrameworkPatcher,
|
||||
() {
|
||||
late AotTools aotTools;
|
||||
late ArgResults argResults;
|
||||
late ArtifactBuilder artifactBuilder;
|
||||
late ArtifactManager artifactManager;
|
||||
late CodePushClientWrapper codePushClientWrapper;
|
||||
late Doctor doctor;
|
||||
late EngineConfig engineConfig;
|
||||
late Directory flutterDirectory;
|
||||
late Directory projectRoot;
|
||||
late ShorebirdLogger logger;
|
||||
late OperatingSystemInterface operatingSystemInterface;
|
||||
late Platform platform;
|
||||
late Progress progress;
|
||||
late ShorebirdArtifacts shorebirdArtifacts;
|
||||
late ShorebirdFlutterValidator flutterValidator;
|
||||
late ShorebirdProcess shorebirdProcess;
|
||||
late ShorebirdEnv shorebirdEnv;
|
||||
late ShorebirdFlutter shorebirdFlutter;
|
||||
late ShorebirdValidator shorebirdValidator;
|
||||
late XcodeBuild xcodeBuild;
|
||||
late IosFrameworkPatcher patcher;
|
||||
|
||||
R runWithOverrides<R>(R Function() body) {
|
||||
return runScoped(
|
||||
body,
|
||||
values: {
|
||||
aotToolsRef.overrideWith(() => aotTools),
|
||||
artifactBuilderRef.overrideWith(() => artifactBuilder),
|
||||
artifactManagerRef.overrideWith(() => artifactManager),
|
||||
codePushClientWrapperRef.overrideWith(() => codePushClientWrapper),
|
||||
doctorRef.overrideWith(() => doctor),
|
||||
engineConfigRef.overrideWith(() => engineConfig),
|
||||
loggerRef.overrideWith(() => logger),
|
||||
osInterfaceRef.overrideWith(() => operatingSystemInterface),
|
||||
platformRef.overrideWith(() => platform),
|
||||
processRef.overrideWith(() => shorebirdProcess),
|
||||
shorebirdArtifactsRef.overrideWith(() => shorebirdArtifacts),
|
||||
shorebirdEnvRef.overrideWith(() => shorebirdEnv),
|
||||
shorebirdFlutterRef.overrideWith(() => shorebirdFlutter),
|
||||
shorebirdValidatorRef.overrideWith(() => shorebirdValidator),
|
||||
xcodeBuildRef.overrideWith(() => xcodeBuild),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
setUpAll(() {
|
||||
registerFallbackValue(Directory(''));
|
||||
registerFallbackValue(File(''));
|
||||
registerFallbackValue(ReleasePlatform.ios);
|
||||
registerFallbackValue(Uri.parse('https://example.com'));
|
||||
setExitFunctionForTests();
|
||||
});
|
||||
|
||||
tearDownAll(restoreExitFunction);
|
||||
|
||||
setUp(() {
|
||||
aotTools = MockAotTools();
|
||||
argResults = MockArgResults();
|
||||
artifactBuilder = MockArtifactBuilder();
|
||||
artifactManager = MockArtifactManager();
|
||||
codePushClientWrapper = MockCodePushClientWrapper();
|
||||
doctor = MockDoctor();
|
||||
engineConfig = MockEngineConfig();
|
||||
operatingSystemInterface = MockOperatingSystemInterface();
|
||||
platform = MockPlatform();
|
||||
progress = MockProgress();
|
||||
projectRoot = Directory.systemTemp.createTempSync();
|
||||
logger = MockShorebirdLogger();
|
||||
shorebirdArtifacts = MockShorebirdArtifacts();
|
||||
shorebirdProcess = MockShorebirdProcess();
|
||||
shorebirdEnv = MockShorebirdEnv();
|
||||
flutterValidator = MockShorebirdFlutterValidator();
|
||||
shorebirdFlutter = MockShorebirdFlutter();
|
||||
shorebirdValidator = MockShorebirdValidator();
|
||||
xcodeBuild = MockXcodeBuild();
|
||||
|
||||
when(() => argResults['build-number']).thenReturn('1.0');
|
||||
|
||||
when(() => logger.progress(any())).thenReturn(progress);
|
||||
|
||||
when(
|
||||
() => shorebirdEnv.getShorebirdProjectRoot(),
|
||||
).thenReturn(projectRoot);
|
||||
|
||||
patcher = IosFrameworkPatcher(
|
||||
argResults: argResults,
|
||||
flavor: null,
|
||||
target: null,
|
||||
);
|
||||
});
|
||||
|
||||
group('archiveDiffer', () {
|
||||
test('is an IosArchiveDiffer', () {
|
||||
expect(patcher.archiveDiffer, isA<IosArchiveDiffer>());
|
||||
});
|
||||
});
|
||||
|
||||
group('primaryReleaseArtifactArch', () {
|
||||
test('is "xcframework"', () {
|
||||
expect(patcher.primaryReleaseArtifactArch, 'xcframework');
|
||||
});
|
||||
});
|
||||
|
||||
group('releaseType', () {
|
||||
test('is ReleaseType.iosFramework', () {
|
||||
expect(patcher.releaseType, ReleaseType.iosFramework);
|
||||
});
|
||||
});
|
||||
|
||||
group('assertPreconditions', () {
|
||||
setUp(() {
|
||||
when(() => doctor.iosCommandValidators)
|
||||
.thenReturn([flutterValidator]);
|
||||
});
|
||||
|
||||
group('when validation succeeds', () {
|
||||
setUp(() {
|
||||
when(
|
||||
() => shorebirdValidator.validatePreconditions(
|
||||
checkUserIsAuthenticated:
|
||||
any(named: 'checkUserIsAuthenticated'),
|
||||
checkShorebirdInitialized:
|
||||
any(named: 'checkShorebirdInitialized'),
|
||||
validators: any(named: 'validators'),
|
||||
supportedOperatingSystems:
|
||||
any(named: 'supportedOperatingSystems'),
|
||||
),
|
||||
).thenAnswer((_) async {});
|
||||
});
|
||||
|
||||
test('returns normally', () async {
|
||||
await expectLater(
|
||||
() => runWithOverrides(patcher.assertPreconditions),
|
||||
returnsNormally,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('when validation fails', () {
|
||||
setUp(() {
|
||||
final exception = ValidationFailedException();
|
||||
when(
|
||||
() => shorebirdValidator.validatePreconditions(
|
||||
checkUserIsAuthenticated:
|
||||
any(named: 'checkUserIsAuthenticated'),
|
||||
checkShorebirdInitialized:
|
||||
any(named: 'checkShorebirdInitialized'),
|
||||
validators: any(named: 'validators'),
|
||||
),
|
||||
).thenThrow(exception);
|
||||
});
|
||||
|
||||
test('exits with code 70', () async {
|
||||
final exception = ValidationFailedException();
|
||||
when(
|
||||
() => shorebirdValidator.validatePreconditions(
|
||||
checkUserIsAuthenticated:
|
||||
any(named: 'checkUserIsAuthenticated'),
|
||||
checkShorebirdInitialized:
|
||||
any(named: 'checkShorebirdInitialized'),
|
||||
validators: any(named: 'validators'),
|
||||
supportedOperatingSystems:
|
||||
any(named: 'supportedOperatingSystems'),
|
||||
),
|
||||
).thenThrow(exception);
|
||||
await expectLater(
|
||||
() => runWithOverrides(patcher.assertPreconditions),
|
||||
exitsWithCode(exception.exitCode),
|
||||
);
|
||||
verify(
|
||||
() => shorebirdValidator.validatePreconditions(
|
||||
checkUserIsAuthenticated: true,
|
||||
checkShorebirdInitialized: true,
|
||||
validators: [flutterValidator],
|
||||
supportedOperatingSystems: {Platform.macOS},
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('assertArgsAreValid', () {
|
||||
group('when release-version is not provided', () {
|
||||
setUp(() {
|
||||
when(() => argResults.wasParsed('release-version'))
|
||||
.thenReturn(false);
|
||||
});
|
||||
|
||||
test('exits with code 64', () async {
|
||||
await expectLater(
|
||||
() => runWithOverrides(patcher.assertArgsAreValid),
|
||||
exitsWithCode(ExitCode.usage),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('when arguments are valid', () {
|
||||
setUp(() {
|
||||
when(() => argResults.wasParsed('release-version'))
|
||||
.thenReturn(true);
|
||||
});
|
||||
|
||||
test('returns normally', () {
|
||||
expect(
|
||||
() => runWithOverrides(patcher.assertArgsAreValid),
|
||||
returnsNormally,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('buildPatchArtifact', () {
|
||||
const flutterVersionAndRevision = '3.10.6 (83305b5088)';
|
||||
|
||||
setUp(() {
|
||||
when(
|
||||
() => shorebirdFlutter.getVersionAndRevision(),
|
||||
).thenAnswer((_) async => flutterVersionAndRevision);
|
||||
});
|
||||
|
||||
group('when build fails', () {
|
||||
setUp(() {
|
||||
when(() => artifactBuilder.buildIosFramework()).thenThrow(
|
||||
ArtifactBuildException('Build failed'),
|
||||
);
|
||||
});
|
||||
|
||||
test('exits with code 70', () async {
|
||||
await expectLater(
|
||||
() => runWithOverrides(patcher.buildPatchArtifact),
|
||||
exitsWithCode(ExitCode.software),
|
||||
);
|
||||
|
||||
verify(() => progress.fail('Build failed'));
|
||||
});
|
||||
});
|
||||
|
||||
group('when elf aot snapshot build fails', () {
|
||||
setUp(() {
|
||||
when(() => artifactBuilder.buildIosFramework()).thenAnswer(
|
||||
(_) async {},
|
||||
);
|
||||
when(() => artifactManager.newestAppDill()).thenReturn(File(''));
|
||||
when(
|
||||
() => artifactBuilder.buildElfAotSnapshot(
|
||||
appDillPath: any(named: 'appDillPath'),
|
||||
outFilePath: any(named: 'outFilePath'),
|
||||
),
|
||||
).thenThrow(const FileSystemException('error'));
|
||||
});
|
||||
|
||||
test('logs error and exits with code 70', () async {
|
||||
await expectLater(
|
||||
() => runWithOverrides(patcher.buildPatchArtifact),
|
||||
exitsWithCode(ExitCode.software),
|
||||
);
|
||||
|
||||
verify(
|
||||
() => progress.fail("FileSystemException: error, path = ''"),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('when build succeeds', () {
|
||||
setUp(() {
|
||||
when(() => artifactBuilder.buildIosFramework()).thenAnswer(
|
||||
(_) async {},
|
||||
);
|
||||
when(() => artifactManager.newestAppDill()).thenReturn(File(''));
|
||||
when(
|
||||
() => artifactBuilder.buildElfAotSnapshot(
|
||||
appDillPath: any(named: 'appDillPath'),
|
||||
outFilePath: any(named: 'outFilePath'),
|
||||
),
|
||||
).thenAnswer(
|
||||
(invocation) async =>
|
||||
File(invocation.namedArguments[#outFilePath] as String)
|
||||
..createSync(recursive: true),
|
||||
);
|
||||
|
||||
Directory(
|
||||
p.join(projectRoot.path, ArtifactManager.appXcframeworkName),
|
||||
).createSync(recursive: true);
|
||||
when(() => artifactManager.getAppXcframeworkDirectory())
|
||||
.thenReturn(projectRoot);
|
||||
});
|
||||
|
||||
test('returns zipped xcframework', () async {
|
||||
final artifact = await runWithOverrides(patcher.buildPatchArtifact);
|
||||
expect(p.basename(artifact.path), equals('App.xcframework.zip'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('createPatchArtifacts', () {
|
||||
const postLinkerFlutterRevision =
|
||||
'deadbeefdeadbeefdeadbeefdeadbeefdeadbeef';
|
||||
const preLinkerFlutterRevision =
|
||||
'83305b5088e6fe327fb3334a73ff190828d85713';
|
||||
const appId = 'appId';
|
||||
const arch = 'aarch64';
|
||||
const releaseId = 1;
|
||||
const linkFileName = 'out.vmcode';
|
||||
const elfAotSnapshotFileName = 'out.aot';
|
||||
const releaseArtifact = ReleaseArtifact(
|
||||
id: 0,
|
||||
releaseId: releaseId,
|
||||
arch: arch,
|
||||
platform: ReleasePlatform.android,
|
||||
hash: '#',
|
||||
size: 42,
|
||||
url: 'https://example.com',
|
||||
);
|
||||
|
||||
void setUpProjectRootArtifacts() {
|
||||
// Create a second app.dill for coverage of newestAppDill file.
|
||||
File(
|
||||
p.join(
|
||||
projectRoot.path,
|
||||
'.dart_tool',
|
||||
'flutter_build',
|
||||
'subdir',
|
||||
'app.dill',
|
||||
),
|
||||
).createSync(recursive: true);
|
||||
File(
|
||||
p.join(projectRoot.path, '.dart_tool', 'flutter_build', 'app.dill'),
|
||||
).createSync(recursive: true);
|
||||
File(p.join(projectRoot.path, 'build', elfAotSnapshotFileName))
|
||||
.createSync(
|
||||
recursive: true,
|
||||
);
|
||||
Directory(
|
||||
p.join(
|
||||
projectRoot.path,
|
||||
'build',
|
||||
'ios',
|
||||
'framework',
|
||||
'Release',
|
||||
'App.xcframework',
|
||||
),
|
||||
).createSync(
|
||||
recursive: true,
|
||||
);
|
||||
File(
|
||||
p.join(projectRoot.path, 'build', linkFileName),
|
||||
).createSync(recursive: true);
|
||||
}
|
||||
|
||||
setUp(() {
|
||||
when(
|
||||
() => codePushClientWrapper.getReleaseArtifact(
|
||||
appId: any(named: 'appId'),
|
||||
releaseId: any(named: 'releaseId'),
|
||||
arch: any(named: 'arch'),
|
||||
platform: any(named: 'platform'),
|
||||
),
|
||||
).thenAnswer((_) async => releaseArtifact);
|
||||
when(() => artifactManager.downloadFile(any())).thenAnswer((_) async {
|
||||
final tempDirectory = Directory.systemTemp.createTempSync();
|
||||
final file = File(p.join(tempDirectory.path, 'libapp.so'))
|
||||
..createSync();
|
||||
return file;
|
||||
});
|
||||
when(
|
||||
() => artifactManager.extractZip(
|
||||
zipFile: any(named: 'zipFile'),
|
||||
outputDirectory: any(named: 'outputDirectory'),
|
||||
),
|
||||
).thenAnswer((invocation) async {
|
||||
final zipFile = invocation.namedArguments[#zipFile] as File;
|
||||
final outDir =
|
||||
invocation.namedArguments[#outputDirectory] as Directory;
|
||||
File(p.join(outDir.path, '${p.basename(zipFile.path)}.zip'))
|
||||
.createSync();
|
||||
});
|
||||
when(() => engineConfig.localEngine).thenReturn(null);
|
||||
});
|
||||
|
||||
group('when release artifact download fails', () {
|
||||
setUp(() {
|
||||
when(
|
||||
() => artifactManager.downloadFile(any()),
|
||||
).thenThrow(Exception('Failed to download release artifact'));
|
||||
});
|
||||
|
||||
test('logs error and exits with code 70', () async {
|
||||
await expectLater(
|
||||
() => runWithOverrides(
|
||||
() => patcher.createPatchArtifacts(
|
||||
appId: appId,
|
||||
releaseId: releaseId,
|
||||
),
|
||||
),
|
||||
exitsWithCode(ExitCode.software),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('when uses linker', () {
|
||||
const linkPercentage = 50.0;
|
||||
late File analyzeSnapshotFile;
|
||||
late File genSnapshotFile;
|
||||
|
||||
setUp(() {
|
||||
final shorebirdRoot = Directory.systemTemp.createTempSync();
|
||||
flutterDirectory = Directory(
|
||||
p.join(shorebirdRoot.path, 'bin', 'cache', 'flutter'),
|
||||
);
|
||||
genSnapshotFile = File(
|
||||
p.join(
|
||||
flutterDirectory.path,
|
||||
'bin',
|
||||
'cache',
|
||||
'artifacts',
|
||||
'engine',
|
||||
'ios-release',
|
||||
'gen_snapshot_arm64',
|
||||
),
|
||||
);
|
||||
analyzeSnapshotFile = File(
|
||||
p.join(
|
||||
flutterDirectory.path,
|
||||
'bin',
|
||||
'cache',
|
||||
'artifacts',
|
||||
'engine',
|
||||
'ios-release',
|
||||
'analyze_snapshot_arm64',
|
||||
),
|
||||
)..createSync(recursive: true);
|
||||
|
||||
when(
|
||||
() => aotTools.link(
|
||||
base: any(named: 'base'),
|
||||
patch: any(named: 'patch'),
|
||||
analyzeSnapshot: any(named: 'analyzeSnapshot'),
|
||||
genSnapshot: any(named: 'genSnapshot'),
|
||||
kernel: any(named: 'kernel'),
|
||||
outputPath: any(named: 'outputPath'),
|
||||
workingDirectory: any(named: 'workingDirectory'),
|
||||
),
|
||||
).thenAnswer((_) async => linkPercentage);
|
||||
when(() => artifactManager.newestAppDill()).thenReturn(File(''));
|
||||
when(() => shorebirdEnv.flutterRevision)
|
||||
.thenReturn(postLinkerFlutterRevision);
|
||||
when(
|
||||
() => shorebirdArtifacts.getArtifactPath(
|
||||
artifact: ShorebirdArtifact.analyzeSnapshot,
|
||||
),
|
||||
).thenReturn(analyzeSnapshotFile.path);
|
||||
when(
|
||||
() => shorebirdArtifacts.getArtifactPath(
|
||||
artifact: ShorebirdArtifact.genSnapshot,
|
||||
),
|
||||
).thenReturn(genSnapshotFile.path);
|
||||
});
|
||||
|
||||
group('when linking fails', () {
|
||||
group('when aot snapshot does not exist', () {
|
||||
test('logs error and exits with code 70', () async {
|
||||
await expectLater(
|
||||
() => runWithOverrides(
|
||||
() => patcher.createPatchArtifacts(
|
||||
appId: appId,
|
||||
releaseId: releaseId,
|
||||
),
|
||||
),
|
||||
exitsWithCode(ExitCode.software),
|
||||
);
|
||||
|
||||
verify(
|
||||
() => logger.err(
|
||||
any(that: startsWith('Unable to find patch AOT file at')),
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
});
|
||||
|
||||
group('when analyzeSnapshot binary does not exist', () {
|
||||
setUp(() {
|
||||
when(
|
||||
() => shorebirdArtifacts.getArtifactPath(
|
||||
artifact: ShorebirdArtifact.analyzeSnapshot),
|
||||
).thenReturn('');
|
||||
setUpProjectRootArtifacts();
|
||||
});
|
||||
|
||||
test('logs error and exits with code 70', () async {
|
||||
await expectLater(
|
||||
() => runWithOverrides(
|
||||
() => patcher.createPatchArtifacts(
|
||||
appId: appId,
|
||||
releaseId: releaseId,
|
||||
),
|
||||
),
|
||||
exitsWithCode(ExitCode.software),
|
||||
);
|
||||
|
||||
verify(
|
||||
() => logger.err('Unable to find analyze_snapshot at '),
|
||||
).called(1);
|
||||
});
|
||||
});
|
||||
|
||||
group('when call to aotTools.link fails', () {
|
||||
setUp(() {
|
||||
when(
|
||||
() => aotTools.link(
|
||||
base: any(named: 'base'),
|
||||
patch: any(named: 'patch'),
|
||||
analyzeSnapshot: any(named: 'analyzeSnapshot'),
|
||||
genSnapshot: any(named: 'genSnapshot'),
|
||||
kernel: any(named: 'kernel'),
|
||||
outputPath: any(named: 'outputPath'),
|
||||
workingDirectory: any(named: 'workingDirectory'),
|
||||
),
|
||||
).thenThrow(Exception('oops'));
|
||||
|
||||
setUpProjectRootArtifacts();
|
||||
});
|
||||
|
||||
test('logs error and exits with code 70', () async {
|
||||
await expectLater(
|
||||
() => runWithOverrides(
|
||||
() => patcher.createPatchArtifacts(
|
||||
appId: appId,
|
||||
releaseId: releaseId,
|
||||
),
|
||||
),
|
||||
exitsWithCode(ExitCode.software),
|
||||
);
|
||||
|
||||
verify(
|
||||
() => progress.fail(
|
||||
'Failed to link AOT files: Exception: oops',
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('when generate patch diff base is supported', () {
|
||||
setUp(() {
|
||||
when(() => aotTools.isGeneratePatchDiffBaseSupported())
|
||||
.thenAnswer((_) async => true);
|
||||
when(
|
||||
() => aotTools.generatePatchDiffBase(
|
||||
analyzeSnapshotPath: any(named: 'analyzeSnapshotPath'),
|
||||
releaseSnapshot: any(named: 'releaseSnapshot'),
|
||||
),
|
||||
).thenAnswer((_) async => File(''));
|
||||
});
|
||||
|
||||
group('when we fail to generate patch diff base', () {
|
||||
setUp(() {
|
||||
when(
|
||||
() => aotTools.generatePatchDiffBase(
|
||||
analyzeSnapshotPath: any(named: 'analyzeSnapshotPath'),
|
||||
releaseSnapshot: any(named: 'releaseSnapshot'),
|
||||
),
|
||||
).thenThrow(Exception('oops'));
|
||||
|
||||
setUpProjectRootArtifacts();
|
||||
});
|
||||
|
||||
test('logs error and exits with code 70', () async {
|
||||
await expectLater(
|
||||
() => runWithOverrides(
|
||||
() => patcher.createPatchArtifacts(
|
||||
appId: appId,
|
||||
releaseId: releaseId,
|
||||
),
|
||||
),
|
||||
exitsWithCode(ExitCode.software),
|
||||
);
|
||||
|
||||
verify(() => progress.fail('Exception: oops')).called(1);
|
||||
});
|
||||
});
|
||||
|
||||
group('when linking and patch diff generation succeeds', () {
|
||||
const diffPath = 'path/to/diff';
|
||||
|
||||
setUp(() {
|
||||
when(
|
||||
() => artifactManager.createDiff(
|
||||
releaseArtifactPath: any(named: 'releaseArtifactPath'),
|
||||
patchArtifactPath: any(named: 'patchArtifactPath'),
|
||||
),
|
||||
).thenAnswer((_) async => diffPath);
|
||||
setUpProjectRootArtifacts();
|
||||
});
|
||||
|
||||
test('returns linked patch artifact in patch bundle', () async {
|
||||
final patchBundle = await runWithOverrides(
|
||||
() => patcher.createPatchArtifacts(
|
||||
appId: appId,
|
||||
releaseId: releaseId,
|
||||
),
|
||||
);
|
||||
|
||||
expect(patchBundle, hasLength(1));
|
||||
expect(
|
||||
patchBundle[Arch.arm64],
|
||||
isA<PatchArtifactBundle>()
|
||||
.having((b) => b.path, 'path', endsWith(diffPath)),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('when generate patch diff base is not supported', () {
|
||||
setUp(() {
|
||||
when(aotTools.isGeneratePatchDiffBaseSupported)
|
||||
.thenAnswer((_) async => false);
|
||||
setUpProjectRootArtifacts();
|
||||
});
|
||||
|
||||
test('returns vmcode file as patch file', () async {
|
||||
final patchBundle = await runWithOverrides(
|
||||
() => patcher.createPatchArtifacts(
|
||||
appId: appId,
|
||||
releaseId: releaseId,
|
||||
),
|
||||
);
|
||||
|
||||
expect(patchBundle, hasLength(1));
|
||||
expect(
|
||||
patchBundle[Arch.arm64],
|
||||
isA<PatchArtifactBundle>()
|
||||
.having((b) => b.path, 'path', endsWith('out.vmcode')),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('when does not use linker', () {
|
||||
setUp(() {
|
||||
when(() => shorebirdEnv.flutterRevision)
|
||||
.thenReturn(preLinkerFlutterRevision);
|
||||
when(() => aotTools.isGeneratePatchDiffBaseSupported())
|
||||
.thenAnswer((_) async => false);
|
||||
|
||||
setUpProjectRootArtifacts();
|
||||
});
|
||||
|
||||
test('returns base patch artifact in patch bundle', () async {
|
||||
final patchArtifacts = await runWithOverrides(
|
||||
() => patcher.createPatchArtifacts(
|
||||
appId: appId,
|
||||
releaseId: releaseId,
|
||||
),
|
||||
);
|
||||
|
||||
expect(patchArtifacts, hasLength(1));
|
||||
verifyNever(
|
||||
() => aotTools.link(
|
||||
base: any(named: 'base'),
|
||||
patch: any(named: 'patch'),
|
||||
analyzeSnapshot: any(named: 'analyzeSnapshot'),
|
||||
genSnapshot: any(named: 'genSnapshot'),
|
||||
kernel: any(named: 'kernel'),
|
||||
outputPath: any(named: 'outputPath'),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('extractReleaseVersionFromArtifact', () {
|
||||
test('throws UnimplementedError', () {
|
||||
expect(
|
||||
() => patcher.extractReleaseVersionFromArtifact(File('')),
|
||||
throwsUnimplementedError,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('createPatchMetadata', () {
|
||||
const allowAssetDiffs = false;
|
||||
const allowNativeDiffs = true;
|
||||
const operatingSystem = 'Mac OS X';
|
||||
const operatingSystemVersion = '10.15.7';
|
||||
const xcodeVersion = '11';
|
||||
|
||||
setUp(() {
|
||||
when(() => argResults['allow-asset-diffs'])
|
||||
.thenReturn(allowAssetDiffs);
|
||||
when(
|
||||
() => argResults['allow-native-diffs'],
|
||||
).thenReturn(allowNativeDiffs);
|
||||
when(() => platform.operatingSystem).thenReturn(operatingSystem);
|
||||
when(
|
||||
() => platform.operatingSystemVersion,
|
||||
).thenReturn(operatingSystemVersion);
|
||||
|
||||
when(() => xcodeBuild.version())
|
||||
.thenAnswer((_) async => xcodeVersion);
|
||||
});
|
||||
|
||||
group('when linker is not enabled', () {
|
||||
test('returns correct metadata', () async {
|
||||
final diffStatus = DiffStatus(
|
||||
hasAssetChanges: false,
|
||||
hasNativeChanges: false,
|
||||
);
|
||||
|
||||
final metadata = await runWithOverrides(
|
||||
() => patcher.createPatchMetadata(diffStatus),
|
||||
);
|
||||
|
||||
expect(
|
||||
metadata,
|
||||
equals(
|
||||
CreatePatchMetadata(
|
||||
releasePlatform: ReleasePlatform.ios,
|
||||
usedIgnoreAssetChangesFlag: allowAssetDiffs,
|
||||
hasAssetChanges: diffStatus.hasAssetChanges,
|
||||
usedIgnoreNativeChangesFlag: allowNativeDiffs,
|
||||
hasNativeChanges: diffStatus.hasNativeChanges,
|
||||
linkPercentage: null,
|
||||
environment: const BuildEnvironmentMetadata(
|
||||
operatingSystem: operatingSystem,
|
||||
operatingSystemVersion: operatingSystemVersion,
|
||||
shorebirdVersion: packageVersion,
|
||||
xcodeVersion: xcodeVersion,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('when linker is enabled', () {
|
||||
const linkPercentage = 100.0;
|
||||
|
||||
setUp(() {
|
||||
patcher.lastBuildLinkPercentage = linkPercentage;
|
||||
});
|
||||
|
||||
test('returns correct metadata', () async {
|
||||
final diffStatus = DiffStatus(
|
||||
hasAssetChanges: false,
|
||||
hasNativeChanges: false,
|
||||
);
|
||||
|
||||
final metadata = await runWithOverrides(
|
||||
() => patcher.createPatchMetadata(diffStatus),
|
||||
);
|
||||
|
||||
expect(
|
||||
metadata,
|
||||
equals(
|
||||
CreatePatchMetadata(
|
||||
releasePlatform: ReleasePlatform.ios,
|
||||
usedIgnoreAssetChangesFlag: allowAssetDiffs,
|
||||
hasAssetChanges: diffStatus.hasAssetChanges,
|
||||
usedIgnoreNativeChangesFlag: allowNativeDiffs,
|
||||
hasNativeChanges: diffStatus.hasNativeChanges,
|
||||
linkPercentage: linkPercentage,
|
||||
environment: const BuildEnvironmentMetadata(
|
||||
operatingSystem: operatingSystem,
|
||||
operatingSystemVersion: operatingSystemVersion,
|
||||
shorebirdVersion: packageVersion,
|
||||
xcodeVersion: xcodeVersion,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
testOn: 'mac-os',
|
||||
);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import 'package:shorebird_cli/src/archive_analysis/archive_analysis.dart';
|
||||
import 'package:shorebird_cli/src/archive_analysis/archive_differ.dart';
|
||||
import 'package:shorebird_cli/src/artifact_builder.dart';
|
||||
import 'package:shorebird_cli/src/artifact_manager.dart';
|
||||
import 'package:shorebird_cli/src/cache.dart';
|
||||
import 'package:shorebird_cli/src/code_push_client_wrapper.dart';
|
||||
import 'package:shorebird_cli/src/commands/patch_new/patch_new.dart';
|
||||
import 'package:shorebird_cli/src/config/config.dart';
|
||||
@@ -75,6 +76,7 @@ void main() {
|
||||
late ArgResults argResults;
|
||||
late ArtifactBuilder artifactBuilder;
|
||||
late ArtifactManager artifactManager;
|
||||
late Cache cache;
|
||||
late CodePushClientWrapper codePushClientWrapper;
|
||||
late ShorebirdLogger logger;
|
||||
late PatchDiffChecker patchDiffChecker;
|
||||
@@ -91,6 +93,7 @@ void main() {
|
||||
values: {
|
||||
artifactBuilderRef.overrideWith(() => artifactBuilder),
|
||||
artifactManagerRef.overrideWith(() => artifactManager),
|
||||
cacheRef.overrideWith(() => cache),
|
||||
codePushClientWrapperRef.overrideWith(() => codePushClientWrapper),
|
||||
loggerRef.overrideWith(() => logger),
|
||||
patchDiffCheckerRef.overrideWith(() => patchDiffChecker),
|
||||
@@ -119,6 +122,7 @@ void main() {
|
||||
argResults = MockArgResults();
|
||||
artifactBuilder = MockArtifactBuilder();
|
||||
artifactManager = MockArtifactManager();
|
||||
cache = MockCache();
|
||||
codePushClientWrapper = MockCodePushClientWrapper();
|
||||
logger = MockShorebirdLogger();
|
||||
progress = MockProgress();
|
||||
@@ -136,6 +140,8 @@ void main() {
|
||||
() => artifactManager.downloadFile(any()),
|
||||
).thenAnswer((_) async => File(''));
|
||||
|
||||
when(() => cache.updateAll()).thenAnswer((_) async => {});
|
||||
|
||||
when(() => codePushClientWrapper.getApp(appId: any(named: 'appId')))
|
||||
.thenAnswer((_) async => appMetadata);
|
||||
when(
|
||||
@@ -278,8 +284,8 @@ void main() {
|
||||
throwsA(isA<UnimplementedError>()),
|
||||
);
|
||||
expect(
|
||||
() => command.getPatcher(ReleaseType.iosFramework),
|
||||
throwsA(isA<UnimplementedError>()),
|
||||
command.getPatcher(ReleaseType.iosFramework),
|
||||
isA<IosFrameworkPatcher>(),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -296,6 +302,7 @@ void main() {
|
||||
verifyInOrder([
|
||||
() => patcher.assertPreconditions(),
|
||||
() => patcher.assertArgsAreValid(),
|
||||
() => cache.updateAll(),
|
||||
() => codePushClientWrapper.getApp(appId: appId),
|
||||
() => codePushClientWrapper.getRelease(
|
||||
appId: appId,
|
||||
@@ -347,6 +354,7 @@ void main() {
|
||||
verifyInOrder([
|
||||
() => patcher.assertPreconditions(),
|
||||
() => patcher.assertArgsAreValid(),
|
||||
() => cache.updateAll(),
|
||||
() => codePushClientWrapper.getApp(appId: appId),
|
||||
() => patcher.buildPatchArtifact(),
|
||||
() => patcher.extractReleaseVersionFromArtifact(any()),
|
||||
@@ -419,6 +427,19 @@ void main() {
|
||||
() => patcher.buildPatchArtifact(),
|
||||
]);
|
||||
});
|
||||
|
||||
test('updates cache with both default and release Flutter revisions',
|
||||
() async {
|
||||
await runWithOverrides(command.run);
|
||||
|
||||
verifyInOrder([
|
||||
cache.updateAll,
|
||||
() => shorebirdEnv.copyWith(
|
||||
flutterRevisionOverride: releaseFlutterRevision,
|
||||
),
|
||||
cache.updateAll,
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -197,7 +197,7 @@ void main() {
|
||||
});
|
||||
|
||||
group('assertArgsAreValid', () {
|
||||
group('when split-per-abi is true', () {
|
||||
group('when release-version was not provided', () {
|
||||
setUp(() {
|
||||
when(() => argResults.wasParsed('release-version')).thenReturn(false);
|
||||
});
|
||||
|
||||
@@ -256,7 +256,6 @@ void main() {
|
||||
'ios',
|
||||
'framework',
|
||||
'Release',
|
||||
'Flutter.xcframework',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:args/args.dart';
|
||||
import 'package:mason_logger/mason_logger.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:scoped/scoped.dart';
|
||||
import 'package:shorebird_cli/src/cache.dart';
|
||||
import 'package:shorebird_cli/src/code_push_client_wrapper.dart';
|
||||
import 'package:shorebird_cli/src/commands/release_new/release_new.dart';
|
||||
import 'package:shorebird_cli/src/config/config.dart';
|
||||
@@ -42,6 +43,7 @@ void main() {
|
||||
);
|
||||
|
||||
late ArgResults argResults;
|
||||
late Cache cache;
|
||||
late CodePushClientWrapper codePushClientWrapper;
|
||||
late Directory shorebirdRoot;
|
||||
late Directory projectRoot;
|
||||
@@ -57,6 +59,7 @@ void main() {
|
||||
return runScoped(
|
||||
body,
|
||||
values: {
|
||||
cacheRef.overrideWith(() => cache),
|
||||
codePushClientWrapperRef.overrideWith(() => codePushClientWrapper),
|
||||
loggerRef.overrideWith(() => logger),
|
||||
shorebirdEnvRef.overrideWith(() => shorebirdEnv),
|
||||
@@ -77,6 +80,7 @@ void main() {
|
||||
|
||||
setUp(() {
|
||||
argResults = MockArgResults();
|
||||
cache = MockCache();
|
||||
codePushClientWrapper = MockCodePushClientWrapper();
|
||||
logger = MockShorebirdLogger();
|
||||
progress = MockProgress();
|
||||
@@ -90,6 +94,8 @@ void main() {
|
||||
when(() => argResults['platform']).thenReturn(['android']);
|
||||
when(() => argResults.wasParsed(any())).thenReturn(true);
|
||||
|
||||
when(cache.updateAll).thenAnswer((_) async => {});
|
||||
|
||||
when(() => codePushClientWrapper.getApp(appId: any(named: 'appId')))
|
||||
.thenAnswer((_) async => appMetadata);
|
||||
when(
|
||||
@@ -204,6 +210,7 @@ void main() {
|
||||
verifyInOrder([
|
||||
releaser.assertPreconditions,
|
||||
releaser.assertArgsAreValid,
|
||||
cache.updateAll,
|
||||
() => codePushClientWrapper.getApp(appId: appId),
|
||||
releaser.buildReleaseArtifacts,
|
||||
() => releaser.getReleaseVersion(
|
||||
@@ -291,6 +298,7 @@ Note: ${lightCyan.wrap('shorebird patch --platform=android')} without the --rele
|
||||
verifyInOrder([
|
||||
releaser.assertPreconditions,
|
||||
releaser.assertArgsAreValid,
|
||||
cache.updateAll,
|
||||
() => codePushClientWrapper.getApp(appId: appId),
|
||||
releaser.buildReleaseArtifacts,
|
||||
() => releaser.getReleaseVersion(
|
||||
|
||||
Reference in New Issue
Block a user