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