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 24 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
c5e17e9
added dt command to run license utility
mossmana Dec 12, 2024
a0d6000
committing update licenses
mossmana Dec 12, 2024
7d89b72
improved yaml example to remove extra newlines and properly handle va…
mossmana Dec 12, 2024
a376d60
fix comment in yaml
mossmana Dec 12, 2024
ced31a6
fixed another comment
mossmana Dec 12, 2024
2c50ba2
handling unconfigured extension type
mossmana Dec 13, 2024
d8753ee
attempt #2 at handling null
mossmana Dec 13, 2024
b6eaac6
attempt #3 at handling null
mossmana Dec 13, 2024
b83d338
attempt #4 at handling null
mossmana Dec 13, 2024
9fa3a5b
better error message
mossmana Dec 13, 2024
70930f5
removed print
mossmana Dec 13, 2024
8dbd992
improved logic for missing license header
mossmana Dec 17, 2024
7804f8c
commited config file
mossmana Dec 17, 2024
a261299
improved handling for extensions that are not configured
mossmana Dec 17, 2024
0f56e71
skip processing files if the license already exists
mossmana Dec 18, 2024
e5f78ef
added additional ignore paths
mossmana Dec 18, 2024
c179450
Workaround for dependency issue, fixed support for missing license an…
mossmana Jan 6, 2025
fdecff3
remove json and last_build_id
mossmana Jan 7, 2025
9b9fbd2
adding edge cases
mossmana Jan 13, 2025
d99d8c9
fixed formatting
mossmana Jan 13, 2025
63cbecb
corrected remove license content
mossmana Jan 14, 2025
d7d3849
excluding files that should retain original dart header
mossmana Jan 17, 2025
b48bf59
added bash header to remove and add_licenses
mossmana Jan 29, 2025
7daf33f
corrected remove_license
mossmana Jan 29, 2025
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
4 changes: 4 additions & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@ workspace:
dev_dependencies:
build_runner: ^2.3.3
flutter_lints: ^5.0.0

dependency_overrides:
analyzer: 6.11.0
dart_style: 2.3.3
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
168 changes: 122 additions & 46 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,14 +208,14 @@ 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) {
// Return just the license headers for the simple case with no stored
// value requested (i.e. content matches licenseText verbatim)
final storedName = _parseStoredName(replacementLicenseText);
if (content.contains(existingLicenseText)) {
final storedName = _parseStoredName(replacementLicenseText);
// Return just the license headers for the simple case with no stored
// value requested (i.e. content matches licenseText verbatim)
replacementLicenseText = replacementLicenseText.replaceAll(
'<$storedName>',
defaultStoredValue ?? DateTime.now().year.toString(),
Expand All @@ -221,17 +227,14 @@ class LicenseHeader {
}
// Return a non-empty map for the case where there is a stored value
// requested (i.e. when there is a '<value>' defined in the license text)
final storedName = _parseStoredName(existingLicenseText);
if (storedName.isNotEmpty) {
return _processHeaders(
storedName: storedName,
existingLicenseText: existingLicenseText,
replacementLicenseText: replacementLicenseText,
content: content,
);
}
return _processHeaders(
storedName: storedName,
existingLicenseText: existingLicenseText,
replacementLicenseText: replacementLicenseText,
content: content,
);
}
throw StateError('License header expected in ${file.path}, but not found!');
throw StateError('License header could not be added to ${file.path}');
}

/// Returns a copy of the given [file] with the [existingHeader] replaced by
Expand All @@ -254,6 +257,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 +295,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 +347,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: _parseStoredName(replacementLicenseText),
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,6 +376,22 @@ 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,
Expand Down Expand Up @@ -356,7 +428,11 @@ class LicenseHeader {
);
}
}
return const (existingHeader: '', replacementHeader: '');
final defaultReplacementHeader = replacementLicenseText.replaceAll(
'<$storedName>',
DateTime.now().year.toString(),
);
return (existingHeader: '', replacementHeader: defaultReplacementHeader);
}

// TODO(mossmana) Add support for multiple stored names
Expand Down
Loading
Loading