blob: b82d111bb5188c09cc5ebc6f41f0dc4c4d3d7908 [file]
// 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:path/path.dart' as p;
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 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 {
final 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({bool offline = false}) async {
if (!offline) {
await _runPubDeps();
}
var dir = p.absolute(p.current);
while (true) {
final candidate = p.join(dir, '.dart_tool', 'package_config.json');
if (File(candidate).existsSync()) break;
final next = p.dirname(dir);
if (next == dir) {
// Give up.
dir = p.current;
break;
}
dir = next;
}
final pubspecLock =
loadYaml(
await File(
p.relative(p.join(dir, 'pubspec.lock')),
).readAsString(),
)
as YamlMap;
final packages = pubspecLock['packages'] as YamlMap?;
return PubspecLock(packages);
}
List<PackageExceptionDetails> checkPackage(
String pkgName,
VersionConstraint constraint, {
String? forArgument,
}) {
final issues = <PackageExceptionDetails>[];
final missingDetails = PackageExceptionDetails.missingDep(
pkgName,
constraint,
);
final pkgDataMap = (_packages == null)
? null
: _packages[pkgName] as YamlMap?;
if (pkgDataMap == null) {
issues.add(missingDetails);
} else {
final 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.
final version = pkgDataMap['version'] as String;
final pkgVersion = Version.parse(version);
if (!constraint.allows(pkgVersion)) {
final 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 {
final buildDaemonConstraint = '^4.0.0';
final issues = <PackageExceptionDetails>[];
final buildDaemonIssues = pubspecLock.checkPackage(
'build_daemon',
VersionConstraint.parse(buildDaemonConstraint),
);
// Only warn of build_daemon issues if they have a dependency on the package.
if (buildDaemonIssues.any((issue) => !issue._missingDependency)) {
final info = await _latestPackageInfo();
final 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)
.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.4.12');
// 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 {
final issues = <PackageExceptionDetails>[];
final 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 {
final response = await get(
Uri.parse('https://pub.dev/api/packages/webdev'),
headers: {HttpHeaders.userAgentHeader: 'webdev $packageVersion'},
);
final responseObj = json.decode(response.body);
final pubspec = Pubspec.fromJson(
responseObj['latest']['pubspec'] as Map<String, dynamic>,
);
final buildDaemonDependency = pubspec.dependencies['build_daemon'];
// This should never be satisfied.
var buildDaemonConstraint = VersionConstraint.parse('0.0.0');
if (buildDaemonDependency is HostedDependency) {
buildDaemonConstraint = buildDaemonDependency.version;
}
final currentVersion = Version.parse(packageVersion);
final pubspecVersion = pubspec.version;
final isNewer = (pubspecVersion == null)
? true
: currentVersion.compareTo(pubspecVersion) < 0;
return _PackageInfo(pubspec.version, buildDaemonConstraint, isNewer);
}