diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..d17e0f4 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,16 @@ +{ + // RustKVM devkit: frontend + aarch64 device build, no host toolchains needed. + // The image is built locally on first open (scripts/devkit.sh image); set + // RUSTKVM_DEVKIT_IMAGE to use a registry copy instead. + "name": "RustKVM devkit", + "image": "${localEnv:RUSTKVM_DEVKIT_IMAGE:ghcr.io/rust-kvm/rustkvm-devkit:nightly-2026-06-14}", + "initializeCommand": "scripts/devkit.sh image --if-missing", + "runArgs": ["--network=host"], + "remoteUser": "ubuntu", + "updateRemoteUserUID": true, + "customizations": { + "vscode": { + "extensions": ["rust-lang.rust-analyzer", "tamasfe.even-better-toml"] + } + } +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..bd67da8 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# SDK patches are applied byte-for-byte; keep their whitespace and line endings. +os/**/*.patch -text -whitespace diff --git a/.github/workflows/buildkit.yml b/.github/workflows/buildkit.yml index f0aac0c..80b6ae6 100644 --- a/.github/workflows/buildkit.yml +++ b/.github/workflows/buildkit.yml @@ -63,7 +63,7 @@ jobs: run: | host="$RUNNER_TEMP/sdk/buildroot/output/rockchip_rk3588/host" test -x "$host/bin/aarch64-buildroot-linux-gnu-gcc" - cp -a "$host" ci/buildkit/rk3588-buildkit + cp -a "$host" "$RUNNER_TEMP/rk3588-buildkit" - name: Upload system images uses: actions/upload-artifact@v4 with: @@ -81,6 +81,7 @@ jobs: - uses: docker/build-push-action@v7 with: context: ci/buildkit + build-contexts: rk3588-buildkit=${{ runner.temp }}/rk3588-buildkit push: true tags: | ${{ env.IMAGE }}:${{ inputs.sdk_tag }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 851ecbb..ed77f4e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -52,7 +52,8 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} env: - RUSTUP_TOOLCHAIN: nightly + # Same pin as ci/devkit/Dockerfile. + RUSTUP_TOOLCHAIN: nightly-2026-06-14 CC_aarch64_unknown_linux_gnu: /opt/rk3588-buildkit/bin/aarch64-buildroot-linux-gnu-gcc steps: - uses: actions/checkout@v7 @@ -65,8 +66,9 @@ jobs: echo "::error::tag ${GITHUB_REF_NAME} does not match workspace version ${version}" exit 1 } - - uses: dtolnay/rust-toolchain@nightly + - uses: dtolnay/rust-toolchain@master with: + toolchain: nightly-2026-06-14 components: rust-src targets: aarch64-unknown-linux-gnu - uses: actions/download-artifact@v8 diff --git a/.gitignore b/.gitignore index ea8c4bf..0a25f36 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +/out diff --git a/AGENTS.md b/AGENTS.md index 7399ce3..8620e3f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -225,11 +225,30 @@ trunk serve # local dev server on :9000 (Trunk.toml) ## CI - **`.github/workflows/ci.yml`** — workspace `rustfmt` (nightly) + `cargo deny` + host `clippy --workspace --release --all-targets -D warnings` + aarch64 `clippy --target aarch64-unknown-linux-gnu` (Ubuntu cross gcc, no link) + `cargo test --workspace` (GStreamer dev libs, stub `rkvm-web/dist`) + `actionlint` over every workflow, gated by a `CI Success` summary check. -- **`.github/workflows/buildkit.yml`** — manual: downloads the Buildroot SDK tar (Google Drive file ID), runs `./build.sh rk3588.mk`, publishes `/opt/rk3588-buildkit` as `ghcr.io//rk3588-buildkit:{sdk_tag,latest}` and uploads the system images as an artifact. -- **`.github/workflows/release.yml`** — tag `vX.Y.Z` (must equal the workspace version): Trunk frontend → `cargo +nightly build -Z build-std` in the buildkit image → GitHub Release with `rustkvm_app-aarch64-unknown-linux-gnu` + `.sha256` (the assets OTA installs). +- **`.github/workflows/buildkit.yml`** — manual: downloads the Buildroot SDK tar (Google Drive file ID), runs `./build.sh rk3588.mk`, publishes `/opt/rk3588-buildkit` as `ghcr.io//rk3588-buildkit:{sdk_tag,latest}` (via the `rk3588-buildkit` named build context of `ci/buildkit/Dockerfile`) and uploads the system images as an artifact. +- **`.github/workflows/release.yml`** — tag `vX.Y.Z` (must equal the workspace version): Trunk frontend → `cargo +nightly-2026-06-14 build -Z build-std` in the buildkit image → GitHub Release with `rustkvm_app-aarch64-unknown-linux-gnu` + `.sha256` (the assets OTA installs). - **`.github/workflows/device-health.yml`** — every 6h and on demand: runs `app/tests/device_live.rs` against a live device (`RKVM_DEVICE_URL` variable, `RKVM_DEVICE_TOKEN` secret; optional `RKVM_PROBE_RUNNER` self-hosted runner label); a no-op when no device is configured. The same test runs locally with those env vars and is skipped when `RKVM_DEVICE_URL` is unset. - **`.github/workflows/web.yml`** — `rkvm-web` only (triggers on `crates/rkvm-web/**`, `crates/rkvm-proto/**`): `rustfmt` (nightly) → `clippy` (stable, `--target wasm32-unknown-unknown -D warnings`) → `trunk build --locked` → `cargo deny`, gated by a `Web CI Success` summary check. +## Reproducible builds (devkit and firmware) + +Only Docker is needed on a host; full guide in [`docs/building.md`](docs/building.md), shipping flow in [`docs/firmware-release.md`](docs/firmware-release.md). + +``` +ci/buildkit/Dockerfile # RK3588 cross gcc + sysroot image (Buildroot host dir as named context, docs/target binaries pruned) +ci/devkit/Dockerfile # buildkit + one pinned nightly (aarch64 + wasm32) + trunk; RUSTUP_TOOLCHAIN overrides `stage2` +ci/firmware/Dockerfile # Ubuntu 22.04 SDK host + repo; runs --privileged (loop devices) +.devcontainer/ # VS Code devcontainer on the devkit image +scripts/devkit.sh # buildkit | image | shell | run CMD | firmware-image | firmware STEPS +scripts/build-app.sh # trunk + device binary → out/app/ (binary, .sha256, BUILD_INFO) +scripts/build-firmware.sh # sync | patch | build | collect → out/firmware/ (sd + eflasher images) +os/rk3588/manifest.xml # FriendlyElec SDK projects pinned to exact commits +os/rk3588/patches// # every local SDK change, applied with git apply after a reset +os/rk3588/rootfs-overlay/ # S99rustkvm, dhcpcd/network config, dropbear devmode wrapper +``` + +Toolchain pins live in `ci/devkit/Dockerfile` (and the nightly in `release.yml`); SDK changes go into `os/rk3588/patches`, never only into a local SDK checkout. + ## Deployment ```bash diff --git a/Cargo.lock b/Cargo.lock index 0f45bd7..192dc21 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2597,6 +2597,18 @@ dependencies = [ "pin-utils", ] +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags 2.13.2", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nom" version = "7.1.3" @@ -3597,9 +3609,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.45" +version = "0.23.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" +checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf" dependencies = [ "aws-lc-rs", "log", @@ -5357,7 +5369,7 @@ dependencies = [ "ipnet", "lazy_static", "log", - "nix", + "nix 0.26.4", "portable-atomic", "rand 0.9.5", "thiserror 1.0.69", @@ -5837,13 +5849,14 @@ dependencies = [ [[package]] name = "zip" -version = "8.6.0" +version = "9.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b" +checksum = "a954114b47a54df49f83d2722814e86b23d4bb1b54a1b90fb7d92b41619ddc22" dependencies = [ "crc32fast", "indexmap", "memchr", + "nix 0.31.3", "typed-path", ] diff --git a/Cargo.toml b/Cargo.toml index 3ce411a..801541b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,7 +60,7 @@ tracing = "0.1.44" tracing-subscriber = { version = "0.3.23", features = ["json", "env-filter"] } uuid = { version = "1.23.3", default-features = false, features = ["v4"] } webrtc = "0.17.1" -zip = { version = "8.0", default-features = false } +zip = { version = "9.0", default-features = false } [workspace.lints.rust] unsafe_op_in_unsafe_fn = "warn" diff --git a/README.md b/README.md index b7edeaf..86fe114 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,9 @@ rustkvm/ │ ├── rkvm-proto/ # protocol types shared with the frontend (HID-RPC, JSON-RPC, keymaps) │ └── rkvm-web/ # Leptos 0.8 CSR frontend, built with Trunk (not a workspace member) ├── assets/images/ # built-in virtual-media images -├── ci/buildkit/ # Docker image for the RK3588 cross-build kit +├── ci/ # Docker images: cross toolchain, devkit, firmware builder +├── os/rk3588/ # firmware layer: pinned SDK manifest, patches, rootfs overlay +├── scripts/ # devkit.sh, build-app.sh, build-firmware.sh ├── rust/ # rust-lang/rust submodule for the self-built stage2 toolchain └── dev_deploy.sh # build and deploy to a device over SSH ``` @@ -223,6 +225,17 @@ RustKVM has two build products: the **WASM frontend** (any host, stable Rust) an **device binary** (cross-compiled for `aarch64-unknown-linux-gnu`). The frontend is embedded into the binary at compile time, so build it first. +**Quickest path, any Linux host with Docker:** + +```bash +scripts/devkit.sh run scripts/build-app.sh # → out/app/rustkvm_app-aarch64-unknown-linux-gnu +scripts/devkit.sh firmware # → out/firmware/ SD and eMMC images +``` + +Pinned versions, caches, proxies and the firmware build are covered in +[`docs/building.md`](docs/building.md). The manual setup below is what the containers +automate. + ### 1. Frontend (any host) ```bash diff --git a/ci/buildkit/Dockerfile b/ci/buildkit/Dockerfile index c5e69fc..f3893d2 100644 --- a/ci/buildkit/Dockerfile +++ b/ci/buildkit/Dockerfile @@ -1,3 +1,25 @@ +# RK3588 cross toolchain + sysroot (the Buildroot `host/` directory) at the +# path .cargo/config.toml expects. The directory comes in as the named build +# context `rk3588-buildkit`: +# +# docker buildx build --build-context rk3588-buildkit=/opt/rk3588-buildkit \ +# -t ghcr.io/rust-kvm/rk3588-buildkit:20250527 ci/buildkit +# +# scripts/devkit.sh buildkit does exactly this. +FROM ubuntu:24.04 AS prune + +COPY --from=rk3588-buildkit . /opt/rk3588-buildkit + +# Keep only what compiling and linking against the sysroot needs: drop +# documentation, translations, the target's own executables and its password +# database (the image is public). +RUN cd /opt/rk3588-buildkit \ + && rm -rf doc man share/doc share/man share/info share/locale share/gtk-doc \ + && cd aarch64-buildroot-linux-gnu/sysroot \ + && rm -rf usr/bin usr/sbin usr/libexec bin sbin \ + usr/share/doc usr/share/man usr/share/info usr/share/locale \ + etc/shadow etc/gshadow + FROM ubuntu:24.04 RUN apt-get update \ @@ -5,6 +27,9 @@ RUN apt-get update \ build-essential ca-certificates curl git pkg-config xz-utils \ && rm -rf /var/lib/apt/lists/* -COPY rk3588-buildkit /opt/rk3588-buildkit +COPY --from=prune /opt/rk3588-buildkit /opt/rk3588-buildkit ENV PATH=/opt/rk3588-buildkit/bin:$PATH + +LABEL org.opencontainers.image.source="https://github.com/Rust-KVM/rustkvm" \ + org.opencontainers.image.description="RK3588 Buildroot cross toolchain and sysroot for RustKVM" diff --git a/ci/devkit/Dockerfile b/ci/devkit/Dockerfile new file mode 100644 index 0000000..c44fedb --- /dev/null +++ b/ci/devkit/Dockerfile @@ -0,0 +1,49 @@ +# RustKVM devkit: everything needed to build the frontend and the device +# binary, on top of the RK3588 buildkit image (cross gcc + sysroot). +# +# scripts/devkit.sh image # build it locally +# scripts/devkit.sh shell # use it +# +# Toolchain versions are pinned here and nowhere else in the container path. +ARG BUILDKIT_IMAGE=ghcr.io/rust-kvm/rk3588-buildkit:20250527 +FROM ${BUILDKIT_IMAGE} + +# One pinned nightly builds both the device binary (-Z build-std) and the +# frontend (wasm32). Matches what release.yml uses. +ARG RUST_NIGHTLY=nightly-2026-06-14 +ARG TRUNK_VERSION=0.21.14 + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev \ + openssh-client rsync sudo \ + && rm -rf /var/lib/apt/lists/* + +ENV RUSTUP_HOME=/usr/local/rustup \ + CARGO_HOME=/usr/local/cargo \ + PATH=/usr/local/cargo/bin:/opt/rk3588-buildkit/bin:$PATH + +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ + | sh -s -- -y --no-modify-path --profile minimal --default-toolchain none \ + && rustup toolchain install "${RUST_NIGHTLY}" --profile minimal \ + --component rust-src,rustfmt,clippy \ + --target aarch64-unknown-linux-gnu,wasm32-unknown-unknown \ + && rustup default "${RUST_NIGHTLY}" \ + && curl -sSfL "https://github.com/trunk-rs/trunk/releases/download/v${TRUNK_VERSION}/trunk-x86_64-unknown-linux-gnu.tar.gz" \ + | tar -xz -C /usr/local/cargo/bin trunk \ + && chmod -R a+w "${RUSTUP_HOME}" "${CARGO_HOME}" + +# RUSTUP_TOOLCHAIN overrides the repo's rust-toolchain.toml (`stage2`, a +# toolchain only the reference workbench has), and crates/rkvm-web's `stable`. +ENV RUSTUP_TOOLCHAIN=${RUST_NIGHTLY} \ + RKVM_WEB_TOOLCHAIN=${RUST_NIGHTLY} \ + CC_aarch64_unknown_linux_gnu=/opt/rk3588-buildkit/bin/aarch64-buildroot-linux-gnu-gcc + +# build.rs reads git metadata from a bind-mounted checkout owned by another uid. +# The devcontainer runs as `ubuntu` (uid remapped to the host user). +RUN git config --system --add safe.directory '*' \ + && (id ubuntu >/dev/null 2>&1 || useradd -m -s /bin/bash -u 1000 ubuntu) + +LABEL org.opencontainers.image.source="https://github.com/Rust-KVM/rustkvm" \ + org.opencontainers.image.description="RustKVM devkit: pinned Rust nightly, trunk and the RK3588 cross toolchain" \ + org.opencontainers.image.licenses="GPL-2.0-only" diff --git a/ci/firmware/Dockerfile b/ci/firmware/Dockerfile new file mode 100644 index 0000000..b2aa197 --- /dev/null +++ b/ci/firmware/Dockerfile @@ -0,0 +1,39 @@ +# Host environment for the FriendlyElec RK3588 Buildroot SDK (kernel, u-boot, +# Buildroot rootfs, SD / eMMC image packing). Used by scripts/build-firmware.sh +# through scripts/devkit.sh firmware. +# +# Image packing uses loop devices, so the container runs --privileged. +FROM ubuntu:22.04 + +ARG DEBIAN_FRONTEND=noninteractive + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + bc bison build-essential ca-certificates ccache clang cmake cpio curl \ + device-tree-compiler dosfstools dwarves e2fsprogs exfatprogs fakeroot file flex \ + gcc-arm-linux-gnueabihf git git-lfs gperf kmod libclang-dev \ + libdbus-1-dev libelf-dev libgmp-dev libmpc-dev libncurses-dev \ + libnl-3-dev libssl-dev llvm-dev lz4 openssl parted patch \ + pkg-config python-is-python3 python3 python3-pip rsync sudo texinfo \ + u-boot-tools udev unzip uuid-dev wget xz-utils zstd \ + && rm -rf /var/lib/apt/lists/* + +# Google's `repo` launcher (v2.69), pinned by content hash. +ARG REPO_SHA256=6ed2f6325705b6ea0af99466d6ebd04adec387a5b6a906cd77bc8c6da65123e5 +RUN curl -sSfL https://storage.googleapis.com/git-repo-downloads/repo -o /usr/local/bin/repo \ + && echo "${REPO_SHA256} /usr/local/bin/repo" | sha256sum -c - \ + && chmod a+rx /usr/local/bin/repo + +# Buildroot refuses some packages as root; the build runs as `builder`, which +# may sudo for the SDK's image-packing steps. devkit.sh maps it to the host uid. +ARG UID=1000 +ARG GID=1000 +RUN groupadd -g "${GID}" builder \ + && useradd -m -s /bin/bash -u "${UID}" -g "${GID}" builder \ + && echo 'builder ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/builder \ + && git config --system --add safe.directory '*' \ + && git config --system user.name "RustKVM firmware build" \ + && git config --system user.email "build@localhost" + +USER builder +WORKDIR /home/builder diff --git a/docs/README.md b/docs/README.md index 26de36c..bfdb8ac 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,7 +4,9 @@ | --- | --- | | [Getting started on RK3588](getting-started.md) | Flashing, deploying, wiring and first login | | [API reference](api.md) | HTTP routes, JSON-RPC, MCP tools, HID-RPC, MQTT, Prometheus metrics | -| [Building from source](../README.md#building-from-source) | Frontend, x86_64 host build and the aarch64 cross toolchain | +| [Building](building.md) | Docker devkit, application package and firmware image, pinned versions | +| [Releasing and shipping firmware](firmware-release.md) | Versioning, release checklist, flashing, factory state, acceptance test | +| [Building from source manually](../README.md#building-from-source) | Frontend, x86_64 host build and the aarch64 cross toolchain without containers | | [Architecture and conventions](../AGENTS.md) | Module tree, coding rules, gotchas | | [Contributing](../CONTRIBUTING.md) | Workflow, CI gates, PR expectations, release process | | [Changelog](../CHANGELOG.md) | What changed between versions | diff --git a/docs/building.md b/docs/building.md new file mode 100644 index 0000000..9c4b43a --- /dev/null +++ b/docs/building.md @@ -0,0 +1,119 @@ +# Building RustKVM + +Two build products, both reproducible on any x86_64 Linux host (WSL2 included) with +only Docker installed: + +| Product | Command | Output | +| --- | --- | --- | +| Application (`rustkvm_app` with the embedded web UI) | `scripts/devkit.sh run scripts/build-app.sh` | `out/app/` | +| Firmware image (kernel, u-boot, rootfs, application) | `scripts/devkit.sh firmware` | `out/firmware/` | + +The scripts also run directly on a host that has the tools installed; the containers +just pin those tools. + +## Pinned versions + +| What | Version | Where it is pinned | +| --- | --- | --- | +| Rust (device binary and frontend) | `nightly-2026-06-14` (rustc 1.98.0-nightly) with `aarch64` and `wasm32` targets | `ci/devkit/Dockerfile`, `release.yml` | +| Trunk | `0.21.14` | `ci/devkit/Dockerfile`, `release.yml`, `web.yml` | +| Cross gcc + sysroot | Buildroot host dir of the firmware below | `rk3588-buildkit` image | +| FriendlyElec Buildroot SDK | exact commit per project | `os/rk3588/manifest.xml` | +| SDK changes | patch files | `os/rk3588/patches/` | +| `repo` launcher | v2.69, sha256-checked | `ci/firmware/Dockerfile` | + +The repository's `rust-toolchain.toml` names `stage2`, a self-built toolchain that only +the reference workbench has. The devkit sets `RUSTUP_TOOLCHAIN`, which takes precedence, +so nobody else needs to build rustc. + +## 1. Application + +```bash +scripts/devkit.sh run scripts/build-app.sh +``` + +The first run pulls the published devkit image, +`ghcr.io/rust-kvm/rustkvm-devkit:nightly-2026-06-14` (about 3 GB unpacked). `out/app/` then +holds `rustkvm_app-aarch64-unknown-linux-gnu`, its `.sha256` (the same pair the OTA +updater installs) and `BUILD_INFO`. + +The devkit image sits on `ghcr.io/rust-kvm/rk3588-buildkit:20250527`, the cross gcc and +sysroot extracted from the firmware build with documentation and target executables +removed. To build both images yourself instead of pulling them: + +```bash +scripts/devkit.sh buildkit [HOST_DIR] # default /opt/rk3588-buildkit, or + # ~/rustkvm-sdk/rk3588/buildroot/output/rockchip_rk3588/host +scripts/devkit.sh image +``` + +Maintainers publish new images with `scripts/devkit.sh push` after bumping the tags in +`scripts/devkit.sh`, `ci/devkit/Dockerfile` and `.devcontainer/devcontainer.json`. + +Day-to-day commands inside the devkit: + +```bash +scripts/devkit.sh shell +cargo clippy --workspace --release --all-targets -- -D warnings +cargo test --workspace +cd crates/rkvm-web && trunk serve # UI dev server on :9000 +``` + +VS Code / any devcontainer client: open the repository and choose *Reopen in +Container*; `.devcontainer/devcontainer.json` uses the same image. + +Deploy to a board on the network as before: `./dev_deploy.sh -r `. + +## 2. Firmware image + +Costs of the first build (the disk figure is the reference workbench's SDK tree; network and time are estimates): + +| | | +| --- | --- | +| Disk | about 60 GB for the SDK checkout and build tree, plus about 10 GB of download and ccache under `~/.cache/rustkvm` | +| Network | about 15 GB on the first sync (shallow clones plus Buildroot package sources) | +| Time | several hours on 16 threads; later builds reuse the tree and ccache | + +```bash +scripts/devkit.sh run scripts/build-app.sh # the image embeds out/app/ +scripts/devkit.sh firmware # sync, patch, build, collect +``` + +Steps can be run separately: `scripts/devkit.sh firmware patch build collect` skips the +sync. The SDK checkout lives in `RUSTKVM_SDK_DIR` (default `~/rustkvm-sdk/rk3588`) and +belongs to the script: the patch step resets every patched project to its pinned commit. + +`out/firmware/` then holds: + +| File | Use | +| --- | --- | +| `rustkvm-rk3588--sd.img.gz` | Boots from a microSD card | +| `rustkvm-rk3588--eflasher.img.gz` | microSD card that writes the system to eMMC | +| `rustkvm-rk3588-.release` | The `/etc/rustkvm-release` baked into the image | +| `rustkvm-rk3588-.SHA256SUMS` | Checksums of the above | + +`` is `--`, e.g. `0.1.0-20261017-1a2b3c4d`; +set `FW_VERSION` to override it for a release. + +The firmware container runs `--privileged` because the SDK packs images through loop +devices. What the SDK changes are and how to edit them: [`os/rk3588/README.md`](../os/rk3588/README.md). + +## Networks behind a proxy + +`scripts/devkit.sh` forwards `http_proxy`, `https_proxy` and `no_proxy` into image builds +and containers and uses host networking, so a proxy on `127.0.0.1` works. Base images +come from Docker Hub; if the Docker daemon itself cannot reach it, configure a registry +mirror or proxy for the daemon, or import the Ubuntu base rootfs: + +```bash +curl -LO https://cdimage.ubuntu.com/ubuntu-base/releases/24.04/release/ubuntu-base-24.04.5-base-amd64.tar.gz +docker import -c 'CMD ["/bin/bash"]' ubuntu-base-24.04.5-base-amd64.tar.gz ubuntu:24.04 +# and the same for 22.04 (firmware builder) +``` + +## Without containers + +The scripts are plain bash. `build-app.sh` needs `trunk`, a nightly with `rust-src` +and the `aarch64-unknown-linux-gnu` target, and `/opt/rk3588-buildkit`. +`build-firmware.sh` needs `repo` and the packages listed in `ci/firmware/Dockerfile` +(Ubuntu 22.04 is what FriendlyElec supports). diff --git a/docs/firmware-release.md b/docs/firmware-release.md new file mode 100644 index 0000000..4575dc6 --- /dev/null +++ b/docs/firmware-release.md @@ -0,0 +1,63 @@ +# Releasing and shipping firmware + +How a RustKVM version goes from a tag to a flashed unit in a box. Building itself is +in [building.md](building.md). + +## Versions + +| Thing | Format | Source of truth | +| --- | --- | --- | +| Application | `X.Y.Z` (semver) | `version` in the root `Cargo.toml`; tag `vX.Y.Z` | +| Firmware image | `X.Y.Z-YYYYMMDD-` | `FW_VERSION` in `scripts/build-firmware.sh`, written to `/etc/rustkvm-release` | +| SDK | commit per project | `os/rk3588/manifest.xml` | + +The application updates itself over the air from GitHub Releases (`app/src/update.rs`); +the firmware image is what a unit leaves the factory with. A firmware release therefore +always embeds a released application version. + +## Release checklist + +1. Bump `version` in `Cargo.toml`, update `CHANGELOG.md`, merge to `main`. +2. Tag `vX.Y.Z`. `release.yml` builds the application and publishes the OTA assets. +3. Build the firmware from that tag with the released binary: + ```bash + git checkout vX.Y.Z + scripts/devkit.sh run scripts/build-app.sh + FW_VERSION=X.Y.Z-$(date -u +%Y%m%d) scripts/devkit.sh firmware + ``` + Check that `out/app/*.sha256` matches the one on the GitHub release. +4. Flash a unit with the eflasher image and run the acceptance test below. +5. Archive `out/firmware/` (images, `.release`, `SHA256SUMS`) with the release; it is + what the production line flashes. + +## Flashing + +| Method | Image | When | +| --- | --- | --- | +| microSD boot | `*-sd.img.gz` | Development, field recovery | +| eMMC via eflasher card | `*-eflasher.img.gz` | Production: write to a card, boot the unit from it, it installs to eMMC | +| USB (Maskrom) | partition images under the SDK's `out/` | Bricked units; RKDevTool or `rkdeveloptool` | + +Write a card with `balenaEtcher`, or `gzip -dc | sudo dd of=/dev/sdX bs=4M conv=fsync`. + +## Factory state + +What a freshly flashed unit looks like, and what "factory reset" must restore: + +| Item | Factory state | +| --- | --- | +| `/userdata/rustkvm/config.toml` | absent → setup wizard on first login (device password or no-password mode) | +| TLS certificate | self-signed, generated at start | +| SSH (dropbear) | off; enabled only by `/userdata/rustkvm/devmode.enable` | +| Root password (serial console) | login disabled unless the build sets `RUSTKVM_ROOT_PASSWORD` (a crypt hash, kept out of the repository) | +| Hostname / mDNS | `rustkvm.local` | +| `/etc/rustkvm-release` | firmware version, app version, git revision | + +## Acceptance test per release + +- Boots to the web UI from eMMC; `/etc/rustkvm-release` shows the expected version. +- Setup wizard appears; password login works afterwards. +- HDMI video at the target's native mode, keyboard and mouse, audio. +- Virtual media mounts an ISO; ATX power/reset if the extension is fitted. +- OTA check reports the current version as up to date. +- `GET /device/health` is `ok`; rebooting twice keeps the configuration. diff --git a/os/rk3588/README.md b/os/rk3588/README.md new file mode 100644 index 0000000..7c4f728 --- /dev/null +++ b/os/rk3588/README.md @@ -0,0 +1,48 @@ +# RK3588 firmware layer + +Everything that turns the public FriendlyElec RK3588 Buildroot SDK into the RustKVM +system image. The SDK itself is never forked or vendored; `scripts/build-firmware.sh` +checks it out at the commits pinned here and applies the rest. + +| Path | Contents | +| --- | --- | +| `manifest.xml` | `repo` manifest: every SDK project at an exact commit on `github.com/friendlyarm` | +| `patches//NNNN-*.patch` | Changes to SDK projects, applied in order with `git apply` | +| `rootfs-overlay/` | Files copied into the root filesystem (init script, network config) | + +The build also adds two things that are not in the tree: the `rustkvm_app` binary +(installed to `/userdata/rustkvm/bin/`) and `/etc/rustkvm-release` with the firmware +version. + +## What the patches do + +| Project | Change | +| --- | --- | +| `buildroot` | dhcpcd privsep seccomp allows `dup3` (SIGSYS at boot); chrony drift file ownership | +| `buildroot` | RustKVM defconfig (GStreamer Opus, RGA, uvc-gadget, dropbear client, update engine); uvc-gadget builds with `-Dwerror=false` and accepts CONTINUOUS frame intervals | +| `kernel` | `nanopi6_linux_defconfig`: NBD built in (virtual media), USB UAC2 gadget | +| `scripts/sd-fuse` | builds the `v4l2loopback` module (operator-camera passthrough) | +| `external` | rockit links `librkAlgoDis.so` | +| `device/friendlyelec` | installs `rustkvm-files` (this overlay) and disables the stock dropbear service | + +## Changing the SDK + +Work in the SDK checkout the script created (`$RUSTKVM_SDK_DIR`, default +`~/rustkvm-sdk/rk3588`), then bring the change back here: + +```bash +# one project, uncommitted change → new patch +git -C ~/rustkvm-sdk/rk3588/kernel diff > os/rk3588/patches/kernel/0002-short-description.patch + +# committed changes on top of the pinned commit +git -C ~/rustkvm-sdk/rk3588/buildroot format-patch --no-signature \ + -o os/rk3588/patches/buildroot --start-number 3 HEAD~1 +``` + +Rerun `scripts/build-firmware.sh patch build collect`; the patch step resets each +patched project to its pinned commit first, so a patch that no longer applies fails +loudly instead of stacking. + +To move to newer SDK commits, edit the `revision` of the project in `manifest.xml` +(the `upstream` attribute names the branch it follows), run the `sync` and `patch` +steps, and refresh any patch that stops applying. diff --git a/os/rk3588/manifest.xml b/os/rk3588/manifest.xml new file mode 100644 index 0000000..1c8bf3c --- /dev/null +++ b/os/rk3588/manifest.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/os/rk3588/patches/buildroot/0001-package-dhcpcd-chrony-fix-privsep-SIGSYS-and-drift-f.patch b/os/rk3588/patches/buildroot/0001-package-dhcpcd-chrony-fix-privsep-SIGSYS-and-drift-f.patch new file mode 100644 index 0000000..ca0bdd9 --- /dev/null +++ b/os/rk3588/patches/buildroot/0001-package-dhcpcd-chrony-fix-privsep-SIGSYS-and-drift-f.patch @@ -0,0 +1,65 @@ +From e1fd8a33db205ecc1b9e6d91b3b3161c25ffcc8c Mon Sep 17 00:00:00 2001 +From: foxxcn +Date: Sun, 11 Oct 2026 10:18:14 +0800 +Subject: [PATCH] package/dhcpcd, chrony: fix privsep SIGSYS and drift file + permissions + +dhcpcd 10.0.4 privsep seccomp filter lacks dup3, which glibc uses for +dup2() on aarch64, so dhcpcd was killed with SIGSYS at boot. + +chronyd runs as the chrony user but /var/lib/chrony was root-owned, so +the drift file could never be written. +--- + package/chrony/chrony.mk | 5 ++++ + .../0002-privsep-linux-allow-dup3.patch | 26 +++++++++++++++++++ + 2 files changed, 31 insertions(+) + create mode 100755 package/dhcpcd/0002-privsep-linux-allow-dup3.patch + +diff --git a/package/chrony/chrony.mk b/package/chrony/chrony.mk +index 765db00779..ce0c31836d 100644 +--- a/package/chrony/chrony.mk ++++ b/package/chrony/chrony.mk +@@ -76,6 +76,11 @@ define CHRONY_INSTALL_TARGET_CMDS + mkdir -p $(TARGET_DIR)/var/lib/chrony + endef + ++# chronyd drops to the chrony user and writes its drift file here. ++define CHRONY_PERMISSIONS ++ /var/lib/chrony d 755 chrony chrony - - - - - ++endef ++ + define CHRONY_INSTALL_INIT_SYSV + $(INSTALL) -D -m 755 package/chrony/S49chrony $(TARGET_DIR)/etc/init.d/S49chrony + endef +diff --git a/package/dhcpcd/0002-privsep-linux-allow-dup3.patch b/package/dhcpcd/0002-privsep-linux-allow-dup3.patch +new file mode 100755 +index 0000000000..58015fd4cb +--- /dev/null ++++ b/package/dhcpcd/0002-privsep-linux-allow-dup3.patch +@@ -0,0 +1,26 @@ ++From: RustKVM ++Subject: [PATCH] privsep-linux: allow dup3 ++ ++aarch64 has no dup2 syscall; glibc implements dup2() with dup3(). The ++seccomp filter only allowed __NR_dup2, so the privileged helper was ++killed with SIGSYS (audit: syscall=24) when it daemonised, leaving no ++dhcpcd running to renew leases. ++ ++Upstream dhcpcd allows __NR_dup3 from 10.0.5 onwards. ++--- ++ src/privsep-linux.c | 3 +++ ++ 1 file changed, 3 insertions(+) ++ ++diff --git a/src/privsep-linux.c b/src/privsep-linux.c ++--- a/src/privsep-linux.c +++++ b/src/privsep-linux.c ++@@ -313,6 +313,9 @@ ++ #ifdef __NR_dup2 ++ SECCOMP_ALLOW(__NR_dup2), // daemonising dups stderr to stdin(/dev/null) ++ #endif +++#ifdef __NR_dup3 +++ SECCOMP_ALLOW(__NR_dup3), // dup2() is dup3() on aarch64 +++#endif ++ #ifdef __NR_epoll_ctl ++ SECCOMP_ALLOW(__NR_epoll_ctl), ++ #endif diff --git a/os/rk3588/patches/buildroot/0002-rustkvm-defconfig-and-package-fixes.patch b/os/rk3588/patches/buildroot/0002-rustkvm-defconfig-and-package-fixes.patch new file mode 100644 index 0000000..6599b9c --- /dev/null +++ b/os/rk3588/patches/buildroot/0002-rustkvm-defconfig-and-package-fixes.patch @@ -0,0 +1,310 @@ +diff --git a/configs/rockchip/base/recovery.config b/configs/rockchip/base/recovery.config +index 13ceaf0e55..dfb931958b 100644 +--- a/configs/rockchip/base/recovery.config ++++ b/configs/rockchip/base/recovery.config +@@ -8,3 +8,6 @@ BR2_PACKAGE_RKUPDATE=y + + + BR2_ROOTFS_DEVICE_CREATION_DYNAMIC_EUDEV=y ++ ++BR2_PACKAGE_RECOVERY_USE_UPDATEENGINE=y ++BR2_PACKAGE_RECOVERY_UPDATEENGINEBIN=y +diff --git a/configs/rockchip/multimedia/gst/audio.config b/configs/rockchip/multimedia/gst/audio.config +index 5a884a08de..7afa6f1623 100644 +--- a/configs/rockchip/multimedia/gst/audio.config ++++ b/configs/rockchip/multimedia/gst/audio.config +@@ -9,3 +9,5 @@ BR2_PACKAGE_GST1_PLUGINS_GOOD_PLUGIN_PULSE=y + BR2_PACKAGE_GST1_PLUGINS_GOOD_PLUGIN_SOUPHTTPSRC=y + BR2_PACKAGE_CA_CERTIFICATES=y + BR2_PACKAGE_LIBSOUP_SSL=y ++BR2_PACKAGE_GST1_PLUGINS_BASE_PLUGIN_OPUS=y ++BR2_PACKAGE_GST1_PLUGINS_BAD_PLUGIN_OPUS=y +diff --git a/configs/rockchip/network/network.config b/configs/rockchip/network/network.config +index f1e4fb0593..31fdcc7857 100644 +--- a/configs/rockchip/network/network.config ++++ b/configs/rockchip/network/network.config +@@ -3,4 +3,4 @@ BR2_PACKAGE_DHCPCD=y + BR2_PACKAGE_DNSMASQ=y + BR2_PACKAGE_DROPBEAR=y + BR2_PACKAGE_GESFTPSERVER=y +-# BR2_PACKAGE_DROPBEAR_CLIENT is not set ++BR2_PACKAGE_DROPBEAR_CLIENT=y +diff --git a/configs/rockchip_rk3588_defconfig b/configs/rockchip_rk3588_defconfig +index f7135da73f..673d32b3cc 100644 +--- a/configs/rockchip_rk3588_defconfig ++++ b/configs/rockchip_rk3588_defconfig +@@ -1,31 +1,180 @@ +-#include "base/base.config" +-#include "chips/rk3588_aarch64.config" +-#include "font/chinese.config" +-#include "fs/exfat.config" +-#include "fs/ntfs.config" +-#include "fs/vfat.config" +-#include "gpu/gpu.config" +-#include "multimedia/audio.config" +-#include "multimedia/camera.config" +-#include "multimedia/gst/audio.config" +-#include "multimedia/gst/camera.config" +-#include "multimedia/gst/rtsp.config" +-#include "multimedia/gst/video.config" +-#include "multimedia/mpp.config" +-#include "tools/benchmark.config" +-#include "tools/common.config" +-#include "tools/test.config" +-#include "network/chromium.config" +-#include "npu2.config" +-#include "powermanager.config" +-#include "weston.config" +-BR2_PACKAGE_VIM=y +-BR2_PACKAGE_VIM_RUNTIME=y +-BR2_PACKAGE_GST1_LIBAV=y +-BR2_PACKAGE_ALSA_UTILS_SPEAKER_TEST=y +-BR2_PACKAGE_RKWIFIBT_AP6398S=y +-BR2_PACKAGE_RKWIFIBT_BTUART="ttyS8" ++BR2_aarch64=y ++BR2_cortex_a76_a55=y ++BR2_PRIMARY_SITE="http://sources.buildroot.net" ++BR2_BACKUP_SITE="" ++BR2_GNU_MIRROR="http://mirrors.kernel.org/gnu" ++BR2_CCACHE=y ++BR2_STRIP_EXCLUDE_FILES="libgraphic_lsf.so" ++BR2_FORCE_HOST_BUILD=y ++BR2_TOOLCHAIN_BUILDROOT_CXX=y ++BR2_TARGET_GENERIC_HOSTNAME="rk3588" ++BR2_TARGET_GENERIC_ISSUE="Welcome to RK3588 Buildroot" ++BR2_ROOTFS_DEVICE_CREATION_DYNAMIC_EUDEV=y ++BR2_ROOTFS_MERGED_USR=y ++BR2_TARGET_SERIAL_SHELL_SH=y ++BR2_ROOTFS_OVERLAY="board/rockchip/common/base board/rockchip/rk3588/fs-overlay/" ++BR2_ROOTFS_POST_BUILD_SCRIPT="board/rockchip/common/post-build.sh" ++BR2_LINUX_KERNEL_CUSTOM_LOCAL=y ++BR2_LINUX_KERNEL_CUSTOM_LOCAL_LOCATION="$(TOPDIR)/../kernel" ++BR2_PACKAGE_ROCKCHIP=y ++BR2_PACKAGE_CAMERA_ENGINE=y ++BR2_PACKAGE_CAMERA_ENGINE_RKAIQ_RKISP_DEMO=y ++BR2_PACKAGE_GSTREAMER1_ROCKCHIP=y ++BR2_PACKAGE_LIBV4L_RKMPP=y ++BR2_PACKAGE_RECOVERY=y ++BR2_PACKAGE_RECOVERY_USE_UPDATEENGINE=y + BR2_PACKAGE_RKNPU2=y +-BR2_TARGET_GENERIC_GETTY_PORT="ttyFIQ0" +-BR2_TARGET_GENERIC_GETTY_BAUDRATE_1500000=y ++BR2_PACKAGE_RKSCRIPT=y ++BR2_PACKAGE_RKTOOLKIT=y ++BR2_PACKAGE_RKUPDATE=y ++BR2_PACKAGE_ROCKCHIP_ALSA_CONFIG=y ++BR2_PACKAGE_ROCKCHIP_RGA=y ++BR2_PACKAGE_ROCKCHIP_MALI=y ++BR2_PACKAGE_ROCKCHIP_MPP_ALLOCATOR_DRM=y ++BR2_PACKAGE_ROCKCHIP_MPP_TESTS=y ++BR2_PACKAGE_ROCKCHIP_TEST=y ++BR2_PACKAGE_ROCKIT=y ++BR2_PACKAGE_BUSYBOX_CONFIG_FRAGMENT_FILES="board/rockchip/common/base/busybox.fragment" ++BR2_PACKAGE_BUSYBOX_SHOW_OTHERS=y ++BR2_PACKAGE_ALSA_UTILS=y ++BR2_PACKAGE_ALSA_UTILS_ALSACONF=y ++BR2_PACKAGE_ALSA_UTILS_AMIXER=y ++BR2_PACKAGE_ALSA_UTILS_APLAY=y ++BR2_PACKAGE_ALSA_UTILS_SPEAKER_TEST=y ++BR2_PACKAGE_GSTREAMER1=y ++BR2_PACKAGE_GST1_PLUGINS_BASE_INSTALL_TOOLS=y ++BR2_PACKAGE_GST1_PLUGINS_BASE_PLUGIN_APP=y ++BR2_PACKAGE_GST1_PLUGINS_BASE_PLUGIN_AUDIORATE=y ++BR2_PACKAGE_GST1_PLUGINS_BASE_PLUGIN_AUDIOTESTSRC=y ++BR2_PACKAGE_GST1_PLUGINS_BASE_PLUGIN_ENCODING=y ++BR2_PACKAGE_GST1_PLUGINS_BASE_PLUGIN_TCP=y ++BR2_PACKAGE_GST1_PLUGINS_BASE_PLUGIN_VIDEOTESTSRC=y ++BR2_PACKAGE_GST1_PLUGINS_BASE_PLUGIN_VIDEORATE=y ++BR2_PACKAGE_GST1_PLUGINS_BASE_PLUGIN_ALSA=y ++BR2_PACKAGE_GST1_PLUGINS_BASE_PLUGIN_OGG=y ++BR2_PACKAGE_GST1_PLUGINS_BASE_PLUGIN_OPUS=y ++BR2_PACKAGE_GST1_PLUGINS_BASE_PLUGIN_THEORA=y ++BR2_PACKAGE_GST1_PLUGINS_BASE_PLUGIN_VORBIS=y ++BR2_PACKAGE_GST1_PLUGINS_GOOD=y ++BR2_PACKAGE_GST1_PLUGINS_GOOD_JPEG=y ++BR2_PACKAGE_GST1_PLUGINS_GOOD_PNG=y ++BR2_PACKAGE_GST1_PLUGINS_GOOD_PLUGIN_ALPHA=y ++BR2_PACKAGE_GST1_PLUGINS_GOOD_PLUGIN_APETAG=y ++BR2_PACKAGE_GST1_PLUGINS_GOOD_PLUGIN_AUDIOPARSERS=y ++BR2_PACKAGE_GST1_PLUGINS_GOOD_PLUGIN_AUPARSE=y ++BR2_PACKAGE_GST1_PLUGINS_GOOD_PLUGIN_AUTODETECT=y ++BR2_PACKAGE_GST1_PLUGINS_GOOD_PLUGIN_DEINTERLACE=y ++BR2_PACKAGE_GST1_PLUGINS_GOOD_PLUGIN_FLV=y ++BR2_PACKAGE_GST1_PLUGINS_GOOD_PLUGIN_ID3DEMUX=y ++BR2_PACKAGE_GST1_PLUGINS_GOOD_PLUGIN_MATROSKA=y ++BR2_PACKAGE_GST1_PLUGINS_GOOD_PLUGIN_MULTIFILE=y ++BR2_PACKAGE_GST1_PLUGINS_GOOD_PLUGIN_RTP=y ++BR2_PACKAGE_GST1_PLUGINS_GOOD_PLUGIN_RTPMANAGER=y ++BR2_PACKAGE_GST1_PLUGINS_GOOD_PLUGIN_RTSP=y ++BR2_PACKAGE_GST1_PLUGINS_GOOD_PLUGIN_UDP=y ++BR2_PACKAGE_GST1_PLUGINS_GOOD_PLUGIN_VIDEOBOX=y ++BR2_PACKAGE_GST1_PLUGINS_GOOD_PLUGIN_VIDEOCROP=y ++BR2_PACKAGE_GST1_PLUGINS_GOOD_PLUGIN_VIDEOFILTER=y ++BR2_PACKAGE_GST1_PLUGINS_GOOD_PLUGIN_VIDEOMIXER=y ++BR2_PACKAGE_GST1_PLUGINS_GOOD_PLUGIN_MPG123=y ++BR2_PACKAGE_GST1_PLUGINS_GOOD_PLUGIN_V4L2=y ++BR2_PACKAGE_GST1_PLUGINS_GOOD_PLUGIN_GDKPIXBUF=y ++BR2_PACKAGE_GST1_PLUGINS_GOOD_PLUGIN_PULSE=y ++BR2_PACKAGE_GST1_PLUGINS_GOOD_PLUGIN_SOUPHTTPSRC=y ++BR2_PACKAGE_GST1_PLUGINS_BAD=y ++BR2_PACKAGE_GST1_PLUGINS_BAD_PLUGIN_ADPCMDEC=y ++BR2_PACKAGE_GST1_PLUGINS_BAD_PLUGIN_ADPCMENC=y ++BR2_PACKAGE_GST1_PLUGINS_BAD_PLUGIN_CAMERABIN2=y ++BR2_PACKAGE_GST1_PLUGINS_BAD_PLUGIN_DEBUGUTILS=y ++BR2_PACKAGE_GST1_PLUGINS_BAD_PLUGIN_DVBSUBOVERLAY=y ++BR2_PACKAGE_GST1_PLUGINS_BAD_PLUGIN_DVDSPU=y ++BR2_PACKAGE_GST1_PLUGINS_BAD_PLUGIN_JPEGFORMAT=y ++BR2_PACKAGE_GST1_PLUGINS_BAD_PLUGIN_MIDI=y ++BR2_PACKAGE_GST1_PLUGINS_BAD_PLUGIN_MPEGDEMUX=y ++BR2_PACKAGE_GST1_PLUGINS_BAD_PLUGIN_MPEGTSDEMUX=y ++BR2_PACKAGE_GST1_PLUGINS_BAD_PLUGIN_MPEGTSMUX=y ++BR2_PACKAGE_GST1_PLUGINS_BAD_PLUGIN_MPEGPSMUX=y ++BR2_PACKAGE_GST1_PLUGINS_BAD_PLUGIN_ONVIF=y ++BR2_PACKAGE_GST1_PLUGINS_BAD_PLUGIN_RTMP=y ++BR2_PACKAGE_GST1_PLUGINS_BAD_PLUGIN_SDP=y ++BR2_PACKAGE_GST1_PLUGINS_BAD_PLUGIN_VIDEOPARSERS=y ++BR2_PACKAGE_GST1_PLUGINS_BAD_PLUGIN_FAAD=y ++BR2_PACKAGE_GST1_PLUGINS_BAD_PLUGIN_FLUIDSYNTH=y ++BR2_PACKAGE_GST1_PLUGINS_BAD_PLUGIN_HLS=y ++BR2_PACKAGE_GST1_PLUGINS_BAD_PLUGIN_KMS=y ++BR2_PACKAGE_GST1_PLUGINS_BAD_PLUGIN_MPEG2ENC=y ++BR2_PACKAGE_GST1_PLUGINS_BAD_PLUGIN_OPUS=y ++BR2_PACKAGE_GST1_PLUGINS_UGLY=y ++BR2_PACKAGE_GST1_PLUGINS_UGLY_PLUGIN_ASFDEMUX=y ++BR2_PACKAGE_GST1_PLUGINS_UGLY_PLUGIN_DVDLPCMDEC=y ++BR2_PACKAGE_GST1_PLUGINS_UGLY_PLUGIN_DVDSUB=y ++BR2_PACKAGE_GST1_PLUGINS_UGLY_PLUGIN_MPEG2DEC=y ++BR2_PACKAGE_GST1_LIBAV=y ++BR2_PACKAGE_PULSEAUDIO_DAEMON=y ++BR2_PACKAGE_LMBENCH=y ++BR2_PACKAGE_UNIXBENCH=y ++BR2_PACKAGE_RT_TESTS=y ++BR2_PACKAGE_STRACE=y ++BR2_PACKAGE_STRESS=y ++BR2_PACKAGE_STRESS_NG=y ++BR2_PACKAGE_WHETSTONE=y ++BR2_PACKAGE_DOSFSTOOLS=y ++BR2_PACKAGE_DOSFSTOOLS_FATLABEL=y ++BR2_PACKAGE_DOSFSTOOLS_FSCK_FAT=y ++BR2_PACKAGE_DOSFSTOOLS_MKFS_FAT=y ++BR2_PACKAGE_EXFAT=y ++BR2_PACKAGE_EXFAT_UTILS=y ++BR2_PACKAGE_FATRESIZE=y ++BR2_PACKAGE_NTFS_3G=y ++BR2_PACKAGE_NTFS_3G_NTFSPROGS=y ++BR2_PACKAGE_DEJAVU=y ++BR2_PACKAGE_FONT_AWESOME=y ++BR2_PACKAGE_SOURCE_HAN_SANS_CN=y ++BR2_PACKAGE_GLMARK2=y ++BR2_PACKAGE_UVC_GADGET=y ++BR2_PACKAGE_WESTON=y ++BR2_PACKAGE_WESTON_SIMPLE_CLIENTS=y ++BR2_PACKAGE_WESTON_DEMO_CLIENTS=y ++# BR2_PACKAGE_EUDEV_ENABLE_HWDB is not set ++BR2_PACKAGE_EVTEST=y ++BR2_PACKAGE_I2C_TOOLS=y ++BR2_PACKAGE_INPUT_EVENT_DAEMON=y ++BR2_PACKAGE_PM_UTILS=y ++BR2_PACKAGE_USBMOUNT=y ++BR2_PACKAGE_ALSA_UCM_CONF=y ++BR2_PACKAGE_LIBMAD=y ++BR2_PACKAGE_CA_CERTIFICATES=y ++BR2_PACKAGE_AT_SPI2_CORE=y ++BR2_PACKAGE_JPEG_TURBO_JPEG6=y ++BR2_PACKAGE_LIBDRM_INSTALL_TESTS=y ++BR2_PACKAGE_WAYLAND_UTILS=y ++BR2_PACKAGE_LIBV4L=y ++BR2_PACKAGE_LIBV4L_UTILS=y ++BR2_PACKAGE_BLUEZ5_UTILS=y ++BR2_PACKAGE_CHRONY=y ++BR2_PACKAGE_CHROMIUM_WAYLAND=y ++BR2_PACKAGE_DHCPCD=y ++BR2_PACKAGE_DNSMASQ=y ++BR2_PACKAGE_DROPBEAR=y ++BR2_PACKAGE_GESFTPSERVER=y ++BR2_PACKAGE_IPUTILS=y ++BR2_PACKAGE_IW=y ++BR2_PACKAGE_LRZSZ=y ++BR2_PACKAGE_WIRELESS_TOOLS=y ++BR2_PACKAGE_ANDROID_ADBD=y ++BR2_PACKAGE_COREUTILS=y ++BR2_PACKAGE_PROCPS_NG=y ++BR2_PACKAGE_PROCRANK_LINUX=y ++BR2_PACKAGE_UTIL_LINUX_MOUNT=y ++BR2_PACKAGE_VIM=y ++BR2_TARGET_ROOTFS_CPIO=y ++BR2_TARGET_ROOTFS_CPIO_GZIP=y ++BR2_TARGET_ROOTFS_EXT2=y ++BR2_TARGET_ROOTFS_EXT2_4=y ++BR2_TARGET_ROOTFS_EXT2_SIZE_AUTO=y ++BR2_TARGET_ROOTFS_SQUASHFS=y + BR2_TARGET_ROOTFS_TAR_XZ=y ++BR2_PACKAGE_HOST_ENVIRONMENT_SETUP=y ++BR2_PACKAGE_HOST_NTFS_3G=y ++BR2_PACKAGE_HOST_PYTHON3=y ++BR2_PACKAGE_HOST_PYTHON3_SSL=y +diff --git a/package/gstreamer1/gstreamer1/gst.sh b/package/gstreamer1/gstreamer1/gst.sh +index 693627340a..050acfb81d 100644 +--- a/package/gstreamer1/gstreamer1/gst.sh ++++ b/package/gstreamer1/gstreamer1/gst.sh +@@ -78,8 +78,8 @@ export GST_V4L2SRC_MAX_RESOLUTION=3840x2160 + + # Try RGA 2D accel in videoconvert, videoscale and videoflip. + # NOTE: Might not success, and might behave different from the official plugin. +-# export GST_VIDEO_CONVERT_USE_RGA=1 +-# export GST_VIDEO_FLIP_USE_RGA=1 ++export GST_VIDEO_CONVERT_USE_RGA=1 ++export GST_VIDEO_FLIP_USE_RGA=1 + + # Default rotation for camerabin2: + # clockwise(90)|rotate-180|counterclockwise(270)|horizontal-flip|vertical-flip +diff --git a/package/uvc-gadget/0003-lib-accept-continuous-frame-intervals.patch b/package/uvc-gadget/0003-lib-accept-continuous-frame-intervals.patch +new file mode 100644 +index 0000000000..5b19aa4e02 +--- /dev/null ++++ b/package/uvc-gadget/0003-lib-accept-continuous-frame-intervals.patch +@@ -0,0 +1,29 @@ ++From: rustkvm ++Date: Fri, 20 Jun 2026 00:00:00 +0000 ++Subject: [PATCH] lib: accept CONTINUOUS frame intervals from the V4L2 source ++ ++v4l2loopback (used to bridge a control-end browser camera into the f_uvc ++gadget for operator-camera passthrough) reports ++V4L2_FRMIVAL_TYPE_CONTINUOUS frame intervals. The original enumerator ++only accepted DISCRETE and STEPWISE and returned -EINVAL otherwise, so ++uvc-gadget aborted with "unable to enumerate formats" and the camera ++could never stream. CONTINUOUS shares the STEPWISE union layout; accept ++both as a range. ++ ++Signed-off-by: rustkvm ++--- a/lib/v4l2.c +++++ b/lib/v4l2.c ++@@ -114,7 +114,13 @@ ++ ival->step.denominator = 1; ++ break; ++ +++ case V4L2_FRMIVAL_TYPE_CONTINUOUS: ++ case V4L2_FRMIVAL_TYPE_STEPWISE: +++ /* v4l2loopback reports CONTINUOUS (and some sources +++ * STEPWISE) frame intervals; the union layout is the same +++ * as STEPWISE, so accept both as a range instead of +++ * bailing out — otherwise browser-camera passthrough via +++ * the loopback can never enumerate. */ ++ ival->min = ivalenum.stepwise.min; ++ ival->max = ivalenum.stepwise.max; ++ ival->step = ivalenum.stepwise.step; +diff --git a/package/uvc-gadget/uvc-gadget.mk b/package/uvc-gadget/uvc-gadget.mk +index 52ca51a557..6c6e98be2e 100644 +--- a/package/uvc-gadget/uvc-gadget.mk ++++ b/package/uvc-gadget/uvc-gadget.mk +@@ -11,6 +11,10 @@ UVC_GADGET_LICENSE = LGPL 2.1+ + + UVC_GADGET_DEPENDENCIES = jpeg + ++# Upstream forces werror=true in meson.build, which breaks the build with ++# recent GCC + _FORTIFY_SOURCE (unused-result on read()/chdir()). ++UVC_GADGET_CONF_OPTS += -Dwerror=false ++ + UVC_GADGET_STATIC = $(BR2_PACKAGE_UVC_GADGET_STATIC) + ifeq ($(UVC_GADGET_STATIC),y) + UVC_GADGET_CFLAGS += -static diff --git a/os/rk3588/patches/device/friendlyelec/0001-rk3588-install-rustkvm-files-disable-dropbear.patch b/os/rk3588/patches/device/friendlyelec/0001-rk3588-install-rustkvm-files-disable-dropbear.patch new file mode 100644 index 0000000..116897e --- /dev/null +++ b/os/rk3588/patches/device/friendlyelec/0001-rk3588-install-rustkvm-files-disable-dropbear.patch @@ -0,0 +1,24 @@ +diff --git a/rk3588/base.mk b/rk3588/base.mk +index 46d7ddb..3929936 100644 +--- a/rk3588/base.mk ++++ b/rk3588/base.mk +@@ -23,6 +23,7 @@ BUILDROOT_FILES+=(device/friendlyelec/rk3588/r8125) + BUILDROOT_FILES+=(device/friendlyelec/rk3588/set-pulse-audio-default-output-to-hdmi0) + BUILDROOT_FILES+=(device/friendlyelec/alsa-config) + BUILDROOT_FILES+=(device/friendlyelec/usbdevice-wrapper) ++BUILDROOT_FILES+=(device/friendlyelec/rustkvm-files) + + # U-boot + # +diff --git a/rk3588/disable-unnecessary-services/install.sh b/rk3588/disable-unnecessary-services/install.sh +index fbcb560..b548d25 100755 +--- a/rk3588/disable-unnecessary-services/install.sh ++++ b/rk3588/disable-unnecessary-services/install.sh +@@ -3,6 +3,7 @@ set -eu + ROOTFS_DIR=$1 + + services+=("40rkaiq_3A") ++services+=("50dropbear") + + (cd ${ROOTFS_DIR} && { + cd etc/init.d diff --git a/os/rk3588/patches/external/0001-rockit-link-librkAlgoDis.patch b/os/rk3588/patches/external/0001-rockit-link-librkAlgoDis.patch new file mode 100644 index 0000000..fd8b6d5 --- /dev/null +++ b/os/rk3588/patches/external/0001-rockit-link-librkAlgoDis.patch @@ -0,0 +1,14 @@ +diff --git a/rockit/mpi/CMakeLists.txt b/rockit/mpi/CMakeLists.txt +index ea77198d..7ac1cf4d 100644 +--- a/rockit/mpi/CMakeLists.txt ++++ b/rockit/mpi/CMakeLists.txt +@@ -12,7 +12,8 @@ if(ARCH64) + ${CMAKE_CURRENT_LIST_DIR}/sdk/lib/lib64/libgraphic_lsf.so + ${CMAKE_CURRENT_LIST_DIR}/sdk/lib/lib64/libRkSwCac.so + ${CMAKE_CURRENT_LIST_DIR}/sdk/lib/lib64/libturbojpeg.so +- ${CMAKE_CURRENT_LIST_DIR}/sdk/lib/lib64/libjpeg.so ++ ${CMAKE_CURRENT_LIST_DIR}/sdk/lib/lib64/libjpeg.so ++ ${CMAKE_CURRENT_LIST_DIR}/sdk/lib/lib64/librkAlgoDis.so + ) + set(ROCKIT_DUMPSYS_FILE ${CMAKE_CURRENT_LIST_DIR}/example/bin/bin64/dumpsys) + else() diff --git a/os/rk3588/patches/kernel/0001-nanopi6-defconfig-nbd-builtin-uac2-gadget.patch b/os/rk3588/patches/kernel/0001-nanopi6-defconfig-nbd-builtin-uac2-gadget.patch new file mode 100644 index 0000000..9f5692c --- /dev/null +++ b/os/rk3588/patches/kernel/0001-nanopi6-defconfig-nbd-builtin-uac2-gadget.patch @@ -0,0 +1,21 @@ +diff --git a/arch/arm64/configs/nanopi6_linux_defconfig b/arch/arm64/configs/nanopi6_linux_defconfig +index b018e5f79d02..54721ea7d254 100644 +--- a/arch/arm64/configs/nanopi6_linux_defconfig ++++ b/arch/arm64/configs/nanopi6_linux_defconfig +@@ -638,7 +638,7 @@ CONFIG_ZRAM_WRITEBACK=y + CONFIG_ZRAM_MEMORY_TRACKING=y + CONFIG_BLK_DEV_LOOP=y + CONFIG_BLK_DEV_DRBD=m +-CONFIG_BLK_DEV_NBD=m ++CONFIG_BLK_DEV_NBD=y + CONFIG_BLK_DEV_RAM=y + CONFIG_BLK_DEV_RAM_COUNT=8 + CONFIG_ATA_OVER_ETH=m +@@ -1423,6 +1423,7 @@ CONFIG_USB_CONFIGFS_F_FS=y + CONFIG_USB_CONFIGFS_F_MIDI=y + CONFIG_USB_CONFIGFS_F_HID=y + CONFIG_USB_CONFIGFS_F_UVC=y ++CONFIG_USB_CONFIGFS_F_UAC2=y + CONFIG_TYPEC_TCPM=y + CONFIG_TYPEC_TCPCI=y + CONFIG_TYPEC_HUSB311=y diff --git a/os/rk3588/patches/scripts/sd-fuse/0001-build-v4l2loopback-module.patch b/os/rk3588/patches/scripts/sd-fuse/0001-build-v4l2loopback-module.patch new file mode 100644 index 0000000..637842a --- /dev/null +++ b/os/rk3588/patches/scripts/sd-fuse/0001-build-v4l2loopback-module.patch @@ -0,0 +1,22 @@ +diff --git a/build-kernel.sh b/build-kernel.sh +index 696ea59..1355550 100755 +--- a/build-kernel.sh ++++ b/build-kernel.sh +@@ -275,6 +275,17 @@ function build_kernel() { + }) + }) + ++ # build v4l2loopback (virtual V4L2 device for UVC operator-camera passthrough) ++ (cd ${OUT} && { ++ if [ ! -d v4l2loopback ]; then ++ git clone https://github.com/umlaeute/v4l2loopback.git -b v0.13.1 --depth 1 v4l2loopback ++ fi ++ (cd v4l2loopback && { ++ make CROSS_COMPILE=${CROSS_COMPILE} ARCH=${ARCH} KERNEL_DIR=${KERNEL_SRC} -C ${KERNEL_SRC} M=$(pwd) modules ++ cp v4l2loopback.ko ${KMODULES_OUTDIR}/lib/modules/${KERNEL_VER} -afv ++ }) ++ }) ++ + # build rtw_8822ce wifi driver + (cd ${OUT} && { + if [ ! -d rtw88 ]; then diff --git a/os/rk3588/rootfs-overlay/etc/dhcpcd.conf b/os/rk3588/rootfs-overlay/etc/dhcpcd.conf new file mode 100644 index 0000000..6b78850 --- /dev/null +++ b/os/rk3588/rootfs-overlay/etc/dhcpcd.conf @@ -0,0 +1,50 @@ +# RustKVM: eth0 and eth1 share their last four MAC bytes, so DUID+IAID +# client IDs collide ("IAID conflicts"); identify each NIC by its MAC. +# A sample configuration for dhcpcd. +# See dhcpcd.conf(5) for details. + +# Allow users of this group to interact with dhcpcd via the control socket. +#controlgroup wheel + +# Inform the DHCP server of our hostname for DDNS. +#hostname + +# Use the hardware address of the interface for the Client ID. +clientid +# or +# Use the same DUID + IAID as set in DHCPv6 for DHCPv4 ClientID as per RFC4361. +# Some non-RFC compliant DHCP servers do not reply with this set. +# In this case, comment out duid and enable clientid above. +#duid + +# Persist interface configuration when dhcpcd exits. +persistent + +# vendorclassid is set to blank to avoid sending the default of +# dhcpcd-::: +vendorclassid + +# A list of options to request from the DHCP server. +option domain_name_servers, domain_name, domain_search +option classless_static_routes +# Respect the network MTU. This is applied to DHCP routes. +option interface_mtu + +# Request a hostname from the network +option host_name + +# Most distributions have NTP support. +#option ntp_servers + +# Rapid commit support. +# Safe to enable by default because it requires the equivalent option set +# on the server to actually work. +option rapid_commit + +# A ServerID is required by RFC2131. +require dhcp_server_identifier + +# Generate SLAAC address using the Hardware Address of the interface +#slaac hwaddr +# OR generate Stable Private IPv6 Addresses based from the DUID +slaac private diff --git a/os/rk3588/rootfs-overlay/etc/init.d/S99rustkvm b/os/rk3588/rootfs-overlay/etc/init.d/S99rustkvm new file mode 100755 index 0000000..207dfba --- /dev/null +++ b/os/rk3588/rootfs-overlay/etc/init.d/S99rustkvm @@ -0,0 +1,74 @@ +#!/bin/sh + +# rustkvm_app arms the hardware watchdog and only disarms it on a graceful +# exit, so it is always stopped with SIGTERM. SIGKILL reboots the device. + +APP=/userdata/rustkvm/bin/rustkvm_app +LOG_DIR=/userdata/rustkvm/log + +stop_app() { + pid=$(pidof rustkvm_app) || return 0 + kill -TERM $pid 2>/dev/null || true + cnt=0 + while [ $cnt -lt 100 ]; do + cnt=$((cnt + 1)) + pidof rustkvm_app >/dev/null 2>&1 || return 0 + sleep 0.1 + done + echo "rustkvm_app still running after 10s" >>$LOG_DIR/rustkvm_app.log + return 1 +} + +case "$1" in +start) + cnt=0 + while [ $cnt -lt 30 ]; do + cnt=$((cnt + 1)) + if mount | grep -w userdata >/dev/null 2>&1; then + break + fi + sleep 0.1 + done + + cnt=0 + while [ $cnt -lt 100 ]; do + cnt=$((cnt + 1)) + if [ -e /dev/video0 ] && [ -d /dev/snd ]; then + break + fi + sleep 0.1 + done + + sleep 2 + + mkdir -p $LOG_DIR + + if [ ! -f "$APP" ]; then + echo "rustkvm_app not found!" >>$LOG_DIR/rustkvm_app.log + exit 1 + fi + + stop_app || exit 1 + + /usr/bin/dropbear.sh & + chmod +x $APP + + nohup setsid $APP >$LOG_DIR/rustkvm_app.log 2>&1 & + ;; + +stop) + stop_app + ;; + +restart) + $0 stop || exit 1 + $0 start + ;; + +*) + echo "Usage: $0 {start|stop|restart}" + exit 1 + ;; +esac + +exit 0 diff --git a/os/rk3588/rootfs-overlay/etc/network/interfaces b/os/rk3588/rootfs-overlay/etc/network/interfaces new file mode 100755 index 0000000..40919d6 --- /dev/null +++ b/os/rk3588/rootfs-overlay/etc/network/interfaces @@ -0,0 +1,5 @@ +# RustKVM: dhcpcd (S41dhcpcd) is the only DHCP client. Running ifupdown's +# udhcpc on the same NICs as well gave every port two DHCP clients and +# duplicate metric-less default routes. +auto lo +iface lo inet loopback diff --git a/os/rk3588/rootfs-overlay/usr/bin/dropbear.sh b/os/rk3588/rootfs-overlay/usr/bin/dropbear.sh new file mode 100755 index 0000000..f4c4713 --- /dev/null +++ b/os/rk3588/rootfs-overlay/usr/bin/dropbear.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +if [ -f "/userdata/rustkvm/devmode.enable" ]; then + mkdir -p /userdata/dropbear/etc + mkdir -p /userdata/dropbear/.ssh + ln -sf /userdata/dropbear/etc /etc/dropbear + ln -sf /userdata/dropbear/.ssh /root/.ssh + #TODO: setup syslog instead of -E(log to stderr) + dropbear -R -E -p 0.0.0.0:22 -P /run/dropbear.pid -s + # dropbear -R -E -p 0.0.0.0:22 -P /run/dropbear.pid +else + # Kill Dropbear if it's already running + killall dropbear >/dev/null 2>&1 || true +fi diff --git a/scripts/build-app.sh b/scripts/build-app.sh new file mode 100755 index 0000000..a193991 --- /dev/null +++ b/scripts/build-app.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# Build the RustKVM application package: frontend (Trunk) + device binary. +# +# scripts/build-app.sh # release build into out/app/ +# scripts/devkit.sh run scripts/build-app.sh # same, inside the devkit +# +# Works on the reference workbench (rust-toolchain.toml → stage2) and in the +# devkit container (RUSTUP_TOOLCHAIN → pinned nightly). +set -euo pipefail + +ROOT=$(cd "$(dirname "$0")/.." && pwd) +OUT=${OUT:-$ROOT/out/app} +TARGET=aarch64-unknown-linux-gnu +ASSET=rustkvm_app-$TARGET + +cd "$ROOT" + +echo "==> frontend (trunk build --release --locked)" +( + cd crates/rkvm-web + if [ -n "${RKVM_WEB_TOOLCHAIN:-}" ]; then + export RUSTUP_TOOLCHAIN=$RKVM_WEB_TOOLCHAIN + else + unset RUSTUP_TOOLCHAIN + fi + trunk build --release --locked +) + +echo "==> device binary ($TARGET)" +cargo build -Z build-std=std,panic_abort --locked --release \ + --target "$TARGET" -p rustkvm --bin rustkvm_app + +version=$(sed -n 's/^version *= *"\(.*\)"/\1/p' Cargo.toml | head -n1) +rev=$(git rev-parse --short=12 HEAD 2>/dev/null || echo unknown) +dirty=$(git status --porcelain --untracked-files=no 2>/dev/null | grep -q . && echo "-dirty" || true) + +mkdir -p "$OUT" +cp "target/$TARGET/release/rustkvm_app" "$OUT/$ASSET" +(cd "$OUT" && sha256sum "$ASSET" >"$ASSET.sha256") +cat >"$OUT/BUILD_INFO" < done: $OUT" +cat "$OUT/BUILD_INFO" diff --git a/scripts/build-firmware.sh b/scripts/build-firmware.sh new file mode 100755 index 0000000..32aea16 --- /dev/null +++ b/scripts/build-firmware.sh @@ -0,0 +1,159 @@ +#!/usr/bin/env bash +# Build the RustKVM RK3588 firmware image from public sources. +# +# scripts/build-firmware.sh [STEP...] +# +# Steps (default: all, in this order): +# sync check out the FriendlyElec Buildroot SDK at the commits pinned in +# os/rk3588/manifest.xml (needs `repo`) +# patch reset patched projects, apply os/rk3588/patches, install the rootfs +# overlay, the application binary and the release file +# build kernel + u-boot + Buildroot + SD image + eMMC (eflasher) image +# collect copy versioned images and SHA256SUMS into out/firmware/ +# +# Environment: +# RUSTKVM_SDK_DIR SDK checkout (default ~/rustkvm-sdk/rk3588). Created and owned by +# this script; never point it at a hand-maintained SDK, because +# `patch` resets the patched projects. +# RUSTKVM_APP rustkvm_app binary to install (default out/app/rustkvm_app-*) +# BOARD_MK FriendlyElec board config (default rk3588.mk) +# RUSTKVM_ROOT_PASSWORD +# root password for the serial console, preferably a crypt hash +# (mkpasswd -m sha-512). Unset: root password login is disabled. +# SSH is key-only either way (devmode, dropbear -s). +# REPO_DEPTH clone depth for `repo sync` (default 1; 0 = full history) +# BR2_DL_DIR Buildroot download cache (honoured by Buildroot itself) +# +# Easiest from any Linux host with Docker: scripts/devkit.sh firmware +set -euo pipefail + +ROOT=$(cd "$(dirname "$0")/.." && pwd) +OS=$ROOT/os/rk3588 +SDK=${RUSTKVM_SDK_DIR:-$HOME/rustkvm-sdk/rk3588} +APP=${RUSTKVM_APP:-$ROOT/out/app/rustkvm_app-aarch64-unknown-linux-gnu} +BOARD_MK=${BOARD_MK:-rk3588.mk} +REPO_DEPTH=${REPO_DEPTH:-1} +OUT=$ROOT/out/firmware +MARKER=.rustkvm-sdk + +app_version=$(sed -n 's/^version *= *"\(.*\)"/\1/p' "$ROOT/Cargo.toml" | head -n1) +rev=$(git -C "$ROOT" rev-parse --short=8 HEAD 2>/dev/null || echo unknown) +FW_VERSION=${FW_VERSION:-$app_version-$(date -u +%Y%m%d)-$rev} + +log() { printf '\033[1;32m==> %s\033[0m\n' "$*"; } +die() { printf '\033[1;31merror: %s\033[0m\n' "$*" >&2; exit 1; } + +guard_sdk() { + mkdir -p "$SDK" + if [ -n "$(ls -A "$SDK")" ] && [ ! -e "$SDK/$MARKER" ]; then + die "$SDK is not empty and was not created by this script; use another RUSTKVM_SDK_DIR" + fi + touch "$SDK/$MARKER" +} + +patched_projects() { + (cd "$OS/patches" && find . -name '*.patch' -printf '%h\n' | sed 's|^\./||' | sort -u) +} + +step_sync() { + command -v repo >/dev/null || die "repo not found (https://gerrit.googlesource.com/git-repo)" + guard_sdk + log "sync SDK into $SDK" + # repo wants a manifest git repository; serve the pinned manifest from a local one. + local mf=$SDK/.rustkvm-manifest + rm -rf "$mf" + git init -q -b rustkvm "$mf" + cp "$OS/manifest.xml" "$mf/default.xml" + git -C "$mf" add default.xml + git -C "$mf" -c user.name=rustkvm -c user.email=build@localhost -c commit.gpgsign=false \ + commit -q -m "pinned manifest" + + # Discard earlier patches so repo can move projects to the pinned commits. + if [ -d "$SDK/.repo" ]; then + while read -r p; do + [ -d "$SDK/$p/.git" ] || continue + git -C "$SDK/$p" reset -q --hard + git -C "$SDK/$p" clean -fdq + done < <(patched_projects) + fi + + cd "$SDK" + local depth=() + [ "$REPO_DEPTH" -gt 0 ] && depth=(--depth="$REPO_DEPTH") + repo init -q -u "file://$mf" -b rustkvm -m default.xml "${depth[@]}" + repo sync -q -c -d --no-tags --force-sync -j"$(nproc)" +} + +step_patch() { + [ -e "$SDK/$MARKER" ] || die "$SDK has no SDK checkout; run the sync step first" + log "apply patches" + while read -r p; do + git -C "$SDK/$p" reset -q --hard + git -C "$SDK/$p" clean -fdq + for f in "$OS/patches/$p"/*.patch; do + echo " $p: $(basename "$f")" + git -C "$SDK/$p" apply --whitespace=nowarn "$f" + done + done < <(patched_projects) + + local defconfig=$SDK/buildroot/configs/rockchip_rk3588_defconfig + sed -i '/^BR2_TARGET_GENERIC_ROOT_PASSWD=/d;/BR2_TARGET_ENABLE_ROOT_LOGIN/d' "$defconfig" + if [ -n "${RUSTKVM_ROOT_PASSWORD:-}" ]; then + log "root password login enabled" + # Buildroot reads the value through make: a crypt hash needs its '$' doubled. + printf 'BR2_TARGET_GENERIC_ROOT_PASSWD="%s"\n' "${RUSTKVM_ROOT_PASSWORD//\$/\$\$}" >>"$defconfig" + else + log "root password login disabled (set RUSTKVM_ROOT_PASSWORD to enable)" + echo '# BR2_TARGET_ENABLE_ROOT_LOGIN is not set' >>"$defconfig" + fi + + log "install rootfs overlay ($FW_VERSION)" + local files=$SDK/device/friendlyelec/rustkvm-files + rm -rf "$files" + cp -a "$OS/rootfs-overlay" "$files" + cat >"$files/etc/rustkvm-release" </dev/null | head -n1) || true + eflasher=$(ls -t "$SDK"/out/*_eflasher.img 2>/dev/null | head -n1) || true + [ -n "$sd" ] || die "no SD image under $SDK/out" + local name=rustkvm-rk3588-$FW_VERSION + gzip -c "$sd" >"$OUT/$name-sd.img.gz" + [ -n "$eflasher" ] && gzip -c "$eflasher" >"$OUT/$name-eflasher.img.gz" + cp "$SDK/device/friendlyelec/rustkvm-files/etc/rustkvm-release" "$OUT/$name.release" + (cd "$OUT" && sha256sum "$name"-* "$name.release" >"$name.SHA256SUMS") + ls -lh "$OUT"/"$name"* +} + +steps=("$@") +[ ${#steps[@]} -eq 0 ] && steps=(sync patch build collect) +[ "${steps[0]}" = all ] && steps=(sync patch build collect) +for s in "${steps[@]}"; do + case "$s" in + sync | patch | build | collect) "step_$s" ;; + *) die "unknown step: $s (sync, patch, build, collect, all)" ;; + esac +done diff --git a/scripts/devkit.sh b/scripts/devkit.sh new file mode 100755 index 0000000..62248b4 --- /dev/null +++ b/scripts/devkit.sh @@ -0,0 +1,119 @@ +#!/usr/bin/env bash +# Containerised build environment for RustKVM. Only Docker is required on the host. +# +# scripts/devkit.sh buildkit [DIR] build the RK3588 toolchain image from a Buildroot +# host dir (default /opt/rk3588-buildkit) +# scripts/devkit.sh image [--if-missing] +# build the devkit image (frontend + device binary); +# --if-missing pulls the published one first +# scripts/devkit.sh push publish the buildkit and devkit images (maintainers) +# scripts/devkit.sh shell interactive shell in the devkit, repo at /work +# scripts/devkit.sh run CMD... run CMD in the devkit, e.g. scripts/build-app.sh +# scripts/devkit.sh firmware-image build the Buildroot SDK host image +# scripts/devkit.sh firmware ARGS... run scripts/build-firmware.sh ARGS in that image +# +# Images (published copies are pulled; override to use your own): +# RUSTKVM_BUILDKIT_IMAGE default ghcr.io/rust-kvm/rk3588-buildkit:20250527 +# RUSTKVM_DEVKIT_IMAGE default ghcr.io/rust-kvm/rustkvm-devkit:nightly-2026-06-14 +# RUSTKVM_FIRMWARE_IMAGE default rustkvm/firmware-builder:local +# Caches (cargo registry, trunk tools, Buildroot downloads, ccache): +# RUSTKVM_CACHE_DIR default ~/.cache/rustkvm +# RUSTKVM_SDK_DIR default ~/rustkvm-sdk/rk3588 (firmware SDK checkout, ~60 GB) +set -euo pipefail + +ROOT=$(cd "$(dirname "$0")/.." && pwd) +# Bump the tags together with the SDK snapshot / RUST_NIGHTLY in ci/devkit/Dockerfile. +BUILDKIT_IMAGE=${RUSTKVM_BUILDKIT_IMAGE:-ghcr.io/rust-kvm/rk3588-buildkit:20250527} +DEVKIT_IMAGE=${RUSTKVM_DEVKIT_IMAGE:-ghcr.io/rust-kvm/rustkvm-devkit:nightly-2026-06-14} +FIRMWARE_IMAGE=${RUSTKVM_FIRMWARE_IMAGE:-rustkvm/firmware-builder:local} +CACHE=${RUSTKVM_CACHE_DIR:-$HOME/.cache/rustkvm} +SDK=${RUSTKVM_SDK_DIR:-$HOME/rustkvm-sdk/rk3588} + +have_image() { docker image inspect "$1" >/dev/null 2>&1; } + +TTY=(-i) +if [ -t 0 ] && [ -t 1 ]; then TTY=(-it); fi + +# Forward the host's proxy settings into builds and containers (host network, +# so a proxy on 127.0.0.1 keeps working). +BUILD_PROXY=(--network host) +RUN_PROXY=() +for v in http_proxy https_proxy no_proxy HTTP_PROXY HTTPS_PROXY NO_PROXY; do + if [ -n "${!v:-}" ]; then + BUILD_PROXY+=(--build-arg "$v") + RUN_PROXY+=(-e "$v") + fi +done + +cmd_buildkit() { + local dir=${1:-/opt/rk3588-buildkit} + if [ ! -x "$dir/bin/aarch64-buildroot-linux-gnu-gcc" ]; then + echo "error: $dir is not a Buildroot host dir (no bin/aarch64-buildroot-linux-gnu-gcc)" >&2 + echo "Build the firmware once (scripts/devkit.sh firmware) and pass" >&2 + echo " \$RUSTKVM_SDK_DIR/buildroot/output/rockchip_rk3588/host" >&2 + exit 1 + fi + docker buildx build --load "${BUILD_PROXY[@]}" --build-context "rk3588-buildkit=$dir" \ + -t "$BUILDKIT_IMAGE" "$ROOT/ci/buildkit" +} + +cmd_image() { + if [ "${1:-}" = "--if-missing" ]; then + have_image "$DEVKIT_IMAGE" && return 0 + docker pull "$DEVKIT_IMAGE" && return 0 + echo "==> $DEVKIT_IMAGE not available; building it locally" + fi + if ! have_image "$BUILDKIT_IMAGE" && ! docker pull "$BUILDKIT_IMAGE" 2>/dev/null; then + echo "==> $BUILDKIT_IMAGE not found; building it from /opt/rk3588-buildkit" + cmd_buildkit + fi + docker build "${BUILD_PROXY[@]}" --build-arg "BUILDKIT_IMAGE=$BUILDKIT_IMAGE" \ + -t "$DEVKIT_IMAGE" "$ROOT/ci/devkit" +} + +devkit_run() { + cmd_image --if-missing + mkdir -p "$CACHE/home" "$CACHE/cargo" + docker run --rm "${TTY[@]}" \ + --user "$(id -u):$(id -g)" \ + -e HOME=/cache/home -e CARGO_HOME=/cache/cargo \ + -v "$ROOT:/work" -v "$CACHE:/cache" -w /work \ + --network host "${RUN_PROXY[@]}" \ + "$DEVKIT_IMAGE" "$@" +} + +cmd_push() { + have_image "$BUILDKIT_IMAGE" || cmd_buildkit + have_image "$DEVKIT_IMAGE" || cmd_image + docker push "$BUILDKIT_IMAGE" + docker push "$DEVKIT_IMAGE" +} + +cmd_firmware_image() { + docker build "${BUILD_PROXY[@]}" --build-arg "UID=$(id -u)" --build-arg "GID=$(id -g)" \ + -t "$FIRMWARE_IMAGE" "$ROOT/ci/firmware" +} + +cmd_firmware() { + have_image "$FIRMWARE_IMAGE" || cmd_firmware_image + mkdir -p "$SDK" "$CACHE/dl" "$CACHE/ccache" + # --privileged: the SDK packs SD/eMMC images through loop devices. + docker run --rm "${TTY[@]}" --privileged \ + -e RUSTKVM_SDK_DIR=/sdk -e BR2_DL_DIR=/cache/dl \ + -e RUSTKVM_ROOT_PASSWORD -e FW_VERSION -e BOARD_MK -e REPO_DEPTH \ + -v "$ROOT:/work" -v "$SDK:/sdk" -v "$CACHE:/cache" \ + -v "$CACHE/ccache:/home/builder/.buildroot-ccache" \ + -w /work --network host "${RUN_PROXY[@]}" \ + "$FIRMWARE_IMAGE" scripts/build-firmware.sh "$@" +} + +case "${1:-}" in + buildkit) shift; cmd_buildkit "$@" ;; + image) shift; cmd_image "$@" ;; + push) cmd_push ;; + shell) devkit_run bash ;; + run) shift; devkit_run "$@" ;; + firmware-image) cmd_firmware_image ;; + firmware) shift; cmd_firmware "$@" ;; + *) sed -n '2,23p' "$0" | sed 's/^# \{0,1\}//'; exit 1 ;; +esac