blob: 2ef4a560294c9b2021700bcd4765b526eba8ce11 [file] [log] [blame]
// 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:path/path.dart' as p;
import 'package:yaml/yaml.dart';
import '../util/constants.dart';
/// The SDK package, we filter this to the core libs and dev compiler
/// resources.
final PackageNode _sdkPackageNode =
PackageNode(r'$sdk', sdkPath, DependencyType.hosted);
/// A graph of the package dependencies for an application.
class PackageGraph {
/// The root application package.
final PackageNode root;
/// All [PackageNode]s indexed by package name.
final Map<String, PackageNode> allPackages;
PackageGraph._(this.root, Map<String, PackageNode> allPackages)
: allPackages = Map.unmodifiable(
Map<String, PackageNode>.from(allPackages)
..putIfAbsent(r'$sdk', () => _sdkPackageNode)) {
if (!root.isRoot) {
throw ArgumentError('Root node must indicate `isRoot`');
}
if (allPackages.values.where((n) => n != root).any((n) => n.isRoot)) {
throw ArgumentError('No nodes other than the root may indicate `isRoot`');
}
}
/// Creates a [PackageGraph] given the [root] [PackageNode].
factory PackageGraph.fromRoot(PackageNode root) {
final allPackages = <String, PackageNode>{root.name: root};
void addDeps(PackageNode package) {
for (var dep in package.dependencies) {
if (allPackages.containsKey(dep.name)) continue;
allPackages[dep.name] = dep;
addDeps(dep);
}
}
addDeps(root);
return PackageGraph._(root, allPackages);
}
/// 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.
final pubspec = File(p.join(packagePath, 'pubspec.yaml'));
if (!pubspec.existsSync()) {
throw StateError(
'Unable to generate package graph, no `pubspec.yaml` found. '
'This program must be ran from the root directory of your package.');
}
final rootPubspec = _pubspecForPath(packagePath);
final rootPackageName = rootPubspec['name'] as String;
final packageLocations = _parsePackageLocations(packagePath)
..remove(rootPackageName);
final dependencyTypes = _parseDependencyTypes(packagePath);
final nodes = <String, PackageNode>{};
final rootNode = PackageNode(
rootPackageName, packagePath, DependencyType.path,
isRoot: true);
nodes[rootPackageName] = rootNode;
for (final packageName in packageLocations.keys) {
if (packageName == rootPackageName) continue;
nodes[packageName] = PackageNode(packageName,
packageLocations[packageName], dependencyTypes[packageName],
isRoot: false);
}
rootNode.dependencies
.addAll(_depsFromYaml(rootPubspec, isRoot: true).map((n) => nodes[n]));
final packageDependencies = _parsePackageDependencies(packageLocations);
for (final packageName in packageDependencies.keys) {
nodes[packageName]
.dependencies
.addAll(packageDependencies[packageName].map((n) => nodes[n]));
}
return PackageGraph._(rootNode, nodes);
}
/// Creates a [PackageGraph] for the package in which you are currently
/// running.
factory PackageGraph.forThisPackage() => PackageGraph.forPath('./');
/// Shorthand to get a package by name.
PackageNode operator [](String packageName) => allPackages[packageName];
@override
String toString() {
var buffer = StringBuffer();
for (var package in allPackages.values) {
buffer.writeln('$package');
}
return buffer.toString();
}
}
/// A node in a [PackageGraph].
class PackageNode {
/// The name of the package as listed in `pubspec.yaml`.
final String name;
/// The type of dependency being used to pull in this package.
///
/// May be `null`.
final DependencyType dependencyType;
/// All the packages that this package directly depends on.
final List<PackageNode> dependencies = [];
/// The absolute path of the current version of this package.
///
/// Paths are platform dependent.
final String path;
/// Whether this node is the [PackageGraph.root].
final bool isRoot;
PackageNode(this.name, String path, this.dependencyType, {bool isRoot})
: path = _toAbsolute(path),
isRoot = isRoot ?? false;
@override
String toString() => '''
$name:
type: $dependencyType
path: $path
dependencies: [${dependencies.map((d) => d.name).join(', ')}]''';
/// Converts [path] to a canonical absolute path, returns `null` if given
/// `null`.
static String _toAbsolute(String path) =>
(path == null) ? null : p.canonicalize(path);
}
/// Parse the `.packages` file and return a Map from package name to the file
/// location for that package.
Map<String, String> _parsePackageLocations(String rootPackagePath) {
var packagesFile = File(p.join(rootPackagePath, '.packages'));
if (!packagesFile.existsSync()) {
throw StateError('Unable to generate package graph, no `.packages` found. '
'This program must be ran from the root directory of your package.');
}
var packageLocations = <String, String>{};
for (final line in packagesFile.readAsLinesSync().skip(1)) {
var firstColon = line.indexOf(':');
var name = line.substring(0, firstColon);
assert(line.endsWith('lib/'));
// Start after package_name:, and strip out trailing 'lib/'.
var uriString = line.substring(firstColon + 1, line.length - 4);
// Strip the trailing slash, if present.
if (uriString.endsWith('/')) {
uriString = uriString.substring(0, uriString.length - 1);
}
var uri = Uri.tryParse(uriString) ?? Uri.file(uriString);
if (!uri.isAbsolute) {
uri = p.toUri(p.join(rootPackagePath, uri.path));
}
packageLocations[name] = uri.toFilePath(windows: Platform.isWindows);
}
return packageLocations;
}
/// The type of dependency being used. This dictates how the package should be
/// watched for changes.
enum DependencyType { github, path, hosted }
/// Parse the `pubspec.lock` file and return a Map from package name to the type
/// of dependency.
Map<String, DependencyType> _parseDependencyTypes(String rootPackagePath) {
final pubspecLock = File(p.join(rootPackagePath, 'pubspec.lock'));
if (!pubspecLock.existsSync()) {
throw StateError(
'Unable to generate package graph, no `pubspec.lock` found. '
'This program must be ran from the root directory of your package.');
}
final dependencyTypes = <String, DependencyType>{};
final dependencies = loadYaml(pubspecLock.readAsStringSync()) as YamlMap;
for (final packageName in dependencies['packages'].keys) {
final source = dependencies['packages'][packageName]['source'];
dependencyTypes[packageName as String] =
_dependencyTypeFromSource(source as String);
}
return dependencyTypes;
}
DependencyType _dependencyTypeFromSource(String source) {
switch (source) {
case 'git':
return DependencyType.github;
case 'hosted':
return DependencyType.hosted;
case 'path':
case 'sdk': // Until Flutter supports another type, assum same as path.
return DependencyType.path;
}
throw ArgumentError('Unable to determine dependency type:\n$source');
}
/// Read the pubspec for each package in [packageLocations] and finds it's
/// dependencies.
Map<String, Set<String>> _parsePackageDependencies(
Map<String, String> packageLocations) {
final dependencies = <String, Set<String>>{};
for (final packageName in packageLocations.keys) {
final pubspec = _pubspecForPath(packageLocations[packageName]);
dependencies[packageName] = _depsFromYaml(pubspec);
}
return dependencies;
}
/// Gets the deps from a yaml file, taking into account dependency_overrides.
Set<String> _depsFromYaml(YamlMap yaml, {bool isRoot = false}) {
var deps = <String>{}..addAll(_stringKeys(yaml['dependencies'] as Map));
if (isRoot) {
deps
..addAll(_stringKeys(yaml['dev_dependencies'] as Map))
..addAll(_stringKeys(yaml['dependency_overrides'] as Map));
}
return deps;
}
Iterable<String> _stringKeys(Map m) =>
m == null ? const [] : m.keys.cast<String>();
/// Should point to the top level directory for the package.
YamlMap _pubspecForPath(String absolutePath) {
var pubspecPath = p.join(absolutePath, 'pubspec.yaml');
var pubspec = File(pubspecPath);
if (!pubspec.existsSync()) {
throw StateError(
'Unable to generate package graph, no `$pubspecPath` found.');
}
return loadYaml(pubspec.readAsStringSync()) as YamlMap;
}