prepare_package.dart 23.4 KB
Newer Older
1 2 3 4
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'dart:async';
6
import 'dart:convert';
7
import 'dart:io' hide Platform;
8
import 'dart:typed_data';
9 10

import 'package:args/args.dart';
11
import 'package:http/http.dart' as http;
12
import 'package:path/path.dart' as path;
13
import 'package:process/process.dart';
14
import 'package:platform/platform.dart' show Platform, LocalPlatform;
15

16
const String chromiumRepo = 'https://chromium.googlesource.com/external/github.com/flutter/flutter';
17 18
const String githubRepo = 'https://github.com/flutter/flutter.git';
const String mingitForWindowsUrl = 'https://storage.googleapis.com/flutter_infra/mingit/'
19
    '603511c649b00bbef0a6122a827ac419b656bc19/mingit.zip';
20 21 22 23
const String gsBase = 'gs://flutter_infra';
const String releaseFolder = '/releases';
const String gsReleaseFolder = '$gsBase$releaseFolder';
const String baseUrl = 'https://storage.googleapis.com/flutter_infra';
24

25
/// Exception class for when a process fails to run, so we can catch
26
/// it and provide something more readable than a stack trace.
27 28
class ProcessRunnerException implements Exception {
  ProcessRunnerException(this.message, [this.result]);
29

30 31
  final String message;
  final ProcessResult result;
32
  int get exitCode => result?.exitCode ?? -1;
33 34

  @override
35 36 37 38 39 40 41
  String toString() {
    String output = runtimeType.toString();
    if (message != null) {
      output += ': $message';
    }
    final String stderr = result?.stderr ?? '';
    if (stderr.isNotEmpty) {
42
      output += ':\n$stderr';
43 44 45 46 47
    }
    return output;
  }
}

48
enum Branch { dev, beta, stable }
49 50 51 52 53 54 55

String getBranchName(Branch branch) {
  switch (branch) {
    case Branch.beta:
      return 'beta';
    case Branch.dev:
      return 'dev';
56 57
    case Branch.stable:
      return 'stable';
58 59 60 61 62 63 64 65 66 67
  }
  return null;
}

Branch fromBranchName(String name) {
  switch (name) {
    case 'beta':
      return Branch.beta;
    case 'dev':
      return Branch.dev;
68 69
    case 'stable':
      return Branch.stable;
70
    default:
71
      throw ArgumentError('Invalid branch name.');
72 73 74 75 76 77 78 79
  }
}

/// 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({
80
    ProcessManager processManager,
81
    this.subprocessOutput = true,
82
    this.defaultWorkingDirectory,
83
    this.platform = const LocalPlatform(),
84
  }) : processManager = processManager ?? const LocalProcessManager() {
85
    environment = Map<String, String>.from(platform.environment);
86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114
  }

  /// 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.
  Map<String, String> 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 a non-zero exit code.
  Future<String> runProcess(
    List<String> commandLine, {
    Directory workingDirectory,
115
    bool failOk = false,
116 117 118 119 120 121
  }) async {
    workingDirectory ??= defaultWorkingDirectory ?? Directory.current;
    if (subprocessOutput) {
      stderr.write('Running "${commandLine.join(' ')}" in ${workingDirectory.path}.\n');
    }
    final List<int> output = <int>[];
122 123
    final Completer<void> stdoutComplete = Completer<void>();
    final Completer<void> stderrComplete = Completer<void>();
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158
    Process process;
    Future<int> 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<int> event) {
          output.addAll(event);
          if (subprocessOutput) {
            stdout.add(event);
          }
        },
        onDone: () async => stdoutComplete.complete(),
      );
      if (subprocessOutput) {
        process.stderr.listen(
          (List<int> 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.toString()}';
159
      throw ProcessRunnerException(message);
160 161 162
    } on ArgumentError catch (e) {
      final String message = 'Running "${commandLine.join(' ')}" in ${workingDirectory.path} '
          'failed with:\n${e.toString()}';
163
      throw ProcessRunnerException(message);
164 165 166 167
    }

    final int exitCode = await allComplete();
    if (exitCode != 0 && !failOk) {
168
      final String message = 'Running "${commandLine.join(' ')}" in ${workingDirectory.path} failed';
169
      throw ProcessRunnerException(
170
        message,
171
        ProcessResult(0, exitCode, null, 'returned $exitCode'),
172
      );
173
    }
174
    return utf8.decoder.convert(output).trim();
175
  }
176 177
}

178
typedef HttpReader = Future<Uint8List> Function(Uri url, {Map<String, String> headers});
179

180 181
/// Creates a pre-populated Flutter archive from a git repo.
class ArchiveCreator {
182
  /// [tempDir] is the directory to use for creating the archive.  The script
183 184 185
  /// 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
186
  /// testing purposes.
187 188 189
  ///
  /// If subprocessOutput is true, then output from processes invoked during
  /// archive creation is echoed to stderr and stdout.
190 191 192 193 194 195
  ArchiveCreator(
    this.tempDir,
    this.outputDir,
    this.revision,
    this.branch, {
    ProcessManager processManager,
196 197
    bool subprocessOutput = true,
    this.platform = const LocalPlatform(),
198
    HttpReader httpReader,
199
  }) : assert(revision.length == 40),
200
       flutterRoot = Directory(path.join(tempDir.path, 'flutter')),
201
       httpReader = httpReader ?? http.readBytes,
202
       _processRunner = ProcessRunner(
203 204 205 206
         processManager: processManager,
         subprocessOutput: subprocessOutput,
         platform: platform,
       ) {
207
    _flutter = path.join(
208
      flutterRoot.absolute.path,
209
      'bin',
210
      'flutter',
211
    );
212
    _processRunner.environment['PUB_CACHE'] = path.join(flutterRoot.absolute.path, '.pub-cache');
213 214
  }

215 216
  /// The platform to use for the environment and determining which
  /// platform we're running on.
217
  final Platform platform;
218 219

  /// The branch to build the archive for.  The branch must contain [revision].
220
  final Branch branch;
221 222 223 224 225

  /// 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.
226
  final String revision;
227 228

  /// The flutter root directory in the [tempDir].
229
  final Directory flutterRoot;
230 231

  /// The temporary directory used to build the archive in.
232
  final Directory tempDir;
233 234

  /// The directory to write the output file to.
235
  final Directory outputDir;
236 237

  final Uri _minGitUri = Uri.parse(mingitForWindowsUrl);
238 239
  final ProcessRunner _processRunner;

240 241 242 243 244
  /// 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;

245 246 247 248 249 250
  File _outputFile;
  String _version;
  String _flutter;

  /// Get the name of the channel as a string.
  String get branchName => getBranchName(branch);
251 252 253

  /// Returns a default archive name when given a Git revision.
  /// Used when an output filename is not given.
254 255
  String get _archiveName {
    final String os = platform.operatingSystem.toLowerCase();
256 257 258 259 260 261 262 263
    // 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';
264 265 266 267 268 269 270 271 272 273
    return 'flutter_${os}_$_version-$branchName.$suffix';
  }

  /// Checks out the flutter repo and prepares it for other operations.
  ///
  /// Returns the version for this release, as obtained from the git tags.
  Future<String> initializeRepo() async {
    await _checkoutFlutter();
    _version = await _getVersion();
    return _version;
274 275
  }

276
  /// Performs all of the steps needed to create an archive.
277 278
  Future<File> createArchive() async {
    assert(_version != null, 'Must run initializeRepo before createArchive');
279
    _outputFile = File(path.join(outputDir.absolute.path, _archiveName));
280 281
    await _installMinGitIfNeeded();
    await _populateCaches();
282 283 284 285 286 287 288 289
    await _archiveFiles(_outputFile);
    return _outputFile;
  }

  /// Returns the version number of this release, according the to tags in
  /// the repo.
  Future<String> _getVersion() async {
    return _runGit(<String>['describe', '--tags', '--abbrev=0']);
290
  }
291 292 293

  /// Clone the Flutter repo and make sure that the git environment is sane
  /// for when the user will unpack it.
294
  Future<void> _checkoutFlutter() async {
295 296
    // 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
297
    // desired revision.
298
    await _runGit(<String>['clone', '-b', branchName, chromiumRepo], workingDirectory: tempDir);
299
    await _runGit(<String>['reset', '--hard', revision]);
300 301

    // Make the origin point to github instead of the chromium mirror.
302
    await _runGit(<String>['remote', 'set-url', 'origin', githubRepo]);
303 304 305
  }

  /// Retrieve the MinGit executable from storage and unpack it.
306
  Future<void> _installMinGitIfNeeded() async {
307
    if (!platform.isWindows) {
308 309
      return;
    }
310
    final Uint8List data = await httpReader(_minGitUri);
311
    final File gitFile = File(path.join(tempDir.absolute.path, 'mingit.zip'));
312 313
    await gitFile.writeAsBytes(data, flush: true);

314
    final Directory minGitPath =
315
        Directory(path.join(flutterRoot.absolute.path, 'bin', 'mingit'));
316
    await minGitPath.create(recursive: true);
317
    await _unzipArchive(gitFile, workingDirectory: minGitPath);
318 319 320
  }

  /// Prepare the archive repo so that it has all of the caches warmed up and
321
  /// is configured for the user to begin working.
322
  Future<void> _populateCaches() async {
323 324 325 326 327 328
    await _runFlutter(<String>['doctor']);
    await _runFlutter(<String>['update-packages']);
    await _runFlutter(<String>['precache']);
    await _runFlutter(<String>['ide-config']);

    // Create each of the templates, since they will call 'pub get' on
329 330
    // themselves when created, and this will warm the cache with their
    // dependencies too.
331
    for (String template in <String>['app', 'package', 'plugin']) {
332
      final String createName = path.join(tempDir.path, 'create_$template');
333
      await _runFlutter(
334 335 336 337 338 339 340
        <String>['create', '--template=$template', createName],
      );
    }

    // 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.
341
    await _runGit(<String>['clean', '-f', '-X', '**/.packages']);
342 343
  }

344
  /// Write the archive to the given output file.
345
  Future<void> _archiveFiles(File outputFile) async {
346
    if (outputFile.path.toLowerCase().endsWith('.zip')) {
347
      await _createZipArchive(outputFile, flutterRoot);
348
    } else if (outputFile.path.toLowerCase().endsWith('.tar.xz')) {
349
      await _createTarArchive(outputFile, flutterRoot);
350 351 352
    }
  }

353
  Future<String> _runFlutter(List<String> args, {Directory workingDirectory}) {
354 355 356 357
    return _processRunner.runProcess(
      <String>[_flutter]..addAll(args),
      workingDirectory: workingDirectory ?? flutterRoot,
    );
358
  }
359

360
  Future<String> _runGit(List<String> args, {Directory workingDirectory}) {
361 362 363 364
    return _processRunner.runProcess(
      <String>['git']..addAll(args),
      workingDirectory: workingDirectory ?? flutterRoot,
    );
365 366
  }

367 368
  /// Unpacks the given zip file into the currentDirectory (if set), or the
  /// same directory as the archive.
369
  Future<String> _unzipArchive(File archive, {Directory workingDirectory}) {
370
    workingDirectory ??= Directory(path.dirname(archive.absolute.path));
371 372 373 374 375 376 377 378 379 380 381 382 383
    List<String> commandLine;
    if (platform.isWindows) {
      commandLine = <String>[
        '7za',
        'x',
        archive.absolute.path,
      ];
    } else {
      commandLine = <String>[
        'unzip',
        archive.absolute.path,
      ];
    }
384
    return _processRunner.runProcess(commandLine, workingDirectory: workingDirectory);
385 386
  }

387 388
  /// Create a zip archive from the directory source.
  Future<String> _createZipArchive(File output, Directory source) {
389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407
    List<String> commandLine;
    if (platform.isWindows) {
      commandLine = <String>[
        '7za',
        'a',
        '-tzip',
        '-mx=9',
        output.absolute.path,
        path.basename(source.path),
      ];
    } else {
      commandLine = <String>[
        'zip',
        '-r',
        '-9',
        output.absolute.path,
        path.basename(source.path),
      ];
    }
408 409
    return _processRunner.runProcess(
      commandLine,
410
      workingDirectory: Directory(path.dirname(source.absolute.path)),
411
    );
412 413
  }

414 415
  /// Create a tar archive from the directory source.
  Future<String> _createTarArchive(File output, Directory source) {
416
    return _processRunner.runProcess(<String>[
417
      'tar',
418
      'cJf',
419 420
      output.absolute.path,
      path.basename(source.absolute.path),
421
    ], workingDirectory: Directory(path.dirname(source.absolute.path)));
422
  }
423
}
424

425 426 427 428 429 430 431 432
class ArchivePublisher {
  ArchivePublisher(
    this.tempDir,
    this.revision,
    this.branch,
    this.version,
    this.outputFile, {
    ProcessManager processManager,
433 434
    bool subprocessOutput = true,
    this.platform = const LocalPlatform(),
435 436
  }) : assert(revision.length == 40),
       platformName = platform.operatingSystem.toLowerCase(),
437
       metadataGsPath = '$gsReleaseFolder/${getMetadataFilename(platform)}',
438
       _processRunner = ProcessRunner(
439 440 441 442 443 444 445 446 447 448 449 450 451 452
         processManager: processManager,
         subprocessOutput: subprocessOutput,
       );

  final Platform platform;
  final String platformName;
  final String metadataGsPath;
  final Branch branch;
  final String revision;
  final String version;
  final Directory tempDir;
  final File outputFile;
  final ProcessRunner _processRunner;
  String get branchName => getBranchName(branch);
453 454
  String get destinationArchivePath => '$branchName/$platformName/${path.basename(outputFile.path)}';
  static String getMetadataFilename(Platform platform) => 'releases_${platform.operatingSystem.toLowerCase()}.json';
455 456

  /// Publish the archive to Google Storage.
457
  Future<void> publishArchive() async {
458 459 460
    final String destGsPath = '$gsReleaseFolder/$destinationArchivePath';
    await _cloudCopy(outputFile.absolute.path, destGsPath);
    assert(tempDir.existsSync());
461
    await _updateMetadata();
462 463
  }

464 465 466 467 468 469 470 471 472 473 474 475 476 477
  Map<String, dynamic> _addRelease(Map<String, dynamic> jsonData) {
    jsonData['base_url'] = '$baseUrl$releaseFolder';
    if (!jsonData.containsKey('current_release')) {
      jsonData['current_release'] = <String, String>{};
    }
    jsonData['current_release'][branchName] = revision;
    if (!jsonData.containsKey('releases')) {
      jsonData['releases'] = <Map<String, dynamic>>[];
    }

    final Map<String, dynamic> newEntry = <String, dynamic>{};
    newEntry['hash'] = revision;
    newEntry['channel'] = branchName;
    newEntry['version'] = version;
478
    newEntry['release_date'] = DateTime.now().toUtc().toIso8601String();
479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499
    newEntry['archive'] = destinationArchivePath;

    // Search for any entries with the same hash and channel and remove them.
    final List<dynamic> releases = jsonData['releases'];
    final List<Map<String, dynamic>> prunedReleases = <Map<String, dynamic>>[];
    for (Map<String, dynamic> entry in releases) {
      if (entry['hash'] != newEntry['hash'] || entry['channel'] != newEntry['channel']) {
        prunedReleases.add(entry);
      }
    }

    prunedReleases.add(newEntry);
    prunedReleases.sort((Map<String, dynamic> a, Map<String, dynamic> b) {
      final DateTime aDate = DateTime.parse(a['release_date']);
      final DateTime bDate = DateTime.parse(b['release_date']);
      return bDate.compareTo(aDate);
    });
    jsonData['releases'] = prunedReleases;
    return jsonData;
  }

500
  Future<void> _updateMetadata() async {
501 502 503 504
    // 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.
505
    final File metadataFile = File(
506 507 508 509
      path.join(tempDir.absolute.path, getMetadataFilename(platform)),
    );
    await _runGsUtil(<String>['cp', metadataGsPath, metadataFile.absolute.path]);
    final String currentMetadata = metadataFile.readAsStringSync();
510
    if (currentMetadata.isEmpty) {
511
      throw ProcessRunnerException('Empty metadata received from server');
512 513
    }

514
    Map<String, dynamic> jsonData;
515
    try {
516 517
      jsonData = json.decode(currentMetadata);
    } on FormatException catch (e) {
518
      throw ProcessRunnerException('Unable to parse JSON metadata received from cloud: $e');
519 520
    }

521
    jsonData = _addRelease(jsonData);
522

523
    const JsonEncoder encoder = JsonEncoder.withIndent('  ');
524 525
    metadataFile.writeAsStringSync(encoder.convert(jsonData));
    await _cloudCopy(metadataFile.absolute.path, metadataGsPath);
526 527
  }

528 529 530
  Future<String> _runGsUtil(
    List<String> args, {
    Directory workingDirectory,
531
    bool failOk = false,
532
  }) async {
533 534 535 536 537 538 539 540
    return _processRunner.runProcess(
      <String>['gsutil']..addAll(args),
      workingDirectory: workingDirectory,
      failOk: failOk,
    );
  }

  Future<String> _cloudCopy(String src, String dest) async {
541 542
    // We often don't have permission to overwrite, but
    // we have permission to remove, so that's what we do.
543
    await _runGsUtil(<String>['rm', dest], failOk: true);
544 545 546 547 548 549 550 551 552 553
    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';
    }
554
    final List<String> args = <String>[];
555 556 557 558 559
    // Use our preferred MIME type for the files we care about
    // and let gsutil figure it out for anything else.
    if (mimeType != null) {
      args.addAll(<String>['-h', 'Content-Type:$mimeType']);
    }
560
    args.addAll(<String>['cp', src, dest]);
561
    return _runGsUtil(args);
562 563 564 565 566 567 568
  }
}

/// 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.
569
///
Ian Hickson's avatar
Ian Hickson committed
570 571
/// Archives contain the executables and customizations for the platform that
/// they are created on.
Ian Hickson's avatar
Ian Hickson committed
572
Future<void> main(List<String> rawArguments) async {
573
  final ArgParser argParser = ArgParser();
574 575 576 577 578
  argParser.addOption(
    'temp_dir',
    defaultsTo: null,
    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, '
579 580 581
        '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.',
582
  );
583 584 585 586
  argParser.addOption('revision',
      defaultsTo: null,
      help: 'The Flutter git repo revision to build the '
          'archive with. Must be the full 40-character hash. Required.');
587
  argParser.addOption(
588 589
    'branch',
    defaultsTo: null,
590
    allowed: Branch.values.map<String>((Branch branch) => getBranchName(branch)),
591
    help: 'The Flutter branch to build the archive with. Required.',
592 593 594 595
  );
  argParser.addOption(
    'output',
    defaultsTo: null,
596 597 598 599 600 601 602 603
    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',
    defaultsTo: false,
604 605 606 607 608 609 610 611 612
    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(
    'help',
    defaultsTo: false,
    negatable: false,
    help: 'Print help for this command.',
613
  );
614

Ian Hickson's avatar
Ian Hickson committed
615
  final ArgResults parsedArguments = argParser.parse(rawArguments);
616

Ian Hickson's avatar
Ian Hickson committed
617
  if (parsedArguments['help']) {
618 619 620 621
    print(argParser.usage);
    exit(0);
  }

622 623 624 625 626 627
  void errorExit(String message, {int exitCode = -1}) {
    stderr.write('Error: $message\n\n');
    stderr.write('${argParser.usage}\n');
    exit(exitCode);
  }

Ian Hickson's avatar
Ian Hickson committed
628
  final String revision = parsedArguments['revision'];
629
  if (revision.isEmpty) {
630 631
    errorExit('Invalid argument: --revision must be specified.');
  }
632 633 634 635
  if (revision.length != 40) {
    errorExit('Invalid argument: --revision must be the entire hash, not just a prefix.');
  }

Ian Hickson's avatar
Ian Hickson committed
636
  if (parsedArguments['branch'].isEmpty) {
637 638
    errorExit('Invalid argument: --branch must be specified.');
  }
639

640
  Directory tempDir;
641
  bool removeTempDir = false;
Ian Hickson's avatar
Ian Hickson committed
642
  if (parsedArguments['temp_dir'] == null || parsedArguments['temp_dir'].isEmpty) {
643
    tempDir = Directory.systemTemp.createTempSync('flutter_package.');
644 645
    removeTempDir = true;
  } else {
Ian Hickson's avatar
Ian Hickson committed
646
    tempDir = Directory(parsedArguments['temp_dir']);
647
    if (!tempDir.existsSync()) {
Ian Hickson's avatar
Ian Hickson committed
648
      errorExit("Temporary directory ${parsedArguments['temp_dir']} doesn't exist.");
649 650 651
    }
  }

652
  Directory outputDir;
Ian Hickson's avatar
Ian Hickson committed
653
  if (parsedArguments['output'] == null) {
654
    outputDir = tempDir;
655
  } else {
Ian Hickson's avatar
Ian Hickson committed
656
    outputDir = Directory(parsedArguments['output']);
657 658
    if (!outputDir.existsSync()) {
      outputDir.createSync(recursive: true);
659
    }
660 661
  }

Ian Hickson's avatar
Ian Hickson committed
662
  final Branch branch = fromBranchName(parsedArguments['branch']);
663
  final ArchiveCreator creator = ArchiveCreator(tempDir, outputDir, revision, branch);
664 665 666
  int exitCode = 0;
  String message;
  try {
667 668
    final String version = await creator.initializeRepo();
    final File outputFile = await creator.createArchive();
Ian Hickson's avatar
Ian Hickson committed
669
    if (parsedArguments['publish']) {
670
      final ArchivePublisher publisher = ArchivePublisher(
671 672 673 674 675 676 677 678 679
        tempDir,
        revision,
        branch,
        version,
        outputFile,
      );
      await publisher.publishArchive();
    }
  } on ProcessRunnerException catch (e) {
680 681
    exitCode = e.exitCode;
    message = e.message;
682 683 684
  } catch (e) {
    exitCode = -1;
    message = e.toString();
685 686
  } finally {
    if (removeTempDir) {
687
      tempDir.deleteSync(recursive: true);
688 689 690 691 692 693 694
    }
    if (exitCode != 0) {
      errorExit(message, exitCode: exitCode);
    }
    exit(0);
  }
}