• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2017 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:async';
6import 'dart:convert';
7import 'dart:io' hide Platform;
8import 'dart:typed_data';
9
10import 'package:args/args.dart';
11import 'package:crypto/crypto.dart';
12import 'package:crypto/src/digest_sink.dart';
13import 'package:http/http.dart' as http;
14import 'package:path/path.dart' as path;
15import 'package:platform/platform.dart' show Platform, LocalPlatform;
16import 'package:process/process.dart';
17
18const String chromiumRepo = 'https://chromium.googlesource.com/external/github.com/flutter/flutter';
19const String githubRepo = 'https://github.com/flutter/flutter.git';
20const String mingitForWindowsUrl = 'https://storage.googleapis.com/flutter_infra/mingit/'
21    '603511c649b00bbef0a6122a827ac419b656bc19/mingit.zip';
22const String gsBase = 'gs://flutter_infra';
23const String releaseFolder = '/releases';
24const String gsReleaseFolder = '$gsBase$releaseFolder';
25const String baseUrl = 'https://storage.googleapis.com/flutter_infra';
26
27/// Exception class for when a process fails to run, so we can catch
28/// it and provide something more readable than a stack trace.
29class PreparePackageException implements Exception {
30  PreparePackageException(this.message, [this.result]);
31
32  final String message;
33  final ProcessResult result;
34  int get exitCode => result?.exitCode ?? -1;
35
36  @override
37  String toString() {
38    String output = runtimeType.toString();
39    if (message != null) {
40      output += ': $message';
41    }
42    final String stderr = result?.stderr ?? '';
43    if (stderr.isNotEmpty) {
44      output += ':\n$stderr';
45    }
46    return output;
47  }
48}
49
50enum Branch { dev, beta, stable }
51
52String getBranchName(Branch branch) {
53  switch (branch) {
54    case Branch.beta:
55      return 'beta';
56    case Branch.dev:
57      return 'dev';
58    case Branch.stable:
59      return 'stable';
60  }
61  return null;
62}
63
64Branch fromBranchName(String name) {
65  switch (name) {
66    case 'beta':
67      return Branch.beta;
68    case 'dev':
69      return Branch.dev;
70    case 'stable':
71      return Branch.stable;
72    default:
73      throw ArgumentError('Invalid branch name.');
74  }
75}
76
77/// A helper class for classes that want to run a process, optionally have the
78/// stderr and stdout reported as the process runs, and capture the stdout
79/// properly without dropping any.
80class ProcessRunner {
81  ProcessRunner({
82    ProcessManager processManager,
83    this.subprocessOutput = true,
84    this.defaultWorkingDirectory,
85    this.platform = const LocalPlatform(),
86  }) : processManager = processManager ?? const LocalProcessManager() {
87    environment = Map<String, String>.from(platform.environment);
88  }
89
90  /// The platform to use for a starting environment.
91  final Platform platform;
92
93  /// Set [subprocessOutput] to show output as processes run. Stdout from the
94  /// process will be printed to stdout, and stderr printed to stderr.
95  final bool subprocessOutput;
96
97  /// Set the [processManager] in order to inject a test instance to perform
98  /// testing.
99  final ProcessManager processManager;
100
101  /// Sets the default directory used when `workingDirectory` is not specified
102  /// to [runProcess].
103  final Directory defaultWorkingDirectory;
104
105  /// The environment to run processes with.
106  Map<String, String> environment;
107
108  /// Run the command and arguments in `commandLine` as a sub-process from
109  /// `workingDirectory` if set, or the [defaultWorkingDirectory] if not. Uses
110  /// [Directory.current] if [defaultWorkingDirectory] is not set.
111  ///
112  /// Set `failOk` if [runProcess] should not throw an exception when the
113  /// command completes with a a non-zero exit code.
114  Future<String> runProcess(
115    List<String> commandLine, {
116    Directory workingDirectory,
117    bool failOk = false,
118  }) async {
119    workingDirectory ??= defaultWorkingDirectory ?? Directory.current;
120    if (subprocessOutput) {
121      stderr.write('Running "${commandLine.join(' ')}" in ${workingDirectory.path}.\n');
122    }
123    final List<int> output = <int>[];
124    final Completer<void> stdoutComplete = Completer<void>();
125    final Completer<void> stderrComplete = Completer<void>();
126    Process process;
127    Future<int> allComplete() async {
128      await stderrComplete.future;
129      await stdoutComplete.future;
130      return process.exitCode;
131    }
132
133    try {
134      process = await processManager.start(
135        commandLine,
136        workingDirectory: workingDirectory.absolute.path,
137        environment: environment,
138      );
139      process.stdout.listen(
140        (List<int> event) {
141          output.addAll(event);
142          if (subprocessOutput) {
143            stdout.add(event);
144          }
145        },
146        onDone: () async => stdoutComplete.complete(),
147      );
148      if (subprocessOutput) {
149        process.stderr.listen(
150          (List<int> event) {
151            stderr.add(event);
152          },
153          onDone: () async => stderrComplete.complete(),
154        );
155      } else {
156        stderrComplete.complete();
157      }
158    } on ProcessException catch (e) {
159      final String message = 'Running "${commandLine.join(' ')}" in ${workingDirectory.path} '
160          'failed with:\n${e.toString()}';
161      throw PreparePackageException(message);
162    } on ArgumentError catch (e) {
163      final String message = 'Running "${commandLine.join(' ')}" in ${workingDirectory.path} '
164          'failed with:\n${e.toString()}';
165      throw PreparePackageException(message);
166    }
167
168    final int exitCode = await allComplete();
169    if (exitCode != 0 && !failOk) {
170      final String message = 'Running "${commandLine.join(' ')}" in ${workingDirectory.path} failed';
171      throw PreparePackageException(
172        message,
173        ProcessResult(0, exitCode, null, 'returned $exitCode'),
174      );
175    }
176    return utf8.decoder.convert(output).trim();
177  }
178}
179
180typedef HttpReader = Future<Uint8List> Function(Uri url, {Map<String, String> headers});
181
182/// Creates a pre-populated Flutter archive from a git repo.
183class ArchiveCreator {
184  /// [tempDir] is the directory to use for creating the archive.  The script
185  /// will place several GiB of data there, so it should have available space.
186  ///
187  /// The processManager argument is used to inject a mock of [ProcessManager] for
188  /// testing purposes.
189  ///
190  /// If subprocessOutput is true, then output from processes invoked during
191  /// archive creation is echoed to stderr and stdout.
192  ArchiveCreator(
193    this.tempDir,
194    this.outputDir,
195    this.revision,
196    this.branch, {
197    this.strict = true,
198    ProcessManager processManager,
199    bool subprocessOutput = true,
200    this.platform = const LocalPlatform(),
201    HttpReader httpReader,
202  })  : assert(revision.length == 40),
203        flutterRoot = Directory(path.join(tempDir.path, 'flutter')),
204        httpReader = httpReader ?? http.readBytes,
205        _processRunner = ProcessRunner(
206          processManager: processManager,
207          subprocessOutput: subprocessOutput,
208          platform: platform,
209        ) {
210    _flutter = path.join(
211      flutterRoot.absolute.path,
212      'bin',
213      'flutter',
214    );
215    _processRunner.environment['PUB_CACHE'] = path.join(flutterRoot.absolute.path, '.pub-cache');
216  }
217
218  /// The platform to use for the environment and determining which
219  /// platform we're running on.
220  final Platform platform;
221
222  /// The branch to build the archive for.  The branch must contain [revision].
223  final Branch branch;
224
225  /// The git revision hash to build the archive for. This revision has
226  /// to be available in the [branch], although it doesn't have to be
227  /// at HEAD, since we clone the branch and then reset to this revision
228  /// to create the archive.
229  final String revision;
230
231  /// The flutter root directory in the [tempDir].
232  final Directory flutterRoot;
233
234  /// The temporary directory used to build the archive in.
235  final Directory tempDir;
236
237  /// The directory to write the output file to.
238  final Directory outputDir;
239
240  /// True if the creator should be strict about checking requirements or not.
241  ///
242  /// In strict mode, will insist that the [revision] be a tagged revision.
243  final bool strict;
244
245  final Uri _minGitUri = Uri.parse(mingitForWindowsUrl);
246  final ProcessRunner _processRunner;
247
248  /// Used to tell the [ArchiveCreator] which function to use for reading
249  /// bytes from a URL. Used in tests to inject a fake reader. Defaults to
250  /// [http.readBytes].
251  final HttpReader httpReader;
252
253  File _outputFile;
254  String _version;
255  String _flutter;
256
257  /// Get the name of the channel as a string.
258  String get branchName => getBranchName(branch);
259
260  /// Returns a default archive name when given a Git revision.
261  /// Used when an output filename is not given.
262  String get _archiveName {
263    final String os = platform.operatingSystem.toLowerCase();
264    // We don't use .tar.xz on Mac because although it can unpack them
265    // on the command line (with tar), the "Archive Utility" that runs
266    // when you double-click on them just does some crazy behavior (it
267    // converts it to a compressed cpio archive, and when you double
268    // click on that, it converts it back to .tar.xz, without ever
269    // unpacking it!) So, we use .zip for Mac, and the files are about
270    // 220MB larger than they need to be. :-(
271    final String suffix = platform.isLinux ? 'tar.xz' : 'zip';
272    return 'flutter_${os}_$_version-$branchName.$suffix';
273  }
274
275  /// Checks out the flutter repo and prepares it for other operations.
276  ///
277  /// Returns the version for this release, as obtained from the git tags.
278  Future<String> initializeRepo() async {
279    await _checkoutFlutter();
280    _version = await _getVersion();
281    return _version;
282  }
283
284  /// Performs all of the steps needed to create an archive.
285  Future<File> createArchive() async {
286    assert(_version != null, 'Must run initializeRepo before createArchive');
287    _outputFile = File(path.join(outputDir.absolute.path, _archiveName));
288    await _installMinGitIfNeeded();
289    await _populateCaches();
290    await _archiveFiles(_outputFile);
291    return _outputFile;
292  }
293
294  /// Returns the version number of this release, according the to tags in the
295  /// repo.
296  ///
297  /// This looks for the tag attached to [revision] and, if it doesn't find one,
298  /// git will give an error.
299  ///
300  /// If [strict] is true, the exact [revision] must be tagged to return the
301  /// version.  If [strict] is not true, will look backwards in time starting at
302  /// [revision] to find the most recent version tag.
303  Future<String> _getVersion() async {
304    if (strict) {
305      try {
306        return _runGit(<String>['describe', '--tags', '--exact-match', revision]);
307      } on PreparePackageException catch (exception) {
308        throw PreparePackageException(
309          'Git error when checking for a version tag attached to revision $revision.\n'
310          'Perhaps there is no tag at that revision?:\n'
311          '$exception'
312        );
313      }
314    } else {
315      return _runGit(<String>['describe', '--tags', '--abbrev=0', revision]);
316    }
317  }
318
319  /// Clone the Flutter repo and make sure that the git environment is sane
320  /// for when the user will unpack it.
321  Future<void> _checkoutFlutter() async {
322    // We want the user to start out the in the specified branch instead of a
323    // detached head. To do that, we need to make sure the branch points at the
324    // desired revision.
325    await _runGit(<String>['clone', '-b', branchName, chromiumRepo], workingDirectory: tempDir);
326    await _runGit(<String>['reset', '--hard', revision]);
327
328    // Make the origin point to github instead of the chromium mirror.
329    await _runGit(<String>['remote', 'set-url', 'origin', githubRepo]);
330  }
331
332  /// Retrieve the MinGit executable from storage and unpack it.
333  Future<void> _installMinGitIfNeeded() async {
334    if (!platform.isWindows) {
335      return;
336    }
337    final Uint8List data = await httpReader(_minGitUri);
338    final File gitFile = File(path.join(tempDir.absolute.path, 'mingit.zip'));
339    await gitFile.writeAsBytes(data, flush: true);
340
341    final Directory minGitPath = Directory(path.join(flutterRoot.absolute.path, 'bin', 'mingit'));
342    await minGitPath.create(recursive: true);
343    await _unzipArchive(gitFile, workingDirectory: minGitPath);
344  }
345
346  /// Prepare the archive repo so that it has all of the caches warmed up and
347  /// is configured for the user to begin working.
348  Future<void> _populateCaches() async {
349    await _runFlutter(<String>['doctor']);
350    await _runFlutter(<String>['update-packages']);
351    await _runFlutter(<String>['precache']);
352    await _runFlutter(<String>['ide-config']);
353
354    // Create each of the templates, since they will call 'pub get' on
355    // themselves when created, and this will warm the cache with their
356    // dependencies too.
357    for (String template in <String>['app', 'package', 'plugin']) {
358      final String createName = path.join(tempDir.path, 'create_$template');
359      await _runFlutter(
360        <String>['create', '--template=$template', createName],
361        // Run it outside the cloned Flutter repo to not nest git repos, since
362        // they'll be git repos themselves too.
363        workingDirectory: tempDir,
364      );
365    }
366
367    // Yes, we could just skip all .packages files when constructing
368    // the archive, but some are checked in, and we don't want to skip
369    // those.
370    await _runGit(<String>['clean', '-f', '-X', '**/.packages']);
371  }
372
373  /// Write the archive to the given output file.
374  Future<void> _archiveFiles(File outputFile) async {
375    if (outputFile.path.toLowerCase().endsWith('.zip')) {
376      await _createZipArchive(outputFile, flutterRoot);
377    } else if (outputFile.path.toLowerCase().endsWith('.tar.xz')) {
378      await _createTarArchive(outputFile, flutterRoot);
379    }
380  }
381
382  Future<String> _runFlutter(List<String> args, {Directory workingDirectory}) {
383    return _processRunner.runProcess(
384      <String>[_flutter, ...args],
385      workingDirectory: workingDirectory ?? flutterRoot,
386    );
387  }
388
389  Future<String> _runGit(List<String> args, {Directory workingDirectory}) {
390    return _processRunner.runProcess(
391      <String>['git', ...args],
392      workingDirectory: workingDirectory ?? flutterRoot,
393    );
394  }
395
396  /// Unpacks the given zip file into the currentDirectory (if set), or the
397  /// same directory as the archive.
398  Future<String> _unzipArchive(File archive, {Directory workingDirectory}) {
399    workingDirectory ??= Directory(path.dirname(archive.absolute.path));
400    List<String> commandLine;
401    if (platform.isWindows) {
402      commandLine = <String>[
403        '7za',
404        'x',
405        archive.absolute.path,
406      ];
407    } else {
408      commandLine = <String>[
409        'unzip',
410        archive.absolute.path,
411      ];
412    }
413    return _processRunner.runProcess(commandLine, workingDirectory: workingDirectory);
414  }
415
416  /// Create a zip archive from the directory source.
417  Future<String> _createZipArchive(File output, Directory source) {
418    List<String> commandLine;
419    if (platform.isWindows) {
420      commandLine = <String>[
421        '7za',
422        'a',
423        '-tzip',
424        '-mx=9',
425        output.absolute.path,
426        path.basename(source.path),
427      ];
428    } else {
429      commandLine = <String>[
430        'zip',
431        '-r',
432        '-9',
433        output.absolute.path,
434        path.basename(source.path),
435      ];
436    }
437    return _processRunner.runProcess(
438      commandLine,
439      workingDirectory: Directory(path.dirname(source.absolute.path)),
440    );
441  }
442
443  /// Create a tar archive from the directory source.
444  Future<String> _createTarArchive(File output, Directory source) {
445    return _processRunner.runProcess(<String>[
446      'tar',
447      'cJf',
448      output.absolute.path,
449      path.basename(source.absolute.path),
450    ], workingDirectory: Directory(path.dirname(source.absolute.path)));
451  }
452}
453
454class ArchivePublisher {
455  ArchivePublisher(
456    this.tempDir,
457    this.revision,
458    this.branch,
459    this.version,
460    this.outputFile, {
461    ProcessManager processManager,
462    bool subprocessOutput = true,
463    this.platform = const LocalPlatform(),
464  })  : assert(revision.length == 40),
465        platformName = platform.operatingSystem.toLowerCase(),
466        metadataGsPath = '$gsReleaseFolder/${getMetadataFilename(platform)}',
467        _processRunner = ProcessRunner(
468          processManager: processManager,
469          subprocessOutput: subprocessOutput,
470        );
471
472  final Platform platform;
473  final String platformName;
474  final String metadataGsPath;
475  final Branch branch;
476  final String revision;
477  final String version;
478  final Directory tempDir;
479  final File outputFile;
480  final ProcessRunner _processRunner;
481  String get branchName => getBranchName(branch);
482  String get destinationArchivePath => '$branchName/$platformName/${path.basename(outputFile.path)}';
483  static String getMetadataFilename(Platform platform) => 'releases_${platform.operatingSystem.toLowerCase()}.json';
484
485  Future<String> _getChecksum(File archiveFile) async {
486    final DigestSink digestSink = DigestSink();
487    final ByteConversionSink sink = sha256.startChunkedConversion(digestSink);
488
489    final Stream<List<int>> stream = archiveFile.openRead();
490    await stream.forEach((List<int> chunk) {
491      sink.add(chunk);
492    });
493    sink.close();
494    return digestSink.value.toString();
495  }
496
497  /// Publish the archive to Google Storage.
498  Future<void> publishArchive() async {
499    final String destGsPath = '$gsReleaseFolder/$destinationArchivePath';
500    await _cloudCopy(outputFile.absolute.path, destGsPath);
501    assert(tempDir.existsSync());
502    await _updateMetadata();
503  }
504
505  Future<Map<String, dynamic>> _addRelease(Map<String, dynamic> jsonData) async {
506    jsonData['base_url'] = '$baseUrl$releaseFolder';
507    if (!jsonData.containsKey('current_release')) {
508      jsonData['current_release'] = <String, String>{};
509    }
510    jsonData['current_release'][branchName] = revision;
511    if (!jsonData.containsKey('releases')) {
512      jsonData['releases'] = <Map<String, dynamic>>[];
513    }
514
515    final Map<String, dynamic> newEntry = <String, dynamic>{};
516    newEntry['hash'] = revision;
517    newEntry['channel'] = branchName;
518    newEntry['version'] = version;
519    newEntry['release_date'] = DateTime.now().toUtc().toIso8601String();
520    newEntry['archive'] = destinationArchivePath;
521    newEntry['sha256'] = await _getChecksum(outputFile);
522
523    // Search for any entries with the same hash and channel and remove them.
524    final List<dynamic> releases = jsonData['releases'];
525    final List<Map<String, dynamic>> prunedReleases = <Map<String, dynamic>>[];
526    for (Map<String, dynamic> entry in releases) {
527      if (entry['hash'] != newEntry['hash'] || entry['channel'] != newEntry['channel']) {
528        prunedReleases.add(entry);
529      }
530    }
531
532    prunedReleases.add(newEntry);
533    prunedReleases.sort((Map<String, dynamic> a, Map<String, dynamic> b) {
534      final DateTime aDate = DateTime.parse(a['release_date']);
535      final DateTime bDate = DateTime.parse(b['release_date']);
536      return bDate.compareTo(aDate);
537    });
538    jsonData['releases'] = prunedReleases;
539    return jsonData;
540  }
541
542  Future<void> _updateMetadata() async {
543    // We can't just cat the metadata from the server with 'gsutil cat', because
544    // Windows wants to echo the commands that execute in gsutil.bat to the
545    // stdout when we do that. So, we copy the file locally and then read it
546    // back in.
547    final File metadataFile = File(
548      path.join(tempDir.absolute.path, getMetadataFilename(platform)),
549    );
550    await _runGsUtil(<String>['cp', metadataGsPath, metadataFile.absolute.path]);
551    final String currentMetadata = metadataFile.readAsStringSync();
552    if (currentMetadata.isEmpty) {
553      throw PreparePackageException('Empty metadata received from server');
554    }
555
556    Map<String, dynamic> jsonData;
557    try {
558      jsonData = json.decode(currentMetadata);
559    } on FormatException catch (e) {
560      throw PreparePackageException('Unable to parse JSON metadata received from cloud: $e');
561    }
562
563    jsonData = await _addRelease(jsonData);
564
565    const JsonEncoder encoder = JsonEncoder.withIndent('  ');
566    metadataFile.writeAsStringSync(encoder.convert(jsonData));
567    await _cloudCopy(metadataFile.absolute.path, metadataGsPath);
568  }
569
570  Future<String> _runGsUtil(
571    List<String> args, {
572    Directory workingDirectory,
573    bool failOk = false,
574  }) async {
575    if (platform.isWindows) {
576      return _processRunner.runProcess(
577        <String>['python', path.join(platform.environment['DEPOT_TOOLS'], 'gsutil.py'), '--', ...args],
578        workingDirectory: workingDirectory,
579        failOk: failOk,
580      );
581    }
582
583    return _processRunner.runProcess(
584      <String>['gsutil.py', '--', ...args],
585      workingDirectory: workingDirectory,
586      failOk: failOk,
587    );
588  }
589
590  Future<String> _cloudCopy(String src, String dest) async {
591    // We often don't have permission to overwrite, but
592    // we have permission to remove, so that's what we do.
593    await _runGsUtil(<String>['rm', dest], failOk: true);
594    String mimeType;
595    if (dest.endsWith('.tar.xz')) {
596      mimeType = 'application/x-gtar';
597    }
598    if (dest.endsWith('.zip')) {
599      mimeType = 'application/zip';
600    }
601    if (dest.endsWith('.json')) {
602      mimeType = 'application/json';
603    }
604    return await _runGsUtil(<String>[
605      // Use our preferred MIME type for the files we care about
606      // and let gsutil figure it out for anything else.
607      if (mimeType != null) ...<String>['-h', 'Content-Type:$mimeType'],
608      'cp',
609      src,
610      dest,
611    ]);
612  }
613}
614
615/// Prepares a flutter git repo to be packaged up for distribution.
616/// It mainly serves to populate the .pub-cache with any appropriate Dart
617/// packages, and the flutter cache in bin/cache with the appropriate
618/// dependencies and snapshots.
619///
620/// Archives contain the executables and customizations for the platform that
621/// they are created on.
622Future<void> main(List<String> rawArguments) async {
623  final ArgParser argParser = ArgParser();
624  argParser.addOption(
625    'temp_dir',
626    defaultsTo: null,
627    help: 'A location where temporary files may be written. Defaults to a '
628        'directory in the system temp folder. Will write a few GiB of data, '
629        'so it should have sufficient free space. If a temp_dir is not '
630        'specified, then the default temp_dir will be created, used, and '
631        'removed automatically.',
632  );
633  argParser.addOption('revision',
634      defaultsTo: null,
635      help: 'The Flutter git repo revision to build the '
636          'archive with. Must be the full 40-character hash. Required.');
637  argParser.addOption(
638    'branch',
639    defaultsTo: null,
640    allowed: Branch.values.map<String>((Branch branch) => getBranchName(branch)),
641    help: 'The Flutter branch to build the archive with. Required.',
642  );
643  argParser.addOption(
644    'output',
645    defaultsTo: null,
646    help: 'The path to the directory where the output archive should be '
647        'written. If --output is not specified, the archive will be written to '
648        "the current directory. If the output directory doesn't exist, it, and "
649        'the path to it, will be created.',
650  );
651  argParser.addFlag(
652    'publish',
653    defaultsTo: false,
654    help: 'If set, will publish the archive to Google Cloud Storage upon '
655        'successful creation of the archive. Will publish under this '
656        'directory: $baseUrl$releaseFolder',
657  );
658  argParser.addFlag(
659    'help',
660    defaultsTo: false,
661    negatable: false,
662    help: 'Print help for this command.',
663  );
664
665  final ArgResults parsedArguments = argParser.parse(rawArguments);
666
667  if (parsedArguments['help']) {
668    print(argParser.usage);
669    exit(0);
670  }
671
672  void errorExit(String message, {int exitCode = -1}) {
673    stderr.write('Error: $message\n\n');
674    stderr.write('${argParser.usage}\n');
675    exit(exitCode);
676  }
677
678  final String revision = parsedArguments['revision'];
679  if (revision.isEmpty) {
680    errorExit('Invalid argument: --revision must be specified.');
681  }
682  if (revision.length != 40) {
683    errorExit('Invalid argument: --revision must be the entire hash, not just a prefix.');
684  }
685
686  if (parsedArguments['branch'].isEmpty) {
687    errorExit('Invalid argument: --branch must be specified.');
688  }
689
690  Directory tempDir;
691  bool removeTempDir = false;
692  if (parsedArguments['temp_dir'] == null || parsedArguments['temp_dir'].isEmpty) {
693    tempDir = Directory.systemTemp.createTempSync('flutter_package.');
694    removeTempDir = true;
695  } else {
696    tempDir = Directory(parsedArguments['temp_dir']);
697    if (!tempDir.existsSync()) {
698      errorExit("Temporary directory ${parsedArguments['temp_dir']} doesn't exist.");
699    }
700  }
701
702  Directory outputDir;
703  if (parsedArguments['output'] == null) {
704    outputDir = tempDir;
705  } else {
706    outputDir = Directory(parsedArguments['output']);
707    if (!outputDir.existsSync()) {
708      outputDir.createSync(recursive: true);
709    }
710  }
711
712  final Branch branch = fromBranchName(parsedArguments['branch']);
713  final ArchiveCreator creator = ArchiveCreator(tempDir, outputDir, revision, branch, strict: parsedArguments['publish']);
714  int exitCode = 0;
715  String message;
716  try {
717    final String version = await creator.initializeRepo();
718    final File outputFile = await creator.createArchive();
719    if (parsedArguments['publish']) {
720      final ArchivePublisher publisher = ArchivePublisher(
721        tempDir,
722        revision,
723        branch,
724        version,
725        outputFile,
726      );
727      await publisher.publishArchive();
728    }
729  } on PreparePackageException catch (e) {
730    exitCode = e.exitCode;
731    message = e.message;
732  } catch (e) {
733    exitCode = -1;
734    message = e.toString();
735  } finally {
736    if (removeTempDir) {
737      tempDir.deleteSync(recursive: true);
738    }
739    if (exitCode != 0) {
740      errorExit(message, exitCode: exitCode);
741    }
742    exit(0);
743  }
744}
745