Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[devtools] Add update license command #8644

Draft
wants to merge 16 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions tool/lib/commands/update_licenses.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright 2024 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
import 'dart:io';

import 'package:args/command_runner.dart';
import 'package:cli_util/cli_logging.dart';
import 'package:path/path.dart' as p;

import '../license_utils.dart';

const _argConfig = 'config';
const _argDirectory = 'directory';
const _dryRun = 'dry-run';

/// This command updates license headers for the configured files.
///
/// The config file is a YAML file as defined in [LicenseConfig].
///
/// If directory is not set, it will default to the current directory.
///
/// When the '--dry-run' flag is passed in, a list of files to update will
/// be logged, but no files will be modified.
///
/// To run this script
/// `dt update-licenses [--f <config-file>] [--d <directory>] [--dry-run]`
class UpdateLicensesCommand extends Command {
UpdateLicensesCommand() {
argParser
..addOption(
_argConfig,
abbr: 'c',
defaultsTo: p.join(Directory.current.path, 'update_licenses.yaml'),
help:
'The path to the YAML license config file. Defaults to '
'update_licenses.yaml',
)
..addOption(
_argDirectory,
defaultsTo: Directory.current.path,
abbr: 'd',
help: 'Update license headers for files in the directory.',
)
..addFlag(
_dryRun,
negatable: false,
defaultsTo: false,
help:
'If set, log a list of files that require an update, but do not '
'modify any files.',
);
}

@override
String get description => 'Update license headers as configured.';

@override
String get name => 'update-licenses';

@override
Future run() async {
final config = LicenseConfig.fromYamlFile(
File(argResults![_argConfig] as String),
);
final directory = Directory(argResults![_argDirectory] as String);
final dryRun = argResults![_dryRun] as bool;
final log = Logger.standard();
final header = LicenseHeader();
final results = await header.bulkUpdate(
directory: directory,
config: config,
dryRun: dryRun,
);
final updatedPaths = results.updatedPaths;
final prefix = dryRun ? 'Requires update: ' : 'Updated: ';
log.stdout('$prefix ${updatedPaths.join(", ")}');
}
}
2 changes: 2 additions & 0 deletions tool/lib/devtools_command_runner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import 'package:devtools_tool/commands/serve.dart';
import 'package:devtools_tool/commands/sync.dart';
import 'package:devtools_tool/commands/tag_version.dart';
import 'package:devtools_tool/commands/update_flutter_sdk.dart';
import 'package:devtools_tool/commands/update_licenses.dart';
import 'package:devtools_tool/commands/update_perfetto.dart';
import 'package:devtools_tool/model.dart';

Expand Down Expand Up @@ -47,6 +48,7 @@ class DevToolsCommandRunner extends CommandRunner {
addCommand(UpdateDartSdkDepsCommand());
addCommand(UpdateDevToolsVersionCommand());
addCommand(UpdateFlutterSdkCommand());
addCommand(UpdateLicensesCommand());
addCommand(UpdatePerfettoCommand());

argParser.addFlag(
Expand Down
146 changes: 114 additions & 32 deletions tool/lib/license_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,27 +25,27 @@ import 'package:yaml/yaml.dart';
/// ```yaml
/// # sequence of license text strings that should be matched against at the top of a file and removed. <value>, which normally represents a date, will be stored.
/// remove_licenses:
/// - |
/// // This is some <value1> multiline license
/// - |-
/// // This is some <value> multiline license
/// // text that should be removed from the file.
/// - |
/// /* This is other <value2> multiline license
/// - |-
/// /* This is other <value> multiline license
/// text that should be removed from the file. */
/// - |
/// # This is more <value3> multiline license
/// - |-
/// # This is more <value> multiline license
/// # text that should be removed from the file.
/// - |
/// - |-
/// // This is some multiline license text to
/// // remove that does not contain a stored value.
/// # sequence of license text strings that should be added to the top of a file. {value} will be replaced.
/// # sequence of license text strings that should be added to the top of a file. <value> will be replaced.
/// add_licenses:
/// - |
/// // This is some <value1> multiline license
/// - |-
/// // This is some <value> multiline license
/// // text that should be added to the file.
/// - |
/// # This is other <value3> multiline license
/// - |-
/// # This is other <value> multiline license
/// # text that should be added to the file.
/// - |
/// - |-
/// // This is some multiline license text to
/// // add that does not contain a stored value.
/// # defines which files should have license text added or updated.
Expand Down Expand Up @@ -121,17 +121,23 @@ class LicenseConfig {
final YamlMap fileTypes;

/// Returns the list of indices for the given [ext] of [removeLicenses]
/// containing the license text to remove.
/// containing the license text to remove if they exist or an empty YamlList.
YamlList getRemoveIndicesForExtension(String ext) {
final fileType = fileTypes[_removeDotFromExtension(ext)];
return fileType['remove'] as YamlList;
if (fileType != null) {
return fileType['remove'] as YamlList;
}
return YamlList();
}

/// Returns the index for the given [ext] of [addLicenses] containing the
/// license text to add.
/// license text to add if it exists or -1.
int getAddIndexForExtension(String ext) {
final fileType = fileTypes[_removeDotFromExtension(ext)];
return fileType['add'];
if (fileType != null) {
return fileType['add'];
}
return -1;
}

/// Returns whether the file should be excluded according to the config.
Expand Down Expand Up @@ -202,7 +208,7 @@ class LicenseHeader {
.handleError(
(e) =>
throw StateError(
'License header expected, but error reading file - $e',
'License header expected, but error reading $file - $e',
),
);
await for (final content in stream) {
Expand Down Expand Up @@ -254,6 +260,24 @@ class LicenseHeader {
return rewrittenFile;
}

/// Returns a copy of the given [file] that is missing a license header
/// with the [replacementHeader] added to the top.
///
/// Reads and writes the entire file contents all at once, so performance may
/// degrade for large files.
File addLicenseHeader({
required File file,
required String replacementHeader,
}) {
final rewrittenFile = File('${file.path}.tmp');
final contents = file.readAsStringSync();
rewrittenFile.writeAsStringSync(
'$replacementHeader${Platform.lineTerminator}$contents',
flush: true,
);
return rewrittenFile;
}

/// Bulk update license headers for files in the [directory] as configured
/// in the [config] and return a processed paths Record containing:
/// - list of included paths
Expand All @@ -274,27 +298,50 @@ class LicenseHeader {
if (!config.shouldExclude(file)) {
includedPathsList.add(file.path);
final extension = p.extension(file.path);
final addIndex = config.getAddIndexForExtension(extension);
if (addIndex == -1) {
// skip if add index doesn't exist for extension
continue;
}
final fileLength = file.lengthSync();
const bufferSize = 20;
final replacementLicenseText = config.addLicenses[addIndex];
final byteCount = min(
bufferSize + replacementLicenseText.length,
fileLength,
);
var replacementInfo = await getReplacementInfo(
file: file,
existingLicenseText: replacementLicenseText,
replacementLicenseText: replacementLicenseText,
byteCount: byteCount as int,
);
if (replacementInfo.existingHeader.isNotEmpty &&
replacementInfo.replacementHeader.isNotEmpty &&
replacementInfo.existingHeader ==
replacementInfo.replacementHeader) {
// Do nothing if the replacement header is the same as the
// existing header
continue;
}
final removeIndices = config.getRemoveIndicesForExtension(extension);
for (final removeIndex in removeIndices) {
final existingLicenseText = config.removeLicenses[removeIndex];
final addIndex = config.getAddIndexForExtension(extension);
final replacementLicenseText = config.addLicenses[addIndex];
final fileLength = file.lengthSync();
const bufferSize = 20;
// Assume that the license text will be near the start of the file,
// but add in some buffer.
final byteCount = min(
bufferSize + existingLicenseText.length,
fileLength,
);
final replacementInfo = await getReplacementInfo(
replacementInfo = await getReplacementInfo(
file: file,
existingLicenseText: existingLicenseText,
replacementLicenseText: replacementLicenseText,
byteCount: byteCount as int,
);
if (replacementInfo.existingHeader.isNotEmpty &&
replacementInfo.replacementHeader.isNotEmpty) {
// Case 1: Existing header needs to be replaced
if (dryRun) {
updatedPathsList.add(file.path);
} else {
Expand All @@ -303,15 +350,27 @@ class LicenseHeader {
existingHeader: replacementInfo.existingHeader,
replacementHeader: replacementInfo.replacementHeader,
);
if (rewrittenFile.lengthSync() > 0) {
file.writeAsStringSync(
rewrittenFile.readAsStringSync(),
mode: FileMode.writeOnly,
flush: true,
);
updatedPathsList.add(file.path);
}
rewrittenFile.deleteSync();
_updateLicense(rewrittenFile, file, updatedPathsList);
}
}
}
if (!updatedPathsList.contains(file.path)) {
final licenseHeaders = _processHeaders(
storedName: '',
existingLicenseText: '',
replacementLicenseText: replacementLicenseText,
content: '',
);
if (licenseHeaders.replacementHeader.isNotEmpty) {
// Case 2: Missing header needs to be added
if (dryRun) {
updatedPathsList.add(file.path);
} else {
final rewrittenFile = addLicenseHeader(
file: file,
replacementHeader: licenseHeaders.replacementHeader,
);
_updateLicense(rewrittenFile, file, updatedPathsList);
}
}
}
Expand All @@ -320,12 +379,35 @@ class LicenseHeader {
return (includedPaths: includedPathsList, updatedPaths: updatedPathsList);
}

void _updateLicense(
File rewrittenFile,
File file,
List<String> updatedPathsList,
) {
if (rewrittenFile.lengthSync() > 0) {
file.writeAsStringSync(
rewrittenFile.readAsStringSync(),
mode: FileMode.writeOnly,
flush: true,
);
updatedPathsList.add(file.path);
}
rewrittenFile.deleteSync();
}

({String existingHeader, String replacementHeader}) _processHeaders({
required String storedName,
required String existingLicenseText,
required String replacementLicenseText,
required String content,
}) {
if (existingLicenseText.isEmpty) {
final defaultReplacementHeader = replacementLicenseText.replaceAll(
'<$storedName>',
DateTime.now().year.toString(),
);
return (existingHeader: '', replacementHeader: defaultReplacementHeader);
}
final matchStr = RegExp.escape(existingLicenseText);
final storedNameIndex = matchStr.indexOf('<$storedName>');
if (storedNameIndex != -1) {
Expand Down
Loading
Loading