diff --git a/.devcontainer/post_create_command.sh b/.devcontainer/post_create_command.sh index 6101a47..8415894 100755 --- a/.devcontainer/post_create_command.sh +++ b/.devcontainer/post_create_command.sh @@ -18,7 +18,7 @@ npm install -g @devcontainers/cli SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" REPOSITORY_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd -P)" -sudo "${REPOSITORY_ROOT}/tools/tool_installer.py" install shellcheck yamlfmt +sudo "${REPOSITORY_ROOT}/tools/tool_installer.py" install shellcheck yamlfmt apm pre-commit install diff --git a/MODULE.bazel b/MODULE.bazel index 913a6ae..fce91a3 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -25,6 +25,8 @@ multitool.hub(lockfile = "//tools:lockfiles/uv.lock.json") multitool.hub(lockfile = "//tools:lockfiles/buildifier.lock.json") multitool.hub(lockfile = "//tools:lockfiles/starpls.lock.json") multitool.hub(lockfile = "//tools:lockfiles/bazelisk.lock.json") +multitool.hub(lockfile = "//tools:lockfiles/apm.lock.json") +multitool.hub(lockfile = "//tools:lockfiles/opencode.lock.json") use_repo(multitool, "multitool") diff --git a/README.md b/README.md index 3584f0c..8ced69a 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,14 @@ should run successfully. > `/var/cache/bazel/` is the path onto which a Docker volume is mounted. > Unless you have setup the same on your host, this will only make `linux-sandbox` work within the devcontainer. +### `gh` and `opencode` configuration + +This devcontainer includes the `gh` and `opencode` tools, but does not include any configuration for them. +Configuration is *not* copied from the host into the container, but is instead stored in Docker volumes. + +This avoids any host dependencies and a complex machinery to copy configuration from the host into the container. +Configure these tools inside the container and they will be kept until the volumes are removed across container rebuilds and different repositories. + ### How to use: codeql The devcontainer codeql installation supports C, C++ and Rust source code analysis. All publicly available diff --git a/scripts/create_builder.sh b/scripts/create_builder.sh index 12724b3..b0d3e70 100755 --- a/scripts/create_builder.sh +++ b/scripts/create_builder.sh @@ -44,6 +44,9 @@ if docker buildx inspect multiarch &>/dev/null; then if ! check_proxy_config; then echo "Builder 'multiarch' exists but has incorrect proxy configuration. Recreating..." docker buildx rm multiarch + elif ! docker buildx inspect multiarch --bootstrap &>/dev/null; then + echo "Builder 'multiarch' exists but is unhealthy. Recreating..." + docker buildx rm multiarch else echo "Builder 'multiarch' already exists with correct configuration." docker buildx use multiarch diff --git a/src/s-core-devcontainer/.devcontainer/devcontainer.json b/src/s-core-devcontainer/.devcontainer/devcontainer.json index f1394b0..fdc8675 100644 --- a/src/s-core-devcontainer/.devcontainer/devcontainer.json +++ b/src/s-core-devcontainer/.devcontainer/devcontainer.json @@ -54,6 +54,7 @@ "llvm-vs-code-extensions.vscode-clangd", "jebbs.plantuml", // preview PlantUML diagrams "hediet.vscode-drawio", // Draw.IO integration + "sst-dev.opencode", // OpenCode VS Code extension "swyddfa.esbonio", // for Sphinx documentation support "rust-lang.rust-analyzer", // Rust language support for Visual Studio Code; see also tasks below "github.vscode-pull-request-github", // GitHub integration 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 index c6e0d0f..0faca54 100644 --- a/src/s-core-devcontainer/.devcontainer/s-core-local/devcontainer-feature.json +++ b/src/s-core-devcontainer/.devcontainer/s-core-local/devcontainer-feature.json @@ -10,6 +10,7 @@ }, "onCreateCommand": "/devcontainer/features/s-core-local/on_create_command.sh", "postCreateCommand": { + "Fix ownership of mounted config volumes": "bash /devcontainer/features/s-core-local/fix_config_volume_ownership.sh", "Setup persistent bash history": "bash /devcontainer/features/s-core-local/setup_command_history.sh", "Enable pre-commit hooks": "bash /devcontainer/features/s-core-local/enable_pre_commit_hooks.sh" }, @@ -18,6 +19,26 @@ "source": "eclipse-s-core-bash-history-${devcontainerId}", "target": "/commandhistory", "type": "volume" + }, + // Persisting the configuration of the OpenCode CLI and GitHub CLI tools. + // A nice way would be to copy the configuration files to the host machine, + // but this is not possible in a devcontainer feature or prebuild devcontainer image, + // so we use volumes instead. + // `initializeCommand` is only run by the `devcontainer.json` of the using repo and from the image. + { + "source": "opencode-config", + "target": "/home/vscode/.config/opencode", + "type": "volume" + }, + { + "source": "opencode-config", + "target": "/home/vscode/.local/share/opencode", + "type": "volume" + }, + { + "source": "gh-config", + "target": "/home/vscode/.config/gh", + "type": "volume" } ] } diff --git a/src/s-core-devcontainer/.devcontainer/s-core-local/fix_config_volume_ownership.sh b/src/s-core-devcontainer/.devcontainer/s-core-local/fix_config_volume_ownership.sh new file mode 100755 index 0000000..ea3362d --- /dev/null +++ b/src/s-core-devcontainer/.devcontainer/s-core-local/fix_config_volume_ownership.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +set -euo pipefail + +current_user_group="$(id -un):$(id -gn)" + +ensure_owner() { + local target_dir="$1" + + if [ ! -d "${target_dir}" ]; then + echo "Error: Config Volume: ${target_dir} does not exist." + exit 1 + fi + + local current_owner_group + current_owner_group=$(stat -c "%U:%G" "${target_dir}") + + if [ "${current_owner_group}" = "${current_user_group}" ]; then + echo "Config Volume: ${target_dir} is already owned by ${current_user_group}." + else + echo "Config Volume: ${target_dir} is owned by ${current_owner_group}. Setting ownership to ${current_user_group}..." + sudo chown -R "${current_user_group}" "${target_dir}" + fi +} + +ensure_owner "/home/vscode/.config/opencode" +ensure_owner "/home/vscode/.local/share/opencode" +ensure_owner "/home/vscode/.config/gh" diff --git a/src/s-core-devcontainer/.devcontainer/s-core-local/install.sh b/src/s-core-devcontainer/.devcontainer/s-core-local/install.sh index 44af67f..6e745a2 100755 --- a/src/s-core-devcontainer/.devcontainer/s-core-local/install.sh +++ b/src/s-core-devcontainer/.devcontainer/s-core-local/install.sh @@ -58,7 +58,7 @@ apt-get install -y "python${python_version}" python3-pip python3-venv apt-get install -y flake8 python3-autopep8 black python3-yapf mypy pydocstyle pycodestyle bandit pipenv virtualenv pylint # Lockfile-managed local developer tools -/usr/local/share/score-tools/tool_installer.py install shellcheck ruff actionlint yamlfmt uv uvx +/usr/local/share/score-tools/tool_installer.py install shellcheck ruff actionlint yamlfmt uv uvx apm opencode # GraphViz # The Ubuntu Noble package of GraphViz diff --git a/src/s-core-devcontainer/.devcontainer/s-core-local/tests/test_default.sh b/src/s-core-devcontainer/.devcontainer/s-core-local/tests/test_default.sh index 40b446e..dea564b 100755 --- a/src/s-core-devcontainer/.devcontainer/s-core-local/tests/test_default.sh +++ b/src/s-core-devcontainer/.devcontainer/s-core-local/tests/test_default.sh @@ -27,6 +27,8 @@ actionlint_lockfile_version="$(/usr/local/share/score-tools/tool_installer.py ve yamlfmt_lockfile_version="$(/usr/local/share/score-tools/tool_installer.py version yamlfmt)" uv_lockfile_version="$(/usr/local/share/score-tools/tool_installer.py version uv)" uvx_lockfile_version="$(/usr/local/share/score-tools/tool_installer.py version uvx)" +apm_lockfile_version="$(/usr/local/share/score-tools/tool_installer.py version apm)" +opencode_lockfile_version="$(/usr/local/share/score-tools/tool_installer.py version opencode)" # pre-commit, it is available via $PATH in login shells, but not in non-login shells check "validate pre-commit is working and has the correct version" bash -c "pre-commit --version | grep '4.5.1'" @@ -71,6 +73,12 @@ check "validate actionlint is working and has the correct version" bash -c "acti # yamlfmt check "validate yamlfmt is working and has the correct version" bash -c "yamlfmt --version | grep '${yamlfmt_lockfile_version}'" +# apm +check "validate apm is working and has the correct version" bash -c "apm --version | grep '${apm_lockfile_version}'" + +# opencode +check "validate opencode is working and has the correct version" bash -c "opencode --version | grep '${opencode_lockfile_version}'" + # uv check "validate uv is working and has the correct version" bash -c "uv --version | grep '${uv_lockfile_version}'" check "validate uvx is working and has the correct version" bash -c "uvx --version | grep '${uvx_lockfile_version}'" diff --git a/src/s-core-devcontainer/.devcontainer/s-core-local/versions.yaml b/src/s-core-devcontainer/.devcontainer/s-core-local/versions.yaml index b4d2a18..8a84853 100644 --- a/src/s-core-devcontainer/.devcontainer/s-core-local/versions.yaml +++ b/src/s-core-devcontainer/.devcontainer/s-core-local/versions.yaml @@ -51,6 +51,10 @@ ripgrep: version: 14.1.0 lcov: version: 2.0 +apm: + version: 0.27.0 +opencode: + version: 1.18.15 pkg_config: version: 1.8.1 libcairo2_dev: diff --git a/tools/BUILD.bazel b/tools/BUILD.bazel index 4dc1500..0864323 100644 --- a/tools/BUILD.bazel +++ b/tools/BUILD.bazel @@ -26,3 +26,5 @@ multitool_aliases("starpls") multitool_aliases("uv") multitool_aliases("uvx") multitool_aliases("yamlfmt") +multitool_aliases("apm") +multitool_aliases("opencode") diff --git a/tools/lockfiles/apm.lock.json b/tools/lockfiles/apm.lock.json new file mode 100644 index 0000000..924f366 --- /dev/null +++ b/tools/lockfiles/apm.lock.json @@ -0,0 +1,40 @@ +{ + "$schema": "https://raw.githubusercontent.com/bazel-contrib/rules_multitool/main/lockfile.schema.json", + "apm": { + "version": "0.27.0", + "binaries": [ + { + "kind": "archive-dir", + "dir": "apm-darwin-arm64", + "url": "https://github.com/microsoft/apm/releases/download/v0.27.0/apm-darwin-arm64.tar.gz", + "sha256": "4c68e5eaa3cfdb0b25734c316deb532835eaf3c3e2f7379a4c7c06918043a641", + "os": "macos", + "cpu": "arm64" + }, + { + "kind": "archive-dir", + "dir": "apm-darwin-x86_64", + "url": "https://github.com/microsoft/apm/releases/download/v0.27.0/apm-darwin-x86_64.tar.gz", + "sha256": "846b30055d96cbc6fa0fcf451f50d13f632b540ffdff344873a025bba607e25a", + "os": "macos", + "cpu": "x86_64" + }, + { + "kind": "archive-dir", + "dir": "apm-linux-x86_64", + "url": "https://github.com/microsoft/apm/releases/download/v0.27.0/apm-linux-x86_64.tar.gz", + "sha256": "be2d8a97ca8816636117ec26da85482d647ae3353213ea022fb1130c2dd3d3b0", + "os": "linux", + "cpu": "x86_64" + }, + { + "kind": "archive-dir", + "dir": "apm-linux-arm64", + "url": "https://github.com/microsoft/apm/releases/download/v0.27.0/apm-linux-arm64.tar.gz", + "sha256": "7df6e64ca9540665367f07af0226077ba92820f6cc759c10a5ca37e038a500e4", + "os": "linux", + "cpu": "arm64" + } + ] + } +} diff --git a/tools/lockfiles/opencode.lock.json b/tools/lockfiles/opencode.lock.json new file mode 100644 index 0000000..fbcb3be --- /dev/null +++ b/tools/lockfiles/opencode.lock.json @@ -0,0 +1,44 @@ +{ + "$schema": "https://raw.githubusercontent.com/bazel-contrib/rules_multitool/main/lockfile.schema.json", + "opencode": { + "version": "1.18.15", + "binaries": [ + { + "kind": "archive", + "file": "opencode", + "url": "https://github.com/anomalyco/opencode/releases/download/v1.18.15/opencode-darwin-arm64.zip", + "sha256": "bd60b57cb9fe0494a5352c807424d36d6d7853cf6dbddb97065c7ccd3c5d391c", + "type": "zip", + "os": "macos", + "cpu": "arm64" + }, + { + "kind": "archive", + "file": "opencode", + "url": "https://github.com/anomalyco/opencode/releases/download/v1.18.15/opencode-darwin-x64.zip", + "sha256": "e97e8185e7b7942f6e14f51b8727dbe023b54772e075bc16fead813680455d17", + "type": "zip", + "os": "macos", + "cpu": "x86_64" + }, + { + "kind": "archive", + "file": "opencode", + "url": "https://github.com/anomalyco/opencode/releases/download/v1.18.15/opencode-linux-x64.tar.gz", + "sha256": "d842e0e8c622c672a481b7dc6f0329009b64db96b2ba6041e56f4f93f0293b1c", + "type": "tar.gz", + "os": "linux", + "cpu": "x86_64" + }, + { + "kind": "archive", + "file": "opencode", + "url": "https://github.com/anomalyco/opencode/releases/download/v1.18.15/opencode-linux-arm64.tar.gz", + "sha256": "500611819ff88916b185649990505a9be76ad13ca5bb4b9323e5abdd39b1c6fb", + "type": "tar.gz", + "os": "linux", + "cpu": "arm64" + } + ] + } +} diff --git a/tools/tool_installer.py b/tools/tool_installer.py index f660f24..0404bc5 100755 --- a/tools/tool_installer.py +++ b/tools/tool_installer.py @@ -49,6 +49,7 @@ class Binary(TypedDict): sha256: str type: NotRequired[str] file: NotRequired[str] + dir: NotRequired[str] class ToolData(TypedDict): @@ -172,6 +173,37 @@ def _extract_member( raise SystemExit(f"Unsupported archive type '{archive_type}' for {tool}") +def _extract_dir( + binary: Binary, archive_path: Path, out_dir: Path, tool: str +) -> None: + """Extract a directory from a tar archive, stripping the top-level prefix.""" + dir_prefix = binary.get("dir") + if dir_prefix is None: + raise SystemExit(f"Binary entry for {tool} does not define 'dir' field") + prefix = dir_prefix.rstrip("/") + "/" + + try: + with tarfile.open(archive_path) as tf: + for member in tf.getmembers(): + if not member.name.startswith(prefix): + continue + rel = member.name[len(prefix):] + if not rel: + continue + dest = out_dir / rel + if member.isdir(): + dest.mkdir(parents=True, exist_ok=True) + elif member.isfile(): + dest.parent.mkdir(parents=True, exist_ok=True) + reader = tf.extractfile(member) + if reader is not None: + dest.write_bytes(reader.read()) + if member.mode & 0o111: + dest.chmod(dest.stat().st_mode | 0o111) + except tarfile.TarError as exc: + raise SystemExit(f"Failed to extract tar archive for {tool}: {exc}") from exc + + def _cmd_install(args: argparse.Namespace) -> int: """Download, verify, and install tools from the lockfile catalog.""" dest_dir = Path(args.destination) @@ -206,6 +238,12 @@ def _cmd_install(args: argparse.Namespace) -> int: _extract_member(binary, download, extracted, tool) if extracted.exists(): _place_binary(extracted, destination) + elif kind == "archive-dir": + extracted_dir = tmp / "extracted_dir" + extracted_dir.mkdir() + _extract_dir(binary, download, extracted_dir, tool) + shutil.copytree(str(extracted_dir), str(dest_dir), dirs_exist_ok=True) + (dest_dir / tool).chmod(0o755) else: raise SystemExit(f"Unsupported kind '{kind}' for {tool}")