Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,25 @@ extension on String {
/// This allow us to just call var?.toPublicKeyEnv() instead of doing
/// a ternary operation to check if the value is null.
Map<String, String> toPublicKeyEnv() => {'SHOREBIRD_PUBLIC_KEY': this};

/// Returns a map with the SHOREBIRD_MODULE_VERSION environment variable.
Map<String, String> toModuleVersionEnv() => {
'SHOREBIRD_MODULE_VERSION': this,
};
}

/// Combines public-key and module-version env vars into a single map for
/// passing through to `flutter build`. Returns `null` when neither is set
/// so we don't override the parent process env with an empty map.
Map<String, String>? _buildEnv({
String? base64PublicKey,
String? moduleVersion,
}) {
if (base64PublicKey == null && moduleVersion == null) return null;
return {
...?base64PublicKey?.toPublicKeyEnv(),
...?moduleVersion?.toModuleVersionEnv(),
};
}

/// @{template artifact_builder}
Expand Down Expand Up @@ -286,6 +305,7 @@ Reason: Exited with code $exitCode.''',
Iterable<Arch>? targetPlatforms,
List<String> args = const [],
String? base64PublicKey,
String? moduleVersion,
}) async {
return _runShorebirdBuildCommand(() async {
const executable = 'flutter';
Expand All @@ -304,7 +324,10 @@ Reason: Exited with code $exitCode.''',
final exitCode = await process.stream(
executable,
arguments,
environment: base64PublicKey?.toPublicKeyEnv(),
environment: _buildEnv(
base64PublicKey: base64PublicKey,
moduleVersion: moduleVersion,
),
// Never run in shell because we always have a fully resolved
// executable path.
runInShell: false,
Expand Down Expand Up @@ -506,6 +529,7 @@ Reason: Exited with code $exitCode.''',
Future<AppleBuildResult> buildIosFramework({
List<String> args = const [],
String? base64PublicKey,
String? moduleVersion,
}) async {
final projectRoot = shorebirdEnv.getShorebirdProjectRoot()!;
// Delete the .dart_tool directory to ensure that the app is rebuilt. This
Expand All @@ -530,7 +554,10 @@ Reason: Exited with code $exitCode.''',
final exitCode = await process.stream(
executable,
arguments,
environment: base64PublicKey?.toPublicKeyEnv(),
environment: _buildEnv(
base64PublicKey: base64PublicKey,
moduleVersion: moduleVersion,
),
// Never run in shell because we always have a fully resolved
// executable path.
runInShell: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,12 @@ class AarPatcher extends Patcher {
@override
Future<File> buildPatchArtifact({String? releaseVersion}) async {
final buildArgs = [...argResults.forwardedArgs, ...extraBuildArgs];

await artifactBuilder.buildAar(
buildNumber: buildNumber,
args: buildArgs,
base64PublicKey: argResults.encodedPublicKey,
moduleVersion: releaseVersion,
);

return File(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,7 @@ class AarReleaser extends Releaser {

@override
Future<void> assertArgsAreValid() async {
if (!argResults.wasParsed('release-version')) {
logger.err('Missing required argument: --release-version');
throw ProcessExit(ExitCode.usage.code);
}

await resolveModuleReleaseVersionArgs();
await assertObfuscationIsSupported();
}

Expand All @@ -86,12 +82,15 @@ class AarReleaser extends Releaser {
final buildArgs = [...argResults.forwardedArgs];
addSplitDebugInfoDefault(buildArgs);
addObfuscationMapArgs(buildArgs);

await artifactBuilder.buildAar(
buildNumber: buildNumber,
targetPlatforms: architectures,
args: buildArgs,
base64PublicKey: base64PublicKey,
moduleVersion: moduleVersion,
);

verifyObfuscationMap();

// Copy release AAR to a new directory to avoid overwriting with
Expand All @@ -111,7 +110,7 @@ class AarReleaser extends Releaser {
Future<String> getReleaseVersion({
required FileSystemEntity releaseArtifactRoot,
}) async {
return argResults['release-version'] as String;
return moduleReleaseVersion;
}

@override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import 'package:shorebird_cli/src/doctor.dart';
import 'package:shorebird_cli/src/executables/xcodebuild.dart';
import 'package:shorebird_cli/src/extensions/arg_results.dart';
import 'package:shorebird_cli/src/flutter_version_constraints.dart';
import 'package:shorebird_cli/src/logging/logging.dart';
import 'package:shorebird_cli/src/metadata/metadata.dart';
import 'package:shorebird_cli/src/release_type.dart';
import 'package:shorebird_cli/src/shorebird_env.dart';
Expand Down Expand Up @@ -50,11 +49,7 @@ class IosFrameworkReleaser extends Releaser {

@override
Future<void> assertArgsAreValid() async {
if (!argResults.wasParsed('release-version')) {
logger.err('Missing required argument: --release-version');
throw ProcessExit(ExitCode.usage.code);
}

await resolveModuleReleaseVersionArgs();
await assertObfuscationIsSupported();
}

Expand Down Expand Up @@ -93,6 +88,7 @@ class IosFrameworkReleaser extends Releaser {
await artifactBuilder.buildIosFramework(
args: buildArgs,
base64PublicKey: base64PublicKey,
moduleVersion: moduleVersion,
);
verifyObfuscationMap();

Expand Down Expand Up @@ -122,7 +118,7 @@ class IosFrameworkReleaser extends Releaser {
Future<String> getReleaseVersion({
required FileSystemEntity releaseArtifactRoot,
}) async {
return argResults['release-version'] as String;
return moduleReleaseVersion;
}

@override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,16 @@ Defaults to "latest" which builds using the latest stable Flutter version.''',
'release-version',
help: '''
The version of the associated release (e.g. "1.0.0"). This should be the version
of the iOS app that is using this module. (aar and ios-framework only)''',
of the host app that is using this module. (aar and ios-framework only)
Cannot be used with --module-version.''',
)
..addOption(
'module-version',
help: '''
The module version to embed in the artifact. This version identifies the module
independently of the host app's version, enabling add-to-app use cases.
Use "git" to automatically use the current git commit hash.
Cannot be used with --release-version. (aar and ios-framework only)''',
)
..addMultiOption(
'target-platform',
Expand Down
106 changes: 106 additions & 0 deletions packages/shorebird_cli/lib/src/commands/release/releaser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:path/path.dart' as p;
import 'package:pub_semver/pub_semver.dart';
import 'package:shorebird_cli/src/artifact_manager.dart';
import 'package:shorebird_cli/src/code_push_client_wrapper.dart';
import 'package:shorebird_cli/src/executables/git.dart';
import 'package:shorebird_cli/src/extensions/arg_results.dart';
import 'package:shorebird_cli/src/flutter_version_constraints.dart';
import 'package:shorebird_cli/src/logging/logging.dart';
Expand Down Expand Up @@ -101,6 +102,111 @@ abstract class Releaser {
/// passed either to Shorebird directly or forwarded to Flutter.
bool get useObfuscation => argResults.flagPresent('obfuscate');

// -- Module version support (shared by AAR and iOS framework releasers) --

/// The module version baked into the artifact. Null when using
/// --release-version (no module version baked in).
String? moduleVersion;

/// The explicit --release-version value. Null in the module-version flow.
String? explicitReleaseVersion;

/// Resolves the full git commit hash for the project.
Future<String> getGitHash() async {
try {
return await git.revParse(
revision: 'HEAD',
directory: projectRoot.path,
);
} on ProcessException {
logger.err(
'Failed to determine git revision. '
'Provide --module-version explicitly or run from a git repository.',
);
throw ProcessExit(ExitCode.software.code);
}
}

/// Checks that the current Flutter version supports module versions.
Future<void> assertFlutterSupportsModuleVersion() async {
final flutterVersion = await shorebirdFlutter.resolveFlutterVersion(
shorebirdEnv.flutterRevision,
);
if (flutterVersion == null ||
flutterVersion < minimumModuleVersionFlutterVersion) {
logger.err(
'Module versions require '
'Flutter $minimumModuleVersionFlutterVersion or later '
'(current: $flutterVersion).',
);
throw ProcessExit(ExitCode.unavailable.code);
}
}

/// Resolves --release-version / --module-version arguments for module
/// releasers (AAR, iOS framework). Call from [assertArgsAreValid].
///
/// Sets [moduleVersion] and [explicitReleaseVersion] based on the args.
Future<void> resolveModuleReleaseVersionArgs() async {
final hasReleaseVersion = argResults.wasParsed('release-version');
final hasModuleVersion = argResults.wasParsed('module-version');

if (hasReleaseVersion && hasModuleVersion) {
logger.err(
'--release-version and --module-version cannot be used together.',
);
throw ProcessExit(ExitCode.usage.code);
}

if (hasReleaseVersion) {
explicitReleaseVersion = argResults['release-version'] as String;
} else if (hasModuleVersion) {
await assertFlutterSupportsModuleVersion();

final moduleVersionArg = argResults['module-version'] as String;
if (moduleVersionArg == 'git') {
moduleVersion = await getGitHash();
} else {
moduleVersion = moduleVersionArg;
}
} else {
if (!shorebirdEnv.canAcceptUserInput) {
logger.err(
'No --release-version or --module-version provided. '
'In non-interactive mode, one of these is required.\n'
'Use --module-version=git to use the git commit hash.',
);
throw ProcessExit(ExitCode.usage.code);
}

await assertFlutterSupportsModuleVersion();

final gitHash = await getGitHash();

moduleVersion = logger.prompt(
'Module version',
defaultValue: gitHash,
);
}

if (moduleVersion != null) {
logger.info(
'This ${artifactDisplayName.toLowerCase()} will embed module '
'version ${lightCyan.wrap(moduleVersion)}, allowing it to work '
'across host apps with different version numbers.\n'
'Module versions are not enforced to be unique by an app store. '
'If you rebuild your module with different native dependencies '
'and reuse the same module version, patches may have unexpected '
'behavior at runtime. We recommend using the git commit hash as '
'the module version (--module-version=git).',
);
}
}

/// Returns the release version for module releasers. Uses module version
/// when set, otherwise the explicit --release-version.
String get moduleReleaseVersion => moduleVersion ?? explicitReleaseVersion!;

/// Path where the obfuscation map is saved during obfuscated builds.
String get obfuscationMapPath => p.join(
projectRoot.path,
Expand Down
8 changes: 8 additions & 0 deletions packages/shorebird_cli/lib/src/config/shorebird_yaml.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class ShorebirdYaml {
this.baseUrl,
this.autoUpdate,
this.patchVerification,
this.moduleVersion,
});

/// Creates a [ShorebirdYaml] from a JSON map.
Expand Down Expand Up @@ -61,6 +62,13 @@ class ShorebirdYaml {

/// The patch verification mode for the app.
final PatchVerification? patchVerification;

/// The module version baked into the artifact at build time.
///
/// Used for add-to-app (AAR) releases where the host app's version differs
/// from the module's version. When present, the engine uses this instead of
/// the host app's version to identify the release for patch checking.
final String? moduleVersion;
}

/// Extension on [ShorebirdYaml] to get the app id for a specific flavor.
Expand Down
4 changes: 4 additions & 0 deletions packages/shorebird_cli/lib/src/config/shorebird_yaml.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ final minimumSupportedLinuxFlutterVersion = Version(3, 27, 4);
/// available in this version.
final minimumSupportedWindowsFlutterVersion = Version(3, 32, 6);

/// Minimum Flutter version for module version support.
///
/// This version introduced SHOREBIRD_MODULE_VERSION env var support in the
/// Flutter tool, which allows AAR releases to embed a version identity
/// independent of the host app's version.
final minimumModuleVersionFlutterVersion = Version(3, 41, 4);

/// Minimum Flutter version for obfuscation support across all platforms.
///
/// Obfuscation requires gen_snapshot changes (--save-obfuscation-map and
Expand Down
Loading