mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00

* Revert "Reverting package changes until I can figure out how to fix Windows. (#14007)"
This reverts commit 6fda8ee821
.
* Make prepare_package run on Windows
340 lines
12 KiB
Dart
340 lines
12 KiB
Dart
// 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);
|
|
}
|
|
}
|