1// Copyright 2014 The Flutter 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
5import 'dart:async';
6import 'dart:io' show exit, stderr;
7
8import 'package:args/args.dart';
9import 'package:file/file.dart';
10import 'package:file/local.dart';
11
12import 'prepare_package/archive_creator.dart';
13import 'prepare_package/archive_publisher.dart';
14import 'prepare_package/common.dart';
15
16const FileSystem fs = LocalFileSystem();
17
18/// Prepares a flutter git repo to be packaged up for distribution. It mainly
19/// serves to populate the .pub-preload-cache with any appropriate Dart
20/// packages, and the flutter cache in bin/cache with the appropriate
21/// dependencies and snapshots.
22///
23/// Archives contain the executables and customizations for the platform that
24/// they are created on.
25Future<void> main(List<String> rawArguments) async {
26 final ArgParser argParser = ArgParser();
27 argParser.addOption(
28 'temp_dir',
29 help:
30 'A location where temporary files may be written. Defaults to a '
31 'directory in the system temp folder. Will write a few GiB of data, '
32 'so it should have sufficient free space. If a temp_dir is not '
33 'specified, then the default temp_dir will be created, used, and '
34 'removed automatically.',
35 );
36 argParser.addOption(
37 'revision',
38 help:
39 'The Flutter git repo revision to build the '
40 'archive with. Must be the full 40-character hash. Required.',
41 );
42 argParser.addOption(
43 'branch',
44 allowed: Branch.values.map<String>((Branch branch) => branch.name),
45 help: 'The Flutter branch to build the archive with. Required.',
46 );
47 argParser.addOption(
48 'output',
49 help:
50 'The path to the directory where the output archive should be '
51 'written. If --output is not specified, the archive will be written to '
52 "the current directory. If the output directory doesn't exist, it, and "
53 'the path to it, will be created.',
54 );
55 argParser.addFlag(
56 'publish',
57 help:
58 'If set, will publish the archive to Google Cloud Storage upon '
59 'successful creation of the archive. Will publish under this '
60 'directory: $baseUrl$releaseFolder',
61 );
62 argParser.addFlag('force', abbr: 'f', help: 'Overwrite a previously uploaded package.');
63 argParser.addFlag(
64 'dry_run',
65 negatable: false,
66 help: 'Prints gsutil commands instead of executing them.',
67 );
68 argParser.addFlag('help', negatable: false, help: 'Print help for this command.');
69
70 final ArgResults parsedArguments = argParser.parse(rawArguments);
71
72 if (parsedArguments['help'] as bool) {
73 print(argParser.usage);
74 exit(0);
75 }
76
77 void errorExit(String message, {int exitCode = -1}) {
78 stderr.write('Error: $message\n\n');
79 stderr.write('${argParser.usage}\n');
80 exit(exitCode);
81 }
82
83 if (!parsedArguments.wasParsed('revision')) {
84 errorExit('Invalid argument: --revision must be specified.');
85 }
86 final String revision = parsedArguments['revision'] as String;
87 if (revision.length != 40) {
88 errorExit('Invalid argument: --revision must be the entire hash, not just a prefix.');
89 }
90
91 if (!parsedArguments.wasParsed('branch')) {
92 errorExit('Invalid argument: --branch must be specified.');
93 }
94
95 final String? tempDirArg = parsedArguments['temp_dir'] as String?;
96 final Directory tempDir;
97 bool removeTempDir = false;
98 if (tempDirArg == null || tempDirArg.isEmpty) {
99 tempDir = fs.systemTempDirectory.createTempSync('flutter_package.');
100 removeTempDir = true;
101 } else {
102 tempDir = fs.directory(tempDirArg);
103 if (!tempDir.existsSync()) {
104 errorExit("Temporary directory $tempDirArg doesn't exist.");
105 }
106 }
107
108 final Directory outputDir;
109 if (parsedArguments['output'] == null) {
110 outputDir = tempDir;
111 } else {
112 outputDir = fs.directory(parsedArguments['output'] as String);
113 if (!outputDir.existsSync()) {
114 outputDir.createSync(recursive: true);
115 }
116 }
117
118 final bool publish = parsedArguments['publish'] as bool;
119 final bool dryRun = parsedArguments['dry_run'] as bool;
120 final Branch branch = Branch.values.byName(parsedArguments['branch'] as String);
121 final ArchiveCreator creator = ArchiveCreator(
122 tempDir,
123 outputDir,
124 revision,
125 branch,
126 fs: fs,
127 strict: publish && !dryRun,
128 );
129 int exitCode = 0;
130 late String message;
131 try {
132 final Map<String, String> version = await creator.initializeRepo();
133 final File outputFile = await creator.createArchive();
134 final ArchivePublisher publisher = ArchivePublisher(
135 tempDir,
136 revision,
137 branch,
138 version,
139 outputFile,
140 dryRun,
141 fs: fs,
142 );
143 await publisher.generateLocalMetadata();
144 if (parsedArguments['publish'] as bool) {
145 await publisher.publishArchive(parsedArguments['force'] as bool);
146 }
147 } on PreparePackageException catch (e) {
148 exitCode = e.exitCode;
149 message = e.message;
150 } catch (e) {
151 exitCode = -1;
152 message = e.toString();
153 } finally {
154 if (removeTempDir) {
155 tempDir.deleteSync(recursive: true);
156 }
157 if (exitCode != 0) {
158 errorExit(message, exitCode: exitCode);
159 }
160 exit(0);
161 }
162}
163