// 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/source/source.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/source/package_map_resolver.dart'; import 'package:analyzer/src/summary/package_bundle_reader.dart'; import 'package:analyzer/src/util/file_paths.dart' as file_paths; import 'package:analyzer/src/workspace/workspace.dart'; import 'package:collection/collection.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 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; final File buildGnFile; /// Information about packages available in the workspace. final Packages packages; GnWorkspace._(this.provider, this.root, this.buildGnFile, this.packages); // TODO(scheglov): Finish switching to [packages]. Map> get packageMap { var packageMap = >{}; for (var package in packages.packages) { packageMap[package.name] = [package.libFolder]; } return 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 resolvers = []; 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 WorkspacePackageImpl? findPackageFor(String filePath) { var startFolder = provider.getFolder(filePath); 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(file_paths.buildGn).exists) { return GnWorkspacePackage(folder, this); } } return null; } /// Find the GN workspace for the given [buildGnFile]. /// /// Return `null` if a workspace could not be found. For a workspace to be /// found, the `.jiri_root` folder must be found, and at least one /// `package_config.json` file must be found in [buildGnFile]'s /// output directory. static GnWorkspace? find(File buildGnFile) { var provider = buildGnFile.provider; for (var folder in buildGnFile.parent.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, buildGnFile); if (packagesFiles.isEmpty) { return null; } var packageMap = {}; for (var packagesFile in packagesFiles) { var packages = parsePackageConfigJsonFile(provider, packagesFile); for (var package in packages.packages) { packageMap[package.name] = package; } } return GnWorkspace._(provider, root, buildGnFile, Packages(packageMap)); } } return null; } /// For a source at `$root/foo/bar`, the packages files are generated in /// `$root/out/-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 _findPackagesFile( ResourceProvider provider, String root, File buildGnFile, ) { path.Context pathContext = provider.pathContext; String sourceDirectory = pathContext.relative( buildGnFile.parent.path, from: root, ); var outDirectory = _getOutDirectory(root, provider); if (outDirectory == null) { return const []; } Folder genDir = outDirectory.getChildAssumingFolder( pathContext.join('dartlang', 'gen', sourceDirectory), ); if (!genDir.exists) { return const []; } return genDir .getChildren() .whereType() .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().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 WorkspacePackageImpl { @override final Folder 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 (!root.contains(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.path == root.path; } @override Packages packagesAvailableTo(String libraryPath) => workspace.packages; @override bool sourceIsInPublicApi(Source source) { var filePath = filePathFromSource(source); if (filePath == null) return false; var libFolder = root.getChildAssumingFolder('lib'); if (libFolder.contains(filePath)) { var libSrcFolder = root .getChildAssumingFolder('lib') .getChildAssumingFolder('src'); return !libSrcFolder.contains(filePath); } return false; } }