From 4b6693a50ed053c9e8ddc2957b9b28ed0e9055da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=81=A5=E8=8E=B9?= Date: Mon, 2 Sep 2024 22:08:10 +0800 Subject: [PATCH] wip: any_app_packager package add package-any-app executable --- packages/any_app_packager/bin/main.dart | 211 +++++++++++++++++- .../lib/src/any_app_packager.dart | 119 ++++++++-- .../lib/src/utils/logger.dart | 6 + packages/any_app_packager/pubspec.yaml | 6 +- packages/flutter_app_packager/pubspec.yaml | 2 +- packages/parse_app_package/pubspec.yaml | 2 +- 6 files changed, 325 insertions(+), 21 deletions(-) create mode 100644 packages/any_app_packager/lib/src/utils/logger.dart diff --git a/packages/any_app_packager/bin/main.dart b/packages/any_app_packager/bin/main.dart index 72267c4f..79e8e296 100644 --- a/packages/any_app_packager/bin/main.dart +++ b/packages/any_app_packager/bin/main.dart @@ -1,3 +1,210 @@ -void main() { - print('any_app_packager is still under development'); +import 'dart:io'; + +import 'package:any_app_packager/src/any_app_packager.dart'; +import 'package:args/args.dart'; +import 'package:args/command_runner.dart'; + +/// Package an application bundle for a specific platform and target +/// +/// This command wrapper defines, parses and transforms all passed arguments, +/// so that they may be passed to `flutter_distributor`. The distributor will +/// then build an application bundle using `flutter_app_packager`. +class PackageCommand extends Command { + PackageCommand(this.appPackager) { + argParser.addOption( + 'platform', + valueHelp: [ + 'android', + 'ios', + 'linux', + 'macos', + 'windows', + 'web', + ].join(','), + help: 'The platform to package the application for', + ); + + argParser.addOption( + 'targets', + aliases: ['target'], + valueHelp: [ + 'apk', + 'aab', + 'appimage', + 'deb', + 'dmg', + 'exe', + 'ipa', + 'msix', + 'pkg', + 'rpm', + 'zip', + ].join(','), + help: 'Comma separated list of bundle types to build.', + ); + + argParser.addOption('channel', valueHelp: ''); + argParser.addOption('artifact-name', valueHelp: ''); + + argParser.addFlag( + 'skip-clean', + help: 'Whether or not to skip \'flutter clean\' before packaging.', + ); + + argParser.addOption( + 'flutter-build-args', + valueHelp: 'verbose,obfuscate', + help: 'Arguments to pass directly to flutter build', + ); + + argParser.addOption( + 'build-target', + valueHelp: 'path', + help: 'The --target argument passed to \'flutter build\'', + ); + + argParser.addOption( + 'build-flavor', + valueHelp: '', + help: 'The --flavor argument passed to \'flutter build\'', + ); + + argParser.addOption( + 'build-target-platform', + valueHelp: '', + help: 'The --target-platform argument passed to \'flutter build\'', + ); + + argParser.addOption( + 'build-export-options-plist', + valueHelp: '', + help: 'The --export-options-plist argument passed \'flutter build\'', + ); + + argParser.addMultiOption( + 'build-dart-define', + valueHelp: 'foo=bar', + help: [ + 'The --dart-define argument(s) passed to \'flutter build\'', + 'You may add multiple \'--build-dart-define key=value\' pairs', + ].join('\n'), + ); + } + + final AnyAppPackager appPackager; + + @override + String get name => 'package'; + + @override + String get description => [ + 'Package the current Flutter application', + '', + 'Options named --build-* are passed to \'flutter build\' as is', + 'Please consult the \'flutter build\' CLI help for more informations.', + ].join('\n'); + + @override + Future run() async { + final String? platform = argResults?['platform']; + final List targets = '${argResults?['targets'] ?? ''}' + .split(',') + .where((e) => e.isNotEmpty) + .toList(); + final String? channel = argResults?['channel']; + final String? artifactName = argResults?['artifact-name']; + final String? flutterBuildArgs = argResults?['flutter-build-args']; + final bool isSkipClean = argResults?.wasParsed('skip-clean') ?? false; + final Map buildArguments = + _generateBuildArgs(flutterBuildArgs); + + // At least `platform` and one `targets` is required for flutter build + if (platform == null) { + print('\nThe \'platform\' options is mandatory!'); + exit(1); + } + + if (targets.isEmpty) { + print('\nAt least one \'target\' must be specified!'); + exit(1); + } + + return appPackager.package( + platform, + targets, + channel: channel, + artifactName: artifactName, + cleanBeforeBuild: !isSkipClean, + buildArguments: buildArguments, + ); + } + + Map _generateBuildArgs(String? flutterBuildArgs) { + Map buildArguments = {}; + + if (argResults?.options == null) return buildArguments; + + for (var option in argResults!.options) { + if (!option.startsWith('build-')) continue; + dynamic value = argResults?[option]; + + if (value is List) { + // ignore: prefer_for_elements_to_map_fromiterable + value = Map.fromIterable( + value, + key: (e) => e.split('=')[0], + value: (e) => e.split('=')[1], + ); + } + + buildArguments.putIfAbsent( + option.replaceAll('build-', ''), + () => value, + ); + } + + for (var arg in flutterBuildArgs?.split(',') ?? []) { + if (arg.split('=').length == 2) { + buildArguments.putIfAbsent( + arg.split('=').first, + () => arg.split('=').last, + ); + } else if (arg.split('=').length == 1) { + buildArguments.putIfAbsent( + arg.split('=')[0], + () => true, + ); + } else { + buildArguments.putIfAbsent(arg, () => true); + } + } + + return buildArguments; + } +} + +Future main(List args) async { + final AnyAppPackager appPackager = AnyAppPackager(); + + final runner = CommandRunner( + 'any_app_packager', + 'Package your any app into OS-specific bundles (.dmg, .exe, etc.) via Dart or the command line.', + ); + runner.argParser + ..addFlag( + 'version', + help: 'Reports the version of this tool.', + negatable: false, + ) + ..addFlag( + 'version-check', + help: 'Check for updates when this command runs.', + defaultsTo: true, + negatable: true, + ); + + runner.addCommand(PackageCommand(appPackager)); + + ArgResults argResults = runner.parse(['package', ...args]); + return runner.runCommand(argResults); } diff --git a/packages/any_app_packager/lib/src/any_app_packager.dart b/packages/any_app_packager/lib/src/any_app_packager.dart index ee2da201..85a36604 100644 --- a/packages/any_app_packager/lib/src/any_app_packager.dart +++ b/packages/any_app_packager/lib/src/any_app_packager.dart @@ -1,26 +1,113 @@ +import 'dart:convert'; import 'dart:io'; +import 'package:any_app_packager/src/utils/logger.dart'; +import 'package:flutter_app_builder/flutter_app_builder.dart'; import 'package:flutter_app_packager/flutter_app_packager.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; class AnyAppPackager { - final FlutterAppPackager _flutterAppPackager = FlutterAppPackager(); + final FlutterAppBuilder _appBuilder = FlutterAppBuilder(); + final FlutterAppPackager _appPackager = FlutterAppPackager(); + + final Map _globalVariables = {}; + Map get globalVariables { + if (_globalVariables.keys.isEmpty) { + for (String key in Platform.environment.keys) { + String? value = Platform.environment[key]; + if ((value ?? '').isNotEmpty) { + _globalVariables[key] = value!; + } + } + } + return _globalVariables; + } /// Packages the app for the given platform and target. - Future package( + Future> package( String platform, - String target, - Map? arguments, - Directory outputDirectory, { - required Directory buildOutputDirectory, - required List buildOutputFiles, - }) { - return _flutterAppPackager.package( - platform, - target, - arguments, - outputDirectory, - buildOutputDirectory: buildOutputDirectory, - buildOutputFiles: buildOutputFiles, - ); + List targets, { + String? channel, + String? artifactName, + required bool cleanBeforeBuild, + required Map buildArguments, + Map? variables, + }) async { + List makeResultList = []; + + try { + Directory outputDirectory = Directory('dist/'); + if (!outputDirectory.existsSync()) { + outputDirectory.createSync(recursive: true); + } + + if (cleanBeforeBuild) { + await _appBuilder.clean(); + } + + bool isBuildOnlyOnce = platform != 'android'; + BuildResult? buildResult; + + final yamlString = File('pubspec.yaml').readAsStringSync(); + Pubspec pubspec = Pubspec.parse(yamlString); + + for (String target in targets) { + logger.info('Packaging ${pubspec.name} ${pubspec.version} as $target:'); + if (!isBuildOnlyOnce || (isBuildOnlyOnce && buildResult == null)) { + try { + buildResult = await _appBuilder.build( + platform, + target: target, + arguments: buildArguments, + environment: variables ?? globalVariables, + ); + print( + const JsonEncoder.withIndent(' ').convert(buildResult.toJson()), + ); + + logger.fine( + 'Successfully built ${buildResult.outputDirectory} in ${buildResult.duration!.inSeconds}s', + ); + } on UnsupportedError catch (error) { + logger.warning('Warning: ${error.message}'); + continue; + } catch (error) { + rethrow; + } + } + + if (buildResult != null) { + String buildMode = + buildArguments.containsKey('profile') ? 'profile' : 'release'; + Map? arguments = { + 'build_mode': buildMode, + 'flavor': buildArguments['flavor'], + 'channel': channel, + 'artifact_name': artifactName, + }; + MakeResult makeResult = await _appPackager.package( + platform, + target, + arguments, + outputDirectory, + buildOutputDirectory: buildResult.outputDirectory, + buildOutputFiles: buildResult.outputFiles, + ); + print( + const JsonEncoder.withIndent(' ').convert(makeResult.toJson()), + ); + FileSystemEntity artifact = makeResult.artifacts.first; + logger.fine('Successfully packaged ${artifact.path}'); + makeResultList.add(makeResult); + } + } + } catch (error) { + logger.severe(error.toString()); + if (error is Error) { + logger.severe(error.stackTrace.toString()); + } + rethrow; + } + return makeResultList; } } diff --git a/packages/any_app_packager/lib/src/utils/logger.dart b/packages/any_app_packager/lib/src/utils/logger.dart new file mode 100644 index 00000000..8c5d4864 --- /dev/null +++ b/packages/any_app_packager/lib/src/utils/logger.dart @@ -0,0 +1,6 @@ +import 'package:logging/logging.dart'; + +Logger logger = Logger('any_app_packager') + ..onRecord.listen((record) { + print(record.message); + }); diff --git a/packages/any_app_packager/pubspec.yaml b/packages/any_app_packager/pubspec.yaml index f83d181d..564f3d51 100644 --- a/packages/any_app_packager/pubspec.yaml +++ b/packages/any_app_packager/pubspec.yaml @@ -8,11 +8,15 @@ environment: sdk: ">=2.16.0 <4.0.0" dependencies: + args: ^2.2.0 + flutter_app_builder: ^0.4.5 flutter_app_packager: ^0.4.5 + logging: ^1.0.2 + pubspec_parse: ^1.1.0 dev_dependencies: dependency_validator: ^3.0.0 mostly_reasonable_lints: ^0.1.2 executables: - packageanyapp: main + package-any-app: main diff --git a/packages/flutter_app_packager/pubspec.yaml b/packages/flutter_app_packager/pubspec.yaml index 86db00aa..a60dd4bf 100644 --- a/packages/flutter_app_packager/pubspec.yaml +++ b/packages/flutter_app_packager/pubspec.yaml @@ -7,7 +7,7 @@ environment: sdk: ">=2.16.0 <4.0.0" dependencies: - archive: ^3.4.10 + archive: ^3.6.1 io: ^1.0.3 liquid_engine: ^0.2.2 msix: ^3.16.6 diff --git a/packages/parse_app_package/pubspec.yaml b/packages/parse_app_package/pubspec.yaml index db9b2361..24dfdc41 100644 --- a/packages/parse_app_package/pubspec.yaml +++ b/packages/parse_app_package/pubspec.yaml @@ -8,7 +8,7 @@ environment: sdk: ">=2.16.0 <4.0.0" dependencies: - archive: ^3.4.10 + archive: ^3.6.1 args: ^2.2.0 plist_parser: ^0.0.11 shell_executor: ^0.1.6