CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
330 lines
10 KiB
Python
330 lines
10 KiB
Python
#!/usr/bin/env python3
|
|
# Copyright (c) Advanced Micro Devices, Inc.
|
|
# SPDX-License-Identifier: MIT
|
|
|
|
"""Replace long-form MIT license headers with SPDX format and add where missing.
|
|
|
|
Processes .cpp, .hpp, .py, .cmake, and CMakeLists.txt files.
|
|
Skips build/, external/, and .git/ directories.
|
|
"""
|
|
|
|
import argparse
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
SPDX_COPYRIGHT = "Copyright (c) Advanced Micro Devices, Inc."
|
|
SPDX_LICENSE = "SPDX-License-Identifier: MIT"
|
|
|
|
DEFAULT_EXCLUDED_DIRS = {"build", "external", "examples", ".git"}
|
|
|
|
# Patterns to match the long-form MIT license block.
|
|
# The block starts with "MIT License" and ends with a line containing "SOFTWARE."
|
|
MIT_LONG_FORM_MARKERS = [
|
|
"MIT License",
|
|
"Permission is hereby granted, free of charge",
|
|
"THE SOFTWARE IS PROVIDED",
|
|
]
|
|
|
|
|
|
def make_spdx_header(comment_prefix):
|
|
"""Return the 2-line SPDX header with the given comment prefix."""
|
|
return f"{comment_prefix} {SPDX_COPYRIGHT}\n" f"{comment_prefix} {SPDX_LICENSE}\n"
|
|
|
|
|
|
def get_comment_prefix(filepath):
|
|
"""Return the comment prefix for the file type."""
|
|
if filepath.endswith((".cpp", ".hpp")):
|
|
return "//"
|
|
if (
|
|
filepath.endswith((".py", ".cmake"))
|
|
or os.path.basename(filepath) == "CMakeLists.txt"
|
|
):
|
|
return "#"
|
|
return None
|
|
|
|
|
|
def strip_comment(line, prefix):
|
|
"""Strip comment prefix from a line and return the text content."""
|
|
stripped = line.rstrip("\n")
|
|
if stripped.startswith(prefix):
|
|
return stripped[len(prefix) :]
|
|
return None
|
|
|
|
|
|
def find_long_mit_block(lines, prefix):
|
|
"""Find the start and end indices of the long-form MIT license block.
|
|
|
|
Returns (start, end) where end is exclusive, or None if not found.
|
|
The search looks in the first 30 lines of the file.
|
|
"""
|
|
search_limit = min(len(lines), 30)
|
|
|
|
# Find the line containing "MIT License"
|
|
mit_start = None
|
|
for i in range(search_limit):
|
|
text = strip_comment(lines[i], prefix)
|
|
if text is not None and "MIT License" in text:
|
|
mit_start = i
|
|
break
|
|
|
|
if mit_start is None:
|
|
return None
|
|
|
|
# Find the end: line containing "SOFTWARE." (the last line of the MIT text)
|
|
mit_end = None
|
|
for i in range(mit_start, min(len(lines), mit_start + 25)):
|
|
text = strip_comment(lines[i], prefix)
|
|
if text is not None and "SOFTWARE." in text:
|
|
mit_end = i + 1 # exclusive
|
|
break
|
|
|
|
if mit_end is None:
|
|
return None
|
|
|
|
# Verify it's actually the MIT license by checking for key phrases
|
|
block_text = "\n".join(lines[mit_start:mit_end])
|
|
if not any(marker in block_text for marker in MIT_LONG_FORM_MARKERS[1:]):
|
|
return None
|
|
|
|
# Expand to include surrounding empty comment lines (e.g. bare "//" or "#")
|
|
# Look backwards from mit_start
|
|
while mit_start > 0:
|
|
prev = lines[mit_start - 1].rstrip("\n")
|
|
if prev == prefix or prev == f"{prefix} ":
|
|
mit_start -= 1
|
|
else:
|
|
break
|
|
|
|
# Look forwards from mit_end
|
|
while mit_end < len(lines):
|
|
nxt = lines[mit_end].rstrip("\n")
|
|
if nxt == prefix or nxt == f"{prefix} ":
|
|
mit_end += 1
|
|
else:
|
|
break
|
|
|
|
# Also consume a blank line after the block
|
|
if mit_end < len(lines) and lines[mit_end].strip() == "":
|
|
mit_end += 1
|
|
|
|
return (mit_start, mit_end)
|
|
|
|
|
|
def has_spdx_header(lines, limit=5):
|
|
"""Check if the file already has an SPDX header in the first few lines."""
|
|
for line in lines[:limit]:
|
|
if "SPDX-License-Identifier:" in line:
|
|
return True
|
|
return False
|
|
|
|
|
|
def has_copyright_line(lines, limit=5):
|
|
"""Check if the file has a copyright line in the first few lines."""
|
|
for line in lines[:limit]:
|
|
if "Copyright" in line and "Advanced Micro Devices" in line:
|
|
return True
|
|
return False
|
|
|
|
|
|
def normalize_spdx_spacing(lines, prefix):
|
|
"""Normalize SPDX-License-Identifier spacing to single space after colon.
|
|
|
|
Returns (modified_lines, changed).
|
|
"""
|
|
changed = False
|
|
result = []
|
|
for line in lines:
|
|
# Match SPDX line with any spacing after colon
|
|
new_line = re.sub(
|
|
rf"^(\s*{re.escape(prefix)}\s*)SPDX-License-Identifier:\s+MIT",
|
|
rf"\1SPDX-License-Identifier: MIT",
|
|
line,
|
|
)
|
|
if new_line != line:
|
|
changed = True
|
|
result.append(new_line)
|
|
return result, changed
|
|
|
|
|
|
def process_file(filepath, dry_run=False, verbose=False):
|
|
"""Process a single file. Returns action taken as a string, or None."""
|
|
prefix = get_comment_prefix(filepath)
|
|
if prefix is None:
|
|
return None
|
|
|
|
with open(filepath, "r", encoding="utf-8", errors="replace") as f:
|
|
lines = f.readlines()
|
|
|
|
if not lines:
|
|
return None
|
|
|
|
# Check for shebang
|
|
has_shebang = lines[0].startswith("#!")
|
|
shebang_line = lines[0] if has_shebang else None
|
|
|
|
# Search range for headers (skip shebang if present)
|
|
search_start = 1 if has_shebang else 0
|
|
search_lines = lines[search_start:]
|
|
|
|
# Case 1: Already has SPDX header — just normalize spacing
|
|
if has_spdx_header(search_lines):
|
|
new_lines, changed = normalize_spdx_spacing(lines, prefix)
|
|
if changed:
|
|
if not dry_run:
|
|
with open(filepath, "w", encoding="utf-8") as f:
|
|
f.writelines(new_lines)
|
|
return "normalized_spacing"
|
|
return "already_spdx"
|
|
|
|
# Case 2: Has long-form MIT header — replace with SPDX
|
|
mit_block = find_long_mit_block(lines, prefix)
|
|
if mit_block is not None:
|
|
start, end = mit_block
|
|
spdx = make_spdx_header(prefix)
|
|
|
|
# Check if there's a Copyright line with a year range before the MIT block
|
|
# that we also want to include in the removed section
|
|
# (the Copyright line is part of the long-form block)
|
|
|
|
new_lines = lines[:start] + [spdx, "\n"] + lines[end:]
|
|
|
|
# Clean up double blank lines that might result
|
|
cleaned = []
|
|
prev_blank = False
|
|
for line in new_lines:
|
|
is_blank = line.strip() == ""
|
|
if is_blank and prev_blank:
|
|
continue
|
|
cleaned.append(line)
|
|
prev_blank = is_blank
|
|
|
|
if not dry_run:
|
|
with open(filepath, "w", encoding="utf-8") as f:
|
|
f.writelines(cleaned)
|
|
return "replaced_long_form"
|
|
|
|
# Case 3: No license header at all — add SPDX
|
|
spdx = make_spdx_header(prefix)
|
|
|
|
if has_shebang:
|
|
new_lines = [shebang_line, spdx, "\n"] + lines[1:]
|
|
else:
|
|
new_lines = [spdx, "\n"] + lines
|
|
|
|
# Clean up double blank lines at the insertion point
|
|
cleaned = []
|
|
prev_blank = False
|
|
for line in new_lines:
|
|
is_blank = line.strip() == ""
|
|
if is_blank and prev_blank:
|
|
continue
|
|
cleaned.append(line)
|
|
prev_blank = is_blank
|
|
|
|
if not dry_run:
|
|
with open(filepath, "w", encoding="utf-8") as f:
|
|
f.writelines(cleaned)
|
|
return "added_header"
|
|
|
|
|
|
def collect_files(root, excluded_dirs):
|
|
"""Walk the tree and yield file paths to process."""
|
|
for dirpath, dirnames, filenames in os.walk(root):
|
|
# Prune excluded directories
|
|
dirnames[:] = [d for d in dirnames if d not in excluded_dirs]
|
|
|
|
for filename in sorted(filenames):
|
|
if (
|
|
filename.endswith((".cpp", ".hpp", ".py", ".cmake"))
|
|
or filename == "CMakeLists.txt"
|
|
):
|
|
yield os.path.join(dirpath, filename)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Replace long-form MIT license with SPDX format"
|
|
)
|
|
parser.add_argument(
|
|
"--dry-run",
|
|
action="store_true",
|
|
help="Preview changes without writing files",
|
|
)
|
|
parser.add_argument(
|
|
"--verbose",
|
|
action="store_true",
|
|
help="Print each file and action taken",
|
|
)
|
|
parser.add_argument(
|
|
"--exclude",
|
|
nargs="*",
|
|
default=None,
|
|
help="Directories to exclude (default: build external examples .git)",
|
|
)
|
|
parser.add_argument(
|
|
"files",
|
|
nargs="*",
|
|
help="Individual files to process, or omit to scan project root",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
excluded_dirs = (
|
|
set(args.exclude) if args.exclude is not None else DEFAULT_EXCLUDED_DIRS
|
|
)
|
|
|
|
stats = {
|
|
"replaced_long_form": 0,
|
|
"added_header": 0,
|
|
"normalized_spacing": 0,
|
|
"already_spdx": 0,
|
|
"skipped": 0,
|
|
}
|
|
|
|
if args.files:
|
|
# Process individual files
|
|
file_list = args.files
|
|
root = os.getcwd()
|
|
else:
|
|
# Scan project root
|
|
root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
file_list = list(collect_files(root, excluded_dirs))
|
|
print(f" Excluding dirs: {', '.join(sorted(excluded_dirs))}")
|
|
print()
|
|
|
|
for filepath in file_list:
|
|
rel = os.path.relpath(filepath, root)
|
|
try:
|
|
action = process_file(filepath, dry_run=args.dry_run, verbose=args.verbose)
|
|
except Exception as e:
|
|
print(f" ERROR {rel}: {e}", file=sys.stderr)
|
|
stats["skipped"] += 1
|
|
continue
|
|
|
|
if action is None:
|
|
stats["skipped"] += 1
|
|
else:
|
|
stats[action] += 1
|
|
if args.verbose or action not in ("already_spdx",):
|
|
label = {
|
|
"replaced_long_form": "REPLACE",
|
|
"added_header": "ADD",
|
|
"normalized_spacing": "NORMALIZE",
|
|
"already_spdx": "OK",
|
|
}.get(action, action)
|
|
print(f" {label:>10} {rel}")
|
|
|
|
print()
|
|
if args.dry_run:
|
|
print("=== DRY RUN (no files modified) ===")
|
|
print(f" Replaced long-form: {stats['replaced_long_form']}")
|
|
print(f" Added missing header: {stats['added_header']}")
|
|
print(f" Normalized spacing: {stats['normalized_spacing']}")
|
|
print(f" Already correct: {stats['already_spdx']}")
|
|
print(f" Skipped/errors: {stats['skipped']}")
|
|
print(f" Total processed: {sum(stats.values())}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|