diff --git a/packages/shorebird_cli/lib/src/artifact_builder/artifact_builder.dart b/packages/shorebird_cli/lib/src/artifact_builder/artifact_builder.dart index 35890374..2901aa57 100644 --- a/packages/shorebird_cli/lib/src/artifact_builder/artifact_builder.dart +++ b/packages/shorebird_cli/lib/src/artifact_builder/artifact_builder.dart @@ -67,6 +67,33 @@ extension on String { /// Builds aabs, ipas, and other artifacts produced by `flutter build`. /// @{endtemplate} class ArtifactBuilder { + /// A general recommendation when building artifacts fails. + static String runVanillaFlutterBuildRecommendation(String buildCommand) => ''' + +${styleBold.wrap('💡 Fix Recommendations')} + +• Check that running `flutter build` with the same command-line arguments +completes successfully by running the following command: + +${lightCyan.wrap(buildCommand)} + +If the above command fails, then this is *NOT* a Shorebird issue and the +the underlying `flutter build` failure must be resolved for Shorebird to +build a release. + +• If `flutter build` completes successfully, please ensure that you are +providing the desired flutter version to the release command via +the `--flutter-version` option. If you do not specify a `--flutter-version` +Shorebird will default to the latest stable version of Flutter. +We strongly encourage always specifying an explicit Flutter version: + +${lightCyan.wrap('shorebird release --flutter-version=3.29.0')} + +• If `flutter build` completes successfully and `shorebird release` +fails when using the same flutter version, please file an issue: +${link(uri: Uri.parse('https://github.com/shorebirdtech/shorebird/issues/new'))} +'''; + /// Builds an aab using `flutter build appbundle`. Runs `flutter pub get` with /// the system installation of Flutter to reset /// `.dart_tool/package_config.json` after the build completes or fails. @@ -100,10 +127,15 @@ class ArtifactBuilder { ); if (exitCode != ExitCode.success.code) { - throw ArtifactBuildException(''' + throw ArtifactBuildException( + ''' Failed to build AAB. Command: $executable ${arguments.join(' ')} -Reason: Exited with code $exitCode.'''); +Reason: Exited with code $exitCode.''', + fixRecommendation: runVanillaFlutterBuildRecommendation( + [executable, ...arguments].join(' '), + ), + ); } }); @@ -165,10 +197,15 @@ Reason: Exited with code $exitCode.'''); ); if (exitCode != ExitCode.success.code) { - throw ArtifactBuildException(''' + throw ArtifactBuildException( + ''' Failed to build APK. Command: $executable ${arguments.join(' ')} -Reason: Exited with code $exitCode.'''); +Reason: Exited with code $exitCode.''', + fixRecommendation: runVanillaFlutterBuildRecommendation( + [executable, ...arguments].join(' '), + ), + ); } }); final projectRoot = shorebirdEnv.getShorebirdProjectRoot()!; @@ -222,10 +259,15 @@ Reason: Exited with code $exitCode.'''); ); if (exitCode != ExitCode.success.code) { - throw ArtifactBuildException(''' + throw ArtifactBuildException( + ''' Failed to build AAR. Command: $executable ${arguments.join(' ')} -Reason: Exited with code $exitCode.'''); +Reason: Exited with code $exitCode.''', + fixRecommendation: runVanillaFlutterBuildRecommendation( + [executable, ...arguments].join(' '), + ), + ); } }); } @@ -257,10 +299,15 @@ Reason: Exited with code $exitCode.'''); ); if (exitCode != ExitCode.success.code) { - throw ArtifactBuildException(''' + throw ArtifactBuildException( + ''' Failed to build linux app. Command: $executable ${arguments.join(' ')} -Reason: Exited with code $exitCode.'''); +Reason: Exited with code $exitCode.''', + fixRecommendation: runVanillaFlutterBuildRecommendation( + [executable, ...arguments].join(' '), + ), + ); } }); } @@ -305,10 +352,15 @@ Reason: Exited with code $exitCode.'''); ); if (exitCode != ExitCode.success.code) { - throw ArtifactBuildException(''' + throw ArtifactBuildException( + ''' Failed to build macOS app. Command: $executable ${arguments.join(' ')} -Reason: Exited with code $exitCode.'''); +Reason: Exited with code $exitCode.''', + fixRecommendation: runVanillaFlutterBuildRecommendation( + [executable, ...arguments].join(' '), + ), + ); } appDillPath = _findAppDill(projectRoot: projectRoot, after: buildStart); @@ -366,10 +418,15 @@ Reason: Exited with code $exitCode.'''); ); if (exitCode != ExitCode.success.code) { - throw ArtifactBuildException(''' + throw ArtifactBuildException( + ''' Failed to build IPA. Command: $executable ${arguments.join(' ')} -Reason: Exited with code $exitCode.'''); +Reason: Exited with code $exitCode.''', + fixRecommendation: runVanillaFlutterBuildRecommendation( + [executable, ...arguments].join(' '), + ), + ); } appDillPath = _findAppDill(projectRoot: projectRoot, after: buildStart); @@ -420,11 +477,15 @@ Reason: Exited with code $exitCode.'''); ); if (exitCode != ExitCode.success.code) { - throw ArtifactBuildException(''' + throw ArtifactBuildException( + ''' Failed to build iOS framework. Command: $executable ${arguments.join(' ')} -Reason: Exited with code $exitCode. -'''); +Reason: Exited with code $exitCode.''', + fixRecommendation: runVanillaFlutterBuildRecommendation( + [executable, ...arguments].join(' '), + ), + ); } appDillPath = _findAppDill(projectRoot: projectRoot, after: buildStart); @@ -535,10 +596,15 @@ Either run `flutter pub get` manually, or follow the steps in ${cannotRunInVSCod ); if (exitCode != ExitCode.success.code) { - throw ArtifactBuildException(''' + throw ArtifactBuildException( + ''' Failed to build windows app. Command: $executable ${arguments.join(' ')} -Reason: Exited with code $exitCode.'''); +Reason: Exited with code $exitCode.''', + fixRecommendation: runVanillaFlutterBuildRecommendation( + [executable, ...arguments].join(' '), + ), + ); } }); diff --git a/packages/shorebird_cli/test/src/artifact_builder/artifact_builder_test.dart b/packages/shorebird_cli/test/src/artifact_builder/artifact_builder_test.dart index f1dd585c..0ffea8fb 100644 --- a/packages/shorebird_cli/test/src/artifact_builder/artifact_builder_test.dart +++ b/packages/shorebird_cli/test/src/artifact_builder/artifact_builder_test.dart @@ -342,7 +342,17 @@ Either run `flutter pub get` manually, or follow the steps in ${cannotRunInVSCod verifyCorrectFlutterPubGet( () => expectLater( () => runWithOverrides(() => builder.buildAppBundle()), - throwsA(isA()), + throwsA( + isA().having( + (e) => e.fixRecommendation, + 'recommendation', + startsWith( + ArtifactBuilder.runVanillaFlutterBuildRecommendation( + '', + ).substring(0, 80), + ), + ), + ), ), ); }); @@ -534,7 +544,17 @@ Either run `flutter pub get` manually, or follow the steps in ${cannotRunInVSCod verifyCorrectFlutterPubGet( () => expectLater( () => runWithOverrides(() => builder.buildApk()), - throwsA(isA()), + throwsA( + isA().having( + (e) => e.fixRecommendation, + 'recommendation', + startsWith( + ArtifactBuilder.runVanillaFlutterBuildRecommendation( + '', + ).substring(0, 80), + ), + ), + ), ), ); }); @@ -651,7 +671,17 @@ Either run `flutter pub get` manually, or follow the steps in ${cannotRunInVSCod () async => runWithOverrides( () => builder.buildAar(buildNumber: buildNumber), ), - throwsA(isA()), + throwsA( + isA().having( + (e) => e.fixRecommendation, + 'recommendation', + startsWith( + ArtifactBuilder.runVanillaFlutterBuildRecommendation( + '', + ).substring(0, 80), + ), + ), + ), ), ); }); @@ -700,14 +730,24 @@ Either run `flutter pub get` manually, or follow the steps in ${cannotRunInVSCod expect( () => runWithOverrides(() => builder.buildLinuxApp()), throwsA( - isA().having( - (e) => e.message, - 'message', - equals(''' + isA() + .having( + (e) => e.message, + 'message', + equals(''' Failed to build linux app. Command: flutter build linux --release Reason: Exited with code 70.'''), - ), + ) + .having( + (e) => e.fixRecommendation, + 'recommendation', + startsWith( + ArtifactBuilder.runVanillaFlutterBuildRecommendation( + '', + ).substring(0, 80), + ), + ), ), ); }); @@ -890,7 +930,17 @@ Reason: Exited with code 70.'''), test('throws ArtifactBuildException', () { expect( () => runWithOverrides(() => builder.buildMacos(codesign: false)), - throwsA(isA()), + throwsA( + isA().having( + (e) => e.fixRecommendation, + 'recommendation', + startsWith( + ArtifactBuilder.runVanillaFlutterBuildRecommendation( + '', + ).substring(0, 80), + ), + ), + ), ); }); }); @@ -1067,7 +1117,17 @@ Reason: Exited with code 70.'''), test('throws ArtifactBuildException', () { expect( () => runWithOverrides(() => builder.buildIpa(codesign: false)), - throwsA(isA()), + throwsA( + isA().having( + (e) => e.fixRecommendation, + 'recommendation', + startsWith( + ArtifactBuilder.runVanillaFlutterBuildRecommendation( + '', + ).substring(0, 80), + ), + ), + ), ); }); }); @@ -1126,7 +1186,17 @@ Reason: Exited with code 70.'''), () async => expectLater( () async => runWithOverrides(() => builder.buildIpa(codesign: false)), - throwsA(isA()), + throwsA( + isA().having( + (e) => e.fixRecommendation, + 'recommendation', + startsWith( + ArtifactBuilder.runVanillaFlutterBuildRecommendation( + '', + ).substring(0, 80), + ), + ), + ), ), ); }); @@ -1271,7 +1341,17 @@ Reason: Exited with code 70.'''), verifyCorrectFlutterPubGet( () => expectLater( () => runWithOverrides(builder.buildIosFramework), - throwsA(isA()), + throwsA( + isA().having( + (e) => e.fixRecommendation, + 'recommendation', + startsWith( + ArtifactBuilder.runVanillaFlutterBuildRecommendation( + '', + ).substring(0, 80), + ), + ), + ), ), ); }); @@ -1391,14 +1471,24 @@ Reason: Exited with code 70.'''), expect( () => runWithOverrides(() => builder.buildWindowsApp()), throwsA( - isA().having( - (e) => e.message, - 'message', - equals(''' + isA() + .having( + (e) => e.message, + 'message', + equals(''' Failed to build windows app. Command: flutter build windows --release Reason: Exited with code 70.'''), - ), + ) + .having( + (e) => e.fixRecommendation, + 'recommendation', + startsWith( + ArtifactBuilder.runVanillaFlutterBuildRecommendation( + '', + ).substring(0, 80), + ), + ), ), ); });