Skip to content
Merged
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
25 changes: 25 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Keep Docker context small and free of secrets / build junk.
.git
.github
target
**/target
fuzz/target
fuzz/corpus
fuzz/artifacts
.cursor
.devcontainer
*.md
!README.md
!CHANGELOG.md
!COMPATIBILITY.md
!TESTING.md
!LICENSE-*
AGENTS.md
CLAUDE.md
REVIEW.md
cubic.yaml
qodana.yaml
scripts
.env
.env.*
**/.DS_Store
171 changes: 171 additions & 0 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
# Docker build + dual push (Docker Hub + GitHub Container Registry).
# Third-party Actions pinned to immutable commit SHAs.
# Local Buildx only (no Docker Build Cloud).
#
# PR: verify build only (contents: read; no registry secrets / packages: write).
# Upstream main: SHA + latest only (never overwrite :X.Y.Z).
# Upstream v* tag matching Cargo.toml: SHA + X.Y.Z + latest.
name: Docker

on:
push:
# REVIEW.md: workflows that change CI must keep Main + main targets.
branches: [main, Main]
tags: ["v*"]
pull_request:
branches: [main, Main]
types: [opened, synchronize, reopened]

permissions:
contents: read

jobs:
docker-verify:
name: docker verify
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
concurrency:
group: docker-verify-${{ github.repository }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
Comment thread
rmems marked this conversation as resolved.
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

# No Docker Hub / GHCR login on PRs — never expose DOCKER_PAT to PR code.
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0

- name: Set image tags
id: image
run: |
set -euo pipefail
SHA="${{ github.sha }}"
echo "runtime_tag=nir-rs:pr-${SHA}" >> "$GITHUB_OUTPUT"

# Full Dockerfile build already runs cargo test in the builder stage.
- name: Build runtime image (load locally)
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: .
push: false
load: true
tags: ${{ steps.image.outputs.runtime_tag }}

- name: Smoke runtime image
run: |
set -euo pipefail
TAG="${{ steps.image.outputs.runtime_tag }}"
docker run --rm "${TAG}" rustc --version
docker run --rm "${TAG}" sh -c 'test -x /usr/local/bin/load_inspect_lif'
docker run --rm "${TAG}" sh -c 'pkg-config --exists hdf5'
# Default path uses fixtures under CARGO_MANIFEST_DIR=/src
docker run --rm "${TAG}" load_inspect_lif
# Runtime user can write target/ for agent cargo builds
docker run --rm "${TAG}" sh -c 'touch /src/.write-check && rm /src/.write-check'

docker-publish:
name: docker publish
runs-on: ubuntu-latest
if: >
github.repository == 'Limen-Neural/nir-rs' &&
github.event_name == 'push' &&
(github.ref == 'refs/heads/main' || github.ref == 'refs/heads/Main' || startsWith(github.ref, 'refs/tags/v'))
Comment thread
rmems marked this conversation as resolved.
# One group for all :latest writers (main/Main + v* tags). Serializes pushes so
# branch and tag jobs cannot race on the mutable :latest tag.
# cancel-in-progress: false — never abort an in-flight publish (incl. immutable :X.Y.Z).
# queue: max — keep up to 100 pending runs (default is single: a later main push
# would replace a pending v* tag and skip the immutable image).
concurrency:
group: docker-publish-${{ github.repository }}
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
cancel-in-progress: false
queue: max
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Log in to Docker Hub
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
username: ${{ vars.DOCKER_USER }}
password: ${{ secrets.DOCKER_PAT }}

- name: Log in to GHCR
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0

- name: Set image tags
id: image
env:
DOCKER_USER: ${{ vars.DOCKER_USER }}
REPO_OWNER: ${{ github.repository_owner }}
REF: ${{ github.ref }}
SHA: ${{ github.sha }}
run: |
set -euo pipefail
VER=$(grep -m1 '^version' Cargo.toml | sed -E 's/.*"([^"]+)".*/\1/')
if [ -z "${VER}" ] || ! printf '%s' "${VER}" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "::error::Failed to extract semver X.Y.Z from Cargo.toml (got: ${VER:-empty})"
exit 1
fi

OWNER=$(printf '%s' "${REPO_OWNER}" | tr '[:upper:]' '[:lower:]')
HUB="${DOCKER_USER}/nir-rs"
GHCR="ghcr.io/${OWNER}/nir-rs"

if [ "${REF}" = "refs/heads/main" ] || [ "${REF}" = "refs/heads/Main" ]; then
TAGS="${HUB}:${SHA},${HUB}:latest,${GHCR}:${SHA},${GHCR}:latest"
elif [[ "${REF}" == refs/tags/v* ]]; then
TAG_NAME="${REF#refs/tags/}"
if [ "${TAG_NAME}" != "v${VER}" ]; then
echo "::error::Git tag ${TAG_NAME} does not match Cargo.toml version v${VER}"
exit 1
fi
TAGS="${HUB}:${SHA},${HUB}:${VER},${HUB}:latest,${GHCR}:${SHA},${GHCR}:${VER},${GHCR}:latest"
else
echo "::error::Unexpected ref ${REF}"
exit 1
fi

echo "tags=${TAGS}" >> "$GITHUB_OUTPUT"

- name: Build and push
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: .
push: true
tags: ${{ steps.image.outputs.tags }}

# First package creation is private by default; public pulls need public visibility.
- name: Ensure GHCR package is public
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
OWNER=$(printf '%s' "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]')
# Org package API (works for org-owned containers).
if gh api --method PUT -H "Accept: application/vnd.github+json" "/orgs/${OWNER}/packages/container/nir-rs/visibility" -f visibility=public; then
echo "GHCR package set public (org API)"
exit 0
fi
# User-owned fallback.
if gh api --method PUT -H "Accept: application/vnd.github+json" "/user/packages/container/nir-rs/visibility" -f visibility=public; then
echo "GHCR package set public (user API)"
exit 0
fi
echo "::warning::Could not set GHCR visibility to public automatically; set ghcr.io/${OWNER}/nir-rs public in package settings."
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ Prefer a prebuilt image when the host supports it (Rust 1.97 + `libhdf5-dev`):

| Path | Consumer |
|------|----------|
| [`Dockerfile`](Dockerfile) → `ghcr.io/limen-neural/nir-rs` + Docker Hub | Published toolchain image (GHCR + Hub) |
| [`.github/workflows/docker.yml`](.github/workflows/docker.yml) | PR verify; `main`/tag dual-publish |
| [`.cursor/environment.json`](.cursor/environment.json) → [`.cursor/Dockerfile`](.cursor/Dockerfile) | Cursor cloud agents |
| [`.devcontainer/devcontainer.json`](.devcontainer/devcontainer.json) | VS Code / Cursor Desktop |
| [`scripts/agent-bootstrap.sh`](scripts/agent-bootstrap.sh) | cubic / Claude / other bare sandboxes |
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,13 @@ First **crates.io** release of the 0.4 line (package name `nir-rs`).
`MANIFEST.toml`, coverage checklist (#29).
- Property tests (`proptest`) for tensor/graph/wire invariants and HDF5
write→read; optional `cargo-fuzz` harnesses under `fuzz/` (#30).
- Docker image + dual publish to **GHCR** (`ghcr.io/limen-neural/nir-rs`) and
**Docker Hub** (`$DOCKER_USER/nir-rs`); PR verify / main+tag push (#26).

### Documentation

- README crates.io / docs.rs badges and dependency snippets (#26).
- README Docker (GHCR + Hub) install/run notes (#26).
- [TESTING.md](TESTING.md) for property tests and local fuzz invocation.

## [0.4.0] - 2026-08-06
Expand Down
65 changes: 65 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# syntax=docker/dockerfile:1
# nir-rs — published image for GHCR + Docker Hub.
#
# Builder: cargo test --all-features + release example with system libhdf5.
# Runtime: Rust pin + libhdf5 + source (no target/) + example binary.
# WORKDIR stays /src so env!(CARGO_MANIFEST_DIR) from the builder still finds
# tests/fixtures. Non-root user owns /src so cargo can recreate target/.
# No Python (AGENTS.md).

ARG RUST_IMAGE=rust:1.97-bookworm

FROM ${RUST_IMAGE} AS builder

RUN apt-get update \
&& apt-get install -y --no-install-recommends \
pkg-config \
libhdf5-dev \
build-essential \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /src
COPY . .

RUN rustup component add clippy rustfmt \
&& cargo test --all-features \
&& cargo build --release --example load_inspect_lif --features hdf5 \
&& cp target/release/examples/load_inspect_lif /tmp/load_inspect_lif \
&& rm -rf target

FROM ${RUST_IMAGE}

RUN apt-get update \
&& apt-get install -y --no-install-recommends \
git \
pkg-config \
libhdf5-dev \
build-essential \
ca-certificates \
curl \
&& rm -rf /var/lib/apt/lists/* \
&& rustup component add clippy rustfmt \
&& useradd --create-home --uid 10001 --shell /bin/bash nir

# Match builder path for CARGO_MANIFEST_DIR baked into the example binary.
# Own /src itself so the runtime user can recreate target/ for cargo build/test.
RUN mkdir -p /src && chown nir:nir /src
WORKDIR /src
COPY --from=builder --chown=nir:nir /src /src
Comment thread
rmems marked this conversation as resolved.
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
COPY --from=builder /tmp/load_inspect_lif /usr/local/bin/load_inspect_lif
RUN chmod 755 /usr/local/bin/load_inspect_lif

USER nir
ENV CARGO_HOME=/home/nir/.cargo \
CARGO_TERM_COLOR=always \
PATH=/home/nir/.cargo/bin:/usr/local/cargo/bin:$PATH
# Pre-warm crate index for agent use; tolerate offline builders.
RUN cargo fetch || true

LABEL org.opencontainers.image.title="nir-rs" \
org.opencontainers.image.description="Pure-Rust NIR graph + HDF5 I/O toolchain image" \
org.opencontainers.image.source="https://github.com/Limen-Neural/nir-rs" \
org.opencontainers.image.licenses="MIT OR Apache-2.0"

CMD ["rustc", "--version"]
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
**Pure-Rust implementation of the Neuromorphic Intermediate Representation (NIR)**

[![CI](https://github.com/Limen-Neural/nir-rs/actions/workflows/ci.yml/badge.svg)](https://github.com/Limen-Neural/nir-rs/actions)
[![Docker](https://github.com/Limen-Neural/nir-rs/actions/workflows/docker.yml/badge.svg)](https://github.com/Limen-Neural/nir-rs/actions/workflows/docker.yml)
[![crates.io](https://img.shields.io/crates/v/nir-rs.svg)](https://crates.io/crates/nir-rs)
[![docs.rs](https://docs.rs/nir-rs/badge.svg)](https://docs.rs/nir-rs)
[![License: MIT OR Apache-2.0](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue.svg)](#license)
Expand Down Expand Up @@ -62,6 +63,36 @@ Tracking: [GitHub milestones](https://github.com/Limen-Neural/nir-rs/milestones)

Compatibility claims are **fixture-backed** only; see also `tests/fixtures/`.

## Docker (GHCR + Docker Hub)

Published images ship a **Rust 1.97 + libhdf5** toolchain with the crate tree and
the `load_inspect_lif` example binary (not an SNN simulator). CI verifies on
PRs and pushes on `main` / version tags — see [`.github/workflows/docker.yml`](.github/workflows/docker.yml).

```bash
# Preferred: GitHub Container Registry (stable org path)
docker pull ghcr.io/limen-neural/nir-rs:latest
Comment thread
rmems marked this conversation as resolved.
# Version tag appears after a matching git tag push (e.g. v0.4.1 → :0.4.1)
docker pull ghcr.io/limen-neural/nir-rs:0.4.1

# Docker Hub: published as <vars.DOCKER_USER>/nir-rs (same tags as GHCR).
# Use the org/user from GitHub Actions repo variables, not a shell placeholder.

docker run --rm ghcr.io/limen-neural/nir-rs:latest rustc --version
# Default input: tests/fixtures/lif_norse.nir (writes a temp copy)
docker run --rm ghcr.io/limen-neural/nir-rs:latest load_inspect_lif
```

First GHCR publish creates a **private** package by default. The publish job
tries to set visibility to **public**; if that fails, an org admin must set
`ghcr.io/limen-neural/nir-rs` public under GitHub Packages.

Local build:

```bash
docker build -t nir-rs:local .
```

## Toolchain & MSRV

**CI and local development pin Rust 1.97.1** (`rust-toolchain.toml`
Expand Down
Loading