195 lines
6.4 KiB
Dart
195 lines
6.4 KiB
Dart
// SPDX-FileCopyrightText: 2026 FocusFlow contributors
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
/// Runs the local SPDX and REUSE metadata validation tool.
|
|
library;
|
|
|
|
import 'dart:io';
|
|
|
|
/// SPDX expression used by package and app source metadata.
|
|
///
|
|
/// The top-level `LICENSE.md` contains the GNU Affero General Public License
|
|
/// version 3 text, so the repository uses the corresponding SPDX identifier.
|
|
const _licenseIdentifier = 'AGPL-3.0-only';
|
|
|
|
/// Repository-local copyright text used in SPDX headers.
|
|
///
|
|
/// The project uses a contributor-oriented copyright holder because individual
|
|
/// source files do not currently carry more specific ownership notices.
|
|
const _copyrightText = '2026 FocusFlow contributors';
|
|
|
|
/// Package directories that must include a pointer to the root license file.
|
|
///
|
|
/// Each Dart package is publishable as its own unit even though publishing is
|
|
/// disabled, so every package folder carries a small `LICENSE.md` reference.
|
|
const _packageDirectories = <String>[
|
|
'packages/scheduler_backup',
|
|
'packages/scheduler_core',
|
|
'packages/scheduler_export',
|
|
'packages/scheduler_export_json',
|
|
'packages/scheduler_integration_tests',
|
|
'packages/scheduler_notifications',
|
|
'packages/scheduler_notifications_desktop',
|
|
'packages/scheduler_persistence',
|
|
'packages/scheduler_persistence_memory',
|
|
'packages/scheduler_persistence_sqlite',
|
|
];
|
|
|
|
/// App directories that must include a pointer to the root license file.
|
|
///
|
|
/// The Flutter app is outside the root Dart workspace, but it still ships with
|
|
/// repository code and therefore follows the same licensing metadata rules.
|
|
const _appDirectories = <String>['apps/focus_flow_flutter'];
|
|
|
|
/// Validates SPDX headers, REUSE metadata, and package/app license pointers.
|
|
void main(List<String> arguments) {
|
|
final failures = <String>[
|
|
..._reuseTomlFailures(),
|
|
..._licensePointerFailures(),
|
|
..._spdxHeaderFailures(),
|
|
];
|
|
|
|
if (failures.isEmpty) {
|
|
stdout.writeln('SPDX and REUSE metadata checks passed.');
|
|
return;
|
|
}
|
|
|
|
stderr.writeln('SPDX and REUSE metadata checks failed:');
|
|
for (final failure in failures) {
|
|
stderr.writeln('- $failure');
|
|
}
|
|
exitCode = 1;
|
|
}
|
|
|
|
/// Checks that `REUSE.toml` exists and covers package/app trees.
|
|
///
|
|
/// This does not replace the upstream REUSE tool, but it keeps the repository's
|
|
/// expected local metadata shape from drifting when contributors add files.
|
|
List<String> _reuseTomlFailures() {
|
|
final file = File('REUSE.toml');
|
|
if (!file.existsSync()) {
|
|
return <String>['REUSE.toml is missing.'];
|
|
}
|
|
|
|
final contents = file.readAsStringSync();
|
|
final failures = <String>[];
|
|
for (final required in <String>[
|
|
'packages/**',
|
|
'apps/focus_flow_flutter/**',
|
|
'SPDX-License-Identifier = "$_licenseIdentifier"',
|
|
'SPDX-FileCopyrightText = "$_copyrightText"',
|
|
]) {
|
|
if (!contents.contains(required)) {
|
|
failures.add('REUSE.toml does not contain `$required`.');
|
|
}
|
|
}
|
|
return failures;
|
|
}
|
|
|
|
/// Checks package and app `LICENSE.md` pointer files.
|
|
///
|
|
/// Pointer files keep each package understandable when opened in isolation while
|
|
/// still making the root `LICENSE.md` the single license text source.
|
|
List<String> _licensePointerFailures() {
|
|
final failures = <String>[];
|
|
for (final directory in <String>[
|
|
..._packageDirectories,
|
|
..._appDirectories
|
|
]) {
|
|
final file = File('$directory/LICENSE.md');
|
|
if (!file.existsSync()) {
|
|
failures.add('$directory/LICENSE.md is missing.');
|
|
continue;
|
|
}
|
|
final contents = file.readAsStringSync();
|
|
if (!contents.contains('../../LICENSE.md')) {
|
|
failures
|
|
.add('$directory/LICENSE.md does not reference ../../LICENSE.md.');
|
|
}
|
|
if (!contents.contains('SPDX-License-Identifier: $_licenseIdentifier')) {
|
|
failures.add('$directory/LICENSE.md is missing its SPDX license header.');
|
|
}
|
|
}
|
|
return failures;
|
|
}
|
|
|
|
/// Checks safe text files for inline SPDX headers.
|
|
///
|
|
/// Binary files, JSON resources, generated platform project files, and XML/plist
|
|
/// files are covered by `REUSE.toml` annotations because adding comments to
|
|
/// those formats can either be invalid or be overwritten by platform tooling.
|
|
List<String> _spdxHeaderFailures() {
|
|
final failures = <String>[];
|
|
for (final path in _trackedFiles()) {
|
|
if (!_requiresInlineSpdxHeader(path)) {
|
|
continue;
|
|
}
|
|
final contents = File(path).readAsStringSync();
|
|
if (!contents.contains('SPDX-License-Identifier: $_licenseIdentifier')) {
|
|
failures.add('$path is missing SPDX-License-Identifier.');
|
|
}
|
|
if (!contents.contains('SPDX-FileCopyrightText: $_copyrightText')) {
|
|
failures.add('$path is missing SPDX-FileCopyrightText.');
|
|
}
|
|
}
|
|
return failures;
|
|
}
|
|
|
|
/// Returns tracked repository files in stable path order.
|
|
///
|
|
/// Git is used so ignored build output and local editor files do not affect the
|
|
/// metadata check.
|
|
List<String> _trackedFiles() {
|
|
final result = Process.runSync(
|
|
'git',
|
|
<String>['ls-files'],
|
|
runInShell: Platform.isWindows,
|
|
);
|
|
if (result.exitCode != 0) {
|
|
throw ProcessException(
|
|
'git',
|
|
<String>['ls-files'],
|
|
'Unable to list tracked files.',
|
|
result.exitCode,
|
|
);
|
|
}
|
|
return (result.stdout as String)
|
|
.split('\n')
|
|
.where((path) => path.trim().isNotEmpty)
|
|
.toList(growable: false)
|
|
..sort();
|
|
}
|
|
|
|
/// Reports whether [path] should carry an inline SPDX header.
|
|
///
|
|
/// The allow-list is intentionally conservative and includes only formats where
|
|
/// adding a leading comment is safe and unlikely to break generated tooling.
|
|
bool _requiresInlineSpdxHeader(String path) {
|
|
if (!_isComplianceScope(path)) {
|
|
return false;
|
|
}
|
|
if (path.endsWith('.g.dart') || path.endsWith('LICENSE.md')) {
|
|
return false;
|
|
}
|
|
return path.endsWith('.dart') ||
|
|
path.endsWith('.yaml') ||
|
|
path.endsWith('.yml') ||
|
|
path.endsWith('.sh') ||
|
|
path.endsWith('.md');
|
|
}
|
|
|
|
/// Reports whether [path] belongs to the checked compliance surface.
|
|
///
|
|
/// The package and app folders are the main requested scope; root scripts and
|
|
/// tool files are included because they enforce the same standards.
|
|
bool _isComplianceScope(String path) {
|
|
return path.startsWith('packages/') ||
|
|
path.startsWith('apps/focus_flow_flutter/') ||
|
|
path.startsWith('scripts/') ||
|
|
path.startsWith('tool/') ||
|
|
path == 'AGENTS.md' ||
|
|
path == 'Focus Flow Contributors.md' ||
|
|
path == 'pubspec.yaml' ||
|
|
path == 'analysis_options.yaml' ||
|
|
path == '.github/workflows/ci.yml';
|
|
}
|