blob: e0aae5f1e50bf477261aa41d497702f96000c522 [file] [log] [blame]
// Copyright (c) 2017, 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:core';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/src/context/packages.dart';
import 'package:analyzer/src/file_system/file_system.dart';
import 'package:analyzer/src/generated/sdk.dart';
import 'package:analyzer/src/generated/source.dart';
import 'package:analyzer/src/generated/source_io.dart';
import 'package:analyzer/src/source/package_map_resolver.dart';
import 'package:analyzer/src/summary/package_bundle_reader.dart';
import 'package:analyzer/src/workspace/workspace.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;
/**
* Information about a Gn workspace.
*/
class GnWorkspace extends Workspace {
/**
* The name of the directory that identifies the root of the workspace.
*/
static const String _jiriRootName = '.jiri_root';
/**
* The name of the file that identifies a set of GN Targets.
*
* For Dart package purposes, a BUILD.gn file identifies a package.
*/
static const String _buildFileName = 'BUILD.gn';
/**
* The resource provider used to access the file system.
*/
final ResourceProvider provider;
/**
* The absolute workspace root path (the directory containing the `.jiri_root`
* directory).
*/
@override
final String root;
/**
* The map from a package name to the list of its `lib/` folders.
*/
Map<String, List<Folder>> _packageMap;
GnWorkspace._(this.provider, this.root, this._packageMap);
@visibleForTesting
Map<String, List<Folder>> get packageMap => _packageMap;
@override
UriResolver get packageUriResolver =>
PackageMapUriResolver(provider, _packageMap);
@override
SourceFactory createSourceFactory(DartSdk sdk, SummaryDataStore summaryData) {
if (summaryData != null) {
throw UnsupportedError(
'Summary files are not supported in a GN workspace.');
}
List<UriResolver> resolvers = <UriResolver>[];
if (sdk != null) {
resolvers.add(DartUriResolver(sdk));
}
resolvers.add(packageUriResolver);
resolvers.add(ResourceUriResolver(provider));
return SourceFactory(resolvers);
}
/**
* Return the file with the given [absolutePath].
*
* Return `null` if the given [absolutePath] is not in the workspace [root].
*/
File findFile(String absolutePath) {
try {
File writableFile = provider.getFile(absolutePath);
if (writableFile.exists) {
return writableFile;
}
} catch (_) {}
return null;
}
@override
WorkspacePackage findPackageFor(String path) {
Folder folder = provider.getFolder(provider.pathContext.dirname(path));
while (true) {
Folder parent = folder.parent;
if (parent == null) {
return null;
}
if (parent.path.length < root.length) {
// We've walked up outside of [root], so [path] is definitely not
// defined in any package in this workspace.
return null;
}
if (folder.getChildAssumingFile(_buildFileName).exists) {
return GnWorkspacePackage(folder.path, this);
}
// Go up a folder.
folder = parent;
}
}
/**
* Find the GN workspace that contains the given [filePath].
*
* Return `null` if a workspace could not be found. For a workspace to be
* found, both a `.jiri_root` file must be found, and at least one "packages"
* file must be found in [filePath]'s output directory.
*/
static GnWorkspace find(ResourceProvider provider, String filePath) {
Resource resource = provider.getResource(filePath);
if (resource is File) {
filePath = resource.parent.path;
}
Folder folder = provider.getFolder(filePath);
while (true) {
Folder parent = folder.parent;
if (parent == null) {
return null;
}
if (folder.getChildAssumingFolder(_jiriRootName).exists) {
// Found the .jiri_root file, must be a non-git workspace.
String root = folder.path;
var packagesFiles = _findPackagesFile(provider, root, filePath);
if (packagesFiles.isEmpty) {
return null;
}
var packageMap = <String, List<Folder>>{};
for (var packagesFile in packagesFiles) {
var packages = parseDotPackagesFile(provider, packagesFile);
for (var package in packages.packages) {
packageMap[package.name] = [package.libFolder];
}
}
return GnWorkspace._(provider, root, packageMap);
}
// Go up a folder.
folder = parent;
}
}
/**
* For a source at `$root/foo/bar`, the packages files are generated in
* `$root/out/<debug|release>-XYZ/dartlang/gen/foo/bar`.
*
* Note that in some cases multiple .packages files can be found at that
* location, for example if the package contains both a library and a binary
* target. For a complete view of the package, all of these files need to be
* taken into account.
*/
static List<File> _findPackagesFile(
ResourceProvider provider,
String root,
String filePath,
) {
path.Context pathContext = provider.pathContext;
String sourceDirectory = pathContext.relative(filePath, from: root);
Folder outDirectory = _getOutDirectory(root, provider);
if (outDirectory == null) {
return const <File>[];
}
Folder genDir = outDirectory.getChildAssumingFolder(
pathContext.join('dartlang', 'gen', sourceDirectory));
if (!genDir.exists) {
return const <File>[];
}
return genDir
.getChildren()
.whereType<File>()
.where((File file) => pathContext.extension(file.path) == '.packages')
.toList();
}
/**
* Returns the output directory of the build, or `null` if it could not be
* found.
*
* First attempts to read a config file at the root of the source tree. If
* that file cannot be found, looks for standard output directory locations.
*/
static Folder _getOutDirectory(String root, ResourceProvider provider) {
const String fuchsiaDirConfigFile = '.fx-build-dir';
path.Context pathContext = provider.pathContext;
File configFile =
provider.getFile(pathContext.join(root, fuchsiaDirConfigFile));
if (configFile.exists) {
String buildDirPath = configFile.readAsStringSync().trim();
if (buildDirPath.isNotEmpty) {
if (pathContext.isRelative(buildDirPath)) {
buildDirPath = pathContext.join(root, buildDirPath);
}
return provider.getFolder(buildDirPath);
}
}
Folder outDirectory = provider.getFolder(pathContext.join(root, 'out'));
if (!outDirectory.exists) {
return null;
}
return outDirectory.getChildren().whereType<Folder>().firstWhere((folder) {
String baseName = pathContext.basename(folder.path);
// Taking a best guess to identify a build dir. This is clearly a fallback
// to the config-based method.
return baseName.startsWith('debug') || baseName.startsWith('release');
}, orElse: () => null);
}
}
/**
* Information about a package defined in a GnWorkspace.
*
* Separate from [Packages] or package maps, this class is designed to simply
* understand whether arbitrary file paths represent libraries declared within
* a given package in a GnWorkspace.
*/
class GnWorkspacePackage extends WorkspacePackage {
@override
final String root;
@override
final GnWorkspace workspace;
GnWorkspacePackage(this.root, this.workspace);
@override
bool contains(Source source) {
String filePath = filePathFromSource(source);
if (filePath == null) return false;
if (workspace.findFile(filePath) == null) {
return false;
}
if (!workspace.provider.pathContext.isWithin(root, filePath)) {
return false;
}
// Just because [filePath] is within [root] does not mean it is in this
// package; it could be in a "subpackage." Must go through the work of
// learning exactly which package [filePath] is contained in.
return workspace.findPackageFor(filePath).root == root;
}
}