blob: 8ebed62d696c14805bf8f34a1ed3f3fdd1227bcc [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 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/src/context/packages.dart';
import 'package:analyzer/src/generated/sdk.dart';
import 'package:analyzer/src/generated/source.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:collection/collection.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.
final 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) {
var startFolder = provider.getFolder(path);
for (var folder in startFolder.withAncestors) {
if (folder.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);
}
}
return null;
}
/// 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.parent2.path;
}
var startFolder = provider.getFolder(filePath);
for (var folder in startFolder.withAncestors) {
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 = parsePackagesFile(provider, packagesFile);
for (var package in packages.packages) {
packageMap[package.name] = [package.libFolder];
}
}
return GnWorkspace._(provider, root, packageMap);
}
}
return null;
}
/// 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 package_config.json 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.
///
/// Additionally, often times the package_config file name is prepended by
/// extra words, which results in file names like
/// `tiler_component_package_config.json`. Because of this, we cannot simply
/// check for `pathContext.basename(file.path) == 'package_config.json'`.
static List<File> _findPackagesFile(
ResourceProvider provider,
String root,
String filePath,
) {
path.Context pathContext = provider.pathContext;
String sourceDirectory = pathContext.relative(filePath, from: root);
var 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) => file.path.endsWith('package_config.json'))
.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>().firstWhereOrNull(
(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');
},
);
}
}
/// 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) {
var 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;
}
@override
Map<String, List<Folder>> packagesAvailableTo(String libraryPath) =>
workspace.packageMap;
@override
bool sourceIsInPublicApi(Source source) {
var filePath = filePathFromSource(source);
if (filePath == null) return false;
var libFolder = workspace.provider.pathContext.join(root, 'lib');
if (workspace.provider.pathContext.isWithin(libFolder, filePath)) {
var libSrcFolder =
workspace.provider.pathContext.join(root, 'lib', 'src');
return !workspace.provider.pathContext.isWithin(libSrcFolder, filePath);
}
return false;
}
}