1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
// Copyright 2014 The Flutter 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 '../base/common.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/net.dart';
import '../base/process.dart';
import '../convert.dart';
import '../globals.dart' as globals;
import 'fuchsia_sdk.dart';
/// This is a basic wrapper class for the Fuchsia SDK's `pm` tool.
class FuchsiaPM {
/// Initializes the staging area at [buildPath] for creating the Fuchsia
/// package for the app named [appName].
///
/// When successful, this creates a file under [buildPath] at `meta/package`.
///
/// NB: The [buildPath] should probably be e.g. `build/fuchsia/pkg`, and the
/// [appName] should probably be the name of the app from the pubspec file.
Future<bool> init(String buildPath, String appName) {
return _runPMCommand(<String>[
'-o',
buildPath,
'-n',
appName,
'init',
]);
}
/// Generates a new private key to be used to sign a Fuchsia package.
///
/// [buildPath] should be the same [buildPath] passed to [init].
Future<bool> genkey(String buildPath, String outKeyPath) {
return _runPMCommand(<String>[
'-o',
buildPath,
'-k',
outKeyPath,
'genkey',
]);
}
/// Updates, signs, and seals a Fuchsia package.
///
/// [buildPath] should be the same [buildPath] passed to [init].
/// [manifestPath] must be a file containing lines formatted as follows:
///
/// data/path/to/file/in/the/package=/path/to/file/on/the/host
///
/// which describe the contents of the Fuchsia package. It must also contain
/// two other entries:
///
/// meta/$APPNAME.cmx=/path/to/cmx/on/the/host/$APPNAME.cmx
/// meta/package=/path/to/package/file/from/init/package
///
/// where $APPNAME is the same [appName] passed to [init], and meta/package
/// is set up to be the file `meta/package` created by [init].
Future<bool> build(String buildPath, String keyPath, String manifestPath) {
return _runPMCommand(<String>[
'-o',
buildPath,
'-k',
keyPath,
'-m',
manifestPath,
'build',
]);
}
/// Constructs a .far representation of the Fuchsia package.
///
/// When successful, creates a file `app_name-0.far` under [buildPath], which
/// is the Fuchsia package.
///
/// [buildPath] should be the same path passed to [init], and [manfiestPath]
/// should be the same manifest passed to [build].
Future<bool> archive(String buildPath, String keyPath, String manifestPath) {
return _runPMCommand(<String>[
'-o',
buildPath,
'-k',
keyPath,
'-m',
manifestPath,
'archive',
]);
}
/// Initializes a new package repository at [repoPath] to be later served by
/// the 'serve' command.
Future<bool> newrepo(String repoPath) {
return _runPMCommand(<String>[
'newrepo',
'-repo',
repoPath,
]);
}
/// Spawns an http server in a new process for serving Fuchsia packages.
///
/// The argument [repoPath] should have previously been an argument to
/// [newrepo]. The [host] should be the host reported by
/// [FuchsiaDevFinder.resolve], and [port] should be an unused port for the
/// http server to bind.
Future<Process> serve(String repoPath, String host, int port) async {
if (globals.fuchsiaArtifacts.pm == null) {
throwToolExit('Fuchsia pm tool not found');
}
if (isIPv6Address(host.split('%').first)) {
host = '[${host.replaceAll('%', '%25')}]';
}
final List<String> command = <String>[
globals.fuchsiaArtifacts.pm.path,
'serve',
'-repo',
repoPath,
'-l',
'$host:$port',
];
final Process process = await processUtils.start(command);
process.stdout
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen(globals.printTrace);
process.stderr
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen(globals.printError);
return process;
}
/// Publishes a Fuchsia package to a served package repository.
///
/// For a package repo initialized with [newrepo] at [repoPath] and served
/// by [serve], this call publishes the `far` package at [packagePath] to
/// the repo such that it will be visible to devices connecting to the
/// package server.
Future<bool> publish(String repoPath, String packagePath) {
return _runPMCommand(<String>[
'publish',
'-a',
'-r',
repoPath,
'-f',
packagePath,
]);
}
Future<bool> _runPMCommand(List<String> args) async {
if (globals.fuchsiaArtifacts.pm == null) {
throwToolExit('Fuchsia pm tool not found');
}
final List<String> command = <String>[globals.fuchsiaArtifacts.pm.path, ...args];
final RunResult result = await processUtils.run(command);
return result.exitCode == 0;
}
}
/// A class for running and retaining state for a Fuchsia package server.
///
/// [FuchsiaPackageServer] takes care of initializing the package repository,
/// spinning up the package server, publishing packages, and shutting down the
/// the server.
///
/// Example usage:
/// var server = FuchsiaPackageServer(
/// '/path/to/repo',
/// 'server_name',
/// await FuchsiaDevFinder.resolve(deviceName),
/// await freshPort());
/// try {
/// await server.start();
/// await server.addPackage(farArchivePath);
/// ...
/// } finally {
/// server.stop();
/// }
class FuchsiaPackageServer {
factory FuchsiaPackageServer(String repo, String name, String host, int port) {
// TODO(jonahwilliams): ensure we only receive valid ipv4 or ipv6 InternetAddresses.
// Temporary work around to receiving ipv6 addresses with trailing information:
// fe80::ec4:7aff:fecc:ea8f%eno2
if (host.contains('%')) {
host = host.split('%').first;
}
return FuchsiaPackageServer._(repo, name, host, port);
}
FuchsiaPackageServer._(this._repo, this.name, this._host, this._port);
static const String deviceHost = 'fuchsia.com';
static const String toolHost = 'flutter_tool';
final String _repo;
final String _host;
final int _port;
Process _process;
/// The URL that can be used by the device to access this package server.
String get url => Uri(scheme: 'http', host: _host, port: _port).toString();
// The name used to reference the server by fuchsia-pkg:// urls.
final String name;
/// Uses [FuchiaPM.newrepo] and [FuchsiaPM.serve] to spin up a new Fuchsia
/// package server.
///
/// Returns false if the repo could not be created or the server could not
/// be spawned, and true otherwise.
Future<bool> start() async {
if (_process != null) {
globals.printError('$this already started!');
return false;
}
// initialize a new repo.
if (!await fuchsiaSdk.fuchsiaPM.newrepo(_repo)) {
globals.printError('Failed to create a new package server repo');
return false;
}
_process = await fuchsiaSdk.fuchsiaPM.serve(_repo, _host, _port);
// Put a completer on _process.exitCode to watch for error.
unawaited(_process.exitCode.whenComplete(() {
// If _process is null, then the server was stopped deliberately.
if (_process != null) {
globals.printError('Error running Fuchsia pm tool "serve" command');
}
}));
return true;
}
/// Forcefully stops the package server process by sending it SIGTERM.
void stop() {
if (_process != null) {
_process.kill();
_process = null;
}
}
/// Uses [FuchsiaPM.publish] to add the Fuchsia 'far' package at
/// [packagePath] to the package server.
///
/// Returns true on success and false if the server wasn't started or the
/// publish command failed.
Future<bool> addPackage(File package) async {
if (_process == null) {
return false;
}
return await fuchsiaSdk.fuchsiaPM.publish(_repo, package.path);
}
@override
String toString() {
final String p = (_process == null) ? 'stopped' : 'running ${_process.pid}';
return 'FuchsiaPackageServer at $_host:$_port ($p)';
}
}