diff --git a/.gitignore b/.gitignore index d16a6bff0..0b3df5ff4 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ build/ .pub/ pubspec.lock -generated +# Include .packages files from tests which are hand coded +!test/package_graph/**/.packages diff --git a/lib/src/package_graph/package_graph.dart b/lib/src/package_graph/package_graph.dart index 000c9722f..b5b4162c5 100644 --- a/lib/src/package_graph/package_graph.dart +++ b/lib/src/package_graph/package_graph.dart @@ -3,6 +3,7 @@ // BSD-style license that can be found in the LICENSE file. import 'dart:io'; +import 'package:path/path.dart' as path; import 'package:yaml/yaml.dart'; /// A graph of the package dependencies for an application. @@ -10,67 +11,68 @@ 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); + /// All [PackageNodes] indexed by package name. + final Map allPackages; - 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, Map allPackages) + : allPackages = new Map.unmodifiable(allPackages); - PackageGraph._(this.root); - - /// Creates a [PackageGraph] for the package in which you are currently - /// running. - factory PackageGraph.forThisPackage() { + /// Creates a [PackageGraph] for the package whose top level directory lives + /// at [packagePath] (no trailing slash). + factory PackageGraph.forPath(String packagePath) { /// Read in the pubspec file and parse it as yaml. - var pubspec = new File('pubspec.yaml'); + var pubspec = new File(path.join(packagePath, 'pubspec.yaml')); if (!pubspec.existsSync()) { - throw 'Unable to generate package graph, no `pubspec.yaml` found.'; + throw 'Unable to generate package graph, no `pubspec.yaml` found. ' + 'This program must be ran from the root directory of your package.'; } - var yaml = loadYaml(pubspec.readAsStringSync()); + var rootYaml = loadYaml(pubspec.readAsStringSync()); /// Read in the `.packages` file to get the locations of all packages. - var packagesFile = new File('.packages'); + var packagesFile = new File(path.join(packagePath, '.packages')); if (!packagesFile.existsSync()) { - throw 'Unable to generate package graph, no `.packages` found.'; + throw 'Unable to generate package graph, no `.packages` found. ' + 'This program must be ran from the root directory of your package.'; } var packageLocations = {}; packagesFile.readAsLinesSync().skip(1).forEach((line) { var firstColon = line.indexOf(':'); var name = line.substring(0, firstColon); - var uriString = line.substring(firstColon + 1); + assert(line.endsWith('lib${Platform.pathSeparator}')); + // Start after package_name:, and strip out trailing `lib` dir. + var uriString = line.substring(firstColon + 1, line.length - 4); + var uri; try { - packageLocations[name] = Uri.parse(uriString); + uri = 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); + uri = new Uri.file(uriString); } + if (!uri.isAbsolute) { + uri = new Uri.file(path.join(packagePath, uri.path)); + } + packageLocations[name] = uri; }); /// Create all [PackageNode]s for all deps. var nodes = {}; + Map rootDeps; 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); + var node = new PackageNode._( + name, yaml['version'], type, packageLocations[name]); nodes[name] = node; - _depsFromYaml(yaml, withOverrides: isRoot).forEach((name, source) { + var deps = _depsFromYaml(yaml, withOverrides: isRoot); + if (isRoot) rootDeps = deps; + deps.forEach((name, source) { var dep = nodes[name]; if (dep == null) { var pubspec = _pubspecForUri(packageLocations[name]); - dep = addNodeAndDeps(pubspec, _dependencyType(source)); + dep = addNodeAndDeps(pubspec, _dependencyType(rootDeps[name])); } node._dependencies.add(dep); }); @@ -78,13 +80,21 @@ class PackageGraph { return node; } - var root = addNodeAndDeps(yaml, PackageDependencyType.Path, isRoot: true); - return new PackageGraph._(root); + var root = + addNodeAndDeps(rootYaml, PackageDependencyType.Path, isRoot: true); + return new PackageGraph._(root, nodes); } + /// Creates a [PackageGraph] for the package in which you are currently + /// running. + factory PackageGraph.forThisPackage() => new PackageGraph.forPath('.'); + + /// Shorthand to get a package by name. + PackageNode operator [](String packageName) => allPackages[packageName]; + String toString() { var buffer = new StringBuffer(); - for (var package in allPackages) { + for (var package in allPackages.values) { buffer.writeln('$package'); } return buffer.toString(); @@ -106,12 +116,16 @@ class PackageNode { final List _dependencies = []; Iterable get dependencies => _dependencies.toList(); - PackageNode._(this.name, this.version, this.dependencyType); + /// The location of the current version of this package. + final Uri location; + + PackageNode._(this.name, this.version, this.dependencyType, this.location); String toString() => ''' $name: version: $version type: $dependencyType + location: $location dependencies: [${dependencies.map((d) => d.name).join(', ')}]'''; } @@ -120,7 +134,7 @@ class PackageNode { enum PackageDependencyType { Pub, Github, Path, } PackageDependencyType _dependencyType(source) { - if (source is String) return PackageDependencyType.Pub; + if (source is String || source == null) return PackageDependencyType.Pub; assert(source is YamlMap); assert(source.keys.length == 1); @@ -147,14 +161,9 @@ Map _depsFromYaml(YamlMap yaml, {bool withOverrides: false}) { return deps; } -/// [uri] should be directly from a `.packages` file, and points to the `lib` -/// dir. +/// [uri] should point to the top level directory for the package. YamlMap _pubspecForUri(Uri uri) { - var libPath = uri.toFilePath(); - assert(libPath.endsWith('lib/')); - var pubspecPath = - libPath.replaceRange(libPath.length - 4, libPath.length, 'pubspec.yaml'); - + var pubspecPath = path.join(uri.toFilePath(), 'pubspec.yaml'); var pubspec = new File(pubspecPath); if (!pubspec.existsSync()) { throw 'Unable to generate package graph, no `$pubspecPath` found.'; diff --git a/test/package_graph/basic_pkg/.packages b/test/package_graph/basic_pkg/.packages new file mode 100644 index 000000000..5383d3dd2 --- /dev/null +++ b/test/package_graph/basic_pkg/.packages @@ -0,0 +1,6 @@ +# Generated by hand on 2016-02-01 +a:pkg/a/lib/ +b:pkg/b/lib/ +c:pkg/c/lib/ +d:pkg/d/lib/ +basic_pkg:lib/ diff --git a/test/package_graph/basic_pkg/pkg/a/pubspec.yaml b/test/package_graph/basic_pkg/pkg/a/pubspec.yaml new file mode 100644 index 000000000..9bd7f49a6 --- /dev/null +++ b/test/package_graph/basic_pkg/pkg/a/pubspec.yaml @@ -0,0 +1,5 @@ +name: a +version: 2.0.0 +dependencies: + b: ^3.0.0 + c: any diff --git a/test/package_graph/basic_pkg/pkg/b/pubspec.yaml b/test/package_graph/basic_pkg/pkg/b/pubspec.yaml new file mode 100644 index 000000000..c90332929 --- /dev/null +++ b/test/package_graph/basic_pkg/pkg/b/pubspec.yaml @@ -0,0 +1,4 @@ +name: b +version: 3.0.0 +dependencies: + c: ^0.4.0 diff --git a/test/package_graph/basic_pkg/pkg/c/pubspec.yaml b/test/package_graph/basic_pkg/pkg/c/pubspec.yaml new file mode 100644 index 000000000..fc2181079 --- /dev/null +++ b/test/package_graph/basic_pkg/pkg/c/pubspec.yaml @@ -0,0 +1,4 @@ +name: c +version: 4.0.0 +dependencies: + basic_pkg: ^1.0.0 diff --git a/test/package_graph/basic_pkg/pubspec.yaml b/test/package_graph/basic_pkg/pubspec.yaml new file mode 100644 index 000000000..9749cb143 --- /dev/null +++ b/test/package_graph/basic_pkg/pubspec.yaml @@ -0,0 +1,8 @@ +name: basic_pkg +version: 1.0.0 +dependencies: + a: 2.0.0 + b: + git: git://github.com/b/b.git + c: + path: pkg/c diff --git a/test/package_graph/no_packages_file/pubspec.yaml b/test/package_graph/no_packages_file/pubspec.yaml new file mode 100644 index 000000000..720ce503a --- /dev/null +++ b/test/package_graph/no_packages_file/pubspec.yaml @@ -0,0 +1,2 @@ +name: a +version: 1.0.0 diff --git a/test/package_graph/no_pubspec/.packages b/test/package_graph/no_pubspec/.packages new file mode 100644 index 000000000..3fc66548b --- /dev/null +++ b/test/package_graph/no_pubspec/.packages @@ -0,0 +1 @@ +# Created by hand, but empty diff --git a/test/package_graph/package_graph_test.dart b/test/package_graph/package_graph_test.dart new file mode 100644 index 000000000..587dd2a29 --- /dev/null +++ b/test/package_graph/package_graph_test.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. +@TestOn('vm') +import 'package:test/test.dart'; + +import 'package:build/build.dart'; + +main() async { + PackageGraph graph; + + group('forThisPackage ', () { + setUp(() async { + graph = await new PackageGraph.forThisPackage(); + }); + + test('root', () { + expectPkg( + graph.root, 'build', isNotEmpty, PackageDependencyType.Path, './'); + }); + }); + + group('basic package ', () { + var basicPkgPath = 'test/package_graph/basic_pkg'; + + setUp(() async { + graph = await new PackageGraph.forPath(basicPkgPath); + }); + + test('allPackages', () { + expect(graph.allPackages, { + 'a': graph['a'], + 'b': graph['b'], + 'c': graph['c'], + 'basic_pkg': graph['basic_pkg'], + }); + }); + + test('root', () { + expectPkg(graph.root, 'basic_pkg', '1.0.0', PackageDependencyType.Path, + basicPkgPath, [graph['a'], graph['b'], graph['c']]); + }); + + test('pub dependency', () { + expectPkg(graph['a'], 'a', '2.0.0', PackageDependencyType.Pub, + '$basicPkgPath/pkg/a/', [graph['b'], graph['c']]); + }); + + test('git dependency', () { + expectPkg(graph['b'], 'b', '3.0.0', PackageDependencyType.Github, + '$basicPkgPath/pkg/b/', [graph['c']]); + }); + + test('path dependency', () { + expectPkg(graph['c'], 'c', '4.0.0', PackageDependencyType.Path, + '$basicPkgPath/pkg/c/', [graph['basic_pkg']]); + }); + }); + + test('missing pubspec throws on create', () { + expect(() => new PackageGraph.forPath('no_pubspec'), throws); + }); + + test('missing .packages file throws on create', () { + expect(() => new PackageGraph.forPath('no_packages_file'), throws); + }); +} + +void expectPkg(PackageNode node, name, version, type, location, + [Iterable dependencies]) { + expect(node.name, name); + expect(node.version, version); + expect(node.dependencyType, type); + expect(node.location.path, location); + if (dependencies != null) { + expect(node.dependencies, unorderedEquals(dependencies)); + } +}