mirror of
https://github.com/flutter/packages.git
synced 2025-06-25 01:39:09 +08:00
258 lines
8.4 KiB
Dart
258 lines
8.4 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.
|
|
|
|
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:file/file.dart';
|
|
import 'package:git/git.dart';
|
|
import 'package:meta/meta.dart';
|
|
import 'package:path/path.dart' as p;
|
|
import 'package:yaml/yaml.dart';
|
|
|
|
import 'common.dart';
|
|
|
|
/// Wraps pub publish with a few niceties used by the flutter/plugin team.
|
|
///
|
|
/// 1. Checks for any modified files in git and refuses to publish if there's an
|
|
/// issue.
|
|
/// 2. Tags the release with the format <package-name>-v<package-version>.
|
|
/// 3. Pushes the release to a remote.
|
|
///
|
|
/// Both 2 and 3 are optional, see `plugin_tools help publish-plugin` for full
|
|
/// usage information.
|
|
///
|
|
/// [processRunner], [print], and [stdin] can be overriden for easier testing.
|
|
class PublishPluginCommand extends PluginCommand {
|
|
/// Creates an instance of the publish command.
|
|
PublishPluginCommand(
|
|
Directory packagesDir,
|
|
FileSystem fileSystem, {
|
|
ProcessRunner processRunner = const ProcessRunner(),
|
|
Print print = print,
|
|
Stdin stdinput,
|
|
}) : _print = print,
|
|
_stdin = stdinput ?? stdin,
|
|
super(packagesDir, fileSystem, processRunner: processRunner) {
|
|
argParser.addOption(
|
|
_packageOption,
|
|
help: 'The package to publish.'
|
|
'If the package directory name is different than its pubspec.yaml name, then this should specify the directory.',
|
|
);
|
|
argParser.addMultiOption(_pubFlagsOption,
|
|
help:
|
|
'A list of options that will be forwarded on to pub. Separate multiple flags with commas.');
|
|
argParser.addFlag(
|
|
_tagReleaseOption,
|
|
help: 'Whether or not to tag the release.',
|
|
defaultsTo: true,
|
|
negatable: true,
|
|
);
|
|
argParser.addFlag(
|
|
_pushTagsOption,
|
|
help:
|
|
'Whether or not tags should be pushed to a remote after creation. Ignored if tag-release is false.',
|
|
defaultsTo: true,
|
|
negatable: true,
|
|
);
|
|
argParser.addOption(
|
|
_remoteOption,
|
|
help:
|
|
'The name of the remote to push the tags to. Ignored if push-tags or tag-release is false.',
|
|
// Flutter convention is to use "upstream" for the single source of truth, and "origin" for personal forks.
|
|
defaultsTo: 'upstream',
|
|
);
|
|
}
|
|
|
|
static const String _packageOption = 'package';
|
|
static const String _tagReleaseOption = 'tag-release';
|
|
static const String _pushTagsOption = 'push-tags';
|
|
static const String _pubFlagsOption = 'pub-publish-flags';
|
|
static const String _remoteOption = 'remote';
|
|
|
|
// Version tags should follow <package-name>-v<semantic-version>. For example,
|
|
// `flutter_plugin_tools-v0.0.24`.
|
|
static const String _tagFormat = '%PACKAGE%-v%VERSION%';
|
|
|
|
@override
|
|
final String name = 'publish-plugin';
|
|
|
|
@override
|
|
final String description =
|
|
'Attempts to publish the given plugin and tag its release on GitHub.';
|
|
|
|
final Print _print;
|
|
final Stdin _stdin;
|
|
// The directory of the actual package that we are publishing.
|
|
StreamSubscription<String> _stdinSubscription;
|
|
|
|
@override
|
|
Future<void> run() async {
|
|
final String package = argResults[_packageOption] as String;
|
|
if (package == null) {
|
|
_print(
|
|
'Must specify a package to publish. See `plugin_tools help publish-plugin`.');
|
|
throw ToolExit(1);
|
|
}
|
|
|
|
_print('Checking local repo...');
|
|
if (!await GitDir.isGitDir(packagesDir.path)) {
|
|
_print('$packagesDir is not a valid Git repository.');
|
|
throw ToolExit(1);
|
|
}
|
|
|
|
final bool shouldPushTag = argResults[_pushTagsOption] == true;
|
|
final String remote = argResults[_remoteOption] as String;
|
|
String remoteUrl;
|
|
if (shouldPushTag) {
|
|
remoteUrl = await _verifyRemote(remote);
|
|
}
|
|
_print('Local repo is ready!');
|
|
|
|
final Directory packageDir = _getPackageDir(package);
|
|
await _publishPlugin(packageDir: packageDir);
|
|
if (argResults[_tagReleaseOption] as bool) {
|
|
await _tagRelease(
|
|
packageDir: packageDir,
|
|
remote: remote,
|
|
remoteUrl: remoteUrl,
|
|
shouldPushTag: shouldPushTag);
|
|
}
|
|
await _finishSuccesfully();
|
|
}
|
|
|
|
Future<void> _publishPlugin({@required Directory packageDir}) async {
|
|
await _checkGitStatus(packageDir);
|
|
await _publish(packageDir);
|
|
_print('Package published!');
|
|
}
|
|
|
|
Future<void> _tagRelease(
|
|
{@required Directory packageDir,
|
|
@required String remote,
|
|
@required String remoteUrl,
|
|
@required bool shouldPushTag}) async {
|
|
final String tag = _getTag(packageDir);
|
|
_print('Tagging release $tag...');
|
|
await processRunner.run(
|
|
'git',
|
|
<String>['tag', tag],
|
|
workingDir: packageDir,
|
|
exitOnError: true,
|
|
logOnError: true,
|
|
);
|
|
if (!shouldPushTag) {
|
|
return;
|
|
}
|
|
|
|
_print('Pushing tag to $remote...');
|
|
await _pushTagToRemote(remote: remote, tag: tag, remoteUrl: remoteUrl);
|
|
}
|
|
|
|
Future<void> _finishSuccesfully() async {
|
|
await _stdinSubscription.cancel();
|
|
_print('Done!');
|
|
}
|
|
|
|
// Returns the packageDirectory based on the package name.
|
|
// Throws ToolExit if the `package` doesn't exist.
|
|
Directory _getPackageDir(String package) {
|
|
final Directory packageDir = packagesDir.childDirectory(package);
|
|
if (!packageDir.existsSync()) {
|
|
_print('${packageDir.absolute.path} does not exist.');
|
|
throw ToolExit(1);
|
|
}
|
|
return packageDir;
|
|
}
|
|
|
|
Future<void> _checkGitStatus(Directory packageDir) async {
|
|
final ProcessResult statusResult = await processRunner.run(
|
|
'git',
|
|
<String>['status', '--porcelain', '--ignored', packageDir.absolute.path],
|
|
workingDir: packageDir,
|
|
logOnError: true,
|
|
exitOnError: true,
|
|
);
|
|
|
|
final String statusOutput = statusResult.stdout as String;
|
|
if (statusOutput.isNotEmpty) {
|
|
_print(
|
|
"There are files in the package directory that haven't been saved in git. Refusing to publish these files:\n\n"
|
|
'$statusOutput\n'
|
|
'If the directory should be clean, you can run `git clean -xdf && git reset --hard HEAD` to wipe all local changes.');
|
|
throw ToolExit(1);
|
|
}
|
|
}
|
|
|
|
Future<String> _verifyRemote(String remote) async {
|
|
final ProcessResult remoteInfo = await processRunner.run(
|
|
'git',
|
|
<String>['remote', 'get-url', remote],
|
|
workingDir: packagesDir,
|
|
exitOnError: true,
|
|
logOnError: true,
|
|
);
|
|
return remoteInfo.stdout as String;
|
|
}
|
|
|
|
Future<void> _publish(Directory packageDir) async {
|
|
final List<String> publishFlags =
|
|
argResults[_pubFlagsOption] as List<String>;
|
|
_print(
|
|
'Running `pub publish ${publishFlags.join(' ')}` in ${packageDir.absolute.path}...\n');
|
|
final Process publish = await processRunner.start(
|
|
'flutter', <String>['pub', 'publish'] + publishFlags,
|
|
workingDirectory: packageDir);
|
|
publish.stdout
|
|
.transform(utf8.decoder)
|
|
.listen((String data) => _print(data));
|
|
publish.stderr
|
|
.transform(utf8.decoder)
|
|
.listen((String data) => _print(data));
|
|
_stdinSubscription = _stdin
|
|
.transform(utf8.decoder)
|
|
.listen((String data) => publish.stdin.writeln(data));
|
|
final int result = await publish.exitCode;
|
|
if (result != 0) {
|
|
_print('Publish failed. Exiting.');
|
|
throw ToolExit(result);
|
|
}
|
|
}
|
|
|
|
String _getTag(Directory packageDir) {
|
|
final File pubspecFile =
|
|
fileSystem.file(p.join(packageDir.path, 'pubspec.yaml'));
|
|
final YamlMap pubspecYaml =
|
|
loadYaml(pubspecFile.readAsStringSync()) as YamlMap;
|
|
final String name = pubspecYaml['name'] as String;
|
|
final String version = pubspecYaml['version'] as String;
|
|
// We should have failed to publish if these were unset.
|
|
assert(name.isNotEmpty && version.isNotEmpty);
|
|
return _tagFormat
|
|
.replaceAll('%PACKAGE%', name)
|
|
.replaceAll('%VERSION%', version);
|
|
}
|
|
|
|
Future<void> _pushTagToRemote(
|
|
{@required String remote,
|
|
@required String tag,
|
|
@required String remoteUrl}) async {
|
|
assert(remote != null && tag != null && remoteUrl != null);
|
|
_print('Ready to push $tag to $remoteUrl (y/n)?');
|
|
final String input = _stdin.readLineSync();
|
|
if (input.toLowerCase() != 'y') {
|
|
_print('Tag push canceled.');
|
|
throw ToolExit(1);
|
|
}
|
|
await processRunner.run(
|
|
'git',
|
|
<String>['push', remote, tag],
|
|
workingDir: packagesDir,
|
|
exitOnError: true,
|
|
logOnError: true,
|
|
);
|
|
}
|
|
}
|