blob: 7ed70e21195875c3d07a231d38c9a55eeb894421 [file] [log] [blame]
// 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]';
}
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) {
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 URL that is stripped of interface name if it is an ipv6 address,
/// which should be supplied to amber_ctl to configure access to host
String get interfaceStrippedUrl => Uri(
scheme: 'http',
host: (isIPv6Address(_host.split('%').first)) ? '[${_host.split('%').first}]' : _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)';
}
}