From 955fe2afab041addd9ede528040cbe4e2c7ef219 Mon Sep 17 00:00:00 2001 From: jakobst1n Date: Sun, 31 May 2026 11:18:00 +0200 Subject: Make a simple devcontainer-system for neovim and opencode --- .gitignore | 1 + Common/devcontainer/Dockerfile-all.m4 | 5 + Common/devcontainer/Dockerfile-base.m4 | 76 ++++++ Common/devcontainer/Dockerfile-python.m4 | 11 + Common/devcontainer/Dockerfile-svelte.m4 | 7 + Common/devcontainer/Makefile | 18 ++ Common/devcontainer/supervisord.conf | 15 ++ bin/devcontainer.sh.m4 | 437 +++++++++++++++++++++++++++++++ 8 files changed, 570 insertions(+) create mode 100644 Common/devcontainer/Dockerfile-all.m4 create mode 100644 Common/devcontainer/Dockerfile-base.m4 create mode 100644 Common/devcontainer/Dockerfile-python.m4 create mode 100644 Common/devcontainer/Dockerfile-svelte.m4 create mode 100644 Common/devcontainer/Makefile create mode 100644 Common/devcontainer/supervisord.conf create mode 100755 bin/devcontainer.sh.m4 diff --git a/.gitignore b/.gitignore index 37807f2..171793b 100644 --- a/.gitignore +++ b/.gitignore @@ -101,3 +101,4 @@ Common/newsboat/urls bin/pingme bin/autoclean.sh +bin/devcontainer.sh diff --git a/Common/devcontainer/Dockerfile-all.m4 b/Common/devcontainer/Dockerfile-all.m4 new file mode 100644 index 0000000..edf5b02 --- /dev/null +++ b/Common/devcontainer/Dockerfile-all.m4 @@ -0,0 +1,5 @@ +m4_ifdef(`DEF_BASEIMAGE', `', `m4_define(DEF_BASEIMAGE)m4_dnl +FROM BASEIMAGE') + +m4_include(`Dockerfile-python.m4') +m4_include(`Dockerfile-svelte.m4') diff --git a/Common/devcontainer/Dockerfile-base.m4 b/Common/devcontainer/Dockerfile-base.m4 new file mode 100644 index 0000000..6cc85f1 --- /dev/null +++ b/Common/devcontainer/Dockerfile-base.m4 @@ -0,0 +1,76 @@ +FROM docker.io/debian:trixie + +ARG DOT_HOME="/root" +ARG DOTFILES_REPO="https://git.jakobstendahl.no/jakobst1n/dotfiles" +ARG GIT_EMAIL="jakob@jakobstendahl.no" +ARG GIT_USER="jakobst1n" + +# Install base requirements +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + fzf \ + man-db \ + less \ + supervisor \ + git \ + make \ + dialog \ + m4 \ + npm \ + sudo + +# Create and switch to non-root-user +#RUN useradd -d ${DOT_HOME} -m devuser +#RUN usermod -a -G sudo devuser +#RUN echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers + +WORKDIR ${DOT_HOME} +#USER devuser + +# Install dotfiles +RUN mkdir -p ${DOT_HOME}/ +RUN git clone ${DOTFILES_REPO} ${DOT_HOME}/dotfiles +WORKDIR ${DOT_HOME}/dotfiles +RUN cat > config < Makefile +RUN make Makefile +RUN make && rm -rf /tmp/nvim && rm -rf /var/lib/api/lists/* +RUN nvim --headless +qa + +# Create project directory +#RUN mkdir -p ${DOT_HOME}/project +#WORKDIR ${DOT_HOME}/project +RUN mkdir -p /workspace +WORKDIR /workspace + +# Install usefull tools +RUN npm i -g opencode-ai + +# Copy supervisord conf +COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf +#RUN mkdir -p ${DOT_HOME}/.local/share/supervisord && \ +# touch ${DOT_HOME}/.local/share/supervisord/supervisord.log +#RUN touch ${DOT_HOME}/.local/share/supervisord/supervisord.pid && \ +# chown devuser:devuser /var/run/supervisord.pid + +CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] diff --git a/Common/devcontainer/Dockerfile-python.m4 b/Common/devcontainer/Dockerfile-python.m4 new file mode 100644 index 0000000..c52c4e1 --- /dev/null +++ b/Common/devcontainer/Dockerfile-python.m4 @@ -0,0 +1,11 @@ +m4_ifdef(`DEF_BASEIMAGE', `', `m4_define(DEF_BASEIMAGE)m4_dnl +FROM BASEIMAGE') + + +RUN apt-get update && apt-get install -y --no-install-recommends \ + python3 \ + python3-pip && \ + rm -rf /var/lib/api/lists/* + +RUN nvim --headless "+MasonInstallSync pywright" "+q!" + diff --git a/Common/devcontainer/Dockerfile-svelte.m4 b/Common/devcontainer/Dockerfile-svelte.m4 new file mode 100644 index 0000000..5539cc4 --- /dev/null +++ b/Common/devcontainer/Dockerfile-svelte.m4 @@ -0,0 +1,7 @@ +m4_ifdef(`DEF_BASEIMAGE', `', `m4_define(DEF_BASEIMAGE)m4_dnl +FROM BASEIMAGE') + +RUN apt-get update && apt-get install -y --no-install-recommends && \ + rm -rf /var/lib/api/lists/* + +RUN nvim --headless "+MasonInstallSync svelte-language-server" "+q!" diff --git a/Common/devcontainer/Makefile b/Common/devcontainer/Makefile new file mode 100644 index 0000000..b5e53f9 --- /dev/null +++ b/Common/devcontainer/Makefile @@ -0,0 +1,18 @@ +BASEIMAGE := devcontainer-base +M4_SRCS := $(wildcard *.m4) +TARGETS := $(patsubst %.m4,%,$(M4_SRCS)) + +.PHONE: all clean + +all: $(TARGETS) + +Dockerfile-base: Dockerfile-base.m4 + m4 -P -D BASEIMAGE="$(BASEIMAGE)" $< > $@ + podman build -t "$(patsubst Dockerfile-%,devcontainer-%,$@)" -f $@ + +%: %.m4 + m4 -P -D BASEIMAGE="$(BASEIMAGE)" $< > $@ + podman build -t "$(patsubst Dockerfile-%,devcontainer-%,$@)" -f $@ + +clean: + rm -f $(TARGETS) diff --git a/Common/devcontainer/supervisord.conf b/Common/devcontainer/supervisord.conf new file mode 100644 index 0000000..0c7314b --- /dev/null +++ b/Common/devcontainer/supervisord.conf @@ -0,0 +1,15 @@ +[supervisord] +nodaemon=true +#user=devuser +logfile=/var/log/supervisord.log +pidfile=/var/run/supervisord.pid + +[program:nvim-headless] +command=nvim --headless --listen 0.0.0.0:7778 +autostart=true +autorestart=true + +[program:opencode] +command=opencode web --port 4096 --hostname 0.0.0.0 +autostart=true +autorestart=true diff --git a/bin/devcontainer.sh.m4 b/bin/devcontainer.sh.m4 new file mode 100755 index 0000000..e202712 --- /dev/null +++ b/bin/devcontainer.sh.m4 @@ -0,0 +1,437 @@ +#!/bin/sh + +set -eu + +SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +REPO_ROOT=$(CDPATH= cd -- "${SCRIPT_DIR}/.." && pwd) +DEVCONTAINER_DIR="${REPO_ROOT}/Common/devcontainer" +DEVCONTAINER_DIR="DT_DOTFILES_DIR/Common/devcontainer" +WORKDIR=$(pwd) +HOMEDIR=${HOME} +CONTAINER_TOOL=${CONTAINER_TOOL:-} +DEFAULT_VARIANT=base +BASE_CONTAINER_PORT=7778 +OPENCODE_CONTAINER_PORT=4096 + +detect_container_tool() { + if [ -n "${CONTAINER_TOOL}" ]; then + if command -v "${CONTAINER_TOOL}" >/dev/null 2>&1; then + printf '%s\n' "${CONTAINER_TOOL}" + return 0 + fi + + printf 'Configured container tool not found: %s\n' "${CONTAINER_TOOL}" >&2 + return 1 + fi + + if command -v podman >/dev/null 2>&1; then + printf 'podman\n' + return 0 + fi + + if command -v docker >/dev/null 2>&1; then + printf 'docker\n' + return 0 + fi + + printf 'Neither podman nor docker is available.\n' >&2 + return 1 +} + +ENGINE= + +require_engine() { + if [ -z "${ENGINE}" ]; then + ENGINE=$(detect_container_tool) || exit 1 + printf 'Container runtime: %s\n' "${ENGINE}" >&2 + fi +} + +usage() { + cat < Open one of: shell, nvim, opencode + images List available image variants from Common/devcontainer + help Show this help + +Options: + -i, --image Image variant to use for start/restart (default: ${DEFAULT_VARIANT}) + +Examples: + $(basename "$0") start --image python + $(basename "$0") status + $(basename "$0") open opencode +EOF +} + +available_variants() { + for file in "${DEVCONTAINER_DIR}"/Dockerfile*; do + [ -f "${file}" ] || continue + name=$(basename "${file}") + case "${name}" in + Dockerfile) + printf 'base\n' + ;; + Dockerfile-*.m4) + ;; + Dockerfile-*) + printf '%s\n' "${name#Dockerfile-}" + ;; + esac + done | sort -u +} + +variant_exists() { + wanted=$1 + available_variants | grep -Fx -- "${wanted}" >/dev/null 2>&1 +} + +image_for_variant() { + variant=$1 + if [ "${variant}" = "base" ]; then + printf 'devcontainer-base\n' + return 0 + fi + + printf 'devcontainer-%s\n' "${variant}" +} + +sanitize_name() { + printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9' '-' +} + +dir_hash() { + printf '%s\n' "${WORKDIR}" | cksum | awk '{print $1}' +} + +container_name() { + base=$(basename "${WORKDIR}") + safe_base=$(sanitize_name "${base}") + printf 'devco-%s-%s\n' "${safe_base}" "$(dir_hash)" +} + +container_exists() { + require_engine + name=$(container_name) + "${ENGINE}" container inspect "${name}" >/dev/null 2>&1 +} + +container_running() { + require_engine + name=$(container_name) + state=$("${ENGINE}" inspect --format '{{.State.Running}}' "${name}" 2>/dev/null || true) + [ "${state}" = "true" ] +} + +require_container() { + if ! container_exists; then + printf 'No devcontainer exists for "%s".\n' "${WORKDIR}" >&2 + exit 1 + fi +} + +require_running_container() { + require_container + if ! container_running; then + printf 'Devcontainer exists for "%s" but is not running.\n' "${WORKDIR}" >&2 + exit 1 + fi +} + +port_for() { + require_engine + name=$(container_name) + port=$1 + "${ENGINE}" port "${name}" "${port}/tcp" 2>/dev/null | awk -F: 'NR==1 {print $NF}' +} + +show_ports() { + require_running_container + nvim_port=$(port_for "${BASE_CONTAINER_PORT}") + opencode_port=$(port_for "${OPENCODE_CONTAINER_PORT}") + printf 'nvim: %s\n' "${nvim_port:-not mapped}" + printf 'opencode: %s\n' "${opencode_port:-not mapped}" + if [ -n "${opencode_port:-}" ]; then + printf 'URL: http://127.0.0.1:%s\n' "${opencode_port}" + fi +} + +start_container() { + if [ "$1" != "base" ]; then +# Check if the Dockerfile exists for the requested variant + if [ ! -f "${DEVCONTAINER_DIR}/Dockerfile-$1" ]; then + # Check if the Makefile exists + if [ -f "${DEVCONTAINER_DIR}/Makefile" ]; then + # Run the Makefile to build the Dockerfile + make -C "${DEVCONTAINER_DIR}" VARIANT=$1 + + if [ $? -ne 0 ]; then + printf 'Failed to build Dockerfile for variant %s.\n' "$1" >&2 + exit 1 + fi + else + printf 'Makefile not found in %s directory.\n' "${DEVCONTAINER_DIR}" >&2 + + printf 'To build this variant, create a Dockerfile-%s file in the %s directory.\n' "$1" "${DEVCONTAINER_DIR}" + exit 1 + fi + fi + + + # Rest of the start_container function + + variant=$1 + + if ! variant_exists "${variant}" && [ "${variant}" != base ]; then + printf 'Unknown image variant: %s\n' "${variant}" >&2 + printf 'Available variants:\n' >&2 + available_variants | sed 's/^/ - /' >&2 + exit 1 + fi + + image=$(image_for_variant "${variant}") + name=$(container_name) + + if container_exists; then + "${ENGINE}" rm -f "${name}" >/dev/null 2>&1 || true + fi + + printf 'Starting devcontainer for "%s"\n' "${WORKDIR}" + printf 'Image: %s\n' "${image}" + printf 'Container: %s\n' "${name}" + + require_engine + + "${ENGINE}" run \ + --rm \ + --detach \ + --name "${name}" \ + --label devcontainer.managed=true \ + --label "devcontainer.dir=${WORKDIR}" \ + --label "devcontainer.variant=${variant}" \ + -p :${BASE_CONTAINER_PORT} \ + -p :${OPENCODE_CONTAINER_PORT} \ + -v "${HOMEDIR}/.local/share/opencode:/root/.local/share/opencode" \ + -v "${WORKDIR}:/workspace" \ + "${image}" >/dev/null + + printf 'Directory: %s\n' "${WORKDIR}" + printf 'Container: %s\n' "${name}" + show_ports + fi + + # Rest of the start_container function + + + + # Rest of the start_container function + + variant=$1 + + if ! variant_exists "${variant}" && [ "${variant}" != base ]; then + + + + exit 1 + fi + + image=$(image_for_variant "${variant}") + name=$(container_name) + + if container_exists; then + "${ENGINE}" rm -f "${name}" >/dev/null 2>&1 || true + fi + + printf 'Starting devcontainer for "%s"\n' "${WORKDIR}" + printf 'Image: %s\n' "${image}" + printf 'Container: %s\n' "${name}" + + require_engine + + "${ENGINE}" run \ + --rm \ + --detach \ + --name "${name}" \ + --label devcontainer.managed=true \ + --label "devcontainer.dir=${WORKDIR}" \ + --label "devcontainer.variant=${variant}" \ + -p :${BASE_CONTAINER_PORT} \ + -p :${OPENCODE_CONTAINER_PORT} \ + -v "${HOMEDIR}/.local/share/opencode:/root/.local/share/opencode" \ + -v "${WORKDIR}:/workspace" \ + "${image}" >/dev/null + + printf 'Directory: %s\n' "${WORKDIR}" + printf 'Container: %s\n' "${name}" + show_ports +} + +stop_container() { + require_container + name=$(container_name) + printf 'Stopping %s\n' "${name}" + "${ENGINE}" stop "${name}" >/dev/null +} + +status_container() { + if ! container_exists; then + printf 'No devcontainer exists for "%s".\n' "${WORKDIR}" + return 0 + fi + + name=$(container_name) + running=false + if container_running; then + running=true + fi + + variant=$("${ENGINE}" inspect --format '{{ index .Config.Labels "devcontainer.variant" }}' "${name}" 2>/dev/null || true) + image=$("${ENGINE}" inspect --format '{{.Config.Image}}' "${name}" 2>/dev/null || true) + + printf 'Directory: %s\n' "${WORKDIR}" + printf 'Container: %s\n' "${name}" + printf 'Running: %s\n' "${running}" + printf 'Image: %s\n' "${image}" + printf 'Variant: %s\n' "${variant}" + + if [ "${running}" = true ]; then + show_ports + fi +} + +open_shell() { + require_running_container + "${ENGINE}" exec -it "$(container_name)" /bin/bash +} + +open_nvim() { + require_running_container + "${ENGINE}" exec -it "$(container_name)" nvim "$@" +} + +open_opencode() { + require_running_container + opencode_port=$(port_for "${OPENCODE_CONTAINER_PORT}") + if [ -z "${opencode_port}" ]; then + printf 'Could not determine the opencode port.\n' >&2 + exit 1 + fi + + url="http://127.0.0.1:${opencode_port}" + printf '%s\n' "${url}" + + if command -v xdg-open >/dev/null 2>&1; then + xdg-open "${url}" >/dev/null 2>&1 || true + fi +} + +list_images() { + available_variants +} + +COMMAND=${1:-start} +if [ $# -gt 0 ]; then + shift +fi + +IMAGE_VARIANT=${DEFAULT_VARIANT} + +while [ $# -gt 0 ]; do + case "$1" in + -i|--image) + if [ $# -lt 2 ] || [ "$2" = "" ]; then + printf 'Missing value for %s\n' "$1" >&2 + exit 1 + fi + IMAGE_VARIANT=$2 + shift 2 + ;; + --help|-h) + usage + exit 0 + ;; + --) + shift + break + ;; + *) + break + ;; + esac +done + +case "${COMMAND}" in + start) + start_container "${IMAGE_VARIANT}" + ;; + stop) + stop_container + ;; + restart) +if container_running || [ "$1" != "base" ]; then + stop_container +elif container_exists; then + + + # Removing an exited container avoids a replace failure when --rm was not triggered. + "${ENGINE}" rm "$(container_name)" >/dev/null 2>&1 || true + fi + start_container "${IMAGE_VARIANT}" + ;; + status) + status_container + ;; + ports) + show_ports + ;; + shell) + open_shell + ;; + nvim) + open_nvim "$@" + ;; + opencode) + open_opencode + ;; + open) + target=${1:-} + shift || true + case "${target}" in + shell) + open_shell + ;; + nvim) + open_nvim "$@" + ;; + opencode) + open_opencode + ;; + *) + printf 'Unknown open target: %s\n' "${target}" >&2 + printf 'Supported targets: shell, nvim, opencode\n' >&2 + exit 1 + ;; + esac + ;; + images) + list_images + ;; + help) + usage + ;; + *) + printf 'Unknown command: %s\n' "${COMMAND}" >&2 + usage >&2 + exit 1 + ;; +esac -- cgit v1.2.3