fix: patch multi dimensional flavor patch and improve release (#1968)

Co-authored-by: Bryan Oltman <bryan@shorebird.dev>
This commit is contained in:
Erick
2024-04-25 13:49:53 -03:00
committed by GitHub
parent 0e51a093ce
commit 955c8ba87f
11 changed files with 226 additions and 95 deletions
@@ -1,5 +1,3 @@
import 'dart:io';
import 'package:mason_logger/mason_logger.dart';
import 'package:path/path.dart' as p;
import 'package:shorebird_cli/src/command.dart';
@@ -51,8 +49,8 @@ class BuildApkCommand extends ShorebirdCommand with ShorebirdBuildMixin {
final buildProgress = logger.progress('Building apk');
try {
await buildApk(flavor: flavor, target: target);
} on ProcessException catch (error) {
buildProgress.fail('Failed to build: ${error.message}');
} on BuildException catch (error) {
buildProgress.fail(error.message);
return ExitCode.software.code;
}
@@ -1,5 +1,3 @@
import 'dart:io';
import 'package:mason_logger/mason_logger.dart';
import 'package:path/path.dart' as p;
import 'package:shorebird_cli/src/command.dart';
@@ -51,8 +49,8 @@ class BuildAppBundleCommand extends ShorebirdCommand with ShorebirdBuildMixin {
final buildProgress = logger.progress('Building appbundle');
try {
await buildAppBundle(flavor: flavor, target: target);
} on ProcessException catch (error) {
buildProgress.fail('Failed to build: ${error.message}');
} on BuildException catch (error) {
buildProgress.fail(error.message);
return ExitCode.software.code;
}
@@ -116,18 +116,9 @@ If this option is not provided, the version number will be determined from the p
final app = await codePushClientWrapper.getApp(appId: appId);
var hasBuiltWithActiveFlutter = false;
late File aabFile;
final projectRoot = shorebirdEnv.getShorebirdProjectRoot()!;
final bundleDirPath = p.join(
projectRoot.path,
'build',
'app',
'outputs',
'bundle',
);
final bundlePath = flavor != null
? p.join(bundleDirPath, '${flavor}Release', 'app-$flavor-release.aab')
: p.join(bundleDirPath, 'release', 'app-release.aab');
final String releaseVersion;
final argReleaseVersion = results['release-version'] as String?;
@@ -142,10 +133,10 @@ If this option is not provided, the version number will be determined from the p
'Building patch with Flutter $flutterVersionString',
);
try {
await buildAppBundle(flavor: flavor, target: target);
aabFile = await buildAppBundle(flavor: flavor, target: target);
buildProgress.complete();
} on ProcessException catch (error) {
buildProgress.fail('Failed to build: ${error.message}');
} on BuildException catch (error) {
buildProgress.fail(error.message);
return ExitCode.software.code;
}
@@ -156,7 +147,7 @@ If this option is not provided, the version number will be determined from the p
);
try {
releaseVersion = await extractReleaseVersionFromAppBundle(bundlePath);
releaseVersion = await extractReleaseVersionFromAppBundle(aabFile.path);
detectReleaseVersionProgress.complete(
'Detected release version $releaseVersion',
);
@@ -202,10 +193,10 @@ Please re-run the release command for this version or create a new release.''');
currentFlutterRevision != release.flutterRevision) {
final buildProgress = logger.progress('Building patch');
try {
await buildAppBundle(flavor: flavor, target: target);
aabFile = await buildAppBundle(flavor: flavor, target: target);
buildProgress.complete();
} on ProcessException catch (error) {
buildProgress.fail('Failed to build: ${error.message}');
} on BuildException catch (error) {
buildProgress.fail(error.message);
return ExitCode.software.code;
}
}
@@ -276,7 +267,7 @@ Looked in:
try {
diffStatus =
await patchDiffChecker.confirmUnpatchableDiffsIfNecessary(
localArtifact: File(bundlePath),
localArtifact: aabFile,
releaseArtifact: releaseAabArtifactFile,
archiveDiffer: _archiveDiffer,
allowAssetChanges: allowAssetDiffs,
@@ -11,7 +11,6 @@ import 'package:shorebird_cli/src/extensions/arg_results.dart';
import 'package:shorebird_cli/src/logger.dart';
import 'package:shorebird_cli/src/platform.dart';
import 'package:shorebird_cli/src/platform/platform.dart';
import 'package:shorebird_cli/src/shorebird_android_artifacts.dart';
import 'package:shorebird_cli/src/shorebird_build_mixin.dart';
import 'package:shorebird_cli/src/shorebird_env.dart';
import 'package:shorebird_cli/src/shorebird_flutter.dart';
@@ -171,21 +170,24 @@ Use `shorebird flutter versions list` to list available versions.
'Building release with Flutter $flutterVersionString',
);
late final File apkFile;
final File aabFile;
try {
await buildAppBundle(
aabFile = await buildAppBundle(
flavor: flavor,
target: target,
targetPlatforms: architectures,
);
if (generateApk) {
await buildApk(
apkFile = await buildApk(
flavor: flavor,
target: target,
targetPlatforms: architectures,
);
}
} on ProcessException catch (error) {
buildProgress.fail('Failed to build: ${error.message}');
} on BuildException catch (error) {
buildProgress.fail(error.message);
return ExitCode.software.code;
}
buildProgress.complete();
@@ -195,27 +197,6 @@ Use `shorebird flutter versions list` to list available versions.
final appId = shorebirdYaml.getAppId(flavor: flavor);
final app = await codePushClientWrapper.getApp(appId: appId);
late final File apkFile;
final File aabFile;
try {
aabFile = shorebirdAndroidArtifacts.findAab(
project: projectRoot,
flavor: flavor,
);
if (generateApk) {
apkFile = shorebirdAndroidArtifacts.findApk(
project: projectRoot,
flavor: flavor,
);
}
} on ArtifactNotFoundException catch (error) {
logger.err(error.toString());
return ExitCode.software.code;
} on MultipleArtifactsFoundException catch (error) {
logger.err(error.toString());
return ExitCode.software.code;
}
final String releaseVersion;
final detectReleaseVersionProgress = logger.progress(
'Detecting release version',
@@ -1,5 +1,6 @@
import 'dart:io';
import 'package:collection/collection.dart';
import 'package:path/path.dart' as p;
import 'package:scoped/scoped.dart';
import 'package:shorebird_cli/src/command.dart';
@@ -16,8 +17,9 @@ class MultipleArtifactsFoundException implements Exception {
@override
String toString() {
final artifacts = foundArtifacts.sortedBy((e) => e.path);
return 'Multiple artifacts found in $buildDir: '
'${foundArtifacts.map((e) => e.path)}';
'${artifacts.map((e) => e.path)}';
}
}
@@ -5,7 +5,9 @@ import 'package:shorebird_cli/src/command.dart';
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';
/// Used to wrap code that invokes `flutter build` with Shorebird's fork of
@@ -24,12 +26,12 @@ class BuildException implements Exception {
}
mixin ShorebirdBuildMixin on ShorebirdCommand {
Future<void> buildAppBundle({
Future<File> buildAppBundle({
String? flavor,
String? target,
Iterable<Arch>? targetPlatforms,
}) async {
return _runShorebirdBuildCommand(() async {
await _runShorebirdBuildCommand(() async {
const executable = 'flutter';
final targetPlatformArgs = targetPlatforms?.targetPlatformArg;
final arguments = [
@@ -49,14 +51,29 @@ mixin ShorebirdBuildMixin on ShorebirdCommand {
);
if (result.exitCode != ExitCode.success.code) {
throw ProcessException(
'flutter',
arguments,
result.stderr.toString(),
result.exitCode,
throw BuildException(
'Failed to build: ${result.stderr}',
);
}
});
final projectRoot = shorebirdEnv.getShorebirdProjectRoot()!;
try {
return shorebirdAndroidArtifacts.findAab(
project: projectRoot,
flavor: flavor,
);
} on MultipleArtifactsFoundException catch (error) {
throw BuildException(
'Build succeeded, but it generated multiple AABs in the '
'build directory. ${error.foundArtifacts.map((e) => e.path)}',
);
} on ArtifactNotFoundException catch (error) {
throw BuildException(
'Build succeeded, but could not find the AAB in the build directory. '
'Expected to find ${error.artifactName}',
);
}
}
Future<void> buildAar({
@@ -93,13 +110,13 @@ mixin ShorebirdBuildMixin on ShorebirdCommand {
});
}
Future<void> buildApk({
Future<File> buildApk({
String? flavor,
String? target,
Iterable<Arch>? targetPlatforms,
bool splitPerAbi = false,
}) async {
return _runShorebirdBuildCommand(() async {
await _runShorebirdBuildCommand(() async {
const executable = 'flutter';
final targetPlatformArgs = targetPlatforms?.targetPlatformArg;
final arguments = [
@@ -124,14 +141,28 @@ mixin ShorebirdBuildMixin on ShorebirdCommand {
);
if (result.exitCode != ExitCode.success.code) {
throw ProcessException(
'flutter',
arguments,
result.stderr.toString(),
result.exitCode,
throw BuildException(
'Failed to build: ${result.stderr}',
);
}
});
final projectRoot = shorebirdEnv.getShorebirdProjectRoot()!;
try {
return shorebirdAndroidArtifacts.findApk(
project: projectRoot,
flavor: flavor,
);
} on MultipleArtifactsFoundException catch (error) {
throw BuildException(
'Build succeeded, but it generated multiple APKs in the '
'build directory. ${error.foundArtifacts.map((e) => e.path)}',
);
} on ArtifactNotFoundException catch (error) {
throw BuildException(
'Build succeeded, but could not find the APK in the build directory. '
'Expected to find ${error.artifactName}',
);
}
}
/// Calls `flutter build ipa`. If [codesign] is false, this will only build
@@ -1,3 +1,5 @@
import 'dart:io';
import 'package:args/args.dart';
import 'package:mason_logger/mason_logger.dart';
import 'package:mocktail/mocktail.dart';
@@ -7,6 +9,7 @@ import 'package:shorebird_cli/src/commands/build/build.dart';
import 'package:shorebird_cli/src/doctor.dart';
import 'package:shorebird_cli/src/logger.dart';
import 'package:shorebird_cli/src/os/operating_system_interface.dart';
import 'package:shorebird_cli/src/shorebird_android_artifacts.dart';
import 'package:shorebird_cli/src/shorebird_env.dart';
import 'package:shorebird_cli/src/shorebird_process.dart';
import 'package:shorebird_cli/src/shorebird_validator.dart';
@@ -29,6 +32,7 @@ void main() {
late ShorebirdFlutterValidator flutterValidator;
late ShorebirdProcess shorebirdProcess;
late ShorebirdValidator shorebirdValidator;
late ShorebirdAndroidArtifacts shorebirdAndroidArtifacts;
R runWithOverrides<R>(R Function() body) {
return runScoped(
@@ -40,11 +44,14 @@ void main() {
processRef.overrideWith(() => shorebirdProcess),
shorebirdEnvRef.overrideWith(() => shorebirdEnv),
shorebirdValidatorRef.overrideWith(() => shorebirdValidator),
shorebirdAndroidArtifactsRef
.overrideWith(() => shorebirdAndroidArtifacts),
},
);
}
setUpAll(() {
registerFallbackValue(Directory(''));
registerFallbackValue(FakeShorebirdProcess());
});
@@ -59,6 +66,7 @@ void main() {
flutterValidator = MockShorebirdFlutterValidator();
shorebirdEnv = MockShorebirdEnv();
shorebirdValidator = MockShorebirdValidator();
shorebirdAndroidArtifacts = MockShorebirdAndroidArtifacts();
when(
() => shorebirdProcess.run(
@@ -83,6 +91,7 @@ void main() {
when(() => operatingSystemInterface.which('flutter'))
.thenReturn('/path/to/flutter');
when(() => shorebirdEnv.flutterRevision).thenReturn('1234');
when(shorebirdEnv.getShorebirdProjectRoot).thenReturn(Directory(''));
when(
() => shorebirdValidator.validatePreconditions(
@@ -95,6 +104,15 @@ void main() {
() => doctor.androidCommandValidators,
).thenReturn([flutterValidator]);
when(
() => shorebirdAndroidArtifacts.findApk(
project: any(named: 'project'),
flavor: any(
named: 'flavor',
),
),
).thenReturn(File(''));
command = runWithOverrides(BuildApkCommand.new)
..testArgResults = argResults;
});
@@ -1,3 +1,5 @@
import 'dart:io';
import 'package:args/args.dart';
import 'package:mason_logger/mason_logger.dart';
import 'package:mocktail/mocktail.dart';
@@ -9,6 +11,7 @@ import 'package:shorebird_cli/src/engine_config.dart';
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_env.dart';
import 'package:shorebird_cli/src/shorebird_process.dart';
import 'package:shorebird_cli/src/shorebird_validator.dart';
@@ -31,6 +34,7 @@ void main() {
late ShorebirdFlutterValidator flutterValidator;
late ShorebirdProcess shorebirdProcess;
late ShorebirdValidator shorebirdValidator;
late ShorebirdAndroidArtifacts shorebirdAndroidArtifacts;
R runWithOverrides<R>(R Function() body) {
return runScoped(
@@ -43,12 +47,15 @@ void main() {
processRef.overrideWith(() => shorebirdProcess),
shorebirdEnvRef.overrideWith(() => shorebirdEnv),
shorebirdValidatorRef.overrideWith(() => shorebirdValidator),
shorebirdAndroidArtifactsRef
.overrideWith(() => shorebirdAndroidArtifacts),
},
);
}
setUpAll(() {
registerFallbackValue(FakeShorebirdProcess());
registerFallbackValue(Directory(''));
});
setUp(() {
@@ -62,6 +69,7 @@ void main() {
shorebirdEnv = MockShorebirdEnv();
shorebirdProcess = MockShorebirdProcess();
shorebirdValidator = MockShorebirdValidator();
shorebirdAndroidArtifacts = MockShorebirdAndroidArtifacts();
when(
() => shorebirdProcess.run(
@@ -89,6 +97,7 @@ void main() {
() => doctor.androidCommandValidators,
).thenReturn([flutterValidator]);
when(() => shorebirdEnv.flutterRevision).thenReturn('1234');
when(shorebirdEnv.getShorebirdProjectRoot).thenReturn(Directory(''));
when(
() => shorebirdValidator.validatePreconditions(
checkUserIsAuthenticated: any(named: 'checkUserIsAuthenticated'),
@@ -96,6 +105,12 @@ void main() {
validators: any(named: 'validators'),
),
).thenAnswer((_) async {});
when(
() => shorebirdAndroidArtifacts.findAab(
project: any(named: 'project'),
flavor: any(named: 'flavor'),
),
).thenReturn(File('app-release.aab'));
command = runWithOverrides(BuildAppBundleCommand.new)
..testArgResults = argResults;
@@ -24,6 +24,7 @@ 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/shorebird_android_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';
@@ -121,6 +122,7 @@ flutter:
late ShorebirdFlutterValidator flutterValidator;
late ShorebirdProcess shorebirdProcess;
late ShorebirdValidator shorebirdValidator;
late ShorebirdAndroidArtifacts shorebirdAndroidArtifacts;
late PatchAndroidCommand command;
R runWithOverrides<R>(R Function() body) {
@@ -144,6 +146,9 @@ flutter:
processRef.overrideWith(() => shorebirdProcess),
shorebirdFlutterRef.overrideWith(() => shorebirdFlutter),
shorebirdValidatorRef.overrideWith(() => shorebirdValidator),
shorebirdAndroidArtifactsRef.overrideWith(
() => shorebirdAndroidArtifacts,
),
},
);
}
@@ -216,6 +221,7 @@ flutter:
shorebirdProcess = MockShorebirdProcess();
shorebirdFlutter = MockShorebirdFlutter();
shorebirdValidator = MockShorebirdValidator();
shorebirdAndroidArtifacts = MockShorebirdAndroidArtifacts();
command = runWithOverrides(
() => PatchAndroidCommand(archiveDiffer: archiveDiffer),
)..testArgResults = argResults;
@@ -393,6 +399,13 @@ flutter:
when(() => platform.operatingSystemVersion)
.thenReturn(operatingSystemVersion);
when(() => shorebirdEnv.canAcceptUserInput).thenReturn(true);
when(
() => shorebirdAndroidArtifacts.findAab(
project: any(named: 'project'),
flavor: any(named: 'flavor'),
),
).thenReturn(File('release.aab'));
});
test('has a description', () {
@@ -921,22 +921,32 @@ Note: ${lightCyan.wrap('shorebird patch android --flavor=$flavor --target=$targe
verifyNever(() => logger.confirm(any()));
});
test('errors when the app bundle cannot be found', () async {
test('errors when multiple apks are found', () async {
shorebirdAndroidArtifacts = MockShorebirdAndroidArtifacts();
when(() => argResults['artifact']).thenReturn('apk');
when(
() => shorebirdAndroidArtifacts.findAab(
project: any(named: 'project'),
flavor: any(named: 'flavor'),
),
).thenReturn(File('app-release.aab'));
when(
() => shorebirdAndroidArtifacts.findApk(
project: any(named: 'project'),
flavor: any(named: 'flavor'),
),
).thenThrow(
ArtifactNotFoundException(
artifactName: 'app-release.aab',
MultipleArtifactsFoundException(
foundArtifacts: [File('a'), File('b')],
buildDir: 'buildDir',
),
);
final exitCode = await runWithOverrides(command.run);
verify(
() => logger.err('Artifact app-release.aab not found in buildDir'),
() => progress.fail(
'Build succeeded, but it generated multiple APKs in the build '
'directory. (a, b)',
),
).called(1);
expect(exitCode, ExitCode.software.code);
});
@@ -963,12 +973,37 @@ Note: ${lightCyan.wrap('shorebird patch android --flavor=$flavor --target=$targe
);
final exitCode = await runWithOverrides(command.run);
verify(
() => logger.err('Artifact app-release.apk not found in buildDir'),
() => progress.fail(
'Build succeeded, but could not find the APK in the build '
'directory. Expected to find app-release.apk',
),
).called(1);
expect(exitCode, ExitCode.software.code);
});
test('errors when multiple artifacts are found', () async {
test('errors when the app bundle cannot be found', () async {
shorebirdAndroidArtifacts = MockShorebirdAndroidArtifacts();
when(
() => shorebirdAndroidArtifacts.findAab(
project: any(named: 'project'),
flavor: any(named: 'flavor'),
),
).thenThrow(
ArtifactNotFoundException(
artifactName: 'app-release.aab',
buildDir: 'buildDir',
),
);
final exitCode = await runWithOverrides(command.run);
verify(
() => progress
.fail('Build succeeded, but could not find the AAB in the build '
'directory. Expected to find app-release.aab'),
).called(1);
expect(exitCode, ExitCode.software.code);
});
test('errors when multiple aabs are found', () async {
shorebirdAndroidArtifacts = MockShorebirdAndroidArtifacts();
when(
() => shorebirdAndroidArtifacts.findAab(
@@ -983,7 +1018,10 @@ Note: ${lightCyan.wrap('shorebird patch android --flavor=$flavor --target=$targe
);
final exitCode = await runWithOverrides(command.run);
verify(
() => logger.err('Multiple artifacts found in buildDir: (a, b)'),
() => progress.fail(
'Build succeeded, but it generated multiple AABs in the build '
'directory. (a, b)',
),
).called(1);
expect(exitCode, ExitCode.software.code);
});
@@ -25,19 +25,34 @@ void main() {
});
test('throws ArtifactNotFoundException for apks', () {
final buildDir = Directory(
path.join(
project.path,
'build',
'app',
'outputs',
'flutter-apk',
),
);
expect(
() => shorebirdAndroidArtifacts.findApk(
project: project,
flavor: null,
),
throwsA(isA<ArtifactNotFoundException>()),
throwsA(
isA<ArtifactNotFoundException>().having(
(exception) => exception.toString(),
'message',
equals('Artifact app-release.apk not found in ${buildDir.path}'),
),
),
);
});
});
group('when build folder exists but not the file', () {
test('throws ArtifactNotFoundException for aabs', () {
Directory(
final buildDir = Directory(
path.join(
project.path,
'build',
@@ -46,13 +61,19 @@ void main() {
'bundle',
'release',
),
).createSync(recursive: true);
)..createSync(recursive: true);
expect(
() => shorebirdAndroidArtifacts.findAab(
project: project,
flavor: null,
),
throwsA(isA<ArtifactNotFoundException>()),
throwsA(
isA<ArtifactNotFoundException>().having(
(exception) => exception.toString(),
'message',
equals('Artifact app-release.aab not found in ${buildDir.path}'),
),
),
);
});
@@ -287,20 +308,28 @@ void main() {
group('when multiple files are found', () {
test('throws MultipleArtifactsFoundException when looking for aab', () {
final buildDir = Directory(
path.join(
project.path,
'build',
'app',
'outputs',
'bundle',
'stablePlayStoreRelease',
),
);
final duplicatedArtifactPath = path.join(
'build',
'app',
'outputs',
'bundle',
'stablePlayStoreRelease',
buildDir.path,
'app---stable-playStore-release.aab',
);
File(
path.join(project.path, duplicatedArtifactPath),
).createSync(recursive: true);
const artifactPath =
'build/app/outputs/bundle/stablePlayStoreRelease/app-stable-playStore-release.aab';
final artifactPath = path.join(
buildDir.path,
'app-stable-playStore-release.aab',
);
File(
path.join(project.path, artifactPath),
).createSync(recursive: true);
@@ -312,16 +341,29 @@ void main() {
project: project,
flavor: flavor,
),
throwsA(isA<MultipleArtifactsFoundException>()),
throwsA(
isA<MultipleArtifactsFoundException>().having(
(exception) => exception.toString(),
'message',
equals('Multiple artifacts found in ${buildDir.path}: '
'($duplicatedArtifactPath, $artifactPath)'),
),
),
);
});
test('throws MultipleArtifactsFoundException when looking for apk', () {
final buildDir = Directory(
path.join(
project.path,
'build',
'app',
'outputs',
'flutter-apk',
),
);
final duplicatedArtifactPath = path.join(
'build',
'app',
'outputs',
'flutter-apk',
buildDir.path,
'app----stableplaystore-release.apk',
);
File(
@@ -329,10 +371,7 @@ void main() {
).createSync(recursive: true);
final artifactPath = path.join(
'build',
'app',
'outputs',
'flutter-apk',
buildDir.path,
'app-stableplaystore-release.apk',
);
File(
@@ -346,7 +385,14 @@ void main() {
project: project,
flavor: flavor,
),
throwsA(isA<MultipleArtifactsFoundException>()),
throwsA(
isA<MultipleArtifactsFoundException>().having(
(exception) => exception.toString(),
'message',
equals('Multiple artifacts found in ${buildDir.path}: '
'($duplicatedArtifactPath, $artifactPath)'),
),
),
);
});
});