Skip to content
Open
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
17 changes: 17 additions & 0 deletions build_release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
# Environment variables (optional):
# CUVSLAM_SRC_DIR Source directory (default: /cuvslam/src)
# CUVSLAM_DST_DIR Build directory (default: /cuvslam/build)
# CUVSLAM_REQUIRE_CLEAN_SOURCE Fail if tracked source files differ from HEAD
# EXTRA_CMAKE_ARGS Additional arguments passed to cmake
#
# Note: Boolean flags (--modules_test, --api_test, --build_lib, --build_docs)
Expand Down Expand Up @@ -99,6 +100,22 @@ if [ ! -d "$SRC" ]; then
exit 1
fi

case "${CUVSLAM_REQUIRE_CLEAN_SOURCE:-false}" in
1|true)
if ! git -c safe.directory="$SRC" -C "$SRC" diff --quiet HEAD --; then
echo "Error: this build requires a clean tracked source tree:" >&2
git -c safe.directory="$SRC" -C "$SRC" status --short --untracked-files=no >&2 || true
exit 1
fi
;;
0|false|"")
;;
*)
echo "Error: invalid CUVSLAM_REQUIRE_CLEAN_SOURCE='${CUVSLAM_REQUIRE_CLEAN_SOURCE}' (expected true/false or 1/0)" >&2
exit 1
;;
esac

set -v # echo each command

mkdir -p $DST
Expand Down
20 changes: 20 additions & 0 deletions cuvslam-skills/cuvslam-ci/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ CI scripts (`scripts/`):
- `cuvslam_kpi_report.py` - KPI math, Markdown `.table`, soft drift check.
- `kpi_baseline_ranges.json` - committed static drift ranges.
- `package_cpp_dist.sh` - creates and validates the curated, versioned C++ SDK archive used by Actions and Releases.
- `Dockerfile` / `build_cuvslam_in_docker.sh` - product build image and wrapper; preserve Git/LFS metadata used by
`get_version()`.

Dataset tooling: `tools/datasets/<name>/` (download + `prepare_<name>.sh`), `tools/cuvslam_app/` (eval runner and `edex_reader.py`).

Expand Down Expand Up @@ -61,11 +63,29 @@ Do not reintroduce gzip: provisioning uses uncompressed `.tar` to cap memory on
- PR config: `pr-verify.yml` runs eval only on `build-test-x86` (fork-gated). `EVAL_CONFIG` is the static slug label for the PR table.
- Active dataset set: `DATASETS[]` in `run_eval.sh` is global; PR and nightly run the same set. There is no per-pipeline dataset selection today. To run a different set in PR vs nightly, add an env-selected subset in `run_eval.sh` and have each workflow pass the selector.

## Task: preserve nightly version provenance

`VERSION` controls the package filename, while `get_version()` is generated independently as
`MAJOR.MINOR.PATCH+<short-git-sha>[-modified]`. The `-modified` suffix means the build container's
tracked worktree differs from `HEAD`.

1. Keep `git-lfs` installed and configured system-wide in `scripts/Dockerfile`. Nightly pulls LFS objects on the host,
and Git without the LFS clean filter misidentifies the materialized files as source modifications.
2. Do not edit tracked files before the C++ build. Pass runner-specific settings such as the Ubuntu Ports mirror
through Docker build arguments instead.
3. Keep `CUVSLAM_REQUIRE_CLEAN_SOURCE=1` on nightly C++ builds. It checks the source using the same image and Git/LFS
configuration that generate the version header.
4. Pass the expected package version and checked-out full Git SHA to `verify_pycuvslam_wheel_in_docker.sh`. The
verifier must reject `-modified` and a mismatched embedded revision before artifacts are uploaded.
5. When changing checkout, LFS, Docker build, or version logic, test both an LFS-materialized clean checkout and an
intentional tracked edit.

## Hard rules

Detail in [reference.md](reference.md). The load-bearing ones:

- Dataset and eval steps stay fork-gated (`if: ... head.repo == github.repository`); never run fork code on dataset runners.
- Eval uses the read-only `AWS_S3_RO_*` secrets; only `provision-datasets.yml` uses the read-write `AWS_S3_*` pair.
- KPI history directories and eval artifact names carry the `platform-cuda-ubuntu` slug so matrix configs never overwrite each other.
- Nightly distributables must report `VERSION+<short-checked-out-sha>` without `-modified`.
- Ruleset, CODEOWNERS, and `.github/workflows/**` changes go in their own `[infra]` MR (enforced by the `isolated-ruleset-change` pre-commit hook).
8 changes: 8 additions & 0 deletions cuvslam-skills/cuvslam-ci/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ Repository secrets, split read from write so fork-reachable jobs never hold a ke
## Nightly artifacts and releases

- `VERSION` is the single package-version source for scheduled, manual, and release runs. Release dispatch additionally requires the `release/vX.Y.Z` branch name to match `VERSION`.
- The runtime library version is generated separately as `MAJOR.MINOR.PATCH+<short-git-sha>[-modified]`. Nightly
requires a clean tracked source tree and verifies that every wheel identifies the checked-out SHA without the
`-modified` suffix.
- Final distributables are packaged once by their producing job and uploaded directly, without an Actions ZIP wrapper: `cuvslam-cpp-<version>-<slug>.tar.gz`, the versioned Python wheels, `cuvslam-docs-<version>.tar.gz`, and `cuvslam-evaluation-<version>.tar.gz`.
- Each C++ archive contains only `bin/{libcuvslam.so,cuvslam_api_launcher}`, `include/cuvslam/{cuvslam2.h,cuvslam_gpu.h,ground_constraint2.h}`, and `LICENSE`. `scripts/package_cpp_dist.sh` creates and validates this manifest.
- A release job downloads the `cuvslam-*` distributables and promotes the same bytes to the draft Release. Test-result artifacts remain Actions-only.
Expand All @@ -93,10 +96,15 @@ Repository secrets, split read from write so fork-reachable jobs never hold a ke
- KPI history publish uses a direct copy: the S3-backed history mount does not implement `rename(2)`, so `run_eval.sh` copies the KPI JSON straight to the target rather than staging to `.tmp` and `mv`.
- Fail-fast on `RUNNER_STORAGE_ROOT`: the nightly eval step errors if it is unset rather than building a filesystem-root path; `check_eval_prerequisites.sh` also requires it.
- Runner requirements: every eval-enabled runner needs the `RUNNER_STORAGE_ROOT` mount and configured repository secrets/variables. The AWS CLI and `check_eval_prerequisites.sh` run in the CI tools image and read the mounted storage and credentials there.
- Version provenance: `scripts/Dockerfile` keeps `git-lfs` filters configured system-wide because nightly materializes
LFS files before mounting the source read-only into the product build container. Runner-specific build configuration
must use Docker build arguments rather than rewriting tracked source files.
- Change isolation: ruleset, CODEOWNERS, and `.github/workflows/**` changes go in their own MR, enforced by the `isolated-ruleset-change` pre-commit hook. Use the `[infra]` MR prefix.

## Learnings

- Thor has surfaced a GitHub runner-agent `set_output` / node24 failure during checkout, independent of this wiring; watch the first Thor eval run.
- `EVAL_CONFIG` in `pr-verify.yml` is a static label matching the build script defaults (CUDA 12.6.3 / Ubuntu 24.04). Update it if those defaults change, or pin the PR build's `CUDA_VERSION` / `UBUNTU_VERSION` so the label cannot drift.
- The AWS CLI and the scripts read `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` (standard names). The `AWS_S3_*` strings in script error messages name the repository secrets to configure, not env vars the scripts read.
- Git LFS materializes pointer files as their binary contents. A build container without the LFS clean filter reports
those files as modified even when the checkout is clean, causing `get_version()` to gain `-modified`.
14 changes: 13 additions & 1 deletion scripts/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,24 @@ RUN echo "Base image: ${BASE_IMAGE:-default}" && \
echo "Actual CUDA:" && (nvcc --version 2>/dev/null || echo "nvcc not found")

ARG DEBIAN_FRONTEND=noninteractive
ARG UBUNTU_PORTS_MIRROR=""

COPY scripts/ubuntu-ports-mirror.conf /tmp/ubuntu-ports-mirror.conf

RUN if [ -n "${UBUNTU_PORTS_MIRROR}" ]; then \
set -eux; \
find /etc/apt -type f \( -name '*.list' -o -name '*.sources' \) \
-exec sed -Ei "s#https?://ports\\.ubuntu\\.com/ubuntu-ports#${UBUNTU_PORTS_MIRROR}#g" {} +; \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Escape the mirror before using it as a sed replacement.

UBUNTU_PORTS_MIRROR is interpolated directly into the replacement expression. A URL containing & is treated as a back-reference, while # or backslashes can corrupt the expression, producing malformed APT sources or a failed build. Escape the replacement before invoking sed.

Suggested fix
+      mirror_escaped="$(printf '%s' "$UBUNTU_PORTS_MIRROR" | sed 's/[\\&#]/\\&/g')"; \
       find /etc/apt -type f \( -name '*.list' -o -name '*.sources' \) \
-          -exec sed -Ei "s#https?://ports\\.ubuntu\\.com/ubuntu-ports#${UBUNTU_PORTS_MIRROR}`#g`" {} +; \
+          -exec sed -Ei "s#https?://ports\\.ubuntu\\.com/ubuntu-ports#${mirror_escaped}`#g`" {} +; \
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
-exec sed -Ei "s#https?://ports\\.ubuntu\\.com/ubuntu-ports#${UBUNTU_PORTS_MIRROR}#g" {} +; \
mirror_escaped="$(printf '%s' "$UBUNTU_PORTS_MIRROR" | sed 's/[\\&#]/\\&/g')"; \
find /etc/apt -type f \( -name '*.list' -o -name '*.sources' \) \
-exec sed -Ei "s#https?://ports\\.ubuntu\\.com/ubuntu-ports#${mirror_escaped}`#g`" {} +; \
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/Dockerfile` at line 21, Escape UBUNTU_PORTS_MIRROR for sed
replacement syntax before the sed invocation in the Dockerfile command, handling
&, #, and backslashes so arbitrary mirror URLs cannot alter or break the
expression. Use the escaped value in the existing replacement while preserving
the current URL substitution behavior.

install -m 0644 /tmp/ubuntu-ports-mirror.conf /etc/apt/apt.conf.d/99-nvidia-ubuntu-ports-mirror; \
fi && \
rm -f /tmp/ubuntu-ports-mirror.conf

RUN apt-get update && apt-get install -y --no-install-recommends \
g++ gcc git cmake ccache patch patchelf curl \
g++ gcc git git-lfs cmake ccache patch patchelf curl \
ca-certificates \
python3 python3-pip python3-dev python3-venv \
doxygen graphviz \
&& git lfs install --system \
&& apt-get clean && rm -rf /var/lib/apt/lists/*

RUN apt-get update && apt-get install -y --no-install-recommends \
Expand Down
5 changes: 5 additions & 0 deletions scripts/build_cuvslam_in_docker.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ if [ "$#" -lt 1 ] || [ "$#" -gt 2 ]; then
echo " CUDA_VERSION CUDA version for base image (default: 12.6.3)"
echo " UBUNTU_VERSION Ubuntu version for base image (default: 24.04)"
echo " BASE_IMAGE Override base Docker image (e.g. NGC Jetson image)"
echo " UBUNTU_PORTS_MIRROR Optional Ubuntu Ports mirror URL for Jetson builds"
echo " EXTRA_CMAKE_ARGS Additional CMake arguments"
echo " CUVSLAM_REQUIRE_CLEAN_SOURCE Fail if tracked source files differ from HEAD"
echo " BUILD_JOBS Parallel build jobs, passed as --jobs to build_release.sh"
echo " (default: min(nproc, MemAvailable/4GB) to avoid OOM)"
exit 1
Expand All @@ -24,6 +26,8 @@ DOCKER_BUILD_ARGS=""
[ -n "${CUDA_VERSION:-}" ] && DOCKER_BUILD_ARGS="$DOCKER_BUILD_ARGS --build-arg CUDA_VERSION=$CUDA_VERSION"
[ -n "${UBUNTU_VERSION:-}" ] && DOCKER_BUILD_ARGS="$DOCKER_BUILD_ARGS --build-arg UBUNTU_VERSION=$UBUNTU_VERSION"
[ -n "${BASE_IMAGE:-}" ] && DOCKER_BUILD_ARGS="$DOCKER_BUILD_ARGS --build-arg BASE_IMAGE=$BASE_IMAGE"
[ -n "${UBUNTU_PORTS_MIRROR:-}" ] \
&& DOCKER_BUILD_ARGS="$DOCKER_BUILD_ARGS --build-arg UBUNTU_PORTS_MIRROR=$UBUNTU_PORTS_MIRROR"
docker build -f "$DOCKERFILE" . --network host $DOCKER_BUILD_ARGS --tag cuvslam:local

INSTALL_DIR="/output"
Expand Down Expand Up @@ -59,5 +63,6 @@ fi
docker run --runtime=nvidia --gpus all --rm $TTY_FLAG $DOCKER_USER $DOCKER_VOLUMES \
-e CUVSLAM_SRC_DIR=/cuvslam \
-e CUVSLAM_DST_DIR=$INSTALL_DIR/build \
-e CUVSLAM_REQUIRE_CLEAN_SOURCE="${CUVSLAM_REQUIRE_CLEAN_SOURCE:-false}" \
-e EXTRA_CMAKE_ARGS="${EXTRA_CMAKE_ARGS:-}" \
cuvslam:local /cuvslam/build_release.sh --build_type="$BUILD_TYPE" "${JOBS_ARG[@]}"
4 changes: 4 additions & 0 deletions scripts/ubuntu-ports-mirror.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Acquire::IndexTargets::deb::DEP-11::DefaultEnabled "false";
Acquire::IndexTargets::deb::DEP-11-icons-small::DefaultEnabled "false";
Acquire::IndexTargets::deb::DEP-11-icons::DefaultEnabled "false";
Acquire::IndexTargets::deb::DEP-11-icons-hidpi::DefaultEnabled "false";
47 changes: 44 additions & 3 deletions scripts/verify_pycuvslam_wheel_in_docker.sh
Original file line number Diff line number Diff line change
@@ -1,16 +1,29 @@
#!/bin/bash
set -euo pipefail

if [ "$#" -ne 1 ]; then
echo "Usage: ./scripts/verify_pycuvslam_wheel_in_docker.sh <build_output_dir>"
if [ "$#" -ne 1 ] && [ "$#" -ne 3 ]; then
echo "Usage: ./scripts/verify_pycuvslam_wheel_in_docker.sh <build_output_dir> [<expected_version> <expected_git_sha>]"
echo " Run after build_pycuvslam_in_docker.sh. Installs the repaired wheel into a"
echo " fresh environment and imports cuvslam, verifying the wheel filename is valid"
echo " (pip-installable) and the auditwheel-repaired extension loads with the"
echo " excluded CUDA libraries resolved from the system."
echo " When expected_version and expected_git_sha are provided, also verifies that"
echo " get_version() identifies that clean source revision without '-modified'."
exit 1
fi

OUTPUT_DIR=$(realpath "$1")
EXPECTED_VERSION="${2:-}"
EXPECTED_GIT_SHA="${3:-}"

if [ "$#" -eq 3 ] && [ -z "$EXPECTED_VERSION" ]; then
echo "Error: expected_version must not be empty when provenance verification is requested." >&2
exit 1
fi
if [ "$#" -eq 3 ] && [[ ! "$EXPECTED_GIT_SHA" =~ ^[0-9a-fA-F]{40,64}$ ]]; then
echo "Error: expected_git_sha must be a full hexadecimal Git object ID." >&2
exit 1
fi

shopt -s nullglob
WHEELS=("$OUTPUT_DIR"/wheel/*.whl)
Expand Down Expand Up @@ -39,11 +52,39 @@ docker run --runtime=nvidia --gpus all --rm $TTY_FLAG --network host \
--user "$(id -u):$(id -g)" --group-add video -e HOME=/tmp \
-v "$OUTPUT_DIR:/output:ro" \
-e WHEEL_NAME="$WHEEL_NAME" \
-e EXPECTED_VERSION="$EXPECTED_VERSION" \
-e EXPECTED_GIT_SHA="$EXPECTED_GIT_SHA" \
cuvslam:local bash -c '
set -euo pipefail
python3 -m venv --system-site-packages /tmp/wheel_venv
. /tmp/wheel_venv/bin/activate
pip install --no-cache-dir "/output/wheel/$WHEEL_NAME"
cd /tmp
python3 -c "import cuvslam; print(\"cuvslam wheel import OK, version:\", cuvslam.get_version())"
python3 - <<PY
import os

import cuvslam

version_info = cuvslam.get_version()
actual_version = version_info[0]
expected_version = os.environ.get("EXPECTED_VERSION", "")
expected_git_sha = os.environ.get("EXPECTED_GIT_SHA", "").lower()

if expected_version:
expected_prefix = f"{expected_version}+"
if not actual_version.startswith(expected_prefix):
raise SystemExit(
f"Expected runtime version prefix {expected_prefix}, got {actual_version}"
)

revision = actual_version[len(expected_prefix):]
if revision.endswith("-modified"):
raise SystemExit(f"Expected a clean runtime version, got {actual_version}")
if len(revision) < 7 or not expected_git_sha.startswith(revision.lower()):
raise SystemExit(
f"Runtime revision {revision} does not identify Git SHA {expected_git_sha}"
)

print("cuvslam wheel import OK, version:", version_info)
PY
'
Loading