diff --git a/.github/workflows/vivado-bitstream.yml b/.github/workflows/vivado-bitstream.yml new file mode 100644 index 0000000..f77f828 --- /dev/null +++ b/.github/workflows/vivado-bitstream.yml @@ -0,0 +1,58 @@ +# Reusable workflow: build an FPGA bitstream on a remote BuildKit builder inside +# the Zubax toolchain container. A consuming repo calls it from a tiny +# workflow_dispatch workflow and runs it locally with `act`. +# +# BuildKit variant of the remote build: no ssh/rsync — buildx talks to buildkitd +# on the build host, which reuses the mega base from its containerd store (no +# re-pull). Synthesis only; board programming stays in the project. +# +# No `actions/checkout`: these are act-only (LAN build host), so they rely on +# `act --bind`, which mounts the real project dir as the workspace — and keeps +# locally-generated, gitignored inputs present. Run any codegen locally BEFORE act. +# +# NOTE (additional path): the remote builder points at an INSECURE buildkitd +# (LAN, no TLS). If docker/setup-buildx-action rejects the plain-tcp endpoint, +# add TLS to buildkitd — or just use `make bit` (buildctl), which is the primary +# path and needs no runner at all. + +name: Containerized bitstream (buildkit) + +on: + workflow_call: + inputs: + buildkit_endpoint: + description: "buildkitd remote endpoint, e.g. tcp://host:1234 (set per-project — no default here)" + required: true + type: string + context: + description: "build context dir (must hold the Dockerfile)" + required: false + type: string + default: '.' + target: + description: "Dockerfile stage to export" + required: false + type: string + default: 'artifact' + outputs: + description: "buildx --output (e.g. type=local,dest=out or type=image,...,push=true)" + required: false + type: string + default: 'type=local,dest=out' + +jobs: + bitstream: + runs-on: ubuntu-latest + steps: + - name: Set up buildx (remote builder) + uses: docker/setup-buildx-action@v4 + with: + driver: remote + endpoint: ${{ inputs.buildkit_endpoint }} + + - name: Build bitstream + uses: docker/build-push-action@v7 + with: + context: ${{ inputs.context }} + target: ${{ inputs.target }} + outputs: ${{ inputs.outputs }} diff --git a/README.md b/README.md index be0e33d..e3b07f9 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,59 @@ docker run --rm --cap-add NET_ADMIN \ ... ``` +## Remote bitstream CI (BuildKit) + +Build FPGA bitstreams on a remote build host **inside the mega image**, driven +from your laptop. `buildkitd` on the build host reuses the mega base from its +containerd image store (no re-pull), runs Vivado in a Dockerfile `RUN`, and +exports just the bitstream. Synthesis only — board programming stays in the +project. + +**Build host, once** (LAN, insecure — fine on a trusted network). Extract the +buildkit binaries from the `moby/buildkit` image, then run `buildkitd` with a +containerd worker sharing docker's image store (namespace `moby`): + +```sh +buildkitd --addr tcp://0.0.0.0:1234 \ + --oci-worker=false --containerd-worker=true \ + --containerd-worker-addr=/run/containerd/containerd.sock \ + --containerd-worker-namespace=moby --containerd-worker-snapshotter=overlayfs +``` + +**Project setup** — copy `ci/Dockerfile.template` → `Dockerfile` (adjust the +`build.tcl` and artifact path), vendor `ci/bitstream.mk` → your `ci/bitstream.mk`, +add a `.dockerignore` that keeps the context small, and a `Makefile`: + +```makefile +# GEN = # optional local codegen; omit if none +include ci/bitstream.mk +``` + +**Build** (primary path — no runner): + +```sh +make bit # buildctl … --output type=local -> out/.bit +make bit-push PUSH_REF=/proj/bitstream:$(git rev-parse --short HEAD) +``` + +Set `BUILDKIT_ADDR` (e.g. `tcp://host:1234`) in the project Makefile or the +environment (`BUILDKIT_HOST`); override `TARGET`, `OUT` as needed. This repo ships +no infrastructure defaults — projects pin their own. + +**Or via act** — `.github/workflows/vivado-bitstream.yml` is a reusable workflow +(`docker/setup-buildx-action` remote driver + `docker/build-push-action`) doing +the same build. A project calls it from a tiny `workflow_dispatch`: + +```yaml +jobs: + bitstream: + uses: Zubax/fpga-toolchain-docker/.github/workflows/vivado-bitstream.yml@main + with: { buildkit_endpoint: tcp://:1234, target: artifact, outputs: type=local,dest=out } +``` + +Run codegen locally first, then +`act workflow_dispatch -s GITHUB_TOKEN="$(gh auth token)"`. + ## License The container glue in this repository is MIT licensed. diff --git a/ci/Dockerfile.template b/ci/Dockerfile.template new file mode 100644 index 0000000..4553981 --- /dev/null +++ b/ci/Dockerfile.template @@ -0,0 +1,19 @@ +# syntax=docker/dockerfile:1 +# Template for remote BuildKit bitstream builds. Copy to your project as +# `Dockerfile` and adjust the RUN (your build.tcl) and the artifact COPY path. +# Build with `make bit` (see the shared ci/bitstream.mk). buildkitd on the build +# host reuses the mega base from its containerd store — no re-pull. + +ARG IMAGE=containers.zubax.com/zubax-fpga-toolchain-mega:2026-07-02 + +FROM ${IMAGE} AS build +# COPY is root-owned and WORKDIR creates root-owned dirs, but the mega base runs +# as user `ubuntu` — build as root so /work stays writable. +USER root +WORKDIR /work +COPY . . +RUN vivado -mode batch -source vivado/build.tcl -nolog -nojournal + +# Thin artifact stage: only the bitstream, so --output/registry stays tiny. +FROM scratch AS artifact +COPY --from=build /work/build/YOUR_BITSTREAM.bit / diff --git a/ci/bitstream.mk b/ci/bitstream.mk new file mode 100644 index 0000000..30d5a42 --- /dev/null +++ b/ci/bitstream.mk @@ -0,0 +1,50 @@ +# Shared make logic for remote containerized FPGA bitstream builds via BuildKit. +# Vendor this file into a project (ci/bitstream.mk) and `include` it from the +# project Makefile — keep the copy in sync with Zubax/fpga-toolchain-docker. +# +# The project Makefile sets these and includes this file: +# BUILDKIT_ADDR buildkitd endpoint, e.g. tcp://HOST:1234 +# (or leave empty and set BUILDKIT_HOST in the environment) +# GEN optional codegen command to run before the build (blank = none) +# CONTEXT build context dir (default .) +# TARGET Dockerfile stage (default artifact) +# OUT local output dir (default out) +# PUSH_REF registry ref for bit-push, e.g. //bitstream: +# +# Targets: +# make bit build + export the bitstream to $(OUT)/ +# make bit-push build + push a thin bitstream image to $(PUSH_REF) + +BUILDKIT_ADDR ?= +CONTEXT ?= . +TARGET ?= artifact +OUT ?= out + +# Pass --addr only when BUILDKIT_ADDR is set; otherwise buildctl uses $BUILDKIT_HOST. +_addr := $(if $(BUILDKIT_ADDR),--addr $(BUILDKIT_ADDR),) + +.PHONY: bit bit-push _gen _need-endpoint + +_need-endpoint: + @test -n "$(BUILDKIT_ADDR)$(BUILDKIT_HOST)" || \ + { echo "error: set BUILDKIT_ADDR (e.g. tcp://host:1234) or BUILDKIT_HOST"; exit 1; } + +_gen: + @$(if $(GEN),$(GEN),true) + +bit: _need-endpoint _gen + buildctl $(_addr) build \ + --frontend dockerfile.v0 \ + --local context=$(CONTEXT) --local dockerfile=$(CONTEXT) \ + --opt target=$(TARGET) --opt image-resolve-mode=local \ + --output type=local,dest=$(OUT) + @echo "==> bitstream exported to $(OUT)/" + +bit-push: _need-endpoint _gen + @test -n "$(PUSH_REF)" || { echo "error: set PUSH_REF=//bitstream:"; exit 1; } + buildctl $(_addr) build \ + --frontend dockerfile.v0 \ + --local context=$(CONTEXT) --local dockerfile=$(CONTEXT) \ + --opt target=$(TARGET) --opt image-resolve-mode=local \ + --output type=image,name=$(PUSH_REF),push=true + @echo "==> pushed $(PUSH_REF)"