diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 676988ec0..588736bfe 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,14 +17,25 @@ repos: args: ['-fallback-style=none', '-style=file', '-i'] - repo: local hooks: - - id: doxygen-check - name: doxygen-check + - id: check-doxygen + name: check-doxygen entry: ./ci/pre-commit/doxygen.sh files: ^include/ types_or: [file] language: system pass_filenames: false verbose: true + - id: check-example-links + name: check-example-links + entry: ./ci/pre-commit/example_links.py + files: ^examples/ + types: [cuda] + language: python + pass_filenames: false + verbose: true + additional_dependencies: + - --extra-index-url=https://pypi.anaconda.org/rapidsai-wheels-nightly/simple + - gitpython default_language_version: python: python3 diff --git a/ci/pre-commit/example_links.py b/ci/pre-commit/example_links.py new file mode 100755 index 000000000..46576bda6 --- /dev/null +++ b/ci/pre-commit/example_links.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python + +import os +import json +import git +import re +import argparse +import zlib +import base64 +import subprocess +import sys + +def open_file(file_path): + with open(file_path, 'r') as file: + data = file.read() + return data + +def write_file(file_path, data): + with open(file_path, 'w') as file: + file.write(data) + +def create_compiler_explorer_url(file_path, COMPILER_ID, COMPILER_FLAGS): + source_code = open_file(file_path) + # Prepare the JSON payload + payload = { + "sessions": [ + { + "id": 1, + "language": "cuda", + "source": source_code, + "compilers": [ + { + "id": COMPILER_ID, + "options": COMPILER_FLAGS, + "libs": [ + { + "id": "cccl", + "version": "trunk" + }, + { + "id": "cuco", + "version": "dev" + } + ] + } + ] + } + ] + } + + # Convert the payload to JSON + config_json = json.dumps(payload, separators=(',', ':')) + + # Compress the JSON using zlib (deflate) + compressed = zlib.compress(config_json.encode('utf-8')) + + # Base64 encode the compressed data + encoded = base64.urlsafe_b64encode(compressed).decode('utf-8') + + # Replace problematic unicode characters + # As per the documentation, map characters in the range \u007F-\uFFFF + encoded = ''.join( + c if '\u0000' <= c <= '\u007F' else '\\u{:04x}'.format(ord(c)) + for c in encoded + ) + + # Construct the final URL + url = f'https://godbolt.org/clientstate/{encoded}' + + return url + +def update_readme_with_urls(readme_path, url_table, args): + # Open and read the README.md file + readme_content = open_file(readme_path) + lines = readme_content.splitlines() + + updated_lines = [] + mismatched_files = [] + # Iterate through each line of the README + for line in lines: + updated_line = line + # Check each file in the url_table + for file_name, url in url_table.items(): + # Check if the file name exists in the line + if file_name in line: + # Look for the specific godbolt link format and replace the URL + updated_line = re.sub( + r"\(see \[live example in godbolt\]\(https?://[^\)]+\)\)", + f"(see [live example in godbolt]({url}))", + line + ) + if updated_line != line: + mismatched_files.append(file_name) + updated_lines.append(updated_line) + + # Join the lines and write the updated content back to README.md + updated_content = "\n".join(updated_lines) + if args.fix: + write_file(readme_path, updated_content) + else: + if mismatched_files: + write_file(readme_path, updated_content) + print(mismatched_files) + sys.exit("FAILED: the above examples have out-of-date Godbolt links") + +COMPILER_ID = 'nvcc125u1' # Desired compiler ID +COMPILER_FLAGS = '-std=c++17 -arch=sm_70 --expt-extended-lambda' # NVCC compiler flags +EXAMPLES_DIR = "examples" # Path to the examples directory relative to the repo root + +def get_changed_cuda_files(): + # Run the git command to get the changed files + result = subprocess.run( + ['git', 'diff', '--cached', '--name-only', '--diff-filter=ACM'], + capture_output=True, + text=True + ) + + # Check if the command was successful + if result.returncode != 0: + print("Error executing git command:", result.stderr) + return [] + + # Get the output and filter for .cu files in EXAMPLES_DIR + return result.stdout.splitlines() + +def main(): + # Parse command-line arguments + parser = argparse.ArgumentParser(description="Ensure CUDA example files have up-to-date Godbolt links in the README.") + parser.add_argument('--fix', action='store_true', + help="Automatically update mismatched or outdated Godbolt links in the README with correct URLs.") + args = parser.parse_args() + + # Initialize the Git repository + repo = git.Repo(search_parent_directories=True) + repo_root = repo.git.rev_parse("--show-toplevel") + + # Ensure we are in the ci directory + ci_dir = os.path.join(repo_root, 'ci') + os.chdir(ci_dir) + + # Initialize a hash table (dictionary) to store file urls + url_table = {} + + # Get all CUDA files in the examples directory + example_files = [] + for root, _, files in os.walk(os.path.join(repo_root, EXAMPLES_DIR)): + for file_name in files: + if file_name.endswith('.cu'): + example_files.append(os.path.relpath(os.path.join(root, file_name), repo_root)) + + # Iterate through the example files and create URLs + for file_path in example_files: + full_file_path = os.path.join(repo_root, file_path) + url = create_compiler_explorer_url(full_file_path, COMPILER_ID, COMPILER_FLAGS) + if url: + url_table[file_path] = url # Store the file and its url in the hash table + # print(f"File: {file_path}: url: {url}") + + # Update README.md with the urls + readme_path = os.path.join(repo_root, "README.md") + update_readme_with_urls(readme_path, url_table, args) + +if __name__ == "__main__": + main()