diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a94069c..dc20f01 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,12 +16,26 @@ concurrency: cancel-in-progress: true jobs: + images: + name: Define Base Images + runs-on: ubuntu-latest + outputs: + lint: ghcr.io/nvidia/cutile-python/lint:2026-01-12-aea51b7409cc + docs: ghcr.io/nvidia/cutile-python/docs:2026-01-12-96c265b9029e + build_py310: ghcr.io/nvidia/cutile-python/build_py_3.10_x86_64:2026-01-12-a3f084500fb0 + build_py311: ghcr.io/nvidia/cutile-python/build_py_3.11_x86_64:2026-01-12-d0a88a59d0fd + build_py312: ghcr.io/nvidia/cutile-python/build_py_3.12_x86_64:2026-01-12-9cf7e54a5580 + build_py313: ghcr.io/nvidia/cutile-python/build_py_3.13_x86_64:2026-01-12-7f9db97c8ad8 + steps: + - run: echo "Defining image tags" + lint: name: Lint + needs: images runs-on: ubuntu-latest timeout-minutes: 10 container: - image: ghcr.io/nvidia/cutile-python/lint:2025-12-06-4cb7d16e4c20 + image: ${{ needs.images.outputs.lint }} steps: - name: Checkout repository uses: actions/checkout@v6 @@ -30,10 +44,73 @@ jobs: run: flake8 - name: Run cpplint - run: python3 ci/cpplint.py + run: python scripts/cpplint.py - name: Check license headers (REUSE) - run: ci/scripts/check_license.sh + run: scripts/check_license.sh - name: Check inline samples are up to date - run: python3 test/tools/inline_samples.py --check + run: python test/tools/inline_samples.py --check + + docs: + name: Build Docs + needs: [images, build] + runs-on: ubuntu-latest + timeout-minutes: 10 + container: + image: ${{ needs.images.outputs.docs }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Download wheel + uses: actions/download-artifact@v4 + with: + name: wheel-py3.12-linux-x86_64 + path: dist/ + + - name: Install wheel + run: pip install dist/*.whl + + - name: Build documentation + run: make -C docs html + + - name: Upload docs artifact + uses: actions/upload-artifact@v4 + with: + name: docs-html + path: docs/build/html + retention-days: 7 + + build: + name: Build Wheel (Python ${{ matrix.python-version }}) + needs: images + runs-on: ubuntu-latest + timeout-minutes: 30 + strategy: + matrix: + include: + - python-version: "3.10" + image_key: build_py310 + - python-version: "3.11" + image_key: build_py311 + - python-version: "3.12" + image_key: build_py312 + - python-version: "3.13" + image_key: build_py313 + container: + image: ${{ needs.images.outputs[matrix.image_key] }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Build wheel + run: python setup.py bdist_wheel + + - name: Upload wheel artifact + uses: actions/upload-artifact@v4 + with: + name: wheel-py${{ matrix.python-version }}-linux-x86_64 + path: dist/*.whl + if-no-files-found: error + retention-days: 7 diff --git a/scripts/check_license.sh b/scripts/check_license.sh new file mode 100755 index 0000000..d663961 --- /dev/null +++ b/scripts/check_license.sh @@ -0,0 +1,12 @@ +#!/bin/bash +# SPDX-FileCopyrightText: Copyright (c) <2025> NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +ignore_files=("src/cuda/tile/VERSION") +outputs=$(reuse lint --lines | grep -v ${ignore_files[@]/#/-e }) +if [ -n "$outputs" ]; then + echo -e "License check failed\n${outputs}" + exit 1 +fi + diff --git a/scripts/cpplint.py b/scripts/cpplint.py new file mode 100644 index 0000000..48c4b1d --- /dev/null +++ b/scripts/cpplint.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) <2025> NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +import os +import sys + +file_extensions = [ + ".h", + ".hpp", + ".hh", + ".c", + ".C", + ".cpp", + ".cxx", + ".cc", + ".pyx", + ".pxd", +] +max_line_len = 100 + + +def should_lint(filename: str): + return any(filename.endswith(x) for x in file_extensions) + + +def lint(paths): + num_errors = 0 + num_files = 0 + + def report_error(message: str): + nonlocal num_errors + print(f"{full_name[len(path) + 1:]}:{i + 1}: {message}", file=sys.stderr) + num_errors += 1 + + for path in paths: + for root, dirs, files in os.walk(path): + for filename in files: + if not should_lint(filename): + continue + full_name = os.path.join(root, filename) + with open(full_name, "r") as f: + for i, line in enumerate(f): + if "noqa" in line: + continue + if "SPDX" in line: + continue + + length = len(line) + if line.endswith("\n"): + length -= 1 + if length > max_line_len: + report_error( + f"Line is longer than {max_line_len} characters" + ) + if length > 0 and line[length - 1].isspace(): + report_error("Trailing whitespace at the end of the line") + num_files += 1 + + if num_errors > 0: + print(f"Found {num_errors} errors", file=sys.stderr) + sys.exit(1) + elif num_files == 0: + print("No input files found!", file=sys.stderr) + sys.exit(2) + else: + print(f"Checked {num_files} files, all OK") + + +if __name__ == "__main__": + script_dir = os.path.dirname(__file__) + project_root = os.getcwd() + dirs = ["cext", "torch_cext", "src"] + paths = [os.path.join(project_root, d) for d in dirs] + lint(paths)