blob: be2a5b97950a09f4aa28b22b5ea0fdd6c714b536 [file] [log] [blame]
Greg Spencerf00c9022017-12-15 15:01:30 -08001// Copyright 2017 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
Michael Goderbauer9e51a602018-01-10 13:37:36 -08005import 'dart:async';
Greg Spencerf00c9022017-12-15 15:01:30 -08006import 'dart:convert';
Greg Spencer8a2df392018-02-06 15:32:19 -08007import 'dart:io' hide Platform;
Michael Goderbauer9e51a602018-01-10 13:37:36 -08008import 'dart:typed_data';
Greg Spencerf00c9022017-12-15 15:01:30 -08009
10import 'package:args/args.dart';
Michael Goderbauer9e51a602018-01-10 13:37:36 -080011import 'package:http/http.dart' as http;
Greg Spencerf00c9022017-12-15 15:01:30 -080012import 'package:path/path.dart' as path;
Michael Goderbauer9e51a602018-01-10 13:37:36 -080013import 'package:process/process.dart';
Greg Spencer8a2df392018-02-06 15:32:19 -080014import 'package:platform/platform.dart' show Platform, LocalPlatform;
Greg Spencerf00c9022017-12-15 15:01:30 -080015
Greg Spencer97e77932018-02-09 15:42:51 -080016const String chromiumRepo = 'https://chromium.googlesource.com/external/github.com/flutter/flutter';
Greg Spencerdf791272018-02-07 12:21:14 -080017const String githubRepo = 'https://github.com/flutter/flutter.git';
18const String mingitForWindowsUrl = 'https://storage.googleapis.com/flutter_infra/mingit/'
Michael Goderbauer9e51a602018-01-10 13:37:36 -080019 '603511c649b00bbef0a6122a827ac419b656bc19/mingit.zip';
Greg Spencerdf791272018-02-07 12:21:14 -080020const String gsBase = 'gs://flutter_infra';
21const String releaseFolder = '/releases';
22const String gsReleaseFolder = '$gsBase$releaseFolder';
23const String baseUrl = 'https://storage.googleapis.com/flutter_infra';
Greg Spencerf00c9022017-12-15 15:01:30 -080024
Greg Spencer8a2df392018-02-06 15:32:19 -080025/// Exception class for when a process fails to run, so we can catch
Greg Spencerf00c9022017-12-15 15:01:30 -080026/// it and provide something more readable than a stack trace.
Greg Spencer8a2df392018-02-06 15:32:19 -080027class ProcessRunnerException implements Exception {
28 ProcessRunnerException(this.message, [this.result]);
Greg Spencerf00c9022017-12-15 15:01:30 -080029
Greg Spencer8a2df392018-02-06 15:32:19 -080030 final String message;
31 final ProcessResult result;
Greg Spencer984a24c2018-03-09 18:58:41 -080032 int get exitCode => result?.exitCode ?? -1;
Greg Spencerf00c9022017-12-15 15:01:30 -080033
34 @override
Greg Spencer8a2df392018-02-06 15:32:19 -080035 String toString() {
36 String output = runtimeType.toString();
37 if (message != null) {
38 output += ': $message';
39 }
40 final String stderr = result?.stderr ?? '';
41 if (stderr.isNotEmpty) {
Greg Spencer984a24c2018-03-09 18:58:41 -080042 output += ':\n$stderr';
Greg Spencer8a2df392018-02-06 15:32:19 -080043 }
44 return output;
45 }
46}
47
48enum Branch { dev, beta, release }
49
50String getBranchName(Branch branch) {
51 switch (branch) {
52 case Branch.beta:
53 return 'beta';
54 case Branch.dev:
55 return 'dev';
56 case Branch.release:
57 return 'release';
58 }
59 return null;
60}
61
62Branch fromBranchName(String name) {
63 switch (name) {
64 case 'beta':
65 return Branch.beta;
66 case 'dev':
67 return Branch.dev;
68 case 'release':
69 return Branch.release;
70 default:
71 throw new ArgumentError('Invalid branch name.');
72 }
73}
74
75/// A helper class for classes that want to run a process, optionally have the
76/// stderr and stdout reported as the process runs, and capture the stdout
77/// properly without dropping any.
78class ProcessRunner {
79 ProcessRunner({
Greg Spencerdf791272018-02-07 12:21:14 -080080 ProcessManager processManager,
Greg Spencer8a2df392018-02-06 15:32:19 -080081 this.subprocessOutput: true,
82 this.defaultWorkingDirectory,
83 this.platform: const LocalPlatform(),
Greg Spencerdf791272018-02-07 12:21:14 -080084 }) : processManager = processManager ?? const LocalProcessManager() {
Greg Spencer8a2df392018-02-06 15:32:19 -080085 environment = new Map<String, String>.from(platform.environment);
86 }
87
88 /// The platform to use for a starting environment.
89 final Platform platform;
90
91 /// Set [subprocessOutput] to show output as processes run. Stdout from the
92 /// process will be printed to stdout, and stderr printed to stderr.
93 final bool subprocessOutput;
94
95 /// Set the [processManager] in order to inject a test instance to perform
96 /// testing.
97 final ProcessManager processManager;
98
99 /// Sets the default directory used when `workingDirectory` is not specified
100 /// to [runProcess].
101 final Directory defaultWorkingDirectory;
102
103 /// The environment to run processes with.
104 Map<String, String> environment;
105
106 /// Run the command and arguments in `commandLine` as a sub-process from
107 /// `workingDirectory` if set, or the [defaultWorkingDirectory] if not. Uses
108 /// [Directory.current] if [defaultWorkingDirectory] is not set.
109 ///
110 /// Set `failOk` if [runProcess] should not throw an exception when the
111 /// command completes with a a non-zero exit code.
112 Future<String> runProcess(
113 List<String> commandLine, {
114 Directory workingDirectory,
115 bool failOk: false,
116 }) async {
117 workingDirectory ??= defaultWorkingDirectory ?? Directory.current;
118 if (subprocessOutput) {
119 stderr.write('Running "${commandLine.join(' ')}" in ${workingDirectory.path}.\n');
120 }
121 final List<int> output = <int>[];
122 final Completer<Null> stdoutComplete = new Completer<Null>();
123 final Completer<Null> stderrComplete = new Completer<Null>();
124 Process process;
125 Future<int> allComplete() async {
126 await stderrComplete.future;
127 await stdoutComplete.future;
128 return process.exitCode;
129 }
130
131 try {
132 process = await processManager.start(
133 commandLine,
134 workingDirectory: workingDirectory.absolute.path,
135 environment: environment,
136 );
137 process.stdout.listen(
138 (List<int> event) {
139 output.addAll(event);
140 if (subprocessOutput) {
141 stdout.add(event);
142 }
143 },
144 onDone: () async => stdoutComplete.complete(),
145 );
146 if (subprocessOutput) {
147 process.stderr.listen(
148 (List<int> event) {
149 stderr.add(event);
150 },
151 onDone: () async => stderrComplete.complete(),
152 );
153 } else {
154 stderrComplete.complete();
155 }
156 } on ProcessException catch (e) {
157 final String message = 'Running "${commandLine.join(' ')}" in ${workingDirectory.path} '
158 'failed with:\n${e.toString()}';
159 throw new ProcessRunnerException(message);
Greg Spencer984a24c2018-03-09 18:58:41 -0800160 } on ArgumentError catch (e) {
161 final String message = 'Running "${commandLine.join(' ')}" in ${workingDirectory.path} '
162 'failed with:\n${e.toString()}';
163 throw new ProcessRunnerException(message);
Greg Spencer8a2df392018-02-06 15:32:19 -0800164 }
165
166 final int exitCode = await allComplete();
167 if (exitCode != 0 && !failOk) {
Greg Spencerd9e37152018-03-20 08:14:08 -0700168 final String message = 'Running "${commandLine.join(' ')}" in ${workingDirectory.path} failed';
Greg Spencer8a2df392018-02-06 15:32:19 -0800169 throw new ProcessRunnerException(
Greg Spencerd9e37152018-03-20 08:14:08 -0700170 message,
171 new ProcessResult(0, exitCode, null, 'returned $exitCode'),
172 );
Greg Spencer8a2df392018-02-06 15:32:19 -0800173 }
Alexander Aprelev2f8474f2018-03-12 15:44:25 -0700174 return utf8.decoder.convert(output).trim();
Greg Spencer8a2df392018-02-06 15:32:19 -0800175 }
Greg Spencerf00c9022017-12-15 15:01:30 -0800176}
177
Greg Spencerdf791272018-02-07 12:21:14 -0800178typedef Future<Uint8List> HttpReader(Uri url, {Map<String, String> headers});
179
Greg Spencerf00c9022017-12-15 15:01:30 -0800180/// Creates a pre-populated Flutter archive from a git repo.
181class ArchiveCreator {
Greg Spencer8a2df392018-02-06 15:32:19 -0800182 /// [tempDir] is the directory to use for creating the archive. The script
Michael Goderbauer9e51a602018-01-10 13:37:36 -0800183 /// will place several GiB of data there, so it should have available space.
184 ///
185 /// The processManager argument is used to inject a mock of [ProcessManager] for
Greg Spencerf00c9022017-12-15 15:01:30 -0800186 /// testing purposes.
Michael Goderbauer9e51a602018-01-10 13:37:36 -0800187 ///
188 /// If subprocessOutput is true, then output from processes invoked during
189 /// archive creation is echoed to stderr and stdout.
Greg Spencer8a2df392018-02-06 15:32:19 -0800190 ArchiveCreator(
191 this.tempDir,
192 this.outputDir,
193 this.revision,
194 this.branch, {
195 ProcessManager processManager,
196 bool subprocessOutput: true,
197 this.platform: const LocalPlatform(),
Greg Spencerdf791272018-02-07 12:21:14 -0800198 HttpReader httpReader,
Greg Spencer8a2df392018-02-06 15:32:19 -0800199 }) : assert(revision.length == 40),
200 flutterRoot = new Directory(path.join(tempDir.path, 'flutter')),
Greg Spencerdf791272018-02-07 12:21:14 -0800201 httpReader = httpReader ?? http.readBytes,
Greg Spencer8a2df392018-02-06 15:32:19 -0800202 _processRunner = new ProcessRunner(
203 processManager: processManager,
204 subprocessOutput: subprocessOutput,
205 platform: platform,
206 ) {
Michael Goderbauer9e51a602018-01-10 13:37:36 -0800207 _flutter = path.join(
Greg Spencer8a2df392018-02-06 15:32:19 -0800208 flutterRoot.absolute.path,
Greg Spencerf00c9022017-12-15 15:01:30 -0800209 'bin',
Michael Goderbauer9e51a602018-01-10 13:37:36 -0800210 'flutter',
Greg Spencerf00c9022017-12-15 15:01:30 -0800211 );
Greg Spencer8a2df392018-02-06 15:32:19 -0800212 _processRunner.environment['PUB_CACHE'] = path.join(flutterRoot.absolute.path, '.pub-cache');
Greg Spencerf00c9022017-12-15 15:01:30 -0800213 }
214
Greg Spencerdf791272018-02-07 12:21:14 -0800215 /// The platform to use for the environment and determining which
216 /// platform we're running on.
Greg Spencer8a2df392018-02-06 15:32:19 -0800217 final Platform platform;
Greg Spencerdf791272018-02-07 12:21:14 -0800218
219 /// The branch to build the archive for. The branch must contain [revision].
Greg Spencer8a2df392018-02-06 15:32:19 -0800220 final Branch branch;
Greg Spencerdf791272018-02-07 12:21:14 -0800221
222 /// The git revision hash to build the archive for. This revision has
223 /// to be available in the [branch], although it doesn't have to be
224 /// at HEAD, since we clone the branch and then reset to this revision
225 /// to create the archive.
Greg Spencer8a2df392018-02-06 15:32:19 -0800226 final String revision;
Greg Spencerdf791272018-02-07 12:21:14 -0800227
228 /// The flutter root directory in the [tempDir].
Greg Spencer8a2df392018-02-06 15:32:19 -0800229 final Directory flutterRoot;
Greg Spencerdf791272018-02-07 12:21:14 -0800230
231 /// The temporary directory used to build the archive in.
Greg Spencer8a2df392018-02-06 15:32:19 -0800232 final Directory tempDir;
Greg Spencerdf791272018-02-07 12:21:14 -0800233
234 /// The directory to write the output file to.
Greg Spencer8a2df392018-02-06 15:32:19 -0800235 final Directory outputDir;
Greg Spencerdf791272018-02-07 12:21:14 -0800236
237 final Uri _minGitUri = Uri.parse(mingitForWindowsUrl);
Greg Spencer8a2df392018-02-06 15:32:19 -0800238 final ProcessRunner _processRunner;
239
Greg Spencerdf791272018-02-07 12:21:14 -0800240 /// Used to tell the [ArchiveCreator] which function to use for reading
241 /// bytes from a URL. Used in tests to inject a fake reader. Defaults to
242 /// [http.readBytes].
243 final HttpReader httpReader;
244
Greg Spencer8a2df392018-02-06 15:32:19 -0800245 File _outputFile;
246 String _version;
247 String _flutter;
248
249 /// Get the name of the channel as a string.
250 String get branchName => getBranchName(branch);
Michael Goderbauer9e51a602018-01-10 13:37:36 -0800251
252 /// Returns a default archive name when given a Git revision.
253 /// Used when an output filename is not given.
Greg Spencer8a2df392018-02-06 15:32:19 -0800254 String get _archiveName {
255 final String os = platform.operatingSystem.toLowerCase();
Greg Spencer29d59e32018-02-27 13:15:32 -0800256 // We don't use .tar.xz on Mac because although it can unpack them
257 // on the command line (with tar), the "Archive Utility" that runs
258 // when you double-click on them just does some crazy behavior (it
259 // converts it to a compressed cpio archive, and when you double
260 // click on that, it converts it back to .tar.xz, without ever
261 // unpacking it!) So, we use .zip for Mac, and the files are about
262 // 220MB larger than they need to be. :-(
263 final String suffix = platform.isLinux ? 'tar.xz' : 'zip';
Greg Spencer8a2df392018-02-06 15:32:19 -0800264 return 'flutter_${os}_$_version-$branchName.$suffix';
265 }
266
267 /// Checks out the flutter repo and prepares it for other operations.
268 ///
269 /// Returns the version for this release, as obtained from the git tags.
270 Future<String> initializeRepo() async {
271 await _checkoutFlutter();
272 _version = await _getVersion();
273 return _version;
Michael Goderbauer9e51a602018-01-10 13:37:36 -0800274 }
275
276 /// Performs all of the steps needed to create an archive.
Greg Spencer8a2df392018-02-06 15:32:19 -0800277 Future<File> createArchive() async {
278 assert(_version != null, 'Must run initializeRepo before createArchive');
279 _outputFile = new File(path.join(outputDir.absolute.path, _archiveName));
Michael Goderbauer9e51a602018-01-10 13:37:36 -0800280 await _installMinGitIfNeeded();
281 await _populateCaches();
Greg Spencer8a2df392018-02-06 15:32:19 -0800282 await _archiveFiles(_outputFile);
283 return _outputFile;
284 }
285
286 /// Returns the version number of this release, according the to tags in
287 /// the repo.
288 Future<String> _getVersion() async {
289 return _runGit(<String>['describe', '--tags', '--abbrev=0']);
Michael Goderbauer9e51a602018-01-10 13:37:36 -0800290 }
Greg Spencerf00c9022017-12-15 15:01:30 -0800291
292 /// Clone the Flutter repo and make sure that the git environment is sane
293 /// for when the user will unpack it.
Greg Spencer8a2df392018-02-06 15:32:19 -0800294 Future<Null> _checkoutFlutter() async {
295 // We want the user to start out the in the specified branch instead of a
296 // detached head. To do that, we need to make sure the branch points at the
Greg Spencerf00c9022017-12-15 15:01:30 -0800297 // desired revision.
Greg Spencerdf791272018-02-07 12:21:14 -0800298 await _runGit(<String>['clone', '-b', branchName, chromiumRepo], workingDirectory: tempDir);
Michael Goderbauer9e51a602018-01-10 13:37:36 -0800299 await _runGit(<String>['reset', '--hard', revision]);
Greg Spencerf00c9022017-12-15 15:01:30 -0800300
301 // Make the origin point to github instead of the chromium mirror.
Greg Spencerc345c1b2018-03-14 12:56:44 -0700302 await _runGit(<String>['remote', 'set-url', 'origin', githubRepo]);
Michael Goderbauer9e51a602018-01-10 13:37:36 -0800303 }
304
305 /// Retrieve the MinGit executable from storage and unpack it.
306 Future<Null> _installMinGitIfNeeded() async {
Greg Spencer8a2df392018-02-06 15:32:19 -0800307 if (!platform.isWindows) {
Michael Goderbauer9e51a602018-01-10 13:37:36 -0800308 return;
309 }
Greg Spencerdf791272018-02-07 12:21:14 -0800310 final Uint8List data = await httpReader(_minGitUri);
Greg Spencer8a2df392018-02-06 15:32:19 -0800311 final File gitFile = new File(path.join(tempDir.absolute.path, 'mingit.zip'));
Michael Goderbauer9e51a602018-01-10 13:37:36 -0800312 await gitFile.writeAsBytes(data, flush: true);
313
Greg Spencer8a2df392018-02-06 15:32:19 -0800314 final Directory minGitPath =
315 new Directory(path.join(flutterRoot.absolute.path, 'bin', 'mingit'));
Michael Goderbauer9e51a602018-01-10 13:37:36 -0800316 await minGitPath.create(recursive: true);
Greg Spencer8a2df392018-02-06 15:32:19 -0800317 await _unzipArchive(gitFile, workingDirectory: minGitPath);
Greg Spencerf00c9022017-12-15 15:01:30 -0800318 }
319
320 /// Prepare the archive repo so that it has all of the caches warmed up and
Michael Goderbauer9e51a602018-01-10 13:37:36 -0800321 /// is configured for the user to begin working.
322 Future<Null> _populateCaches() async {
323 await _runFlutter(<String>['doctor']);
324 await _runFlutter(<String>['update-packages']);
325 await _runFlutter(<String>['precache']);
326 await _runFlutter(<String>['ide-config']);
Greg Spencerf00c9022017-12-15 15:01:30 -0800327
Michael Goderbauer9e51a602018-01-10 13:37:36 -0800328 // Create each of the templates, since they will call 'pub get' on
Greg Spencerf00c9022017-12-15 15:01:30 -0800329 // themselves when created, and this will warm the cache with their
330 // dependencies too.
Greg Spencerdf791272018-02-07 12:21:14 -0800331 for (String template in <String>['app', 'package', 'plugin']) {
Greg Spencer8a2df392018-02-06 15:32:19 -0800332 final String createName = path.join(tempDir.path, 'create_$template');
Michael Goderbauer9e51a602018-01-10 13:37:36 -0800333 await _runFlutter(
Greg Spencerf00c9022017-12-15 15:01:30 -0800334 <String>['create', '--template=$template', createName],
335 );
336 }
337
338 // Yes, we could just skip all .packages files when constructing
339 // the archive, but some are checked in, and we don't want to skip
340 // those.
Michael Goderbauer9e51a602018-01-10 13:37:36 -0800341 await _runGit(<String>['clean', '-f', '-X', '**/.packages']);
Greg Spencerf00c9022017-12-15 15:01:30 -0800342 }
343
Michael Goderbauer9e51a602018-01-10 13:37:36 -0800344 /// Write the archive to the given output file.
345 Future<Null> _archiveFiles(File outputFile) async {
Greg Spencerf00c9022017-12-15 15:01:30 -0800346 if (outputFile.path.toLowerCase().endsWith('.zip')) {
Greg Spencer8a2df392018-02-06 15:32:19 -0800347 await _createZipArchive(outputFile, flutterRoot);
Greg Spencera556ad02017-12-15 16:40:49 -0800348 } else if (outputFile.path.toLowerCase().endsWith('.tar.xz')) {
Greg Spencer8a2df392018-02-06 15:32:19 -0800349 await _createTarArchive(outputFile, flutterRoot);
Greg Spencerf00c9022017-12-15 15:01:30 -0800350 }
351 }
352
Greg Spencer8a2df392018-02-06 15:32:19 -0800353 Future<String> _runFlutter(List<String> args, {Directory workingDirectory}) {
Greg Spencerd9e37152018-03-20 08:14:08 -0700354 return _processRunner.runProcess(
355 <String>[_flutter]..addAll(args),
356 workingDirectory: workingDirectory ?? flutterRoot,
357 );
Greg Spencer8a2df392018-02-06 15:32:19 -0800358 }
Michael Goderbauer9e51a602018-01-10 13:37:36 -0800359
360 Future<String> _runGit(List<String> args, {Directory workingDirectory}) {
Greg Spencerd9e37152018-03-20 08:14:08 -0700361 return _processRunner.runProcess(
362 <String>['git']..addAll(args),
363 workingDirectory: workingDirectory ?? flutterRoot,
364 );
Greg Spencerf00c9022017-12-15 15:01:30 -0800365 }
366
Michael Goderbauer9e51a602018-01-10 13:37:36 -0800367 /// Unpacks the given zip file into the currentDirectory (if set), or the
368 /// same directory as the archive.
Greg Spencer8a2df392018-02-06 15:32:19 -0800369 Future<String> _unzipArchive(File archive, {Directory workingDirectory}) {
Greg Spencer8a2df392018-02-06 15:32:19 -0800370 workingDirectory ??= new Directory(path.dirname(archive.absolute.path));
Greg Spencerc345c1b2018-03-14 12:56:44 -0700371 List<String> commandLine;
372 if (platform.isWindows) {
373 commandLine = <String>[
374 '7za',
375 'x',
376 archive.absolute.path,
377 ];
378 } else {
379 commandLine = <String>[
380 'unzip',
381 archive.absolute.path,
382 ];
383 }
Greg Spencer8a2df392018-02-06 15:32:19 -0800384 return _processRunner.runProcess(commandLine, workingDirectory: workingDirectory);
Greg Spencerf00c9022017-12-15 15:01:30 -0800385 }
386
Michael Goderbauer9e51a602018-01-10 13:37:36 -0800387 /// Create a zip archive from the directory source.
Michael Goderbauer9e51a602018-01-10 13:37:36 -0800388 Future<String> _createZipArchive(File output, Directory source) {
Greg Spencer29d59e32018-02-27 13:15:32 -0800389 List<String> commandLine;
390 if (platform.isWindows) {
391 commandLine = <String>[
392 '7za',
393 'a',
394 '-tzip',
395 '-mx=9',
396 output.absolute.path,
397 path.basename(source.path),
398 ];
399 } else {
400 commandLine = <String>[
401 'zip',
402 '-r',
403 '-9',
404 output.absolute.path,
405 path.basename(source.path),
406 ];
407 }
Greg Spencerd9e37152018-03-20 08:14:08 -0700408 return _processRunner.runProcess(
409 commandLine,
410 workingDirectory: new Directory(path.dirname(source.absolute.path)),
411 );
Greg Spencerb7169c12018-01-09 17:42:42 -0800412 }
Michael Goderbauer9e51a602018-01-10 13:37:36 -0800413
414 /// Create a tar archive from the directory source.
415 Future<String> _createTarArchive(File output, Directory source) {
Greg Spencer8a2df392018-02-06 15:32:19 -0800416 return _processRunner.runProcess(<String>[
Michael Goderbauer9e51a602018-01-10 13:37:36 -0800417 'tar',
418 'cJf',
419 output.absolute.path,
420 path.basename(source.absolute.path),
421 ], workingDirectory: new Directory(path.dirname(source.absolute.path)));
422 }
Greg Spencer8a2df392018-02-06 15:32:19 -0800423}
Michael Goderbauer9e51a602018-01-10 13:37:36 -0800424
Greg Spencer8a2df392018-02-06 15:32:19 -0800425class ArchivePublisher {
426 ArchivePublisher(
427 this.tempDir,
428 this.revision,
429 this.branch,
430 this.version,
431 this.outputFile, {
432 ProcessManager processManager,
433 bool subprocessOutput: true,
434 this.platform: const LocalPlatform(),
435 }) : assert(revision.length == 40),
436 platformName = platform.operatingSystem.toLowerCase(),
Greg Spencerd9e37152018-03-20 08:14:08 -0700437 metadataGsPath = '$gsReleaseFolder/${getMetadataFilename(platform)}',
Greg Spencer8a2df392018-02-06 15:32:19 -0800438 _processRunner = new ProcessRunner(
439 processManager: processManager,
440 subprocessOutput: subprocessOutput,
441 );
442
Greg Spencer8a2df392018-02-06 15:32:19 -0800443 final Platform platform;
444 final String platformName;
445 final String metadataGsPath;
446 final Branch branch;
447 final String revision;
448 final String version;
449 final Directory tempDir;
450 final File outputFile;
451 final ProcessRunner _processRunner;
452 String get branchName => getBranchName(branch);
Greg Spencerd9e37152018-03-20 08:14:08 -0700453 String get destinationArchivePath => '$branchName/$platformName/${path.basename(outputFile.path)}';
454 static String getMetadataFilename(Platform platform) => 'releases_${platform.operatingSystem.toLowerCase()}.json';
Greg Spencer8a2df392018-02-06 15:32:19 -0800455
456 /// Publish the archive to Google Storage.
457 Future<Null> publishArchive() async {
458 final String destGsPath = '$gsReleaseFolder/$destinationArchivePath';
459 await _cloudCopy(outputFile.absolute.path, destGsPath);
460 assert(tempDir.existsSync());
Greg Spencer984a24c2018-03-09 18:58:41 -0800461 await _updateMetadata();
Greg Spencer8a2df392018-02-06 15:32:19 -0800462 }
463
Greg Spencer05ccf562018-03-22 13:14:31 -0700464 Map<String, dynamic> _addRelease(Map<String, dynamic> jsonData) {
465 jsonData['base_url'] = '$baseUrl$releaseFolder';
466 if (!jsonData.containsKey('current_release')) {
467 jsonData['current_release'] = <String, String>{};
468 }
469 jsonData['current_release'][branchName] = revision;
470 if (!jsonData.containsKey('releases')) {
471 jsonData['releases'] = <Map<String, dynamic>>[];
472 }
473
474 final Map<String, dynamic> newEntry = <String, dynamic>{};
475 newEntry['hash'] = revision;
476 newEntry['channel'] = branchName;
477 newEntry['version'] = version;
478 newEntry['release_date'] = new DateTime.now().toUtc().toIso8601String();
479 newEntry['archive'] = destinationArchivePath;
480
481 // Search for any entries with the same hash and channel and remove them.
482 final List<dynamic> releases = jsonData['releases'];
483 final List<Map<String, dynamic>> prunedReleases = <Map<String, dynamic>>[];
484 for (Map<String, dynamic> entry in releases) {
485 if (entry['hash'] != newEntry['hash'] || entry['channel'] != newEntry['channel']) {
486 prunedReleases.add(entry);
487 }
488 }
489
490 prunedReleases.add(newEntry);
491 prunedReleases.sort((Map<String, dynamic> a, Map<String, dynamic> b) {
492 final DateTime aDate = DateTime.parse(a['release_date']);
493 final DateTime bDate = DateTime.parse(b['release_date']);
494 return bDate.compareTo(aDate);
495 });
496 jsonData['releases'] = prunedReleases;
497 return jsonData;
498 }
499
Greg Spencer8a2df392018-02-06 15:32:19 -0800500 Future<Null> _updateMetadata() async {
Greg Spencerd9e37152018-03-20 08:14:08 -0700501 // We can't just cat the metadata from the server with 'gsutil cat', because
502 // Windows wants to echo the commands that execute in gsutil.bat to the
503 // stdout when we do that. So, we copy the file locally and then read it
504 // back in.
505 final File metadataFile = new File(
506 path.join(tempDir.absolute.path, getMetadataFilename(platform)),
507 );
508 await _runGsUtil(<String>['cp', metadataGsPath, metadataFile.absolute.path]);
509 final String currentMetadata = metadataFile.readAsStringSync();
Greg Spencer8a2df392018-02-06 15:32:19 -0800510 if (currentMetadata.isEmpty) {
511 throw new ProcessRunnerException('Empty metadata received from server');
Michael Goderbauer9e51a602018-01-10 13:37:36 -0800512 }
513
Greg Spencer8a2df392018-02-06 15:32:19 -0800514 Map<String, dynamic> jsonData;
Michael Goderbauer9e51a602018-01-10 13:37:36 -0800515 try {
Greg Spencer8a2df392018-02-06 15:32:19 -0800516 jsonData = json.decode(currentMetadata);
517 } on FormatException catch (e) {
518 throw new ProcessRunnerException('Unable to parse JSON metadata received from cloud: $e');
Michael Goderbauer9e51a602018-01-10 13:37:36 -0800519 }
520
Greg Spencer05ccf562018-03-22 13:14:31 -0700521 jsonData = _addRelease(jsonData);
Greg Spencer8a2df392018-02-06 15:32:19 -0800522
Alexandre Ardhuin7667db62018-03-14 06:24:49 +0100523 const JsonEncoder encoder = const JsonEncoder.withIndent(' ');
Greg Spencerd9e37152018-03-20 08:14:08 -0700524 metadataFile.writeAsStringSync(encoder.convert(jsonData));
525 await _cloudCopy(metadataFile.absolute.path, metadataGsPath);
Greg Spencer8a2df392018-02-06 15:32:19 -0800526 }
527
Greg Spencerd9e37152018-03-20 08:14:08 -0700528 Future<String> _runGsUtil(
529 List<String> args, {
530 Directory workingDirectory,
531 bool failOk: false,
532 }) async {
Greg Spencer8a2df392018-02-06 15:32:19 -0800533 return _processRunner.runProcess(
534 <String>['gsutil']..addAll(args),
535 workingDirectory: workingDirectory,
536 failOk: failOk,
537 );
538 }
539
540 Future<String> _cloudCopy(String src, String dest) async {
Greg Spencer97e77932018-02-09 15:42:51 -0800541 // We often don't have permission to overwrite, but
542 // we have permission to remove, so that's what we do.
Greg Spencer8a2df392018-02-06 15:32:19 -0800543 await _runGsUtil(<String>['rm', dest], failOk: true);
Greg Spencer97e77932018-02-09 15:42:51 -0800544 String mimeType;
545 if (dest.endsWith('.tar.xz')) {
546 mimeType = 'application/x-gtar';
547 }
548 if (dest.endsWith('.zip')) {
549 mimeType = 'application/zip';
550 }
551 if (dest.endsWith('.json')) {
552 mimeType = 'application/json';
553 }
Greg Spencer0b6c1932018-02-22 09:03:10 -0800554 final List<String> args = <String>[];
Greg Spencer97e77932018-02-09 15:42:51 -0800555 // Use our preferred MIME type for the files we care about
556 // and let gsutil figure it out for anything else.
557 if (mimeType != null) {
558 args.addAll(<String>['-h', 'Content-Type:$mimeType']);
559 }
Greg Spencer0b6c1932018-02-22 09:03:10 -0800560 args.addAll(<String>['cp', src, dest]);
Greg Spencer97e77932018-02-09 15:42:51 -0800561 return _runGsUtil(args);
Michael Goderbauer9e51a602018-01-10 13:37:36 -0800562 }
Greg Spencerf00c9022017-12-15 15:01:30 -0800563}
564
565/// Prepares a flutter git repo to be packaged up for distribution.
566/// It mainly serves to populate the .pub-cache with any appropriate Dart
567/// packages, and the flutter cache in bin/cache with the appropriate
568/// dependencies and snapshots.
Michael Goderbauer9e51a602018-01-10 13:37:36 -0800569///
570/// Note that archives contain the executables and customizations for the
571/// platform that they are created on.
572Future<Null> main(List<String> argList) async {
Greg Spencerf00c9022017-12-15 15:01:30 -0800573 final ArgParser argParser = new ArgParser();
574 argParser.addOption(
575 'temp_dir',
576 defaultsTo: null,
577 help: 'A location where temporary files may be written. Defaults to a '
578 'directory in the system temp folder. Will write a few GiB of data, '
Greg Spencer8a2df392018-02-06 15:32:19 -0800579 'so it should have sufficient free space. If a temp_dir is not '
580 'specified, then the default temp_dir will be created, used, and '
581 'removed automatically.',
Greg Spencerf00c9022017-12-15 15:01:30 -0800582 );
Greg Spencer8a2df392018-02-06 15:32:19 -0800583 argParser.addOption('revision',
584 defaultsTo: null,
585 help: 'The Flutter git repo revision to build the '
586 'archive with. Must be the full 40-character hash. Required.');
Greg Spencerf00c9022017-12-15 15:01:30 -0800587 argParser.addOption(
Greg Spencer8a2df392018-02-06 15:32:19 -0800588 'branch',
589 defaultsTo: null,
590 allowed: Branch.values.map((Branch branch) => getBranchName(branch)),
591 help: 'The Flutter branch to build the archive with. Required.',
Greg Spencerf00c9022017-12-15 15:01:30 -0800592 );
593 argParser.addOption(
594 'output',
595 defaultsTo: null,
Greg Spencer8a2df392018-02-06 15:32:19 -0800596 help: 'The path to the directory where the output archive should be '
597 'written. If --output is not specified, the archive will be written to '
598 "the current directory. If the output directory doesn't exist, it, and "
599 'the path to it, will be created.',
Greg Spencerf00c9022017-12-15 15:01:30 -0800600 );
Greg Spencer8a2df392018-02-06 15:32:19 -0800601 argParser.addFlag(
602 'publish',
603 defaultsTo: false,
Greg Spencerdf791272018-02-07 12:21:14 -0800604 help: 'If set, will publish the archive to Google Cloud Storage upon '
605 'successful creation of the archive. Will publish under this '
606 'directory: $baseUrl$releaseFolder',
607 );
608 argParser.addFlag(
609 'help',
610 defaultsTo: false,
611 negatable: false,
612 help: 'Print help for this command.',
Greg Spencer8a2df392018-02-06 15:32:19 -0800613 );
614
Greg Spencerf00c9022017-12-15 15:01:30 -0800615 final ArgResults args = argParser.parse(argList);
616
Greg Spencerdf791272018-02-07 12:21:14 -0800617 if (args['help']) {
618 print(argParser.usage);
619 exit(0);
620 }
621
Greg Spencerf00c9022017-12-15 15:01:30 -0800622 void errorExit(String message, {int exitCode = -1}) {
623 stderr.write('Error: $message\n\n');
624 stderr.write('${argParser.usage}\n');
625 exit(exitCode);
626 }
627
Greg Spencer8a2df392018-02-06 15:32:19 -0800628 final String revision = args['revision'];
629 if (revision.isEmpty) {
Greg Spencerf00c9022017-12-15 15:01:30 -0800630 errorExit('Invalid argument: --revision must be specified.');
631 }
Greg Spencer8a2df392018-02-06 15:32:19 -0800632 if (revision.length != 40) {
633 errorExit('Invalid argument: --revision must be the entire hash, not just a prefix.');
634 }
635
636 if (args['branch'].isEmpty) {
637 errorExit('Invalid argument: --branch must be specified.');
638 }
Greg Spencerf00c9022017-12-15 15:01:30 -0800639
Michael Goderbauer9e51a602018-01-10 13:37:36 -0800640 Directory tempDir;
Greg Spencerf00c9022017-12-15 15:01:30 -0800641 bool removeTempDir = false;
642 if (args['temp_dir'] == null || args['temp_dir'].isEmpty) {
Michael Goderbauer9e51a602018-01-10 13:37:36 -0800643 tempDir = Directory.systemTemp.createTempSync('flutter_');
Greg Spencerf00c9022017-12-15 15:01:30 -0800644 removeTempDir = true;
645 } else {
Michael Goderbauer9e51a602018-01-10 13:37:36 -0800646 tempDir = new Directory(args['temp_dir']);
647 if (!tempDir.existsSync()) {
Greg Spencerf00c9022017-12-15 15:01:30 -0800648 errorExit("Temporary directory ${args['temp_dir']} doesn't exist.");
649 }
650 }
651
Greg Spencer8a2df392018-02-06 15:32:19 -0800652 Directory outputDir;
653 if (args['output'] == null) {
654 outputDir = tempDir;
Michael Goderbauer9e51a602018-01-10 13:37:36 -0800655 } else {
Greg Spencer8a2df392018-02-06 15:32:19 -0800656 outputDir = new Directory(args['output']);
657 if (!outputDir.existsSync()) {
658 outputDir.createSync(recursive: true);
Michael Goderbauer9e51a602018-01-10 13:37:36 -0800659 }
Greg Spencerf00c9022017-12-15 15:01:30 -0800660 }
661
Greg Spencer8a2df392018-02-06 15:32:19 -0800662 final Branch branch = fromBranchName(args['branch']);
663 final ArchiveCreator creator = new ArchiveCreator(tempDir, outputDir, revision, branch);
Greg Spencerf00c9022017-12-15 15:01:30 -0800664 int exitCode = 0;
665 String message;
666 try {
Greg Spencer8a2df392018-02-06 15:32:19 -0800667 final String version = await creator.initializeRepo();
668 final File outputFile = await creator.createArchive();
669 if (args['publish']) {
670 final ArchivePublisher publisher = new ArchivePublisher(
671 tempDir,
672 revision,
673 branch,
674 version,
675 outputFile,
676 );
677 await publisher.publishArchive();
678 }
679 } on ProcessRunnerException catch (e) {
Greg Spencerf00c9022017-12-15 15:01:30 -0800680 exitCode = e.exitCode;
681 message = e.message;
Greg Spencer984a24c2018-03-09 18:58:41 -0800682 } catch (e) {
683 exitCode = -1;
684 message = e.toString();
Greg Spencerf00c9022017-12-15 15:01:30 -0800685 } finally {
686 if (removeTempDir) {
Michael Goderbauer9e51a602018-01-10 13:37:36 -0800687 tempDir.deleteSync(recursive: true);
Greg Spencerf00c9022017-12-15 15:01:30 -0800688 }
689 if (exitCode != 0) {
690 errorExit(message, exitCode: exitCode);
691 }
692 exit(0);
693 }
Greg Spencerf00c9022017-12-15 15:01:30 -0800694}