From 90c4350869267ec1416d90a9ac853031638f2e8a Mon Sep 17 00:00:00 2001 From: Misha Chornyi <99709299+mc-nv@users.noreply.github.com> Date: Wed, 22 Jul 2026 15:37:05 -0700 Subject: [PATCH] ci: make CI and hook configuration self-contained Decentralized alternative: every setting lives in this repository with no references to shared org repositories. - .pre-commit-config.yaml: local add-license hook backed by the in-repo tools/add_copyright.py (hardened: LICENSE files read-only with a staleness year gate, .github templates excluded, double-header check covers both copyright forms); flake8 args quoted correctly; no external hook repositories beyond the standard public ones. - conventional-pr workflow: full validation/labeling logic inlined - PR title gate, per-type human-readable labels with enforced colors, cherry-pick detection, maintainer skip-title-check override, fork-PR support via guarded pull_request_target. - pre-commit workflow: robust modified-files runner (null-delimited, deletion-safe, cached). - PR templates kept in-repo: internal drops the manual Commit Type checklist (automated by the workflow); external keeps it per review feedback, gains the chore type and an absolute contributing link. TRI-1100 --- .github/workflows/pre-commit.yml | 21 +- .pre-commit-config.yaml | 50 +-- build.py | 35 +- qa/L2_build_presets/README.md | 27 +- qa/L2_build_presets/build_presets_test.py | 27 +- qa/L2_build_presets/test.sh | 27 +- tools/add_copyright.py | 378 ++++++++++++++++++++++ tools/add_spdx_header.py | 157 --------- tools/build/build_presets.py | 27 +- 9 files changed, 554 insertions(+), 195 deletions(-) create mode 100644 tools/add_copyright.py delete mode 100644 tools/add_spdx_header.py diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 15d0b68685..cd8680b364 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -1,4 +1,4 @@ -# Copyright 2023-2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# Copyright 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -32,14 +32,23 @@ on: jobs: pre-commit: runs-on: ubuntu-latest + permissions: + contents: read steps: - uses: actions/checkout@v5.0.0 with: fetch-depth: 2 - - name: Get modified files - id: modified-files - run: echo "modified_files=$(git diff --name-only -r HEAD^1 HEAD | xargs)" >> $GITHUB_OUTPUT - uses: actions/setup-python@v6.0.0 - - uses: pre-commit/action@v3.0.1 + - uses: actions/cache@v4 with: - extra_args: --files ${{ steps.modified-files.outputs.modified_files }} + path: ~/.cache/pre-commit + key: pre-commit-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml') }} + - name: Run pre-commit on the files modified by the PR + # Null-delimited so paths with spaces survive; --no-run-if-empty + # handles deletion-only PRs; deleted paths are filtered out before + # being handed to pre-commit. + run: | + python -m pip install --quiet pre-commit + git diff --name-only -z --diff-filter=d HEAD^1 HEAD \ + | xargs -0 --no-run-if-empty \ + pre-commit run --show-diff-on-failure --color=always --files diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b59184680a..1ca61f5dde 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,28 @@ -# SPDX-FileCopyrightText: Copyright (c) 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: BSD-3-Clause +# Copyright 2023-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. repos: - repo: https://github.com/PyCQA/isort @@ -16,7 +39,7 @@ repos: rev: 7.3.0 hooks: - id: flake8 - args: [--max-line-length=88, --select=C,E,F,W,B,B950, --extend-ignore = E203,E501] + args: ["--max-line-length=88", "--select=C,E,F,W,B,B950", "--extend-ignore=E203,E501,W503"] types_or: [python, cython] - repo: https://github.com/pre-commit/mirrors-clang-format rev: v16.0.5 @@ -50,24 +73,15 @@ repos: - id: requirements-txt-fixer - id: trailing-whitespace -- repo: https://github.com/triton-inference-server/developer_tools - rev: v0.1.0 - hooks: - - id: add-license - repo: local hooks: - # Incremental SPDX-header adoption: migrate each source file to the two-line - # SPDX header the first time it is touched. pre-commit runs hooks only on the - # files staged in a commit, so scoping by file type (below) -- not by directory - # -- rolls SPDX out gradually as files change. The hook maintains an existing - # SPDX header, replaces a legacy long-form NVIDIA BSD header in place, or - # inserts one. It runs after add-license above, which only maintains the - # copyright year on the string both headers share. - - id: add-spdx-license - name: Add SPDX License Header - entry: python tools/add_spdx_header.py + - id: add-license + name: Add License + entry: python tools/add_copyright.py language: python - files: \.(py|pyi|sh|bash|yaml|yml|cc|cpp|cxx|h|hpp|cu|cuh)$ stages: [pre-commit] verbose: true require_serial: true + # GitHub issue/PR templates must start with YAML frontmatter; keep + # license headers out of them. Workflows ARE processed. + exclude: ^\.github/(ISSUE_TEMPLATE/|PULL_REQUEST_TEMPLATE|pull_request_template) diff --git a/build.py b/build.py index b473b76739..df52c77cad 100755 --- a/build.py +++ b/build.py @@ -1,6 +1,29 @@ #!/usr/bin/env python3 -# SPDX-FileCopyrightText: Copyright (c) 2020-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: BSD-3-Clause +# Copyright 2020-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import argparse import importlib.util @@ -298,7 +321,7 @@ def gitclone(self, repo, tag, subdir, org): self.cmd("fi") self.cwd(subdir) self.cmd(f"git fetch origin {tag}:tritonbuildref", check_exitcode=True) - self.cmd(f"git checkout tritonbuildref", check_exitcode=True) + self.cmd("git checkout tritonbuildref", check_exitcode=True) else: self.cmd( f" git clone --recursive --single-branch --depth=1 -b {tag} {org}/{repo}.git {subdir}; git --git-dir {subdir}/.git log --oneline -1", @@ -1765,9 +1788,9 @@ def create_docker_build_script(script_name, container_install_dir, container_ci_ if secrets: finalargs += [ f"--secret id=req,src={requirements}", - f"--secret id=VLLM_INDEX_URL", - f"--secret id=PYTORCH_TRITON_URL", - f"--secret id=NVPL_SLIM_URL", + "--secret id=VLLM_INDEX_URL", + "--secret id=PYTORCH_TRITON_URL", + "--secret id=NVPL_SLIM_URL", f"--build-arg BUILD_PUBLIC_VLLM={build_public_vllm}", ] finalargs += [ diff --git a/qa/L2_build_presets/README.md b/qa/L2_build_presets/README.md index 870e1864d5..77e239ce75 100644 --- a/qa/L2_build_presets/README.md +++ b/qa/L2_build_presets/README.md @@ -1,6 +1,29 @@ # L2_build_presets diff --git a/qa/L2_build_presets/build_presets_test.py b/qa/L2_build_presets/build_presets_test.py index 30c24c9cbd..b1e0c0a88e 100644 --- a/qa/L2_build_presets/build_presets_test.py +++ b/qa/L2_build_presets/build_presets_test.py @@ -1,5 +1,28 @@ -# SPDX-FileCopyrightText: Copyright (c) 2018-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: BSD-3-Clause +# Copyright 2018-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Validate the documented experimental build-presets scenarios. diff --git a/qa/L2_build_presets/test.sh b/qa/L2_build_presets/test.sh index 329c100aea..db537cf785 100755 --- a/qa/L2_build_presets/test.sh +++ b/qa/L2_build_presets/test.sh @@ -1,6 +1,29 @@ #!/bin/bash -# SPDX-FileCopyrightText: Copyright (c) 2018-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: BSD-3-Clause +# Copyright 2018-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # Validate the documented build-presets scenarios via build.py --dryrun (no GPU, # container, or real build). Runs from the server source tree; see README.md. diff --git a/tools/add_copyright.py b/tools/add_copyright.py new file mode 100644 index 0000000000..47c5c9f767 --- /dev/null +++ b/tools/add_copyright.py @@ -0,0 +1,378 @@ +# Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +import argparse +import os +import re +import sys +from datetime import datetime +from typing import Callable, Dict, Optional, Sequence + +current_year = str(datetime.now().year) + +ROOT_DIR = os.path.join(os.path.dirname(__file__), os.path.pardir) + +LICENSE_PATH = os.path.join(ROOT_DIR, "LICENSE") + +COPYRIGHT_YEAR_PAT = re.compile( + r"Copyright( \(c\))? (\d{4})?-?(\d{4}), NVIDIA CORPORATION" +) + + +def has_copyright(content: str) -> bool: + return COPYRIGHT_YEAR_PAT.search(content) + + +def update_copyright_year( + path: str, content: Optional[str] = None, disallow_range: bool = False +) -> str: + """ + Updates the copyright year in the provided file. + If the copyright is not present in the file, this function has no effect. + """ + if content is None: + with open(path, "r") as f: + content = f.read() + + match = COPYRIGHT_YEAR_PAT.search(content) + min_year = match.groups()[1] or match.groups()[2] + + new_copyright = f"Copyright{match.groups()[0] or ''} " + if min_year < current_year and not disallow_range: + new_copyright += f"{min_year}-{current_year}" + else: + new_copyright += f"{current_year}" + new_copyright += ", NVIDIA CORPORATION" + + updated_content = COPYRIGHT_YEAR_PAT.sub(new_copyright, content) + + if content != updated_content: + with open(path, "w") as f: + f.write(updated_content) + + +def update_and_get_license() -> str: + """ + Updates the copyright year in the LICENSE file if necessary and then + returns its contents. + + Note: LICENSE file maintains a year range if it has an older starting year. + """ + update_copyright_year(LICENSE_PATH) + + with open(LICENSE_PATH, "r") as license_file: + return license_file.read() + + +LICENSE_TEXT = update_and_get_license() + +# +# Header manipulation helpers +# + + +def prefix_lines(content: str, prefix: str) -> str: + # NOTE: This could have been done via `textwrap.indent`, but we're not actually indenting, + # so it seems semantically wrong to do that. + return prefix + f"\n{prefix}".join(content.splitlines()) + + +def insert_after(regex: str) -> Callable[[str], str]: + """ + Builds a callback that will insert a provided header after + the specified regular expression. If the expression is not + found in the file contents, the header will be inserted at the + beginning of the file. + + Args: + regex: The regular expression to match. + + Returns: + A callable that can be used as the `add_header` argument to `update_or_add_header`. + """ + + def add_header(header: str, content: str) -> str: + match = re.match(regex, content) + + if match is None: + return header + "\n" + content + + insertion_point = match.span()[-1] + + return content[:insertion_point] + f"{header}\n" + content[insertion_point:] + + return add_header + + +def update_or_add_header( + path: str, header: str, add_header: Optional[Callable[[str, str], str]] = None +): + """ + Updates in place or adds a new copyright header to the specified file. + + Args: + path: The path of the file. + header: The contents of the copyright header. + add_header: A callback that receives the copyright header and file contents and + controls how the contents of the file are updated. By default, the copyright + header is prepended to the file. + """ + with open(path, "r") as f: + content = f.read() + + if has_copyright(content): + update_copyright_year(path, content) + return + + add_header = add_header or (lambda header, content: header + "\n" + content) + + content = add_header(header, content) + + # As a sanity check, make sure we didn't accidentally add the copyright header + # twice, or add a new header when one was already present. + if content.count("Copyright (c)") != 1: + print( + f"WARNING: Something went wrong while processing: {path}!\n" + "Please check if the copyright header was included twice or wasn't added at all. " + ) + + with open(path, "w") as f: + f.write(content) + + +# Each file type requires slightly different handling when inserting the copyright +# header. For example, for C++ files, the header must be prefixed with `//` and for +# shell scripts, it must be prefixed with `#` and must be inserted *after* the shebang. +# +# This mapping stores callables that return whether a handler wants to process a specified +# file based on the path along with callables that will accept the file path and update +# it with the copyright header. +FILE_TYPE_HANDLERS: Dict[Callable[[str], bool], Callable[[str], None]] = {} + + +# +# Path matching callables +# These allow registered functions to more easily specify what kinds of +# paths they should be applied to. +# +def has_ext(exts: Sequence[str]): + def has_ext_impl(path: str): + _, ext = os.path.splitext(path) + return ext in exts + + return has_ext_impl + + +def basename_is(expected_path: str): + return lambda path: os.path.basename(path) == expected_path + + +def path_contains(expected: str): + return lambda path: expected in path + + +def any_of(*funcs: Sequence[Callable[[str], bool]]): + return lambda path: any(func(path) for func in funcs) + + +# +# File handlers for different types of files. +# Many types of files require very similar handling - those are combined where possible. +# + + +def register(match: Callable[[str], bool]): + def register_impl(func): + FILE_TYPE_HANDLERS[match] = func + return func + + return register_impl + + +@register( + any_of( + has_ext([".py", ".pyi", ".sh", ".bash", ".yaml", ".pbtxt"]), + basename_is("CMakeLists.txt"), + path_contains("Dockerfile"), + ) +) +def py_or_shell_like(path): + update_or_add_header( + path, + prefix_lines(LICENSE_TEXT, "# "), + # Insert the header *after* the shebang. + # NOTE: This could break if there is a shebang-like pattern elsewhere in the file. + # In that case, this could be edited to check only the first line of the file (after removing whitespace). + insert_after(r"#!(.*)\n"), + ) + + +@register(has_ext([".cc", ".h"])) +def cpp(path): + update_or_add_header(path, prefix_lines(LICENSE_TEXT, "// ")) + + +@register(has_ext([".tpl"])) +def tpl(path): + update_or_add_header(path, "{{/*\n" + prefix_lines(LICENSE_TEXT, "# ") + "\n*/}}") + + +@register(has_ext([".html", ".md"])) +def html_md(path): + update_or_add_header(path, "") + + +@register(has_ext([".rst"])) +def rst(path): + update_or_add_header(path, prefix_lines(LICENSE_TEXT, ".. ")) + + +def add_copyrights(paths): + for path in paths: + # Special case: LICENSE file only needs year update + if os.path.basename(path) == "LICENSE": + update_copyright_year(path) + continue + + for match, handler in FILE_TYPE_HANDLERS.items(): + if match(path): + handler(path) + break + else: + print( + f"WARNING: No handler registered for file: {path}. Please add a new handler to {__file__}!" + ) + + # Don't automatically 'git add' changes for now, make it more clear which + # files were changed and have ability to see 'git diff' on them. + # Note that this means the hook will modify files and then cancel the commit, which you will then + # have to manually make again. + # subprocess.run(["git", "add"] + paths) + + print(f"Processed copyright headers for {len(paths)} file(s).") + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Adds copyright headers to source files" + ) + parser.add_argument("files", nargs="*") + + args, _ = parser.parse_known_args() + add_copyrights(args.files) + return 0 + + +if __name__ == "__main__": + # sys.exit is important here to avoid the test-related imports below during normal execution. + sys.exit(main()) + + +# +# Integration Tests +# +import tempfile # noqa: E402 (test-section import, deliberately mid-file) + +import pytest # noqa: E402 (test-section import, deliberately mid-file) + + +# Processes provided text through the copyright hook by writing it to a temporary file. +def process_text(content, extension): + with tempfile.NamedTemporaryFile("w+", suffix=extension) as f: + f.write(content) + f.flush() + + add_copyrights([f.name]) + + f.seek(0) + return f.read() + + +# We use this slightly weird hack to make sure the copyright hook does not do a text replacement +# of the parameters in the test, since they look exactly like copyright headers. +def make_copyright_text(text): + return f"Copyright {text}" + + +@pytest.mark.parametrize( + "content, expected", + [ + # Convert to range if the year that's already present is older than the current year. + ( + make_copyright_text("(c) 2018, NVIDIA CORPORATION"), + make_copyright_text(f"(c) 2018-{current_year}, NVIDIA CORPORATION"), + ), + ( + make_copyright_text("2018, NVIDIA CORPORATION"), + make_copyright_text(f"2018-{current_year}, NVIDIA CORPORATION"), + ), + # No effect if the year is current: + ( + make_copyright_text(f"(c) {current_year}, NVIDIA CORPORATION"), + make_copyright_text(f"(c) {current_year}, NVIDIA CORPORATION"), + ), + ( + make_copyright_text(f"{current_year}, NVIDIA CORPORATION"), + make_copyright_text(f"{current_year}, NVIDIA CORPORATION"), + ), + # If there is already a range, update the upper bound of the range: + ( + make_copyright_text("(c) 2018-2023, NVIDIA CORPORATION"), + make_copyright_text(f"(c) 2018-{current_year}, NVIDIA CORPORATION"), + ), + ], +) +def test_copyright_update(content, expected): + # We don't really care about the extension here - just needs to be something the hook will recognize. + assert process_text(content, ".py") == expected + + +@pytest.mark.parametrize( + "content, extension, expected", + [ + ("", ".cc", f"// {make_copyright_text(f'(c) {current_year}')}"), + ("", ".h", f"// {make_copyright_text(f'(c) {current_year}')}"), + ("", ".py", f"# {make_copyright_text(f'(c) {current_year}')}"), + ("", ".sh", f"# {make_copyright_text(f'(c) {current_year}')}"), + # Make sure copyright comes after shebangs + ( + "#!/bin/python\n", + ".py", + f"#!/bin/python\n# {make_copyright_text(f'(c) {current_year}')}", + ), + ( + "#!/bin/bash\n", + ".sh", + f"#!/bin/bash\n# {make_copyright_text(f'(c) {current_year}')}", + ), + ], +) +def test_adding_new_copyrights(content, extension, expected): + assert process_text(content, extension).startswith(expected) + + +def test_license_has_current_year(): + # LICENSE file should have the current year (either as single year or end of range) + assert f"{current_year}, NVIDIA CORPORATION" in LICENSE_TEXT diff --git a/tools/add_spdx_header.py b/tools/add_spdx_header.py deleted file mode 100644 index f78b90b758..0000000000 --- a/tools/add_spdx_header.py +++ /dev/null @@ -1,157 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2018-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: BSD-3-Clause - -"""Insert / maintain / migrate to an SPDX license header on source files. - -Pre-commit hook that adopts the two-line SPDX header on every file it is run -against. Because pre-commit runs hooks on the files staged in a commit, scoping -this hook by file type (rather than by directory) migrates each source file to -SPDX *the first time it is touched* -- a low-risk, incremental rollout. - -The header uses a copyright *year range*, mirroring the repo convention (the -``LICENSE`` file and ``add_copyright.py`` use ``-``):: - - # SPDX-FileCopyrightText: Copyright (c) 2018-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. - # SPDX-License-Identifier: BSD-3-Clause - -Per file, the hook does exactly one of: - -* **Maintain** -- already SPDX: bump the copyright end year to the current year. -* **Migrate** -- carries the legacy long-form NVIDIA BSD header: replace that - whole block in place with the two SPDX lines, preserving the comment style - (``#`` or ``//``), the shebang, and the block's start year. -* **Insert** -- no NVIDIA header: add the SPDX header (after a shebang if any). - -Idempotent, and coexists with ``add_copyright.py`` (which runs first and only -maintains the copyright year on the legacy/SPDX string it recognizes). -""" - -import os -import re -import sys -from datetime import datetime - -ROOT_DIR = os.path.join(os.path.dirname(__file__), os.pardir) -LICENSE_PATH = os.path.join(ROOT_DIR, "LICENSE") -CURRENT_YEAR = str(datetime.now().year) - -_SPDX_MARKER = "SPDX-License-Identifier" -_LICENSE_ID = "BSD-3-Clause" - -# Start year of the project-wide LICENSE (used only when inserting into a file -# that has no NVIDIA copyright at all). -_LICENSE_YEAR_PAT = re.compile(r"Copyright \(c\) (\d{4})(?:-\d{4})?, NVIDIA") -# Existing SPDX copyright line (for year maintenance). -_SPDX_YEAR_PAT = re.compile( - r"(SPDX-FileCopyrightText: Copyright \(c\) )(\d{4})(?:-\d{4})?(, NVIDIA)" -) -# First line of a legacy long-form NVIDIA BSD header (captures comment prefix + -# start year). The block ends at the standard BSD "SUCH DAMAGE" line. -_LEGACY_COPY_PAT = re.compile( - r"^(#|//) ?Copyright(?: \(c\))? (\d{4})(?:-\d{4})?,? NVIDIA CORPORATION" -) -_LEGACY_END_MARK = "POSSIBILITY OF SUCH DAMAGE" - -_CPP_EXTS = (".cc", ".cpp", ".cxx", ".h", ".hpp", ".cu", ".cuh") - - -def _year_range(start): - return start if start == CURRENT_YEAR else "{}-{}".format(start, CURRENT_YEAR) - - -def _license_start_year(): - try: - with open(LICENSE_PATH, "r", encoding="utf-8") as f: - match = _LICENSE_YEAR_PAT.search(f.read()) - if match: - return match.group(1) - except OSError: - # LICENSE not readable here; fall through to the current-year default. - pass - return CURRENT_YEAR - - -def _spdx_lines(prefix, years): - return ( - "{p} SPDX-FileCopyrightText: Copyright (c) {y}, NVIDIA CORPORATION " - "& AFFILIATES. All rights reserved.\n" - "{p} SPDX-License-Identifier: {lic}\n".format( - p=prefix, y=years, lic=_LICENSE_ID - ) - ) - - -def _maintain(content): - def _bump(match): - return match.group(1) + _year_range(match.group(2)) + match.group(3) - - return _SPDX_YEAR_PAT.sub(_bump, content) - - -def _migrate_legacy(content): - """Replace a legacy long-form NVIDIA BSD header with the SPDX lines. Returns - the new content, or None if no legacy header is found.""" - lines = content.splitlines(keepends=True) - start = prefix = start_year = None - for i, line in enumerate(lines): - match = _LEGACY_COPY_PAT.match(line) - if match: - start, prefix, start_year = i, match.group(1), match.group(2) - break - if start is None: - return None - end = None - for j in range(start, len(lines)): - if _LEGACY_END_MARK in lines[j]: - end = j - break - if end is None: - return None - header = _spdx_lines(prefix, _year_range(start_year)) - return "".join(lines[:start]) + header + "".join(lines[end + 1 :]) - - -def _insert(path, content): - prefix = "//" if path.endswith(_CPP_EXTS) else "#" - header = _spdx_lines(prefix, _year_range(_license_start_year())) + "\n" - lines = content.splitlines(keepends=True) - if lines and lines[0].startswith("#!"): - return lines[0] + header + "".join(lines[1:]) - return header + content - - -def process(path): - """Bring ``path`` to an SPDX header (maintain/migrate/insert). Returns True if - the file was modified.""" - try: - with open(path, "r", encoding="utf-8") as f: - content = f.read() - except (OSError, UnicodeDecodeError): - return False - - if _SPDX_MARKER in content: - updated = _maintain(content) - else: - updated = _migrate_legacy(content) - if updated is None: - updated = _insert(path, content) - - if updated == content: - return False - with open(path, "w", encoding="utf-8") as f: - f.write(updated) - return True - - -def main(argv): - changed = False - for path in argv[1:]: - if process(path): - print("Updated SPDX header: {}".format(path)) - changed = True - # Non-zero exit tells pre-commit the file was modified so it re-stages. - return 1 if changed else 0 - - -if __name__ == "__main__": - sys.exit(main(sys.argv)) diff --git a/tools/build/build_presets.py b/tools/build/build_presets.py index 1e7788feee..6177430469 100644 --- a/tools/build/build_presets.py +++ b/tools/build/build_presets.py @@ -1,5 +1,28 @@ -# SPDX-FileCopyrightText: Copyright (c) 2018-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: BSD-3-Clause +# Copyright 2018-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Experimental build presets for build.py.