diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..79776e5 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,15 @@ +# Git files +.git +.gitignore +.gitattributes + +# GitHub files +.github + +# Documentation +README.md +LICENCE + +# Editor configs +.editorconfig +.vscode diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a8e8231 --- /dev/null +++ b/.editorconfig @@ -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 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..5a6e62d --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +# automatically normalize line endings to the user's GIT preferences +* text=auto +*.sh text eol=lf diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..2548f50 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @MatejGomboc diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..4df32d9 --- /dev/null +++ b/.github/dependabot.yml @@ -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" diff --git a/.github/workflows/ci_pr_validation.yml b/.github/workflows/ci_pr_validation.yml new file mode 100644 index 0000000..eeb1299 --- /dev/null +++ b/.github/workflows/ci_pr_validation.yml @@ -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' diff --git a/.github/workflows/scheduled_build.yml b/.github/workflows/scheduled_build.yml new file mode 100644 index 0000000..60fb276 --- /dev/null +++ b/.github/workflows/scheduled_build.yml @@ -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. diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..d025d82 --- /dev/null +++ b/.vscode/extensions.json @@ -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" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..425da02 --- /dev/null +++ b/.vscode/settings.json @@ -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" + } +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..548538c --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/LICENSE b/LICENCE similarity index 100% rename from LICENSE rename to LICENCE diff --git a/README.md b/README.md index d280c6d..2afd28d 100644 --- a/README.md +++ b/README.md @@ -1 +1,52 @@ -# arm-dev-env \ No newline at end of file +# 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 . + +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.