mirror of
https://github.com/flutter/packages.git
synced 2025-06-17 02:48:43 +08:00
[flutter_plugin_tools] Adds update-excerpts command (#5339)
This commit is contained in:
225
script/tool/lib/src/update_excerpts_command.dart
Normal file
225
script/tool/lib/src/update_excerpts_command.dart
Normal file
@ -0,0 +1,225 @@
|
||||
// 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:io' as io;
|
||||
|
||||
import 'package:file/file.dart';
|
||||
import 'package:flutter_plugin_tools/src/common/core.dart';
|
||||
import 'package:git/git.dart';
|
||||
import 'package:platform/platform.dart';
|
||||
import 'package:yaml/yaml.dart';
|
||||
import 'package:yaml_edit/yaml_edit.dart';
|
||||
|
||||
import 'common/package_looping_command.dart';
|
||||
import 'common/process_runner.dart';
|
||||
import 'common/repository_package.dart';
|
||||
|
||||
/// A command to update README code excerpts from code files.
|
||||
class UpdateExcerptsCommand extends PackageLoopingCommand {
|
||||
/// Creates a excerpt updater command instance.
|
||||
UpdateExcerptsCommand(
|
||||
Directory packagesDir, {
|
||||
ProcessRunner processRunner = const ProcessRunner(),
|
||||
Platform platform = const LocalPlatform(),
|
||||
GitDir? gitDir,
|
||||
}) : super(
|
||||
packagesDir,
|
||||
processRunner: processRunner,
|
||||
platform: platform,
|
||||
gitDir: gitDir,
|
||||
) {
|
||||
argParser.addFlag(_failOnChangeFlag, hide: true);
|
||||
}
|
||||
|
||||
static const String _failOnChangeFlag = 'fail-on-change';
|
||||
|
||||
static const String _buildRunnerConfigName = 'excerpt';
|
||||
// The name of the build_runner configuration file that will be in an example
|
||||
// directory if the package is set up to use `code-excerpt`.
|
||||
static const String _buildRunnerConfigFile =
|
||||
'build.$_buildRunnerConfigName.yaml';
|
||||
|
||||
// The relative directory path to put the extracted excerpt yaml files.
|
||||
static const String _excerptOutputDir = 'excerpts';
|
||||
|
||||
// The filename to store the pre-modification copy of the pubspec.
|
||||
static const String _originalPubspecFilename =
|
||||
'pubspec.plugin_tools_original.yaml';
|
||||
|
||||
@override
|
||||
final String name = 'update-excerpts';
|
||||
|
||||
@override
|
||||
final String description = 'Updates code excerpts in README.md files, based '
|
||||
'on code from code files, via code-excerpt';
|
||||
|
||||
@override
|
||||
Future<PackageResult> runForPackage(RepositoryPackage package) async {
|
||||
final Iterable<RepositoryPackage> configuredExamples = package
|
||||
.getExamples()
|
||||
.where((RepositoryPackage example) =>
|
||||
example.directory.childFile(_buildRunnerConfigFile).existsSync());
|
||||
|
||||
if (configuredExamples.isEmpty) {
|
||||
return PackageResult.skip(
|
||||
'No $_buildRunnerConfigFile found in example(s).');
|
||||
}
|
||||
|
||||
final Directory repoRoot =
|
||||
packagesDir.fileSystem.directory((await gitDir).path);
|
||||
|
||||
for (final RepositoryPackage example in configuredExamples) {
|
||||
_addSubmoduleDependencies(example, repoRoot: repoRoot);
|
||||
|
||||
try {
|
||||
// Ensure that dependencies are available.
|
||||
final int pubGetExitCode = await processRunner.runAndStream(
|
||||
'dart', <String>['pub', 'get'],
|
||||
workingDir: example.directory);
|
||||
if (pubGetExitCode != 0) {
|
||||
return PackageResult.fail(
|
||||
<String>['Unable to get script dependencies']);
|
||||
}
|
||||
|
||||
// Update the excerpts.
|
||||
if (!await _extractSnippets(example)) {
|
||||
return PackageResult.fail(<String>['Unable to extract excerpts']);
|
||||
}
|
||||
if (!await _injectSnippets(example, targetPackage: package)) {
|
||||
return PackageResult.fail(<String>['Unable to inject excerpts']);
|
||||
}
|
||||
} finally {
|
||||
// Clean up the pubspec changes and extracted excerpts directory.
|
||||
_undoPubspecChanges(example);
|
||||
final Directory excerptDirectory =
|
||||
example.directory.childDirectory(_excerptOutputDir);
|
||||
if (excerptDirectory.existsSync()) {
|
||||
excerptDirectory.deleteSync(recursive: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (getBoolArg(_failOnChangeFlag)) {
|
||||
final String? stateError = await _validateRepositoryState();
|
||||
if (stateError != null) {
|
||||
printError('README.md is out of sync with its source excerpts.\n\n'
|
||||
'If you edited code in README.md directly, you should instead edit '
|
||||
'the example source files. If you edited source files, run the '
|
||||
'repository tooling\'s "$name" command on this package, and update '
|
||||
'your PR with the resulting changes.');
|
||||
return PackageResult.fail(<String>[stateError]);
|
||||
}
|
||||
}
|
||||
|
||||
return PackageResult.success();
|
||||
}
|
||||
|
||||
/// Runs the extraction step to create the excerpt files for the given
|
||||
/// example, returning true on success.
|
||||
Future<bool> _extractSnippets(RepositoryPackage example) async {
|
||||
final int exitCode = await processRunner.runAndStream(
|
||||
'dart',
|
||||
<String>[
|
||||
'run',
|
||||
'build_runner',
|
||||
'build',
|
||||
'--config',
|
||||
_buildRunnerConfigName,
|
||||
'--output',
|
||||
_excerptOutputDir,
|
||||
'--delete-conflicting-outputs',
|
||||
],
|
||||
workingDir: example.directory);
|
||||
return exitCode == 0;
|
||||
}
|
||||
|
||||
/// Runs the injection step to update [targetPackage]'s README with the latest
|
||||
/// excerpts from [example], returning true on success.
|
||||
Future<bool> _injectSnippets(
|
||||
RepositoryPackage example, {
|
||||
required RepositoryPackage targetPackage,
|
||||
}) async {
|
||||
final String relativeReadmePath =
|
||||
getRelativePosixPath(targetPackage.readmeFile, from: example.directory);
|
||||
final int exitCode = await processRunner.runAndStream(
|
||||
'dart',
|
||||
<String>[
|
||||
'run',
|
||||
'code_excerpt_updater',
|
||||
'--write-in-place',
|
||||
'--yaml',
|
||||
'--no-escape-ng-interpolation',
|
||||
relativeReadmePath,
|
||||
],
|
||||
workingDir: example.directory);
|
||||
return exitCode == 0;
|
||||
}
|
||||
|
||||
/// Adds `code_excerpter` and `code_excerpt_updater` to [package]'s
|
||||
/// `dev_dependencies` using path-based references to the submodule copies.
|
||||
///
|
||||
/// This is done on the fly rather than being checked in so that:
|
||||
/// - Just building examples don't require everyone to check out submodules.
|
||||
/// - Examples can be analyzed/built even on versions of Flutter that these
|
||||
/// submodules do not support.
|
||||
void _addSubmoduleDependencies(RepositoryPackage package,
|
||||
{required Directory repoRoot}) {
|
||||
final String pubspecContents = package.pubspecFile.readAsStringSync();
|
||||
// Save aside a copy of the current pubspec state. This allows restoration
|
||||
// to the previous state regardless of its git status at the time the script
|
||||
// ran.
|
||||
package.directory
|
||||
.childFile(_originalPubspecFilename)
|
||||
.writeAsStringSync(pubspecContents);
|
||||
|
||||
// Update the actual pubspec.
|
||||
final YamlEditor editablePubspec = YamlEditor(pubspecContents);
|
||||
const String devDependenciesKey = 'dev_dependencies';
|
||||
final YamlNode root = editablePubspec.parseAt(<String>[]);
|
||||
// Ensure that there's a `dev_dependencies` entry to update.
|
||||
if ((root as YamlMap)[devDependenciesKey] == null) {
|
||||
editablePubspec.update(<String>['dev_dependencies'], YamlMap());
|
||||
}
|
||||
final Set<String> submoduleDependencies = <String>{
|
||||
'code_excerpter',
|
||||
'code_excerpt_updater',
|
||||
};
|
||||
final String relativeRootPath =
|
||||
getRelativePosixPath(repoRoot, from: package.directory);
|
||||
for (final String dependency in submoduleDependencies) {
|
||||
editablePubspec.update(<String>[
|
||||
devDependenciesKey,
|
||||
dependency
|
||||
], <String, String>{
|
||||
'path': '$relativeRootPath/site-shared/packages/$dependency'
|
||||
});
|
||||
}
|
||||
package.pubspecFile.writeAsStringSync(editablePubspec.toString());
|
||||
}
|
||||
|
||||
/// Restores the version of the pubspec that was present before running
|
||||
/// [_addSubmoduleDependencies].
|
||||
void _undoPubspecChanges(RepositoryPackage package) {
|
||||
package.directory
|
||||
.childFile(_originalPubspecFilename)
|
||||
.renameSync(package.pubspecFile.path);
|
||||
}
|
||||
|
||||
/// Checks the git state, returning an error string unless nothing has
|
||||
/// changed.
|
||||
Future<String?> _validateRepositoryState() async {
|
||||
final io.ProcessResult modifiedFiles = await processRunner.run(
|
||||
'git',
|
||||
<String>['ls-files', '--modified'],
|
||||
workingDir: packagesDir,
|
||||
logOnError: true,
|
||||
);
|
||||
if (modifiedFiles.exitCode != 0) {
|
||||
return 'Unable to determine local file state';
|
||||
}
|
||||
|
||||
final String stdout = modifiedFiles.stdout as String;
|
||||
return stdout.trim().isEmpty ? null : 'Snippets are out of sync';
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user