blob: 476cace463a3974538c3ef909ce5b47f41669307 [file] [log] [blame]
// Copyright (c) 2018, 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';
import 'package:graphs/graphs.dart';
import 'package:path/path.dart' as p;
import 'package:json_annotation/json_annotation.dart';
import 'common.dart';
import 'module_library.dart';
import 'modules.dart';
import 'platform.dart';
part 'meta_module.g.dart';
/// Returns the top level directory in [path].
///
/// Throws an [ArgumentError] if [path] is just a filename with no directory.
String _topLevelDir(String path) {
var parts = p.url.split(p.url.normalize(path));
String error;
if (parts.length == 1) {
error = 'The path `$path` does not contain a directory.';
} else if (parts.first == '..') {
error = 'The path `$path` reaches outside the root directory.';
}
if (error != null) {
throw ArgumentError(
'Cannot compute top level dir for path `$path`. $error');
}
return parts.first;
}
/// Creates a module containing [componentLibraries].
Module _moduleForComponent(
List<ModuleLibrary> componentLibraries, DartPlatform platform) {
// Name components based on first alphabetically sorted node, preferring
// public srcs (not under lib/src).
var sources = componentLibraries.map((n) => n.id).toSet();
var nonSrcIds = sources.where((id) => !id.path.startsWith('lib/src/'));
var primaryId =
nonSrcIds.isNotEmpty ? nonSrcIds.reduce(_min) : sources.reduce(_min);
// Expand to include all the part files of each node, these aren't
// included as individual `_AssetNodes`s in `connectedComponents`.
sources.addAll(componentLibraries.expand((n) => n.parts));
var directDependencies = <AssetId>{}
..addAll(componentLibraries.expand((n) => n.depsForPlatform(platform)))
..removeAll(sources);
var isSupported = componentLibraries
.expand((l) => l.sdkDeps)
.every(platform.supportsLibrary);
return Module(primaryId, sources, directDependencies, platform, isSupported);
}
/// Gets the local (same top level dir of the same package) transitive deps of
/// [module] using [assetsToModules].
Set<AssetId> _localTransitiveDeps(
Module module, Map<AssetId, Module> assetsToModules) {
var localTransitiveDeps = <AssetId>{};
var nextIds = module.directDependencies;
var seenIds = <AssetId>{};
while (nextIds.isNotEmpty) {
var ids = nextIds;
seenIds.addAll(ids);
nextIds = <AssetId>{};
for (var id in ids) {
var module = assetsToModules[id];
if (module == null) continue; // Skip non-local modules
if (localTransitiveDeps.add(module.primarySource)) {
nextIds.addAll(module.directDependencies.difference(seenIds));
}
}
}
return localTransitiveDeps;
}
/// Creates a map of modules to the entrypoint modules that transitively
/// depend on those modules.
Map<AssetId, Set<AssetId>> _findReverseEntrypointDeps(
Iterable<Module> entrypointModules, Iterable<Module> modules) {
var reverseDeps = <AssetId, Set<AssetId>>{};
var assetsToModules = <AssetId, Module>{};
for (var module in modules) {
for (var assetId in module.sources) {
assetsToModules[assetId] = module;
}
}
for (var module in entrypointModules) {
for (var moduleDep in _localTransitiveDeps(module, assetsToModules)) {
reverseDeps
.putIfAbsent(moduleDep, () => <AssetId>{})
.add(module.primarySource);
}
}
return reverseDeps;
}
/// Merges [modules] into a minimum set of [Module]s using the
/// following rules:
///
/// * If it is an entrypoint module do not merge it.
/// * If it is not depended on my any entrypoint do not merge it.
/// * If it is depended on by no entrypoint merge it into the entrypoint
/// modules
/// * Else merge it into with others that are depended on by the same set of
/// entrypoints
List<Module> _mergeModules(Iterable<Module> modules, Set<AssetId> entrypoints) {
var entrypointModules =
modules.where((m) => m.sources.any(entrypoints.contains)).toList();
// Groups of modules that can be merged into an existing entrypoint module.
var entrypointModuleGroups = {
for (var m in entrypointModules) m.primarySource: [m],
};
// Maps modules to entrypoint modules that transitively depend on them.
var modulesToEntryPoints =
_findReverseEntrypointDeps(entrypointModules, modules);
// Modules which are not depended on by any entrypoint
var standaloneModules = <Module>[];
// Modules which are merged with others.
var mergedModules = <String, List<Module>>{};
for (var module in modules) {
// Skip entrypoint modules.
if (entrypointModuleGroups.containsKey(module.primarySource)) continue;
// The entry points that transitively import this module.
var entrypointIds = modulesToEntryPoints[module.primarySource];
// If no entrypoint imports the module, just leave it alone.
if (entrypointIds == null || entrypointIds.isEmpty) {
standaloneModules.add(module);
continue;
}
// If there are multiple entry points for a given resource we must create
// a new shared module. Use `$` to signal that it is a shared module.
if (entrypointIds.length > 1) {
var mId = (entrypointIds.toList()..sort()).map((m) => m.path).join('\$');
mergedModules.putIfAbsent(mId, () => []).add(module);
} else {
entrypointModuleGroups[entrypointIds.single].add(module);
}
}
return mergedModules.values
.map(Module.merge)
.map(_withConsistentPrimarySource)
.followedBy(entrypointModuleGroups.values.map(Module.merge))
.followedBy(standaloneModules)
.toList();
}
Module _withConsistentPrimarySource(Module m) => Module(m.sources.reduce(_min),
m.sources, m.directDependencies, m.platform, m.isSupported);
T _min<T extends Comparable<T>>(T a, T b) => a.compareTo(b) < 0 ? a : b;
/// Compute modules for the internal strongly connected components of
/// [libraries].
///
/// This should only be called with [libraries] all in the same package and top
/// level directory within the package.
///
/// A dependency is considered "internal" if it is within [libraries]. Any
/// "external" deps are ignored during this computation since we are only
/// considering the strongly connected components within [libraries], but they
/// will be maintained as a dependency of the module to be used at a later step.
///
/// Part files are also tracked but ignored during computation of strongly
/// connected components, as they must always be a part of the containing
/// library's module.
List<Module> _computeModules(
Map<AssetId, ModuleLibrary> libraries, DartPlatform platform) {
assert(() {
var dir = _topLevelDir(libraries.values.first.id.path);
return libraries.values.every((l) => _topLevelDir(l.id.path) == dir);
}());
final connectedComponents = stronglyConnectedComponents<ModuleLibrary>(
libraries.values,
(n) => n
.depsForPlatform(platform)
// Only "internal" dependencies
.where(libraries.containsKey)
.map((dep) => libraries[dep]),
equals: (a, b) => a.id == b.id,
hashCode: (l) => l.id.hashCode);
final entryIds =
libraries.values.where((l) => l.isEntryPoint).map((l) => l.id).toSet();
return _mergeModules(
connectedComponents.map((c) => _moduleForComponent(c, platform)),
entryIds);
}
@JsonSerializable()
class MetaModule {
@JsonKey(name: 'm', nullable: false)
final List<Module> modules;
MetaModule(List<Module> modules) : modules = List.unmodifiable(modules);
/// Generated factory constructor.
factory MetaModule.fromJson(Map<String, dynamic> json) =>
_$MetaModuleFromJson(json);
Map<String, dynamic> toJson() => _$MetaModuleToJson(this);
static Future<MetaModule> forLibraries(
AssetReader reader,
List<AssetId> libraryIds,
ModuleStrategy strategy,
DartPlatform platform) async {
var libraries = <ModuleLibrary>[];
for (var id in libraryIds) {
libraries.add(ModuleLibrary.deserialize(
id.changeExtension('').changeExtension('.dart'),
await reader.readAsString(id)));
}
switch (strategy) {
case ModuleStrategy.fine:
return _fineModulesForLibraries(reader, libraries, platform);
case ModuleStrategy.coarse:
return _coarseModulesForLibraries(reader, libraries, platform);
}
throw StateError('Unrecognized module strategy $strategy');
}
}
MetaModule _coarseModulesForLibraries(
AssetReader reader, List<ModuleLibrary> libraries, DartPlatform platform) {
var librariesByDirectory = <String, Map<AssetId, ModuleLibrary>>{};
for (var library in libraries) {
final dir = _topLevelDir(library.id.path);
if (!librariesByDirectory.containsKey(dir)) {
librariesByDirectory[dir] = <AssetId, ModuleLibrary>{};
}
librariesByDirectory[dir][library.id] = library;
}
final modules = librariesByDirectory.values
.expand((libs) => _computeModules(libs, platform))
.toList();
_sortModules(modules);
return MetaModule(modules);
}
MetaModule _fineModulesForLibraries(
AssetReader reader, List<ModuleLibrary> libraries, DartPlatform platform) {
var modules = libraries
.map((library) => Module(
library.id,
library.parts.followedBy([library.id]),
library.depsForPlatform(platform),
platform,
library.sdkDeps.every(platform.supportsLibrary)))
.toList();
_sortModules(modules);
return MetaModule(modules);
}
/// Sorts [modules] in place so we get deterministic output.
void _sortModules(List<Module> modules) {
modules.sort((a, b) => a.primarySource.compareTo(b.primarySource));
}