// 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. 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:pub_semver/pub_semver.dart'; import 'package:pubspec_parse/pubspec_parse.dart'; import 'common/core.dart'; import 'common/plugin_command.dart'; import 'common/process_runner.dart'; import 'common/pub_version_finder.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, { ProcessRunner processRunner = const ProcessRunner(), http.Client? httpClient, }) : _pubVersionFinder = PubVersionFinder(httpClient: httpClient ?? http.Client()), super(packagesDir, 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 _validStatus = [ _statusNeedsPublish, _statusMessageNoPublish, _statusMessageError ]; @override final String name = 'publish-check'; @override final String description = 'Checks to make sure that a plugin *could* be published.'; final PubVersionFinder _pubVersionFinder; // The output JSON when the _machineFlag is on. final Map _machineOutput = {}; final List _humanMessages = []; @override Future 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 _runCommand() async { final List failedPackages = []; 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 _hasValidPublishCheckRun(Directory package) async { final io.Process process = await processRunner.start( 'flutter', ['pub', 'publish', '--', '--dry-run'], workingDirectory: package, ); final StringBuffer outputBuffer = StringBuffer(); final Completer stdOutCompleter = Completer(); process.stdout.listen( (List event) { final String output = String.fromCharCodes(event); if (output.isNotEmpty) { print(output); outputBuffer.write(output); } }, onDone: () => stdOutCompleter.complete(), ); final Completer stdInCompleter = Completer(); process.stderr.listen( (List 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( {required String packageName, required Version? version}) async { final PubVersionFinderResponse pubVersionFinderResponse = await _pubVersionFinder.getPackageVersion(package: packageName); switch (pubVersionFinderResponse.result) { case PubVersionFinderResult.success: return pubVersionFinderResponse.versions.contains(version) ? _PublishCheckResult._published : _PublishCheckResult._notPublished; case PubVersionFinderResult.fail: print(''' Error fetching version on pub for $packageName. HTTP Status ${pubVersionFinderResponse.httpResponse.statusCode} HTTP response: ${pubVersionFinderResponse.httpResponse.body} '''); return _PublishCheckResult._error; case PubVersionFinderResult.noPackageFound: return _PublishCheckResult._notPublished; } } void _setStatus(String status) { assert(_validStatus.contains(status)); _machineOutput[_statusKey] = status; } String _prettyJson(Map 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, }