| // 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); |
| } |
| } |