| // 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. |
| |
| import 'dart:async'; |
| import 'dart:convert'; |
| import 'dart:io'; |
| import 'dart:typed_data'; |
| |
| import 'package:args/args.dart'; |
| import 'package:http/http.dart' as http; |
| import 'package:path/path.dart' as path; |
| import 'package:process/process.dart'; |
| |
| const String CHROMIUM_REPO = |
| 'https://chromium.googlesource.com/external/github.com/flutter/flutter'; |
| const String GITHUB_REPO = 'https://github.com/flutter/flutter.git'; |
| const String MINGIT_FOR_WINDOWS_URL = 'https://storage.googleapis.com/flutter_infra/mingit/' |
| '603511c649b00bbef0a6122a827ac419b656bc19/mingit.zip'; |
| |
| /// Error class for when a process fails to run, so we can catch |
| /// it and provide something more readable than a stack trace. |
| class ProcessFailedException extends Error { |
| ProcessFailedException([this.message, this.exitCode]); |
| |
| String message = ''; |
| int exitCode = 0; |
| |
| @override |
| String toString() => message; |
| } |
| |
| /// Creates a pre-populated Flutter archive from a git repo. |
| class ArchiveCreator { |
| /// [_tempDir] is the directory to use for creating the archive. The script |
| /// will place several GiB of data there, so it should have available space. |
| /// |
| /// The processManager argument is used to inject a mock of [ProcessManager] for |
| /// testing purposes. |
| /// |
| /// If subprocessOutput is true, then output from processes invoked during |
| /// archive creation is echoed to stderr and stdout. |
| ArchiveCreator(this._tempDir, {ProcessManager processManager, bool subprocessOutput: true}) |
| : _flutterRoot = new Directory(path.join(_tempDir.path, 'flutter')), |
| _processManager = processManager ?? const LocalProcessManager(), |
| _subprocessOutput = subprocessOutput { |
| _flutter = path.join( |
| _flutterRoot.absolute.path, |
| 'bin', |
| 'flutter', |
| ); |
| _environment = new Map<String, String>.from(Platform.environment); |
| _environment['PUB_CACHE'] = path.join(_flutterRoot.absolute.path, '.pub-cache'); |
| } |
| |
| final Directory _flutterRoot; |
| final Directory _tempDir; |
| final bool _subprocessOutput; |
| final ProcessManager _processManager; |
| String _flutter; |
| final Uri _minGitUri = Uri.parse(MINGIT_FOR_WINDOWS_URL); |
| Map<String, String> _environment; |
| |
| /// Returns a default archive name when given a Git revision. |
| /// Used when an output filename is not given. |
| static String defaultArchiveName(String revision) { |
| final String os = Platform.operatingSystem.toLowerCase(); |
| final String id = revision.length > 10 ? revision.substring(0, 10) : revision; |
| final String suffix = Platform.isWindows ? 'zip' : 'tar.xz'; |
| return 'flutter_${os}_$id.$suffix'; |
| } |
| |
| /// Performs all of the steps needed to create an archive. |
| Future<File> createArchive(String revision, File outputFile) async { |
| await _checkoutFlutter(revision); |
| await _installMinGitIfNeeded(); |
| await _populateCaches(); |
| await _archiveFiles(outputFile); |
| return outputFile; |
| } |
| |
| /// Clone the Flutter repo and make sure that the git environment is sane |
| /// for when the user will unpack it. |
| Future<Null> _checkoutFlutter(String revision) async { |
| // We want the user to start out the in the 'master' branch instead of a |
| // detached head. To do that, we need to make sure master points at the |
| // desired revision. |
| await _runGit(<String>['clone', '-b', 'master', CHROMIUM_REPO], workingDirectory: _tempDir); |
| await _runGit(<String>['reset', '--hard', revision]); |
| |
| // Make the origin point to github instead of the chromium mirror. |
| await _runGit(<String>['remote', 'remove', 'origin']); |
| await _runGit(<String>['remote', 'add', 'origin', GITHUB_REPO]); |
| } |
| |
| /// Retrieve the MinGit executable from storage and unpack it. |
| Future<Null> _installMinGitIfNeeded() async { |
| if (!Platform.isWindows) { |
| return; |
| } |
| final Uint8List data = await http.readBytes(_minGitUri); |
| final File gitFile = new File(path.join(_tempDir.path, 'mingit.zip')); |
| await gitFile.writeAsBytes(data, flush: true); |
| |
| final Directory minGitPath = new Directory(path.join(_flutterRoot.path, 'bin', 'mingit')); |
| await minGitPath.create(recursive: true); |
| await _unzipArchive(gitFile, currentDirectory: minGitPath); |
| } |
| |
| /// Prepare the archive repo so that it has all of the caches warmed up and |
| /// is configured for the user to begin working. |
| Future<Null> _populateCaches() async { |
| 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 |
| // themselves when created, and this will warm the cache with their |
| // dependencies too. |
| for (String template in <String>['app', 'package', 'plugin']) { |
| final String createName = path.join(_tempDir.path, 'create_$template'); |
| await _runFlutter( |
| <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. |
| await _runGit(<String>['clean', '-f', '-X', '**/.packages']); |
| } |
| |
| /// Write the archive to the given output file. |
| Future<Null> _archiveFiles(File outputFile) async { |
| if (outputFile.path.toLowerCase().endsWith('.zip')) { |
| await _createZipArchive(outputFile, _flutterRoot); |
| } else if (outputFile.path.toLowerCase().endsWith('.tar.xz')) { |
| await _createTarArchive(outputFile, _flutterRoot); |
| } |
| } |
| |
| Future<String> _runFlutter(List<String> args) => _runProcess(<String>[_flutter]..addAll(args)); |
| |
| Future<String> _runGit(List<String> args, {Directory workingDirectory}) { |
| return _runProcess(<String>['git']..addAll(args), workingDirectory: workingDirectory); |
| } |
| |
| /// Unpacks the given zip file into the currentDirectory (if set), or the |
| /// same directory as the archive. |
| /// |
| /// May only be run on Windows (since 7Zip is not available on other platforms). |
| Future<String> _unzipArchive(File archive, {Directory currentDirectory}) { |
| assert(Platform.isWindows); // 7Zip is only available on Windows. |
| currentDirectory ??= new Directory(path.dirname(archive.absolute.path)); |
| final List<String> commandLine = <String>['7za', 'x', archive.absolute.path]; |
| return _runProcess(commandLine, workingDirectory: currentDirectory); |
| } |
| |
| /// Create a zip archive from the directory source. |
| /// |
| /// May only be run on Windows (since 7Zip is not available on other platforms). |
| Future<String> _createZipArchive(File output, Directory source) { |
| assert(Platform.isWindows); // 7Zip is only available on Windows. |
| final List<String> commandLine = <String>[ |
| '7za', |
| 'a', |
| '-tzip', |
| '-mx=9', |
| output.absolute.path, |
| path.basename(source.absolute.path), |
| ]; |
| return _runProcess(commandLine, |
| workingDirectory: new Directory(path.dirname(source.absolute.path))); |
| } |
| |
| /// Create a tar archive from the directory source. |
| Future<String> _createTarArchive(File output, Directory source) { |
| return _runProcess(<String>[ |
| 'tar', |
| 'cJf', |
| output.absolute.path, |
| path.basename(source.absolute.path), |
| ], workingDirectory: new Directory(path.dirname(source.absolute.path))); |
| } |
| |
| /// Run the command and arguments in commandLine as a sub-process from |
| /// workingDirectory if set, or the current directory if not. |
| Future<String> _runProcess(List<String> commandLine, {Directory workingDirectory}) async { |
| workingDirectory ??= _flutterRoot; |
| if (_subprocessOutput) { |
| stderr.write('Running "${commandLine.join(' ')}" in ${workingDirectory.path}.\n'); |
| } |
| final List<int> output = <int>[]; |
| final Completer<Null> stdoutComplete = new Completer<Null>(); |
| final Completer<Null> stderrComplete = new Completer<Null>(); |
| 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()}'; |
| throw new ProcessFailedException(message, -1); |
| } |
| |
| final int exitCode = await allComplete(); |
| if (exitCode != 0) { |
| final String message = 'Running "${commandLine.join(' ')}" in ${workingDirectory.path} ' |
| 'failed with $exitCode.'; |
| throw new ProcessFailedException(message, exitCode); |
| } |
| return UTF8.decoder.convert(output).trim(); |
| } |
| } |
| |
| /// 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. |
| /// |
| /// Note that archives contain the executables and customizations for the |
| /// platform that they are created on. |
| Future<Null> main(List<String> argList) async { |
| final ArgParser argParser = new ArgParser(); |
| 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, ' |
| 'so it should have sufficient free space.', |
| ); |
| argParser.addOption( |
| 'revision', |
| defaultsTo: 'master', |
| help: 'The Flutter revision to build the archive with. Defaults to the ' |
| "master branch's HEAD revision.", |
| ); |
| argParser.addOption( |
| 'output', |
| defaultsTo: null, |
| help: 'The path to the file where the output archive should be ' |
| 'written. The output file must end in ".tar.xz" on Linux and Mac, ' |
| 'and ".zip" on Windows. 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.', |
| ); |
| final ArgResults args = argParser.parse(argList); |
| |
| void errorExit(String message, {int exitCode = -1}) { |
| stderr.write('Error: $message\n\n'); |
| stderr.write('${argParser.usage}\n'); |
| exit(exitCode); |
| } |
| |
| if (args['revision'].isEmpty) { |
| errorExit('Invalid argument: --revision must be specified.'); |
| } |
| |
| Directory tempDir; |
| bool removeTempDir = false; |
| if (args['temp_dir'] == null || args['temp_dir'].isEmpty) { |
| tempDir = Directory.systemTemp.createTempSync('flutter_'); |
| removeTempDir = true; |
| } else { |
| tempDir = new Directory(args['temp_dir']); |
| if (!tempDir.existsSync()) { |
| errorExit("Temporary directory ${args['temp_dir']} doesn't exist."); |
| } |
| } |
| |
| final String output = (args['output'] == null || args['output'].isEmpty) |
| ? path.join(path.current, ArchiveCreator.defaultArchiveName(args['revision'])) |
| : args['output']; |
| |
| /// Sanity check the output filename. |
| final String outputFilename = path.basename(output); |
| if (Platform.isWindows) { |
| if (!outputFilename.endsWith('.zip')) { |
| errorExit('The argument to --output must end in .zip on Windows.'); |
| } |
| } else { |
| if (!outputFilename.endsWith('.tar.xz')) { |
| errorExit('The argument to --output must end in .tar.xz on Linux and Mac.'); |
| } |
| } |
| |
| final Directory outputDirectory = new Directory(path.dirname(output)); |
| if (!outputDirectory.existsSync()) { |
| outputDirectory.createSync(recursive: true); |
| } |
| final File outputFile = new File(path.join(outputDirectory.absolute.path, outputFilename)); |
| |
| final ArchiveCreator preparer = new ArchiveCreator(tempDir); |
| int exitCode = 0; |
| String message; |
| try { |
| await preparer.createArchive(args['revision'], outputFile); |
| } on ProcessFailedException catch (e) { |
| exitCode = e.exitCode; |
| message = e.message; |
| } finally { |
| if (removeTempDir) { |
| tempDir.deleteSync(recursive: true); |
| } |
| if (exitCode != 0) { |
| errorExit(message, exitCode: exitCode); |
| } |
| exit(0); |
| } |
| } |