From bde4da614289d06af790013ed5c56cc508b82cc9 Mon Sep 17 00:00:00 2001 From: Oliver Pajonk Date: Fri, 4 Jul 2025 14:14:01 +0000 Subject: [PATCH] Initial Eclipse S-CORE Development Container --- .devcontainer/devcontainer-lock.json | 24 ++++ .devcontainer/devcontainer.json | 24 ++++ .devcontainer/post_create_command.sh | 3 + .github/CODEOWNERS | 1 + .github/workflows/ci.yaml | 49 +++++++ .github/workflows/release.yaml | 42 ++++++ .gitignore | 2 + .pre-commit-config.yaml | 10 ++ README.md | 134 +++++++++++++++++- resources/devcontainer_success.png | Bin 0 -> 2710 bytes resources/reopen_in_container.png | Bin 0 -> 16938 bytes scripts/build.sh | 4 + scripts/test-utils.sh | 37 +++++ scripts/test.sh | 26 ++++ .../.devcontainer/Dockerfile | 13 ++ .../.devcontainer/devcontainer-lock.json | 29 ++++ .../.devcontainer/devcontainer.json | 128 +++++++++++++++++ .../s-core-local/devcontainer-feature.json | 40 ++++++ .../.devcontainer/s-core-local/install.sh | 91 ++++++++++++ .../s-core-local/post_create_command.sh | 15 ++ .../s-core-local/tests/test_default.sh | 17 +++ src/s-core-devcontainer/manifest.json | 5 + src/s-core-devcontainer/test-project/test.sh | 23 +++ 23 files changed, 715 insertions(+), 2 deletions(-) create mode 100644 .devcontainer/devcontainer-lock.json create mode 100644 .devcontainer/devcontainer.json create mode 100755 .devcontainer/post_create_command.sh create mode 100644 .github/CODEOWNERS create mode 100644 .github/workflows/ci.yaml create mode 100644 .github/workflows/release.yaml create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 resources/devcontainer_success.png create mode 100644 resources/reopen_in_container.png create mode 100755 scripts/build.sh create mode 100755 scripts/test-utils.sh create mode 100755 scripts/test.sh create mode 100644 src/s-core-devcontainer/.devcontainer/Dockerfile create mode 100644 src/s-core-devcontainer/.devcontainer/devcontainer-lock.json create mode 100644 src/s-core-devcontainer/.devcontainer/devcontainer.json create mode 100644 src/s-core-devcontainer/.devcontainer/s-core-local/devcontainer-feature.json create mode 100755 src/s-core-devcontainer/.devcontainer/s-core-local/install.sh create mode 100755 src/s-core-devcontainer/.devcontainer/s-core-local/post_create_command.sh create mode 100755 src/s-core-devcontainer/.devcontainer/s-core-local/tests/test_default.sh create mode 100644 src/s-core-devcontainer/manifest.json create mode 100755 src/s-core-devcontainer/test-project/test.sh diff --git a/.devcontainer/devcontainer-lock.json b/.devcontainer/devcontainer-lock.json new file mode 100644 index 0000000..681a747 --- /dev/null +++ b/.devcontainer/devcontainer-lock.json @@ -0,0 +1,24 @@ +{ + "features": { + "ghcr.io/devcontainers-extra/features/pre-commit:2": { + "version": "2.0.18", + "resolved": "ghcr.io/devcontainers-extra/features/pre-commit@sha256:6e0bb2ce80caca1d94f44dab5d0653d88a1c00984e590adb7c6bce012d0ade6e", + "integrity": "sha256:6e0bb2ce80caca1d94f44dab5d0653d88a1c00984e590adb7c6bce012d0ade6e" + }, + "ghcr.io/devcontainers/features/docker-in-docker:2": { + "version": "2.12.2", + "resolved": "ghcr.io/devcontainers/features/docker-in-docker@sha256:842d2ed40827dc91b95ef727771e170b0e52272404f00dba063cee94eafac4bb", + "integrity": "sha256:842d2ed40827dc91b95ef727771e170b0e52272404f00dba063cee94eafac4bb" + }, + "ghcr.io/devcontainers/features/git-lfs:1": { + "version": "1.2.5", + "resolved": "ghcr.io/devcontainers/features/git-lfs@sha256:71c2b371cf12ab7fcec47cf17369c6f59156100dad9abf9e4c593049d789de72", + "integrity": "sha256:71c2b371cf12ab7fcec47cf17369c6f59156100dad9abf9e4c593049d789de72" + }, + "ghcr.io/devcontainers/features/git:1": { + "version": "1.3.4", + "resolved": "ghcr.io/devcontainers/features/git@sha256:f24645e64ad39a596131a50ec96f7d5cf7a2a87544cce772dd4b7182a233e98a", + "integrity": "sha256:f24645e64ad39a596131a50ec96f7d5cf7a2a87544cce772dd4b7182a233e98a" + } + } +} \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..72db3cd --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,24 @@ +{ + "image": "mcr.microsoft.com/devcontainers/javascript-node:0-18", + "features": { + "ghcr.io/devcontainers/features/git:1": {}, + "ghcr.io/devcontainers/features/git-lfs:1": {}, + "ghcr.io/devcontainers/features/docker-in-docker:2": {}, + "ghcr.io/devcontainers-extra/features/pre-commit:2": { + "version": "4.2.0" + } + }, + "postCreateCommand": "${containerWorkspaceFolder}/.devcontainer/post_create_command.sh", + "customizations": { + "vscode": { + "extensions": [ + "mads-hartmann.bash-ide-vscode", + "dbaeumer.vscode-eslint", + "EditorConfig.EditorConfig" + ], + "settings": { + "files.insertFinalNewline": true + } + } + } +} diff --git a/.devcontainer/post_create_command.sh b/.devcontainer/post_create_command.sh new file mode 100755 index 0000000..ebeac73 --- /dev/null +++ b/.devcontainer/post_create_command.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +npm install -g @devcontainers/cli +pre-commit install diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..b8126a3 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @opajonk @lurtz diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..a7ec364 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,49 @@ +name: 'Validate DevContainer' +description: 'This workflow is checking that updates do not break stuff. If on main branch, publish to latest tag.' +on: + pull_request: + push: + branches: + - main + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + build: + name: 'Check, Build, Test, Publish DevContainer' + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + id-token: write + + steps: + - name: Checkout (GitHub) + uses: actions/checkout@v3 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GHCR_TOKEN }} + + - name: Check, Build, Test, Publish + uses: devcontainers/ci@v0.3 + with: + cacheFrom: ghcr.io/eclipse-score/devcontainer + imageName: ghcr.io/eclipse-score/devcontainer + # publish latest from main branch; tags are handled in release workflow + imageTag: latest + refFilterForPush: 'refs/heads/main' + runCmd: | + # Check + pre-commit run --show-diff-on-failure --color=always --all-files || exit -1 + + # Build + ./scripts/build.sh + + # Test + ./scripts/test.sh diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..c401b83 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,42 @@ +name: 'Validate & Publish DevContainer' +description: 'This workflow is checking that for releases, updates do not break stuff and publishes the released container.' +on: + push: + tags: + - '[0-9]+.[0-9]+.[0-9]+' + +jobs: + build: + name: 'Check, Build, Test, Publish DevContainer' + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + id-token: write + + steps: + - name: Checkout (GitHub) + uses: actions/checkout@v3 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GHCR_TOKEN }} + + - name: Check, Build, Test, Publish + uses: devcontainers/ci@v0.3 + with: + imageName: ghcr.io/eclipse-score/devcontainer + cacheFrom: ghcr.io/eclipse-score/devcontainer + imageTag: ${{ github.ref_name }} + runCmd: | + # Check + pre-commit run --show-diff-on-failure --color=always --all-files || exit -1 + + # Build + ./scripts/build.sh + + # Test + ./scripts/test.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..18153c5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# Exported image files shall never be committed. +/export.img diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..69e4bd9 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,10 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: cef0300fd0fc4d2a87a85fa2093c6b283ea36f4b # v5.0.0 + hooks: + - id: check-yaml + - id: end-of-file-fixer + exclude: "devcontainer-lock.json" + - id: trailing-whitespace + - id: check-shebang-scripts-are-executable + - id: check-executables-have-shebangs diff --git a/README.md b/README.md index 1d063b9..b6e44f7 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,132 @@ -# devcontainer -Common Devcontainer for Eclipse S-CORE +# Common DevContainer for Eclipse S-CORE +This repository contains the common [development container](https://containers.dev) for [Eclipse S-CORE](https://github.com/eclipse-score). +It contains all tools required to develop (modify, build, ...) Eclipse S-CORE. +All tool version are well-defined, and all tools are pre-configured to work as expected for Eclipse S-CORE development. +The container is [pre-built](https://containers.dev/guide/prebuild) in GitHub Actions as part of this repository, tested, published, and ready for use. + +Using the pre-built container in an Eclipse S-CORE repository is described in the [Usage](#usage) section. + +Modifying the content of the container is explained in the [Development](#development) section. + +## Usage + +> **NOTE:** There are several development environments which support development containers; most notably [Visual Studio Code](https://code.visualstudio.com), but also [IntelliJ IDEA](https://www.jetbrains.com/idea) and others. +> See [here](https://containers.dev/supporting) for a more complete list. +> In the following, we assume that [Visual Studio Code](https://code.visualstudio.com) and its Dev Containers extension is used. +The [Dev Containers extension homepage](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) has a description how to get up to speed on Windows, macOS and Linux operating systems. +From here on, we assume that such a development container setup is installed and running. + +### First-Time Setup + +Add a file called `.devcontainer/devcontainer.json` to your repository. +It should contain the following: + +````json +{ + "name": "eclipse-s-core", + "image": "ghcr.io/eclipse-score/devcontainer:", + "initializeCommand": "mkdir -p ${localEnv:HOME}/.cache/bazel" +} +```` + +The `` must be a [valid, published release](https://github.com/eclipse-score/devcontainer/tags). +You can also use `latest` as `` to automatically follow the `main` branch - but be aware that this can result in undesired updates. +The `initializeCommand` is required to ensure the default Bazel cache directory exists on your host system. + +To start using the container, click the **Reopen in Container** button when prompted by Visual Studio Code: + +![Reopen in Container](resources/reopen_in_container.png) + +Alternatively, you can press Ctrl + Shift + p and run from there "Dev Containers: Reopen in Container". + +The first time you do this, the container will be downloaded. +This may take some time. +Afterwards, Visual Studio Code should show this in the lower left corner of your window: + +![Dev container success](resources/devcontainer_success.png) + +### Inside the Container + +Open a Terminal, and - for example - type `bazel build ...` to execute the default build of the repository. + +After you have build the code, create [compilation databases](https://clang.llvm.org/docs/JSONCompilationDatabase.html) via Visual Studio Code [Task](https://code.visualstudio.com/docs/debugtest/tasks): + +- C++: Ctrl + Shift + p -> `Tasks: Run Task` -> `Update compile_commands.json` +- Rust: Ctrl + Shift + p -> `Tasks: Run Task` -> `Generate rust-project.json` + +These databases are used by Visual Studio Code to support code navigation and auto-completion with the help of [language servers](https://microsoft.github.io/language-server-protocol/). + +Congratulations, you are now a dev container enthusiast ๐Ÿ˜Š. + +## Development + +> **NOTE:** This is about the development *of the DevContainer*, not about development of Eclipse S-CORE *using* the DevContainer. + +The [Eclipse S-CORE](https://github.com/eclipse-score) development container is developed using - a development container! +That means, the usage is similarly simple: + +```` +git clone https://github.com/eclipse-score/devcontainer.git +code devcontainer +```` +and "Reopen in Container". + +### Repository Structure +Ordered by importance: + +* `src/s-core-devcontainer/` contains the sources for the Eclipse S-CORE DevContainer. +It uses pre-existing [DevContainer features](https://containers.dev/implementors/features/) to provide some standard tools like Git, LLVM, and others. +In addition, it uses a so-called "local" feature (cf. `src/s-core-devcontainer/.devcontainer/s-core-local`) for the remaining tools and configuration. +* `scripts/` contains scripts to build and test the container. +* `.devcontainer/` contains the definition of the DevContainer for **this** repository, i.e. the "devcontainer devcontainer". +There should rarely be a need to modify this. +* `.github/` contains the regular GitHub setup, with code owners and CI. +* `resources/` contains a few screenshots. + +### Modify, Build, Test, Use + +It is very simple to develop the development container. +You can change files related to the container and then simply run the `scripts/*`. +They are used by the CI, but especially the build and test scripts can be run also locally out of the box: +````console +$ ./scripts/build.sh +[... build output..] +{"outcome":"success","imageName":["vsc-s-core-devcontainer-209943ec6ff795f57b20cdf85a70c904d1e3b4a329d1e01c79f0ffea615c6e40-features"]} + +$ ./scripts/test.sh +[... test output...] +๐Ÿ’ฏ All passed! +```` +You can now also use this freshly built development container locally on your machine, e.g. to test the container as part of an Eclipse S-CORE module. +For this you must understand that you have the following situation: +``` ++---------------------------------+ +| Development Container A | +| +---------------------------+ | +| | S-CORE DevContainer image | | +| +---------------------------+ | ++---------------------------------+ +``` +`Development Container A` is the one you are running right now to develop the `S-CORE DevContainer` . +So in order to execute `S-CORE DevContainer` on your host (and test it as part of an S-CORE module), you need to + +* export this newly built S-CORE DevContainer image +* import the image on your host machine +* use the image name in the `.devcontainer/devcontainer.json` of the targeted S-CORE module + +Concretely, this can be done as follows: + +* Run `docker save > export.img` in `Development Container A`. +For example, given above build output, this would be `docker save vsc-s-core-devcontainer-209943ec6ff795f57b20cdf85a70c904d1e3b4a329d1e01c79f0ffea615c6e40-features > export.img` +* On your **host machine** (!!), open a console and run `docker load < /path/to/export.img`. +* In the working copy of the targeted S-CORE module, edit the file `.devcontainer/devcontainer.json` and change the `"image": "..."` entry to `"image": ""`. +Given above build output, this would be `"image": "vsc-s-core-devcontainer-209943ec6ff795f57b20cdf85a70c904d1e3b4a329d1e01c79f0ffea615c6e40-features"`. +The Visual Studio Code instance related to the targeted S-CORE module will now ask you to rebuild the DevContainer. +Do so, and you have a running instance of `S-CORE DevContainer` related to the targeted S-CORE module. + +### Version Pinning + +The `S-CORE DevContainer` pins feature and tool versions. +For tools that are not part of a trusted distribution (i.e. downloaded directly), it also pins the SHA256 hash. +While not being the most convenient choice for tool updates, this makes our supply chain much more secure. +Additionally, there are no "surprise updates" which unexpectedly break things. diff --git a/resources/devcontainer_success.png b/resources/devcontainer_success.png new file mode 100644 index 0000000000000000000000000000000000000000..24b4e4d7a7ed6a58f03be3507634de361bd692aa GIT binary patch literal 2710 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 literal 0 HcmV?d00001 diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..7146afa --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euxo pipefail + +devcontainer build --workspace-folder src/s-core-devcontainer diff --git a/scripts/test-utils.sh b/scripts/test-utils.sh new file mode 100755 index 0000000..0f9ad06 --- /dev/null +++ b/scripts/test-utils.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +set -eo pipefail + +if [[ -z ${HOME} ]]; then + HOME="/root" +fi + +FAILED=() + +echoStderr() +{ + echo "$@" 1>&2 +} + +check() { + LABEL=$1 + shift + echo -e "\n๐Ÿงช Testing ${LABEL}" + if "$@"; then + echo "โœ… Passed!" + return 0 + else + echoStderr "โŒ ${LABEL} check failed." + FAILED+=("${LABEL}") + return 1 + fi +} + +reportResults() { + if [[ ${#FAILED[@]} -ne 0 ]]; then + echoStderr -e "\n๐Ÿ’ฅ Failed tests:" "${FAILED[@]}" + exit 1 + else + echo -e "\n๐Ÿ’ฏ All passed!" + exit 0 + fi +} diff --git a/scripts/test.sh b/scripts/test.sh new file mode 100755 index 0000000..f20ffe0 --- /dev/null +++ b/scripts/test.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -euxo pipefail + +IMAGE="s-core-devcontainer" + +export DOCKER_BUILDKIT=1 + +SCRIPT_PATH=$(readlink -f "$0") +SCRIPT_DIR=$(dirname -- "${SCRIPT_PATH}") +PROJECT_DIR=$(dirname -- "${SCRIPT_DIR}") +ID_LABEL="test-container=${IMAGE}" + +devcontainer up \ + --id-label "${ID_LABEL}" \ + --workspace-folder "${PROJECT_DIR}/src/${IMAGE}/" \ + --remove-existing-container + +CONTAINER_ID=$(docker container ls --filter "label=${ID_LABEL}" --quiet) +IMAGE_NAME=$(docker container inspect --format '{{ .Config.Image }}' "${CONTAINER_ID}") +IMAGE_ID=$(docker image ls --filter "reference=${IMAGE_NAME}" --quiet) + +# Run actual test +echo "(*) Running test..." +devcontainer exec --workspace-folder "${PROJECT_DIR}/src/${IMAGE}" --id-label "${ID_LABEL}" \ + /bin/sh -c 'set -e && cd test-project && \ + ./test.sh' diff --git a/src/s-core-devcontainer/.devcontainer/Dockerfile b/src/s-core-devcontainer/.devcontainer/Dockerfile new file mode 100644 index 0000000..b281a62 --- /dev/null +++ b/src/s-core-devcontainer/.devcontainer/Dockerfile @@ -0,0 +1,13 @@ +ARG VARIANT="noble" +FROM buildpack-deps:${VARIANT}-curl + +LABEL dev.containers.features="common" + +ARG VARIANT +RUN if [ "$VARIANT" = "noble" ]; then \ + if id "ubuntu" &>/dev/null; then \ + echo "Deleting user 'ubuntu' for $VARIANT" && userdel -f -r ubuntu || echo "Failed to delete ubuntu user for $VARIANT"; \ + else \ + echo "User 'ubuntu' does not exist for $VARIANT"; \ + fi; \ + fi diff --git a/src/s-core-devcontainer/.devcontainer/devcontainer-lock.json b/src/s-core-devcontainer/.devcontainer/devcontainer-lock.json new file mode 100644 index 0000000..6c39aa0 --- /dev/null +++ b/src/s-core-devcontainer/.devcontainer/devcontainer-lock.json @@ -0,0 +1,29 @@ +{ + "features": { + "ghcr.io/devcontainers-community/features/llvm": { + "version": "3.2.0", + "resolved": "ghcr.io/devcontainers-community/features/llvm@sha256:4f464ab97a59439286a55490b55ba9851616f6f76ac3025e134127ac08ad79e2", + "integrity": "sha256:4f464ab97a59439286a55490b55ba9851616f6f76ac3025e134127ac08ad79e2" + }, + "ghcr.io/devcontainers/features/common-utils": { + "version": "2.5.3", + "resolved": "ghcr.io/devcontainers/features/common-utils@sha256:3cf7ca93154faf9bdb128f3009cf1d1a91750ec97cc52082cf5d4edef5451f85", + "integrity": "sha256:3cf7ca93154faf9bdb128f3009cf1d1a91750ec97cc52082cf5d4edef5451f85" + }, + "ghcr.io/devcontainers/features/git": { + "version": "1.3.4", + "resolved": "ghcr.io/devcontainers/features/git@sha256:f24645e64ad39a596131a50ec96f7d5cf7a2a87544cce772dd4b7182a233e98a", + "integrity": "sha256:f24645e64ad39a596131a50ec96f7d5cf7a2a87544cce772dd4b7182a233e98a" + }, + "ghcr.io/devcontainers/features/git-lfs": { + "version": "1.2.5", + "resolved": "ghcr.io/devcontainers/features/git-lfs@sha256:71c2b371cf12ab7fcec47cf17369c6f59156100dad9abf9e4c593049d789de72", + "integrity": "sha256:71c2b371cf12ab7fcec47cf17369c6f59156100dad9abf9e4c593049d789de72" + }, + "ghcr.io/devcontainers/features/python": { + "version": "1.7.1", + "resolved": "ghcr.io/devcontainers/features/python@sha256:cf9b6d879790a594b459845b207c5e1762a0c8f954bb8033ff396e497f9c301b", + "integrity": "sha256:cf9b6d879790a594b459845b207c5e1762a0c8f954bb8033ff396e497f9c301b" + } + } +} \ No newline at end of file diff --git a/src/s-core-devcontainer/.devcontainer/devcontainer.json b/src/s-core-devcontainer/.devcontainer/devcontainer.json new file mode 100644 index 0000000..2f04edb --- /dev/null +++ b/src/s-core-devcontainer/.devcontainer/devcontainer.json @@ -0,0 +1,128 @@ +{ + "build": { + // Installs latest version from the Distribution + "dockerfile": "./Dockerfile", + "context": "." + }, + "features": { + "ghcr.io/devcontainers/features/git": { + "version": "2.49.0", + "ppa": "true" + }, + "ghcr.io/devcontainers/features/git-lfs": { + // Installs the latest version from the Distribution + }, + "ghcr.io/devcontainers/features/common-utils": { + // Installs latest version from the Distribution + "installZsh": "false", + "username": "vscode", + "userUid": "1000", + "userGid": "1000", + "upgradePackages": "false" // WARNING: do *not* enable; this would include packages also from other features, which may have been pinned to a specific version + }, + "ghcr.io/devcontainers-community/features/llvm": { + // Full semantic version pinning does not work with this feature. + // In case we want this, the feature needs to be replaced with a custom installation script. + "version": "20" + }, + "ghcr.io/devcontainers/features/python": { + "version": "3.12.11" + }, + "./s-core-local": { + "BAZEL_VERSION": "7.5.0", + "BUILDIFIER_VERSION": "8.2.1", + // The following sha256sum is for the binary buildifier-linux-amd64 + // from the GitHub release page of buildtools + // It is generated by running 'sha256sum buildifier-linux-amd64' + "BUILDIFIER_SHA256": "6ceb7b0ab7cf66fceccc56a027d21d9cc557a7f34af37d2101edb56b92fcfa1a", + "BAZEL_COMPILE_COMMANDS_VERSION": "0.17.2", + // The following sha256sums are for the deb package bazel-compile-commands_-_amd64.deb + // where is the Ubuntu codename (e.g., jammy, focal, noble) + // and is the version of bazel-compile-commands + // The format is: :;:;... + // For example: jammy:6cde78e1a58c8f9047446cce25a81a676b0f293194175c9fe61586224c0b6f84;noble:97239b316df58fd3370a8aa6350790ececd5e4a1de30efb42d45cb1c300a179a;... + // It is generated by running 'sha256sum bazel-compile-commands_-_amd64.deb' + "BAZEL_COMPILE_COMMANDS_SHA256": "jammy:6cde78e1a58c8f9047446cce25a81a676b0f293194175c9fe61586224c0b6f84;noble:97239b316df58fd3370a8aa6350790ececd5e4a1de30efb42d45cb1c300a179a;focal:c861f4f36cc2884eefbd488f19c49a9674ac093210a503bce032eac8b94b3660", + // required by the rust-analyzer VS Code extension + "RUST_ANALYZER_VERSION": "2025-06-30", + // The following sha256sum is for the binary rust-analyzer-x86_64-unknown-linux-gnu.gz + // It is generated by running 'sha256sum rust-analyzer-x86_64-unknown-linux-gnu.gz' + "RUST_ANALYZER_SHA256": "9f40579d05a54ff084e449c3721a3bc8c907aab676e745c78de31c62e74ea778" + } + }, + "remoteUser": "vscode", + "initializeCommand": "mkdir -p ${localEnv:HOME}/.cache/bazel", + "customizations": { + "vscode": { + "extensions": [ + "mads-hartmann.bash-ide-vscode", + "bazelbuild.vscode-bazel", + "dbaeumer.vscode-eslint", + "EditorConfig.EditorConfig", + "llvm-vs-code-extensions.vscode-clangd", + "jebbs.plantuml", //to preview PlantUML diagrams + "hediet.vscode-drawio", //for drawio integration + "swyddfa.esbonio", // for Sphinx documentation support + "rust-lang.rust-analyzer" // Rust language support for Visual Studio Code; see also tasks below + ], + "settings": { + "files.insertFinalNewline": true, + "editor.formatOnSave": false, + "[cpp]": { + "editor.defaultFormatter": "llvm-vs-code-extensions.vscode-clangd" + }, + "clangd.path": "/usr/bin/clangd", + "clangd.arguments": [ + "--compile-commands-dir=${workspaceFolder}/", + "--pretty", + "--background-index" + ], + "C_Cpp.intelliSenseEngine": "disabled", + "tasks": { + "version": "2.0.0", + "tasks": [ + { + "label": "Run bazel-compile-commands", + "command": "bazel-compile-commands", + "type": "shell", + "isBackground": true, + "hide": true + }, + { + "label": "Update compile_commands.json", + "command": "${command:clangd.restart}", + "isBackground": true, + "dependsOn": [ + "Run bazel-compile-commands" + ], + "group": { + "kind": "build", + "isDefault": true + } + }, + { // see https://bazelbuild.github.io/rules_rust/rust_analyzer.html#vscode + "label": "Generate rust-project.json", + "command": "bazel", + "args": [ + "run", + "@rules_rust//tools/rust_analyzer:gen_rust_project" + ], + "options": { + "cwd": "${workspaceFolder}" + }, + "group": "build", + "problemMatcher": [], + "presentation": { + "reveal": "never", + "panel": "dedicated" + }, + "runOptions": { + "runOn": "folderOpen" + } + } + ] + } + } + } + } +} diff --git a/src/s-core-devcontainer/.devcontainer/s-core-local/devcontainer-feature.json b/src/s-core-devcontainer/.devcontainer/s-core-local/devcontainer-feature.json new file mode 100644 index 0000000..3930707 --- /dev/null +++ b/src/s-core-devcontainer/.devcontainer/s-core-local/devcontainer-feature.json @@ -0,0 +1,40 @@ +{ + "name": "Eclipse S-CORE-specific Local Tools", + "id": "s-core-local", + "version": "1.0.0", + "description": "Tools which are not available as already existing development container feature", + "options": { + "BAZEL_VERSION": { + "type": "string", + "default": "7.5.0", + "description": "Version of Bazel to install" + }, + "BUILDIFIER_VERSION": { + "type": "string", + "default": "8.2.1", + "description": "Version of Buildifier to install" + }, + "BUILDIFIER_SHA256": { + "type": "string", + "default": "", + "description": "sha256sum of the Buildifier binary to verify the download" + }, + "BAZEL_COMPILE_COMMANDS_VERSION": { + "type": "string", + "default": "0.17.2", + "description": "Version of Bazel Compile Commands to install" + }, + "BAZEL_COMPILE_COMMANDS_SHA256": { + "type": "string", + "default": "", + "description": "sha256sums of Bazel Compile Commands to verify the download; format: :;:;..." + } + }, + "postCreateCommand": "/devcontainer/features/s-core-local/post_create_command.sh", + "mounts": [ { + "source": "${localEnv:HOME}/.cache/bazel", // default Bazel cache directory + "target": "/var/cache/bazel", + "type": "bind" + } + ] +} diff --git a/src/s-core-devcontainer/.devcontainer/s-core-local/install.sh b/src/s-core-devcontainer/.devcontainer/s-core-local/install.sh new file mode 100755 index 0000000..8705455 --- /dev/null +++ b/src/s-core-devcontainer/.devcontainer/s-core-local/install.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Copy feature sources and tests to expected location +FEATURES_DIR="/devcontainer/features" +SCRIPT_PATH=$(readlink -f "$0") +SCRIPT_DIR=$(dirname -- "${SCRIPT_PATH}") +mkdir -p "${FEATURES_DIR}" +COPY_TARGET="${FEATURES_DIR}/$(basename "${SCRIPT_DIR%%_*}")" +cp -R "${SCRIPT_DIR}" "${COPY_TARGET}" +rm -f "${COPY_TARGET}/devcontainer-features.env" "${COPY_TARGET}/devcontainer-features-install.sh" + +# Check if required variables are set +if [ -z "${BAZEL_VERSION:-}" ]; then + echo "Error: BAZEL_VERSION is not set." + exit 1 +fi +if [ -z "${BUILDIFIER_VERSION:-}" ]; then + echo "Error: BUILDIFIER_VERSION is not set." + exit 1 +fi +if [ -z "${BUILDIFIER_SHA256:-}" ]; then + echo "Error: BUILDIFIER_SHA256 is not set." + exit 1 +fi +if [ -z "${BAZEL_COMPILE_COMMANDS_VERSION:-}" ]; then + echo "Error: BAZEL_COMPILE_COMMANDS_VERSION is not set." + exit 1 +fi +if [ -z "${BAZEL_COMPILE_COMMANDS_SHA256:-}" ]; then + echo "Error: BAZEL_COMPILE_COMMANDS_SHA256 is not set." + exit 1 +fi + +DEBIAN_FRONTEND=noninteractive + +# Install "common" tools +apt-get update +apt-get install -y \ + curl + +# GraphViz +apt-get install -y graphviz + +# Protobuf compiler, via APT (needed by FEO) +apt-get install -y protobuf-compiler + +# Bazel, via APT +# - ghcr.io/devcontainers-community/features/bazel uses bazelisk, which has a few problems: +# - It does not install bash autocompletion. +# - The bazel version is not pinned, which is required to be reproducible and to have coordinated, tested tool updates. +# - In general, pre-built containers *shall not* download "more tools" from the internet. +# This is an operational risk (security, availability); it makes the build non-reproducible, +# and it prevents the container from working in air-gapped environments. +apt-get install apt-transport-https curl gnupg -y +curl -fsSL https://bazel.build/bazel-release.pub.gpg | gpg --dearmor > bazel-archive-keyring.gpg +mv bazel-archive-keyring.gpg /usr/share/keyrings +echo "deb [arch=amd64 signed-by=/usr/share/keyrings/bazel-archive-keyring.gpg] https://storage.googleapis.com/bazel-apt stable jdk1.8" | tee /etc/apt/sources.list.d/bazel.list +apt-get update +apt-get install -y bazel=${BAZEL_VERSION} + +# Buildifier, directly from GitHub (apparently no APT repository available) +# The version is pinned to a specific release, and the SHA256 checksum is provided by the devcontainer-features.json file. +curl -L "https://github.com/bazelbuild/buildtools/releases/download/v${BUILDIFIER_VERSION}/buildifier-linux-amd64" -o /usr/local/bin/buildifier +echo "${BUILDIFIER_SHA256} /usr/local/bin/buildifier" | sha256sum -c - || exit -1 +chmod +x /usr/local/bin/buildifier + +# Code completion for C++ code of Bazel projects +# (see https://github.com/kiron1/bazel-compile-commands) +# The version is pinned to a specific release, and the SHA256 checksum is provided by the devcontainer-features.json file. +source /etc/lsb-release +curl -L "https://github.com/kiron1/bazel-compile-commands/releases/download/v${BAZEL_COMPILE_COMMANDS_VERSION}/bazel-compile-commands_${BAZEL_COMPILE_COMMANDS_VERSION}-${DISTRIB_CODENAME}_amd64.deb" -o /tmp/bazel-compile-commands.deb +# Extract correct sha256 for current DISTRIB_CODENAME and check +BAZEL_COMPILE_COMMANDS_DEB_SHA256=$(echo "${BAZEL_COMPILE_COMMANDS_SHA256}" | tr ';' '\n' | grep "^${DISTRIB_CODENAME}:" | cut -d: -f2) +echo "${BAZEL_COMPILE_COMMANDS_DEB_SHA256} /tmp/bazel-compile-commands.deb" | sha256sum -c - || exit -1 +apt-get install -y --no-install-recommends --fix-broken /tmp/bazel-compile-commands.deb +rm /tmp/bazel-compile-commands.deb + +# Code completion for Rust code of Bazel projects (language server part) +# (see https://bazelbuild.github.io/rules_rust/rust_analyzer.html and https://rust-analyzer.github.io/book/rust_analyzer_binary.html) +# The version is pinned to a specific release, and the SHA256 checksum is provided by the devcontainer-features.json file. +curl -L https://github.com/rust-lang/rust-analyzer/releases/download/${RUST_ANALYZER_VERSION}/rust-analyzer-x86_64-unknown-linux-gnu.gz > /tmp/rust-analyzer.gz +echo "${RUST_ANALYZER_SHA256} /tmp/rust-analyzer.gz" | sha256sum -c - || exit -1 +gunzip -d /tmp/rust-analyzer.gz +mv /tmp/rust-analyzer /usr/local/bin/rust-analyzer +chmod +x /usr/local/bin/rust-analyzer + +# Cleanup +apt-get autoremove -y +apt-get clean +rm -rf /var/lib/apt/lists/* diff --git a/src/s-core-devcontainer/.devcontainer/s-core-local/post_create_command.sh b/src/s-core-devcontainer/.devcontainer/s-core-local/post_create_command.sh new file mode 100755 index 0000000..0da43c3 --- /dev/null +++ b/src/s-core-devcontainer/.devcontainer/s-core-local/post_create_command.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Configure Bazel to use the cache directory that is mounted from the host +echo "startup --output_user_root=/var/cache/bazel" >> ~/.bazelrc + +# Configure clangd to remove the -fno-canonical-system-headers flag, which is +# GCC-specific. If not done, there is an annoying error message on the first +# line of every C++ file when being displayed in Visual Studio Code. +mkdir -p ~/.config/clangd +cat > ~/.config/clangd/config.yaml <