blob: b470f1bf976c0815e03880701f009d0448a170de [file] [log] [blame]
// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
library pub.command.build;
import 'dart:async';
import 'package:barback/barback.dart';
import 'package:path/path.dart' as path;
import '../barback/build_environment.dart';
import '../barback.dart' as barback;
import '../command.dart';
import '../exit_codes.dart' as exit_codes;
import '../io.dart';
import '../log.dart' as log;
import '../utils.dart';
final _arrow = getSpecial('\u2192', '=>');
/// The set of top level directories in the entrypoint package that can be
/// built.
final _allowedBuildDirectories = new Set<String>.from([
"benchmark", "bin", "example", "test", "web"
]);
/// Handles the `build` pub command.
class BuildCommand extends PubCommand {
String get description =>
"Copy and compile all Dart entrypoints in the 'web' directory.";
String get usage => "pub build [options]";
List<String> get aliases => const ["deploy", "settle-up"];
bool get takesArguments => true;
// TODO(nweiz): make this configurable.
/// The path to the application's build output directory.
String get target => path.join(entrypoint.root.dir, 'build');
/// The build mode.
BarbackMode get mode => new BarbackMode(commandOptions['mode']);
/// The number of files that have been built and written to disc so far.
int builtFiles = 0;
/// The names of the top-level build directories that will be built.
final buildDirectories = new Set<String>();
BuildCommand() {
commandParser.addOption('mode', defaultsTo: BarbackMode.RELEASE.toString(),
help: 'Mode to run transformers in.');
commandParser.addFlag('all', help: "Build all buildable directories.",
defaultsTo: false, negatable: false);
}
Future onRun() {
var exitCode = _parseBuildDirectories();
if (exitCode != exit_codes.SUCCESS) return flushThenExit(exitCode);
cleanDir(target);
// Since this server will only be hit by the transformer loader and isn't
// user-facing, just use an IPv4 address to avoid a weird bug on the
// OS X buildbots.
return BuildEnvironment.create(entrypoint, "127.0.0.1", 0, mode,
WatcherType.NONE, buildDirectories, useDart2JS: true)
.then((environment) {
// Show in-progress errors, but not results. Those get handled implicitly
// by getAllAssets().
environment.server.barback.errors.listen((error) {
log.error(log.red("Build error:\n$error"));
});
return log.progress("Building ${entrypoint.root.name}",
() => environment.server.barback.getAllAssets()).then((assets) {
// Find all of the JS entrypoints we built.
var dart2JSEntrypoints = assets
.where((asset) => asset.id.path.endsWith(".dart.js"))
.map((asset) => asset.id);
return Future.wait(assets.map(_writeAsset)).then((_) {
builtFiles += _copyBrowserJsFiles(dart2JSEntrypoints);
log.message("Built $builtFiles ${pluralize('file', builtFiles)}!");
});
});
}).catchError((error) {
// If [getAllAssets()] throws a BarbackException, the error has already
// been reported.
if (error is! BarbackException) throw error;
log.error(log.red("Build failed."));
return flushThenExit(exit_codes.DATA);
});
}
/// Parses the command-line arguments to determine the set of top-level
/// directories to build.
///
/// If there are no arguments to `pub build`, this will just be "web".
///
/// If the `--all` flag is set, then it will be all buildable directories
/// that exist.
///
/// Otherwise, all arguments should be the names of directories to include.
///
/// Returns the exit code of an error, or zero if it parsed correctly.
int _parseBuildDirectories() {
if (commandOptions["all"]) {
if (commandOptions.rest.isNotEmpty) {
log.error(
'Build directory names are not allowed if "--all" is passed.');
return exit_codes.USAGE;
}
// Include every build directory that exists in the package.
var allowed = _allowedBuildDirectories.where(
(d) => dirExists(path.join(entrypoint.root.dir, d)));
if (allowed.isEmpty) {
var buildDirs = toSentence(ordered(_allowedBuildDirectories.map(
(name) => '"$name"')));
log.error('There are no buildable directories.\n'
'The supported directories are $buildDirs.');
return exit_codes.DATA;
}
buildDirectories.addAll(allowed);
return exit_codes.SUCCESS;
}
buildDirectories.addAll(commandOptions.rest);
// If no directory were specified, default to "web".
if (buildDirectories.isEmpty) {
buildDirectories.add("web");
}
// Make sure the arguments are known directories.
var disallowed = buildDirectories.where(
(dir) => !_allowedBuildDirectories.contains(dir));
if (disallowed.isNotEmpty) {
var dirs = pluralize("directory", disallowed.length,
plural: "directories");
var names = toSentence(ordered(disallowed).map((name) => '"$name"'));
var allowed = toSentence(ordered(_allowedBuildDirectories.map(
(name) => '"$name"')));
log.error('Unsupported build $dirs $names.\n'
'The allowed directories are $allowed.');
return exit_codes.USAGE;
}
// Make sure all of the build directories exist.
var missing = buildDirectories.where(
(dir) => !dirExists(path.join(entrypoint.root.dir, dir)));
if (missing.length == 1) {
log.error('Directory "${missing.single}" does not exist.');
return exit_codes.DATA;
} else if (missing.isNotEmpty) {
var names = toSentence(ordered(missing).map((name) => '"$name"'));
log.error('Directories $names do not exist.');
return exit_codes.DATA;
}
return exit_codes.SUCCESS;
}
/// Writes [asset] to the appropriate build directory.
///
/// If [asset] is in the special "assets" directory, writes it to every
/// build directory.
Future _writeAsset(Asset asset) {
// In release mode, strip out .dart files since all relevant ones have been
// compiled to JavaScript already.
if (mode == BarbackMode.RELEASE && asset.id.extension == ".dart") {
return new Future.value();
}
// If the asset is from a package's "lib" directory, we make it available
// as an input for transformers, but don't want it in the final output.
// (Any Dart code in there should be imported and compiled to JS, anything
// else we want to omit.)
if (asset.id.path.startsWith("lib/")) {
return new Future.value();
}
// Figure out the output directory for the asset, which is the same as the
// path pub serve would use to serve it.
var relativeUrl = barback.idtoUrlPath(entrypoint.root.name, asset.id,
useWebAsRoot: false);
// Remove the leading "/".
relativeUrl = relativeUrl.substring(1);
// If the asset is from the shared "assets" directory, copy it into all of
// the top-level build directories.
if (relativeUrl.startsWith("assets/")) {
builtFiles += buildDirectories.length;
return Future.wait(buildDirectories.map(
(buildDir) => _writeOutputFile(asset,
path.url.join(buildDir, relativeUrl))));
}
builtFiles++;
return _writeOutputFile(asset, relativeUrl);
}
/// Writes the contents of [asset] to [relativeUrl] within the build
/// directory.
Future _writeOutputFile(Asset asset, String relativeUrl) {
var relativePath = path.fromUri(new Uri(path: relativeUrl));
var destPath = path.join(target, relativePath);
ensureDir(path.dirname(destPath));
return createFileFromStream(asset.read(), destPath);
}
/// If this package depends directly on the `browser` package, this ensures
/// that the JavaScript bootstrap files are copied into `packages/browser/`
/// directories next to each entrypoint in [entrypoints].
///
/// Returns the number of files it copied.
int _copyBrowserJsFiles(Iterable<AssetId> entrypoints) {
// Must depend on the browser package.
if (!entrypoint.root.dependencies.any(
(dep) => dep.name == 'browser' && dep.source == 'hosted')) {
return 0;
}
// Get all of the directories that contain Dart entrypoints.
var entrypointDirs = entrypoints
.map((id) => path.url.split(id.path))
.map((relative) => path.dirname(path.joinAll(relative)))
.toSet();
for (var dir in entrypointDirs) {
// TODO(nweiz): we should put browser JS files next to any HTML file
// rather than any entrypoint. An HTML file could import an entrypoint
// that's not adjacent.
_addBrowserJs(dir, "dart");
_addBrowserJs(dir, "interop");
}
return entrypointDirs.length * 2;
}
// TODO(nweiz): do something more principled when issue 6101 is fixed.
/// Ensures that the [name].js file is copied into [directory] in [target],
/// under `packages/browser/`.
void _addBrowserJs(String directory, String name) {
var jsPath = path.join(
target, directory, 'packages', 'browser', '$name.js');
ensureDir(path.dirname(jsPath));
// TODO(rnystrom): This won't work if we get rid of symlinks and the top
// level "packages" directory. Will need to copy from the browser
// directory.
copyFile(path.join(entrypoint.packagesDir, 'browser', '$name.js'), jsPath);
}
}