| // 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:convert'; |
| import 'dart:io'; |
| |
| import 'package:args/args.dart'; |
| import 'package:path/path.dart' as path; |
| |
| const String CHROMIUM_REPO = |
| 'https://chromium.googlesource.com/external/github.com/flutter/flutter'; |
| const String GITHUB_REPO = 'https://github.com/flutter/flutter.git'; |
| |
| /// The type of the process runner function. This allows us to |
| /// inject a fake process runner into the ArchiveCreator for tests. |
| typedef ProcessResult ProcessRunner( |
| String executable, |
| List<String> arguments, { |
| String workingDirectory, |
| Map<String, String> environment, |
| bool includeParentEnvironment, |
| bool runInShell, |
| Encoding stdoutEncoding, |
| Encoding stderrEncoding, |
| }); |
| |
| /// 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. Will place |
| /// several GiB of data there, so it should have available space. |
| /// [outputFile] is the name of the output archive. It should end in either |
| /// ".tar.xz" or ".zip". |
| /// The runner argument is used to inject a mock of [Process.runSync] for |
| /// testing purposes. |
| ArchiveCreator(this.tempDir, this.outputFile, {ProcessRunner runner}) |
| : assert(outputFile.path.toLowerCase().endsWith('.zip') || |
| outputFile.path.toLowerCase().endsWith('.tar.xz')), |
| flutterRoot = new Directory(path.join(tempDir.path, 'flutter')), |
| _runner = runner ?? Process.runSync { |
| flutter = path.join( |
| flutterRoot.absolute.path, |
| 'bin', |
| Platform.isWindows ? 'flutter.bat' : '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 File outputFile; |
| final ProcessRunner _runner; |
| String flutter; |
| final String git = Platform.isWindows ? 'git.bat' : 'git'; |
| final String zip = Platform.isWindows ? '7za.exe' : 'zip'; |
| final String tar = Platform.isWindows ? 'tar.exe' : 'tar'; |
| Map<String, String> environment; |
| |
| /// Clone the Flutter repo and make sure that the git environment is sane |
| /// for when the user will unpack it. |
| void checkoutFlutter(String revision) { |
| // 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. |
| runGit(<String>['clone', '-b', 'master', CHROMIUM_REPO], workingDirectory: tempDir); |
| runGit(<String>['reset', '--hard', revision]); |
| |
| // Make the origin point to github instead of the chromium mirror. |
| runGit(<String>['remote', 'remove', 'origin']); |
| runGit(<String>['remote', 'add', 'origin', GITHUB_REPO]); |
| } |
| |
| /// Prepare the archive repo so that it has all of the caches warmed up and |
| /// is configured for the user to being working. |
| void prepareArchive() { |
| runFlutter(<String>['doctor']); |
| runFlutter(<String>['update-packages']); |
| runFlutter(<String>['precache']); |
| 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'); |
| 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. |
| runGit(<String>['clean', '-f', '-X', '**/.packages']); |
| } |
| |
| /// Create the archive into the given output file. |
| void createArchive() { |
| if (outputFile.path.toLowerCase().endsWith('.zip')) { |
| createZipArchive(outputFile, flutterRoot); |
| } else if (outputFile.path.toLowerCase().endsWith('.tar.xz')) { |
| createTarArchive(outputFile, flutterRoot); |
| } |
| } |
| |
| String _runProcess(String executable, List<String> args, {Directory workingDirectory}) { |
| workingDirectory ??= flutterRoot; |
| stderr.write('Running "$executable ${args.join(' ')}" in ${workingDirectory.path}.\n'); |
| ProcessResult result; |
| try { |
| result = _runner( |
| executable, |
| args, |
| workingDirectory: workingDirectory.absolute.path, |
| environment: environment, |
| includeParentEnvironment: false, |
| ); |
| } on ProcessException catch (e) { |
| final String message = 'Running "$executable ${args.join(' ')}" in ${workingDirectory.path} ' |
| 'failed with:\n${e.toString()}\n PATH: ${environment['PATH']}'; |
| throw new ProcessFailedException(message, -1); |
| } catch (e) { |
| rethrow; |
| } |
| stdout.write(result.stdout); |
| stderr.write(result.stderr); |
| if (result.exitCode != 0) { |
| final String message = 'Running "$executable ${args.join(' ')}" in ${workingDirectory.path} ' |
| 'failed with ${result.exitCode}.'; |
| throw new ProcessFailedException(message, result.exitCode); |
| } |
| return result.stdout.trim(); |
| } |
| |
| String runFlutter(List<String> args) { |
| return _runProcess(flutter, args); |
| } |
| |
| String runGit(List<String> args, {Directory workingDirectory}) { |
| return _runProcess(git, args, workingDirectory: workingDirectory); |
| } |
| |
| void createZipArchive(File output, Directory source) { |
| final List<String> args = <String>[]; |
| if (Platform.isWindows) { |
| // We use 7-Zip on Windows, which has different args. |
| args.addAll(<String>['a', '-tzip', '-mx=9']); |
| } else { |
| args.addAll(<String>['-r', '-9', '-q']); |
| } |
| args.addAll(<String>[ |
| output.absolute.path, |
| path.basename(source.absolute.path), |
| ]); |
| |
| _runProcess(zip, args, |
| workingDirectory: new Directory(path.dirname(source.absolute.path))); |
| } |
| |
| void createTarArchive(File output, Directory source) { |
| final List<String> args = <String>[ |
| 'cJf', |
| output.absolute.path, |
| path.basename(source.absolute.path), |
| ]; |
| _runProcess(tar, args, |
| workingDirectory: new Directory(path.dirname(source.absolute.path))); |
| } |
| } |
| |
| /// 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. |
| void main(List<String> argList) { |
| 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 where the output archive should be written. ' |
| 'The suffix determines the output format: .tar.xz or .zip are the ' |
| 'only formats supported.', |
| ); |
| 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 tmpDir; |
| bool removeTempDir = false; |
| if (args['temp_dir'] == null || args['temp_dir'].isEmpty) { |
| tmpDir = Directory.systemTemp.createTempSync('flutter_'); |
| removeTempDir = true; |
| } else { |
| tmpDir = new Directory(args['temp_dir']); |
| if (!tmpDir.existsSync()) { |
| errorExit("Temporary directory ${args['temp_dir']} doesn't exist."); |
| } |
| } |
| |
| String outputFileString = args['output']; |
| if (outputFileString == null || outputFileString.isEmpty) { |
| final String suffix = Platform.isWindows ? '.zip' : '.tar.xz'; |
| outputFileString = path.join(tmpDir.path, 'flutter_${args['revision']}$suffix'); |
| } else if (!outputFileString.toLowerCase().endsWith('.zip') && |
| !outputFileString.toLowerCase().endsWith('.tar.xz')) { |
| errorExit('Output file has unsupported suffix. It should be either ".zip" or ".tar.xz".'); |
| } |
| |
| final File outputFile = new File(outputFileString); |
| if (outputFile.existsSync()) { |
| errorExit('Output file ${outputFile.absolute.path} already exists.'); |
| } |
| |
| final ArchiveCreator preparer = new ArchiveCreator(tmpDir, outputFile); |
| int exitCode = 0; |
| String message; |
| try { |
| preparer.checkoutFlutter(args['revision']); |
| preparer.prepareArchive(); |
| preparer.createArchive(); |
| } on ProcessFailedException catch (e) { |
| exitCode = e.exitCode; |
| message = e.message; |
| } catch (e) { |
| rethrow; |
| } finally { |
| if (removeTempDir) { |
| tmpDir.deleteSync(recursive: true); |
| } |
| if (exitCode != 0) { |
| errorExit(message, exitCode: exitCode); |
| } |
| exit(0); |
| } |
| } |