# Roc OCI Images Implementation Plan **Goal:** Build and publish three OCI container images (base, dev, ci) providing the Roc compiler, LSP, and development environment, stored in a single Containerfile with multi-stage targets. **Architecture:** Single `Containerfile` with three build targets (`base`, `dev`, `ci`). A shell script downloads the official Roc release tarball from GitHub. A Makefile provides local build/push commands. A Gitea Actions workflow builds and publishes on version tags. **Tech Stack:** Docker/podman multi-stage builds, bash, Gitea Actions, GitHub Releases API --- ### Task 1: download-roc.sh script **Files:** - Create: `scripts/download-roc.sh` - [ ] **Step 1: Write the download script** This script accepts `ROC_VERSION` and `TARGETARCH` and downloads/extracts the corresponding Roc release tarball into `/opt/roc/`. ```bash #!/usr/bin/env bash set -euo pipefail ROC_VERSION="${1:?Usage: $0 [target-arch]}" TARGETARCH="${2:-amd64}" # Map Docker buildx arch names to Roc tarball arch names case "$TARGETARCH" in amd64) ROC_ARCH="x86_64" ;; arm64) ROC_ARCH="aarch64" ;; *) echo "Unsupported architecture: $TARGETARCH" >&2; exit 1 ;; esac TARBALL="roc-linux_${ROC_ARCH}-${ROC_VERSION}.tar.gz" URL="https://github.com/roc-lang/roc/releases/download/${ROC_VERSION}/${TARBALL}" echo "Downloading Roc ${ROC_VERSION} for ${ROC_ARCH}..." curl -fsSL "$URL" -o /tmp/roc.tar.gz echo "Extracting..." tar -xzf /tmp/roc.tar.gz -C /tmp/ # The tarball contains a single directory with an unpredictable nightly name # e.g. roc_nightly-linux_x86_64-2025-09-09-d73ea109/ EXTRACTED_DIR=$(find /tmp -maxdepth 1 -type d -name "roc_*" | head -1) if [ -z "$EXTRACTED_DIR" ]; then echo "ERROR: Could not find extracted Roc directory in /tmp" >&2 exit 1 fi echo "Installing to /opt/roc..." mkdir -p /opt/roc/bin /opt/roc/lib install -m 0755 "$EXTRACTED_DIR/roc" /opt/roc/bin/roc # LSP may or may not be present in older releases; install if found if [ -f "$EXTRACTED_DIR/roc_language_server" ]; then install -m 0755 "$EXTRACTED_DIR/roc_language_server" /opt/roc/bin/roc_language_server fi # WASI libc is needed for some platforms if [ -d "$EXTRACTED_DIR/lib" ]; then cp -r "$EXTRACTED_DIR/lib/"* /opt/roc/lib/ fi # License if [ -f "$EXTRACTED_DIR/LICENSE" ]; then cp "$EXTRACTED_DIR/LICENSE" /opt/roc/LICENSE fi # Cleanup rm -rf /tmp/roc.tar.gz "$EXTRACTED_DIR" echo "Roc ${ROC_VERSION} installed successfully." /opt/roc/bin/roc --version ``` - [ ] **Step 2: Make it executable and commit** ```bash chmod +x scripts/download-roc.sh git add scripts/download-roc.sh git commit -m "feat: add Roc release download/extract script" ``` --- ### Task 2: Containerfile — base target **Files:** - Create: `Containerfile` - [ ] **Step 1: Write the base stage** ```dockerfile # Roc compiler base image # Provides just the Roc compiler binary and WASI runtime library. ARG ROC_VERSION=0.0.0-alpha2 FROM debian:bookworm-slim AS curl-stage RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl && rm -rf /var/lib/apt/lists/* ARG ROC_VERSION ARG TARGETARCH COPY scripts/download-roc.sh /tmp/download-roc.sh RUN /tmp/download-roc.sh "$ROC_VERSION" "$TARGETARCH" FROM debian:bookworm-slim AS base ARG ROC_VERSION ARG TARGETARCH LABEL org.opencontainers.image.title="roc-tools-base" LABEL org.opencontainers.image.description="Roc compiler base image" LABEL org.opencontainers.image.version="${ROC_VERSION}" LABEL org.opencontainers.image.source="https://git.roo.lol/jbrechtel/roc-lang-tools" RUN groupadd --gid 1000 roc && useradd --uid 1000 --gid roc --shell /bin/sh --create-home roc COPY --from=curl-stage /opt/roc /opt/roc ENV PATH="/opt/roc/bin:${PATH}" USER roc WORKDIR /home/roc ENTRYPOINT ["roc"] CMD ["--help"] ``` - [ ] **Step 2: Commit** ```bash git add Containerfile git commit -m "feat: add Containerfile with base build target" ``` --- ### Task 3: Containerfile — dev target - [ ] **Step 1: Add dev stage to Containerfile** Append to Containerfile: ```dockerfile FROM base AS dev LABEL org.opencontainers.image.title="roc-tools-dev" LABEL org.opencontainers.image.description="Roc development environment with LSP and dev tools" LABEL org.opencontainers.image.version="${ROC_VERSION}" USER root RUN apt-get update && apt-get install -y --no-install-recommends \ git \ curl \ bash \ ca-certificates \ && rm -rf /var/lib/apt/lists/* USER roc ENTRYPOINT ["bash"] ``` - [ ] **Step 2: Commit** ```bash git add Containerfile git commit -m "feat: add dev build target to Containerfile" ``` --- ### Task 4: Containerfile — ci target - [ ] **Step 1: Add ci stage to Containerfile** Append to Containerfile: ```dockerfile FROM base AS ci LABEL org.opencontainers.image.title="roc-tools-ci" LABEL org.opencontainers.image.description="Roc CI/CD runner image" LABEL org.opencontainers.image.version="${ROC_VERSION}" USER root RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates \ && rm -rf /var/lib/apt/lists/* USER roc ENTRYPOINT ["roc"] CMD ["--help"] ``` - [ ] **Step 2: Commit** ```bash git add Containerfile git commit -m "feat: add ci build target to Containerfile" ``` --- ### Task 5: Makefile **Files:** - Create: `Makefile` - [ ] **Step 1: Write the Makefile** ```makefile ROC_VERSION ?= 0.0.0-alpha2 REGISTRY ?= git.roo.lol/jbrechtel .PHONY: build-base build-dev build-ci build-all push-all build-base: docker build \ --target base \ --build-arg ROC_VERSION=$(ROC_VERSION) \ -t $(REGISTRY)/roc-tools-base:$(ROC_VERSION) \ -t $(REGISTRY)/roc-tools-base:latest \ . build-dev: docker build \ --target dev \ --build-arg ROC_VERSION=$(ROC_VERSION) \ -t $(REGISTRY)/roc-tools-dev:$(ROC_VERSION) \ -t $(REGISTRY)/roc-tools-dev:latest \ . build-ci: docker build \ --target ci \ --build-arg ROC_VERSION=$(ROC_VERSION) \ -t $(REGISTRY)/roc-tools-ci:$(ROC_VERSION) \ -t $(REGISTRY)/roc-tools-ci:latest \ . build-all: build-base build-dev build-ci push-base: docker push $(REGISTRY)/roc-tools-base:$(ROC_VERSION) docker push $(REGISTRY)/roc-tools-base:latest push-dev: docker push $(REGISTRY)/roc-tools-dev:$(ROC_VERSION) docker push $(REGISTRY)/roc-tools-dev:latest push-ci: docker push $(REGISTRY)/roc-tools-ci:$(ROC_VERSION) docker push $(REGISTRY)/roc-tools-ci:latest push-all: build-all push-base push-dev push-ci ``` - [ ] **Step 2: Commit** ```bash git add Makefile git commit -m "feat: add Makefile with build and push targets" ``` --- ### Task 6: Gitea Actions workflow **Files:** - Create: `.gitea/workflows/build.yaml` - [ ] **Step 1: Write the workflow** ```yaml name: Build and Publish OCI Images on: push: tags: - 'v*' env: REGISTRY: git.roo.lol REGISTRY_USER: ${{ github.actor }} REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_TOKEN }} IMAGE_NAMESPACE: ${{ github.repository_owner }} jobs: build-and-push: runs-on: ubuntu-latest permissions: packages: write contents: read steps: - name: Checkout repository uses: actions/checkout@v4 - name: Extract Roc version from tag id: version run: | # Strip leading 'v' from tag name: v0.0.0-alpha2 -> 0.0.0-alpha2 ROC_VERSION="${GITHUB_REF_NAME#v}" echo "roc_version=${ROC_VERSION}" >> "$GITHUB_OUTPUT" echo "ROC_VERSION=${ROC_VERSION}" >> "$GITHUB_ENV" - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Log in to registry run: | echo "$REGISTRY_PASSWORD" | docker login $REGISTRY -u $REGISTRY_USER --password-stdin - name: Build and push base image uses: docker/build-push-action@v6 with: context: . target: base build-args: ROC_VERSION=${{ steps.version.outputs.roc_version }} tags: | ${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/roc-tools-base:${{ steps.version.outputs.roc_version }} ${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/roc-tools-base:latest push: true cache-from: type=gha cache-to: type=gha,mode=max - name: Build and push dev image uses: docker/build-push-action@v6 with: context: . target: dev build-args: ROC_VERSION=${{ steps.version.outputs.roc_version }} tags: | ${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/roc-tools-dev:${{ steps.version.outputs.roc_version }} ${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/roc-tools-dev:latest push: true cache-from: type=gha cache-to: type=gha,mode=max - name: Build and push ci image uses: docker/build-push-action@v6 with: context: . target: ci build-args: ROC_VERSION=${{ steps.version.outputs.roc_version }} tags: | ${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/roc-tools-ci:${{ steps.version.outputs.roc_version }} ${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/roc-tools-ci:latest push: true cache-from: type=gha cache-to: type=gha,mode=max ``` - [ ] **Step 2: Commit** ```bash mkdir -p .gitea/workflows git add .gitea/workflows/build.yaml git commit -m "feat: add Gitea Actions workflow for building and publishing OCI images" ``` --- ### Task 7: Add .gitignore **Files:** - Create: `.gitignore` - [ ] **Step 1: Write .gitignore** ``` *.tar.gz /tmp/ ``` - [ ] **Step 2: Commit** ```bash git add .gitignore git commit -m "chore: add .gitignore" ```