diff --git a/.gitignore b/.gitignore index 0b3df5ff4..4a5e7f9b9 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,4 @@ build/ pubspec.lock # Include .packages files from tests which are hand coded -!test/package_graph/**/.packages +!test/fixtures/**/.packages diff --git a/e2e_example/.gitignore b/e2e_example/.gitignore new file mode 100644 index 000000000..fe688680b --- /dev/null +++ b/e2e_example/.gitignore @@ -0,0 +1 @@ +**/*.txt.copy diff --git a/lib/build.dart b/lib/build.dart index db8bc7c06..7dad2a564 100644 --- a/lib/build.dart +++ b/lib/build.dart @@ -3,6 +3,7 @@ // BSD-style license that can be found in the LICENSE file. export 'src/asset/asset.dart'; export 'src/asset/exceptions.dart'; +export 'src/asset/file_based.dart'; export 'src/asset/id.dart'; export 'src/asset/reader.dart'; export 'src/asset/writer.dart'; diff --git a/lib/src/asset/exceptions.dart b/lib/src/asset/exceptions.dart index 4051a1b68..9e1f8b43f 100644 --- a/lib/src/asset/exceptions.dart +++ b/lib/src/asset/exceptions.dart @@ -1,6 +1,7 @@ // 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.dart'; import 'id.dart'; class AssetNotFoundException implements Exception { @@ -11,3 +12,32 @@ class AssetNotFoundException implements Exception { @override String toString() => 'AssetNotFoundException: $assetId'; } + +class PackageNotFoundException implements Exception { + final String name; + + PackageNotFoundException(this.name); + + @override + String toString() => 'PackageNotFoundException: $name'; +} + +class InvalidOutputException implements Exception { + final Asset asset; + + InvalidOutputException(this.asset); + + @override + String toString() => 'InvalidOutputException: $asset\n' + 'Files may only be output in the root (application) package.'; +} + +class InvalidInputException implements Exception { + final AssetId assetId; + + InvalidInputException(this.assetId); + + @override + String toString() => 'InvalidInputException: $assetId\n' + 'For package dependencies, only files under `lib` may be used as inputs.'; +} diff --git a/lib/src/asset/file_based.dart b/lib/src/asset/file_based.dart new file mode 100644 index 000000000..a8a8a915d --- /dev/null +++ b/lib/src/asset/file_based.dart @@ -0,0 +1,77 @@ +// 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:path/path.dart' as path; + +import '../asset/asset.dart'; +import '../asset/exceptions.dart'; +import '../asset/id.dart'; +import '../asset/reader.dart'; +import '../asset/writer.dart'; +import '../package_graph/package_graph.dart'; +import 'exceptions.dart'; + +/// Basic [AssetReader] which uses a [PackageGraph] to look up where to read +/// files from disk. +class FileBasedAssetReader implements AssetReader { + final PackageGraph packageGraph; + + FileBasedAssetReader(this.packageGraph); + + @override + Future hasInput(AssetId id) async { + _checkInput(id); + return _fileFor(id, packageGraph).exists(); + } + + @override + Future readAsString(AssetId id, {Encoding encoding: UTF8}) async { + _checkInput(id); + + var file = await _fileFor(id, packageGraph); + if (!await file.exists()) { + throw new AssetNotFoundException(id); + } + return file.readAsString(encoding: encoding); + } + + /// Checks that [id] is a valid input, and throws an [InvalidInputException] + /// if its not. + void _checkInput(AssetId id) { + if (id.package != packageGraph.root.name && !id.path.startsWith('lib/')) { + throw new InvalidInputException(id); + } + } +} + +/// Basic [AssetWriter] which uses a [PackageGraph] to look up where to write +/// files to disk. +class FileBasedAssetWriter implements AssetWriter { + final PackageGraph packageGraph; + + FileBasedAssetWriter(this.packageGraph); + + @override + Future writeAsString(Asset asset, {Encoding encoding: UTF8}) async { + if (asset.id.package != packageGraph.root.name) { + throw new InvalidOutputException(asset); + } + + var file = _fileFor(asset.id, packageGraph); + await file.create(recursive: true); + await file.writeAsString(asset.stringContents, encoding: encoding); + } +} + +/// Returns a [File] for [id] given [packageGraph]. +File _fileFor(AssetId id, PackageGraph packageGraph) { + var package = packageGraph[id.package]; + if (package == null) { + throw new PackageNotFoundException(id.package); + } + return new File(path.join(package.location.toFilePath(), id.path)); +} diff --git a/lib/src/generate/build.dart b/lib/src/generate/build.dart index ebe78e916..dfc3f9157 100644 --- a/lib/src/generate/build.dart +++ b/lib/src/generate/build.dart @@ -2,33 +2,62 @@ // 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:path/path.dart' as path; import 'package:yaml/yaml.dart'; import '../asset/asset.dart'; +import '../asset/file_based.dart'; import '../asset/id.dart'; import '../asset/reader.dart'; import '../asset/writer.dart'; import '../builder/builder.dart'; import '../builder/build_step_impl.dart'; +import '../package_graph/package_graph.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 { +/// +/// A [packageGraph] may be supplied, otherwise one will be constructed using +/// [PackageGraph.forThisPackage]. The default functionality assumes you are +/// running in the root directory of a package, with both a `pubspec.yaml` and +/// `.packages` file present. +/// +/// A [reader] and [writer] may also be supplied, which can read/write assets +/// to arbitrary locations or file systems. By default they will write to the +/// current directory, and will use the `packageGraph` to know where to read +/// files from. +Future build(List> phaseGroups, + {PackageGraph packageGraph, AssetReader reader, AssetWriter writer}) async { + packageGraph ??= new PackageGraph.forThisPackage(); + reader ??= new FileBasedAssetReader(packageGraph); + writer ??= new FileBasedAssetWriter(packageGraph); + return runZoned(() { _validatePhases(phaseGroups); return _runPhases(phaseGroups); - } catch (e, s) { + }, onError: (e, s) { return new BuildResult(BuildStatus.Failure, BuildType.Full, [], exception: e, stackTrace: s); - } + }, zoneValues: { + _assetReaderKey: reader, + _assetWriterKey: writer, + _packageGraphKey: packageGraph, + }); } +/// Keys for reading zone local values. +Symbol _assetReaderKey = #buildAssetReader; +Symbol _assetWriterKey = #buildAssetWriter; +Symbol _packageGraphKey = #buildPackageGraph; + +/// Getters for zone local values. +AssetReader get _reader => Zone.current[_assetReaderKey]; +AssetWriter get _writer => Zone.current[_assetWriterKey]; +PackageGraph get _packageGraph => Zone.current[_packageGraphKey]; + /// The local package name from your pubspec. final String _localPackageName = () { var pubspec = new File('pubspec.yaml'); @@ -89,18 +118,15 @@ List _assetIdsFor(List inputSets) { /// 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(); + var root = _packageGraph[inputSet.package].location.toFilePath(); for (var glob in inputSet.globs) { - files.addAll(glob.listSync(followLinks: false).where( + files.addAll(glob.listSync(followLinks: false, root: root).where( (e) => e is File && !_ignoredDirs.contains(path.split(e.path)[1]))); } return files; } +const _ignoredDirs = const ['build']; /// Runs [builder] with [inputs] as inputs. Stream _runBuilder(Builder builder, List inputs) async* { @@ -116,43 +142,3 @@ Stream _runBuilder(Builder builder, List inputs) async* { } } } - -/// 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 hasInput(AssetId id) async { - assert(id.package == _localPackageName); - return new File(id.path).exists(); - } - - @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/test/asset/file_based_test.dart b/test/asset/file_based_test.dart new file mode 100644 index 000000000..b6dda3989 --- /dev/null +++ b/test/asset/file_based_test.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. +@TestOn('vm') +import 'dart:io'; + +import 'package:test/test.dart'; + +import 'package:build/build.dart'; + +import '../common/common.dart'; + +final packageGraph = new PackageGraph.forPath('test/fixtures/basic_pkg'); + +main() async { + + group('FileBasedAssetReader', () { + final reader = new FileBasedAssetReader(packageGraph); + + test('can read any application package files', () async { + expect(await reader.readAsString(makeAssetId('basic_pkg|hello.txt')), + 'world\n'); + expect(await reader.readAsString(makeAssetId('basic_pkg|lib/hello.txt')), + 'world\n'); + expect(await reader.readAsString(makeAssetId('basic_pkg|web/hello.txt')), + 'world\n'); + }); + + test('can only read package dependency files in the lib dir', () async { + expect(await reader.readAsString(makeAssetId('a|lib/a.txt')), 'A\n'); + expect(reader.readAsString(makeAssetId('a|web/a.txt')), + throwsA(invalidInputException)); + expect(reader.readAsString(makeAssetId('a|a.txt')), + throwsA(invalidInputException)); + }); + + test('can check for existence of any application package files', () async { + expect(await reader.hasInput(makeAssetId('basic_pkg|hello.txt')), isTrue); + expect(await reader.hasInput(makeAssetId('basic_pkg|lib/hello.txt')), + isTrue); + expect(await reader.hasInput(makeAssetId('basic_pkg|web/hello.txt')), + isTrue); + + expect(await reader.hasInput(makeAssetId('basic_pkg|a.txt')), isFalse); + expect( + await reader.hasInput(makeAssetId('basic_pkg|lib/a.txt')), isFalse); + }); + + test('can only check for existence of package dependency files in lib', + () async { + expect(await reader.hasInput(makeAssetId('a|lib/a.txt')), isTrue); + expect(await reader.hasInput(makeAssetId('a|lib/b.txt')), isFalse); + expect(reader.hasInput(makeAssetId('a|web/a.txt')), + throwsA(invalidInputException)); + expect(reader.hasInput(makeAssetId('a|a.txt')), + throwsA(invalidInputException)); + expect(reader.hasInput(makeAssetId('foo|bar.txt')), + throwsA(invalidInputException)); + }); + + test('throws when attempting to read a non-existent file', () async { + expect(reader.readAsString(makeAssetId('basic_pkg|foo.txt')), + throwsA(assetNotFoundException)); + expect(reader.readAsString(makeAssetId('a|lib/b.txt')), + throwsA(assetNotFoundException)); + expect(reader.readAsString(makeAssetId('foo|lib/bar.txt')), + throwsA(packageNotFoundException)); + }); + }); + + group('FileBasedAssetWriter', () { + final writer = new FileBasedAssetWriter(packageGraph); + + test('can output files in the application package', () async { + var asset = makeAsset('basic_pkg|test_file.txt', 'test'); + await writer.writeAsString(asset); + var id = asset.id; + var file = new File('test/fixtures/${id.package}/${id.path}'); + expect(await file.exists(), isTrue); + expect(await file.readAsString(), 'test'); + await file.delete(); + }); + + test('can\'t output files in package dependencies', () async { + var asset = makeAsset('a|test.txt'); + expect(writer.writeAsString(asset), throwsA(invalidOutputException)); + }); + + test('can\'t output files in arbitrary packages', () async { + var asset = makeAsset('foo|bar.txt'); + expect(writer.writeAsString(asset), throwsA(invalidOutputException)); + }); + }); +} diff --git a/test/common/assets.dart b/test/common/assets.dart new file mode 100644 index 000000000..498f4415b --- /dev/null +++ b/test/common/assets.dart @@ -0,0 +1,35 @@ +// 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 'in_memory_writer.dart'; + +int _nextId = 0; +AssetId makeAssetId([String assetIdString]) { + if (assetIdString == null) { + assetIdString = 'a|web/asset_$_nextId.txt'; + _nextId++; + } + return new AssetId.parse(assetIdString); +} + +Asset makeAsset([String assetIdString, String contents]) { + var id = makeAssetId(assetIdString); + return new Asset(id, contents ?? '$id'); +} + +Map makeAssets(Map assetsMap) { + var assets = {}; + assetsMap.forEach((idString, content) { + var asset = makeAsset(idString, content); + assets[asset.id] = asset; + }); + return assets; +} + +void addAssets(Iterable assets, InMemoryAssetWriter writer) { + for (var asset in assets) { + writer.assets[asset.id] = asset.stringContents; + } +} diff --git a/test/common/common.dart b/test/common/common.dart index 2f5f3fb09..780269815 100644 --- a/test/common/common.dart +++ b/test/common/common.dart @@ -1,43 +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 'in_memory_writer.dart'; - +export 'assets.dart'; export 'copy_builder.dart'; export 'file_combiner_builder.dart'; export 'in_memory_reader.dart'; export 'in_memory_writer.dart'; export 'generic_builder_transformer.dart'; +export 'matchers.dart'; export 'stub_reader.dart'; export 'stub_writer.dart'; - -int _nextId = 0; -AssetId makeAssetId([String assetIdString]) { - if (assetIdString == null) { - assetIdString = 'a|web/asset_$_nextId.txt'; - _nextId++; - } - return new AssetId.parse(assetIdString); -} - -Asset makeAsset([String assetIdString, String contents]) { - var id = makeAssetId(assetIdString); - return new Asset(id, contents ?? '$id'); -} - -Map makeAssets(Map assetsMap) { - var assets = {}; - assetsMap.forEach((idString, content) { - var asset = makeAsset(idString, content); - assets[asset.id] = asset; - }); - return assets; -} - -void addAssets(Iterable assets, InMemoryAssetWriter writer) { - for (var asset in assets) { - writer.assets[asset.id] = asset.stringContents; - } -} diff --git a/test/common/matchers.dart b/test/common/matchers.dart new file mode 100644 index 000000000..051ef455d --- /dev/null +++ b/test/common/matchers.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 'package:test/test.dart'; + +import 'package:build/build.dart'; + +final assetNotFoundException = new isInstanceOf(); +final invalidInputException = new isInstanceOf(); +final invalidOutputException = new isInstanceOf(); +final packageNotFoundException = new isInstanceOf(); diff --git a/test/package_graph/basic_pkg/.packages b/test/fixtures/basic_pkg/.packages similarity index 100% rename from test/package_graph/basic_pkg/.packages rename to test/fixtures/basic_pkg/.packages diff --git a/test/fixtures/basic_pkg/hello.txt b/test/fixtures/basic_pkg/hello.txt new file mode 100644 index 000000000..cc628ccd1 --- /dev/null +++ b/test/fixtures/basic_pkg/hello.txt @@ -0,0 +1 @@ +world diff --git a/test/fixtures/basic_pkg/lib/hello.txt b/test/fixtures/basic_pkg/lib/hello.txt new file mode 100644 index 000000000..cc628ccd1 --- /dev/null +++ b/test/fixtures/basic_pkg/lib/hello.txt @@ -0,0 +1 @@ +world diff --git a/test/fixtures/basic_pkg/pkg/a/a.txt b/test/fixtures/basic_pkg/pkg/a/a.txt new file mode 100644 index 000000000..f70f10e4d --- /dev/null +++ b/test/fixtures/basic_pkg/pkg/a/a.txt @@ -0,0 +1 @@ +A diff --git a/test/fixtures/basic_pkg/pkg/a/lib/a.txt b/test/fixtures/basic_pkg/pkg/a/lib/a.txt new file mode 100644 index 000000000..f70f10e4d --- /dev/null +++ b/test/fixtures/basic_pkg/pkg/a/lib/a.txt @@ -0,0 +1 @@ +A diff --git a/test/package_graph/basic_pkg/pkg/a/pubspec.yaml b/test/fixtures/basic_pkg/pkg/a/pubspec.yaml similarity index 100% rename from test/package_graph/basic_pkg/pkg/a/pubspec.yaml rename to test/fixtures/basic_pkg/pkg/a/pubspec.yaml diff --git a/test/fixtures/basic_pkg/pkg/a/web/a.txt b/test/fixtures/basic_pkg/pkg/a/web/a.txt new file mode 100644 index 000000000..f70f10e4d --- /dev/null +++ b/test/fixtures/basic_pkg/pkg/a/web/a.txt @@ -0,0 +1 @@ +A diff --git a/test/package_graph/basic_pkg/pkg/b/pubspec.yaml b/test/fixtures/basic_pkg/pkg/b/pubspec.yaml similarity index 100% rename from test/package_graph/basic_pkg/pkg/b/pubspec.yaml rename to test/fixtures/basic_pkg/pkg/b/pubspec.yaml diff --git a/test/package_graph/basic_pkg/pkg/c/pubspec.yaml b/test/fixtures/basic_pkg/pkg/c/pubspec.yaml similarity index 100% rename from test/package_graph/basic_pkg/pkg/c/pubspec.yaml rename to test/fixtures/basic_pkg/pkg/c/pubspec.yaml diff --git a/test/package_graph/basic_pkg/pubspec.yaml b/test/fixtures/basic_pkg/pubspec.yaml similarity index 100% rename from test/package_graph/basic_pkg/pubspec.yaml rename to test/fixtures/basic_pkg/pubspec.yaml diff --git a/test/fixtures/basic_pkg/web/hello.txt b/test/fixtures/basic_pkg/web/hello.txt new file mode 100644 index 000000000..cc628ccd1 --- /dev/null +++ b/test/fixtures/basic_pkg/web/hello.txt @@ -0,0 +1 @@ +world diff --git a/test/package_graph/no_packages_file/pubspec.yaml b/test/fixtures/no_packages_file/pubspec.yaml similarity index 100% rename from test/package_graph/no_packages_file/pubspec.yaml rename to test/fixtures/no_packages_file/pubspec.yaml diff --git a/test/package_graph/no_pubspec/.packages b/test/fixtures/no_pubspec/.packages similarity index 100% rename from test/package_graph/no_pubspec/.packages rename to test/fixtures/no_pubspec/.packages diff --git a/test/package_graph/package_graph_test.dart b/test/package_graph/package_graph_test.dart index 587dd2a29..cfa00534b 100644 --- a/test/package_graph/package_graph_test.dart +++ b/test/package_graph/package_graph_test.dart @@ -21,7 +21,7 @@ main() async { }); group('basic package ', () { - var basicPkgPath = 'test/package_graph/basic_pkg'; + var basicPkgPath = 'test/fixtures/basic_pkg'; setUp(() async { graph = await new PackageGraph.forPath(basicPkgPath); @@ -58,11 +58,12 @@ main() async { }); test('missing pubspec throws on create', () { - expect(() => new PackageGraph.forPath('no_pubspec'), throws); + expect(() => new PackageGraph.forPath('test/fixtures/no_pubspec'), throws); }); test('missing .packages file throws on create', () { - expect(() => new PackageGraph.forPath('no_packages_file'), throws); + expect(() => new PackageGraph.forPath('test/fixtures/no_packages_file'), + throws); }); }