2017-12-15 23:01:30 +00:00
|
|
|
// 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.
|
|
|
|
|
2018-01-10 21:37:36 +00:00
|
|
|
import 'dart:async';
|
2017-12-15 23:01:30 +00:00
|
|
|
import 'dart:convert';
|
2018-02-06 23:32:19 +00:00
|
|
|
import 'dart:io' hide Platform;
|
2018-01-10 21:37:36 +00:00
|
|
|
import 'dart:typed_data';
|
2017-12-15 23:01:30 +00:00
|
|
|
|
|
|
|
import 'package:args/args.dart';
|
2019-02-26 20:02:01 +00:00
|
|
|
import 'package:crypto/crypto.dart';
|
|
|
|
import 'package:crypto/src/digest_sink.dart';
|
2018-01-10 21:37:36 +00:00
|
|
|
import 'package:http/http.dart' as http;
|
2017-12-15 23:01:30 +00:00
|
|
|
import 'package:path/path.dart' as path;
|
2018-02-06 23:32:19 +00:00
|
|
|
import 'package:platform/platform.dart' show Platform, LocalPlatform;
|
2019-02-26 20:02:01 +00:00
|
|
|
import 'package:process/process.dart';
|
2017-12-15 23:01:30 +00:00
|
|
|
|
2018-02-09 23:42:51 +00:00
|
|
|
const String chromiumRepo = 'https://chromium.googlesource.com/external/github.com/flutter/flutter';
|
2018-02-07 20:21:14 +00:00
|
|
|
const String githubRepo = 'https://github.com/flutter/flutter.git';
|
|
|
|
const String mingitForWindowsUrl = 'https://storage.googleapis.com/flutter_infra/mingit/'
|
2018-01-10 21:37:36 +00:00
|
|
|
'603511c649b00bbef0a6122a827ac419b656bc19/mingit.zip';
|
2018-02-07 20:21:14 +00:00
|
|
|
const String gsBase = 'gs://flutter_infra';
|
|
|
|
const String releaseFolder = '/releases';
|
|
|
|
const String gsReleaseFolder = '$gsBase$releaseFolder';
|
|
|
|
const String baseUrl = 'https://storage.googleapis.com/flutter_infra';
|
2017-12-15 23:01:30 +00:00
|
|
|
|
2018-02-06 23:32:19 +00:00
|
|
|
/// Exception class for when a process fails to run, so we can catch
|
2017-12-15 23:01:30 +00:00
|
|
|
/// it and provide something more readable than a stack trace.
|
2019-03-01 22:38:40 +00:00
|
|
|
class PreparePackageException implements Exception {
|
|
|
|
PreparePackageException(this.message, [this.result]);
|
2017-12-15 23:01:30 +00:00
|
|
|
|
2018-02-06 23:32:19 +00:00
|
|
|
final String message;
|
|
|
|
final ProcessResult result;
|
2018-03-10 02:58:41 +00:00
|
|
|
int get exitCode => result?.exitCode ?? -1;
|
2017-12-15 23:01:30 +00:00
|
|
|
|
|
|
|
@override
|
2018-02-06 23:32:19 +00:00
|
|
|
String toString() {
|
|
|
|
String output = runtimeType.toString();
|
|
|
|
if (message != null) {
|
|
|
|
output += ': $message';
|
|
|
|
}
|
|
|
|
final String stderr = result?.stderr ?? '';
|
|
|
|
if (stderr.isNotEmpty) {
|
2018-03-10 02:58:41 +00:00
|
|
|
output += ':\n$stderr';
|
2018-02-06 23:32:19 +00:00
|
|
|
}
|
|
|
|
return output;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-10-30 23:57:54 +00:00
|
|
|
enum Branch { dev, beta, stable }
|
2018-02-06 23:32:19 +00:00
|
|
|
|
|
|
|
String getBranchName(Branch branch) {
|
|
|
|
switch (branch) {
|
|
|
|
case Branch.beta:
|
|
|
|
return 'beta';
|
|
|
|
case Branch.dev:
|
|
|
|
return 'dev';
|
2018-10-30 23:57:54 +00:00
|
|
|
case Branch.stable:
|
|
|
|
return 'stable';
|
2018-02-06 23:32:19 +00:00
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
Branch fromBranchName(String name) {
|
|
|
|
switch (name) {
|
|
|
|
case 'beta':
|
|
|
|
return Branch.beta;
|
|
|
|
case 'dev':
|
|
|
|
return Branch.dev;
|
2018-10-30 23:57:54 +00:00
|
|
|
case 'stable':
|
|
|
|
return Branch.stable;
|
2018-02-06 23:32:19 +00:00
|
|
|
default:
|
2018-09-12 06:29:29 +00:00
|
|
|
throw ArgumentError('Invalid branch name.');
|
2018-02-06 23:32:19 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// 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({
|
2018-02-07 20:21:14 +00:00
|
|
|
ProcessManager processManager,
|
2018-06-05 06:50:40 +00:00
|
|
|
this.subprocessOutput = true,
|
2018-02-06 23:32:19 +00:00
|
|
|
this.defaultWorkingDirectory,
|
2018-06-05 06:50:40 +00:00
|
|
|
this.platform = const LocalPlatform(),
|
2018-02-07 20:21:14 +00:00
|
|
|
}) : processManager = processManager ?? const LocalProcessManager() {
|
2018-09-12 06:29:29 +00:00
|
|
|
environment = Map<String, String>.from(platform.environment);
|
2018-02-06 23:32:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// 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,
|
2018-06-05 06:50:40 +00:00
|
|
|
bool failOk = false,
|
2018-02-06 23:32:19 +00:00
|
|
|
}) async {
|
|
|
|
workingDirectory ??= defaultWorkingDirectory ?? Directory.current;
|
|
|
|
if (subprocessOutput) {
|
|
|
|
stderr.write('Running "${commandLine.join(' ')}" in ${workingDirectory.path}.\n');
|
|
|
|
}
|
|
|
|
final List<int> output = <int>[];
|
2018-10-04 16:44:23 +00:00
|
|
|
final Completer<void> stdoutComplete = Completer<void>();
|
|
|
|
final Completer<void> stderrComplete = Completer<void>();
|
2018-02-06 23:32:19 +00:00
|
|
|
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()}';
|
2019-03-01 22:38:40 +00:00
|
|
|
throw PreparePackageException(message);
|
2018-03-10 02:58:41 +00:00
|
|
|
} on ArgumentError catch (e) {
|
|
|
|
final String message = 'Running "${commandLine.join(' ')}" in ${workingDirectory.path} '
|
|
|
|
'failed with:\n${e.toString()}';
|
2019-03-01 22:38:40 +00:00
|
|
|
throw PreparePackageException(message);
|
2018-02-06 23:32:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
final int exitCode = await allComplete();
|
|
|
|
if (exitCode != 0 && !failOk) {
|
2018-03-20 15:14:08 +00:00
|
|
|
final String message = 'Running "${commandLine.join(' ')}" in ${workingDirectory.path} failed';
|
2019-03-01 22:38:40 +00:00
|
|
|
throw PreparePackageException(
|
2018-03-20 15:14:08 +00:00
|
|
|
message,
|
2018-09-12 06:29:29 +00:00
|
|
|
ProcessResult(0, exitCode, null, 'returned $exitCode'),
|
2018-03-20 15:14:08 +00:00
|
|
|
);
|
2018-02-06 23:32:19 +00:00
|
|
|
}
|
2018-03-12 22:44:25 +00:00
|
|
|
return utf8.decoder.convert(output).trim();
|
2018-02-06 23:32:19 +00:00
|
|
|
}
|
2017-12-15 23:01:30 +00:00
|
|
|
}
|
|
|
|
|
2018-09-14 19:06:19 +00:00
|
|
|
typedef HttpReader = Future<Uint8List> Function(Uri url, {Map<String, String> headers});
|
2018-02-07 20:21:14 +00:00
|
|
|
|
2017-12-15 23:01:30 +00:00
|
|
|
/// Creates a pre-populated Flutter archive from a git repo.
|
|
|
|
class ArchiveCreator {
|
2018-02-06 23:32:19 +00:00
|
|
|
/// [tempDir] is the directory to use for creating the archive. The script
|
2018-01-10 21:37:36 +00:00
|
|
|
/// 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
|
2017-12-15 23:01:30 +00:00
|
|
|
/// testing purposes.
|
2018-01-10 21:37:36 +00:00
|
|
|
///
|
|
|
|
/// If subprocessOutput is true, then output from processes invoked during
|
|
|
|
/// archive creation is echoed to stderr and stdout.
|
2018-02-06 23:32:19 +00:00
|
|
|
ArchiveCreator(
|
|
|
|
this.tempDir,
|
|
|
|
this.outputDir,
|
|
|
|
this.revision,
|
|
|
|
this.branch, {
|
2019-03-01 22:38:40 +00:00
|
|
|
this.strict = true,
|
2018-02-06 23:32:19 +00:00
|
|
|
ProcessManager processManager,
|
2018-06-05 06:50:40 +00:00
|
|
|
bool subprocessOutput = true,
|
|
|
|
this.platform = const LocalPlatform(),
|
2018-02-07 20:21:14 +00:00
|
|
|
HttpReader httpReader,
|
2019-02-26 20:02:01 +00:00
|
|
|
}) : assert(revision.length == 40),
|
|
|
|
flutterRoot = Directory(path.join(tempDir.path, 'flutter')),
|
|
|
|
httpReader = httpReader ?? http.readBytes,
|
|
|
|
_processRunner = ProcessRunner(
|
|
|
|
processManager: processManager,
|
|
|
|
subprocessOutput: subprocessOutput,
|
|
|
|
platform: platform,
|
|
|
|
) {
|
2018-01-10 21:37:36 +00:00
|
|
|
_flutter = path.join(
|
2018-02-06 23:32:19 +00:00
|
|
|
flutterRoot.absolute.path,
|
2017-12-15 23:01:30 +00:00
|
|
|
'bin',
|
2018-01-10 21:37:36 +00:00
|
|
|
'flutter',
|
2017-12-15 23:01:30 +00:00
|
|
|
);
|
2018-02-06 23:32:19 +00:00
|
|
|
_processRunner.environment['PUB_CACHE'] = path.join(flutterRoot.absolute.path, '.pub-cache');
|
2018-01-10 21:37:36 +00:00
|
|
|
}
|
|
|
|
|
2018-02-07 20:21:14 +00:00
|
|
|
/// The platform to use for the environment and determining which
|
|
|
|
/// platform we're running on.
|
2018-02-06 23:32:19 +00:00
|
|
|
final Platform platform;
|
2018-02-07 20:21:14 +00:00
|
|
|
|
|
|
|
/// The branch to build the archive for. The branch must contain [revision].
|
2018-02-06 23:32:19 +00:00
|
|
|
final Branch branch;
|
2018-02-07 20:21:14 +00:00
|
|
|
|
|
|
|
/// 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.
|
2018-02-06 23:32:19 +00:00
|
|
|
final String revision;
|
2018-02-07 20:21:14 +00:00
|
|
|
|
|
|
|
/// The flutter root directory in the [tempDir].
|
2018-02-06 23:32:19 +00:00
|
|
|
final Directory flutterRoot;
|
2018-02-07 20:21:14 +00:00
|
|
|
|
|
|
|
/// The temporary directory used to build the archive in.
|
2018-02-06 23:32:19 +00:00
|
|
|
final Directory tempDir;
|
2018-02-07 20:21:14 +00:00
|
|
|
|
|
|
|
/// The directory to write the output file to.
|
2018-02-06 23:32:19 +00:00
|
|
|
final Directory outputDir;
|
2018-02-07 20:21:14 +00:00
|
|
|
|
2019-03-01 22:38:40 +00:00
|
|
|
/// 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;
|
|
|
|
|
2018-02-07 20:21:14 +00:00
|
|
|
final Uri _minGitUri = Uri.parse(mingitForWindowsUrl);
|
2018-02-06 23:32:19 +00:00
|
|
|
final ProcessRunner _processRunner;
|
|
|
|
|
2018-02-07 20:21:14 +00:00
|
|
|
/// 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;
|
|
|
|
|
2018-02-06 23:32:19 +00:00
|
|
|
File _outputFile;
|
|
|
|
String _version;
|
|
|
|
String _flutter;
|
|
|
|
|
|
|
|
/// Get the name of the channel as a string.
|
|
|
|
String get branchName => getBranchName(branch);
|
2018-01-10 21:37:36 +00:00
|
|
|
|
|
|
|
/// Returns a default archive name when given a Git revision.
|
|
|
|
/// Used when an output filename is not given.
|
2018-02-06 23:32:19 +00:00
|
|
|
String get _archiveName {
|
|
|
|
final String os = platform.operatingSystem.toLowerCase();
|
2018-02-27 21:15:32 +00:00
|
|
|
// 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';
|
2018-02-06 23:32:19 +00:00
|
|
|
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;
|
2017-12-15 23:01:30 +00:00
|
|
|
}
|
|
|
|
|
2018-01-10 21:37:36 +00:00
|
|
|
/// Performs all of the steps needed to create an archive.
|
2018-02-06 23:32:19 +00:00
|
|
|
Future<File> createArchive() async {
|
|
|
|
assert(_version != null, 'Must run initializeRepo before createArchive');
|
2018-09-12 06:29:29 +00:00
|
|
|
_outputFile = File(path.join(outputDir.absolute.path, _archiveName));
|
2018-01-10 21:37:36 +00:00
|
|
|
await _installMinGitIfNeeded();
|
|
|
|
await _populateCaches();
|
2018-02-06 23:32:19 +00:00
|
|
|
await _archiveFiles(_outputFile);
|
|
|
|
return _outputFile;
|
|
|
|
}
|
|
|
|
|
2019-03-01 22:38:40 +00:00
|
|
|
/// Returns the version number of this release, according the to tags in the
|
|
|
|
/// repo.
|
|
|
|
///
|
|
|
|
/// 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.
|
2018-02-06 23:32:19 +00:00
|
|
|
Future<String> _getVersion() async {
|
2019-03-01 22:38:40 +00:00
|
|
|
if (strict) {
|
|
|
|
try {
|
|
|
|
return _runGit(<String>['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 {
|
|
|
|
return _runGit(<String>['describe', '--tags', '--abbrev=0', revision]);
|
|
|
|
}
|
2018-01-10 21:37:36 +00:00
|
|
|
}
|
2017-12-15 23:01:30 +00:00
|
|
|
|
|
|
|
/// Clone the Flutter repo and make sure that the git environment is sane
|
|
|
|
/// for when the user will unpack it.
|
2018-10-04 16:44:23 +00:00
|
|
|
Future<void> _checkoutFlutter() async {
|
2018-02-06 23:32:19 +00:00
|
|
|
// 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
|
2017-12-15 23:01:30 +00:00
|
|
|
// desired revision.
|
2018-02-07 20:21:14 +00:00
|
|
|
await _runGit(<String>['clone', '-b', branchName, chromiumRepo], workingDirectory: tempDir);
|
2018-01-10 21:37:36 +00:00
|
|
|
await _runGit(<String>['reset', '--hard', revision]);
|
2017-12-15 23:01:30 +00:00
|
|
|
|
|
|
|
// Make the origin point to github instead of the chromium mirror.
|
2018-03-14 19:56:44 +00:00
|
|
|
await _runGit(<String>['remote', 'set-url', 'origin', githubRepo]);
|
2018-01-10 21:37:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Retrieve the MinGit executable from storage and unpack it.
|
2018-10-04 16:44:23 +00:00
|
|
|
Future<void> _installMinGitIfNeeded() async {
|
2018-02-06 23:32:19 +00:00
|
|
|
if (!platform.isWindows) {
|
2018-01-10 21:37:36 +00:00
|
|
|
return;
|
|
|
|
}
|
2018-02-07 20:21:14 +00:00
|
|
|
final Uint8List data = await httpReader(_minGitUri);
|
2018-09-12 06:29:29 +00:00
|
|
|
final File gitFile = File(path.join(tempDir.absolute.path, 'mingit.zip'));
|
2018-01-10 21:37:36 +00:00
|
|
|
await gitFile.writeAsBytes(data, flush: true);
|
|
|
|
|
2019-02-26 20:02:01 +00:00
|
|
|
final Directory minGitPath = Directory(path.join(flutterRoot.absolute.path, 'bin', 'mingit'));
|
2018-01-10 21:37:36 +00:00
|
|
|
await minGitPath.create(recursive: true);
|
2018-02-06 23:32:19 +00:00
|
|
|
await _unzipArchive(gitFile, workingDirectory: minGitPath);
|
2017-12-15 23:01:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Prepare the archive repo so that it has all of the caches warmed up and
|
2018-01-10 21:37:36 +00:00
|
|
|
/// is configured for the user to begin working.
|
2018-10-04 16:44:23 +00:00
|
|
|
Future<void> _populateCaches() async {
|
2018-01-10 21:37:36 +00:00
|
|
|
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
|
2017-12-15 23:01:30 +00:00
|
|
|
// themselves when created, and this will warm the cache with their
|
|
|
|
// dependencies too.
|
2018-02-07 20:21:14 +00:00
|
|
|
for (String template in <String>['app', 'package', 'plugin']) {
|
2018-02-06 23:32:19 +00:00
|
|
|
final String createName = path.join(tempDir.path, 'create_$template');
|
2018-01-10 21:37:36 +00:00
|
|
|
await _runFlutter(
|
2017-12-15 23:01:30 +00:00
|
|
|
<String>['create', '--template=$template', createName],
|
2019-01-11 02:04:23 +00:00
|
|
|
// Run it outside the cloned Flutter repo to not nest git repos, since
|
|
|
|
// they'll be git repos themselves too.
|
|
|
|
workingDirectory: tempDir,
|
2017-12-15 23:01:30 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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.
|
2018-01-10 21:37:36 +00:00
|
|
|
await _runGit(<String>['clean', '-f', '-X', '**/.packages']);
|
2017-12-15 23:01:30 +00:00
|
|
|
}
|
|
|
|
|
2018-01-10 21:37:36 +00:00
|
|
|
/// Write the archive to the given output file.
|
2018-10-04 16:44:23 +00:00
|
|
|
Future<void> _archiveFiles(File outputFile) async {
|
2017-12-15 23:01:30 +00:00
|
|
|
if (outputFile.path.toLowerCase().endsWith('.zip')) {
|
2018-02-06 23:32:19 +00:00
|
|
|
await _createZipArchive(outputFile, flutterRoot);
|
2017-12-16 00:40:49 +00:00
|
|
|
} else if (outputFile.path.toLowerCase().endsWith('.tar.xz')) {
|
2018-02-06 23:32:19 +00:00
|
|
|
await _createTarArchive(outputFile, flutterRoot);
|
2017-12-15 23:01:30 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-02-06 23:32:19 +00:00
|
|
|
Future<String> _runFlutter(List<String> args, {Directory workingDirectory}) {
|
2018-03-20 15:14:08 +00:00
|
|
|
return _processRunner.runProcess(
|
2019-06-27 19:23:16 +00:00
|
|
|
<String>[_flutter, ...args],
|
2018-03-20 15:14:08 +00:00
|
|
|
workingDirectory: workingDirectory ?? flutterRoot,
|
|
|
|
);
|
2018-02-06 23:32:19 +00:00
|
|
|
}
|
2017-12-15 23:01:30 +00:00
|
|
|
|
2018-01-10 21:37:36 +00:00
|
|
|
Future<String> _runGit(List<String> args, {Directory workingDirectory}) {
|
2018-03-20 15:14:08 +00:00
|
|
|
return _processRunner.runProcess(
|
2019-06-27 19:23:16 +00:00
|
|
|
<String>['git', ...args],
|
2018-03-20 15:14:08 +00:00
|
|
|
workingDirectory: workingDirectory ?? flutterRoot,
|
|
|
|
);
|
2017-12-15 23:01:30 +00:00
|
|
|
}
|
|
|
|
|
2018-01-10 21:37:36 +00:00
|
|
|
/// Unpacks the given zip file into the currentDirectory (if set), or the
|
|
|
|
/// same directory as the archive.
|
2018-02-06 23:32:19 +00:00
|
|
|
Future<String> _unzipArchive(File archive, {Directory workingDirectory}) {
|
2018-09-12 06:29:29 +00:00
|
|
|
workingDirectory ??= Directory(path.dirname(archive.absolute.path));
|
2018-03-14 19:56:44 +00:00
|
|
|
List<String> commandLine;
|
|
|
|
if (platform.isWindows) {
|
|
|
|
commandLine = <String>[
|
|
|
|
'7za',
|
|
|
|
'x',
|
|
|
|
archive.absolute.path,
|
|
|
|
];
|
|
|
|
} else {
|
|
|
|
commandLine = <String>[
|
|
|
|
'unzip',
|
|
|
|
archive.absolute.path,
|
|
|
|
];
|
|
|
|
}
|
2018-02-06 23:32:19 +00:00
|
|
|
return _processRunner.runProcess(commandLine, workingDirectory: workingDirectory);
|
2017-12-15 23:01:30 +00:00
|
|
|
}
|
|
|
|
|
2018-01-10 21:37:36 +00:00
|
|
|
/// Create a zip archive from the directory source.
|
2019-08-28 22:20:47 +00:00
|
|
|
Future<String> _createZipArchive(File output, Directory source) async {
|
2018-02-27 21:15:32 +00:00
|
|
|
List<String> commandLine;
|
|
|
|
if (platform.isWindows) {
|
2019-08-28 22:20:47 +00:00
|
|
|
// Unhide the .git folder, https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/attrib.
|
|
|
|
await _processRunner.runProcess(
|
|
|
|
<String>['attrib', '-h', '.git'],
|
|
|
|
workingDirectory: Directory(source.absolute.path),
|
|
|
|
);
|
2018-02-27 21:15:32 +00:00
|
|
|
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),
|
|
|
|
];
|
|
|
|
}
|
2018-03-20 15:14:08 +00:00
|
|
|
return _processRunner.runProcess(
|
|
|
|
commandLine,
|
2018-09-12 06:29:29 +00:00
|
|
|
workingDirectory: Directory(path.dirname(source.absolute.path)),
|
2018-03-20 15:14:08 +00:00
|
|
|
);
|
2017-12-15 23:01:30 +00:00
|
|
|
}
|
|
|
|
|
2018-01-10 21:37:36 +00:00
|
|
|
/// Create a tar archive from the directory source.
|
|
|
|
Future<String> _createTarArchive(File output, Directory source) {
|
2018-02-06 23:32:19 +00:00
|
|
|
return _processRunner.runProcess(<String>[
|
2018-01-10 21:37:36 +00:00
|
|
|
'tar',
|
2017-12-16 00:40:49 +00:00
|
|
|
'cJf',
|
2017-12-15 23:01:30 +00:00
|
|
|
output.absolute.path,
|
|
|
|
path.basename(source.absolute.path),
|
2018-09-12 06:29:29 +00:00
|
|
|
], workingDirectory: Directory(path.dirname(source.absolute.path)));
|
2018-01-10 21:37:36 +00:00
|
|
|
}
|
2018-02-06 23:32:19 +00:00
|
|
|
}
|
2018-01-10 21:37:36 +00:00
|
|
|
|
2018-02-06 23:32:19 +00:00
|
|
|
class ArchivePublisher {
|
|
|
|
ArchivePublisher(
|
|
|
|
this.tempDir,
|
|
|
|
this.revision,
|
|
|
|
this.branch,
|
|
|
|
this.version,
|
|
|
|
this.outputFile, {
|
|
|
|
ProcessManager processManager,
|
2018-06-05 06:50:40 +00:00
|
|
|
bool subprocessOutput = true,
|
|
|
|
this.platform = const LocalPlatform(),
|
2019-02-26 20:02:01 +00:00
|
|
|
}) : assert(revision.length == 40),
|
|
|
|
platformName = platform.operatingSystem.toLowerCase(),
|
|
|
|
metadataGsPath = '$gsReleaseFolder/${getMetadataFilename(platform)}',
|
|
|
|
_processRunner = ProcessRunner(
|
|
|
|
processManager: processManager,
|
|
|
|
subprocessOutput: subprocessOutput,
|
|
|
|
);
|
2018-02-06 23:32:19 +00:00
|
|
|
|
|
|
|
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);
|
2018-03-20 15:14:08 +00:00
|
|
|
String get destinationArchivePath => '$branchName/$platformName/${path.basename(outputFile.path)}';
|
|
|
|
static String getMetadataFilename(Platform platform) => 'releases_${platform.operatingSystem.toLowerCase()}.json';
|
2018-02-06 23:32:19 +00:00
|
|
|
|
2019-02-26 20:02:01 +00:00
|
|
|
Future<String> _getChecksum(File archiveFile) async {
|
|
|
|
final DigestSink digestSink = DigestSink();
|
|
|
|
final ByteConversionSink sink = sha256.startChunkedConversion(digestSink);
|
|
|
|
|
|
|
|
final Stream<List<int>> stream = archiveFile.openRead();
|
|
|
|
await stream.forEach((List<int> chunk) {
|
|
|
|
sink.add(chunk);
|
|
|
|
});
|
|
|
|
sink.close();
|
|
|
|
return digestSink.value.toString();
|
|
|
|
}
|
|
|
|
|
2018-02-06 23:32:19 +00:00
|
|
|
/// Publish the archive to Google Storage.
|
2018-10-04 16:44:23 +00:00
|
|
|
Future<void> publishArchive() async {
|
2018-02-06 23:32:19 +00:00
|
|
|
final String destGsPath = '$gsReleaseFolder/$destinationArchivePath';
|
|
|
|
await _cloudCopy(outputFile.absolute.path, destGsPath);
|
|
|
|
assert(tempDir.existsSync());
|
2018-03-10 02:58:41 +00:00
|
|
|
await _updateMetadata();
|
2018-02-06 23:32:19 +00:00
|
|
|
}
|
|
|
|
|
2019-02-26 20:02:01 +00:00
|
|
|
Future<Map<String, dynamic>> _addRelease(Map<String, dynamic> jsonData) async {
|
2018-03-22 20:14:31 +00:00
|
|
|
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;
|
2018-09-12 06:29:29 +00:00
|
|
|
newEntry['release_date'] = DateTime.now().toUtc().toIso8601String();
|
2018-03-22 20:14:31 +00:00
|
|
|
newEntry['archive'] = destinationArchivePath;
|
2019-02-26 20:02:01 +00:00
|
|
|
newEntry['sha256'] = await _getChecksum(outputFile);
|
2018-03-22 20:14:31 +00:00
|
|
|
|
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
|
2018-10-04 16:44:23 +00:00
|
|
|
Future<void> _updateMetadata() async {
|
2018-03-20 15:14:08 +00:00
|
|
|
// 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.
|
2018-09-12 06:29:29 +00:00
|
|
|
final File metadataFile = File(
|
2018-03-20 15:14:08 +00:00
|
|
|
path.join(tempDir.absolute.path, getMetadataFilename(platform)),
|
|
|
|
);
|
|
|
|
await _runGsUtil(<String>['cp', metadataGsPath, metadataFile.absolute.path]);
|
|
|
|
final String currentMetadata = metadataFile.readAsStringSync();
|
2018-02-06 23:32:19 +00:00
|
|
|
if (currentMetadata.isEmpty) {
|
2019-03-01 22:38:40 +00:00
|
|
|
throw PreparePackageException('Empty metadata received from server');
|
2018-01-10 21:37:36 +00:00
|
|
|
}
|
|
|
|
|
2018-02-06 23:32:19 +00:00
|
|
|
Map<String, dynamic> jsonData;
|
2018-01-10 21:37:36 +00:00
|
|
|
try {
|
2018-02-06 23:32:19 +00:00
|
|
|
jsonData = json.decode(currentMetadata);
|
|
|
|
} on FormatException catch (e) {
|
2019-03-01 22:38:40 +00:00
|
|
|
throw PreparePackageException('Unable to parse JSON metadata received from cloud: $e');
|
2018-01-10 21:37:36 +00:00
|
|
|
}
|
|
|
|
|
2019-02-26 20:02:01 +00:00
|
|
|
jsonData = await _addRelease(jsonData);
|
2018-02-06 23:32:19 +00:00
|
|
|
|
2018-08-02 10:02:32 +00:00
|
|
|
const JsonEncoder encoder = JsonEncoder.withIndent(' ');
|
2018-03-20 15:14:08 +00:00
|
|
|
metadataFile.writeAsStringSync(encoder.convert(jsonData));
|
|
|
|
await _cloudCopy(metadataFile.absolute.path, metadataGsPath);
|
2018-02-06 23:32:19 +00:00
|
|
|
}
|
|
|
|
|
2018-03-20 15:14:08 +00:00
|
|
|
Future<String> _runGsUtil(
|
|
|
|
List<String> args, {
|
|
|
|
Directory workingDirectory,
|
2018-06-05 06:50:40 +00:00
|
|
|
bool failOk = false,
|
2018-03-20 15:14:08 +00:00
|
|
|
}) async {
|
2019-01-26 02:43:20 +00:00
|
|
|
if (platform.isWindows) {
|
|
|
|
return _processRunner.runProcess(
|
2019-06-27 19:23:16 +00:00
|
|
|
<String>['python', path.join(platform.environment['DEPOT_TOOLS'], 'gsutil.py'), '--', ...args],
|
2019-01-26 02:43:20 +00:00
|
|
|
workingDirectory: workingDirectory,
|
|
|
|
failOk: failOk,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2018-02-06 23:32:19 +00:00
|
|
|
return _processRunner.runProcess(
|
2019-06-27 19:23:16 +00:00
|
|
|
<String>['gsutil.py', '--', ...args],
|
2018-02-06 23:32:19 +00:00
|
|
|
workingDirectory: workingDirectory,
|
|
|
|
failOk: failOk,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<String> _cloudCopy(String src, String dest) async {
|
2018-02-09 23:42:51 +00:00
|
|
|
// We often don't have permission to overwrite, but
|
|
|
|
// we have permission to remove, so that's what we do.
|
2018-02-06 23:32:19 +00:00
|
|
|
await _runGsUtil(<String>['rm', dest], failOk: true);
|
2018-02-09 23:42:51 +00:00
|
|
|
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';
|
|
|
|
}
|
2019-07-02 19:11:56 +00:00
|
|
|
return await _runGsUtil(<String>[
|
|
|
|
// Use our preferred MIME type for the files we care about
|
|
|
|
// and let gsutil figure it out for anything else.
|
|
|
|
if (mimeType != null) ...<String>['-h', 'Content-Type:$mimeType'],
|
|
|
|
'cp',
|
|
|
|
src,
|
|
|
|
dest,
|
|
|
|
]);
|
2017-12-15 23:01:30 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// 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.
|
2018-01-10 21:37:36 +00:00
|
|
|
///
|
2018-09-22 09:02:56 +00:00
|
|
|
/// Archives contain the executables and customizations for the platform that
|
|
|
|
/// they are created on.
|
2018-10-30 02:44:00 +00:00
|
|
|
Future<void> main(List<String> rawArguments) async {
|
2018-09-12 06:29:29 +00:00
|
|
|
final ArgParser argParser = ArgParser();
|
2017-12-15 23:01:30 +00:00
|
|
|
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, '
|
2018-02-06 23:32:19 +00:00
|
|
|
'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.',
|
2017-12-15 23:01:30 +00:00
|
|
|
);
|
2018-02-06 23:32:19 +00:00
|
|
|
argParser.addOption('revision',
|
|
|
|
defaultsTo: null,
|
|
|
|
help: 'The Flutter git repo revision to build the '
|
|
|
|
'archive with. Must be the full 40-character hash. Required.');
|
2017-12-15 23:01:30 +00:00
|
|
|
argParser.addOption(
|
2018-02-06 23:32:19 +00:00
|
|
|
'branch',
|
|
|
|
defaultsTo: null,
|
2018-10-01 19:29:08 +00:00
|
|
|
allowed: Branch.values.map<String>((Branch branch) => getBranchName(branch)),
|
2018-02-06 23:32:19 +00:00
|
|
|
help: 'The Flutter branch to build the archive with. Required.',
|
2017-12-15 23:01:30 +00:00
|
|
|
);
|
|
|
|
argParser.addOption(
|
|
|
|
'output',
|
|
|
|
defaultsTo: null,
|
2018-02-06 23:32:19 +00:00
|
|
|
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,
|
2018-02-07 20:21:14 +00:00
|
|
|
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.',
|
2017-12-15 23:01:30 +00:00
|
|
|
);
|
2018-02-06 23:32:19 +00:00
|
|
|
|
2018-10-30 02:44:00 +00:00
|
|
|
final ArgResults parsedArguments = argParser.parse(rawArguments);
|
2017-12-15 23:01:30 +00:00
|
|
|
|
2018-10-30 02:44:00 +00:00
|
|
|
if (parsedArguments['help']) {
|
2018-02-07 20:21:14 +00:00
|
|
|
print(argParser.usage);
|
|
|
|
exit(0);
|
|
|
|
}
|
|
|
|
|
2017-12-15 23:01:30 +00:00
|
|
|
void errorExit(String message, {int exitCode = -1}) {
|
|
|
|
stderr.write('Error: $message\n\n');
|
|
|
|
stderr.write('${argParser.usage}\n');
|
|
|
|
exit(exitCode);
|
|
|
|
}
|
|
|
|
|
2018-10-30 02:44:00 +00:00
|
|
|
final String revision = parsedArguments['revision'];
|
2018-02-06 23:32:19 +00:00
|
|
|
if (revision.isEmpty) {
|
2017-12-15 23:01:30 +00:00
|
|
|
errorExit('Invalid argument: --revision must be specified.');
|
|
|
|
}
|
2018-02-06 23:32:19 +00:00
|
|
|
if (revision.length != 40) {
|
|
|
|
errorExit('Invalid argument: --revision must be the entire hash, not just a prefix.');
|
|
|
|
}
|
|
|
|
|
2018-10-30 02:44:00 +00:00
|
|
|
if (parsedArguments['branch'].isEmpty) {
|
2018-02-06 23:32:19 +00:00
|
|
|
errorExit('Invalid argument: --branch must be specified.');
|
|
|
|
}
|
2017-12-15 23:01:30 +00:00
|
|
|
|
2018-01-10 21:37:36 +00:00
|
|
|
Directory tempDir;
|
2017-12-15 23:01:30 +00:00
|
|
|
bool removeTempDir = false;
|
2018-10-30 02:44:00 +00:00
|
|
|
if (parsedArguments['temp_dir'] == null || parsedArguments['temp_dir'].isEmpty) {
|
2018-08-17 20:17:23 +00:00
|
|
|
tempDir = Directory.systemTemp.createTempSync('flutter_package.');
|
2017-12-15 23:01:30 +00:00
|
|
|
removeTempDir = true;
|
|
|
|
} else {
|
2018-10-30 02:44:00 +00:00
|
|
|
tempDir = Directory(parsedArguments['temp_dir']);
|
2018-01-10 21:37:36 +00:00
|
|
|
if (!tempDir.existsSync()) {
|
2018-10-30 02:44:00 +00:00
|
|
|
errorExit("Temporary directory ${parsedArguments['temp_dir']} doesn't exist.");
|
2017-12-15 23:01:30 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-02-06 23:32:19 +00:00
|
|
|
Directory outputDir;
|
2018-10-30 02:44:00 +00:00
|
|
|
if (parsedArguments['output'] == null) {
|
2018-02-06 23:32:19 +00:00
|
|
|
outputDir = tempDir;
|
2018-01-10 21:37:36 +00:00
|
|
|
} else {
|
2018-10-30 02:44:00 +00:00
|
|
|
outputDir = Directory(parsedArguments['output']);
|
2018-02-06 23:32:19 +00:00
|
|
|
if (!outputDir.existsSync()) {
|
|
|
|
outputDir.createSync(recursive: true);
|
2018-01-10 21:37:36 +00:00
|
|
|
}
|
2017-12-15 23:01:30 +00:00
|
|
|
}
|
|
|
|
|
2018-10-30 02:44:00 +00:00
|
|
|
final Branch branch = fromBranchName(parsedArguments['branch']);
|
2019-03-01 22:38:40 +00:00
|
|
|
final ArchiveCreator creator = ArchiveCreator(tempDir, outputDir, revision, branch, strict: parsedArguments['publish']);
|
2017-12-15 23:01:30 +00:00
|
|
|
int exitCode = 0;
|
|
|
|
String message;
|
|
|
|
try {
|
2018-02-06 23:32:19 +00:00
|
|
|
final String version = await creator.initializeRepo();
|
|
|
|
final File outputFile = await creator.createArchive();
|
2018-10-30 02:44:00 +00:00
|
|
|
if (parsedArguments['publish']) {
|
2018-09-12 06:29:29 +00:00
|
|
|
final ArchivePublisher publisher = ArchivePublisher(
|
2018-02-06 23:32:19 +00:00
|
|
|
tempDir,
|
|
|
|
revision,
|
|
|
|
branch,
|
|
|
|
version,
|
|
|
|
outputFile,
|
|
|
|
);
|
|
|
|
await publisher.publishArchive();
|
|
|
|
}
|
2019-03-01 22:38:40 +00:00
|
|
|
} on PreparePackageException catch (e) {
|
2017-12-15 23:01:30 +00:00
|
|
|
exitCode = e.exitCode;
|
|
|
|
message = e.message;
|
2018-03-10 02:58:41 +00:00
|
|
|
} catch (e) {
|
|
|
|
exitCode = -1;
|
|
|
|
message = e.toString();
|
2017-12-15 23:01:30 +00:00
|
|
|
} finally {
|
|
|
|
if (removeTempDir) {
|
2018-01-10 21:37:36 +00:00
|
|
|
tempDir.deleteSync(recursive: true);
|
2017-12-15 23:01:30 +00:00
|
|
|
}
|
|
|
|
if (exitCode != 0) {
|
|
|
|
errorExit(message, exitCode: exitCode);
|
|
|
|
}
|
|
|
|
exit(0);
|
|
|
|
}
|
|
|
|
}
|