| // Copyright 2013 The Flutter Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'package:file/file.dart'; |
| import 'package:glob/glob.dart'; |
| import 'package:path/path.dart' as p; |
| import 'package:pubspec_parse/pubspec_parse.dart'; |
| |
| import 'ci_config.dart'; |
| import 'core.dart'; |
| import 'output_utils.dart'; |
| import 'pending_changelog_entry.dart'; |
| |
| export 'package:pubspec_parse/pubspec_parse.dart' show Pubspec; |
| export 'ci_config.dart'; |
| export 'core.dart' show FlutterPlatform; |
| export 'pending_changelog_entry.dart'; |
| |
| /// A package in the repository. |
| // |
| // TODO(stuartmorgan): Add more package-related info here, such as an on-demand |
| // cache of the parsed pubspec. |
| class RepositoryPackage { |
| /// Creates a representation of the package at [directory]. |
| RepositoryPackage(this.directory); |
| |
| /// The location of the package. |
| final Directory directory; |
| |
| /// The path to the package. |
| String get path => directory.path; |
| |
| /// Returns the string to use when referring to the package in user-targeted |
| /// messages. |
| /// |
| /// Callers should not expect a specific format for this string, since |
| /// it uses heuristics to try to be precise without being overly verbose. If |
| /// an exact format (e.g., published name, or basename) is required, that |
| /// should be used instead. |
| String get displayName { |
| List<String> components = directory.fileSystem.path.split(directory.path); |
| // Remove everything up to the packages directory. |
| final int packagesIndex = components.indexOf('packages'); |
| if (packagesIndex != -1) { |
| components = components.sublist(packagesIndex + 1); |
| } |
| // For the common federated plugin pattern of `foo/foo_subpackage`, drop |
| // the first part since it's not useful. |
| if (components.length >= 2 && components[1].startsWith('${components[0]}_')) { |
| components = components.sublist(1); |
| } |
| return p.posix.joinAll(components); |
| } |
| |
| /// The package's top-level pubspec.yaml. |
| File get pubspecFile => directory.childFile('pubspec.yaml'); |
| |
| /// The package's top-level README. |
| File get readmeFile => directory.childFile('README.md'); |
| |
| /// The package's top-level README. |
| File get changelogFile => directory.childFile('CHANGELOG.md'); |
| |
| /// The package's top-level README. |
| File get authorsFile => directory.childFile('AUTHORS'); |
| |
| /// The package's top-level ci_config.yaml. |
| File get ciConfigFile => directory.childFile('ci_config.yaml'); |
| |
| /// The lib directory containing the package's code. |
| Directory get libDirectory => directory.childDirectory('lib'); |
| |
| /// The test directory containing the package's Dart tests. |
| Directory get testDirectory => directory.childDirectory('test'); |
| |
| /// The path to the script that is run by the `custom-test` command. |
| File get customTestScript => directory.childDirectory('tool').childFile('run_tests.dart'); |
| |
| /// The path to the script that is run before publishing. |
| File get prePublishScript => directory.childDirectory('tool').childFile('pre_publish.dart'); |
| |
| /// The directory containing pending changelog entries. |
| Directory get pendingChangelogsDirectory => directory.childDirectory('pending_changelogs'); |
| |
| /// Returns the directory containing support for [platform]. |
| Directory platformDirectory(FlutterPlatform platform) { |
| late final String directoryName; |
| switch (platform) { |
| case FlutterPlatform.android: |
| directoryName = 'android'; |
| case FlutterPlatform.ios: |
| directoryName = 'ios'; |
| case FlutterPlatform.linux: |
| directoryName = 'linux'; |
| case FlutterPlatform.macos: |
| directoryName = 'macos'; |
| case FlutterPlatform.web: |
| directoryName = 'web'; |
| case FlutterPlatform.windows: |
| directoryName = 'windows'; |
| } |
| return directory.childDirectory(directoryName); |
| } |
| |
| /// Returns true if the package is an app that supports [platform]. |
| /// |
| /// The "app" prefix on this method is because this currently only works |
| /// for app packages (e.g., examples). |
| // TODO(stuartmorgan): Add support for non-app packages, by parsing the |
| // pubspec for `flutter:platform:` or `platform:` sections. |
| bool appSupportsPlatform(FlutterPlatform platform) { |
| return platformDirectory(platform).existsSync(); |
| } |
| |
| late final Pubspec _parsedPubspec = Pubspec.parse(pubspecFile.readAsStringSync()); |
| |
| /// Returns the parsed [pubspecFile]. |
| /// |
| /// Caches for future use. |
| Pubspec parsePubspec() => _parsedPubspec; |
| |
| late final CIConfig? _parsedCIConfig = ciConfigFile.existsSync() |
| ? CIConfig.parse(ciConfigFile.readAsStringSync()) |
| : null; |
| |
| /// Returns the parsed [ciConfigFile], or null if it does not exist. |
| /// |
| /// Throws if the file exists but is not a valid ci_config.yaml. |
| CIConfig? parseCIConfig() => _parsedCIConfig; |
| |
| /// Returns true if the package depends on Flutter. |
| bool requiresFlutter() { |
| final Pubspec pubspec = parsePubspec(); |
| return _includesFlutterSdkDependency(pubspec.dependencies) || |
| _includesFlutterSdkDependency(pubspec.devDependencies); |
| } |
| |
| /// True if this package has a dependency on Flutter. |
| bool _includesFlutterSdkDependency(Map<String, Dependency> deps) { |
| const flutterSdkDependencyName = 'flutter'; |
| return deps.values.whereType<SdkDependency>().any( |
| (SdkDependency dependency) => dependency.sdk == flutterSdkDependencyName, |
| ); |
| } |
| |
| /// True if this appears to be a federated plugin package, according to |
| /// repository conventions. |
| bool get isFederated => |
| directory.parent.basename != 'packages' && |
| directory.basename.startsWith(directory.parent.basename); |
| |
| /// True if this appears to be the app-facing package of a federated plugin, |
| /// according to repository conventions. |
| bool get isAppFacing => |
| directory.parent.basename != 'packages' && directory.basename == directory.parent.basename; |
| |
| /// True if this appears to be a platform interface package, according to |
| /// repository conventions. |
| bool get isPlatformInterface => directory.basename.endsWith('_platform_interface'); |
| |
| /// True if this appears to be a platform implementation package, according to |
| /// repository conventions. |
| bool get isPlatformImplementation => |
| // Any part of a federated plugin that isn't the platform interface and |
| // isn't the app-facing package should be an implementation package. |
| isFederated && !isPlatformInterface && directory.basename != directory.parent.basename; |
| |
| /// True if this appears to be a top-level package, according to repository |
| /// conventions. |
| bool get isTopLevel => getEnclosingPackage() == null; |
| |
| /// True if this appears to be an example package, according to package |
| /// conventions. |
| bool get isExample { |
| final RepositoryPackage? enclosingPackage = getEnclosingPackage(); |
| if (enclosingPackage == null) { |
| // An example package is enclosed in another package. |
| return false; |
| } |
| // Check whether this is one of the enclosing package's examples. |
| return enclosingPackage.getExamples().any((RepositoryPackage p) => p.path == path); |
| } |
| |
| /// Returns the Flutter example packages contained in the package, if any. |
| Iterable<RepositoryPackage> getExamples() { |
| final Directory exampleDirectory = directory.childDirectory('example'); |
| if (!exampleDirectory.existsSync()) { |
| return <RepositoryPackage>[]; |
| } |
| if (isPackage(exampleDirectory)) { |
| return <RepositoryPackage>[RepositoryPackage(exampleDirectory)]; |
| } |
| // Only look at the subdirectories of the example directory if the example |
| // directory itself is not a Dart package, and only look one level below the |
| // example directory for other Dart packages. |
| return exampleDirectory |
| .listSync() |
| .where((FileSystemEntity entity) => isPackage(entity)) |
| // isPackage guarantees that the cast to Directory is safe. |
| .map((FileSystemEntity entity) => RepositoryPackage(entity as Directory)); |
| } |
| |
| /// Returns the package that this package is a part of, if any. |
| RepositoryPackage? getEnclosingPackage() { |
| Directory current = directory.parent; |
| while (current.path != current.parent.path) { |
| if (isPackage(current)) { |
| return RepositoryPackage(current); |
| } |
| // Stop walking up if we hit a known repo root directory. |
| if (current.basename == 'packages' || current.basename == 'third_party') { |
| break; |
| } |
| current = current.parent; |
| } |
| return null; |
| } |
| |
| /// True if this package is located within a directory that is ignored |
| /// by the enclosing package's `.pubignore` file. |
| bool get isPubIgnored { |
| final RepositoryPackage? enclosingPackage = getEnclosingPackage(); |
| if (enclosingPackage == null) { |
| return false; |
| } |
| final File pubignoreFile = enclosingPackage.directory.childFile('.pubignore'); |
| if (!pubignoreFile.existsSync()) { |
| return false; |
| } |
| |
| final String relativePath = p.relative(directory.path, from: enclosingPackage.directory.path); |
| final List<String> segments = p.split(relativePath); |
| final candidatePaths = <String>[]; |
| for (var i = 1; i <= segments.length; i++) { |
| final String prefix = p.posix.joinAll(segments.sublist(0, i)); |
| candidatePaths.add(prefix); |
| candidatePaths.add('$prefix/'); |
| } |
| |
| final List<String> ignoreLines = pubignoreFile.readAsLinesSync(); |
| |
| for (final line in ignoreLines) { |
| final String trimmed = line.trim(); |
| if (trimmed.isEmpty || trimmed.startsWith('#')) { |
| continue; |
| } |
| |
| var pattern = trimmed; |
| final bool isAnchored = pattern.startsWith('/'); |
| if (isAnchored) { |
| pattern = pattern.substring(1); |
| } |
| if (pattern.endsWith('/')) { |
| pattern = '$pattern**'; |
| } |
| |
| try { |
| final globExact = Glob(pattern); |
| final Glob? globNested = isAnchored ? null : Glob('**/$pattern'); |
| for (final candidate in candidatePaths) { |
| if (globExact.matches(candidate) || |
| (globNested != null && globNested.matches(candidate))) { |
| return true; |
| } |
| } |
| } on FormatException catch (e) { |
| printWarning('Warning: Invalid glob pattern "$trimmed" in ${pubignoreFile.path}: $e'); |
| } |
| } |
| return false; |
| } |
| |
| /// Returns all Dart package folders (e.g., examples) under this package. |
| Iterable<RepositoryPackage> getSubpackages({bool includeExamples = true}) { |
| return directory |
| .listSync(recursive: true, followLinks: false) |
| .where(isPackage) |
| .map( |
| (FileSystemEntity directory) => |
| // isPackage guarantees that this cast is valid. |
| RepositoryPackage(directory as Directory), |
| ) |
| .where((RepositoryPackage p) => includeExamples || (p.directory.basename != 'example')); |
| } |
| |
| /// Returns true if the package is not marked as "publish_to: none". |
| bool isPublishable() { |
| return parsePubspec().publishTo != 'none'; |
| } |
| |
| /// Returns the parsed changelog entries for the package. |
| /// |
| /// This method reads through the files in the pending_changelogs folder |
| /// and parses each file as a changelog entry. |
| /// |
| /// Throws if the folder does not exist, or if any of the files are not |
| /// valid changelog entries. |
| List<PendingChangelogEntry> getPendingChangelogs() { |
| final entries = <PendingChangelogEntry>[]; |
| |
| final Directory pendingChangelogsDir = pendingChangelogsDirectory; |
| if (!pendingChangelogsDir.existsSync()) { |
| throw FormatException('No pending_changelogs folder found for $displayName.'); |
| } |
| |
| final List<File> pendingChangelogFiles = pendingChangelogsDir |
| .listSync() |
| .whereType<File>() |
| .where((File file) => !PendingChangelogEntry.isTemplate(file)) |
| .toList(); |
| final Iterable<File> unexpectedFiles = pendingChangelogFiles.where( |
| (File file) => !file.basename.endsWith('.yaml'), |
| ); |
| if (unexpectedFiles.isNotEmpty) { |
| throw FormatException( |
| 'Found non-YAML file(s) in pending_changelogs: ${unexpectedFiles.map((file) => file.path).join(',')}', |
| ); |
| } |
| |
| for (final file in pendingChangelogFiles) { |
| try { |
| entries.add(PendingChangelogEntry.parse(file.readAsStringSync(), file)); |
| } on FormatException catch (e) { |
| throw FormatException('Malformed pending changelog file: ${file.path}\n$e'); |
| } |
| } |
| return entries; |
| } |
| } |