blob: 4d486e82ad06e19adc36f30c9b2eca84c518c6ec [file] [log] [blame] [edit]
// 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<String> 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 = <Package>[
...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<Package> makePackageConfigs(List<String> 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<Package> makeSpecialPackageConfigs(
String packageNamePrefix, List<String> 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<Package> makeCfePackageConfigs(List<String> packageDirs) =>
makeSpecialPackageConfigs('front_end', packageDirs);
/// Generates package configurations for the special pseudo-packages used by the
/// _fe_analyzer_shared id tests.
Iterable<Package> makeFeAnalyzerSharedPackageConfigs(
List<String> packageDirs) =>
makeSpecialPackageConfigs('_fe_analyzer_shared', packageDirs);
/// Generates package configurations for the special pseudo-packages used by the
/// frontend_server tests.
Iterable<Package> makeFrontendServerPackageConfigs(List<String> packageDirs) =>
makeSpecialPackageConfigs('frontend_server', packageDirs);
/// Generates package configurations for the special pseudo-packages used by the
/// pkg/vm unit tests (`pkg/vm/test`).
Iterable<Package> makePkgVmPackageConfigs(List<String> packageDirs) =>
makeSpecialPackageConfigs('pkg_vm', packageDirs);
/// Finds the paths of the subdirectories of [parentPath] that contain pubspecs.
///
/// This method recurses until it finds a pubspec.yaml file.
Iterable<String> listSubdirectories(String parentPath) sync* {
final parent = Directory(join(repoRoot, parentPath));
for (var child in parent.listSync().whereType<Directory>()) {
var name = basename(child.path);
// Don't recurse into dot directories.
if (name.startsWith('.')) continue;
final pubspecFile = File(join(child.path, 'pubspec.yaml'));
if (pubspecFile.existsSync() && !isWorkspacePubspec(pubspecFile)) {
// Stop recursing when we find a pubspec file (and that pubspec does not
// define a pub workspace).
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();
}
/// Returns whether the given pubspec defines a workspace.
bool isWorkspacePubspec(File pubspecFile) {
if (!pubspecFile.existsSync()) {
print('Error: Missing pubspec for ${pubspecFile.path}');
exit(1);
}
var contents = pubspecFile.readAsLinesSync();
return contents.any((line) => line.startsWith('workspace:'));
}
/// 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<String, Object?> 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<Package> packages;
final Map<String, Object?>? extraData;
PackageConfig(this.packages, {this.extraData});
String generateJson(String relativeTo) {
var config = <String, Object?>{};
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;