From 1844c90b35ebfa9b204b3b30cc5b0a78d2ef12e9 Mon Sep 17 00:00:00 2001 From: Jiwen Cai Date: Tue, 13 Oct 2026 22:19:45 +0000 Subject: [PATCH] Prepare component licensing for EgoPost Establish the OpenMDW license boundary while preserving Apache defaults and keeping EgoPost out of current distributions. Automate source annotations and license validation, with canonical license texts kept free of project headers. Signed-off-by: Jiwen Cai --- .pre-commit-config.yaml | 9 +- AGENTS.md | 18 ++ CONTRIBUTING.md | 38 +--- LICENSES/Apache-2.0.txt | 4 +- LICENSES/LicenseRef-OpenMDW-1.1.txt | 49 +++++ LICENSES/MIT.txt | 2 + README.md | 6 + REUSE.toml | 7 + pyproject.toml | 3 + scripts/check_licenses.py | 176 ++++++++++++++++++ src/core/python/CMakeLists.txt | 1 + src/core/python/pyproject.toml.in | 6 +- src/python/isaaccapture_egopost/LICENSE.md | 52 ++++++ tests/python/packaging/pyproject.toml | 2 +- tests/python/packaging/test_license_policy.py | 166 +++++++++++++++++ tests/python/packaging/test_wheel_contents.py | 13 ++ 16 files changed, 515 insertions(+), 37 deletions(-) create mode 100644 LICENSES/LicenseRef-OpenMDW-1.1.txt create mode 100644 scripts/check_licenses.py create mode 100644 src/python/isaaccapture_egopost/LICENSE.md create mode 100644 tests/python/packaging/test_license_policy.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 95942cfb0e..71554b4c01 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,9 +34,12 @@ repos: rev: v6.2.0 hooks: - id: reuse-lint-file - name: REUSE lint (SPDX headers on changed files) - # Include CMakeLists.txt and Dockerfile.xxx by matching those explicitly in the regex - files: \.(py|js|ts|cpp|c|h|hpp|cxx|hxx|cmake|md|yaml|yml|sh|fbs|toml|html)$|CMakeLists\.txt$|Dockerfile(\.[A-Za-z0-9_-]+)?$ + name: REUSE licenses (source files and complete EgoPost subtree) + entry: python scripts/check_licenses.py --fix + args: [] # Clear the upstream hook's lint-file argument. + stages: [pre-commit] + pass_filenames: false + always_run: true # Copyright year: ensure SPDX-FileCopyrightText includes current year (--fix updates in place) - repo: local hooks: diff --git a/AGENTS.md b/AGENTS.md index 5d42231f8f..1f66146d09 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -47,6 +47,24 @@ If you cannot run a shell, use your search/glob tools on the pattern `**/AGENTS. Optional context index: [`src/core/AGENTS.md`](src/core/AGENTS.md) (also on the ancestor walk—read it when working under `src/core/`). +## Product naming + +- Spell the product name “Isaac Capture” in prose; avoid the ambiguous shorthand “Capture”. + +## Component licensing + +- Keep licensing summaries brief in README and CONTRIBUTING; automate routine + checks and updates rather than documenting manual header maintenance. + +- Preserve root `LICENSE.md` verbatim as Apache-2.0; explain component exceptions + in README/CONTRIBUTING and local license files, so GitHub can recognize the default. +- EgoPost uses `LicenseRef-OpenMDW-1.1`; preserve upstream exceptions and notices. + Keep license-boundary changes separate from copyright-template and year-policy changes. +- Keep canonical texts in `LICENSES/` unmodified, including template placeholders; + do not prepend project copyright or SPDX headers or include them in header repair. +- Source hosting does not authorize wheel inclusion. Keep package discovery explicit + and verify license metadata/notices in each artifact before shipping a new component. + ## CMake & include structure When you create or edit a `CMakeLists.txt`, add or move a target, place a header, diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a1a2bc161a..ae507e62ef 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,6 +23,10 @@ Thanks for your interest in contributing! This document outlines how to build, t - Follow PEP 8 for style. - Use type annotations (PEP 484) and prefer formatted string literals (PEP 701, f-strings). +## License + +- Contributions use the license of the component/file being changed: Apache-2.0 by default, OpenMDW-1.1 for EgoPost, with explicit third-party exceptions preserved. The DCO below applies to each contribution. + ## Pre-commit We use [pre-commit](https://pre-commit.com/) to run Python linters and formatters (including @@ -50,31 +54,10 @@ ruff format . ### License headers -We use [REUSE](https://reuse.software/) to enforce SPDX license headers on source files. Pre-commit runs `reuse lint-file` on changed files (e.g. `.py`, `.cpp`, `.md`, `.yaml`): - -1. **Required fields**: Each file must have `SPDX-FileCopyrightText` and `SPDX-License-Identifier`. -2. **License**: `SPDX-License-Identifier` must be `Apache-2.0` (full text in `LICENSES/Apache-2.0.txt`). - -Example header: - -``` -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -``` - -To add or fix headers, install the [reuse tool](https://github.com/fsfe/reuse-tool) - -```bash -uv tool install reuse -``` - -and run for example: - -```bash -reuse annotate -t compact -l Apache-2.0 --skip-unrecognised -r path/to/file -``` - -REUSE does not auto-update the copyright year when you touch a file; include the current year when adding or editing headers. +Pre-commit automatically checks and updates license headers with [REUSE](https://reuse.software/). +It adds NVIDIA headers to unannotated first-party files and selects the license by +file location, preserving existing attribution and explicit third-party exceptions. +Review and stage any automatic edits, then commit again. ## CI GitHub Actions builds the default targets and runs tests. @@ -85,10 +68,7 @@ GitHub Actions builds the default targets and runs tests. 3. Describe motivation, changes, and testing in the PR 4. Link related issues -## License -- Your contributions are under the repository’s license unless stated otherwise - -### Signing Your Work +## Signing Your Work * We require that all contributors "sign-off" on their commits. This certifies that the contribution is your original work, or you have rights to submit it under the same license, or a compatible license. diff --git a/LICENSES/Apache-2.0.txt b/LICENSES/Apache-2.0.txt index 1a89b9054d..261eeb9e9f 100644 --- a/LICENSES/Apache-2.0.txt +++ b/LICENSES/Apache-2.0.txt @@ -178,7 +178,7 @@ APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" + boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2020 NVIDIA Corporation + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/LICENSES/LicenseRef-OpenMDW-1.1.txt b/LICENSES/LicenseRef-OpenMDW-1.1.txt new file mode 100644 index 0000000000..ec297ac545 --- /dev/null +++ b/LICENSES/LicenseRef-OpenMDW-1.1.txt @@ -0,0 +1,49 @@ +OpenMDW License Agreement, version 1.1 (OpenMDW-1.1) + +By exercising rights granted to you under this agreement, you accept and agree +to its terms. + +As used in this agreement, "Model Materials" means the materials provided to +you under this agreement, consisting of: (1) one or more machine learning +models (including architecture and parameters); and (2) all related artifacts +(including associated data, documentation and software) that are provided to +you hereunder. + +Subject to your compliance with this agreement, permission is hereby granted, +free of charge, to deal in the Model Materials without restriction, including +under all copyright, patent, database, and trade secret rights included or +embodied therein. + +If you distribute any portion of the Model Materials, you shall retain in your +distribution (1) a copy of this agreement, and (2) all copyright notices and +other notices of origin included in the Model Materials that are applicable to +your distribution. + +If you file, maintain, or voluntarily participate in a lawsuit against any +person or entity asserting that the Model Materials directly or indirectly +infringe any patent or copyright, then all rights and grants made to you +hereunder are terminated, unless that lawsuit was in response to a +corresponding lawsuit first brought against you. + +This agreement does not impose any restrictions or obligations with respect to +any use, modification, or sharing of any outputs generated by using the Model +Materials. + +THE MODEL MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE, NONINFRINGEMENT, ACCURACY, OR THE +ABSENCE OF LATENT OR OTHER DEFECTS OR ERRORS, WHETHER OR NOT DISCOVERABLE, ALL +TO THE GREATEST EXTENT PERMISSIBLE UNDER APPLICABLE LAW. + +YOU ARE SOLELY RESPONSIBLE FOR (1) CLEARING RIGHTS OF OTHER PERSONS THAT MAY +APPLY TO THE MODEL MATERIALS OR ANY USE THEREOF, INCLUDING WITHOUT LIMITATION +ANY PERSON'S COPYRIGHTS OR OTHER RIGHTS INCLUDED OR EMBODIED IN THE MODEL +MATERIALS; (2) OBTAINING ANY NECESSARY CONSENTS, PERMISSIONS OR OTHER RIGHTS +REQUIRED FOR ANY USE OF THE MODEL MATERIALS; OR (3) PERFORMING ANY DUE +DILIGENCE OR UNDERTAKING ANY OTHER INVESTIGATIONS INTO THE MODEL MATERIALS OR +ANYTHING INCORPORATED OR EMBODIED THEREIN. + +IN NO EVENT SHALL THE PROVIDERS OF THE MODEL MATERIALS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE MODEL MATERIALS, THE +USE THEREOF OR OTHER DEALINGS THEREIN. diff --git a/LICENSES/MIT.txt b/LICENSES/MIT.txt index 9cf106272a..242da6233b 100644 --- a/LICENSES/MIT.txt +++ b/LICENSES/MIT.txt @@ -1,5 +1,7 @@ MIT License +Copyright (c) + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights diff --git a/README.md b/README.md index 90a0379079..3cc6d168ce 100644 --- a/README.md +++ b/README.md @@ -63,3 +63,9 @@ Our [documentation page](https://nvidia.github.io/IsaacCapture) provides everyth Isaac Teleop is design to work side by side with [NVIDIA Isaac Lab](https://github.com/isaac-sim/IsaacLab) starting with Isaac Lab [3.0 Beta release](https://github.com/isaac-sim/IsaacLab/releases/tag/v3.0.0-beta). To get started, please refer to Isaac Lab's [Installation](https://isaac-sim.github.io/IsaacLab/develop/source/setup/installation/index.html) guide for more details. Then follow the [CloudXR teleoperation in Isaac Lab](https://isaac-sim.github.io/IsaacLab/develop/source/how-to/cloudxr_teleoperation.html) to get started with Teleoperation in Sim. + +## Licensing + +The default license is [Apache-2.0](LICENSE.md). +EgoPost is licensed under [OpenMDW-1.1](src/python/isaaccapture_egopost/LICENSE.md). +Third-party materials may carry their own licenses. diff --git a/REUSE.toml b/REUSE.toml index b3e4d93312..38c21963fb 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -57,3 +57,10 @@ path = "deps/third_party/chromium/LICENSE" precedence = "override" SPDX-FileCopyrightText = "Copyright 2015 The Chromium Authors" SPDX-License-Identifier = "BSD-3-Clause" + +# EgoPost's default never overrides upstream headers or sidecar licenses. +# The license-only scaffold has no covered files yet; the canonical text is reserved for imports. +[[annotations]] +path = "src/python/isaaccapture_egopost/**" +precedence = "closest" +SPDX-License-Identifier = "LicenseRef-OpenMDW-1.1" diff --git a/pyproject.toml b/pyproject.toml index 695d841299..767461704b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ description = "Isaac Teleop" readme = "README.md" requires-python = ">=3.11" license = "Apache-2.0" +license-files = ["LICENSE.md"] authors = [ { name = "NVIDIA" }, ] @@ -102,6 +103,8 @@ package = false isaacteleop = { path = "src/compat" } [tool.scikit-build] +# EgoPost is hosted here but is not part of the Isaac Capture distribution. +sdist.exclude = ["src/python/isaaccapture_egopost/**", "LICENSES/LicenseRef-OpenMDW-1.1.txt"] # Floor matches CMakeLists' cmake_minimum_required (3.24, for `cmake --fresh`). # Ubuntu 22.04's apt CMake is 3.22 and no longer satisfies it, so scikit-build-core # fetches CMake from PyPI there; the <4 cap keeps that fetch on the 3.x line rather diff --git a/scripts/check_licenses.py b/scripts/check_licenses.py new file mode 100644 index 0000000000..445342fbac --- /dev/null +++ b/scripts/check_licenses.py @@ -0,0 +1,176 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Check and fix Isaac Capture source headers, including all EgoPost assets.""" + +import argparse +import re +import subprocess +import sys +from datetime import date +from pathlib import Path + +from reuse.global_licensing import ReuseTOML +from reuse.project import Project + +EGOPOST_SUBTREE = Path("src/python/isaaccapture_egopost") +EGOPOST_LICENSE = "LicenseRef-OpenMDW-1.1" +DEFAULT_LICENSE = "Apache-2.0" +COPYRIGHT_HOLDER = "NVIDIA CORPORATION & AFFILIATES. All rights reserved." +SOURCE_FILES = re.compile( + r"\.(py|js|ts|cpp|c|h|hpp|cxx|hxx|cmake|md|yaml|yml|sh|fbs|toml|html)$" + r"|CMakeLists\.txt$|Dockerfile(\.[A-Za-z0-9_-]+)?$" +) + + +def covered_paths(project: Project) -> list[Path]: + # Check source files repository-wide; EgoPost additionally includes all assets. + paths = (path.relative_to(project.root) for path in project.all_files()) + return sorted( + path + for path in paths + if path.is_relative_to(EGOPOST_SUBTREE) or SOURCE_FILES.search(str(path)) + ) + + +def third_party_path(path: Path) -> bool: + # Directory exemptions follow the repository layout: third_party lives in deps. + return path.parts[0] == "deps" + + +def is_egopost_default(project: Project, path: Path, info) -> bool: + if info.source_type.value != "reuse-toml" or info.source_path != "REUSE.toml": + return False + annotation = ReuseTOML.from_file(project.root / "REUSE.toml").find_annotations_item( + path + ) + # A subtree fallback is policy, not an explicit per-file exception. + return annotation is not None and f"{EGOPOST_SUBTREE}/**" in annotation.paths + + +def check_egopost_default(project: Project) -> bool: + annotations = ReuseTOML.from_file(project.root / "REUSE.toml").annotations + defaults = [item for item in annotations if f"{EGOPOST_SUBTREE}/**" in item.paths] + if not defaults or any( + {str(expr) for expr in item.spdx_expressions} != {EGOPOST_LICENSE} + or item.precedence.value != "closest" + for item in defaults + ): + print( + f"REUSE.toml: EgoPost default must be {EGOPOST_LICENSE} with closest precedence" + ) + return False + return True + + +def first_party_licenses(project: Project, path: Path) -> set[str] | None: + if third_party_path(path): + return None + infos = project.reuse_info_of(path) + notices = {notice.name for info in infos for notice in info.copyright_notices} + explicit_exception = any( + info.spdx_expressions + and info.source_type.value != "file-header" + and not is_egopost_default(project, path, info) + for info in infos + ) + if ( + explicit_exception + or not notices + or not all("NVIDIA CORPORATION & AFFILIATES" in notice for notice in notices) + ): + return None + return {str(expr) for info in infos for expr in info.spdx_expressions} + + +def fix_license(project: Project, path: Path) -> bool: + if third_party_path(path): + return False + expected = ( + EGOPOST_LICENSE if path.is_relative_to(EGOPOST_SUBTREE) else DEFAULT_LICENSE + ) + infos = project.reuse_info_of(path) + notices = {notice for info in infos for notice in info.copyright_notices} + effective = {str(expr) for info in infos for expr in info.spdx_expressions} + expressions = first_party_licenses(project, path) + add_copyright = not notices and effective <= {expected} + if add_copyright or expressions == set(): + command = [ + sys.executable, + "-m", + "reuse", + "annotate", + "--fallback-dot-license", + "-l", + expected, + ] + if add_copyright: + command += ["-c", COPYRIGHT_HOLDER, "--year", str(date.today().year)] + subprocess.run(command + [str(path)], check=True) + return True + if expressions is None: + return False + if expected == EGOPOST_LICENSE and expressions == {"Apache-2.0"}: + content = path.read_text() + # Replace only the default header tag, retaining copyright and other notices. + updated, count = re.subn( + r"(SPDX-License-Identifier:[ \t]*)Apache-2\.0(?=[ \t]*(?:\*/|-->)?[ \t]*$)", + rf"\g<1>{EGOPOST_LICENSE}", + content, + count=1, + flags=re.MULTILINE, + ) + if count: + path.write_text(updated) + print(f"{path}: updated default license to {EGOPOST_LICENSE}") + return True + return False + + +def check_policy(project: Project, path: Path) -> bool: + expressions = first_party_licenses(project, path) + if expressions is not None and EGOPOST_LICENSE not in expressions: + print( + f"{path}: expected {EGOPOST_LICENSE}; document any license exception explicitly" + ) + return False + return True + + +def main(fix: bool = False) -> int: + project = Project.from_directory(Path.cwd()) + if not check_egopost_default(project): + return 1 + paths = covered_paths(project) + changed = False + if fix: + for path in paths: + changed = fix_license(project, path) or changed + project = Project.from_directory(Path.cwd()) + ok = True + # Pass complete coverage on every invocation, including metadata-only changes. + for start in range(0, len(paths), 200): + result = subprocess.run( + [sys.executable, "-m", "reuse", "lint-file", "--"] + + [str(path) for path in paths[start : start + 200]], + check=False, + ) + ok = result.returncode == 0 and ok + for path in paths: + if path.is_relative_to(EGOPOST_SUBTREE): + ok = check_policy(project, path) and ok + if changed: + print( + "License headers updated; review and stage the changes before committing." + ) + return 0 if ok and not changed else 1 + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--fix", + action="store_true", + help="add default NVIDIA headers and repair default license tags", + ) + sys.exit(main(fix=parser.parse_args().fix)) diff --git a/src/core/python/CMakeLists.txt b/src/core/python/CMakeLists.txt index 4034dfc860..35ffc430ed 100644 --- a/src/core/python/CMakeLists.txt +++ b/src/core/python/CMakeLists.txt @@ -42,6 +42,7 @@ add_custom_target(python_package ALL COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_BINARY_DIR}/pyproject.toml" "${CMAKE_BINARY_DIR}/python_package/$/" COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_SOURCE_DIR}/MANIFEST.in" "${CMAKE_BINARY_DIR}/python_package/$/" COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_SOURCE_DIR}/setup.py" "${CMAKE_BINARY_DIR}/python_package/$/" + COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_SOURCE_DIR}/LICENSE.md" "${CMAKE_BINARY_DIR}/python_package/$/LICENSE.md" COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_SOURCE_DIR}/README.md" "${CMAKE_BINARY_DIR}/python_package/$/README.md" COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_SOURCE_DIR}/requirements.txt" "${CMAKE_BINARY_DIR}/python_package/$/" COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_SOURCE_DIR}/requirements-dev.txt" "${CMAKE_BINARY_DIR}/python_package/$/" diff --git a/src/core/python/pyproject.toml.in b/src/core/python/pyproject.toml.in index 4ade1d4327..fff1897dca 100644 --- a/src/core/python/pyproject.toml.in +++ b/src/core/python/pyproject.toml.in @@ -9,7 +9,7 @@ # See: numpy.org/devdocs/dev/depending_on_numpy.html [build-system] -requires = ["setuptools>=64", "wheel", "numpy>=2.0"] +requires = ["setuptools>=77", "wheel", "numpy>=2.0"] build-backend = "setuptools.build_meta" [project] @@ -19,6 +19,7 @@ description = "Isaac Teleop - Teleoperation with Device I/O" readme = "README.md" requires-python = ">=3.11" license = "Apache-2.0" +license-files = ["LICENSE.md"] authors = [ {name = "NVIDIA"} ] @@ -55,7 +56,8 @@ py-modules = ["isaacteleop"] # TODO(1.9): drop the compatibility module. where = ["."] namespaces = false include = [ - "isaaccapture*", + "isaaccapture", + "isaaccapture.*", "robotic_grounding", "robotic_grounding.retarget", ] diff --git a/src/python/isaaccapture_egopost/LICENSE.md b/src/python/isaaccapture_egopost/LICENSE.md new file mode 100644 index 0000000000..a5b8c94f0e --- /dev/null +++ b/src/python/isaaccapture_egopost/LICENSE.md @@ -0,0 +1,52 @@ +Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + + +OpenMDW License Agreement, version 1.1 (OpenMDW-1.1) + +By exercising rights granted to you under this agreement, you accept and agree +to its terms. + +As used in this agreement, "Model Materials" means the materials provided to +you under this agreement, consisting of: (1) one or more machine learning +models (including architecture and parameters); and (2) all related artifacts +(including associated data, documentation and software) that are provided to +you hereunder. + +Subject to your compliance with this agreement, permission is hereby granted, +free of charge, to deal in the Model Materials without restriction, including +under all copyright, patent, database, and trade secret rights included or +embodied therein. + +If you distribute any portion of the Model Materials, you shall retain in your +distribution (1) a copy of this agreement, and (2) all copyright notices and +other notices of origin included in the Model Materials that are applicable to +your distribution. + +If you file, maintain, or voluntarily participate in a lawsuit against any +person or entity asserting that the Model Materials directly or indirectly +infringe any patent or copyright, then all rights and grants made to you +hereunder are terminated, unless that lawsuit was in response to a +corresponding lawsuit first brought against you. + +This agreement does not impose any restrictions or obligations with respect to +any use, modification, or sharing of any outputs generated by using the Model +Materials. + +THE MODEL MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE, NONINFRINGEMENT, ACCURACY, OR THE +ABSENCE OF LATENT OR OTHER DEFECTS OR ERRORS, WHETHER OR NOT DISCOVERABLE, ALL +TO THE GREATEST EXTENT PERMISSIBLE UNDER APPLICABLE LAW. + +YOU ARE SOLELY RESPONSIBLE FOR (1) CLEARING RIGHTS OF OTHER PERSONS THAT MAY +APPLY TO THE MODEL MATERIALS OR ANY USE THEREOF, INCLUDING WITHOUT LIMITATION +ANY PERSON'S COPYRIGHTS OR OTHER RIGHTS INCLUDED OR EMBODIED IN THE MODEL +MATERIALS; (2) OBTAINING ANY NECESSARY CONSENTS, PERMISSIONS OR OTHER RIGHTS +REQUIRED FOR ANY USE OF THE MODEL MATERIALS; OR (3) PERFORMING ANY DUE +DILIGENCE OR UNDERTAKING ANY OTHER INVESTIGATIONS INTO THE MODEL MATERIALS OR +ANYTHING INCORPORATED OR EMBODIED THEREIN. + +IN NO EVENT SHALL THE PROVIDERS OF THE MODEL MATERIALS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE MODEL MATERIALS, THE +USE THEREOF OR OTHER DEALINGS THEREIN. diff --git a/tests/python/packaging/pyproject.toml b/tests/python/packaging/pyproject.toml index d923d3659c..7ebe555168 100644 --- a/tests/python/packaging/pyproject.toml +++ b/tests/python/packaging/pyproject.toml @@ -8,7 +8,7 @@ description = "Assertions over the wheel this build produced" requires-python = ">=3.11,<3.14" [project.optional-dependencies] -dev = ["pytest"] +dev = ["pytest", "reuse==6.2.0"] [tool.pytest.ini_options] pythonpath = ["."] diff --git a/tests/python/packaging/test_license_policy.py b/tests/python/packaging/test_license_policy.py new file mode 100644 index 0000000000..d8cad15f69 --- /dev/null +++ b/tests/python/packaging/test_license_policy.py @@ -0,0 +1,166 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Exercise licensing checks on real temporary REUSE projects.""" + +import runpy +import shutil +from pathlib import Path + +import pytest +from repo_paths import repo_root +from reuse.project import Project + +CHECK = runpy.run_path(str(repo_root() / "scripts/check_licenses.py")) +SUBTREE = Path("src/python/isaaccapture_egopost") +# REUSE-IgnoreStart +COPYRIGHT = "SPDX-FileCopyrightText: Copyright (c) 2020 NVIDIA CORPORATION & AFFILIATES. All rights reserved." + + +@pytest.fixture +def project(tmp_path, monkeypatch): + shutil.copytree(repo_root() / "LICENSES", tmp_path / "LICENSES") + (tmp_path / SUBTREE).mkdir(parents=True) + (tmp_path / "REUSE.toml").write_text( + "version = 1\n[[annotations]]\n" + f'path = "{SUBTREE}/**"\nprecedence = "closest"\n' + 'SPDX-License-Identifier = "LicenseRef-OpenMDW-1.1"\n' + ) + monkeypatch.chdir(tmp_path) + return tmp_path + + +@pytest.mark.parametrize("license_id", ["Apache-2.0", "Not-A-License"]) +def test_incorrect_egopost_license_fails(project, license_id): + (project / SUBTREE / "module.py").write_text( + f"# {COPYRIGHT}\n# SPDX-License-Identifier: {license_id}\n" + ) + assert CHECK["main"]() == 1 + assert CHECK["main"](fix=True) == 1 + if license_id == "Apache-2.0": + assert CHECK["main"](fix=True) == 0 + content = (project / SUBTREE / "module.py").read_text() + assert COPYRIGHT in content + assert "SPDX-License-Identifier: LicenseRef-OpenMDW-1.1" in content + + +def test_unannotated_first_party_asset_gets_sidecar(project): + path = project / SUBTREE / "config.json" + path.write_text("{}\n") + assert CHECK["main"]() == 1 + assert CHECK["main"](fix=True) == 1 + assert path.read_text() == "{}\n" + sidecar = path.with_suffix(".json.license").read_text() + assert "NVIDIA CORPORATION & AFFILIATES" in sidecar + assert "LicenseRef-OpenMDW-1.1" in sidecar + assert CHECK["main"](fix=True) == 0 + + +def test_unannotated_dependency_requires_review(project): + path = project / "deps" / "third_party" / "vendor.py" + path.parent.mkdir(parents=True) + path.write_text("value = 1\n") + assert CHECK["main"](fix=True) == 1 + assert path.read_text() == "value = 1\n" + + +def test_third_party_header_and_asset_sidecar_survive(project): + module = SUBTREE / "vendor.py" + source = ( + "# SPDX-FileCopyrightText: 2018 Original Author\n" + "# SPDX-License-Identifier: MIT\n" + ) + (project / module).write_text(source) + (project / SUBTREE / "config.json").write_text("{}\n") + (project / SUBTREE / "config.json.license").write_text( + "SPDX-FileCopyrightText: 2018 Original Author\nSPDX-License-Identifier: MIT\n" + ) + assert CHECK["main"](fix=True) == 0 + infos = Project.from_directory(project).reuse_info_of(module) + assert {str(e) for info in infos for e in info.spdx_expressions} == {"MIT"} + assert (project / module).read_text() == source + + +@pytest.mark.parametrize("license_id", ["Apache-2.0", "LicenseRef-Missing"]) +def test_metadata_only_change_rechecks_assets(project, license_id): + (project / SUBTREE / "config.json").write_text("{}\n") + metadata = project / "REUSE.toml" + metadata.write_text( + metadata.read_text() + + 'SPDX-FileCopyrightText = "2020 NVIDIA CORPORATION & AFFILIATES"\n' + ) + assert CHECK["main"]() == 0 + metadata.write_text( + metadata.read_text().replace("LicenseRef-OpenMDW-1.1", license_id) + ) + assert CHECK["main"]() == 1 + + +@pytest.mark.parametrize("source", ["sidecar", "toml"]) +def test_explicit_first_party_exception_survives(project, source): + path = project / SUBTREE / "module.py" + path.write_text(f"# {COPYRIGHT}\n# SPDX-License-Identifier: Apache-2.0\n") + if source == "sidecar": + path.with_suffix(".py.license").write_text( + f"{COPYRIGHT}\nSPDX-License-Identifier: Apache-2.0\n" + ) + else: + metadata = project / "REUSE.toml" + metadata.write_text( + metadata.read_text() + + ( + f'\n[[annotations]]\npath = "{SUBTREE}/module.py"\n' + 'precedence = "override"\n' + f'{COPYRIGHT.split(": ", 1)[0]} = "2020 NVIDIA CORPORATION & AFFILIATES"\n' + 'SPDX-License-Identifier = "Apache-2.0"\n' + ) + ) + assert CHECK["main"](fix=True) == 0 + + +def test_missing_default_tag_is_added_without_changing_copyright(project): + path = project / "module.py" + path.write_text(f"# {COPYRIGHT}\n\nvalue = 1\n") + before = Project.from_directory(project).reuse_info_of(path)[0].copyright_notices + assert CHECK["main"](fix=True) == 1 + content = path.read_text() + after = Project.from_directory(project).reuse_info_of(path)[0].copyright_notices + assert after == before + assert "SPDX-License-Identifier: Apache-2.0" in content + assert "value = 1" in content + assert CHECK["main"](fix=True) == 0 + + +def test_unannotated_core_file_gets_apache_header(project): + path = project / "module.py" + path.write_text("value = 1\n") + assert CHECK["main"](fix=True) == 1 + content = path.read_text() + assert "NVIDIA CORPORATION & AFFILIATES" in content + assert "SPDX-License-Identifier: Apache-2.0" in content + assert CHECK["main"](fix=True) == 0 + + +def test_copyright_only_header_uses_validated_egopost_default(project): + path = project / SUBTREE / "module.py" + path.write_text(f"# {COPYRIGHT}\nvalue = 1\n") + assert CHECK["first_party_licenses"]( + Project.from_directory(project), path.relative_to(project) + ) == {"LicenseRef-OpenMDW-1.1"} + assert CHECK["main"]() == 0 + metadata = project / "REUSE.toml" + metadata.write_text( + metadata.read_text().replace("LicenseRef-OpenMDW-1.1", "Apache-2.0") + ) + assert ( + CHECK["check_policy"]( + Project.from_directory(project), path.relative_to(project) + ) + is False + ) + assert CHECK["main"]() == 1 + assert CHECK["main"](fix=True) == 1 + assert path.read_text() == f"# {COPYRIGHT}\nvalue = 1\n" + + +# REUSE-IgnoreEnd diff --git a/tests/python/packaging/test_wheel_contents.py b/tests/python/packaging/test_wheel_contents.py index d564cdeab5..2d3e7eae4a 100644 --- a/tests/python/packaging/test_wheel_contents.py +++ b/tests/python/packaging/test_wheel_contents.py @@ -121,3 +121,16 @@ def test_spacemouse_plugin_ships_runnable_for_teleop_session() -> None: assert (info.external_attr >> 16) & 0o111, ( f"{info.filename} lost its executable bit" ) + + +def test_capture_license_ships_in_distribution_metadata() -> None: + with zipfile.ZipFile(_wheel()) as archive: + metadata_path = next( + name for name in archive.namelist() if name.endswith(".dist-info/METADATA") + ) + metadata = BytesParser().parsebytes(archive.read(metadata_path)) + assert metadata["License-Expression"] == "Apache-2.0" + assert "LICENSE.md" in metadata.get_all("License-File", []) + license_path = metadata_path.removesuffix("METADATA") + "licenses/LICENSE.md" + assert archive.read(license_path) == (repo_root() / "LICENSE.md").read_bytes() + assert not any("isaaccapture_egopost/" in name for name in archive.namelist())