// Copyright (c) 2020, 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. /// Generates the repo's ".dart_tool/package_config.json" file. library; import 'dart:convert'; import 'dart:io'; // Important! Do not add package: imports to this file. // Do not add relative deps for libraries that themselves use package deps. // This tool runs before the .dart_tool/package_config.json file is created, so // can not itself use package references. final repoRoot = dirname(dirname(fromUri(Platform.script))); void main(List args) { final fluteExists = Directory(join(repoRoot, platform('third_party/flute'))).existsSync(); var packageDirs = [ ...listSubdirectories(platform('pkg')), ...listSubdirectories(platform('third_party/pkg')), if (fluteExists) ...listSubdirectories(platform('third_party/flute')), platform('pkg/vm_service/test/test_package'), platform('runtime/observatory'), platform('runtime/observatory/tests/service/observatory_test_package'), platform('runtime/tools/heapsnapshot'), platform('sdk/lib/_internal/sdk_library_metadata'), platform('third_party/devtools/devtools_shared'), platform('tools/package_deps'), ]; var cfePackageDirs = [ platform('pkg/front_end/testcases'), ]; var feAnalyzerSharedPackageDirs = [ platform('pkg/_fe_analyzer_shared/test/exhaustiveness/data'), platform('pkg/_fe_analyzer_shared/test/flow_analysis/assigned_variables'), platform('pkg/_fe_analyzer_shared/test/flow_analysis/definite_assignment'), platform( 'pkg/_fe_analyzer_shared/test/flow_analysis/definite_unassignment'), platform('pkg/_fe_analyzer_shared/test/flow_analysis/nullability'), platform('pkg/_fe_analyzer_shared/test/flow_analysis/reachability'), platform('pkg/_fe_analyzer_shared/test/flow_analysis/type_promotion'), platform('pkg/_fe_analyzer_shared/test/flow_analysis/why_not_promoted'), platform('pkg/_fe_analyzer_shared/test/inheritance'), ]; var frontendServerPackageDirs = [ platform('pkg/frontend_server/test/fixtures'), ]; var pkgVmPackageDirs = [ platform('pkg/vm/testcases'), ]; var sampleDirs = listSubdirectories(platform('samples')).toList(); // Validate that all the given directories exist. var hasMissingDirectories = false; for (var path in [ ...packageDirs, ...cfePackageDirs, ...feAnalyzerSharedPackageDirs, ...pkgVmPackageDirs, ...sampleDirs, ]) { if (!Directory(join(repoRoot, path)).existsSync()) { stderr.writeln("Unable to locate directory: '$path'."); hasMissingDirectories = true; } } if (hasMissingDirectories) { exit(1); } var packages = [ ...makePackageConfigs(packageDirs), ...makeCfePackageConfigs(cfePackageDirs), ...makeFeAnalyzerSharedPackageConfigs(feAnalyzerSharedPackageDirs), ...makeFrontendServerPackageConfigs(frontendServerPackageDirs), ...makePkgVmPackageConfigs(pkgVmPackageDirs), ...makePackageConfigs(sampleDirs), ]; packages.sort((a, b) => a.name.compareTo(b.name)); // Remove any packages with identical names. final uniqueNames = packages.map((p) => p.name).toSet(); var hasDuplicatePackages = false; for (var name in uniqueNames) { var matches = [ for (final p in packages) if (p.name == name) p ]; if (matches.length > 1) { print('Duplicates found for package:$name'); for (var package in matches) { print(' ${package.rootUri}'); } hasDuplicatePackages = true; } } if (hasDuplicatePackages) { exit(1); } var configFile = File(join(repoRoot, '.dart_tool', 'package_config.json')); var packageConfig = PackageConfig( packages, extraData: { 'copyright': [ 'Copyright (c) 2020, 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.', ], 'comment': [ 'Package configuration for all packages in pkg/ and third_party/pkg/', ], }, ); writeIfDifferent(configFile, packageConfig.generateJson('..')); } /// Writes the given [contents] string to [file] if the contents are different /// than what's currently in the file. /// /// This updates the file to the given contents, while preserving the file /// timestamp if there are no changes. void writeIfDifferent(File file, String contents) { if (!file.existsSync() || file.readAsStringSync() != contents) { file.writeAsStringSync(contents); } } /// Generates package configurations for each package in [packageDirs]. Iterable makePackageConfigs(List packageDirs) sync* { for (var packageDir in packageDirs) { var name = pubspecName(packageDir); var version = pubspecLanguageVersion(packageDir); var hasLibDirectory = Directory(join(repoRoot, packageDir, 'lib')).existsSync(); yield Package( name: name, rootUri: packageDir, packageUri: hasLibDirectory ? 'lib/' : null, languageVersion: version, ); } } /// Generates package configurations for the special pseudo-packages. Iterable makeSpecialPackageConfigs( String packageNamePrefix, List packageDirs) sync* { // TODO: Remove the use of '.nonexisting/'. for (var packageDir in packageDirs) { yield Package( name: '${packageNamePrefix}_${basename(packageDir)}', rootUri: packageDir, packageUri: '.nonexisting/', ); } } /// Generates package configurations for the special pseudo-packages used by the /// CFE unit tests (`pkg/front_end/test/unit_test_suites.dart`). Iterable makeCfePackageConfigs(List packageDirs) => makeSpecialPackageConfigs('front_end', packageDirs); /// Generates package configurations for the special pseudo-packages used by the /// _fe_analyzer_shared id tests. Iterable makeFeAnalyzerSharedPackageConfigs( List packageDirs) => makeSpecialPackageConfigs('_fe_analyzer_shared', packageDirs); /// Generates package configurations for the special pseudo-packages used by the /// frontend_server tests. Iterable makeFrontendServerPackageConfigs(List packageDirs) => makeSpecialPackageConfigs('frontend_server', packageDirs); /// Generates package configurations for the special pseudo-packages used by the /// pkg/vm unit tests (`pkg/vm/test`). Iterable makePkgVmPackageConfigs(List packageDirs) => makeSpecialPackageConfigs('pkg_vm', packageDirs); /// Finds the paths of the subdirectories of [dirPath] that contain pubspecs. /// /// This method recurses until it finds a pubspec.yaml file. Iterable listSubdirectories(String parentPath) sync* { final parent = Directory(join(repoRoot, parentPath)); for (var child in parent.listSync().whereType()) { var name = basename(child.path); // Don't recurse into dot directories. if (name.startsWith('.')) continue; if (File(join(child.path, 'pubspec.yaml')).existsSync()) { yield join(parentPath, name); } else { yield* listSubdirectories(join(parentPath, name)); } } } final versionRE = RegExp(r"(?:\^|>=)(\d+\.\d+)"); /// Parses the package name in the pubspec for [packageDir] String pubspecName(String packageDir) { var pubspecFile = File(join(repoRoot, packageDir, 'pubspec.yaml')); if (!pubspecFile.existsSync()) { print("Error: Missing pubspec for $packageDir."); exit(1); } var contents = pubspecFile.readAsLinesSync(); if (!contents.any((line) => line.contains('name: '))) { print("Error: Pubspec for $packageDir has no name."); exit(1); } var name = contents.firstWhere((line) => line.contains('name: ')); return name.trim().substring('name:'.length).trim(); } /// Infers the language version from the SDK constraint in the pubspec for /// [packageDir]. /// /// The version is returned in the form `major.minor`. String pubspecLanguageVersion(String packageDir) { var pubspecFile = File(join(repoRoot, packageDir, 'pubspec.yaml')); if (!pubspecFile.existsSync()) { print("Error: Missing pubspec for $packageDir."); exit(1); } var contents = pubspecFile.readAsLinesSync(); if (!contents.any((line) => line.contains('sdk: '))) { print("Error: Pubspec for $packageDir has no SDK constraint."); exit(1); } // Handle either "sdk: >=2.14.0 <3.0.0" or "sdk: ^2.3.0". var sdkConstraint = contents.firstWhere((line) => line.contains('sdk: ')); sdkConstraint = sdkConstraint.trim().substring('sdk:'.length).trim(); if (sdkConstraint.startsWith('"') || sdkConstraint.startsWith("'")) { sdkConstraint = sdkConstraint.substring(1, sdkConstraint.length - 2); } var match = versionRE.firstMatch(sdkConstraint); if (match == null) { print("Error: unknown version range for $packageDir: '$sdkConstraint'."); exit(1); } return match[1]!; } class Package { final String name; final String rootUri; final String? packageUri; final String? languageVersion; Package({ required this.name, required this.rootUri, this.packageUri, this.languageVersion, }); Map toMap(String relativeTo) { return { 'name': name, 'rootUri': posix(join(relativeTo, rootUri)), if (packageUri != null) 'packageUri': posix(packageUri!), if (languageVersion != null) 'languageVersion': languageVersion, }; } } class PackageConfig { final List packages; final Map? extraData; PackageConfig(this.packages, {this.extraData}); String generateJson(String relativeTo) { var config = {}; if (extraData != null) { for (var key in extraData!.keys) { config[key] = extraData![key]; } } config['configVersion'] = 2; config['generator'] = 'tools/generate_package_config.dart'; config['packages'] = packages.map((package) => package.toMap(relativeTo)).toList(); var jsonString = JsonEncoder.withIndent(' ').convert(config); return '$jsonString\n'; } } // Below are some (very simplified) versions of the package:path functions. final String _separator = Platform.pathSeparator; String dirname(String s) { return s.substring(0, s.lastIndexOf(_separator)); } String join(String s1, String s2, [String? s3]) { if (s3 != null) { return join(join(s1, s2), s3); } else { return s1.endsWith(_separator) ? '$s1$s2' : '$s1$_separator$s2'; } } String basename(String s) { while (s.endsWith(_separator)) { s = s.substring(0, s.length - 1); } return s.substring(s.lastIndexOf(_separator) + 1); } String fromUri(Uri uri) => uri.toFilePath(); /// Given a platform path, return a posix one. String posix(String s) => Platform.isWindows ? s.replaceAll(_separator, '/') : s; /// Given a posix path, return a platform one. String platform(String s) => Platform.isWindows ? s.replaceAll('/', _separator) : s;