From 7291f89abf9ac8810785c74ad1f3c02aeb168e4a Mon Sep 17 00:00:00 2001 From: Eric Shi Date: Sat, 21 Feb 2026 14:40:02 -0800 Subject: [PATCH] Add pre-commit CI job and replace artifact-based generated-file checks The check_generated_files.py pre-commit hook regenerates exports.h, version.h, and __init__.pyi from Python source without needing build artifacts. Add it (along with a version-consistency hook) to .pre-commit-config.yaml and run it in a new lightweight pre-commit CI job in both GitHub Actions and GitLab CI. This replaces the artifact-dependent git-diff checks: - GitHub: remove check-generated-files job, remove stubs check from check-build-docs-output - GitLab: slim check-generated-files down to docs-only, rename to check-generated-docs Move pre-commit hook scripts to tools/pre-commit-hooks/ for discoverability. Signed-off-by: Eric Shi --- .github/workflows/ci.yml | 44 ---- .gitlab-ci.yml | 22 +- .pre-commit-config.yaml | 16 ++ build_lib.py | 75 +------ .../publishing/check_version_consistency.py | 142 ------------- .../pre-commit-hooks/check_generated_files.py | 74 +++++++ .../check_version_consistency.py | 189 ++++++++++++++++++ warp/_src/generated_files.py | 105 ++++++++++ 8 files changed, 389 insertions(+), 278 deletions(-) delete mode 100755 tools/ci/publishing/check_version_consistency.py create mode 100644 tools/pre-commit-hooks/check_generated_files.py create mode 100755 tools/pre-commit-hooks/check_version_consistency.py create mode 100644 warp/_src/generated_files.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 38df12dc77..eef615a593 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,8 +53,6 @@ jobs: platform: macos cuda-sub-packages: '' cuda-non-cuda-packages: '' - outputs: - artifact-url: ${{ steps.native-headers.outputs.artifact-url }} steps: - name: Checkout repository uses: actions/checkout@v4 @@ -102,14 +100,6 @@ jobs: name: build-artifact-${{ matrix.platform }} path: warp/bin/ - - name: Upload native headers - uses: actions/upload-artifact@v4 - id: native-headers - with: - name: native-headers-${{ matrix.platform }} - path: | - warp/native/exports.h - warp/native/version.h test-warp: runs-on: ${{ matrix.os }} @@ -260,40 +250,6 @@ jobs: run: > git diff --exit-code docs/api_reference docs/language_reference || (echo "Please run build_docs.py (or download from $ARTIFACT_URL) and add docs/api_reference and docs/language_reference to your pull request." && false) - - name: Check stubs - env: - ARTIFACT_URL: ${{ needs.build-docs.outputs.artifact-url }} - run: > - git diff --exit-code warp/__init__.pyi || - (echo "Please run build_docs.py (or download from $ARTIFACT_URL) and add warp/__init__.pyi to your pull request." && false) - - check-generated-files: - runs-on: ubuntu-latest - needs: build-warp - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - name: Install uv - uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 - with: - version: "0.8.23" - - name: Set up Python - run: uv python install - - name: Download native headers - uses: actions/download-artifact@v4 - with: - name: native-headers-ubuntu-x86_64 - path: warp/native/ - - name: Check version consistency - run: uv run --no-project tools/ci/publishing/check_version_consistency.py --verbose - - name: Check exports.h is committed - run: > - git diff --exit-code warp/native/exports.h || - (echo "Please run build_lib.py and add warp/native/exports.h to your pull request." && false) - - name: Check version.h is committed - run: > - git diff --exit-code warp/native/version.h || - (echo "Please run build_lib.py and add warp/native/version.h to your pull request." && false) check-unnamespaced-symbols: runs-on: ubuntu-latest diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 51c8dd1fd9..c60aedb884 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -958,13 +958,12 @@ merge request docs: after_script: - echo "View the website at https://$CI_PROJECT_ROOT_NAMESPACE.$CI_PAGES_DOMAIN/-/$CI_PROJECT_NAME/-/jobs/$CI_JOB_ID/artifacts/public/index.html" -# Merge requests: Ensure that exports.h, __init__.pyi, and docs reference files have been +# Merge requests: Ensure that docs reference files generated by Sphinx have been # manually added to the MR if they are changed -check generated files: +check generated docs: stage: deploy image: $UV_TRIXIE_IMAGE needs: - - job: linux-x86_64 build - job: merge request docs optional: true extends: @@ -976,25 +975,12 @@ check generated files: when: on_failure expose_as: "Generated source files" paths: - - warp/native/exports.h - - warp/native/version.h - - warp/__init__.pyi - docs/api_reference - docs/language_reference script: - # Check version consistency across VERSION.md, config.py, and version.h - - uv run tools/ci/publishing/check_version_consistency.py --verbose - # Check that generated documentation files are committed - - > - git diff --exit-code warp/__init__.pyi docs/api_reference docs/language_reference || - (echo "Please run build_docs.py (or download from $CI_JOB_URL/artifacts/browse) and add modified files to your merge request." && false) - # Check that generated native files are committed - - > - git diff --exit-code warp/native/exports.h || - (echo "Please run build_lib.py (or download from $CI_JOB_URL/artifacts/browse) and add warp/native/exports.h to your merge request." && false) - > - git diff --exit-code warp/native/version.h || - (echo "Please run build_lib.py (or download from $CI_JOB_URL/artifacts/browse) and add warp/native/version.h to your merge request." && false) + git diff --exit-code docs/api_reference docs/language_reference || + (echo "Please run build_docs.py (or download from $CI_JOB_URL/artifacts/browse) and add docs/api_reference and docs/language_reference to your merge request." && false) # Build documentation and publish on GitLab # This only runs in the default branch pipeline. The "pages" name is special for GitLab. diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e3c1e3cdf8..47a4ed7125 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,6 +11,22 @@ ci: # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks repos: + - repo: local + hooks: + - id: warp-check-generated-files + name: "warp: regenerate generated files" + entry: python tools/pre-commit-hooks/check_generated_files.py + language: python + # NOTE: ruff version must match ruff-pre-commit rev below to avoid formatting drift. + additional_dependencies: [numpy, ruff==0.14.10] + always_run: true + pass_filenames: false + - id: warp-check-version-consistency + name: "warp: check version consistency and regenerate version.h" + entry: python tools/pre-commit-hooks/check_version_consistency.py --verbose + language: python + always_run: true + pass_filenames: false - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. rev: v0.14.10 diff --git a/build_lib.py b/build_lib.py index 0823f2f3e2..12f08aac5a 100644 --- a/build_lib.py +++ b/build_lib.py @@ -38,7 +38,7 @@ import build_llvm import warp._src.build_dll as build_dll import warp.config as config -from warp._src.context import export_builtins +from warp._src.generated_files import generate_exports_header_file, generate_version_header def handle_ci_nightly_build(base_path: str) -> str | None: @@ -92,40 +92,6 @@ def handle_ci_nightly_build(base_path: str) -> str | None: return dev_version_string -def generate_version_header(base_path: str, version: str) -> None: - """Generate version.h with WP_VERSION_STRING macro.""" - version_header_path = os.path.join(base_path, "warp", "native", "version.h") - current_year = datetime.date.today().year - - copyright_notice = f"""/* - * SPDX-FileCopyrightText: Copyright (c) {current_year} NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -""" - - with open(version_header_path, "w") as f: - f.write(copyright_notice) - f.write("#ifndef WP_VERSION_H\n") - f.write("#define WP_VERSION_H\n\n") - f.write(f'#define WP_VERSION_STRING "{version}"\n\n') - f.write("#endif // WP_VERSION_H\n") - - print(f"Generated {version_header_path} with version {version}") - - def find_cuda_sdk() -> str | None: # check environment variables for env in ["WARP_CUDA_PATH", "CUDA_HOME", "CUDA_PATH"]: @@ -249,45 +215,6 @@ def lib_name(name: str) -> str: return f"{name}.so" -def generate_exports_header_file(base_path: str) -> None: - """Generates warp/native/exports.h, which lets built-in functions be callable from outside kernels.""" - export_path = os.path.join(base_path, "warp", "native", "exports.h") - os.makedirs(os.path.dirname(export_path), exist_ok=True) - - try: - with open(export_path, "w") as f: - copyright_notice = """/* - * SPDX-FileCopyrightText: Copyright (c) 2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -""" - f.write(copyright_notice) - export_builtins(f) - - print(f"Finished writing {export_path}") - except FileNotFoundError: - print(f"Error: The file '{export_path}' was not found.") - except PermissionError: - print(f"Error: Permission denied. Unable to write to '{export_path}'.") - except OSError as e: - print(f"Error: An OS-related error occurred: {e}") - except Exception as e: - print(f"An unexpected error occurred: {e}") - - def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser( description="Build Warp native libraries with optional CUDA, LLVM, and MathDx support", diff --git a/tools/ci/publishing/check_version_consistency.py b/tools/ci/publishing/check_version_consistency.py deleted file mode 100755 index 3801dc16e6..0000000000 --- a/tools/ci/publishing/check_version_consistency.py +++ /dev/null @@ -1,142 +0,0 @@ -#!/usr/bin/env python3 -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Check version consistency across VERSION.md, config.py, and version.h""" - -import argparse -import re -import sys -from pathlib import Path - - -def read_version_md(path: Path) -> str: - """Read version from VERSION.md.""" - with open(path) as f: - version = f.readline().strip() - return version - - -def read_config_py_version(path: Path) -> str: - """Read version from warp/config.py.""" - with open(path) as f: - content = f.read() - - match = re.search(r'^version:\s*str\s*=\s*["\']([^"\']+)["\']', content, re.MULTILINE) - if not match: - raise ValueError(f"Could not find version in {path}") - - return match.group(1) - - -def read_version_h(path: Path) -> str: - """Read WP_VERSION_STRING from warp/native/version.h.""" - with open(path) as f: - content = f.read() - - match = re.search(r'#define\s+WP_VERSION_STRING\s+"([^"]+)"', content) - if not match: - raise ValueError(f"Could not find WP_VERSION_STRING in {path}") - - return match.group(1) - - -def check_version_consistency(base_path: Path, verbose: bool = False) -> bool: - """Check that versions are consistent across all files. - - Args: - base_path: Root directory of the Warp repository. - verbose: Print detailed information. - - Returns: - True if all versions match, False otherwise. - """ - version_md_path = base_path / "VERSION.md" - config_py_path = base_path / "warp" / "config.py" - version_h_path = base_path / "warp" / "native" / "version.h" - - # Check that all files exist - missing_files: list[str] = [] - for path in [version_md_path, config_py_path, version_h_path]: - if not path.exists(): - missing_files.append(str(path)) - - if missing_files: - print("ERROR: Missing version files:") - for file_path in missing_files: - print(f" - {file_path}") - return False - - # Read versions - try: - version_md = read_version_md(version_md_path) - config_py_version = read_config_py_version(config_py_path) - version_h_version = read_version_h(version_h_path) - except (OSError, ValueError) as e: - print(f"ERROR: Failed to read version files: {e}") - return False - - if verbose: - print(f"VERSION.md: {version_md}") - print(f"config.py: {config_py_version}") - print(f"version.h: {version_h_version}") - - # Check consistency - all_match = True - - if version_md != config_py_version: - print(f"ERROR: Version mismatch between VERSION.md ({version_md}) and config.py ({config_py_version})") - all_match = False - - if version_md != version_h_version: - print(f"ERROR: Version mismatch between VERSION.md ({version_md}) and version.h ({version_h_version})") - all_match = False - - if config_py_version != version_h_version: - print(f"ERROR: Version mismatch between config.py ({config_py_version}) and version.h ({version_h_version})") - all_match = False - - if all_match: - if verbose: - print(f"✓ All versions consistent: {version_md}") - return True - else: - print("\nPlease ensure VERSION.md, warp/config.py, and warp/native/version.h all contain the same version.") - print("After updating VERSION.md, run 'uv run build_lib.py' to regenerate version.h and update config.py.") - return False - - -def main(): - parser = argparse.ArgumentParser(description="Check version consistency across Warp source files") - parser.add_argument( - "--base-path", - type=Path, - default=Path(__file__).parent.parent.parent.parent, - help="Base path to the Warp repository (default: script location's repo root)", - ) - parser.add_argument( - "-v", "--verbose", - action="store_true", - help="Print detailed version information", - ) - - args = parser.parse_args() - - success = check_version_consistency(args.base_path, verbose=args.verbose) - sys.exit(0 if success else 1) - - -if __name__ == "__main__": - main() diff --git a/tools/pre-commit-hooks/check_generated_files.py b/tools/pre-commit-hooks/check_generated_files.py new file mode 100644 index 0000000000..05c44c7c16 --- /dev/null +++ b/tools/pre-commit-hooks/check_generated_files.py @@ -0,0 +1,74 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Pre-commit hook that regenerates exports.h and __init__.pyi. + +These files are derived from warp/_src/generated_files.py (which delegates to +warp/_src/context.py) and related source files. +When those sources change, the generated files must be updated to match. +This hook regenerates them in place; the pre-commit framework detects any +modifications and blocks the commit so the developer can stage them. + +version.h is handled separately by the warp-check-version-consistency hook, +which has no heavy dependencies and can run on pre-commit.ci. + +Usage (pre-commit hook): + Configured in .pre-commit-config.yaml with language: python + and additional_dependencies: [numpy, ruff] +""" + +from __future__ import annotations + +import os +import subprocess +import sys + +# Ensure the repo root is on sys.path so warp is importable in the +# pre-commit isolated virtualenv (which only has additional_dependencies). +base_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +sys.path.insert(0, base_path) + +import warp # noqa: E402 — populates builtin_functions via builtins.py +from warp._src.generated_files import export_stubs, generate_exports_header_file # noqa: E402 + + +def main() -> int: + # Regenerate warp/native/exports.h + generate_exports_header_file(base_path) + + # Regenerate warp/__init__.pyi + stub_path = os.path.join(base_path, "warp", "__init__.pyi") + with open(stub_path, "w", encoding="utf-8") as f: + export_stubs(f) + + # Format with ruff so the generated file is stable across hook runs. + # export_stubs() emits trailing whitespace and non-canonical formatting; + # without this step, ruff-format would modify the file on every commit. + # ruff is provided via additional_dependencies in .pre-commit-config.yaml. + from ruff.__main__ import find_ruff_bin # noqa: PLC0415 + + ruff = find_ruff_bin() + subprocess.run([ruff, "format", stub_path], check=True) + # Exit code 1 = unfixable lint violations (expected for generated code). + # Exit code 2 = internal ruff error (bad config, crash) which should fail the hook. + result = subprocess.run([ruff, "check", "--fix", stub_path]) + if result.returncode >= 2: + raise SystemExit(result.returncode) + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/pre-commit-hooks/check_version_consistency.py b/tools/pre-commit-hooks/check_version_consistency.py new file mode 100755 index 0000000000..becf58628d --- /dev/null +++ b/tools/pre-commit-hooks/check_version_consistency.py @@ -0,0 +1,189 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Pre-commit hook that checks version consistency and regenerates version.h. + +VERSION.md is the single source of truth for the Warp version string. +This hook regenerates warp/native/version.h from VERSION.md (fixer) and +verifies that warp/config.py agrees (checker). If config.py disagrees the +commit is blocked with an actionable error message. + +This hook has zero dependencies beyond the standard library, so it runs +everywhere — including pre-commit.ci. + +Usage (pre-commit hook): + Configured in .pre-commit-config.yaml with language: python +""" + +from __future__ import annotations + +import argparse +import datetime +import re +import sys +from pathlib import Path + + +def _c_copyright_header(year: int | str) -> str: + """Return a C-style copyright/license block for generated headers. + + NOTE: This duplicates warp._src.generated_files._c_copyright_header so + that this hook can run without importing the warp package. + """ + return f"""/* + * SPDX-FileCopyrightText: Copyright (c) {year} NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +""" + + +def _regenerate_version_header(base_path: Path, version: str) -> None: + """Regenerate warp/native/version.h from the given version string. + + Only writes the file when the content has actually changed, so that file + modification timestamps are preserved and downstream build systems (make, + ninja, etc.) don't trigger unnecessary rebuilds. + + NOTE: Output must stay byte-identical to + warp._src.generated_files.generate_version_header. + """ + version_header_path = base_path / "warp" / "native" / "version.h" + + new_content = _c_copyright_header(datetime.date.today().year) + new_content += "#ifndef WP_VERSION_H\n" + new_content += "#define WP_VERSION_H\n\n" + new_content += f'#define WP_VERSION_STRING "{version}"\n\n' + new_content += "#endif // WP_VERSION_H\n" + + try: + with open(version_header_path) as f: + if f.read() == new_content: + return + except FileNotFoundError: + pass + + with open(version_header_path, "w") as f: + f.write(new_content) + + +def read_version_md(path: Path) -> str: + """Read and validate version from VERSION.md.""" + with open(path) as f: + version = f.readline().strip() + if not re.fullmatch(r"\d+\.\d+\.\d+(\.\w+)?", version): + raise ValueError(f"VERSION.md contains invalid version string: {version!r}") + return version + + +def read_config_py_version(path: Path) -> str: + """Read version from warp/config.py.""" + with open(path) as f: + content = f.read() + + match = re.search(r'^version:\s*str\s*=\s*["\']([^"\']+)["\']', content, re.MULTILINE) + if not match: + raise ValueError(f"Could not find version in {path}") + + return match.group(1) + + +def check_version_consistency(base_path: Path, verbose: bool = False) -> bool: + """Regenerate version.h and check that config.py matches VERSION.md. + + Args: + base_path: Root directory of the Warp repository. + verbose: Print detailed information. + + Returns: + True if config.py matches VERSION.md, False otherwise. + """ + version_md_path = base_path / "VERSION.md" + config_py_path = base_path / "warp" / "config.py" + + # Check that source files exist + for path in [version_md_path, config_py_path]: + if not path.exists(): + print(f"ERROR: Missing file: {path}", file=sys.stderr) + return False + + # Read versions from source files + try: + version_md = read_version_md(version_md_path) + config_py_version = read_config_py_version(config_py_path) + except (OSError, ValueError) as e: + print(f"ERROR: Failed to read version files: {e}", file=sys.stderr) + return False + + # Regenerate version.h from VERSION.md (the source of truth). + # pre-commit detects the file modification and asks the developer to stage it. + _regenerate_version_header(base_path, version_md) + + if verbose: + print(f"VERSION.md: {version_md}") + print(f"config.py: {config_py_version}") + print(f"version.h: regenerated from VERSION.md") + + # Check that config.py (a source file) matches VERSION.md + if version_md != config_py_version: + print( + f"ERROR: Version mismatch between VERSION.md ({version_md}) and config.py ({config_py_version})", + file=sys.stderr, + ) + print( + "\nPlease ensure VERSION.md and warp/config.py contain the same version.", + file=sys.stderr, + ) + return False + + if verbose: + print(f"All versions consistent: {version_md}") + return True + + +def main() -> int: + parser = argparse.ArgumentParser(description="Check version consistency across Warp source files") + parser.add_argument( + "--base-path", + type=Path, + default=Path(__file__).parent.parent.parent, + help="Base path to the Warp repository (default: script location's repo root)", + ) + parser.add_argument( + "-v", "--verbose", + action="store_true", + help="Print detailed version information", + ) + + args = parser.parse_args() + + success = check_version_consistency(args.base_path, verbose=args.verbose) + return 0 if success else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/warp/_src/generated_files.py b/warp/_src/generated_files.py new file mode 100644 index 0000000000..6d451ff004 --- /dev/null +++ b/warp/_src/generated_files.py @@ -0,0 +1,105 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Functions that generate derived source files (headers, stubs). + +These functions have no build-toolchain dependencies (no CUDA, LLVM, etc.) +but do require the ``warp`` package (and transitively ``numpy``) to be +importable. They are used by ``build_lib.py``, ``build_docs.py``, and the +pre-commit hooks in ``tools/pre-commit-hooks/``. + +Re-exports ``export_builtins`` and ``export_stubs`` from +``warp._src.context`` for convenience. +""" + +from __future__ import annotations + +import datetime +import os + +from warp._src.context import export_builtins, export_stubs + +__all__ = [ + "export_builtins", + "export_stubs", + "generate_exports_header_file", + "generate_version_header", +] + + +def _c_copyright_header(year: int | str) -> str: + """Return a C-style copyright/license block for generated headers.""" + return f"""/* + * SPDX-FileCopyrightText: Copyright (c) {year} NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +""" + + +def generate_version_header(base_path: str, version: str) -> None: + """Generate version.h with WP_VERSION_STRING macro. + + Only writes the file when the content has actually changed, so that file + modification timestamps are preserved and downstream build systems don't + trigger unnecessary rebuilds. + + NOTE: The pre-commit hook ``check_version_consistency.py`` has an inlined + copy of this function (``_regenerate_version_header``) so it can run + without importing ``warp``. Keep the two in sync. + """ + version_header_path = os.path.join(base_path, "warp", "native", "version.h") + + new_content = _c_copyright_header(datetime.date.today().year) + new_content += "#ifndef WP_VERSION_H\n" + new_content += "#define WP_VERSION_H\n\n" + new_content += f'#define WP_VERSION_STRING "{version}"\n\n' + new_content += "#endif // WP_VERSION_H\n" + + try: + with open(version_header_path) as f: + if f.read() == new_content: + print(f"{version_header_path} is up to date (version {version})") + return + except FileNotFoundError: + pass + + with open(version_header_path, "w") as f: + f.write(new_content) + + print(f"Generated {version_header_path} with version {version}") + + +def generate_exports_header_file(base_path: str) -> None: + """Generate warp/native/exports.h with host-side wrappers for built-in functions.""" + export_path = os.path.join(base_path, "warp", "native", "exports.h") + + with open(export_path, "w") as f: + f.write(_c_copyright_header(2022)) + export_builtins(f) + + print(f"Finished writing {export_path}")