blob: 1a88922c2fc695bd85274821596ada401681426a [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/asset_environment.dart';
import '../exit_codes.dart' as exit_codes;
import '../io.dart';
import '../log.dart' as log;
import '../utils.dart';
import 'barback.dart';
final _arrow = getSpecial('\u2192', '=>');
/// Handles the `build` pub command.
class BuildCommand extends BarbackCommand {
String get name => "build";
String get description => "Apply transformers to build a package.";
String get invocation => "pub build [options] [directories...]";
String get docUrl => "http://dartlang.org/tools/pub/cmd/pub-build.html";
List<String> get aliases => const ["deploy", "settle-up"];
/// The path to the application's build output directory.
String get outputDirectory => argResults["output"];
List<String> get defaultSourceDirectories => ["web"];
/// The number of files that have been built and written to disc so far.
int builtFiles = 0;
BuildCommand() {
argParser.addOption("format",
help: "How output should be displayed.",
allowed: ["text", "json"], defaultsTo: "text");
argParser.addOption("output", abbr: "o",
help: "Directory to write build outputs to.",
defaultsTo: "build");
}
Future onRunTransformerCommand() async {
cleanDir(outputDirectory);
var errorsJson = [];
var logJson = [];
// 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 AssetEnvironment.create(entrypoint, mode, useDart2JS: true)
.then((environment) {
// Show in-progress errors, but not results. Those get handled
// implicitly by getAllAssets().
environment.barback.errors.listen((error) {
log.error(log.red("Build error:\n$error"));
if (log.json.enabled) {
// Wrap the error in a map in case we end up decorating it with
// more properties later.
errorsJson.add({
"error": error.toString()
});
}
});
// If we're using JSON output, the regular server logging is disabled.
// Instead, we collect it here to include in the final JSON result.
if (log.json.enabled) {
environment.barback.log.listen(
(entry) => logJson.add(_logEntryToJson(entry)));
}
return log.progress("Building ${entrypoint.root.name}", () {
// Register all of the build directories.
// TODO(rnystrom): We don't actually need to bind servers for these, we
// just need to add them to barback's sources. Add support to
// BuildEnvironment for going the latter without the former.
return Future.wait(sourceDirectories.map(
(dir) => environment.serveDirectory(dir))).then((_) {
return environment.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((_) {
return _copyBrowserJsFiles(dart2JSEntrypoints, assets);
}).then((_) {
log.message('Built $builtFiles ${pluralize('file', builtFiles)} '
'to "$outputDirectory".');
log.json.message({
"buildResult": "success",
"outputDirectory": outputDirectory,
"numFiles": builtFiles,
"log": logJson
});
});
});
}).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."));
log.json.message({
"buildResult": "failure",
"errors": errorsJson,
"log": logJson
});
return flushThenExit(exit_codes.DATA);
});
}
/// Writes [asset] to the appropriate build directory.
///
/// If [asset] is in the special "packages" directory, writes it to every
/// build directory.
Future _writeAsset(Asset asset) async {
// 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 null;
}
var destPath = _idToPath(asset.id);
// If the asset is from a public directory, copy it into all of the
// top-level build directories.
if (path.isWithin("packages", destPath)) {
return Future.wait(sourceDirectories.map((buildDir) =>
_writeOutputFile(asset, path.join(buildDir, destPath))));
}
return _writeOutputFile(asset, destPath);
}
/// Converts [id] to a relative path in the output directory for that asset.
///
/// This corresponds to the URL that could be used to request that asset from
/// pub serve.
///
/// Examples (where entrypoint is "myapp"):
///
/// myapp|web/index.html -> web/index.html
/// myapp|lib/lib.dart -> packages/myapp/lib.dart
/// foo|lib/foo.dart -> packages/foo/foo.dart
/// myapp|test/main.dart -> test/main.dart
/// foo|test/main.dart -> ERROR
///
/// Throws a [FormatException] if [id] is not a valid public asset.
String _idToPath(AssetId id) {
var parts = path.split(path.fromUri(id.path));
if (parts.length < 2) {
throw new FormatException(
"Can not build assets from top-level directory.");
}
// Map "lib" to the "packages" directory.
if (parts[0] == "lib") {
return path.join("packages", id.package, path.joinAll(parts.skip(1)));
}
// Shouldn't be trying to access non-public directories of other packages.
assert(id.package == entrypoint.root.name);
// Allow any path in the entrypoint package.
return path.joinAll(parts);
}
/// Writes the contents of [asset] to [relativePath] within the build
/// directory.
Future _writeOutputFile(Asset asset, String relativePath) {
builtFiles++;
var destPath = path.join(outputDirectory, 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].
Future _copyBrowserJsFiles(Iterable<AssetId> entrypoints, AssetSet assets) {
// Must depend on the browser package.
if (!entrypoint.root.immediateDependencies.any(
(dep) => dep.name == 'browser' && dep.source == 'hosted')) {
return new Future.value();
}
// Get all of the subdirectories that contain Dart entrypoints.
var entrypointDirs = entrypoints
// Convert the asset path to a native-separated one and get the
// directory containing the entrypoint.
.map((id) => path.dirname(path.fromUri(id.path)))
// Don't copy files to the top levels of the build directories since
// the normal lib asset copying will take care of that.
.where((dir) => path.split(dir).length > 1)
.toSet();
var jsAssets = assets.where((asset) =>
asset.id.package == 'browser' && asset.id.extension == '.js');
return Future.wait(entrypointDirs.expand((dir) {
// 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.
return jsAssets.map((asset) {
var jsPath = path.join(dir, _idToPath(asset.id));
return _writeOutputFile(asset, jsPath);
});
}));
}
/// Converts [entry] to a JSON object for use with JSON-formatted output.
Map _logEntryToJson(LogEntry entry) {
var data = {
"level": entry.level.name,
"transformer": {
"name": entry.transform.transformer.toString(),
"primaryInput": {
"package": entry.transform.primaryId.package,
"path": entry.transform.primaryId.path
},
},
"assetId": {
"package": entry.assetId.package,
"path": entry.assetId.path
},
"message": entry.message
};
if (entry.span != null) {
data["span"] = {
"url": entry.span.sourceUrl,
"start": {
"line": entry.span.start.line,
"column": entry.span.start.column
},
"end": {
"line": entry.span.end.line,
"column": entry.span.end.column
},
};
}
return data;
}
}