Create open Shorebird workspace meta-repo

This commit is contained in:
Tony
2026-06-24 03:16:46 +08:00
commit 56f121c517
27 changed files with 1135 additions and 0 deletions
@@ -0,0 +1,7 @@
import 'package:license_flavor_patch_test/license.dart';
void main() {
final license = currentLicenseType();
print('license:${license.name}');
print(proFeatureEnabled ? 'pro-feature:enabled' : 'pro-feature:off');
}
@@ -0,0 +1,14 @@
enum LicenseType { free, pro, enterprise }
LicenseType currentLicenseType() {
const license = String.fromEnvironment('LICENSE_TYPE', defaultValue: 'free');
return switch (license) {
'pro' => LicenseType.pro,
'enterprise' => LicenseType.enterprise,
_ => LicenseType.free,
};
}
bool get proFeatureEnabled =>
currentLicenseType() == LicenseType.pro ||
currentLicenseType() == LicenseType.enterprise;
@@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
import 'license.dart';
void main() => runApp(const LicenseFlavorPatchApp());
class LicenseFlavorPatchApp extends StatelessWidget {
const LicenseFlavorPatchApp({super.key});
@override
Widget build(BuildContext context) {
final license = currentLicenseType();
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('License Patch Test')),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'license:${license.name}',
key: const ValueKey('license-status'),
),
Text(
proFeatureEnabled ? 'pro-feature:enabled' : 'pro-feature:off',
key: const ValueKey('feature-status'),
),
],
),
),
),
);
}
}
@@ -0,0 +1,189 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
async:
dependency: transitive
description:
name: async
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
url: "https://pub.dev"
source: hosted
version: "2.13.1"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
characters:
dependency: transitive
description:
name: characters
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.4.1"
clock:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.2"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.3"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
matcher:
dependency: transitive
description:
name: matcher
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev"
source: hosted
version: "0.12.19"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
version: "0.13.0"
meta:
dependency: transitive
description:
name: meta
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.17.0"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
source_span:
dependency: transitive
description:
name: source_span
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.dev"
source: hosted
version: "1.10.2"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.dev"
source: hosted
version: "0.7.10"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
source: hosted
version: "2.2.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
url: "https://pub.dev"
source: hosted
version: "15.2.0"
sdks:
dart: ">=3.9.0 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54"
@@ -0,0 +1,19 @@
name: license_flavor_patch_test
description: Test app for encrypted flavor/license AOT patch verification.
publish_to: none
environment:
sdk: ^3.9.0
dependencies:
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
flutter:
uses-material-design: true
assets:
- shorebird.yaml
@@ -0,0 +1,5 @@
app_id: license-flavor-patch-test
flavors:
free: license-flavor-free
pro: license-flavor-pro
enterprise: license-flavor-enterprise
@@ -0,0 +1,19 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:license_flavor_patch_test/license.dart';
import 'package:license_flavor_patch_test/main.dart';
void main() {
final isProFlavor = currentLicenseType() == LicenseType.pro;
test('pro flavor enables pro feature gate', () {
expect(currentLicenseType(), LicenseType.pro);
expect(proFeatureEnabled, isTrue);
}, skip: !isProFlavor);
testWidgets('renders pro license status labels', (tester) async {
await tester.pumpWidget(const LicenseFlavorPatchApp());
expect(find.text('license:pro'), findsOneWidget);
expect(find.text('pro-feature:enabled'), findsOneWidget);
}, skip: !isProFlavor);
}
@@ -0,0 +1,17 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:license_flavor_patch_test/license.dart';
import 'package:license_flavor_patch_test/main.dart';
void main() {
test('defaults to free license flavor', () {
expect(currentLicenseType(), LicenseType.free);
expect(proFeatureEnabled, isFalse);
});
testWidgets('renders free license status labels', (tester) async {
await tester.pumpWidget(const LicenseFlavorPatchApp());
expect(find.text('license:free'), findsOneWidget);
expect(find.text('pro-feature:off'), findsOneWidget);
});
}
@@ -0,0 +1,363 @@
import 'dart:convert';
import 'dart:io';
Future<void> main() async {
final appDir = File.fromUri(Platform.script).parent.parent;
final workspaceRoot = appDir.parent.parent;
final dartRoot = Directory('${workspaceRoot.path}/dart-sdk-new');
final patchBuildDir = Directory('${dartRoot.path}/out/ReleaseX64AotPatch');
final toolWorkspace = Directory('${workspaceRoot.path}/shorebird');
final workDir = Directory('${appDir.path}/build/open_aot_patch_verify');
final argsFile = File('${patchBuildDir.path}/args.gn');
if (!argsFile.existsSync()) {
throw StateError(
'Missing ${argsFile.path}. Build the patch runtime first with '
'dart_enable_aot_patching=true and dart_dynamic_modules=false.',
);
}
final args = argsFile.readAsStringSync();
if (!args.contains('dart_enable_aot_patching = true') ||
!args.contains('dart_dynamic_modules = false')) {
throw StateError(
'${argsFile.path} must enable AOT patching and disable dynamic modules.',
);
}
if (workDir.existsSync()) {
workDir.deleteSync(recursive: true);
}
workDir.createSync(recursive: true);
final packageConfig = File('${workDir.path}/package_config.json');
packageConfig.writeAsStringSync(
jsonEncode({
'configVersion': 2,
'packages': [
{
'name': 'license_flavor_patch_test',
'rootUri': appDir.uri.toString(),
'packageUri': 'lib/',
'languageVersion': '3.9',
},
],
}),
);
final baseMap = File('${workDir.path}/base.obfuscation.json');
final patchMap = File('${workDir.path}/patch.obfuscation.json');
final baseVmcode = File('${workDir.path}/base_free.vmcode');
final patchVmcode = File('${workDir.path}/patch_pro.vmcode');
final reconstructedVmcode = File('${workDir.path}/reconstructed_pro.vmcode');
await _compileAot(
workspaceRoot: workspaceRoot,
dartRoot: dartRoot,
patchBuildDir: patchBuildDir,
packageConfig: packageConfig,
licenseType: 'free',
outputVmcode: baseVmcode,
saveObfuscationMap: baseMap,
);
await _compileAot(
workspaceRoot: workspaceRoot,
dartRoot: dartRoot,
patchBuildDir: patchBuildDir,
packageConfig: packageConfig,
licenseType: 'pro',
outputVmcode: patchVmcode,
loadObfuscationMap: baseMap,
saveObfuscationMap: patchMap,
);
_expectStatus(
await _runAot(patchBuildDir, baseVmcode),
license: 'free',
proFeature: false,
);
_expectStatus(
await _runAot(patchBuildDir, patchVmcode),
license: 'pro',
proFeature: true,
);
final artifact = File('${workDir.path}/patch.json');
final encrypted = File('${workDir.path}/patch.encrypted.json');
const keyHex =
'000102030405060708090a0b0c0d0e0f'
'101112131415161718191a1b1c1d1e1f';
const wrongKeyHex =
'1f1e1d1c1b1a19181716151413121110'
'0f0e0d0c0b0a09080706050403020100';
const nonceHex = '000102030405060708090a0b';
const offlineExpiresAt = '2030-01-01T00:00:00Z';
const beforeExpiry = '2029-01-01T00:00:00Z';
const afterExpiry = '2031-01-01T00:00:00Z';
await _runTool(toolWorkspace, [
'link',
'--base=${baseVmcode.path}',
'--patch=${patchVmcode.path}',
'--output=${artifact.path}',
'--app-id=license-flavor-patch-test',
'--app-build-id=host-aot-smoke',
'--base-flavor-id=free',
'--base-license-type=free',
'--flavor-id=pro',
'--license-type=pro',
'--sdk-hash=${_sha256File(File('${patchBuildDir.path}/gen_snapshot.exe'))}',
'--target-os=windows',
'--target-arch=x64',
'--obfuscation-map-hash=${_sha256File(baseMap)}',
'--offline-expires-at=$offlineExpiresAt',
'--full-snapshot=true',
]);
final artifactJson = (jsonDecode(artifact.readAsStringSync()) as Map)
.cast<String, Object?>();
if (artifactJson['payload_kind'] != 'full-snapshot') {
throw StateError('Expected a directly loadable full-snapshot payload.');
}
final metadata = (artifactJson['metadata'] as Map).cast<String, Object?>();
if (metadata['offline_expires_at'] != '2030-01-01T00:00:00.000Z') {
throw StateError('Expected normalized offline_expires_at metadata.');
}
await _runTool(toolWorkspace, [
'encrypt',
'--input=${artifact.path}',
'--output=${encrypted.path}',
'--key-id=test-key',
'--key-hex=$keyHex',
'--nonce-hex=$nonceHex',
]);
await _runTool(toolWorkspace, [
'verify',
'--input=${encrypted.path}',
'--key-hex=$keyHex',
'--artifact-sha256=${_sha256File(encrypted)}',
'--base-flavor-id=free',
'--base-license-type=free',
'--flavor-id=pro',
'--license-type=pro',
'--base=${baseVmcode.path}',
'--now=$beforeExpiry',
]);
await _runToolExpectFailure(toolWorkspace, [
'verify',
'--input=${encrypted.path}',
'--key-hex=$keyHex',
'--base-flavor-id=free',
'--base-license-type=free',
'--flavor-id=pro',
'--license-type=pro',
'--base=${baseVmcode.path}',
'--now=$afterExpiry',
]);
await _runToolExpectFailure(toolWorkspace, [
'verify',
'--input=${encrypted.path}',
'--key-hex=$wrongKeyHex',
'--base-flavor-id=free',
'--base-license-type=free',
'--flavor-id=pro',
'--license-type=pro',
'--base=${baseVmcode.path}',
]);
await _runToolExpectFailure(toolWorkspace, [
'verify',
'--input=${encrypted.path}',
'--key-hex=$keyHex',
'--base-flavor-id=free',
'--base-license-type=free',
'--flavor-id=enterprise',
'--license-type=pro',
'--base=${baseVmcode.path}',
]);
await _runTool(toolWorkspace, [
'dump-blobs',
'--input=${encrypted.path}',
'--key-hex=$keyHex',
'--base=${baseVmcode.path}',
'--output=${reconstructedVmcode.path}',
'--now=$beforeExpiry',
]);
if (_sha256File(reconstructedVmcode) != _sha256File(patchVmcode)) {
throw StateError('Reconstructed patch vmcode does not match pro snapshot.');
}
_expectStatus(
await _runAot(patchBuildDir, reconstructedVmcode),
license: 'pro',
proFeature: true,
);
await _runToolExpectFailure(toolWorkspace, [
'dump-blobs',
'--input=${encrypted.path}',
'--key-hex=$keyHex',
'--base=${baseVmcode.path}',
'--output=${reconstructedVmcode.path}',
'--now=$afterExpiry',
]);
stdout.writeln(
'AOT patch applied to license_flavor_patch_test successfully.',
);
stdout.writeln('base: license:free / pro-feature:off');
stdout.writeln('patch: license:pro / pro-feature:enabled');
stdout.writeln('artifact: ${encrypted.path}');
stdout.writeln('vmcode: ${reconstructedVmcode.path}');
}
Future<void> _compileAot({
required Directory workspaceRoot,
required Directory dartRoot,
required Directory patchBuildDir,
required File packageConfig,
required String licenseType,
required File outputVmcode,
required File saveObfuscationMap,
File? loadObfuscationMap,
}) async {
final dart = '${dartRoot.path}/tools/sdks/dart-sdk/bin/dart.exe';
final genKernel = '${dartRoot.path}/pkg/vm/bin/gen_kernel.dart';
final source =
'${workspaceRoot.path}/testapps/license_flavor_patch_test/bin/license_status.dart';
final dill = File('${outputVmcode.parent.path}/${licenseType}_app.dill');
await _run(dart, [
genKernel,
'--packages',
packageConfig.path,
'--platform',
'${patchBuildDir.path}/vm_platform_product.dill',
'--aot',
'--target-os',
'windows',
'-Ddart.vm.product=true',
'-DLICENSE_TYPE=$licenseType',
'-o',
dill.path,
'--invocation-modes=compile',
'--verbosity=all',
source,
]);
final genSnapshot = '${patchBuildDir.path}/gen_snapshot.exe';
await _run(genSnapshot, [
'--snapshot-kind=app-aot-elf',
'--elf=${outputVmcode.path}',
'--strip',
'--obfuscate',
if (loadObfuscationMap != null)
'--load-obfuscation-map=${loadObfuscationMap.path}',
'--save-obfuscation-map=${saveObfuscationMap.path}',
dill.path,
]);
}
Future<String> _runAot(Directory patchBuildDir, File vmcode) async {
final result = await _run('${patchBuildDir.path}/dartaotruntime.exe', [
vmcode.path,
]);
return result.stdout as String;
}
Future<ProcessResult> _runTool(
Directory toolWorkspace,
List<String> args,
) async {
return _run(
'${toolWorkspace.parent.path}/dart-sdk-new/tools/sdks/dart-sdk/bin/dart.exe',
['packages/open_aot_patch_tools/bin/open_aot_patch_tools.dart', ...args],
workingDirectory: toolWorkspace,
);
}
Future<ProcessResult> _runToolExpectFailure(
Directory toolWorkspace,
List<String> args,
) async {
final result = await Process.run(
'${toolWorkspace.parent.path}/dart-sdk-new/tools/sdks/dart-sdk/bin/dart.exe',
['packages/open_aot_patch_tools/bin/open_aot_patch_tools.dart', ...args],
workingDirectory: toolWorkspace.path,
);
if (result.exitCode == 0) {
throw StateError(
'Expected open_aot_patch_tools ${args.join(' ')} to fail.',
);
}
return result;
}
Future<ProcessResult> _run(
String executable,
List<String> args, {
Directory? workingDirectory,
}) async {
final result = await Process.run(
executable,
args,
workingDirectory: workingDirectory?.path,
);
if (result.exitCode != 0) {
throw ProcessException(
executable,
args,
[
'exit code ${result.exitCode}',
'stdout:',
result.stdout,
'stderr:',
result.stderr,
].join('\n'),
result.exitCode,
);
}
return result;
}
void _expectStatus(
String output, {
required String license,
required bool proFeature,
}) {
final lines = const LineSplitter()
.convert(output.trim())
.where((line) => line.isNotEmpty)
.toList();
final expectedFeature = proFeature ? 'enabled' : 'off';
if (!lines.contains('license:$license') ||
!lines.contains('pro-feature:$expectedFeature')) {
throw StateError(
'Unexpected app status. Expected license:$license and '
'pro-feature:$expectedFeature, got:\n$output',
);
}
}
String _sha256File(File file) {
final result = Process.runSync('certutil', [
'-hashfile',
file.path,
'SHA256',
]);
if (result.exitCode != 0) {
throw ProcessException(
'certutil',
['-hashfile', file.path, 'SHA256'],
result.stderr.toString(),
result.exitCode,
);
}
final match = RegExp(
r'\b[0-9a-fA-F]{64}\b',
).firstMatch('${result.stdout}\n${result.stderr}');
if (match == null) {
throw StateError('Could not parse SHA256 hash for ${file.path}.');
}
return match.group(0)!.toLowerCase();
}