Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

name: "Docs"

on:
workflow_dispatch:

defaults:
run:
shell: bash -x -e -u -o pipefail {0}

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

env:
UV_CACHE_DIR: /tmp/uv-cache

jobs:
build:
name: Build versioned docs
runs-on: linux-amd64-gpu-h100-latest-1
timeout-minutes: 60
container:
image: nvcr.io/nvidia/cuda:13.1.0-runtime-ubuntu24.04
options: --gpus all
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Install apt requirements
env:
DEBIAN_FRONTEND: "noninteractive"
TZ: "Etc/UTC"
run: |
apt-get update && \
apt-get install -y curl \
git \
graphviz \
build-essential

- name: Setup UV
env:
UV_VERSION: "0.9.25"
UV_CHECKSUM: "1e1aea6cead1a07a7cee24f6eaec415b"
run: |
UV_INSTALLER=$(mktemp)
curl -LsSf "https://astral.sh/uv/${UV_VERSION}/install.sh" -o "$UV_INSTALLER"
echo "${UV_CHECKSUM} ${UV_INSTALLER}" | md5sum -c -
sh "$UV_INSTALLER"
rm "$UV_INSTALLER"
echo "$HOME/.local/bin" >> "$GITHUB_PATH"

- name: Install dependencies
run: |
export PATH="$HOME/.local/bin:$PATH"
uv sync --all-extras --group docs

- name: Build versioned documentation
run: |
export PATH="$HOME/.local/bin:$PATH"
make docs-versioned

- name: Verify site artifact
run: |
test -f docs/_build/site/index.html
test -f docs/_build/site/main/index.html
test -f docs/_build/site/versions.json
test -f docs/_build/site/_static/switcher.json
test -f docs/_build/site/.nojekyll
grep -q "version-switcher__container" docs/_build/site/main/index.html
grep -q "theme_switcher_version_match = 'main'" docs/_build/site/main/index.html

- name: Upload docs site artifact
uses: actions/upload-artifact@v4
with:
name: nvalchemi-docs-site
path: docs/_build/site
if-no-files-found: error
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ htmlcov/
.coverage
.coverage.*
.cache
.uv-cache/
nosetests.xml
coverage.xml
*.cover
Expand Down
26 changes: 26 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,19 @@

.DEFAULT_GOAL := help

UV_CACHE_DIR ?= $(CURDIR)/.uv-cache
XDG_CACHE_HOME ?= $(CURDIR)/.cache
MPLCONFIGDIR ?= $(XDG_CACHE_HOME)/matplotlib
# Keep `uv run` aligned with the selected CUDA stack. Bare `uv run` performs a
# sync without extras, which can replace a CUDA 12 environment with the default.
CUDA_EXTRA ?= cu13
OPTIONAL_EXTRAS ?=
UV_EXTRA_FLAGS = --extra $(CUDA_EXTRA) $(foreach extra,$(OPTIONAL_EXTRAS),--extra $(extra))
UV_SYNC ?= uv sync $(UV_EXTRA_FLAGS)
UV_RUN ?= uv run $(UV_EXTRA_FLAGS)
export UV_CACHE_DIR
export XDG_CACHE_HOME
export MPLCONFIGDIR

# ==============================================================================
# INSTALLATION
Expand Down Expand Up @@ -179,10 +185,30 @@ docs-install-examples: ## Install example dependencies
docs: docs-install-examples ## Build documentation
cd docs && make html

DOCS_SITE_URL ?= https://nvidia.github.io/nvalchemi-toolkit
DOCS_PREVIEW_HOST ?= 127.0.0.1
DOCS_PREVIEW_PORT ?= 8000
DOCS_PREVIEW_URL ?= http://$(DOCS_PREVIEW_HOST):$(DOCS_PREVIEW_PORT)
DOCS_UV_RUN ?= uv run --group docs

.PHONY: docs-versioned
docs-versioned: docs-install-examples ## Build versioned documentation site
$(DOCS_UV_RUN) python docs/build_versioned.py --site-url "$(DOCS_SITE_URL)"

.PHONY: docs-versioned-preview
docs-versioned-preview: docs-install-examples ## Build versioned documentation site for local preview
PLOT_GALLERY=False FILENAME_PATTERN='^$$' $(DOCS_UV_RUN) python docs/build_versioned.py --site-url "$(DOCS_PREVIEW_URL)"

.PHONY: docs-versioned-serve
docs-versioned-serve: ## Serve the local versioned documentation preview
@echo "Serving docs at $(DOCS_PREVIEW_URL)/main/"
$(DOCS_UV_RUN) python -m http.server "$(DOCS_PREVIEW_PORT)" --bind "$(DOCS_PREVIEW_HOST)" --directory docs/_build/site

.PHONY: docs-clean
docs-clean: ## Clean documentation build
cd docs && make clean
rm -rf docs/examples/
rm -rf docs/_build/site


.PHONY: docs-rebuild
Expand Down
194 changes: 194 additions & 0 deletions docs/build_versioned.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
# 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.

"""Build the versioned Sphinx documentation site."""

from __future__ import annotations

import argparse
import json
import os
import shutil
import subprocess
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parents[1]
DOCS_ROOT = REPO_ROOT / "docs"
DEFAULT_OUTPUT_DIR = DOCS_ROOT / "_build" / "site"
DEFAULT_SITE_URL = "https://nvidia.github.io/nvalchemi-toolkit"


def _run(command: list[str], env: dict[str, str] | None = None) -> None:
"""Run a command from the repository root."""
subprocess.run(command, cwd=REPO_ROOT, check=True, env=env) # noqa: S603


def _versioned_tags(output_dir: Path) -> list[str]:
"""Return release tags that were emitted by sphinx-multiversion."""
result = subprocess.run( # noqa: S603
["git", "tag", "--sort=-v:refname", "--list", "v[0-9]*"], # noqa: S607
cwd=REPO_ROOT,
check=True,
capture_output=True,
text=True,
)
available_versions = {
path.name
for path in output_dir.iterdir()
if path.is_dir() and (path / "index.html").exists()
}
return [
tag
for tag in result.stdout.splitlines()
if tag.strip() and tag.strip() in available_versions
]


def _site_versions(output_dir: Path, site_url: str) -> list[dict[str, str]]:
"""Return PyData-compatible version switcher entries."""
normalized_site_url = site_url.rstrip("/")
versions = []
if (output_dir / "main" / "index.html").exists():
versions.append(
{
"name": "main (development)",
"version": "main",
"url": f"{normalized_site_url}/main/",
}
)

versions.extend(
[
{
"name": tag,
"version": tag,
"url": f"{normalized_site_url}/{tag}/",
}
for tag in _versioned_tags(output_dir)
]
)
return versions


def _write_version_manifests(output_dir: Path, site_url: str) -> None:
"""Write root and theme-static version switcher manifests."""
versions = _site_versions(output_dir, site_url)
manifest = json.dumps(versions, indent=2) + "\n"

(output_dir / "versions.json").write_text(
manifest,
encoding="utf-8",
)
static_dir = output_dir / "_static"
static_dir.mkdir(exist_ok=True)
(static_dir / "switcher.json").write_text(manifest, encoding="utf-8")


def _write_root_redirect(output_dir: Path) -> None:
"""Write a root page that redirects to the main documentation."""
(output_dir / "index.html").write_text(
"""<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="refresh" content="0; url=main/">
<link rel="canonical" href="main/">
<title>ALCHEMI Toolkit documentation</title>
</head>
<body>
<p><a href="main/">Continue to the ALCHEMI Toolkit documentation.</a></p>
<script>window.location.replace("main/" + window.location.search + window.location.hash);</script>
</body>
</html>
""",
encoding="utf-8",
)


def _write_legacy_404_redirect(output_dir: Path) -> None:
"""Write a GitHub Pages fallback for pre-versioned deep links."""
(output_dir / "404.html").write_text(
"""<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>ALCHEMI Toolkit documentation</title>
</head>
<body>
<p>Redirecting to the current ALCHEMI Toolkit documentation.</p>
<script>
(function () {
var root = "/nvalchemi-toolkit/";
var path = window.location.pathname;
if (path.indexOf(root) !== 0) {
return;
}
var relative = path.slice(root.length);
if (!relative || relative.indexOf("main/") === 0 || /^v\\d/.test(relative)) {
return;
}
window.location.replace(root + "main/" + relative + window.location.search + window.location.hash);
}());
</script>
</body>
</html>
""",
encoding="utf-8",
)


def build_versioned_docs(output_dir: Path, site_url: str) -> None:
"""Build the versioned documentation site into an output directory."""
if output_dir.exists():
shutil.rmtree(output_dir)
output_dir.parent.mkdir(parents=True, exist_ok=True)

normalized_site_url = site_url.rstrip("/")
build_env = os.environ.copy()
build_env["DOCS_SITE_URL"] = normalized_site_url
build_env["DOCS_SWITCHER_JSON_URL"] = f"{normalized_site_url}/_static/switcher.json"

_run(["sphinx-multiversion", str(DOCS_ROOT), str(output_dir)], env=build_env)
(output_dir / ".nojekyll").touch()
_write_version_manifests(output_dir, site_url)
_write_root_redirect(output_dir)
_write_legacy_404_redirect(output_dir)


def parse_args() -> argparse.Namespace:
"""Parse command-line arguments."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--output-dir",
type=Path,
default=DEFAULT_OUTPUT_DIR,
help="Directory where the complete versioned site will be written.",
)
parser.add_argument(
"--site-url",
default=DEFAULT_SITE_URL,
help="Public site URL used in versions.json.",
)
return parser.parse_args()


def main() -> None:
"""Build the versioned documentation site."""
args = parse_args()
build_versioned_docs(args.output_dir, args.site_url)


if __name__ == "__main__":
main()
Loading