diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..3857b77 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,25 @@ +{ + "image": "mcr.microsoft.com/devcontainers/javascript-node:1-20-bookworm", + "customizations": { + "vscode": { + "settings": { + "json.schemas": [ + { + "fileMatch": [ + "*/devcontainer-feature.json" + ], + "url": "https://raw.githubusercontent.com/devcontainers/spec/main/schemas/devContainerFeature.schema.json" + } + ] + }, + "extensions": [ + "mads-hartmann.bash-ide-vscode" + ] + } + }, + "features": { + "ghcr.io/devcontainers/features/docker-in-docker:2": {} + }, + "remoteUser": "node", + "updateContentCommand": "npm install -g @devcontainers/cli" +} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..2afc657 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,47 @@ +name: "Release dev container features & Generate Documentation" +on: + workflow_dispatch: + +jobs: + deploy: + if: ${{ github.ref == 'refs/heads/main' }} + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + packages: write + steps: + - uses: actions/checkout@v4 + + - name: "Publish Features" + uses: devcontainers/action@v1 + with: + publish-features: "true" + base-path-to-features: "./src" + generate-docs: "true" + + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Create PR for Documentation + id: push_image_info + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -e + echo "Start." + # Configure git and Push updates + git config --global user.email github-actions[bot]@users.noreply.github.com + git config --global user.name github-actions[bot] + git config pull.rebase false + branch=automated-documentation-update-$GITHUB_RUN_ID + git checkout -b $branch + message='Automated documentation update' + # Add / update and commit + git add */**/README.md + git commit -m 'Automated documentation update [skip ci]' || export NO_UPDATES=true + # Push + if [ "$NO_UPDATES" != "true" ] ; then + git push origin "$branch" + gh pr create --title "$message" --body "$message" + fi diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..f852a12 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,58 @@ +name: "CI - Test Features" +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +jobs: + test-autogenerated: + runs-on: ubuntu-latest + continue-on-error: true + strategy: + matrix: + features: + - color + - hello + baseImage: + - debian:latest + - ubuntu:latest + - mcr.microsoft.com/devcontainers/base:ubuntu + steps: + - uses: actions/checkout@v4 + + - name: "Install latest devcontainer CLI" + run: npm install -g @devcontainers/cli + + - name: "Generating tests for '${{ matrix.features }}' against '${{ matrix.baseImage }}'" + run: devcontainer features test --skip-scenarios -f ${{ matrix.features }} -i ${{ matrix.baseImage }} . + + test-scenarios: + runs-on: ubuntu-latest + continue-on-error: true + strategy: + matrix: + features: + - color + - hello + steps: + - uses: actions/checkout@v4 + + - name: "Install latest devcontainer CLI" + run: npm install -g @devcontainers/cli + + - name: "Generating tests for '${{ matrix.features }}' scenarios" + run: devcontainer features test -f ${{ matrix.features }} --skip-autogenerated --skip-duplicated . + + test-global: + runs-on: ubuntu-latest + continue-on-error: true + steps: + - uses: actions/checkout@v4 + + - name: "Install latest devcontainer CLI" + run: npm install -g @devcontainers/cli + + - name: "Testing global scenarios" + run: devcontainer features test --global-scenarios-only . diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..da2fc60 --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,16 @@ +name: "Validate devcontainer-feature.json files" +on: + workflow_dispatch: + pull_request: + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: "Validate devcontainer-feature.json files" + uses: devcontainers/action@v1 + with: + validate-only: "true" + base-path-to-features: "./src" diff --git a/LICENSE b/LICENSE index c47b2fe..dfcc56c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,21 @@ -MIT License - -Copyright (c) 2025 kbrd-space - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +MIT License + +Copyright (c) 2025 kbrd-space + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2234a59 --- /dev/null +++ b/README.md @@ -0,0 +1,128 @@ +# Dev Container Features: ZMK Feature Template + +> This repo provides a starting point and example for creating custom dev container Features, hosted for free on GitHub Container Registry. The example in this repository follows the [dev container Feature distribution specification](https://containers.dev/implementors/features-distribution/). +). + +## Example Contents + +This repository contains a single Feature: `zmk`. This Feature serves as a template for implementing your own dev container Feature. Below is a sample `devcontainer.json` alongside example usage of the Feature. + +### `zmk` + +Running `zmk` inside the built container will execute the logic provided in its `install.sh` script. You can customize its behavior via options defined in its `devcontainer-feature.json`. + +```jsonc +{ + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "ghcr.io///zmk:0": { + // Add options here if defined in devcontainer-feature.json + } + } +} +``` + +## Repo and Feature Structure + +This repository has a `src` folder. Each Feature has its own sub-folder, containing at least a `devcontainer-feature.json` and an entrypoint script `install.sh`. + +``` +├── src +│ └── zmk +│ ├── devcontainer-feature.json +│ └── install.sh +``` + +An [implementing tool](https://containers.dev/supporting#tools) will composite [the documented dev container properties](https://containers.dev/implementors/features/#devcontainer-feature-json-properties) from the feature's `devcontainer-feature.json` file, and execute the `install.sh` entrypoint script in the container during build time. Implementing tools are also free to process attributes under the `customizations` property as desired. + +### Options + +All available options for a Feature should be declared in the `devcontainer-feature.json`. The syntax for the `options` property can be found in the [devcontainer Feature json properties reference](https://containers.dev/implementors/features/#devcontainer-feature-json-properties). + +Options are exported as Feature-scoped environment variables. The option name is capitalized and sanitized according to [option resolution](https://containers.dev/implementors/features/#option-resolution). + +```bash +#!/bin/bash + +echo "Activating feature 'zmk'" +echo "The provided option is: ${OPTION_NAME}" + +... +``` + +## Distributing Features + +### Versioning + +Features are individually versioned by the `version` attribute in a Feature's `devcontainer-feature.json`. Features are versioned according to the semver specification. More details can be found in [the dev container Feature specification](https://containers.dev/implementors/features/#versioning). + +### Publishing + +> NOTE: The Distribution spec can be [found here](https://containers.dev/implementors/features-distribution/). +> +> While any registry [implementing the OCI Distribution spec](https://github.com/opencontainers/distribution-spec) can be used, this template will leverage GHCR (GitHub Container Registry) as the backing registry. + +Features are meant to be easily sharable units of dev container configuration and installation code. + +This repo contains a **GitHub Action** [workflow](.github/workflows/release.yaml) that will publish each Feature to GHCR. + +*Allow GitHub Actions to create and approve pull requests* should be enabled in the repository's `Settings > Actions > General > Workflow permissions` for auto generation of `src//README.md` per Feature (which merges any existing `src//NOTES.md`). + +By default, each Feature will be prefixed with the `/` namespace. For example, the Feature in this repository can be referenced in a `devcontainer.json` with: + +``` +ghcr.io///zmk:1 +``` + +The provided GitHub Action will also publish a second "metadata" package with just the namespace, eg: `ghcr.io//`. This contains information useful for tools aiding in Feature discovery. + +### Marking Feature Public + +Note that by default, GHCR packages are marked as `private`. To stay within the free tier, Features need to be marked as `public`. + +This can be done by navigating to the Feature's "package settings" page in GHCR, and setting the visibility to 'public`. The URL may look something like: + +``` +https://github.com/users//packages/container/%2F/settings +``` + +### Adding Features to the Index + +If you'd like your Feature to appear in the [public index](https://containers.dev/features) so that other community members can find it, you can do the following: + +* Go to [github.com/devcontainers/devcontainers.github.io](https://github.com/devcontainers/devcontainers.github.io) + * This is the GitHub repo backing the [containers.dev](https://containers.dev/) spec site +* Open a PR to modify the [collection-index.yml](https://github.com/devcontainers/devcontainers.github.io/blob/gh-pages/_data/collection-index.yml) file + +This index is from where [supporting tools](https://containers.dev/supporting) like [VS Code Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) and [GitHub Codespaces](https://github.com/features/codespaces) surface Features for their dev container creation UI. + +#### Using private Features in Codespaces + +For any Features hosted in GHCR that are kept private, the `GITHUB_TOKEN` access token in your environment will need to have `package:read` and `contents:read` for the associated repository. + +Many implementing tools use a broadly scoped access token and will work automatically. GitHub Codespaces uses repo-scoped tokens, and therefore you'll need to add the permissions in `devcontainer.json` + +An example `devcontainer.json` can be found below. + +```jsonc +{ + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "ghcr.io///zmk:1": { + // Add options here if defined + } + }, + "customizations": { + "codespaces": { + "repositories": { + "/": { + "permissions": { + "packages": "read", + "contents": "read" + } + } + } + } + } +} +``` diff --git a/src/zmk/devcontainer-feature.json b/src/zmk/devcontainer-feature.json new file mode 100644 index 0000000..5e6262a --- /dev/null +++ b/src/zmk/devcontainer-feature.json @@ -0,0 +1,54 @@ +{ + "name": "ZMK", + "id": "zmk", + "version": "0.0.1", + "description": "ZMK Dev environment", + "options": { + "zephyr_git_org": { + "type": "string", + "proposals": [ + "zephyrproject-rtos", + "zmkfirmware" + ], + "default": "zmkfirmware", + "description": "Select or enter a Zephyr git organization (e.g. 'zephyrproject-rtos')." + }, + "zephyr_git_reponame": { + "type": "string", + "proposals": [ + "zephyr" + ], + "default": "zephyr", + "description": "Select or enter a Zephyr git repository name (e.g. 'zephyr')." + }, + "zephyr_git_revision": { + "type": "string", + "proposals": [ + "main", + "zephyr-v3.5.0", + "zmk-v2.3.0-with-fixes" + ], + "default": "zmk-v2.3.0-with-fixes", + "description": "Select or enter a Zephyr git revision (e.g. 'main', 'zephyr-v3.5.0', or 'zmk-v2.3.0-with-fixes')." + }, + "zephyr_sdk_version": { + "type": "string", + "proposals": [ + "0.17.1" + ], + "default": "0.17.1", + "description": "Select or enter a Zephyr git revision (e.g. '0.17.1')." + }, + "architecture": { + "type": "string", + "proposals": [ + "arm" + ], + "default": "arm", + "description": "Select or enter an architecture (e.g. 'arm')." + } + }, + "installsAfter": [ + "ghcr.io/devcontainers/features/common-utils" + ] +} diff --git a/src/zmk/install.sh b/src/zmk/install.sh new file mode 100644 index 0000000..6e819c9 --- /dev/null +++ b/src/zmk/install.sh @@ -0,0 +1,131 @@ +#!/bin/sh +set -eux +export DEBIAN_FRONTEND=noninteractive + +# Define variables +ZEPHYR_GIT_ORG="${ZEPHYR_GIT_ORG:-zephyrproject-rtos}" +ZEPHYR_GIT_REPONAME="${ZEPHYR_GIT_REPONAME:-zephyr}" +ZEPHYR_GIT_REVISION="${ZEPHYR_GIT_REVISION:-main}" +ZEPHYR_SDK_VERSION="${ZEPHYR_SDK_VERSION:-0.17.1}" +ZEPHYR_ARCHITECTURE="${ARCHITECTURE:-arm}" + +if [ "$(id -u)" -ne 0 ]; then + echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' + exit 1 +fi + +architecture="$(uname -m)" +if [ "${architecture}" != "x86_64" ]; then + echo "(!) Architecture $architecture unsupported" + exit 1 +fi + +check_packages() { + if ! dpkg -s "$@" > /dev/null 2>&1; then + if [ "$(find /var/lib/apt/lists/* | wc -l)" = "0" ]; then + echo "Running apt-get update..." + apt-get update -y + fi + apt-get -y install --no-install-recommends "$@" + fi +} + +# Install base utilities +check_packages ca-certificates curl + +# Install Zephyr dependencies +if [ "$(uname -m)" = "x86_64" ]; then + gcc_multilib="gcc-multilib"; +else + gcc_multilib=""; +fi +check_packages \ + ccache \ + dfu-util \ + device-tree-compiler \ + file \ + gcc \ + g++ \ + "${gcc_multilib}" \ + git \ + gperf \ + make \ + protobuf-compiler \ + ninja-build \ + python3 \ + python3-dev \ + python3-pip \ + python3-setuptools \ + python3-wheel \ + ssh + +# Install python-related dependencies for Zephyr +PIP_BREAK_SYSTEM_PACKAGES=1 pip3 install \ + -r "https://raw.githubusercontent.com/${ZEPHYR_GIT_ORG}/${ZEPHYR_GIT_REPONAME}/refs/heads/${ZEPHYR_GIT_REVISION}/scripts/requirements-base.txt" +PIP_BREAK_SYSTEM_PACKAGES=1 pip3 install cmake protobuf~=4.25 grpcio-tools + +export LC_ALL=C +export PAGER=less + +# Install Node source +check_packages \ + gnupg \ + && mkdir -p /etc/apt/keyrings \ + && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \ + && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_18.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list + +check_packages \ + clang-format \ + gdb \ + gpg \ + gpg-agent \ + less \ + libsdl2-dev \ + locales \ + nano \ + nodejs \ + python3 \ + python3-dev \ + python3-pip \ + python3-setuptools \ + python3-tk \ + python3-wheel \ + socat \ + tio \ + wget \ + xz-utils + +PIP_BREAK_SYSTEM_PACKAGES=1 pip3 install \ + -r https://raw.githubusercontent.com/${ZEPHYR_GIT_ORG}/${ZEPHYR_GIT_REPONAME}/refs/heads/${ZEPHYR_GIT_REVISION}/scripts/requirements-build-test.txt \ + -r https://raw.githubusercontent.com/${ZEPHYR_GIT_ORG}/${ZEPHYR_GIT_REPONAME}/refs/heads/${ZEPHYR_GIT_REVISION}/scripts/requirements-run-test.txt +# ENV ZEPHYR_SDK_VERSION=${ZEPHYR_SDK_VERSION} + +# Install Zephyr SDK +export minimal_sdk_file_name="zephyr-sdk-${ZEPHYR_SDK_VERSION}_linux-$(uname -m)_minimal" \ + && if [ "${ZEPHYR_ARCHITECTURE}" = "arm" ]; then arch_format="eabi"; else arch_format="elf"; fi \ + && if [ "${ZEPHYR_ARCHITECTURE#xtensa}" = "${ZEPHYR_ARCHITECTURE}" ]; then arch_sep="-"; else arch_sep="_"; fi + +check_packages \ + wget \ + xz-utils + +mkdir -p /tmp/zephyr-sdk && TMP=$(mktemp -d -p /tmp/zephyr-sdk) +(cd ${TMP} \ + && wget -q "https://github.com/zephyrproject-rtos/sdk-ng/releases/download/v${ZEPHYR_SDK_VERSION}/${minimal_sdk_file_name}.tar.xz" \ + && tar xvfJ ${minimal_sdk_file_name}.tar.xz \ + && mv zephyr-sdk-${ZEPHYR_SDK_VERSION} /opt/ \ + && rm ${minimal_sdk_file_name}.tar.xz) + +(cd /opt/zephyr-sdk-${ZEPHYR_SDK_VERSION} \ + && ./setup.sh -h -c -t ${ZEPHYR_ARCHITECTURE}${arch_sep}zephyr-${arch_format}) + +# Clean up +apt-get remove -y --purge \ + g++ \ + python3-dev \ + python3-pip \ + python3-setuptools \ + python3-wheel \ + xz-utils +apt-get clean \ + && rm -rf /var/lib/apt/lists/* diff --git a/test/zmk/scenarios.json b/test/zmk/scenarios.json new file mode 100644 index 0000000..eb0caf2 --- /dev/null +++ b/test/zmk/scenarios.json @@ -0,0 +1,8 @@ +{ + "zephyrproject": { + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "zmk": {} + } + } +} diff --git a/test/zmk/test.sh b/test/zmk/test.sh new file mode 100644 index 0000000..a28097e --- /dev/null +++ b/test/zmk/test.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +set -e + +source dev-container-features-test-lib + +check "cmake installed" cmake --version +check "python3 installed" python3 --version +check "dtc installed" dtc --version +check "gcc installed" gcc --version +check "node installed" node --version +check "git installed" git --version +check "west installed" west --version + +reportResults diff --git a/test/zmk/zephyrproject.sh b/test/zmk/zephyrproject.sh new file mode 100644 index 0000000..f0f21b4 --- /dev/null +++ b/test/zmk/zephyrproject.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +set -e + +source dev-container-features-test-lib + +TMP_DIR="/tmp/zephyrproject" +mkdir -p "${TMP_DIR}" + +(cd "${TMP_DIR}" && check "west init" west init) +(cd "${TMP_DIR}" && check "west update" west update --fetch-opt=--filter=tree:0) +(cd "${TMP_DIR}" && check "west zephyr export" west zephyr-export) +(cd "${TMP_DIR}/zephyr" && check "west build" west build -p always -b nrf52840_mdk samples/basic/blinky) + +reportResults