mirror of
https://github.com/flutter/packages.git
synced 2025-06-06 19:39:13 +08:00
299 lines
9.9 KiB
Dart
299 lines
9.9 KiB
Dart
// Copyright 2013 The Flutter Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
// @dart=2.9
|
|
|
|
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'dart:io' as io;
|
|
|
|
import 'package:colorize/colorize.dart';
|
|
import 'package:file/file.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:meta/meta.dart';
|
|
import 'package:pub_semver/pub_semver.dart';
|
|
import 'package:pubspec_parse/pubspec_parse.dart';
|
|
|
|
import 'common.dart';
|
|
|
|
/// A command to check that packages are publishable via 'dart publish'.
|
|
class PublishCheckCommand extends PluginCommand {
|
|
/// Creates an instance of the publish command.
|
|
PublishCheckCommand(
|
|
Directory packagesDir,
|
|
FileSystem fileSystem, {
|
|
ProcessRunner processRunner = const ProcessRunner(),
|
|
this.httpClient,
|
|
}) : _pubVersionFinder =
|
|
PubVersionFinder(httpClient: httpClient ?? http.Client()),
|
|
super(packagesDir, fileSystem, processRunner: processRunner) {
|
|
argParser.addFlag(
|
|
_allowPrereleaseFlag,
|
|
help: 'Allows the pre-release SDK warning to pass.\n'
|
|
'When enabled, a pub warning, which asks to publish the package as a pre-release version when '
|
|
'the SDK constraint is a pre-release version, is ignored.',
|
|
defaultsTo: false,
|
|
);
|
|
argParser.addFlag(_machineFlag,
|
|
help: 'Switch outputs to a machine readable JSON. \n'
|
|
'The JSON contains a "status" field indicating the final status of the command, the possible values are:\n'
|
|
' $_statusNeedsPublish: There is at least one package need to be published. They also passed all publish checks.\n'
|
|
' $_statusMessageNoPublish: There are no packages needs to be published. Either no pubspec change detected or all versions have already been published.\n'
|
|
' $_statusMessageError: Some error has occurred.',
|
|
defaultsTo: false,
|
|
negatable: true);
|
|
}
|
|
|
|
static const String _allowPrereleaseFlag = 'allow-pre-release';
|
|
static const String _machineFlag = 'machine';
|
|
static const String _statusNeedsPublish = 'needs-publish';
|
|
static const String _statusMessageNoPublish = 'no-publish';
|
|
static const String _statusMessageError = 'error';
|
|
static const String _statusKey = 'status';
|
|
static const String _humanMessageKey = 'humanMessage';
|
|
|
|
final List<String> _validStatus = <String>[
|
|
_statusNeedsPublish,
|
|
_statusMessageNoPublish,
|
|
_statusMessageError
|
|
];
|
|
|
|
@override
|
|
final String name = 'publish-check';
|
|
|
|
@override
|
|
final String description =
|
|
'Checks to make sure that a plugin *could* be published.';
|
|
|
|
/// The custom http client used to query versions on pub.
|
|
final http.Client httpClient;
|
|
|
|
final PubVersionFinder _pubVersionFinder;
|
|
|
|
// The output JSON when the _machineFlag is on.
|
|
final Map<String, dynamic> _machineOutput = <String, dynamic>{};
|
|
|
|
final List<String> _humanMessages = <String>[];
|
|
|
|
@override
|
|
Future<void> run() async {
|
|
final ZoneSpecification logSwitchSpecification = ZoneSpecification(
|
|
print: (Zone self, ZoneDelegate parent, Zone zone, String message) {
|
|
final bool logMachineMessage = getBoolArg(_machineFlag);
|
|
if (logMachineMessage && message != _prettyJson(_machineOutput)) {
|
|
_humanMessages.add(message);
|
|
} else {
|
|
parent.print(zone, message);
|
|
}
|
|
});
|
|
|
|
await runZoned(_runCommand, zoneSpecification: logSwitchSpecification);
|
|
}
|
|
|
|
Future<void> _runCommand() async {
|
|
final List<Directory> failedPackages = <Directory>[];
|
|
|
|
String status = _statusMessageNoPublish;
|
|
await for (final Directory plugin in getPlugins()) {
|
|
final _PublishCheckResult result = await _passesPublishCheck(plugin);
|
|
switch (result) {
|
|
case _PublishCheckResult._notPublished:
|
|
if (failedPackages.isEmpty) {
|
|
status = _statusNeedsPublish;
|
|
}
|
|
break;
|
|
case _PublishCheckResult._published:
|
|
break;
|
|
case _PublishCheckResult._error:
|
|
failedPackages.add(plugin);
|
|
status = _statusMessageError;
|
|
break;
|
|
}
|
|
}
|
|
_pubVersionFinder.httpClient.close();
|
|
|
|
if (failedPackages.isNotEmpty) {
|
|
final String error =
|
|
'The following ${failedPackages.length} package(s) failed the '
|
|
'publishing check:';
|
|
final String joinedFailedPackages = failedPackages.join('\n');
|
|
_printImportantStatusMessage('$error\n$joinedFailedPackages',
|
|
isError: true);
|
|
} else {
|
|
_printImportantStatusMessage('All packages passed publish check!',
|
|
isError: false);
|
|
}
|
|
|
|
if (getBoolArg(_machineFlag)) {
|
|
_setStatus(status);
|
|
_machineOutput[_humanMessageKey] = _humanMessages;
|
|
print(_prettyJson(_machineOutput));
|
|
}
|
|
|
|
if (failedPackages.isNotEmpty) {
|
|
throw ToolExit(1);
|
|
}
|
|
}
|
|
|
|
Pubspec _tryParsePubspec(Directory package) {
|
|
final File pubspecFile = package.childFile('pubspec.yaml');
|
|
|
|
try {
|
|
return Pubspec.parse(pubspecFile.readAsStringSync());
|
|
} on Exception catch (exception) {
|
|
print(
|
|
'Failed to parse `pubspec.yaml` at ${pubspecFile.path}: $exception}',
|
|
);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
Future<bool> _hasValidPublishCheckRun(Directory package) async {
|
|
final io.Process process = await processRunner.start(
|
|
'flutter',
|
|
<String>['pub', 'publish', '--', '--dry-run'],
|
|
workingDirectory: package,
|
|
);
|
|
|
|
final StringBuffer outputBuffer = StringBuffer();
|
|
|
|
final Completer<void> stdOutCompleter = Completer<void>();
|
|
process.stdout.listen(
|
|
(List<int> event) {
|
|
final String output = String.fromCharCodes(event);
|
|
if (output.isNotEmpty) {
|
|
print(output);
|
|
outputBuffer.write(output);
|
|
}
|
|
},
|
|
onDone: () => stdOutCompleter.complete(),
|
|
);
|
|
|
|
final Completer<void> stdInCompleter = Completer<void>();
|
|
process.stderr.listen(
|
|
(List<int> event) {
|
|
final String output = String.fromCharCodes(event);
|
|
if (output.isNotEmpty) {
|
|
// The final result is always printed on stderr, whether success or
|
|
// failure.
|
|
final bool isError = !output.contains('has 0 warnings');
|
|
_printImportantStatusMessage(output, isError: isError);
|
|
outputBuffer.write(output);
|
|
}
|
|
},
|
|
onDone: () => stdInCompleter.complete(),
|
|
);
|
|
|
|
if (await process.exitCode == 0) {
|
|
return true;
|
|
}
|
|
|
|
if (!getBoolArg(_allowPrereleaseFlag)) {
|
|
return false;
|
|
}
|
|
|
|
await stdOutCompleter.future;
|
|
await stdInCompleter.future;
|
|
|
|
final String output = outputBuffer.toString();
|
|
return output.contains('Package has 1 warning') &&
|
|
output.contains(
|
|
'Packages with an SDK constraint on a pre-release of the Dart SDK should themselves be published as a pre-release version.');
|
|
}
|
|
|
|
Future<_PublishCheckResult> _passesPublishCheck(Directory package) async {
|
|
final String packageName = package.basename;
|
|
print('Checking that $packageName can be published.');
|
|
|
|
final Pubspec pubspec = _tryParsePubspec(package);
|
|
if (pubspec == null) {
|
|
print('no pubspec');
|
|
return _PublishCheckResult._error;
|
|
} else if (pubspec.publishTo == 'none') {
|
|
print('Package $packageName is marked as unpublishable. Skipping.');
|
|
return _PublishCheckResult._published;
|
|
}
|
|
|
|
final Version version = pubspec.version;
|
|
final _PublishCheckResult alreadyPublishedResult =
|
|
await _checkIfAlreadyPublished(
|
|
packageName: packageName, version: version);
|
|
if (alreadyPublishedResult == _PublishCheckResult._published) {
|
|
print(
|
|
'Package $packageName version: $version has already be published on pub.');
|
|
return alreadyPublishedResult;
|
|
} else if (alreadyPublishedResult == _PublishCheckResult._error) {
|
|
print('Check pub version failed $packageName');
|
|
return _PublishCheckResult._error;
|
|
}
|
|
|
|
if (await _hasValidPublishCheckRun(package)) {
|
|
print('Package $packageName is able to be published.');
|
|
return _PublishCheckResult._notPublished;
|
|
} else {
|
|
print('Unable to publish $packageName');
|
|
return _PublishCheckResult._error;
|
|
}
|
|
}
|
|
|
|
// Check if `packageName` already has `version` published on pub.
|
|
Future<_PublishCheckResult> _checkIfAlreadyPublished(
|
|
{String packageName, Version version}) async {
|
|
final PubVersionFinderResponse pubVersionFinderResponse =
|
|
await _pubVersionFinder.getPackageVersion(package: packageName);
|
|
_PublishCheckResult result;
|
|
switch (pubVersionFinderResponse.result) {
|
|
case PubVersionFinderResult.success:
|
|
result = pubVersionFinderResponse.versions.contains(version)
|
|
? _PublishCheckResult._published
|
|
: _PublishCheckResult._notPublished;
|
|
break;
|
|
case PubVersionFinderResult.fail:
|
|
print('''
|
|
Error fetching version on pub for $packageName.
|
|
HTTP Status ${pubVersionFinderResponse.httpResponse.statusCode}
|
|
HTTP response: ${pubVersionFinderResponse.httpResponse.body}
|
|
''');
|
|
result = _PublishCheckResult._error;
|
|
break;
|
|
case PubVersionFinderResult.noPackageFound:
|
|
result = _PublishCheckResult._notPublished;
|
|
break;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void _setStatus(String status) {
|
|
assert(_validStatus.contains(status));
|
|
_machineOutput[_statusKey] = status;
|
|
}
|
|
|
|
String _prettyJson(Map<String, dynamic> map) {
|
|
return const JsonEncoder.withIndent(' ').convert(_machineOutput);
|
|
}
|
|
|
|
void _printImportantStatusMessage(String message, {@required bool isError}) {
|
|
final String statusMessage = '${isError ? 'ERROR' : 'SUCCESS'}: $message';
|
|
if (getBoolArg(_machineFlag)) {
|
|
print(statusMessage);
|
|
} else {
|
|
final Colorize colorizedMessage = Colorize(statusMessage);
|
|
if (isError) {
|
|
colorizedMessage.red();
|
|
} else {
|
|
colorizedMessage.green();
|
|
}
|
|
print(colorizedMessage);
|
|
}
|
|
}
|
|
}
|
|
|
|
enum _PublishCheckResult {
|
|
_notPublished,
|
|
|
|
_published,
|
|
|
|
_error,
|
|
}
|