diff --git a/internal/provision/devcontainer/go/install.go b/internal/provision/devcontainer/go/install.go index 94a7cdc..fd6ab73 100644 --- a/internal/provision/devcontainer/go/install.go +++ b/internal/provision/devcontainer/go/install.go @@ -17,6 +17,9 @@ func Install(cfg state.Config) error { if err := executil.RunEmbeddedScript( "devcontainer/go/install.sh", + "RUNTIME=go", + "ARCHIVE_EXT=tar.gz", + "RUNTIME_VERSION="+cfg.Version, ); err != nil { return fmt.Errorf("go devcontainer install failed: %w", err) } diff --git a/internal/provision/devcontainer/java/install.go b/internal/provision/devcontainer/java/install.go index abd3b37..81f1a41 100644 --- a/internal/provision/devcontainer/java/install.go +++ b/internal/provision/devcontainer/java/install.go @@ -17,6 +17,9 @@ func Install(cfg state.Config) error { if err := executil.RunEmbeddedScript( "devcontainer/java/install.sh", + "RUNTIME=java", + "ARCHIVE_EXT=tar.gz", + "RUNTIME_VERSION="+cfg.Version, ); err != nil { return fmt.Errorf("java devcontainer install failed: %w", err) } diff --git a/internal/provision/devcontainer/node/install.go b/internal/provision/devcontainer/node/install.go index a6d34c2..fa410d2 100644 --- a/internal/provision/devcontainer/node/install.go +++ b/internal/provision/devcontainer/node/install.go @@ -15,9 +15,11 @@ func Install(cfg state.Config) error { return nil } - // Execute embedded installer (mirrors game server model) if err := executil.RunEmbeddedScript( "devcontainer/node/install.sh", + "RUNTIME=node", + "ARCHIVE_EXT=tar.xz", + "RUNTIME_VERSION="+cfg.Version, ); err != nil { return fmt.Errorf("node devcontainer install failed: %w", err) } diff --git a/internal/provision/devcontainer/python/install.go b/internal/provision/devcontainer/python/install.go index fe5a9ad..bed2e56 100644 --- a/internal/provision/devcontainer/python/install.go +++ b/internal/provision/devcontainer/python/install.go @@ -17,6 +17,9 @@ func Install(cfg state.Config) error { if err := executil.RunEmbeddedScript( "devcontainer/python/install.sh", + "RUNTIME=python", + "ARCHIVE_EXT=tar.xz", + "RUNTIME_VERSION="+cfg.Version, ); err != nil { return fmt.Errorf("python devcontainer install failed: %w", err) } diff --git a/internal/provision/executil/embedded_exec.go b/internal/provision/executil/embedded_exec.go index f19768d..a1a7bc2 100644 --- a/internal/provision/executil/embedded_exec.go +++ b/internal/provision/executil/embedded_exec.go @@ -5,22 +5,30 @@ import ( "fmt" "os" "os/exec" + "path" "strings" "zlh-agent/scripts" ) // RunEmbeddedScript executes an embedded script via bash by piping its contents to stdin. -// This mirrors RunScript's stdout/stderr behavior without requiring any files on disk. -func RunEmbeddedScript(path string) error { - normalized := normalizeEmbeddedPath(path) - data, err := scripts.FS.ReadFile(normalized) +// If the script is a devcontainer installer, this automatically prepends the shared common.sh +// so that tiny per-runtime installers can call install_runtime in the same shell session. +func RunEmbeddedScript(scriptPath string, extraEnv ...string) error { + normalized := normalizeEmbeddedPath(scriptPath) + + payload, err := loadEmbeddedPayload(normalized) if err != nil { - return fmt.Errorf("embedded script not found: %s", path) + return err } cmd := exec.Command("bash") - cmd.Stdin = bytes.NewReader(data) + cmd.Stdin = bytes.NewReader(payload) + + // Inherit current env and overlay any provided vars (e.g., RUNTIME_VERSION=24). + if len(extraEnv) > 0 { + cmd.Env = append(os.Environ(), extraEnv...) + } // Match RunScript behavior (executil.go) cmd.Stdout = os.Stdout @@ -29,6 +37,33 @@ func RunEmbeddedScript(path string) error { return cmd.Run() } -func normalizeEmbeddedPath(path string) string { - return strings.TrimPrefix(path, "scripts/") +func loadEmbeddedPayload(normalized string) ([]byte, error) { + // If this is a devcontainer installer, prepend the shared library. + if strings.HasPrefix(normalized, "devcontainer/") { + commonPath := "devcontainer/lib/common.sh" + common, err := scripts.FS.ReadFile(commonPath) + if err != nil { + return nil, fmt.Errorf("embedded script missing: %s: %w", commonPath, err) + } + + body, err := scripts.FS.ReadFile(normalized) + if err != nil { + return nil, fmt.Errorf("embedded script missing: %s: %w", normalized, err) + } + + // Ensure bash sees common first, then runtime installer. + return append(append(common, '\n'), body...), nil + } + + data, err := scripts.FS.ReadFile(normalized) + if err != nil { + return nil, fmt.Errorf("embedded script missing: %s: %w", normalized, err) + } + return data, nil +} + +func normalizeEmbeddedPath(p string) string { + p = strings.TrimPrefix(p, "scripts/") + p = path.Clean(p) + return strings.TrimPrefix(p, "/") } diff --git a/logs/agent.log b/logs/agent.log new file mode 100644 index 0000000..e69de29 diff --git a/scripts/devcontainer/go/install.sh b/scripts/devcontainer/go/install.sh index d80e368..5a33ba5 100644 --- a/scripts/devcontainer/go/install.sh +++ b/scripts/devcontainer/go/install.sh @@ -1,27 +1,5 @@ -#!/usr/bin/env bash -set -euo pipefail - RUNTIME="go" -ARCHIVE_FILE="go-${DEST_VERSION}.tar.gz" +ARCHIVE_EXT="tar.gz" +ARCHIVE_PREFIX="go" - -ARTIFACT_BASE_URL="${ZLH_ARTIFACT_BASE_URL:-http://10.60.0.251:8080}" -DEST_DIR="${RUNTIME_ROOT}/${DEST_VERSION}" -ARCHIVE_FILE="go-${DEST_VERSION}.tar.gz" -URL="${ARTIFACT_BASE_URL%/}/devcontainer/go/${DEST_VERSION}/${ARCHIVE_FILE}" - -mkdir -p "${RUNTIME_ROOT}" - -if [ ! -d "${DEST_DIR}" ]; then - curl -fL "${URL}" -o /tmp/${ARCHIVE_FILE} - mkdir -p "${DEST_DIR}" - tar -xf /tmp/${ARCHIVE_FILE} -C "${DEST_DIR}" --strip-components=1 -fi - -ln -sfn "${DEST_DIR}" "${RUNTIME_ROOT}/current" -ln -sfn "${DEST_DIR}/bin" "${RUNTIME_ROOT}/bin" - -cat >/etc/profile.d/zlh-go.sh <<'EOF' -export PATH="/opt/zlh/runtime/go/bin:$PATH" -EOF -chmod +x /etc/profile.d/zlh-go.sh +install_runtime diff --git a/scripts/devcontainer/java/install.sh b/scripts/devcontainer/java/install.sh index 74e0f4c..87a6428 100644 --- a/scripts/devcontainer/java/install.sh +++ b/scripts/devcontainer/java/install.sh @@ -1,26 +1,5 @@ -#!/usr/bin/env bash -set -euo pipefail - RUNTIME="java" -ARCHIVE_FILE="jdk-${DEST_VERSION}.tar.gz" +ARCHIVE_EXT="tar.gz" +ARCHIVE_PREFIX="jdk" -ARTIFACT_BASE_URL="${ZLH_ARTIFACT_BASE_URL:-http://10.60.0.251:8080}" -DEST_DIR="${RUNTIME_ROOT}/${DEST_VERSION}" -ARCHIVE_FILE="jdk-${DEST_VERSION}.tar.gz" -URL="${ARTIFACT_BASE_URL%/}/devcontainer/java/${DEST_VERSION}/${ARCHIVE_FILE}" - -mkdir -p "${RUNTIME_ROOT}" - -if [ ! -d "${DEST_DIR}" ]; then - curl -fL "${URL}" -o /tmp/${ARCHIVE_FILE} - mkdir -p "${DEST_DIR}" - tar -xf /tmp/${ARCHIVE_FILE} -C "${DEST_DIR}" --strip-components=1 -fi - -ln -sfn "${DEST_DIR}" "${RUNTIME_ROOT}/current" -ln -sfn "${DEST_DIR}/bin" "${RUNTIME_ROOT}/bin" - -cat >/etc/profile.d/zlh-java.sh <<'EOF' -export PATH="/opt/zlh/runtime/java/bin:$PATH" -EOF -chmod +x /etc/profile.d/zlh-java.sh +install_runtime diff --git a/scripts/devcontainer/lib/common.sh b/scripts/devcontainer/lib/common.sh new file mode 100644 index 0000000..ff8a132 --- /dev/null +++ b/scripts/devcontainer/lib/common.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${RUNTIME:?RUNTIME required}" +: "${RUNTIME_VERSION:?RUNTIME_VERSION required}" +: "${ARCHIVE_EXT:?ARCHIVE_EXT required}" + +ZLH_ARTIFACT_BASE_URL="${ZLH_ARTIFACT_BASE_URL:-http://10.60.0.251:8080}" +ZLH_RUNTIME_ROOT="${ZLH_RUNTIME_ROOT:-/opt/zlh/runtime}" +ARCHIVE_PREFIX="${ARCHIVE_PREFIX:-${RUNTIME}}" + +RUNTIME_ROOT="${ZLH_RUNTIME_ROOT}/${RUNTIME}" +DEST_DIR="${RUNTIME_ROOT}/${RUNTIME_VERSION}" +CURRENT_LINK="${RUNTIME_ROOT}/current" + +log() { echo "[zlh-installer:${RUNTIME}] $*"; } +fail() { echo "[zlh-installer:${RUNTIME}] ERROR: $*" >&2; exit 1; } + +artifact_name() { + echo "${ARCHIVE_PREFIX}-${RUNTIME_VERSION}.${ARCHIVE_EXT}" +} + +artifact_url() { + echo "${ZLH_ARTIFACT_BASE_URL%/}/devcontainer/${RUNTIME}/${RUNTIME_VERSION}/$(artifact_name)" +} + +download_artifact() { + local url out + url="$(artifact_url)" + out="/tmp/$(artifact_name)" + + log "Downloading ${url}" + if command -v curl >/dev/null 2>&1; then + curl -fL "${url}" -o "${out}" + elif command -v wget >/dev/null 2>&1; then + wget -O "${out}" "${url}" + else + fail "curl or wget is required" + fi +} + +extract_artifact() { + local out + out="/tmp/$(artifact_name)" + + log "Extracting to ${DEST_DIR}" + mkdir -p "${DEST_DIR}" + + case "${ARCHIVE_EXT}" in + tar.xz|tar.gz) + tar -xf "${out}" -C "${DEST_DIR}" --strip-components=1 + ;; + *) + fail "Unsupported archive type: ${ARCHIVE_EXT}" + ;; + esac +} + +update_symlinks() { + ln -sfn "${DEST_DIR}" "${CURRENT_LINK}" + ln -sfn "${CURRENT_LINK}/bin" "${RUNTIME_ROOT}/bin" +} + +write_profile() { + cat >/etc/profile.d/zlh-${RUNTIME}.sh </etc/profile.d/zlh-${RUNTIME}.sh </dev/null 2>&1; then - curl -fL "${URL}" -o "${TMP_DIR}/${ARCHIVE_FILE}" - elif command -v wget >/dev/null 2>&1; then - wget -O "${TMP_DIR}/${ARCHIVE_FILE}" "${URL}" - else - echo "[python][ERROR] curl or wget is required" - exit 1 - fi - - echo "[python] Extracting ${ARCHIVE_FILE} -> ${DEST_DIR}" - mkdir -p "${DEST_DIR}" - tar -xf "${TMP_DIR}/${ARCHIVE_FILE}" -C "${DEST_DIR}" --strip-components=1 -fi - -# Stable symlinks (same model as Node) -ln -sfn "${DEST_DIR}" "${RUNTIME_ROOT}/current" -ln -sfn "${DEST_DIR}/bin" "${RUNTIME_ROOT}/bin" - -# System-wide PATH export -cat >/etc/profile.d/zlh-python.sh <<'EOF' -export PATH="/opt/zlh/runtime/python/bin:$PATH" -EOF - -chmod +x /etc/profile.d/zlh-python.sh - -# Permissions sanity -chmod -R 755 "${DEST_DIR}" - -echo "[python] Python ${DEST_VERSION} installed successfully" +install_runtime diff --git a/zlh-agent b/zlh-agent index dbb3a77..07e39d3 100755 Binary files a/zlh-agent and b/zlh-agent differ