Skip to content

Commit fc0bb78

Browse files
authored
feat(devcontainer): add compose-based local devcontainer bootstrap (#65)
- Move the devcontainer setup to docker-compose and generate a local override for host-specific mounts and environment. - Bootstrap shell history and optional host-matched CLI tools inside the container to make local development setup more reproducible.
1 parent 23d3469 commit fc0bb78

8 files changed

Lines changed: 332 additions & 42 deletions

File tree

.devcontainer/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.zsh_history
2+
.generated/

.devcontainer/devcontainer.json

Lines changed: 26 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,28 @@
1-
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
2-
// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-dockerfile
1+
// For format details, see https://containers.dev/implementors/json_reference/
32
{
4-
"name": "rmcs-develop",
5-
"image": "qzhhhi/rmcs-develop:latest-full",
6-
"privileged": true,
7-
"mounts": [
8-
{
9-
"source": "/dev",
10-
"target": "/dev",
11-
"type": "bind"
12-
},
13-
{
14-
"source": "/tmp/.X11-unix",
15-
"target": "/tmp/.X11-unix",
16-
"type": "bind"
17-
}
18-
],
19-
"containerEnv": {
20-
"DISPLAY": "${localEnv:DISPLAY}"
21-
},
22-
"runArgs": [
23-
"--network",
24-
"host"
25-
],
26-
"customizations": {
27-
"vscode": {
28-
"extensions": [
29-
// C++ language support
30-
"llvm-vs-code-extensions.vscode-clangd",
31-
// Python language support
32-
"ms-python.vscode-pylance",
33-
"ms-python.python",
34-
"ms-python.debugpy",
35-
// CMake language support
36-
"twxs.cmake",
37-
// Code spell checking
38-
"streetsidesoftware.code-spell-checker",
39-
// Git enhancements
40-
"mhutchie.git-graph"
41-
]
42-
}
43-
}
3+
"name": "rmcs-develop",
4+
"dockerComposeFile": [
5+
"docker-compose.yml",
6+
".generated/docker-compose.local.override.yml"
7+
],
8+
"service": "rmcs-develop",
9+
"workspaceFolder": "/workspaces/RMCS",
10+
"initializeCommand": "bash .devcontainer/scripts/generate-local-override.sh && bash .devcontainer/scripts/probe-host-tools.sh",
11+
"postCreateCommand": "bash .devcontainer/scripts/setup-shell-history.sh && bash .devcontainer/scripts/bootstrap-tools.sh",
12+
"customizations": {
13+
"vscode": {
14+
"settings": {
15+
"remote.autoForwardPorts": false
16+
},
17+
"extensions": [
18+
"llvm-vs-code-extensions.vscode-clangd",
19+
"ms-python.vscode-pylance",
20+
"ms-python.python",
21+
"ms-python.debugpy",
22+
"KylinIdeTeam.cmake-intellisence",
23+
"streetsidesoftware.code-spell-checker",
24+
"mhutchie.git-graph"
25+
]
26+
}
27+
}
4428
}

.devcontainer/docker-compose.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
services:
2+
rmcs-develop:
3+
image: qzhhhi/rmcs-develop:latest-full
4+
privileged: true
5+
network_mode: host
6+
init: true
7+
command: /bin/sh -c "while sleep 1000; do :; done"
8+
working_dir: /home/ubuntu
9+
volumes:
10+
- type: bind
11+
source: /dev
12+
target: /dev
13+
- type: bind
14+
source: /tmp/.X11-unix
15+
target: /tmp/.X11-unix
16+
- type: bind
17+
source: ..
18+
target: /workspaces/RMCS
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
#!/usr/bin/env bash
2+
3+
set -eu
4+
5+
installed_tools=""
6+
skipped_tools=""
7+
manifest_path="/workspaces/RMCS/.devcontainer/.generated/host-tools.manifest"
8+
9+
if [ ! -f "$manifest_path" ]; then
10+
printf 'Warning: host tools manifest not found at %s\n' "$manifest_path" >&2
11+
fi
12+
13+
if ! command -v npm >/dev/null 2>&1; then
14+
printf 'Warning: npm is not available in the container, skipping host tool bootstrap.\n' >&2
15+
exit 0
16+
fi
17+
18+
add_item() {
19+
local current_list="$1"
20+
local item="$2"
21+
22+
if [ -z "$current_list" ]; then
23+
printf '%s' "$item"
24+
else
25+
printf '%s\n%s' "$current_list" "$item"
26+
fi
27+
}
28+
29+
install_tool() {
30+
local tool_name="$1"
31+
local package_name="$2"
32+
33+
if [ ! -f "$manifest_path" ]; then
34+
skipped_tools=$(add_item "$skipped_tools" "$tool_name (host manifest missing)")
35+
return 0
36+
fi
37+
38+
local manifest_line=""
39+
local version=""
40+
41+
manifest_line=$(grep -m 1 -E "^${tool_name}=" "$manifest_path" 2>/dev/null || true)
42+
if [ -z "$manifest_line" ]; then
43+
skipped_tools=$(add_item "$skipped_tools" "$tool_name (not installed on host)")
44+
else
45+
version=${manifest_line#*=}
46+
if printf '%s' "$version" | grep -Eq '^(v)?[0-9]+([.][0-9]+)*([-.][0-9A-Za-z]+)*$'; then
47+
version=${version#v}
48+
if sudo npm install -g "$package_name@$version"; then
49+
installed_tools=$(add_item "$installed_tools" "$tool_name@$version")
50+
else
51+
skipped_tools=$(add_item "$skipped_tools" "$tool_name (install failed for $version)")
52+
fi
53+
else
54+
printf 'Warning: skipping %s because version could not be parsed on host\n' "$tool_name" >&2
55+
skipped_tools=$(add_item "$skipped_tools" "$tool_name (version could not be parsed on host)")
56+
fi
57+
fi
58+
}
59+
60+
install_tool "codex" "@openai/codex"
61+
install_tool "claude" "@anthropic-ai/claude-code"
62+
install_tool "opencode" "opencode-ai"
63+
install_tool "lark-cli" "@larksuite/cli"
64+
65+
printf 'Bootstrap summary:\n'
66+
if [ -n "$installed_tools" ]; then
67+
printf 'Installed:\n%s\n' "$installed_tools"
68+
else
69+
printf 'Installed: none\n'
70+
fi
71+
72+
if [ -n "$skipped_tools" ]; then
73+
printf 'Skipped:\n%s\n' "$skipped_tools"
74+
else
75+
printf 'Skipped: none\n'
76+
fi
77+
78+
exit 0
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
#!/usr/bin/env bash
2+
set -eu
3+
4+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5+
generated_dir="$SCRIPT_DIR/../.generated"
6+
output_file="$generated_dir/docker-compose.local.override.yml"
7+
repo_root="$(cd "$SCRIPT_DIR/../.." && pwd)"
8+
9+
environment_entries=""
10+
mounts=""
11+
12+
escape_yaml_double_quoted() {
13+
value=$1
14+
value=${value//\\/\\\\}
15+
value=${value//\"/\\\"}
16+
value=${value//$'\n'/\\n}
17+
printf '%s' "$value"
18+
}
19+
20+
add_env() {
21+
env_name=$1
22+
env_value=$2
23+
escaped_env_value=$(escape_yaml_double_quoted "$env_value")
24+
25+
if [ -n "$environment_entries" ]; then
26+
environment_entries="${environment_entries} ${env_name}: \"${escaped_env_value}\"\n"
27+
else
28+
environment_entries=" ${env_name}: \"${escaped_env_value}\"\n"
29+
fi
30+
}
31+
32+
add_mount() {
33+
host_path=$1
34+
target_path=$2
35+
36+
if [ -e "$host_path" ]; then
37+
escaped_host_path=$(escape_yaml_double_quoted "$host_path")
38+
escaped_target_path=$(escape_yaml_double_quoted "$target_path")
39+
mounts="${mounts} - type: bind\n source: \"${escaped_host_path}\"\n target: \"${escaped_target_path}\"\n"
40+
fi
41+
}
42+
43+
add_ro_mount() {
44+
host_path=$1
45+
target_path=$2
46+
47+
if [ -e "$host_path" ]; then
48+
escaped_host_path=$(escape_yaml_double_quoted "$host_path")
49+
escaped_target_path=$(escape_yaml_double_quoted "$target_path")
50+
mounts="${mounts} - type: bind\n source: \"${escaped_host_path}\"\n target: \"${escaped_target_path}\"\n read_only: true\n"
51+
fi
52+
}
53+
54+
add_env "HOST_WORKSPACE_FOLDER" "$repo_root"
55+
56+
if [ -n "${DISPLAY:-}" ]; then
57+
add_env "DISPLAY" "$DISPLAY"
58+
fi
59+
if [ -n "${HTTP_PROXY:-}" ]; then
60+
add_env "HTTP_PROXY" "$HTTP_PROXY"
61+
fi
62+
if [ -n "${HTTPS_PROXY:-}" ]; then
63+
add_env "HTTPS_PROXY" "$HTTPS_PROXY"
64+
fi
65+
if [ -n "${NO_PROXY:-}" ]; then
66+
add_env "NO_PROXY" "$NO_PROXY"
67+
fi
68+
if [ -n "${http_proxy:-}" ]; then
69+
add_env "http_proxy" "$http_proxy"
70+
fi
71+
if [ -n "${https_proxy:-}" ]; then
72+
add_env "https_proxy" "$https_proxy"
73+
fi
74+
if [ -n "${no_proxy:-}" ]; then
75+
add_env "no_proxy" "$no_proxy"
76+
fi
77+
78+
add_mount "$HOME/.codex" "/home/ubuntu/.codex"
79+
add_mount "$HOME/.claude" "/home/ubuntu/.claude"
80+
add_mount "$HOME/.claude.json" "/home/ubuntu/.claude.json"
81+
add_mount "$HOME/.lark-cli" "/home/ubuntu/.lark-cli"
82+
add_mount "$HOME/.config/opencode" "/home/ubuntu/.config/opencode"
83+
add_mount "$HOME/.local/share/lark-cli" "/home/ubuntu/.local/share/lark-cli"
84+
add_mount "$HOME/.local/share/opencode" "/home/ubuntu/.local/share/opencode"
85+
add_ro_mount "$HOME/.agents/skills" "/home/ubuntu/.agents/skills"
86+
87+
mkdir -p "$generated_dir"
88+
89+
{
90+
printf '%s\n' '# Generated by generate-local-override.sh - do not commit'
91+
printf '%s\n' 'services:'
92+
printf '%s\n' ' rmcs-develop:'
93+
printf '%s\n' ' environment:'
94+
if [ -n "$environment_entries" ]; then
95+
printf '%b' "$environment_entries"
96+
else
97+
printf '%s\n' ' {}'
98+
fi
99+
printf '%s\n' ' volumes:'
100+
if [ -n "$mounts" ]; then
101+
printf '%b' "$mounts"
102+
else
103+
printf '%s\n' ' []'
104+
fi
105+
} > "$output_file"
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
#!/usr/bin/env bash
2+
3+
set -eu
4+
5+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6+
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
7+
8+
parse_version() {
9+
local version_output="$1"
10+
local parsed_version=""
11+
local version_regex='v?[0-9]+([.][0-9]+)*([-.][0-9A-Za-z]+)*'
12+
13+
if [[ $version_output =~ $version_regex ]]; then
14+
parsed_version=${BASH_REMATCH[0]}
15+
parsed_version=${parsed_version#v}
16+
printf '%s' "$parsed_version"
17+
return 0
18+
fi
19+
20+
return 1
21+
}
22+
23+
probe_tool() {
24+
tool_name="$1"
25+
26+
if ! command -v "$tool_name" >/dev/null 2>&1; then
27+
printf '%s\n' 'absent'
28+
return 0
29+
fi
30+
31+
version_output=""
32+
if version_output=$($tool_name --version 2>/dev/null); then
33+
if version=$(parse_version "$version_output"); then
34+
printf '%s\n' "$version"
35+
else
36+
printf '%s\n' 'unparseable'
37+
fi
38+
else
39+
printf '%s\n' 'unparseable'
40+
fi
41+
}
42+
43+
generated_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
44+
manifest_dir="$REPO_ROOT/.devcontainer/.generated"
45+
manifest_path="$manifest_dir/host-tools.manifest"
46+
tmp_manifest_path="$manifest_path.tmp"
47+
48+
mkdir -p "$manifest_dir"
49+
50+
codex_version="$(probe_tool codex)"
51+
claude_version="$(probe_tool claude)"
52+
opencode_version="$(probe_tool opencode)"
53+
lark_cli_version="$(probe_tool lark-cli)"
54+
55+
{
56+
printf '%s\n' '# Generated by probe-host-tools.sh - do not commit'
57+
printf 'generated_at=%s\n' "$generated_at"
58+
printf 'codex=%s\n' "$codex_version"
59+
printf 'claude=%s\n' "$claude_version"
60+
printf 'opencode=%s\n' "$opencode_version"
61+
printf 'lark-cli=%s\n' "$lark_cli_version"
62+
} > "$tmp_manifest_path"
63+
64+
mv "$tmp_manifest_path" "$manifest_path"
65+
66+
printf 'Host tool probe summary:\n'
67+
printf ' codex=%s\n' "$codex_version"
68+
printf ' claude=%s\n' "$claude_version"
69+
printf ' opencode=%s\n' "$opencode_version"
70+
printf ' lark-cli=%s\n' "$lark_cli_version"
71+
printf ' manifest=%s\n' "$manifest_path"
72+
printf 'Tip: rerun `bash /workspaces/RMCS/.devcontainer/scripts/bootstrap-tools.sh` to resync container tool versions with the host.\n'
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/usr/bin/env bash
2+
3+
set -eu
4+
5+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6+
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
7+
8+
history_path="$REPO_ROOT/.devcontainer/.zsh_history"
9+
history_link="$HOME/.zsh_history"
10+
11+
mkdir -p "$(dirname "$history_path")"
12+
touch "$history_path"
13+
rm -f "$history_link"
14+
ln -s "$history_path" "$history_link"
15+
16+
printf 'Shell history symlinked: %s -> %s\n' "$history_link" "$history_path"

Dockerfile

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,12 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
113113
apt-get autoremove -y && apt-get clean && \
114114
rm -rf /var/lib/apt/lists/* /tmp/*
115115

116+
# Install Node.js 24 LTS (required by agent CLIs)
117+
RUN curl -fsSL https://deb.nodesource.com/setup_24.x | bash - && \
118+
apt-get install -y --no-install-recommends nodejs && \
119+
apt-get autoremove -y && apt-get clean && \
120+
rm -rf /var/lib/apt/lists/* /tmp/*
121+
116122
# Install llvm-toolchain
117123
ARG LLVM_VERSION=22
118124
RUN mkdir -p /etc/apt/keyrings && \
@@ -172,6 +178,15 @@ RUN case "${TARGETARCH}" in \
172178
# Change user
173179
RUN chsh -s /bin/zsh ubuntu && \
174180
echo "ubuntu ALL=(ALL:ALL) NOPASSWD:ALL" >> /etc/sudoers
181+
182+
# Precreate generic XDG-style parent directories for direct bind mounts under ubuntu's home.
183+
RUN mkdir -p \
184+
/home/ubuntu/.agents \
185+
/home/ubuntu/.cache \
186+
/home/ubuntu/.config \
187+
/home/ubuntu/.local/share \
188+
/home/ubuntu/.local/state && \
189+
chown -R ubuntu:ubuntu /home/ubuntu/.agents /home/ubuntu/.cache /home/ubuntu/.config /home/ubuntu/.local
175190
WORKDIR /home/ubuntu
176191
ENV USER=ubuntu
177192
ENV WORKDIR=/home/ubuntu

0 commit comments

Comments
 (0)