diff --git a/internal/http/agent.go b/internal/http/agent.go index 4b81f59..992bd62 100755 --- a/internal/http/agent.go +++ b/internal/http/agent.go @@ -48,7 +48,6 @@ func runProvisionPipeline(cfg *state.Config) error { return err } - // Extra Minecraft verification ONLY for Minecraft if strings.ToLower(cfg.Game) == "minecraft" { if err := minecraft.VerifyMinecraftInstallWithRepair(*cfg); err != nil { state.SetError(err) @@ -67,39 +66,40 @@ func runProvisionPipeline(cfg *state.Config) error { ----------------------------------------------------------------------------*/ func ensureProvisioned(cfg *state.Config) error { - /* --------------------------------------------------------- - DEV CONTAINERS (FIRST-CLASS) - --------------------------------------------------------- */ - if cfg.ContainerType == "dev" { +if cfg.ContainerType == "dev" { - // If not provisioned yet, install - if !devcontainer.IsProvisioned() { - return runProvisionPipeline(cfg) - } + if !devcontainer.IsProvisioned() { + if err := runProvisionPipeline(cfg); err != nil { + return err + } + } - // Verify runtime - switch strings.ToLower(cfg.Runtime) { + var err error - case "node": - return node.Verify(*cfg) + switch strings.ToLower(cfg.Runtime) { + case "node": + err = node.Verify(*cfg) + case "python": + err = python.Verify(*cfg) + case "go": + err = goenv.Verify(*cfg) + case "java": + err = java.Verify(*cfg) + default: + return fmt.Errorf("unsupported devcontainer runtime: %s", cfg.Runtime) + } - case "python": - return python.Verify(*cfg) + if err != nil { + return err + } - case "go": - return goenv.Verify(*cfg) + // ✅ DEV READY = RUNNING + state.SetState(state.StateRunning) + state.SetError(nil) + return nil +} - case "java": - return java.Verify(*cfg) - default: - return fmt.Errorf("unsupported devcontainer runtime: %s", cfg.Runtime) - } - } - - /* --------------------------------------------------------- - GAME SERVERS (EXISTING LOGIC) - --------------------------------------------------------- */ dir := provision.ServerDir(*cfg) game := strings.ToLower(cfg.Game) variant := strings.ToLower(cfg.Variant) @@ -109,12 +109,10 @@ func ensureProvisioned(cfg *state.Config) error { isSteam := provision.IsSteamGame(game) isForgeLike := variant == "forge" || variant == "neoforge" - // ---------- STEAM GAMES ALWAYS REQUIRE PROVISION ---------- if isSteam { return runProvisionPipeline(cfg) } - // ---------- FORGE / NEOFORGE ---------- if isForgeLike { runSh := filepath.Join(dir, "run.sh") libraries := filepath.Join(dir, "libraries") @@ -122,11 +120,9 @@ func ensureProvisioned(cfg *state.Config) error { if fileExists(runSh) && dirExists(libraries) { return nil } - return runProvisionPipeline(cfg) } - // ---------- VANILLA / PAPER / PURPUR / FABRIC ---------- jar := filepath.Join(dir, "server.jar") if fileExists(jar) { return nil @@ -165,14 +161,54 @@ func handleConfig(w http.ResponseWriter, r *http.Request) { return } - // Dev containers may not start a server process if c.ContainerType != "dev" { + if err := system.StartServer(&c); err != nil { log.Println("[agent] start error:", err) state.SetError(err) state.SetState(state.StateError) return } + + // ------------------------------------------------- + // FORGE / NEOFORGE: wait → stop → patch → restart + // ------------------------------------------------- + game := strings.ToLower(c.Game) + variant := strings.ToLower(c.Variant) + + if game == "minecraft" && (variant == "forge" || variant == "neoforge") { + + deadline := time.Now().Add(5 * time.Minute) + for { + if state.GetState() == state.StateRunning { + break + } + if time.Now().After(deadline) { + err := fmt.Errorf("forge did not reach running state") + log.Println("[agent]", err) + state.SetError(err) + state.SetState(state.StateError) + return + } + time.Sleep(2 * time.Second) + } + + _ = system.StopServer() + + if err := minecraft.EnforceForgeServerProperties(c); err != nil { + log.Println("[agent] forge post-start error:", err) + state.SetError(err) + state.SetState(state.StateError) + return + } + + if err := system.StartServer(&c); err != nil { + log.Println("[agent] restart error:", err) + state.SetError(err) + state.SetState(state.StateError) + return + } + } } log.Println("[agent] async provision+start complete") @@ -184,7 +220,7 @@ func handleConfig(w http.ResponseWriter, r *http.Request) { } /* -------------------------------------------------------------------------- - /start — manual UI start + /start ----------------------------------------------------------------------------*/ func handleStart(w http.ResponseWriter, r *http.Request) { cfg, err := state.LoadConfig() @@ -245,7 +281,7 @@ func handleRestart(w http.ResponseWriter, r *http.Request) { } /* -------------------------------------------------------------------------- - /status — API polls this + /status ----------------------------------------------------------------------------*/ func handleStatus(w http.ResponseWriter, r *http.Request) { cfg, _ := state.LoadConfig() diff --git a/internal/provision/devcontainer/go/install.go b/internal/provision/devcontainer/go/install.go index 8d38f14..94a7cdc 100644 --- a/internal/provision/devcontainer/go/install.go +++ b/internal/provision/devcontainer/go/install.go @@ -2,7 +2,6 @@ package goenv import ( "fmt" - "path/filepath" "zlh-agent/internal/provision/executil" "zlh-agent/internal/provision/markers" @@ -16,14 +15,9 @@ func Install(cfg state.Config) error { return nil } - scriptPath := filepath.Join( - executil.ScriptsRoot, - "devcontainer", - "go", - "install.sh", - ) - - if err := executil.RunScript(scriptPath); err != nil { + if err := executil.RunEmbeddedScript( + "devcontainer/go/install.sh", + ); err != nil { return fmt.Errorf("go devcontainer install failed: %w", err) } diff --git a/internal/provision/devcontainer/go/verify.go b/internal/provision/devcontainer/go/verify.go index e61c937..68a1e6c 100644 --- a/internal/provision/devcontainer/go/verify.go +++ b/internal/provision/devcontainer/go/verify.go @@ -2,14 +2,22 @@ package goenv import ( "fmt" + "os" "os/exec" "zlh-agent/internal/state" ) +const goBin = "/opt/zlh/runtime/go/bin/go" + func Verify(cfg state.Config) error { - if _, err := exec.LookPath("go"); err != nil { - return fmt.Errorf("go binary not found in PATH") + if _, err := os.Stat(goBin); err != nil { + return fmt.Errorf("go binary missing at %s", goBin) } + + if err := exec.Command(goBin, "version").Run(); err != nil { + return fmt.Errorf("go runtime not executable: %w", err) + } + return nil } diff --git a/internal/provision/devcontainer/java/install.go b/internal/provision/devcontainer/java/install.go index b28954e..abd3b37 100644 --- a/internal/provision/devcontainer/java/install.go +++ b/internal/provision/devcontainer/java/install.go @@ -2,7 +2,6 @@ package java import ( "fmt" - "path/filepath" "zlh-agent/internal/provision/executil" "zlh-agent/internal/provision/markers" @@ -10,21 +9,17 @@ import ( ) func Install(cfg state.Config) error { + const marker = "devcontainer-java" - if markers.IsPresent("devcontainer-java") { + if markers.IsPresent(marker) { return nil } - scriptPath := filepath.Join( - executil.ScriptsRoot, - "devcontainer", - "java", - "install.sh", - ) - - if err := executil.RunScript(scriptPath); err != nil { + if err := executil.RunEmbeddedScript( + "devcontainer/java/install.sh", + ); err != nil { return fmt.Errorf("java devcontainer install failed: %w", err) } - return markers.Write("devcontainer-java") + return markers.Write(marker) } diff --git a/internal/provision/devcontainer/java/verify.go b/internal/provision/devcontainer/java/verify.go index cc70975..8bb2f4e 100644 --- a/internal/provision/devcontainer/java/verify.go +++ b/internal/provision/devcontainer/java/verify.go @@ -2,14 +2,22 @@ package java import ( "fmt" + "os" "os/exec" "zlh-agent/internal/state" ) +const javaBin = "/opt/zlh/runtime/java/bin/java" + func Verify(cfg state.Config) error { - if _, err := exec.LookPath("java"); err != nil { - return fmt.Errorf("java binary not found in PATH") + if _, err := os.Stat(javaBin); err != nil { + return fmt.Errorf("java binary missing at %s", javaBin) } + + if err := exec.Command(javaBin, "-version").Run(); err != nil { + return fmt.Errorf("java runtime not executable: %w", err) + } + return nil } diff --git a/internal/provision/devcontainer/node/install.go b/internal/provision/devcontainer/node/install.go index e0f918f..a6d34c2 100644 --- a/internal/provision/devcontainer/node/install.go +++ b/internal/provision/devcontainer/node/install.go @@ -17,7 +17,7 @@ func Install(cfg state.Config) error { // Execute embedded installer (mirrors game server model) if err := executil.RunEmbeddedScript( - "scripts/devcontainer/node/install.sh", + "devcontainer/node/install.sh", ); err != nil { return fmt.Errorf("node devcontainer install failed: %w", err) } diff --git a/internal/provision/devcontainer/node/verify.go b/internal/provision/devcontainer/node/verify.go index 5de377c..a3611c6 100644 --- a/internal/provision/devcontainer/node/verify.go +++ b/internal/provision/devcontainer/node/verify.go @@ -2,21 +2,34 @@ package node import ( "fmt" + "os" "os/exec" "zlh-agent/internal/state" ) +const ( + nodeBin = "/opt/zlh/runtime/node/bin/node" + npmBin = "/opt/zlh/runtime/node/bin/npm" +) + func Verify(cfg state.Config) error { - // Version is optional at verify-time; existence is authoritative - if _, err := exec.LookPath("node"); err != nil { - return fmt.Errorf("node binary not found in PATH") + // Node must exist + if _, err := os.Stat(nodeBin); err != nil { + return fmt.Errorf("node binary missing at %s", nodeBin) } - if _, err := exec.LookPath("npm"); err != nil { - return fmt.Errorf("npm binary not found in PATH") + // Node must execute + if err := exec.Command(nodeBin, "-v").Run(); err != nil { + return fmt.Errorf("node runtime not executable: %w", err) } + // npm must exist (installation completeness) + if _, err := os.Stat(npmBin); err != nil { + return fmt.Errorf("npm missing at %s", npmBin) + } + + // Do NOT execute npm here — it is PATH-dependent by design return nil } diff --git a/internal/provision/devcontainer/python/install.go b/internal/provision/devcontainer/python/install.go index 1f1a709..fe5a9ad 100644 --- a/internal/provision/devcontainer/python/install.go +++ b/internal/provision/devcontainer/python/install.go @@ -2,7 +2,6 @@ package python import ( "fmt" - "path/filepath" "zlh-agent/internal/provision/executil" "zlh-agent/internal/provision/markers" @@ -16,14 +15,9 @@ func Install(cfg state.Config) error { return nil } - scriptPath := filepath.Join( - executil.ScriptsRoot, - "devcontainer", - "python", - "install.sh", - ) - - if err := executil.RunScript(scriptPath); err != nil { + if err := executil.RunEmbeddedScript( + "devcontainer/python/install.sh", + ); err != nil { return fmt.Errorf("python devcontainer install failed: %w", err) } diff --git a/internal/provision/devcontainer/python/verify.go b/internal/provision/devcontainer/python/verify.go index 8ad9775..eb6f5e6 100644 --- a/internal/provision/devcontainer/python/verify.go +++ b/internal/provision/devcontainer/python/verify.go @@ -2,17 +2,34 @@ package python import ( "fmt" + "os" "os/exec" "zlh-agent/internal/state" ) +const ( + pythonBin = "/opt/zlh/runtime/python/bin/python3" + pipBin = "/opt/zlh/runtime/python/bin/pip3" +) + func Verify(cfg state.Config) error { - if _, err := exec.LookPath("python3"); err != nil { - return fmt.Errorf("python3 binary not found in PATH") + + // python3 must exist + if _, err := os.Stat(pythonBin); err != nil { + return fmt.Errorf("python3 binary missing at %s", pythonBin) } - if _, err := exec.LookPath("pip3"); err != nil { - return fmt.Errorf("pip3 binary not found in PATH") + + // python3 must execute + if err := exec.Command(pythonBin, "--version").Run(); err != nil { + return fmt.Errorf("python3 runtime not executable: %w", err) } + + // pip must exist (completeness check only) + if _, err := os.Stat(pipBin); err != nil { + return fmt.Errorf("pip3 missing at %s", pipBin) + } + + // Do NOT execute pip (PATH + env dependent) return nil } diff --git a/internal/provision/executil/embedded_exec.go b/internal/provision/executil/embedded_exec.go index 37f25a3..f19768d 100644 --- a/internal/provision/executil/embedded_exec.go +++ b/internal/provision/executil/embedded_exec.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "os/exec" + "strings" "zlh-agent/scripts" ) @@ -12,7 +13,8 @@ import ( // 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 { - data, err := scripts.FS.ReadFile(path) + normalized := normalizeEmbeddedPath(path) + data, err := scripts.FS.ReadFile(normalized) if err != nil { return fmt.Errorf("embedded script not found: %s", path) } @@ -25,4 +27,8 @@ func RunEmbeddedScript(path string) error { cmd.Stderr = os.Stderr return cmd.Run() -} \ No newline at end of file +} + +func normalizeEmbeddedPath(path string) string { + return strings.TrimPrefix(path, "scripts/") +} diff --git a/internal/provision/executil/embedded_exec_test.go b/internal/provision/executil/embedded_exec_test.go new file mode 100644 index 0000000..2b3971d --- /dev/null +++ b/internal/provision/executil/embedded_exec_test.go @@ -0,0 +1,21 @@ +package executil + +import ( + "testing" + + "zlh-agent/scripts" +) + +func TestNormalizeEmbeddedPath(t *testing.T) { + paths := []string{ + "devcontainer/node/install.sh", + "scripts/devcontainer/node/install.sh", + } + + for _, path := range paths { + normalized := normalizeEmbeddedPath(path) + if _, err := scripts.FS.ReadFile(normalized); err != nil { + t.Fatalf("expected embedded script to resolve for %q (normalized to %q): %v", path, normalized, err) + } + } +} diff --git a/internal/provision/minecraft/forge_post.go b/internal/provision/minecraft/forge_post.go new file mode 100644 index 0000000..932c4f0 --- /dev/null +++ b/internal/provision/minecraft/forge_post.go @@ -0,0 +1,53 @@ +package minecraft + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "zlh-agent/internal/state" +) + +/* + EnforceForgeServerProperties + - Runs AFTER first Forge/NeoForge server start + - Ensures Velocity-compatible settings + - Requires restart to take effect +*/ +func EnforceForgeServerProperties(cfg state.Config) error { + variant := strings.ToLower(cfg.Variant) + if variant != "forge" && variant != "neoforge" { + return nil + } + + // Forge working directory is authoritative + serverDir := filepath.Join("/opt/zlh/minecraft", variant, "world") + propsPath := filepath.Join(serverDir, "server.properties") + + data, err := os.ReadFile(propsPath) + if err != nil { + return fmt.Errorf("read server.properties: %w", err) + } + + lines := strings.Split(string(data), "\n") + found := false + + for i, l := range lines { + if strings.HasPrefix(l, "online-mode=") { + lines[i] = "online-mode=false" + found = true + } + } + + if !found { + lines = append(lines, "online-mode=false") + } + + out := strings.Join(lines, "\n") + if err := os.WriteFile(propsPath, []byte(out), 0644); err != nil { + return fmt.Errorf("write server.properties: %w", err) + } + + return nil +} diff --git a/scripts/devcontainer/go/install.sh b/scripts/devcontainer/go/install.sh index 8fe7e25..d80e368 100644 --- a/scripts/devcontainer/go/install.sh +++ b/scripts/devcontainer/go/install.sh @@ -1,51 +1,27 @@ #!/usr/bin/env bash set -euo pipefail -echo "[go] starting go devcontainer install" +RUNTIME="go" +ARCHIVE_FILE="go-${DEST_VERSION}.tar.gz" -# -------------------------------------------------- -# Config -# -------------------------------------------------- -RUNTIME_ROOT="/opt/zlh/runtime/go" -ARTIFACT_ROOT="/opt/zlh/devcontainer/go" -VERSION="${RUNTIME_VERSION:-1.25}" +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}" -SRC_DIR="${ARTIFACT_ROOT}/${VERSION}" -DEST_DIR="${RUNTIME_ROOT}/${VERSION}" -SYMLINK="${RUNTIME_ROOT}/current" +mkdir -p "${RUNTIME_ROOT}" -# -------------------------------------------------- -# Guards -# -------------------------------------------------- -if [ ! -d "${SRC_DIR}" ]; then - echo "[go][ERROR] artifact directory not found: ${SRC_DIR}" - exit 1 -fi - -if [ -x "${DEST_DIR}/bin/go" ]; then - echo "[go] go ${VERSION} already installed" -else - ARCHIVE="$(ls ${SRC_DIR}/go*.linux-amd64.tar.gz 2>/dev/null | head -n1)" - if [ -z "${ARCHIVE}" ]; then - echo "[go][ERROR] no Go archive found in ${SRC_DIR}" - exit 1 - fi - - echo "[go] installing go ${VERSION}" +if [ ! -d "${DEST_DIR}" ]; then + curl -fL "${URL}" -o /tmp/${ARCHIVE_FILE} mkdir -p "${DEST_DIR}" - - tar -xzf "${ARCHIVE}" -C "${DEST_DIR}" --strip-components=1 + tar -xf /tmp/${ARCHIVE_FILE} -C "${DEST_DIR}" --strip-components=1 fi -# -------------------------------------------------- -# Symlink current -# -------------------------------------------------- -ln -sfn "${DEST_DIR}" "${SYMLINK}" +ln -sfn "${DEST_DIR}" "${RUNTIME_ROOT}/current" +ln -sfn "${DEST_DIR}/bin" "${RUNTIME_ROOT}/bin" -# -------------------------------------------------- -# Verify -# -------------------------------------------------- -"${SYMLINK}/bin/go" version - -echo "[go] go ${VERSION} install complete" +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 diff --git a/scripts/devcontainer/java/install.sh b/scripts/devcontainer/java/install.sh index d5274d3..74e0f4c 100644 --- a/scripts/devcontainer/java/install.sh +++ b/scripts/devcontainer/java/install.sh @@ -1,62 +1,26 @@ #!/usr/bin/env bash set -euo pipefail -echo "[java] starting java devcontainer install" +RUNTIME="java" +ARCHIVE_FILE="jdk-${DEST_VERSION}.tar.gz" -# -------------------------------------------------- -# Config -# -------------------------------------------------- -RUNTIME_ROOT="/opt/zlh/runtime/java" -ARTIFACT_ROOT="/opt/zlh/devcontainer/java" +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}" -VERSION="${RUNTIME_VERSION:-17}" +mkdir -p "${RUNTIME_ROOT}" -SRC_DIR="${ARTIFACT_ROOT}/${VERSION}" -DEST_DIR="${RUNTIME_ROOT}/${VERSION}" -SYMLINK="${RUNTIME_ROOT}/current" - -# -------------------------------------------------- -# Guards -# -------------------------------------------------- -if [ ! -d "${SRC_DIR}" ]; then - echo "[java][ERROR] artifact directory not found: ${SRC_DIR}" - exit 1 -fi - -if [ -x "${DEST_DIR}/bin/java" ]; then - echo "[java] java ${VERSION} already installed" -else - ARCHIVE="$(ls ${SRC_DIR}/*.tar.gz 2>/dev/null | head -n1)" - if [ -z "${ARCHIVE}" ]; then - echo "[java][ERROR] no Java archive found in ${SRC_DIR}" - exit 1 - fi - - echo "[java] installing java ${VERSION}" +if [ ! -d "${DEST_DIR}" ]; then + curl -fL "${URL}" -o /tmp/${ARCHIVE_FILE} mkdir -p "${DEST_DIR}" - - TMP_DIR="$(mktemp -d)" - tar -xzf "${ARCHIVE}" -C "${TMP_DIR}" - - # Java archives contain a single root dir - EXTRACTED_DIR="$(find ${TMP_DIR} -maxdepth 1 -type d -name 'jdk*' | head -n1)" - if [ -z "${EXTRACTED_DIR}" ]; then - echo "[java][ERROR] failed to locate extracted jdk directory" - exit 1 - fi - - mv "${EXTRACTED_DIR}"/* "${DEST_DIR}/" - rm -rf "${TMP_DIR}" + tar -xf /tmp/${ARCHIVE_FILE} -C "${DEST_DIR}" --strip-components=1 fi -# -------------------------------------------------- -# Symlink current -# -------------------------------------------------- -ln -sfn "${DEST_DIR}" "${SYMLINK}" +ln -sfn "${DEST_DIR}" "${RUNTIME_ROOT}/current" +ln -sfn "${DEST_DIR}/bin" "${RUNTIME_ROOT}/bin" -# -------------------------------------------------- -# Verify -# -------------------------------------------------- -"${SYMLINK}/bin/java" -version - -echo "[java] java ${VERSION} install complete" +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 diff --git a/scripts/devcontainer/node/install.sh b/scripts/devcontainer/node/install.sh index 8fb62d8..5a3aae6 100644 --- a/scripts/devcontainer/node/install.sh +++ b/scripts/devcontainer/node/install.sh @@ -1,40 +1,33 @@ #!/usr/bin/env bash set -euo pipefail -RUNTIME_ROOT="/opt/zlh/runtime/node" -ARTIFACT_ROOT="/opt/zlh/devcontainer/node" +RUNTIME="node" +RUNTIME_ROOT="/opt/zlh/runtime/${RUNTIME}" +DEST_VERSION="${RUNTIME_VERSION:?RUNTIME_VERSION required}" -VERSION="${RUNTIME_VERSION:-24}" +ARTIFACT_BASE_URL="${ZLH_ARTIFACT_BASE_URL:-http://10.60.0.251:8080}" +ARCHIVE_FILE="${RUNTIME}-${DEST_VERSION}.tar.xz" +URL="${ARTIFACT_BASE_URL%/}/devcontainer/${RUNTIME}/${DEST_VERSION}/${ARCHIVE_FILE}" -ARCHIVE="node-v${VERSION}.*-linux-x64.tar.xz" -SRC_DIR="${ARTIFACT_ROOT}/${VERSION}" -DEST_DIR="${RUNTIME_ROOT}/${VERSION}" -echo "[node] Installing Node.js version ${VERSION}" +DEST_DIR="${RUNTIME_ROOT}/${DEST_VERSION}" + +echo "[${RUNTIME}] Installing ${RUNTIME} ${DEST_VERSION}" -# Ensure runtime root exists mkdir -p "${RUNTIME_ROOT}" -# Idempotency check -if [ -d "${DEST_DIR}" ]; then - echo "[node] Node ${VERSION} already installed" -else - ARCHIVE_PATH=$(ls "${SRC_DIR}/${ARCHIVE}" 2>/dev/null || true) - - if [ -z "${ARCHIVE_PATH}" ]; then - echo "[node][ERROR] Artifact not found for Node ${VERSION}" - exit 1 - fi - - echo "[node] Extracting ${ARCHIVE_PATH}" +if [ ! -d "${DEST_DIR}" ]; then + curl -fL "${URL}" -o /tmp/${ARCHIVE_FILE} mkdir -p "${DEST_DIR}" - tar -xJf "${ARCHIVE_PATH}" -C "${DEST_DIR}" --strip-components=1 + tar -xf /tmp/${ARCHIVE_FILE} -C "${DEST_DIR}" --strip-components=1 fi -# Update stable symlink ln -sfn "${DEST_DIR}" "${RUNTIME_ROOT}/current" +ln -sfn "${DEST_DIR}/bin" "${RUNTIME_ROOT}/bin" + +cat >/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] building python ${VERSION}" + echo "[python] Extracting ${ARCHIVE_FILE} -> ${DEST_DIR}" mkdir -p "${DEST_DIR}" - - BUILD_DIR="$(mktemp -d)" - tar -xzf "${ARCHIVE}" -C "${BUILD_DIR}" - - PY_SRC="$(find ${BUILD_DIR} -maxdepth 1 -type d -name 'Python-*' | head -n1)" - if [ -z "${PY_SRC}" ]; then - echo "[python][ERROR] failed to locate extracted python source" - exit 1 - fi - - cd "${PY_SRC}" - - ./configure \ - --prefix="${DEST_DIR}" \ - --enable-optimizations \ - --with-ensurepip=install - - make -j"$(nproc)" - make install - - cd / - rm -rf "${BUILD_DIR}" + tar -xf "${TMP_DIR}/${ARCHIVE_FILE}" -C "${DEST_DIR}" --strip-components=1 fi -# -------------------------------------------------- -# Symlink current -# -------------------------------------------------- -ln -sfn "${DEST_DIR}" "${SYMLINK}" +# Stable symlinks (same model as Node) +ln -sfn "${DEST_DIR}" "${RUNTIME_ROOT}/current" +ln -sfn "${DEST_DIR}/bin" "${RUNTIME_ROOT}/bin" -# -------------------------------------------------- -# Verify -# -------------------------------------------------- -"${SYMLINK}/bin/python3" --version -"${SYMLINK}/bin/pip3" --version +# System-wide PATH export +cat >/etc/profile.d/zlh-python.sh <<'EOF' +export PATH="/opt/zlh/runtime/python/bin:$PATH" +EOF -echo "[python] python ${VERSION} install complete" +chmod +x /etc/profile.d/zlh-python.sh + +# Permissions sanity +chmod -R 755 "${DEST_DIR}" + +echo "[python] Python ${DEST_VERSION} installed successfully" diff --git a/zlh-agent b/zlh-agent index 670d45d..dbb3a77 100755 Binary files a/zlh-agent and b/zlh-agent differ