diff --git a/.devcontainer/.gitignore b/.devcontainer/.gitignore new file mode 100644 index 0000000..4ea2e1d --- /dev/null +++ b/.devcontainer/.gitignore @@ -0,0 +1,2 @@ +.zsh_history +.generated/ diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 456f2ab..d781ac6 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,60 +1,28 @@ -// For format details, see https://aka.ms/devcontainer.json. For config options, see the -// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-dockerfile +// For format details, see https://containers.dev/implementors/json_reference/ { "name": "librmcs-develop", - "image": "qzhhhi/librmcs-develop:latest", - "privileged": true, - "mounts": [ - { - "source": "/dev", - "target": "/dev", - "type": "bind" - }, - { - "source": "/tmp/.X11-unix", - "target": "/tmp/.X11-unix", - "type": "bind" - }, - { - "source": "${localEnv:HOME}", - "target": "/mnt/home", - "type": "bind" - } - ], - "onCreateCommand": "bash -c '[ -d /mnt/home/.codex ] && ln -sfn /mnt/home/.codex ~/.codex; [ -d /mnt/home/.claude ] && ln -sfn /mnt/home/.claude ~/.claude; true'", - "postCreateCommand": "bash -c '[ -d ~/.codex ] && sudo npm i -g @openai/codex; [ -d ~/.claude ] && curl -fsSL https://claude.ai/install.sh | bash && sudo ln -sf ~/.local/bin/claude /usr/local/bin/claude; true'", - "containerEnv": { - "DISPLAY": "${localEnv:DISPLAY}", - "HTTP_PROXY": "${localEnv:HTTP_PROXY}", - "HTTPS_PROXY": "${localEnv:HTTPS_PROXY}", - "NO_PROXY": "${localEnv:NO_PROXY}", - "http_proxy": "${localEnv:http_proxy}", - "https_proxy": "${localEnv:https_proxy}", - "no_proxy": "${localEnv:no_proxy}", - "HOST_WORKSPACE_FOLDER": "${localWorkspaceFolder}" - }, - "runArgs": [ - "--network=host" + "dockerComposeFile": [ + "docker-compose.yml", + ".generated/docker-compose.local.override.yml" ], + "service": "librmcs-develop", + "workspaceFolder": "/workspaces/librmcs", + "initializeCommand": "bash .devcontainer/scripts/generate-local-override.sh && bash .devcontainer/scripts/probe-host-tools.sh", + "postCreateCommand": "bash .devcontainer/scripts/setup-shell-history.sh && bash .devcontainer/scripts/bootstrap-tools.sh", "customizations": { "vscode": { "settings": { "remote.autoForwardPorts": false }, "extensions": [ - // C++ language support "llvm-vs-code-extensions.vscode-clangd", - // Python language support "ms-python.vscode-pylance", "ms-python.python", "ms-python.debugpy", - // CMake language support "KylinIdeTeam.cmake-intellisence", - // Code spell checking "streetsidesoftware.code-spell-checker", - // Git enhancements "mhutchie.git-graph" ] } } -} \ No newline at end of file +} diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 0000000..ebe7ed2 --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,16 @@ +services: + librmcs-develop: + image: qzhhhi/librmcs-develop:latest + network_mode: host + device_cgroup_rules: + - "c 189:* rw" + init: true + command: /bin/sh -c "while sleep 1000; do :; done" + working_dir: /home/ubuntu + volumes: + - type: bind + source: /dev + target: /dev + - type: bind + source: .. + target: /workspaces/librmcs diff --git a/.devcontainer/scripts/bootstrap-tools.sh b/.devcontainer/scripts/bootstrap-tools.sh new file mode 100755 index 0000000..0065624 --- /dev/null +++ b/.devcontainer/scripts/bootstrap-tools.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash + +set -eu + +installed_tools="" +skipped_tools="" +manifest_path="/workspaces/librmcs/.devcontainer/.generated/host-tools.manifest" + +if [ ! -f "$manifest_path" ]; then + printf 'Warning: host tools manifest not found at %s\n' "$manifest_path" >&2 +fi + +if ! command -v npm >/dev/null 2>&1; then + printf 'Warning: npm is not available in the container, skipping host tool bootstrap.\n' >&2 + exit 0 +fi + +add_item() { + local current_list="$1" + local item="$2" + + if [ -z "$current_list" ]; then + printf '%s' "$item" + else + printf '%s\n%s' "$current_list" "$item" + fi +} + +install_tool() { + local tool_name="$1" + local package_name="$2" + + if [ ! -f "$manifest_path" ]; then + skipped_tools=$(add_item "$skipped_tools" "$tool_name (host manifest missing)") + return 0 + fi + + local manifest_line="" + local version="" + + manifest_line=$(grep -m 1 -E "^${tool_name}=" "$manifest_path" 2>/dev/null || true) + if [ -z "$manifest_line" ]; then + skipped_tools=$(add_item "$skipped_tools" "$tool_name (not installed on host)") + else + version=${manifest_line#*=} + if printf '%s' "$version" | grep -Eq '^(v)?[0-9]+([.][0-9]+)*([-.][0-9A-Za-z]+)*$'; then + version=${version#v} + if sudo npm install -g "$package_name@$version"; then + installed_tools=$(add_item "$installed_tools" "$tool_name@$version") + else + skipped_tools=$(add_item "$skipped_tools" "$tool_name (install failed for $version)") + fi + else + printf 'Warning: skipping %s because version could not be parsed on host\n' "$tool_name" >&2 + skipped_tools=$(add_item "$skipped_tools" "$tool_name (version could not be parsed on host)") + fi + fi +} + +install_tool "codex" "@openai/codex" +install_tool "claude" "@anthropic-ai/claude-code" +install_tool "opencode" "opencode-ai" +install_tool "lark-cli" "@larksuite/cli" + +printf 'Bootstrap summary:\n' +if [ -n "$installed_tools" ]; then + printf 'Installed:\n%s\n' "$installed_tools" +else + printf 'Installed: none\n' +fi + +if [ -n "$skipped_tools" ]; then + printf 'Skipped:\n%s\n' "$skipped_tools" +else + printf 'Skipped: none\n' +fi + +exit 0 diff --git a/.devcontainer/scripts/generate-local-override.sh b/.devcontainer/scripts/generate-local-override.sh new file mode 100755 index 0000000..01f2e30 --- /dev/null +++ b/.devcontainer/scripts/generate-local-override.sh @@ -0,0 +1,122 @@ +#!/usr/bin/env bash +set -eu + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +generated_dir="$SCRIPT_DIR/../.generated" +output_file="$generated_dir/docker-compose.local.override.yml" +repo_root="$(cd "$SCRIPT_DIR/../.." && pwd)" + +environment_entries="" +mounts="" + +escape_yaml_double_quoted() { + value=$1 + value=${value//\\/\\\\} + value=${value//\"/\\\"} + value=${value//$'\n'/\\n} + printf '%s' "$value" +} + +add_env() { + env_name=$1 + env_value=$2 + escaped_env_value=$(escape_yaml_double_quoted "$env_value") + + if [ -n "$environment_entries" ]; then + environment_entries="${environment_entries} ${env_name}: \"${escaped_env_value}\"\n" + else + environment_entries=" ${env_name}: \"${escaped_env_value}\"\n" + fi +} + +add_mount() { + host_path=$1 + target_path=$2 + + if [ -e "$host_path" ]; then + escaped_host_path=$(escape_yaml_double_quoted "$host_path") + escaped_target_path=$(escape_yaml_double_quoted "$target_path") + mounts="${mounts} - type: bind\n source: \"${escaped_host_path}\"\n target: \"${escaped_target_path}\"\n" + fi +} + +add_ro_mount() { + host_path=$1 + target_path=$2 + + if [ -e "$host_path" ]; then + escaped_host_path=$(escape_yaml_double_quoted "$host_path") + escaped_target_path=$(escape_yaml_double_quoted "$target_path") + mounts="${mounts} - type: bind\n source: \"${escaped_host_path}\"\n target: \"${escaped_target_path}\"\n read_only: true\n" + fi +} + +add_env "HOST_WORKSPACE_FOLDER" "$repo_root" + +if [ -n "${DISPLAY:-}" ]; then + add_env "DISPLAY" "$DISPLAY" + add_mount "/tmp/.X11-unix" "/tmp/.X11-unix" +fi +if [ -n "${WAYLAND_DISPLAY:-}" ]; then + wayland_display_name=${WAYLAND_DISPLAY##*/} + if [[ "$WAYLAND_DISPLAY" = /* ]]; then + wayland_socket=$WAYLAND_DISPLAY + elif [ -n "${XDG_RUNTIME_DIR:-}" ]; then + wayland_socket="${XDG_RUNTIME_DIR}/${WAYLAND_DISPLAY}" + else + wayland_socket="" + fi + + if [ -n "$wayland_socket" ] && [ -S "$wayland_socket" ]; then + add_env "WAYLAND_DISPLAY" "$wayland_display_name" + add_env "XDG_RUNTIME_DIR" "/tmp" + add_mount "$wayland_socket" "/tmp/${wayland_display_name}" + fi +fi +if [ -n "${HTTP_PROXY:-}" ]; then + add_env "HTTP_PROXY" "$HTTP_PROXY" +fi +if [ -n "${HTTPS_PROXY:-}" ]; then + add_env "HTTPS_PROXY" "$HTTPS_PROXY" +fi +if [ -n "${NO_PROXY:-}" ]; then + add_env "NO_PROXY" "$NO_PROXY" +fi +if [ -n "${http_proxy:-}" ]; then + add_env "http_proxy" "$http_proxy" +fi +if [ -n "${https_proxy:-}" ]; then + add_env "https_proxy" "$https_proxy" +fi +if [ -n "${no_proxy:-}" ]; then + add_env "no_proxy" "$no_proxy" +fi + +add_mount "$HOME/.codex" "/home/ubuntu/.codex" +add_mount "$HOME/.claude" "/home/ubuntu/.claude" +add_mount "$HOME/.claude.json" "/home/ubuntu/.claude.json" +add_mount "$HOME/.lark-cli" "/home/ubuntu/.lark-cli" +add_mount "$HOME/.config/opencode" "/home/ubuntu/.config/opencode" +add_mount "$HOME/.local/share/lark-cli" "/home/ubuntu/.local/share/lark-cli" +add_mount "$HOME/.local/share/opencode" "/home/ubuntu/.local/share/opencode" +add_ro_mount "$HOME/.agents/skills" "/home/ubuntu/.agents/skills" + +mkdir -p "$generated_dir" + +{ + printf '%s\n' '# Generated by generate-local-override.sh - do not commit' + printf '%s\n' 'services:' + printf '%s\n' ' librmcs-develop:' + printf '%s\n' ' environment:' + if [ -n "$environment_entries" ]; then + printf '%b' "$environment_entries" + else + printf '%s\n' ' {}' + fi + printf '%s\n' ' volumes:' + if [ -n "$mounts" ]; then + printf '%b' "$mounts" + else + printf '%s\n' ' []' + fi +} > "$output_file" diff --git a/.devcontainer/scripts/probe-host-tools.sh b/.devcontainer/scripts/probe-host-tools.sh new file mode 100644 index 0000000..8777ab4 --- /dev/null +++ b/.devcontainer/scripts/probe-host-tools.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash + +set -eu + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +parse_version() { + local version_output="$1" + local parsed_version="" + local version_regex='v?[0-9]+([.][0-9]+)*([-.][0-9A-Za-z]+)*' + + if [[ $version_output =~ $version_regex ]]; then + parsed_version=${BASH_REMATCH[0]} + parsed_version=${parsed_version#v} + printf '%s' "$parsed_version" + return 0 + fi + + return 1 +} + +probe_tool() { + tool_name="$1" + + if ! command -v "$tool_name" >/dev/null 2>&1; then + printf '%s\n' 'absent' + return 0 + fi + + version_output="" + if version_output=$($tool_name --version 2>/dev/null); then + if version=$(parse_version "$version_output"); then + printf '%s\n' "$version" + else + printf '%s\n' 'unparseable' + fi + else + printf '%s\n' 'unparseable' + fi +} + +generated_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)" +manifest_dir="$REPO_ROOT/.devcontainer/.generated" +manifest_path="$manifest_dir/host-tools.manifest" +tmp_manifest_path="$manifest_path.tmp" + +mkdir -p "$manifest_dir" + +codex_version="$(probe_tool codex)" +claude_version="$(probe_tool claude)" +opencode_version="$(probe_tool opencode)" +lark_cli_version="$(probe_tool lark-cli)" + +{ + printf '%s\n' '# Generated by probe-host-tools.sh - do not commit' + printf 'generated_at=%s\n' "$generated_at" + printf 'codex=%s\n' "$codex_version" + printf 'claude=%s\n' "$claude_version" + printf 'opencode=%s\n' "$opencode_version" + printf 'lark-cli=%s\n' "$lark_cli_version" +} > "$tmp_manifest_path" + +mv "$tmp_manifest_path" "$manifest_path" + +printf 'Host tool probe summary:\n' +printf ' codex=%s\n' "$codex_version" +printf ' claude=%s\n' "$claude_version" +printf ' opencode=%s\n' "$opencode_version" +printf ' lark-cli=%s\n' "$lark_cli_version" +printf ' manifest=%s\n' "$manifest_path" +printf 'Tip: rerun `bash /workspaces/librmcs/.devcontainer/scripts/bootstrap-tools.sh` to resync container tool versions with the host.\n' diff --git a/.devcontainer/scripts/setup-shell-history.sh b/.devcontainer/scripts/setup-shell-history.sh new file mode 100644 index 0000000..5b96827 --- /dev/null +++ b/.devcontainer/scripts/setup-shell-history.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +set -eu + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +history_path="$REPO_ROOT/.devcontainer/.zsh_history" +history_link="$HOME/.zsh_history" + +mkdir -p "$(dirname "$history_path")" +touch "$history_path" +rm -f "$history_link" +ln -s "$history_path" "$history_link" + +printf 'Shell history symlinked: %s -> %s\n' "$history_link" "$history_path" diff --git a/.gitignore b/.gitignore index 8bf86ec..14be2ce 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,3 @@ __pycache__/ build/ compile_commands.json - -.devcontainer/.zsh_history \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index b504e23..da6ba70 100644 --- a/Dockerfile +++ b/Dockerfile @@ -127,6 +127,16 @@ EOF # Configure 'ubuntu' user and sudo privileges RUN chsh -s /bin/zsh ubuntu && \ echo "ubuntu ALL=(ALL:ALL) NOPASSWD:ALL" >> /etc/sudoers + +# Precreate generic XDG-style parent directories for direct bind mounts under ubuntu's home. +RUN mkdir -p \ + /home/ubuntu/.agents \ + /home/ubuntu/.cache \ + /home/ubuntu/.config \ + /home/ubuntu/.local/share \ + /home/ubuntu/.local/state && \ + chown -R ubuntu:ubuntu /home/ubuntu/.agents /home/ubuntu/.cache /home/ubuntu/.config /home/ubuntu/.local + WORKDIR /home/ubuntu ENV USER=ubuntu ENV WORKDIR=/home/ubuntu @@ -134,5 +144,4 @@ USER ubuntu # Install Oh My Zsh and configure theme RUN sh -c "$(wget https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh -O -)" \ - && sed -i 's/ZSH_THEME=\"[a-z0-9\\-]*\"/ZSH_THEME="af-magic"/g' ~/.zshrc \ - && ln -s /workspaces/librmcs/.devcontainer/.zsh_history ~/.zsh_history + && sed -i 's/ZSH_THEME=\"[a-z0-9\\-]*\"/ZSH_THEME="af-magic"/g' ~/.zshrc