Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Git files
.git
.gitignore
.gitattributes

# GitHub files
.github

# Documentation
README.md
LICENCE

# Editor configs
.editorconfig
.vscode
21 changes: 21 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# http://editorconfig.org
# VS2022: https://marketplace.visualstudio.com/items?itemName=MadsKristensen.TrailingWhitespace64
# VSCode: https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig

# top-most EditorConfig file
root = true

# default settings:
[*]
indent_style = space
indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true

# Markdown files need trailing whitespace for line breaks
[*.md]
trim_trailing_whitespace = false

# all Shell scripts must use Linux line endings
[*.sh]
end_of_line = lf
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# automatically normalize line endings to the user's GIT preferences
* text=auto
*.sh text eol=lf
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* @MatejGomboc
25 changes: 25 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
time: "00:00"
labels:
- "dependencies"
- "github-actions"
commit-message:
prefix: "ci"
include: "scope"

- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "daily"
time: "00:00"
labels:
- "dependencies"
- "docker"
commit-message:
prefix: "docker"
include: "scope"
65 changes: 65 additions & 0 deletions .github/workflows/ci_pr_validation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
name: PR Validation

on:
pull_request:
branches: ["**"] # Run PR validation on any branch.

workflow_dispatch: # Manual trigger option.

jobs:
verify_pr:
runs-on: ubuntu-latest
timeout-minutes: 15

permissions:
contents: read
security-events: write

steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6

- name: Lint Dockerfile
uses: hadolint/hadolint-action@2332a7b74a6de0dda2e2221d575162eba76ba5e5 # v3.3.0
with:
dockerfile: Dockerfile
failure-threshold: error

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

- name: Build Docker image (no push, just build)
id: build
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
with:
context: .
platforms: linux/amd64
push: false # Never push during PR validation.
load: true # Load image locally for scanning.
tags: ci-pr-validation:${{ github.event.pull_request.number || github.run_number }}
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: false # Disabled implicit provenance.

- name: Run Trivy vulnerability scanner (table output)
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
image-ref: ci-pr-validation:${{ github.event.pull_request.number || github.run_number }}
format: 'table'
severity: 'CRITICAL,HIGH,MEDIUM,LOW'
exit-code: '0' # Don't fail the build on vulnerabilities, just report the findings.

- name: Run Trivy vulnerability scanner (SARIF output)
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
image-ref: ci-pr-validation:${{ github.event.pull_request.number || github.run_number }}
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH,MEDIUM,LOW'
exit-code: '0' # Don't fail the build on vulnerabilities, just report the findings.

- name: Upload Trivy results to GitHub Security tab
uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
if: always()
with:
sarif_file: 'trivy-results.sarif'
40 changes: 40 additions & 0 deletions .github/workflows/scheduled_build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: Scheduled Build

on:
schedule:
- cron: '0 0 * * 6' # Weekly on Saturday 00:00 UTC, to catch upstream drift.

workflow_dispatch: # Manual trigger option.

jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 20

permissions:
contents: read

steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6

- name: Lint Dockerfile
uses: hadolint/hadolint-action@2332a7b74a6de0dda2e2221d575162eba76ba5e5 # v3.3.0
with:
dockerfile: Dockerfile
failure-threshold: error

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

# No build cache: the point of this scheduled run is to actually
# re-download the pinned external artifacts (ARM toolchain, CMake,
# Ninja, Task) so we notice if an upstream URL or release disappears.
- name: Build Docker image (no cache, no push)
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
with:
context: .
platforms: linux/amd64
push: false
no-cache: true
provenance: false # Disabled implicit provenance.
11 changes: 11 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"recommendations": [
"davidanson.vscode-markdownlint",
"donjayamanne.githistory",
"editorconfig.editorconfig",
"felipecaputo.git-project-manager",
"github.vscode-github-actions",
"ms-vscode.notepadplusplus-keybindings",
"redhat.vscode-yaml"
]
}
14 changes: 14 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"editor.insertSpaces": true,
"editor.tabSize": 4,
"editor.trimAutoWhitespace": true,
"files.trimTrailingWhitespace": true,
"files.insertFinalNewline": true,
"editor.renderWhitespace": "boundary",
"[markdown]": {
"files.trimTrailingWhitespace": false
},
"[shellscript]": {
"files.eol": "\n"
}
}
126 changes: 126 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
FROM debian:13-slim AS downloader

ARG NINJA_VERSION=v1.13.1
ARG CMAKE_VERSION=v4.1.1
ARG ARM_NONE_EABI_VERSION=14.3.rel1
ARG TASK_VERSION=v3.45.5

# hadolint ignore=DL3002
USER root:root

WORKDIR /root

SHELL ["/bin/bash", "-o", "pipefail", "-c"]

# hadolint ignore=DL3008,DL4001
RUN apt-get update && \
apt-get install -y --no-install-recommends ca-certificates wget unzip tar xz-utils curl jq && \
\
NINJA_FILE="ninja-linux.zip" && \
NINJA_API_RESPONSE=$(curl -sSL "https://api.github.com/repos/ninja-build/ninja/releases/tags/${NINJA_VERSION}") && \
NINJA_DIGEST=$(echo "${NINJA_API_RESPONSE}" | jq -r ".assets[] | select(.name == \"${NINJA_FILE}\") | .digest") && \
if [ -z "${NINJA_DIGEST}" ] || [ "${NINJA_DIGEST}" = "null" ]; then \
echo "ERROR: Failed to get Ninja digest from GitHub API"; \
echo "API Response: ${NINJA_API_RESPONSE}"; \
exit 1; \
fi && \
if [[ ! "${NINJA_DIGEST}" =~ ^sha256: ]]; then \
echo "ERROR: Unexpected Ninja digest format: ${NINJA_DIGEST}"; \
echo "Expected format: sha256:HASH"; \
exit 1; \
fi && \
NINJA_HASH=$(echo "${NINJA_DIGEST}" | cut -d: -f2) && \
wget -q "https://github.com/ninja-build/ninja/releases/download/${NINJA_VERSION}/${NINJA_FILE}" && \
echo "${NINJA_HASH} ${NINJA_FILE}" | sha256sum --check && \
unzip -q "${NINJA_FILE}" -d /usr/local/bin && \
rm "${NINJA_FILE}" && \
\
CMAKE_VERSION_NUM="${CMAKE_VERSION#v}" && \
CMAKE_FILE="cmake-${CMAKE_VERSION_NUM}-linux-x86_64.tar.gz" && \
CMAKE_API_RESPONSE=$(curl -sSL "https://api.github.com/repos/Kitware/CMake/releases/tags/${CMAKE_VERSION}") && \
CMAKE_DIGEST=$(echo "${CMAKE_API_RESPONSE}" | jq -r ".assets[] | select(.name == \"${CMAKE_FILE}\") | .digest") && \
if [ -z "${CMAKE_DIGEST}" ] || [ "${CMAKE_DIGEST}" = "null" ]; then \
echo "ERROR: Failed to get CMake digest from GitHub API"; \
echo "API Response: ${CMAKE_API_RESPONSE}"; \
exit 1; \
fi && \
if [[ ! "${CMAKE_DIGEST}" =~ ^sha256: ]]; then \
echo "ERROR: Unexpected CMake digest format: ${CMAKE_DIGEST}"; \
echo "Expected format: sha256:HASH"; \
exit 1; \
fi && \
CMAKE_HASH=$(echo "${CMAKE_DIGEST}" | cut -d: -f2) && \
wget -q "https://github.com/Kitware/CMake/releases/download/${CMAKE_VERSION}/${CMAKE_FILE}" && \
echo "${CMAKE_HASH} ${CMAKE_FILE}" | sha256sum --check && \
mkdir -p /opt/cmake && \
tar -xzf "${CMAKE_FILE}" -C /opt/cmake --strip-components=1 && \
rm "${CMAKE_FILE}" && \
\
ARM_NONE_EABI_FILE="arm-gnu-toolchain-${ARM_NONE_EABI_VERSION}-x86_64-arm-none-eabi.tar.xz" && \
ARM_NONE_EABI_HASH_FILE="${ARM_NONE_EABI_FILE}.sha256asc" && \
wget -q "https://developer.arm.com/-/media/Files/downloads/gnu/${ARM_NONE_EABI_VERSION}/binrel/${ARM_NONE_EABI_FILE}" && \
wget -q "https://developer.arm.com/-/media/Files/downloads/gnu/${ARM_NONE_EABI_VERSION}/binrel/${ARM_NONE_EABI_HASH_FILE}" && \
sha256sum --check "${ARM_NONE_EABI_HASH_FILE}" && \
mkdir -p /opt/arm-none-eabi-gcc && \
tar -xf "${ARM_NONE_EABI_FILE}" -C /opt/arm-none-eabi-gcc --strip-components=1 && \
rm "${ARM_NONE_EABI_FILE}" "${ARM_NONE_EABI_HASH_FILE}" && \
\
TASK_FILE="task_linux_amd64.tar.gz" && \
TASK_API_RESPONSE=$(curl -sSL "https://api.github.com/repos/go-task/task/releases/tags/${TASK_VERSION}") && \
TASK_DIGEST=$(echo "${TASK_API_RESPONSE}" | jq -r ".assets[] | select(.name == \"${TASK_FILE}\") | .digest") && \
if [ -z "${TASK_DIGEST}" ] || [ "${TASK_DIGEST}" = "null" ]; then \
echo "ERROR: Failed to get Task digest from GitHub API"; \
echo "API Response: ${TASK_API_RESPONSE}"; \
exit 1; \
fi && \
if [[ ! "${TASK_DIGEST}" =~ ^sha256: ]]; then \
echo "ERROR: Unexpected Task digest format: ${TASK_DIGEST}"; \
echo "Expected format: sha256:HASH"; \
exit 1; \
fi && \
TASK_HASH=$(echo "${TASK_DIGEST}" | cut -d: -f2) && \
wget -q "https://github.com/go-task/task/releases/download/${TASK_VERSION}/${TASK_FILE}" && \
echo "${TASK_HASH} ${TASK_FILE}" | sha256sum --check && \
tar -xzf "${TASK_FILE}" -C /usr/local/bin task && \
rm "${TASK_FILE}" && \
chmod +x /usr/local/bin/task

FROM debian:13-slim

LABEL org.opencontainers.image.title="ARM Embedded Development Environment" \
org.opencontainers.image.description="Development environment for building and testing the Embedded Society's ARM Cortex-M libraries" \
org.opencontainers.image.authors="The Embedded Society" \
org.opencontainers.image.source="https://github.com/embedded-society/arm-dev-env" \
org.opencontainers.image.vendor="https://github.com/embedded-society" \
org.opencontainers.image.licenses="GPL-3.0"

# hadolint ignore=DL3002
USER root:root

WORKDIR /root

ENV PATH="/opt/arm-none-eabi-gcc/bin:/opt/cmake/bin:${PATH}"

COPY --from=downloader /usr/local/bin/ninja /usr/local/bin/
COPY --from=downloader /usr/local/bin/task /usr/local/bin/
COPY --from=downloader /opt/cmake/ /opt/cmake/
COPY --from=downloader /opt/arm-none-eabi-gcc/ /opt/arm-none-eabi-gcc/

SHELL ["/bin/bash", "-o", "pipefail", "-c"]

# hadolint ignore=DL3008
RUN apt-get update && \
apt-get install -y --no-install-recommends ca-certificates git llvm-19-tools && \
apt-get upgrade -y && \
rm -rf /var/lib/apt/lists/* && \
ln -s /usr/bin/FileCheck-19 /usr/bin/FileCheck && \
ln -s /usr/bin/FileCheck-19 /usr/bin/filecheck && \
ninja --version && \
cmake --version && \
arm-none-eabi-gcc --version && \
git --version && \
filecheck --version && \
FileCheck --version && \
task --version

CMD ["/bin/bash"]
File renamed without changes.
53 changes: 52 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,52 @@
# arm-dev-env
# arm-dev-env

Development environment for building and testing the Embedded Society's ARM Cortex-M libraries.

It is a single [`Dockerfile`](Dockerfile) bundling the cross-compilation toolchain and test tooling. Instead of pulling a prebuilt image, consumers build it on demand — as a VS Code / IDE dev container, as a CI container, or as a plain Docker image.

## Use as a dev container

Reference the `Dockerfile` from a consuming repository's `.devcontainer/devcontainer.json`. When this repo is added there as a git submodule named `arm-dev-env`:

```json
{
"name": "arm-dev-env",
"build": {
"dockerfile": "../arm-dev-env/Dockerfile",
"context": "../arm-dev-env"
},
"initializeCommand": "git submodule update --init --recursive"
}
```

## Build and run manually

Build the image:

```bash
docker build -t arm-dev-env .
```

Open an interactive shell with the current directory mounted:

```bash
docker run -it --rm -v "$(pwd)":/workspace -w /workspace arm-dev-env /bin/bash
```

## Included tools

- **Build system** — CMake, Ninja, [Task](https://taskfile.dev)
- **ARM toolchain** — the complete `arm-none-eabi-*` GNU bare-metal suite
- **Testing** — LLVM FileCheck (available as both `FileCheck` and `filecheck`)
- **Version control** — Git

## Licence

Copyright (C) 2026 The Embedded Society <https://github.com/embedded-society/arm-dev-env>.

This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

See also the attached [LICENCE](LICENCE) file.
Loading