Skip to content

Commit efec5c3

Browse files
fix: add .copyrightignore and align make format/lint with pre-commit (#107)
## Summary - **`make lint` was failing** because the copyright fixer had no exclusion list for community markdown files (`README.md`, `CHANGELOG.md`, etc.) and dot-config directories (`.agent/`, `.claude/`, `.cursor/`, `.github/`). Pre-commit worked because it runs in fix mode, silently adding headers rather than failing. - **Adds `.copyrightignore`** — a gitignore-style file at the repo root where exclusions are declared. Supports bare filenames (`README.md`), directory patterns (`.github/`), and path globs. No Python code changes needed to add future exclusions. - **Aligns `make format` / `make lint` / pre-commit** so they use the same fixers with the same scope: - `make format` now mutates (ruff format + full ruff check --fix + copyright fix on whole repo) — same behavior as pre-commit. - `make lint` is now read-only (ruff check without --fix, ty check, copyright --check) — safe for CI. - Previously `ruff-lint.sh` ran `ruff check --fix`, mutating files in what should be a check-only command. - **Adds missing SPDX headers** to 39 project files (docs, tools, scripts) that were missing them. Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent b5bc72a commit efec5c3

6 files changed

Lines changed: 94 additions & 14 deletions

File tree

.copyrightignore

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Files and directories excluded from SPDX copyright header checks.
2+
3+
# Standard community / documentation files
4+
CHANGELOG.md
5+
CITATION.md
6+
CODE_OF_CONDUCT.md
7+
CONTRIBUTING.md
8+
PULL_REQUEST_TEMPLATE.md
9+
README.md
10+
SECURITY.md
11+
THIRD_PARTY.md
12+
design.md
13+
14+
# AI / IDE / CI configuration directories
15+
.agent/
16+
.claude/
17+
.cursor/
18+
.github/
19+
20+
# AI assistant config files
21+
CLAUDE.md

CONTRIBUTING.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -293,22 +293,26 @@ Before submitting a PR:
293293

294294
### Formatting
295295

296-
We use [Ruff](https://docs.astral.sh/ruff/) for code formatting and import sorting. The formatter runs on changed files against the `main` branch.
296+
We use [Ruff](https://docs.astral.sh/ruff/) for code formatting and linting auto-fix, plus a copyright header fixer. `make format` mutates files — it runs the same fixers as pre-commit.
297297

298298
```bash
299-
# Format code and sort imports
299+
# Format code, auto-fix lint issues, and add missing copyright headers
300300
make format
301301
```
302302

303303
### Linting and Type Checking
304304

305-
We use [Ruff](https://docs.astral.sh/ruff/) for linting and [ty](https://github.com/astral-sh/ty) for type checking. Both run on changed files against `main`.
305+
We use [Ruff](https://docs.astral.sh/ruff/) for linting and [ty](https://github.com/astral-sh/ty) for type checking. `make lint` is read-only — it reports errors without modifying files.
306306

307307
```bash
308-
# Run ruff linter (with auto-fix) and ty type checker
308+
# Check linting, type errors, and missing copyright headers (no auto-fix)
309309
make lint
310310
```
311311

312+
### Copyright Headers
313+
314+
All source files (`.py`, `.sh`, `.yaml`, `.yml`, `.md`) require SPDX copyright headers. `make format` adds them automatically. Files excluded from this requirement (community files like `README.md`, config directories like `.github/`) are listed in `.copyrightignore` at the repo root.
315+
312316
### Pre-commit Hooks
313317

314318
We recommend setting up pre-commit hooks to catch formatting, linting, and type issues before committing:
@@ -317,7 +321,7 @@ We recommend setting up pre-commit hooks to catch formatting, linting, and type
317321
prek install
318322
```
319323

320-
This installs hooks that run Ruff (format + lint), ty type checking, and uv lock verification on each commit.
324+
This installs hooks that run Ruff (format + lint), copyright header fixer, ty type checking, and uv lock verification on each commit.
321325

322326
## Documentation
323327

Makefile

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,13 +127,16 @@ docs-deploy: ## Deploy the documentation site to GitHub Pages
127127

128128

129129
### CODE QUALITY ###
130+
# `make format` mutates files (same fixers as pre-commit).
131+
# `make lint` is read-only (same checks as CI).
130132

131133
.PHONY: format
132-
format: ## Format the code
134+
format: ## Format the code (ruff format + fix + copyright headers)
133135
bash tools/format/format.sh
136+
uv run --script tools/lint/copyright_fixer.py .
134137

135138
.PHONY: lint
136-
lint: ## Lint the code
139+
lint: ## Lint the code (read-only checks)
137140
bash tools/lint/ruff-lint.sh
138141
bash tools/lint/run-ty-check.sh
139142
uv run --script tools/lint/copyright_fixer.py --check .

tools/format/format.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,14 @@ if [ "$CHECK_MODE" = true ]; then
6363
# shellcheck disable=SC2086
6464
$RUFF format --check "$NSS_ROOT_PATH" $filtered_files
6565
# shellcheck disable=SC2086
66-
$RUFF check --select I "$NSS_ROOT_PATH" $filtered_files
66+
$RUFF check "$NSS_ROOT_PATH" $filtered_files
6767
# shellcheck disable=SC2086
6868
uv run --script tools/lint/copyright_fixer.py --check $filtered_files
6969
else
7070
# shellcheck disable=SC2086
7171
$RUFF format "$NSS_ROOT_PATH" $filtered_files
7272
# shellcheck disable=SC2086
73-
$RUFF check --select I --fix "$NSS_ROOT_PATH" $filtered_files
73+
$RUFF check --fix "$NSS_ROOT_PATH" $filtered_files
7474
# shellcheck disable=SC2086
7575
uv run --script tools/lint/copyright_fixer.py $filtered_files
7676
fi

tools/lint/copyright_fixer.py

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333

3434
_EXTENSIONS = frozenset({".py", ".sh", ".md", ".yaml", ".yml"})
3535

36+
_COPYRIGHT_IGNORE_FILE = ".copyrightignore"
37+
3638
# Comment-style headers by file type
3739
_HASH_HEADER = (
3840
f"# SPDX-FileCopyrightText: Copyright (c) 2025-{_CURRENT_YEAR} NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n"
@@ -99,6 +101,40 @@ def _is_ruff_excluded(relpath: str, excludes: list[str]) -> bool:
99101
return False
100102

101103

104+
def _load_copyright_excludes(repo_root: str | None) -> list[str]:
105+
"""Load exclude patterns from .copyrightignore at the repo root."""
106+
if repo_root is None:
107+
return []
108+
ignore_path = os.path.join(repo_root, _COPYRIGHT_IGNORE_FILE)
109+
if not os.path.isfile(ignore_path):
110+
return []
111+
with open(ignore_path, encoding="utf-8") as fh:
112+
return [line.strip() for line in fh if line.strip() and not line.startswith("#")]
113+
114+
115+
def _is_copyright_excluded(relpath: str, patterns: list[str]) -> bool:
116+
"""Return True if the file matches any .copyrightignore pattern.
117+
118+
Pattern semantics (gitignore-like):
119+
- Trailing ``/`` matches any path component (directory).
120+
- Patterns without ``/`` match the file's basename anywhere in the tree.
121+
- Patterns containing ``/`` (but not trailing) match from repo root.
122+
"""
123+
p = Path(relpath)
124+
for pat in patterns:
125+
if pat.endswith("/"):
126+
dirname = pat.rstrip("/")
127+
if dirname in p.parts:
128+
return True
129+
elif "/" in pat:
130+
if fnmatch(relpath, pat):
131+
return True
132+
else:
133+
if fnmatch(p.name, pat):
134+
return True
135+
return False
136+
137+
102138
# --- core helpers ---
103139

104140

@@ -120,25 +156,31 @@ def _read_head(path: str, nbytes: int = 512) -> str:
120156

121157

122158
def _collect_files_from_dir(root: str) -> list[str]:
123-
"""Collect files under *root* matching _EXTENSIONS, respecting .gitignore and ruff excludes."""
159+
"""Collect files under *root* matching _EXTENSIONS, respecting .gitignore, ruff excludes, and .copyrightignore."""
124160
repo = _get_repo(root)
125161

126162
if repo is not None:
127163
repo_root = str(repo.working_tree_dir)
128164
ruff_excludes = _load_ruff_excludes(repo_root)
165+
copyright_excludes = _load_copyright_excludes(repo_root)
129166
raw = repo.git.ls_files("--cached", "--others", "--exclude-standard", "-z", "--", root)
130167
git_files = [f for f in raw.split("\0") if f]
131168
target_files = [os.path.join(repo_root, f) for f in git_files if os.path.splitext(f)[1] in _EXTENSIONS]
132169
else:
133170
ruff_excludes = _load_ruff_excludes(None)
171+
copyright_excludes = _load_copyright_excludes(None)
134172
target_files = []
135173
for dirpath, _, filenames in os.walk(root):
136174
for fname in filenames:
137175
if os.path.splitext(fname)[1] in _EXTENSIONS:
138176
target_files.append(os.path.join(dirpath, fname))
139177

140-
if ruff_excludes:
141-
target_files = [f for f in target_files if not _is_ruff_excluded(os.path.relpath(f, root), ruff_excludes)]
178+
target_files = [
179+
f
180+
for f in target_files
181+
if not _is_copyright_excluded(rel := os.path.relpath(f, root), copyright_excludes)
182+
and not (ruff_excludes and _is_ruff_excluded(rel, ruff_excludes))
183+
]
142184

143185
return target_files
144186

@@ -192,7 +234,17 @@ def _resolve_targets(paths: list[Path]) -> tuple[list[str], str | None]:
192234
root = str(paths[0].resolve())
193235
return _collect_files_from_dir(root), root
194236

195-
files = [str(p.resolve()) for p in paths if p.is_file() and os.path.splitext(str(p))[1] in _EXTENSIONS]
237+
repo = _get_repo(str(paths[0].resolve()))
238+
repo_root = str(repo.working_tree_dir) if repo else None
239+
copyright_excludes = _load_copyright_excludes(repo_root)
240+
241+
files = [
242+
str(p.resolve())
243+
for p in paths
244+
if p.is_file()
245+
and os.path.splitext(str(p))[1] in _EXTENSIONS
246+
and not _is_copyright_excluded(str(p), copyright_excludes)
247+
]
196248
return files, None
197249

198250

tools/lint/ruff-lint.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,4 @@ else
4646
fi
4747

4848
# shellcheck disable=SC2086
49-
$RUFF check --fix $filtered_files # no quotes around $filtered_files to preserve newlines
49+
$RUFF check $filtered_files # no quotes around $filtered_files to preserve newlines

0 commit comments

Comments
 (0)