| // Copyright (c) 2018, 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 'dart:async'; |
| import 'dart:convert'; |
| import 'dart:io'; |
| |
| import 'package:http/http.dart'; |
| import 'package:pub_semver/pub_semver.dart'; |
| import 'package:pubspec_parse/pubspec_parse.dart'; |
| import 'package:yaml/yaml.dart'; |
| |
| import 'util.dart'; |
| import 'version.dart'; |
| |
| class PackageException implements Exception { |
| final List<PackageExceptionDetails> details; |
| |
| final String? unsupportedArgument; |
| |
| PackageException(this.details, {this.unsupportedArgument}); |
| } |
| |
| class PackageExceptionDetails { |
| final String error; |
| final String? description; |
| final bool _missingDependency; |
| |
| const PackageExceptionDetails._(this.error, |
| {this.description, bool missingDependency = false}) |
| : _missingDependency = missingDependency; |
| |
| static const noPubspecLock = |
| PackageExceptionDetails._('`pubspec.lock` does not exist.', |
| description: 'Run `$appName` in a Dart package directory. ' |
| 'Run `dart pub get` first.', |
| missingDependency: true); |
| |
| static PackageExceptionDetails missingDep( |
| String pkgName, VersionConstraint constraint) => |
| PackageExceptionDetails._( |
| 'You must have a dependency on `$pkgName` in `pubspec.yaml`.', |
| description: ''' |
| # pubspec.yaml |
| dev_dependencies: |
| $pkgName: $constraint''', |
| missingDependency: true); |
| |
| @override |
| String toString() => [error, description].join('\n'); |
| } |
| |
| Future _runPubDeps() async { |
| var result = Process.runSync(dartPath, ['pub', 'deps']); |
| |
| if (result.exitCode == 65 || result.exitCode == 66) { |
| throw PackageException( |
| [PackageExceptionDetails._((result.stderr as String).trim())]); |
| } |
| |
| if (result.exitCode != 0) { |
| throw ProcessException( |
| dartPath, |
| ['pub', 'deps'], |
| '***OUT***\n${result.stdout}\n***ERR***\n${result.stderr}\n***', |
| exitCode); |
| } |
| } |
| |
| class PubspecLock { |
| final YamlMap? _packages; |
| |
| PubspecLock(this._packages); |
| |
| static Future<PubspecLock> read() async { |
| await _runPubDeps(); |
| |
| var pubspecLock = |
| loadYaml(await File('pubspec.lock').readAsString()) as YamlMap; |
| |
| var packages = pubspecLock['packages'] as YamlMap?; |
| return PubspecLock(packages); |
| } |
| |
| List<PackageExceptionDetails> checkPackage( |
| String pkgName, VersionConstraint constraint, |
| {String? forArgument, bool requireDirect = true}) { |
| var issues = <PackageExceptionDetails>[]; |
| var missingDetails = |
| PackageExceptionDetails.missingDep(pkgName, constraint); |
| |
| var pkgDataMap = |
| (_packages == null) ? null : _packages[pkgName] as YamlMap?; |
| if (pkgDataMap == null) { |
| issues.add(missingDetails); |
| } else { |
| var dependency = pkgDataMap['dependency'] as String?; |
| if (requireDirect && |
| dependency != null && |
| !dependency.startsWith('direct ')) { |
| issues.add(missingDetails); |
| } |
| |
| var source = pkgDataMap['source'] as String?; |
| if (source == 'hosted') { |
| // NOTE: pkgDataMap['description'] should be: |
| // `{url: https://pub.dev, name: [pkgName]}` |
| // If a user is playing around here, they are on their own. |
| |
| var version = pkgDataMap['version'] as String; |
| var pkgVersion = Version.parse(version); |
| if (!constraint.allows(pkgVersion)) { |
| var error = 'The `$pkgName` version – $pkgVersion – is not ' |
| 'within the allowed constraint – $constraint.'; |
| issues.add(PackageExceptionDetails._(error)); |
| } |
| } else { |
| // NOTE: Intentionally not checking non-hosted dependencies: git, path |
| // If a user is playing around here, they are on their own. |
| } |
| } |
| return issues; |
| } |
| } |
| |
| Future<List<PackageExceptionDetails>> _validateBuildDaemonVersion( |
| PubspecLock pubspecLock) async { |
| var buildDaemonConstraint = '^4.0.0'; |
| |
| var issues = <PackageExceptionDetails>[]; |
| |
| var buildDaemonIssues = pubspecLock.checkPackage( |
| 'build_daemon', |
| VersionConstraint.parse(buildDaemonConstraint), |
| requireDirect: false, |
| ); |
| |
| // Only warn of build_daemon issues if they have a dependency on the package. |
| if (buildDaemonIssues.any((issue) => !issue._missingDependency)) { |
| var info = await _latestPackageInfo(); |
| var issuePreamble = |
| 'This version of webdev does not support the `build_daemon` ' |
| 'protocol used by your version of `build_runner`.'; |
| // Check if the newer version supports the `build_daemon` transitive version |
| // used by their application. |
| if (info.isNewer && |
| pubspecLock |
| .checkPackage('build_daemon', info.buildDaemonConstraint, |
| requireDirect: false) |
| .isEmpty) { |
| issues.add(PackageExceptionDetails._('$issuePreamble\n' |
| 'A newer version of webdev is available which supports ' |
| 'your version of the `build_daemon`. Please update.')); |
| } else { |
| issues.add(PackageExceptionDetails._('$issuePreamble\n' |
| 'Please add a dev dependency on `build_daemon` with constraint: ' |
| '$buildDaemonConstraint')); |
| } |
| } |
| return issues; |
| } |
| |
| final buildRunnerConstraint = VersionConstraint.parse('^2.4.0'); |
| final buildWebCompilersConstraint = VersionConstraint.parse('^4.0.4'); |
| |
| // Note the minimum versions should never be dev versions as users will not |
| // get them by default. |
| Future<void> checkPubspecLock(PubspecLock pubspecLock, |
| {required bool requireBuildWebCompilers}) async { |
| var issues = <PackageExceptionDetails>[]; |
| |
| var buildRunnerIssues = |
| pubspecLock.checkPackage('build_runner', buildRunnerConstraint); |
| |
| issues.addAll(buildRunnerIssues); |
| |
| if (requireBuildWebCompilers) { |
| issues.addAll(pubspecLock.checkPackage( |
| 'build_web_compilers', buildWebCompilersConstraint)); |
| } |
| |
| if (buildRunnerIssues.isEmpty) { |
| issues.addAll(await _validateBuildDaemonVersion(pubspecLock)); |
| } |
| |
| if (issues.isNotEmpty) { |
| throw PackageException(issues); |
| } |
| } |
| |
| class _PackageInfo { |
| final Version? version; |
| final VersionConstraint buildDaemonConstraint; |
| final bool isNewer; |
| _PackageInfo(this.version, this.buildDaemonConstraint, this.isNewer); |
| } |
| |
| /// Returns the package info for the latest webdev release. |
| Future<_PackageInfo> _latestPackageInfo() async { |
| var response = await get(Uri.parse('https://pub.dev/api/packages/webdev'), |
| headers: {HttpHeaders.userAgentHeader: 'webdev $packageVersion'}); |
| var responseObj = json.decode(response.body); |
| var pubspec = Pubspec.fromJson( |
| responseObj['latest']['pubspec'] as Map<String, dynamic>); |
| var buildDaemonDependency = pubspec.dependencies['build_daemon']; |
| // This should never be satisfied. |
| var buildDaemonConstraint = VersionConstraint.parse('0.0.0'); |
| if (buildDaemonDependency is HostedDependency) { |
| buildDaemonConstraint = buildDaemonDependency.version; |
| } |
| var currentVersion = Version.parse(packageVersion); |
| var pubspecVersion = pubspec.version; |
| var isNewer = (pubspecVersion == null) |
| ? true |
| : currentVersion.compareTo(pubspecVersion) < 0; |
| return _PackageInfo(pubspec.version, buildDaemonConstraint, isNewer); |
| } |