diff --git a/.gitignore b/.gitignore index cd452c335..d16a6bff0 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ packages build/ .pub/ pubspec.lock + +generated diff --git a/e2e_example/lib/copy_builder.dart b/e2e_example/lib/copy_builder.dart new file mode 100644 index 000000000..632b88dd0 --- /dev/null +++ b/e2e_example/lib/copy_builder.dart @@ -0,0 +1,31 @@ +// Copyright (c) 2016, 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. +import 'dart:async'; + +import 'package:build/build.dart'; + +// Makes copies of things! +class CopyBuilder extends Builder { + @override + Future build(BuildStep buildStep) async { + var input = buildStep.input; + var copy = new Asset(_copiedAssetId(input.id), input.stringContents); + await buildStep.writeAsString(copy); + } + + @override + List declareOutputs(AssetId inputId) => [_copiedAssetId(inputId)]; + + /// Only runs on the root package, and copies all *.txt files. + static List buildPhases(PackageGraph graph) { + var phase = new Phase([ + new CopyBuilder() + ], [ + new InputSet(graph.root.name, filePatterns: ['**/*.txt']) + ]); + return [phase]; + } +} + +AssetId _copiedAssetId(AssetId inputId) => inputId.addExtension('.copy'); diff --git a/e2e_example/lib/transformer.dart b/e2e_example/lib/transformer.dart new file mode 100644 index 000000000..4b1422b05 --- /dev/null +++ b/e2e_example/lib/transformer.dart @@ -0,0 +1,12 @@ +// Copyright (c) 2016, 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. +import 'package:build/build.dart'; + +import 'copy_builder.dart'; + +class CopyTransformer extends BuilderTransformer { + final List builders = [new CopyBuilder()]; + + CopyTransformer.asPlugin(_); +} diff --git a/e2e_example/pubspec.yaml b/e2e_example/pubspec.yaml new file mode 100644 index 000000000..0d0d58121 --- /dev/null +++ b/e2e_example/pubspec.yaml @@ -0,0 +1,13 @@ +name: e2e_example +version: 0.1.0 + +environment: + sdk: '>=1.9.1 <2.0.0' + +dependencies: + build: + path: ../ + +transformers: +- e2e_example: + $include: 'web/*.txt' diff --git a/e2e_example/tool/build.dart b/e2e_example/tool/build.dart new file mode 100644 index 000000000..5200ed9f1 --- /dev/null +++ b/e2e_example/tool/build.dart @@ -0,0 +1,36 @@ +// Copyright (c) 2016, 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. +import 'package:build/build.dart'; + +import 'package:e2e_example/copy_builder.dart'; + +main() async { + /// Builds a full package dependency graph for the current package. + var graph = new PackageGraph.forThisPackage(); + + /// Give [Builder]s access to a [PackageGraph] so they can choose which + /// packages to run on. This simplifies user code a lot, and helps to mitigate + /// the transitive deps issue. + var phases = CopyBuilder.buildPhases(graph); + + var result = await build([phases]); + + if (result.status == BuildStatus.Success) { + print(''' +Build Succeeded! + +Type: ${result.buildType} +Outputs: ${result.outputs}'''); + } else { + print(''' +Build Failed :( + +Type: ${result.buildType} +Outputs: ${result.outputs} + +Exception: ${result.exception} +Stack Trace: +${result.stackTrace}'''); + } +} diff --git a/e2e_example/web/foo.txt b/e2e_example/web/foo.txt new file mode 100644 index 000000000..5716ca598 --- /dev/null +++ b/e2e_example/web/foo.txt @@ -0,0 +1 @@ +bar diff --git a/lib/build.dart b/lib/build.dart index 74430f8da..0e5b4c162 100644 --- a/lib/build.dart +++ b/lib/build.dart @@ -1,4 +1,13 @@ -// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// Copyright (c) 2016, 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 build; +export 'src/asset/asset.dart'; +export 'src/asset/id.dart'; +export 'src/builder/build_step.dart'; +export 'src/builder/builder.dart'; +export 'src/generate/build_result.dart'; +export 'src/generate/build.dart'; +export 'src/generate/input_set.dart'; +export 'src/generate/phase.dart'; +export 'src/package_graph/package_graph.dart'; +export 'src/transformer/transformer.dart'; diff --git a/lib/src/asset/asset.dart b/lib/src/asset/asset.dart new file mode 100644 index 000000000..21aafc688 --- /dev/null +++ b/lib/src/asset/asset.dart @@ -0,0 +1,17 @@ +// Copyright (c) 2016, 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. +import 'id.dart'; + +/// A fully realized asset whose content is available synchronously. +class Asset { + /// The id for this asset. + final AssetId id; + + /// The content for this asset. + final String stringContents; + + Asset(this.id, this.stringContents); + + String toString() => 'Asset: $id'; +} diff --git a/lib/src/asset/id.dart b/lib/src/asset/id.dart new file mode 100644 index 000000000..8d03c2c04 --- /dev/null +++ b/lib/src/asset/id.dart @@ -0,0 +1,110 @@ +// Copyright (c) 2016, 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. +import 'package:path/path.dart' as pathos; + +/// Forked from the `barback` package. +/// +/// Identifies an asset within a package. +class AssetId implements Comparable { + /// The name of the package containing this asset. + final String package; + + /// The path to the asset relative to the root directory of [package]. + /// + /// Source (i.e. read from disk) and generated (i.e. the output of a + /// [Transformer]) assets all have paths. Even intermediate assets that are + /// generated and then consumed by later transformations will still have + /// a path used to identify it. + /// + /// Asset paths always use forward slashes as path separators, regardless of + /// the host platform. + final String path; + + /// Gets the file extension of the asset, if it has one, including the ".". + String get extension => pathos.extension(path); + + /// Creates a new AssetId at [path] within [package]. + /// + /// The [path] will be normalized: any backslashes will be replaced with + /// forward slashes (regardless of host OS) and "." and ".." will be removed + /// where possible. + AssetId(this.package, String path) + : path = _normalizePath(path); + + /// Parses an [AssetId] string of the form "package|path/to/asset.txt". + /// + /// The [path] will be normalized: any backslashes will be replaced with + /// forward slashes (regardless of host OS) and "." and ".." will be removed + /// where possible. + factory AssetId.parse(String description) { + var parts = description.split("|"); + if (parts.length != 2) { + throw new FormatException('Could not parse "$description".'); + } + + if (parts[0].isEmpty) { + throw new FormatException( + 'Cannot have empty package name in "$description".'); + } + + if (parts[1].isEmpty) { + throw new FormatException( + 'Cannot have empty path in "$description".'); + } + + return new AssetId(parts[0], parts[1]); + } + + /// Deserializes an [AssetId] from [data], which must be the result of + /// calling [serialize] on an existing [AssetId]. + /// + /// Note that this is intended for communicating ids across isolates and not + /// for persistent storage of asset identifiers. There is no guarantee of + /// backwards compatibility in serialization form across versions. + AssetId.deserialize(data) + : package = data[0], + path = data[1]; + + /// Returns `true` of [other] is an [AssetId] with the same package and path. + operator ==(other) => + other is AssetId && + package == other.package && + path == other.path; + + int get hashCode => package.hashCode ^ path.hashCode; + + int compareTo(AssetId other) { + var packageComp = package.compareTo(other.package); + if (packageComp != 0) return packageComp; + return path.compareTo(other.path); + } + + /// Returns a new [AssetId] with the same [package] as this one and with the + /// [path] extended to include [extension]. + AssetId addExtension(String extension) => + new AssetId(package, "$path$extension"); + + /// Returns a new [AssetId] with the same [package] and [path] as this one + /// but with file extension [newExtension]. + AssetId changeExtension(String newExtension) => + new AssetId(package, pathos.withoutExtension(path) + newExtension); + + String toString() => "$package|$path"; + + /// Serializes this [AssetId] to an object that can be sent across isolates + /// and passed to [deserialize]. + serialize() => [package, path]; +} + +String _normalizePath(String path) { + if (pathos.isAbsolute(path)) { + throw new ArgumentError('Asset paths must be relative, but got "$path".'); + } + + // Normalize path separators so that they are always "/" in the AssetID. + path = path.replaceAll(r"\", "/"); + + // Collapse "." and "..". + return pathos.posix.normalize(path); +} diff --git a/lib/src/asset/reader.dart b/lib/src/asset/reader.dart new file mode 100644 index 000000000..011acfe7c --- /dev/null +++ b/lib/src/asset/reader.dart @@ -0,0 +1,11 @@ +// Copyright (c) 2016, 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. +import 'dart:async'; +import 'dart:convert'; + +import 'id.dart'; + +abstract class AssetReader { + Future readAsString(AssetId id, {Encoding encoding: UTF8}); +} diff --git a/lib/src/asset/writer.dart b/lib/src/asset/writer.dart new file mode 100644 index 000000000..733363085 --- /dev/null +++ b/lib/src/asset/writer.dart @@ -0,0 +1,11 @@ +// Copyright (c) 2016, 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. +import 'dart:async'; +import 'dart:convert'; + +import 'asset.dart'; + +abstract class AssetWriter { + Future writeAsString(Asset asset, {Encoding encoding: UTF8}); +} diff --git a/lib/src/builder/build_step.dart b/lib/src/builder/build_step.dart new file mode 100644 index 000000000..054219f7b --- /dev/null +++ b/lib/src/builder/build_step.dart @@ -0,0 +1,31 @@ +// Copyright (c) 2016, 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. +import 'dart:async'; +import 'dart:convert'; + +import '../asset/asset.dart'; +import '../asset/id.dart'; + +/// A single step in the build processes. This represents a single input and +/// it also handles tracking of dependencies. +class BuildStep { + /// The primary input for this build step. + final Asset input; + + /// Reads an [Asset] by [id] as a [String] using [encoding]. + /// + /// If [trackAsDependency] is true, then [id] will be marked as a dependency + /// of all [expectedOutputs]. + Future readAsString(AssetId id, + {Encoding encoding: UTF8, bool trackAsDependency: true}); + + /// Outputs an [Asset] using the current [AssetWriter], and adds [asset] to + /// [outputs]. + void writeAsString(Asset asset, {Encoding encoding: UTF8}); + + /// Explicitly adds [id] as a dependency of all [expectedOutputs]. This is + /// not generally necessary unless forcing `trackAsDependency: false` when + /// calling [readAsString]. + void addDependency(AssetId id); +} diff --git a/lib/src/builder/build_step_impl.dart b/lib/src/builder/build_step_impl.dart new file mode 100644 index 000000000..450181ce0 --- /dev/null +++ b/lib/src/builder/build_step_impl.dart @@ -0,0 +1,78 @@ +// Copyright (c) 2016, 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. +import 'dart:async'; +import 'dart:collection'; +import 'dart:convert'; + +import '../asset/asset.dart'; +import '../asset/id.dart'; +import '../asset/reader.dart'; +import '../asset/writer.dart'; +import 'build_step.dart'; + +/// A single step in the build processes. This represents a single input and +/// its expected and real outputs. It also handles tracking of dependencies. +class BuildStepImpl implements BuildStep { + /// The primary input for this build step. + @override + final Asset input; + + /// The list of all outputs which are expected/allowed to be output from this + /// step. + final List expectedOutputs; + + /// The actual outputs of this build step. + UnmodifiableListView get outputs => new UnmodifiableListView(_outputs); + final List _outputs = []; + + /// A future that completes once all outputs current are done writing. + Future get outputsCompleted => _outputsCompleted; + Future _outputsCompleted = new Future(() {}); + + /// The dependencies read in during this build step. + UnmodifiableListView get dependencies => + new UnmodifiableListView(_dependencies); + final Set _dependencies = new Set(); + + /// Used internally for reading files. + final AssetReader _reader; + + /// Used internally for writing files. + final AssetWriter _writer; + + BuildStepImpl( + this.input, List expectedOutputs, this._reader, this._writer) + : expectedOutputs = new List.unmodifiable(expectedOutputs) { + /// The [input] is always a dependency. + _dependencies.add(input.id); + } + + /// Reads an [Asset] by [id] as a [String] using [encoding]. + /// + /// If [trackAsDependency] is true, then [id] will be marked as a dependency + /// of all [expectedOutputs]. + @override + Future readAsString(AssetId id, + {Encoding encoding: UTF8, bool trackAsDependency: true}) { + if (trackAsDependency) _dependencies.add(id); + return _reader.readAsString(id, encoding: encoding); + } + + /// Outputs an [Asset] using the current [AssetWriter], and adds [asset] to + /// [outputs]. + @override + void writeAsString(Asset asset, {Encoding encoding: UTF8}) { + _outputs.add(asset); + var done = _writer.writeAsString(asset, encoding: encoding); + _outputsCompleted = _outputsCompleted.then((_) => done); + } + + /// Explicitly adds [id] as a dependency of all [expectedOutputs]. This is + /// not generally necessary unless forcing `trackAsDependency: false` when + /// calling [readAsString]. + @override + void addDependency(AssetId id) { + _dependencies.add(id); + } +} diff --git a/lib/src/builder/builder.dart b/lib/src/builder/builder.dart new file mode 100644 index 000000000..c99d09b9b --- /dev/null +++ b/lib/src/builder/builder.dart @@ -0,0 +1,21 @@ +// Copyright (c) 2016, 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. +import 'dart:async'; + +import '../asset/id.dart'; +import 'build_step.dart'; + +/// The basic builder class, used to build new files from existing ones. +abstract class Builder { + /// Generates the outputs for a given [BuildStep]. + Future build(BuildStep buildStep); + + /// Declares all potential output assets given [inputId]. If an empty [List] + /// is returned then the asset will never be provided to [build]. + /// + /// Note: Reading the contents of [inputId] is not supported during this + /// phase. You can however choose later on not to actually output assets which + /// you return here, but no other [Builder] will be able to output them. + List declareOutputs(AssetId inputId); +} diff --git a/lib/src/generate/build.dart b/lib/src/generate/build.dart new file mode 100644 index 000000000..5fc665252 --- /dev/null +++ b/lib/src/generate/build.dart @@ -0,0 +1,153 @@ +// Copyright (c) 2016, 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. +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:glob/glob.dart'; +import 'package:path/path.dart' as path; +import 'package:yaml/yaml.dart'; + +import '../asset/asset.dart'; +import '../asset/id.dart'; +import '../asset/reader.dart'; +import '../asset/writer.dart'; +import '../builder/builder.dart'; +import '../builder/build_step_impl.dart'; +import 'build_result.dart'; +import 'input_set.dart'; +import 'phase.dart'; + +/// Runs all of the [Phases] in [phaseGroups]. +Future build(List> phaseGroups) async { + try { + _validatePhases(phaseGroups); + return _runPhases(phaseGroups); + } catch (e, s) { + return new BuildResult(BuildStatus.Failure, BuildType.Full, [], + exception: e, stackTrace: s); + } +} + +/// The local package name from your pubspec. +final String _localPackageName = () { + var pubspec = new File('pubspec.yaml'); + if (!pubspec.existsSync()) { + throw 'Build scripts must be invoked from the top level package directory, ' + 'which must contain a pubspec.yaml'; + } + var yaml = loadYaml(pubspec.readAsStringSync()) as YamlMap; + if (yaml['name'] == null) { + throw 'You must have a `name` specified in your pubspec.yaml file.'; + } + return yaml['name']; +}(); + +/// Validates the phases. +void _validatePhases(List> phaseGroups) { + if (phaseGroups.length > 1) { + // Don't support using generated files as inputs yet, so we only support + // one phase. + throw new UnimplementedError( + 'Only one phase group is currently supported.'); + } +} + +/// Runs the [phaseGroups] and returns a [Future] which completes +/// once all [Phase]s are done. +Future _runPhases(List> phaseGroups) async { + var outputs = []; + for (var group in phaseGroups) { + for (var phase in group) { + var inputs = _assetIdsFor(phase.inputSets); + for (var builder in phase.builders) { + // TODO(jakemac): Optimize, we can run all the builders in a phase + // at the same time instead of sequentially. + await for (var output in _runBuilder(builder, inputs)) { + outputs.add(output); + } + } + } + } + return new BuildResult(BuildStatus.Success, BuildType.Full, outputs); +} + +/// Gets all [AssetId]s matching [inputSets] in the current package. +List _assetIdsFor(List inputSets) { + var ids = []; + for (var inputSet in inputSets) { + var files = _filesMatching(inputSet); + for (var file in files) { + var segments = file.uri.pathSegments; + var newPath = path.joinAll(segments.getRange( + segments.indexOf(inputSet.package) + 1, segments.length)); + ids.add(new AssetId(inputSet.package, newPath)); + } + } + return ids; +} + +/// Returns all files matching [inputSet]. +Set _filesMatching(InputSet inputSet) { + if (inputSet.package != _localPackageName) { + throw new UnimplementedError('Running on packages other than the ' + 'local package is not yet supported'); + } + + var files = new Set(); + for (var glob in inputSet.globs) { + files.addAll(glob.listSync(followLinks: false).where( + (e) => e is File && !_ignoredDirs.contains(path.split(e.path)[1]))); + } + return files; +} + +/// Runs [builder] with [inputs] as inputs. +Stream _runBuilder(Builder builder, List inputs) async* { + for (var input in inputs) { + var expectedOutputs = builder.declareOutputs(input); + var inputAsset = new Asset(input, await _reader.readAsString(input)); + var buildStep = + new BuildStepImpl(inputAsset, expectedOutputs, _reader, _writer); + await builder.build(buildStep); + await buildStep.outputsCompleted; + for (var output in buildStep.outputs) { + yield output; + } + } +} + +/// Very simple [AssetReader], only works on local package and assumes you are +/// running from the root of the package. +class _SimpleAssetReader implements AssetReader { + const _SimpleAssetReader(); + + @override + Future readAsString(AssetId id, {Encoding encoding: UTF8}) async { + assert(id.package == _localPackageName); + return new File(id.path).readAsString(encoding: encoding); + } +} + +const AssetReader _reader = const _SimpleAssetReader(); + +/// Very simple [AssetWriter], only works on local package and assumes you are +/// running from the root of the package. +class _SimpleAssetWriter implements AssetWriter { + final _outputDir; + + const _SimpleAssetWriter(this._outputDir); + + @override + Future writeAsString(Asset asset, {Encoding encoding: UTF8}) async { + assert(asset.id.package == _localPackageName); + var file = new File(path.join(_outputDir, asset.id.path)); + await file.create(recursive: true); + await file.writeAsString(asset.stringContents, encoding: encoding); + } +} + +const AssetWriter _writer = const _SimpleAssetWriter('generated'); + +const _ignoredDirs = const ['generated', 'build', 'packages']; diff --git a/lib/src/generate/build_result.dart b/lib/src/generate/build_result.dart new file mode 100644 index 000000000..2e33080cc --- /dev/null +++ b/lib/src/generate/build_result.dart @@ -0,0 +1,33 @@ +// Copyright (c) 2016, 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. +import '../asset/asset.dart'; + +/// The result of an individual build, this may be an incremental build or +/// a full build. +class BuildResult { + /// The status of this build. + final BuildStatus status; + + /// The [Exception] that was thrown during this build if it failed. + final Exception exception; + + /// The [StackTrace] for [exception] if non-null. + final StackTrace stackTrace; + + /// The type of this build. + final BuildType buildType; + + /// All outputs created/updated during this build. + final List outputs; + + BuildResult(this.status, this.buildType, List outputs, + {this.exception, this.stackTrace}) + : outputs = new List.unmodifiable(outputs); +} + +/// The status of a build. +enum BuildStatus { Success, Failure, } + +/// The type of a build. +enum BuildType { Incremental, Full } diff --git a/lib/src/generate/input_set.dart b/lib/src/generate/input_set.dart new file mode 100644 index 000000000..ba6e4691c --- /dev/null +++ b/lib/src/generate/input_set.dart @@ -0,0 +1,22 @@ +// Copyright (c) 2016, 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. +import 'package:glob/glob.dart'; + +/// Represents a set of files in a package which should be used as primary +/// inputs to a `Builder`. +class InputSet { + /// The package that the [globs] should be ran on. + final String package; + + /// The [Glob]s for files from [package] to use as inputs. + /// + /// Note: If the [package] is a package dependency, then only files under + /// `lib` will be available, but for the application package any files can be + /// listed. + final List globs; + + InputSet(this.package, {Iterable filePatterns}) + : this.globs = new List.unmodifiable( + filePatterns.map((pattern) => new Glob(pattern))); +} diff --git a/lib/src/generate/phase.dart b/lib/src/generate/phase.dart new file mode 100644 index 000000000..4f982e668 --- /dev/null +++ b/lib/src/generate/phase.dart @@ -0,0 +1,20 @@ +// Copyright (c) 2016, 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. +import '../builder/builder.dart'; +import 'input_set.dart'; + +/// A single phase in the build process. None of the [Builder]s in a single +/// phase should depend on any of the outputs of other [Builder]s in that same +/// phase. +class Phase { + /// The list of all [Builder]s that should be run on all [InputSet]s. + final List builders; + + /// The list of all [InputSet]s that should be used as primary inputs. + final List inputSets; + + Phase(List builders, List inputSets) + : builders = new List.unmodifiable(builders), + inputSets = new List.unmodifiable(inputSets); +} diff --git a/lib/src/package_graph/package_graph.dart b/lib/src/package_graph/package_graph.dart new file mode 100644 index 000000000..000c9722f --- /dev/null +++ b/lib/src/package_graph/package_graph.dart @@ -0,0 +1,163 @@ +// Copyright (c) 2016, 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. +import 'dart:io'; + +import 'package:yaml/yaml.dart'; + +/// A graph of the package dependencies for an application. +class PackageGraph { + /// The root application package. + final PackageNode root; + + /// All the package dependencies of [root], transitive and direct. + Set get allPackages { + var seen = new Set()..add(root); + + void addDeps(PackageNode package) { + for (var dep in package.dependencies) { + if (seen.contains(dep)) continue; + seen.add(dep); + addDeps(dep); + } + } + addDeps(root); + + return seen; + } + + PackageGraph._(this.root); + + /// Creates a [PackageGraph] for the package in which you are currently + /// running. + factory PackageGraph.forThisPackage() { + /// Read in the pubspec file and parse it as yaml. + var pubspec = new File('pubspec.yaml'); + if (!pubspec.existsSync()) { + throw 'Unable to generate package graph, no `pubspec.yaml` found.'; + } + var yaml = loadYaml(pubspec.readAsStringSync()); + + /// Read in the `.packages` file to get the locations of all packages. + var packagesFile = new File('.packages'); + if (!packagesFile.existsSync()) { + throw 'Unable to generate package graph, no `.packages` found.'; + } + var packageLocations = {}; + packagesFile.readAsLinesSync().skip(1).forEach((line) { + var firstColon = line.indexOf(':'); + var name = line.substring(0, firstColon); + var uriString = line.substring(firstColon + 1); + try { + packageLocations[name] = Uri.parse(uriString); + } on FormatException catch (_) { + /// Some types of deps don't have a scheme, and just point to a relative + /// path. + packageLocations[name] = new Uri.file(uriString); + } + }); + + /// Create all [PackageNode]s for all deps. + var nodes = {}; + PackageNode addNodeAndDeps(YamlMap yaml, PackageDependencyType type, + {bool isRoot: false}) { + var name = yaml['name']; + assert(!nodes.containsKey(name)); + var node = new PackageNode._(name, yaml['version'], type); + nodes[name] = node; + + _depsFromYaml(yaml, withOverrides: isRoot).forEach((name, source) { + var dep = nodes[name]; + if (dep == null) { + var pubspec = _pubspecForUri(packageLocations[name]); + dep = addNodeAndDeps(pubspec, _dependencyType(source)); + } + node._dependencies.add(dep); + }); + + return node; + } + + var root = addNodeAndDeps(yaml, PackageDependencyType.Path, isRoot: true); + return new PackageGraph._(root); + } + + String toString() { + var buffer = new StringBuffer(); + for (var package in allPackages) { + buffer.writeln('$package'); + } + return buffer.toString(); + } +} + +/// A node in a [PackageGraph]. +class PackageNode { + /// The name of the package as listed in the pubspec.yaml + final String name; + + /// The version of the package as listed in the pubspec.yaml + final String version; + + /// The type of dependency being used to pull in this package. + final PackageDependencyType dependencyType; + + /// All the packages that this package directly depends on. + final List _dependencies = []; + Iterable get dependencies => _dependencies.toList(); + + PackageNode._(this.name, this.version, this.dependencyType); + + String toString() => ''' + $name: + version: $version + type: $dependencyType + dependencies: [${dependencies.map((d) => d.name).join(', ')}]'''; +} + +/// The type of dependency being used. This dictates how the package should be +/// watched for changes. +enum PackageDependencyType { Pub, Github, Path, } + +PackageDependencyType _dependencyType(source) { + if (source is String) return PackageDependencyType.Pub; + + assert(source is YamlMap); + assert(source.keys.length == 1); + + var typeString = source.keys.first; + switch (typeString) { + case 'git': + return PackageDependencyType.Github; + case 'path': + return PackageDependencyType.Path; + default: + throw 'Unrecognized package dependency type `$typeString`'; + } +} + +/// Gets the deps from a yaml file, taking into account dependency_overrides. +Map _depsFromYaml(YamlMap yaml, {bool withOverrides: false}) { + var deps = new Map.from(yaml['dependencies'] as YamlMap ?? {}); + if (withOverrides) { + yaml['dependency_overrides']?.forEach((dep, source) { + deps[dep] = source; + }); + } + return deps; +} + +/// [uri] should be directly from a `.packages` file, and points to the `lib` +/// dir. +YamlMap _pubspecForUri(Uri uri) { + var libPath = uri.toFilePath(); + assert(libPath.endsWith('lib/')); + var pubspecPath = + libPath.replaceRange(libPath.length - 4, libPath.length, 'pubspec.yaml'); + + var pubspec = new File(pubspecPath); + if (!pubspec.existsSync()) { + throw 'Unable to generate package graph, no `$pubspecPath` found.'; + } + return loadYaml(pubspec.readAsStringSync()); +} diff --git a/lib/src/transformer/transformer.dart b/lib/src/transformer/transformer.dart new file mode 100644 index 000000000..3bd21d896 --- /dev/null +++ b/lib/src/transformer/transformer.dart @@ -0,0 +1,94 @@ +// Copyright (c) 2016, 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. +import 'dart:async'; +import 'dart:convert'; + +import 'package:barback/barback.dart' as barback show Asset, AssetId; +import 'package:barback/barback.dart' hide Asset, AssetId; + +import '../asset/asset.dart' as build; +import '../asset/id.dart' as build; +import '../asset/reader.dart'; +import '../asset/writer.dart'; +import '../builder/builder.dart'; +import '../builder/build_step_impl.dart'; + +abstract class BuilderTransformer implements Transformer, DeclaringTransformer { + List get builders; + + @override + String get allowedExtensions => null; + + @override + bool isPrimary(barback.AssetId id) => + _expectedOutputs(id, builders).isNotEmpty; + + @override + Future apply(Transform transform) async { + var input = await _toBuildAsset(transform.primaryInput); + var reader = new _TransformAssetReader(transform); + var writer = new _TransformAssetWriter(transform); + + var futures = []; + for (var builder in builders) { + var expected = _expectedOutputs(transform.primaryInput.id, [builder]); + if (expected.isEmpty) continue; + + var buildStep = new BuildStepImpl(input, expected, reader, writer); + futures.add(builder.build(buildStep)); + } + + await Future.wait(futures); + } + + @override + void declareOutputs(DeclaringTransform transform) { + for (var outputId in _expectedOutputs(transform.primaryId, builders)) { + transform.declareOutput(_toBarbackAssetId(outputId)); + } + } +} + +/// Very simple [AssetReader] which uses a [Transform]. +class _TransformAssetReader implements AssetReader { + final Transform transform; + + _TransformAssetReader(this.transform); + + @override + Future readAsString(build.AssetId id, {Encoding encoding: UTF8}) => + transform.readInputAsString(_toBarbackAssetId(id), encoding: encoding); +} + +/// Very simple [AssetWriter] which uses a [Transform]. +class _TransformAssetWriter implements AssetWriter { + final Transform transform; + + _TransformAssetWriter(this.transform); + + @override + Future writeAsString(build.Asset asset, {Encoding encoding: UTF8}) async => + transform.addOutput(_toBarbackAsset(asset)); +} + +/// All the expected outputs for [id] given [builders]. +Iterable _expectedOutputs( + barback.AssetId id, Iterable builders) sync* { + for (var builder in builders) { + yield* builder.declareOutputs(_toBuildAssetId(id)); + } +} + +barback.AssetId _toBarbackAssetId(build.AssetId id) => + new barback.AssetId(id.package, id.path); + +build.AssetId _toBuildAssetId(barback.AssetId id) => + new build.AssetId(id.package, id.path); + +barback.Asset _toBarbackAsset(build.Asset asset) => + new barback.Asset.fromString( + _toBarbackAssetId(asset.id), asset.stringContents); + +Future _toBuildAsset(barback.Asset asset) async => + new build.Asset(_toBuildAssetId(asset.id), await asset.readAsString()); diff --git a/pubspec.yaml b/pubspec.yaml index d9f3c2a70..4a908d986 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,5 +7,12 @@ homepage: https://github.com/dart-lang/build environment: sdk: '>=1.9.1 <2.0.0' +dependencies: + barback: ^0.15.0 + glob: ^1.1.0 + path: ^1.1.0 + stack_trace: ^1.6.0 + yaml: ^2.1.0 + dev_dependencies: test: ^0.12.0