// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:async'; import 'dart:convert'; import 'dart:io' hide Platform; import 'dart:typed_data'; import 'package:args/args.dart'; import 'package:crypto/crypto.dart'; import 'package:crypto/src/digest_sink.dart'; import 'package:http/http.dart' as http; import 'package:path/path.dart' as path; import 'package:platform/platform.dart' show LocalPlatform, Platform; import 'package:process/process.dart'; const String gobMirror = 'https://flutter.googlesource.com/mirrors/flutter'; const String githubRepo = 'https://github.com/flutter/flutter.git'; const String mingitForWindowsUrl = 'https://storage.googleapis.com/flutter_infra_release/mingit/' '603511c649b00bbef0a6122a827ac419b656bc19/mingit.zip'; const String releaseFolder = '/releases'; const String gsBase = 'gs://flutter_infra_release'; const String gsReleaseFolder = '$gsBase$releaseFolder'; const String baseUrl = 'https://storage.googleapis.com/flutter_infra_release'; const int shortCacheSeconds = 60; const String frameworkVersionTag = 'frameworkVersionFromGit'; const String dartVersionTag = 'dartSdkVersion'; const String dartTargetArchTag = 'dartTargetArch'; /// Exception class for when a process fails to run, so we can catch /// it and provide something more readable than a stack trace. class PreparePackageException implements Exception { PreparePackageException(this.message, [this.result]); final String message; final ProcessResult? result; int get exitCode => result?.exitCode ?? -1; @override String toString() { String output = runtimeType.toString(); if (message != null) { output += ': $message'; } final String stderr = result?.stderr as String? ?? ''; if (stderr.isNotEmpty) { output += ':\n$stderr'; } return output; } } enum Branch { beta, stable, master, main; } /// A helper class for classes that want to run a process, optionally have the /// stderr and stdout reported as the process runs, and capture the stdout /// properly without dropping any. class ProcessRunner { ProcessRunner({ ProcessManager? processManager, this.subprocessOutput = true, this.defaultWorkingDirectory, this.platform = const LocalPlatform(), }) : processManager = processManager ?? const LocalProcessManager() { environment = Map.from(platform.environment); } /// The platform to use for a starting environment. final Platform platform; /// Set [subprocessOutput] to show output as processes run. Stdout from the /// process will be printed to stdout, and stderr printed to stderr. final bool subprocessOutput; /// Set the [processManager] in order to inject a test instance to perform /// testing. final ProcessManager processManager; /// Sets the default directory used when `workingDirectory` is not specified /// to [runProcess]. final Directory? defaultWorkingDirectory; /// The environment to run processes with. late Map environment; /// Run the command and arguments in `commandLine` as a sub-process from /// `workingDirectory` if set, or the [defaultWorkingDirectory] if not. Uses /// [Directory.current] if [defaultWorkingDirectory] is not set. /// /// Set `failOk` if [runProcess] should not throw an exception when the /// command completes with a non-zero exit code. Future runProcess( List commandLine, { Directory? workingDirectory, bool failOk = false, }) async { workingDirectory ??= defaultWorkingDirectory ?? Directory.current; if (subprocessOutput) { stderr.write('Running "${commandLine.join(' ')}" in ${workingDirectory.path}.\n'); } final List output = []; final Completer stdoutComplete = Completer(); final Completer stderrComplete = Completer(); late Process process; Future allComplete() async { await stderrComplete.future; await stdoutComplete.future; return process.exitCode; } try { process = await processManager.start( commandLine, workingDirectory: workingDirectory.absolute.path, environment: environment, ); process.stdout.listen( (List event) { output.addAll(event); if (subprocessOutput) { stdout.add(event); } }, onDone: () async => stdoutComplete.complete(), ); if (subprocessOutput) { process.stderr.listen( (List event) { stderr.add(event); }, onDone: () async => stderrComplete.complete(), ); } else { stderrComplete.complete(); } } on ProcessException catch (e) { final String message = 'Running "${commandLine.join(' ')}" in ${workingDirectory.path} ' 'failed with:\n$e'; throw PreparePackageException(message); } on ArgumentError catch (e) { final String message = 'Running "${commandLine.join(' ')}" in ${workingDirectory.path} ' 'failed with:\n$e'; throw PreparePackageException(message); } final int exitCode = await allComplete(); if (exitCode != 0 && !failOk) { final String message = 'Running "${commandLine.join(' ')}" in ${workingDirectory.path} failed'; throw PreparePackageException( message, ProcessResult(0, exitCode, null, 'returned $exitCode'), ); } return utf8.decoder.convert(output).trim(); } } typedef HttpReader = Future Function(Uri url, {Map headers}); /// Creates a pre-populated Flutter archive from a git repo. class ArchiveCreator { /// [tempDir] is the directory to use for creating the archive. The script /// will place several GiB of data there, so it should have available space. /// /// The processManager argument is used to inject a mock of [ProcessManager] for /// testing purposes. /// /// If subprocessOutput is true, then output from processes invoked during /// archive creation is echoed to stderr and stdout. factory ArchiveCreator( Directory tempDir, Directory outputDir, String revision, Branch branch, { bool strict = true, ProcessManager? processManager, bool subprocessOutput = true, Platform platform = const LocalPlatform(), HttpReader? httpReader, }) { final Directory flutterRoot = Directory(path.join(tempDir.path, 'flutter')); final ProcessRunner processRunner = ProcessRunner( processManager: processManager, subprocessOutput: subprocessOutput, platform: platform, )..environment['PUB_CACHE'] = path.join( flutterRoot.absolute.path, '.pub-cache', ); final String flutterExecutable = path.join( flutterRoot.absolute.path, 'bin', 'flutter', ); final String dartExecutable = path.join( flutterRoot.absolute.path, 'bin', 'cache', 'dart-sdk', 'bin', 'dart', ); return ArchiveCreator._( tempDir: tempDir, platform: platform, flutterRoot: flutterRoot, outputDir: outputDir, revision: revision, branch: branch, strict: strict, processRunner: processRunner, httpReader: httpReader ?? http.readBytes, flutterExecutable: flutterExecutable, dartExecutable: dartExecutable, ); } ArchiveCreator._({ required this.tempDir, required this.platform, required this.flutterRoot, required this.outputDir, required this.revision, required this.branch, required this.strict, required ProcessRunner processRunner, required this.httpReader, required String flutterExecutable, required String dartExecutable, }) : assert(revision.length == 40), _processRunner = processRunner, _flutter = flutterExecutable, _dart = dartExecutable; /// The platform to use for the environment and determining which /// platform we're running on. final Platform platform; /// The branch to build the archive for. The branch must contain [revision]. final Branch branch; /// The git revision hash to build the archive for. This revision has /// to be available in the [branch], although it doesn't have to be /// at HEAD, since we clone the branch and then reset to this revision /// to create the archive. final String revision; /// The flutter root directory in the [tempDir]. final Directory flutterRoot; /// The temporary directory used to build the archive in. final Directory tempDir; /// The directory to write the output file to. final Directory outputDir; /// True if the creator should be strict about checking requirements or not. /// /// In strict mode, will insist that the [revision] be a tagged revision. final bool strict; final Uri _minGitUri = Uri.parse(mingitForWindowsUrl); final ProcessRunner _processRunner; /// Used to tell the [ArchiveCreator] which function to use for reading /// bytes from a URL. Used in tests to inject a fake reader. Defaults to /// [http.readBytes]. final HttpReader httpReader; final Map _version = {}; late String _flutter; late String _dart; late final Future _dartArch = (() async { // Parse 'arch' out of a string like '... "os_arch"\n'. return (await _runDart(['--version'])) .trim().split(' ').last.replaceAll('"', '').split('_')[1]; })(); /// Returns a default archive name when given a Git revision. /// Used when an output filename is not given. Future get _archiveName async { final String os = platform.operatingSystem.toLowerCase(); // Include the intended host archetecture in the file name for non-x64. final String arch = await _dartArch == 'x64' ? '' : '${await _dartArch}_'; // We don't use .tar.xz on Mac because although it can unpack them // on the command line (with tar), the "Archive Utility" that runs // when you double-click on them just does some crazy behavior (it // converts it to a compressed cpio archive, and when you double // click on that, it converts it back to .tar.xz, without ever // unpacking it!) So, we use .zip for Mac, and the files are about // 220MB larger than they need to be. :-( final String suffix = platform.isLinux ? 'tar.xz' : 'zip'; final String package = '${os}_$arch${_version[frameworkVersionTag]}'; return 'flutter_$package-${branch.name}.$suffix'; } /// Checks out the flutter repo and prepares it for other operations. /// /// Returns the version for this release as obtained from the git tags, and /// the dart version as obtained from `flutter --version`. Future> initializeRepo() async { await _checkoutFlutter(); if (_version.isEmpty) { _version.addAll(await _getVersion()); } return _version; } /// Performs all of the steps needed to create an archive. Future createArchive() async { assert(_version.isNotEmpty, 'Must run initializeRepo before createArchive'); final File outputFile = File(path.join( outputDir.absolute.path, await _archiveName, )); await _installMinGitIfNeeded(); await _populateCaches(); await _validate(); await _archiveFiles(outputFile); return outputFile; } /// Validates the integrity of the release package. /// /// Currently only checks that macOS binaries are codesigned. Will throw a /// [PreparePackageException] if the test failes. Future _validate() async { // Only validate in strict mode, which means `--publish` if (!strict || !platform.isMacOS) { return; } // Validate that the dart binary is codesigned try { // TODO(fujino): Use the conductor https://github.com/flutter/flutter/issues/81701 await _processRunner.runProcess( [ 'codesign', '-vvvv', '--check-notarization', _dart, ], workingDirectory: flutterRoot, ); } on PreparePackageException catch (e) { throw PreparePackageException( 'The binary $_dart was not codesigned!\n${e.message}', ); } } /// Returns the version map of this release, according the to tags in the /// repo and the output of `flutter --version --machine`. /// /// This looks for the tag attached to [revision] and, if it doesn't find one, /// git will give an error. /// /// If [strict] is true, the exact [revision] must be tagged to return the /// version. If [strict] is not true, will look backwards in time starting at /// [revision] to find the most recent version tag. /// /// The version found as a git tag is added to the information given by /// `flutter --version --machine` with the `frameworkVersionFromGit` tag, and /// returned. Future> _getVersion() async { String gitVersion; if (strict) { try { gitVersion = await _runGit(['describe', '--tags', '--exact-match', revision]); } on PreparePackageException catch (exception) { throw PreparePackageException( 'Git error when checking for a version tag attached to revision $revision.\n' 'Perhaps there is no tag at that revision?:\n' '$exception' ); } } else { gitVersion = await _runGit(['describe', '--tags', '--abbrev=0', revision]); } // Run flutter command twice, once to make sure the flutter command is built // and ready (and thus won't output any junk on stdout the second time), and // once to capture theJSON output. The second run should be fast. await _runFlutter(['--version', '--machine']); final String versionJson = await _runFlutter(['--version', '--machine']); final Map versionMap = {}; final Map result = json.decode(versionJson) as Map; result.forEach((String key, dynamic value) => versionMap[key] = value.toString()); versionMap[frameworkVersionTag] = gitVersion; versionMap[dartTargetArchTag] = await _dartArch; return versionMap; } /// Clone the Flutter repo and make sure that the git environment is sane /// for when the user will unpack it. Future _checkoutFlutter() async { // We want the user to start out the in the specified branch instead of a // detached head. To do that, we need to make sure the branch points at the // desired revision. await _runGit(['clone', '-b', branch.name, gobMirror], workingDirectory: tempDir); await _runGit(['reset', '--hard', revision]); // Make the origin point to github instead of the chromium mirror. await _runGit(['remote', 'set-url', 'origin', githubRepo]); // Minify `.git` footprint (saving about ~100 MB as of Oct 2022) await _runGit(['gc', '--prune=now', '--aggressive']); } /// Retrieve the MinGit executable from storage and unpack it. Future _installMinGitIfNeeded() async { if (!platform.isWindows) { return; } final Uint8List data = await httpReader(_minGitUri); final File gitFile = File(path.join(tempDir.absolute.path, 'mingit.zip')); await gitFile.writeAsBytes(data, flush: true); final Directory minGitPath = Directory(path.join(flutterRoot.absolute.path, 'bin', 'mingit')); await minGitPath.create(recursive: true); await _unzipArchive(gitFile, workingDirectory: minGitPath); } /// Prepare the archive repo so that it has all of the caches warmed up and /// is configured for the user to begin working. Future _populateCaches() async { await _runFlutter(['doctor']); await _runFlutter(['update-packages']); await _runFlutter(['precache']); await _runFlutter(['ide-config']); // Create each of the templates, since they will call 'pub get' on // themselves when created, and this will warm the cache with their // dependencies too. for (final String template in ['app', 'package', 'plugin']) { final String createName = path.join(tempDir.path, 'create_$template'); await _runFlutter( ['create', '--template=$template', createName], // Run it outside the cloned Flutter repo to not nest git repos, since // they'll be git repos themselves too. workingDirectory: tempDir, ); } // Yes, we could just skip all .packages files when constructing // the archive, but some are checked in, and we don't want to skip // those. await _runGit([ 'clean', '-f', // Do not -X as it could lead to entire bin/cache getting cleaned '-x', '--', '**/.packages', ]); /// Remove package_config files and any contents in .dart_tool await _runGit([ 'clean', '-f', '-x', '--', '**/.dart_tool/', ]); // Ensure the above commands do not clean out the cache final Directory flutterCache = Directory(path.join(flutterRoot.absolute.path, 'bin', 'cache')); if (!flutterCache.existsSync()) { throw Exception('The flutter cache was not found at ${flutterCache.path}!'); } /// Remove git subfolder from .pub-cache, this contains the flutter goldens /// and new flutter_gallery. final Directory gitCache = Directory(path.join(flutterRoot.absolute.path, '.pub-cache', 'git')); if (gitCache.existsSync()) { gitCache.deleteSync(recursive: true); } } /// Write the archive to the given output file. Future _archiveFiles(File outputFile) async { if (outputFile.path.toLowerCase().endsWith('.zip')) { await _createZipArchive(outputFile, flutterRoot); } else if (outputFile.path.toLowerCase().endsWith('.tar.xz')) { await _createTarArchive(outputFile, flutterRoot); } } Future _runDart(List args, {Directory? workingDirectory}) { return _processRunner.runProcess( [_dart, ...args], workingDirectory: workingDirectory ?? flutterRoot, ); } Future _runFlutter(List args, {Directory? workingDirectory}) { return _processRunner.runProcess( [_flutter, ...args], workingDirectory: workingDirectory ?? flutterRoot, ); } Future _runGit(List args, {Directory? workingDirectory}) { return _processRunner.runProcess( ['git', ...args], workingDirectory: workingDirectory ?? flutterRoot, ); } /// Unpacks the given zip file into the currentDirectory (if set), or the /// same directory as the archive. Future _unzipArchive(File archive, {Directory? workingDirectory}) { workingDirectory ??= Directory(path.dirname(archive.absolute.path)); List commandLine; if (platform.isWindows) { commandLine = [ '7za', 'x', archive.absolute.path, ]; } else { commandLine = [ 'unzip', archive.absolute.path, ]; } return _processRunner.runProcess(commandLine, workingDirectory: workingDirectory); } /// Create a zip archive from the directory source. Future _createZipArchive(File output, Directory source) async { List commandLine; if (platform.isWindows) { // Unhide the .git folder, https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/attrib. await _processRunner.runProcess( ['attrib', '-h', '.git'], workingDirectory: Directory(source.absolute.path), ); commandLine = [ '7za', 'a', '-tzip', '-mx=9', output.absolute.path, path.basename(source.path), ]; } else { commandLine = [ 'zip', '-r', '-9', '--symlinks', output.absolute.path, path.basename(source.path), ]; } return _processRunner.runProcess( commandLine, workingDirectory: Directory(path.dirname(source.absolute.path)), ); } /// Create a tar archive from the directory source. Future _createTarArchive(File output, Directory source) { return _processRunner.runProcess([ 'tar', 'cJf', output.absolute.path, path.basename(source.absolute.path), ], workingDirectory: Directory(path.dirname(source.absolute.path))); } } class ArchivePublisher { ArchivePublisher( this.tempDir, this.revision, this.branch, this.version, this.outputFile, this.dryRun, { ProcessManager? processManager, bool subprocessOutput = true, this.platform = const LocalPlatform(), }) : assert(revision.length == 40), platformName = platform.operatingSystem.toLowerCase(), metadataGsPath = '$gsReleaseFolder/${getMetadataFilename(platform)}', _processRunner = ProcessRunner( processManager: processManager, subprocessOutput: subprocessOutput, ); final Platform platform; final String platformName; final String metadataGsPath; final Branch branch; final String revision; final Map version; final Directory tempDir; final File outputFile; final ProcessRunner _processRunner; final bool dryRun; String get destinationArchivePath => '${branch.name}/$platformName/${path.basename(outputFile.path)}'; static String getMetadataFilename(Platform platform) => 'releases_${platform.operatingSystem.toLowerCase()}.json'; Future _getChecksum(File archiveFile) async { final DigestSink digestSink = DigestSink(); final ByteConversionSink sink = sha256.startChunkedConversion(digestSink); final Stream> stream = archiveFile.openRead(); await stream.forEach((List chunk) { sink.add(chunk); }); sink.close(); return digestSink.value.toString(); } /// Publish the archive to Google Storage. /// /// This method will throw if the target archive already exists on cloud /// storage. Future publishArchive([bool forceUpload = false]) async { final String destGsPath = '$gsReleaseFolder/$destinationArchivePath'; if (!forceUpload) { if (await _cloudPathExists(destGsPath) && !dryRun) { throw PreparePackageException( 'File $destGsPath already exists on cloud storage!', ); } } await _cloudCopy( src: outputFile.absolute.path, dest: destGsPath, ); assert(tempDir.existsSync()); final String gcsPath = '$gsReleaseFolder/${getMetadataFilename(platform)}'; await _publishMetadata(gcsPath); } /// Downloads and updates the metadata file without publishing it. Future generateLocalMetadata() async { await _updateMetadata('$gsReleaseFolder/${getMetadataFilename(platform)}'); } Future> _addRelease(Map jsonData) async { jsonData['base_url'] = '$baseUrl$releaseFolder'; if (!jsonData.containsKey('current_release')) { jsonData['current_release'] = {}; } (jsonData['current_release'] as Map)[branch.name] = revision; if (!jsonData.containsKey('releases')) { jsonData['releases'] = >[]; } final Map newEntry = {}; newEntry['hash'] = revision; newEntry['channel'] = branch.name; newEntry['version'] = version[frameworkVersionTag]; newEntry['dart_sdk_version'] = version[dartVersionTag]; newEntry['dart_sdk_arch'] = version[dartTargetArchTag]; newEntry['release_date'] = DateTime.now().toUtc().toIso8601String(); newEntry['archive'] = destinationArchivePath; newEntry['sha256'] = await _getChecksum(outputFile); // Search for any entries with the same hash and channel and remove them. final List releases = jsonData['releases'] as List; jsonData['releases'] = >[ for (final Map entry in releases.cast>()) if (entry['hash'] != newEntry['hash'] || entry['channel'] != newEntry['channel'] || entry['dart_sdk_arch'] != newEntry['dart_sdk_arch']) entry, newEntry, ]..sort((Map a, Map b) { final DateTime aDate = DateTime.parse(a['release_date'] as String); final DateTime bDate = DateTime.parse(b['release_date'] as String); return bDate.compareTo(aDate); }); return jsonData; } Future _updateMetadata(String gsPath) async { // We can't just cat the metadata from the server with 'gsutil cat', because // Windows wants to echo the commands that execute in gsutil.bat to the // stdout when we do that. So, we copy the file locally and then read it // back in. final File metadataFile = File( path.join(tempDir.absolute.path, getMetadataFilename(platform)), ); await _runGsUtil(['cp', gsPath, metadataFile.absolute.path]); Map jsonData = {}; if (!dryRun) { final String currentMetadata = metadataFile.readAsStringSync(); if (currentMetadata.isEmpty) { throw PreparePackageException('Empty metadata received from server'); } try { jsonData = json.decode(currentMetadata) as Map; } on FormatException catch (e) { throw PreparePackageException('Unable to parse JSON metadata received from cloud: $e'); } } // Run _addRelease, even on a dry run, so we can inspect the metadata on a // dry run. On a dry run, the only thing in the metadata file be the new // release. jsonData = await _addRelease(jsonData); const JsonEncoder encoder = JsonEncoder.withIndent(' '); metadataFile.writeAsStringSync(encoder.convert(jsonData)); } /// Publishes the metadata file to GCS. Future _publishMetadata(String gsPath) async { final File metadataFile = File( path.join(tempDir.absolute.path, getMetadataFilename(platform)), ); await _cloudCopy( src: metadataFile.absolute.path, dest: gsPath, // This metadata file is used by the website, so we don't want a long // latency between publishing a release and it being available on the // site. cacheSeconds: shortCacheSeconds, ); } Future _runGsUtil( List args, { Directory? workingDirectory, bool failOk = false, }) async { if (dryRun) { print('gsutil.py -- $args'); return ''; } if (platform.isWindows) { return _processRunner.runProcess( ['python3', path.join(platform.environment['DEPOT_TOOLS']!, 'gsutil.py'), '--', ...args], workingDirectory: workingDirectory, failOk: failOk, ); } return _processRunner.runProcess( ['gsutil.py', '--', ...args], workingDirectory: workingDirectory, failOk: failOk, ); } /// Determine if a file exists at a given [cloudPath]. Future _cloudPathExists(String cloudPath) async { try { await _runGsUtil( ['stat', cloudPath], ); } on PreparePackageException { // `gsutil stat gs://path/to/file` will exit with 1 if file does not exist return false; } return true; } Future _cloudCopy({ required String src, required String dest, int? cacheSeconds, }) async { // We often don't have permission to overwrite, but // we have permission to remove, so that's what we do. await _runGsUtil(['rm', dest], failOk: true); String? mimeType; if (dest.endsWith('.tar.xz')) { mimeType = 'application/x-gtar'; } if (dest.endsWith('.zip')) { mimeType = 'application/zip'; } if (dest.endsWith('.json')) { mimeType = 'application/json'; } return _runGsUtil([ // Use our preferred MIME type for the files we care about // and let gsutil figure it out for anything else. if (mimeType != null) ...['-h', 'Content-Type:$mimeType'], if (cacheSeconds != null) ...['-h', 'Cache-Control:max-age=$cacheSeconds'], 'cp', src, dest, ]); } } /// Prepares a flutter git repo to be packaged up for distribution. /// It mainly serves to populate the .pub-cache with any appropriate Dart /// packages, and the flutter cache in bin/cache with the appropriate /// dependencies and snapshots. /// /// Archives contain the executables and customizations for the platform that /// they are created on. Future main(List rawArguments) async { final ArgParser argParser = ArgParser(); argParser.addOption( 'temp_dir', help: 'A location where temporary files may be written. Defaults to a ' 'directory in the system temp folder. Will write a few GiB of data, ' 'so it should have sufficient free space. If a temp_dir is not ' 'specified, then the default temp_dir will be created, used, and ' 'removed automatically.', ); argParser.addOption('revision', help: 'The Flutter git repo revision to build the ' 'archive with. Must be the full 40-character hash. Required.'); argParser.addOption( 'branch', allowed: Branch.values.map((Branch branch) => branch.name), help: 'The Flutter branch to build the archive with. Required.', ); argParser.addOption( 'output', help: 'The path to the directory where the output archive should be ' 'written. If --output is not specified, the archive will be written to ' "the current directory. If the output directory doesn't exist, it, and " 'the path to it, will be created.', ); argParser.addFlag( 'publish', help: 'If set, will publish the archive to Google Cloud Storage upon ' 'successful creation of the archive. Will publish under this ' 'directory: $baseUrl$releaseFolder', ); argParser.addFlag( 'force', abbr: 'f', help: 'Overwrite a previously uploaded package.', ); argParser.addFlag( 'dry_run', negatable: false, help: 'Prints gsutil commands instead of executing them.', ); argParser.addFlag( 'help', negatable: false, help: 'Print help for this command.', ); final ArgResults parsedArguments = argParser.parse(rawArguments); if (parsedArguments['help'] as bool) { print(argParser.usage); exit(0); } void errorExit(String message, {int exitCode = -1}) { stderr.write('Error: $message\n\n'); stderr.write('${argParser.usage}\n'); exit(exitCode); } if (!parsedArguments.wasParsed('revision')) { errorExit('Invalid argument: --revision must be specified.'); } final String revision = parsedArguments['revision'] as String; if (revision.length != 40) { errorExit('Invalid argument: --revision must be the entire hash, not just a prefix.'); } if (!parsedArguments.wasParsed('branch')) { errorExit('Invalid argument: --branch must be specified.'); } final String? tempDirArg = parsedArguments['temp_dir'] as String?; Directory tempDir; bool removeTempDir = false; if (tempDirArg == null || tempDirArg.isEmpty) { tempDir = Directory.systemTemp.createTempSync('flutter_package.'); removeTempDir = true; } else { tempDir = Directory(tempDirArg); if (!tempDir.existsSync()) { errorExit("Temporary directory $tempDirArg doesn't exist."); } } Directory outputDir; if (parsedArguments['output'] == null) { outputDir = tempDir; } else { outputDir = Directory(parsedArguments['output'] as String); if (!outputDir.existsSync()) { outputDir.createSync(recursive: true); } } final bool publish = parsedArguments['publish'] as bool; final bool dryRun = parsedArguments['dry_run'] as bool; final Branch branch = Branch.values.byName(parsedArguments['branch'] as String); final ArchiveCreator creator = ArchiveCreator( tempDir, outputDir, revision, branch, strict: publish && !dryRun, ); int exitCode = 0; late String message; try { final Map version = await creator.initializeRepo(); final File outputFile = await creator.createArchive(); final ArchivePublisher publisher = ArchivePublisher( tempDir, revision, branch, version, outputFile, dryRun, ); await publisher.generateLocalMetadata(); if (parsedArguments['publish'] as bool) { await publisher.publishArchive(parsedArguments['force'] as bool); } } on PreparePackageException catch (e) { exitCode = e.exitCode; message = e.message; } catch (e) { exitCode = -1; message = e.toString(); } finally { if (removeTempDir) { tempDir.deleteSync(recursive: true); } if (exitCode != 0) { errorExit(message, exitCode: exitCode); } exit(0); } }