// 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 arguments, { String workingDirectory, Map 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.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 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(['clone', '-b', 'master', CHROMIUM_REPO], workingDirectory: tempDir); runGit(['reset', '--hard', revision]); // Make the origin point to github instead of the chromium mirror. runGit(['remote', 'remove', 'origin']); runGit(['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(['doctor']); runFlutter(['update-packages']); runFlutter(['precache']); runFlutter(['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 ['app', 'package', 'plugin']) { final String createName = path.join(tempDir.path, 'create_$template'); runFlutter( ['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(['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 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 args) { return _runProcess(flutter, args); } String runGit(List args, {Directory workingDirectory}) { return _runProcess(git, args, workingDirectory: workingDirectory); } void createZipArchive(File output, Directory source) { final List args = []; if (Platform.isWindows) { // We use 7-Zip on Windows, which has different args. args.addAll(['a', '-tzip', '-mx=9']); } else { args.addAll(['-r', '-9', '-q']); } args.addAll([ 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 args = [ '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 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); } }