agent update 12-21-25

This commit is contained in:
jester 2025-12-21 22:11:44 +00:00
parent 146737cf50
commit 8645548c4a
17 changed files with 312 additions and 251 deletions

View File

@ -48,7 +48,6 @@ func runProvisionPipeline(cfg *state.Config) error {
return err return err
} }
// Extra Minecraft verification ONLY for Minecraft
if strings.ToLower(cfg.Game) == "minecraft" { if strings.ToLower(cfg.Game) == "minecraft" {
if err := minecraft.VerifyMinecraftInstallWithRepair(*cfg); err != nil { if err := minecraft.VerifyMinecraftInstallWithRepair(*cfg); err != nil {
state.SetError(err) state.SetError(err)
@ -67,39 +66,40 @@ func runProvisionPipeline(cfg *state.Config) error {
----------------------------------------------------------------------------*/ ----------------------------------------------------------------------------*/
func ensureProvisioned(cfg *state.Config) error { func ensureProvisioned(cfg *state.Config) error {
/* --------------------------------------------------------- if cfg.ContainerType == "dev" {
DEV CONTAINERS (FIRST-CLASS)
--------------------------------------------------------- */
if cfg.ContainerType == "dev" {
// If not provisioned yet, install
if !devcontainer.IsProvisioned() { if !devcontainer.IsProvisioned() {
return runProvisionPipeline(cfg) if err := runProvisionPipeline(cfg); err != nil {
return err
}
} }
// Verify runtime var err error
switch strings.ToLower(cfg.Runtime) { switch strings.ToLower(cfg.Runtime) {
case "node": case "node":
return node.Verify(*cfg) err = node.Verify(*cfg)
case "python": case "python":
return python.Verify(*cfg) err = python.Verify(*cfg)
case "go": case "go":
return goenv.Verify(*cfg) err = goenv.Verify(*cfg)
case "java": case "java":
return java.Verify(*cfg) err = java.Verify(*cfg)
default: default:
return fmt.Errorf("unsupported devcontainer runtime: %s", cfg.Runtime) return fmt.Errorf("unsupported devcontainer runtime: %s", cfg.Runtime)
} }
if err != nil {
return err
} }
/* --------------------------------------------------------- // ✅ DEV READY = RUNNING
GAME SERVERS (EXISTING LOGIC) state.SetState(state.StateRunning)
--------------------------------------------------------- */ state.SetError(nil)
return nil
}
dir := provision.ServerDir(*cfg) dir := provision.ServerDir(*cfg)
game := strings.ToLower(cfg.Game) game := strings.ToLower(cfg.Game)
variant := strings.ToLower(cfg.Variant) variant := strings.ToLower(cfg.Variant)
@ -109,12 +109,10 @@ func ensureProvisioned(cfg *state.Config) error {
isSteam := provision.IsSteamGame(game) isSteam := provision.IsSteamGame(game)
isForgeLike := variant == "forge" || variant == "neoforge" isForgeLike := variant == "forge" || variant == "neoforge"
// ---------- STEAM GAMES ALWAYS REQUIRE PROVISION ----------
if isSteam { if isSteam {
return runProvisionPipeline(cfg) return runProvisionPipeline(cfg)
} }
// ---------- FORGE / NEOFORGE ----------
if isForgeLike { if isForgeLike {
runSh := filepath.Join(dir, "run.sh") runSh := filepath.Join(dir, "run.sh")
libraries := filepath.Join(dir, "libraries") libraries := filepath.Join(dir, "libraries")
@ -122,11 +120,9 @@ func ensureProvisioned(cfg *state.Config) error {
if fileExists(runSh) && dirExists(libraries) { if fileExists(runSh) && dirExists(libraries) {
return nil return nil
} }
return runProvisionPipeline(cfg) return runProvisionPipeline(cfg)
} }
// ---------- VANILLA / PAPER / PURPUR / FABRIC ----------
jar := filepath.Join(dir, "server.jar") jar := filepath.Join(dir, "server.jar")
if fileExists(jar) { if fileExists(jar) {
return nil return nil
@ -165,14 +161,54 @@ func handleConfig(w http.ResponseWriter, r *http.Request) {
return return
} }
// Dev containers may not start a server process
if c.ContainerType != "dev" { if c.ContainerType != "dev" {
if err := system.StartServer(&c); err != nil { if err := system.StartServer(&c); err != nil {
log.Println("[agent] start error:", err) log.Println("[agent] start error:", err)
state.SetError(err) state.SetError(err)
state.SetState(state.StateError) state.SetState(state.StateError)
return 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") 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) { func handleStart(w http.ResponseWriter, r *http.Request) {
cfg, err := state.LoadConfig() 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) { func handleStatus(w http.ResponseWriter, r *http.Request) {
cfg, _ := state.LoadConfig() cfg, _ := state.LoadConfig()

View File

@ -2,7 +2,6 @@ package goenv
import ( import (
"fmt" "fmt"
"path/filepath"
"zlh-agent/internal/provision/executil" "zlh-agent/internal/provision/executil"
"zlh-agent/internal/provision/markers" "zlh-agent/internal/provision/markers"
@ -16,14 +15,9 @@ func Install(cfg state.Config) error {
return nil return nil
} }
scriptPath := filepath.Join( if err := executil.RunEmbeddedScript(
executil.ScriptsRoot, "devcontainer/go/install.sh",
"devcontainer", ); err != nil {
"go",
"install.sh",
)
if err := executil.RunScript(scriptPath); err != nil {
return fmt.Errorf("go devcontainer install failed: %w", err) return fmt.Errorf("go devcontainer install failed: %w", err)
} }

View File

@ -2,14 +2,22 @@ package goenv
import ( import (
"fmt" "fmt"
"os"
"os/exec" "os/exec"
"zlh-agent/internal/state" "zlh-agent/internal/state"
) )
const goBin = "/opt/zlh/runtime/go/bin/go"
func Verify(cfg state.Config) error { func Verify(cfg state.Config) error {
if _, err := exec.LookPath("go"); err != nil { if _, err := os.Stat(goBin); err != nil {
return fmt.Errorf("go binary not found in PATH") 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 return nil
} }

View File

@ -2,7 +2,6 @@ package java
import ( import (
"fmt" "fmt"
"path/filepath"
"zlh-agent/internal/provision/executil" "zlh-agent/internal/provision/executil"
"zlh-agent/internal/provision/markers" "zlh-agent/internal/provision/markers"
@ -10,21 +9,17 @@ import (
) )
func Install(cfg state.Config) error { func Install(cfg state.Config) error {
const marker = "devcontainer-java"
if markers.IsPresent("devcontainer-java") { if markers.IsPresent(marker) {
return nil return nil
} }
scriptPath := filepath.Join( if err := executil.RunEmbeddedScript(
executil.ScriptsRoot, "devcontainer/java/install.sh",
"devcontainer", ); err != nil {
"java",
"install.sh",
)
if err := executil.RunScript(scriptPath); err != nil {
return fmt.Errorf("java devcontainer install failed: %w", err) return fmt.Errorf("java devcontainer install failed: %w", err)
} }
return markers.Write("devcontainer-java") return markers.Write(marker)
} }

View File

@ -2,14 +2,22 @@ package java
import ( import (
"fmt" "fmt"
"os"
"os/exec" "os/exec"
"zlh-agent/internal/state" "zlh-agent/internal/state"
) )
const javaBin = "/opt/zlh/runtime/java/bin/java"
func Verify(cfg state.Config) error { func Verify(cfg state.Config) error {
if _, err := exec.LookPath("java"); err != nil { if _, err := os.Stat(javaBin); err != nil {
return fmt.Errorf("java binary not found in PATH") 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 return nil
} }

View File

@ -17,7 +17,7 @@ func Install(cfg state.Config) error {
// Execute embedded installer (mirrors game server model) // Execute embedded installer (mirrors game server model)
if err := executil.RunEmbeddedScript( if err := executil.RunEmbeddedScript(
"scripts/devcontainer/node/install.sh", "devcontainer/node/install.sh",
); err != nil { ); err != nil {
return fmt.Errorf("node devcontainer install failed: %w", err) return fmt.Errorf("node devcontainer install failed: %w", err)
} }

View File

@ -2,21 +2,34 @@ package node
import ( import (
"fmt" "fmt"
"os"
"os/exec" "os/exec"
"zlh-agent/internal/state" "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 { func Verify(cfg state.Config) error {
// Version is optional at verify-time; existence is authoritative // Node must exist
if _, err := exec.LookPath("node"); err != nil { if _, err := os.Stat(nodeBin); err != nil {
return fmt.Errorf("node binary not found in PATH") return fmt.Errorf("node binary missing at %s", nodeBin)
} }
if _, err := exec.LookPath("npm"); err != nil { // Node must execute
return fmt.Errorf("npm binary not found in PATH") 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 return nil
} }

View File

@ -2,7 +2,6 @@ package python
import ( import (
"fmt" "fmt"
"path/filepath"
"zlh-agent/internal/provision/executil" "zlh-agent/internal/provision/executil"
"zlh-agent/internal/provision/markers" "zlh-agent/internal/provision/markers"
@ -16,14 +15,9 @@ func Install(cfg state.Config) error {
return nil return nil
} }
scriptPath := filepath.Join( if err := executil.RunEmbeddedScript(
executil.ScriptsRoot, "devcontainer/python/install.sh",
"devcontainer", ); err != nil {
"python",
"install.sh",
)
if err := executil.RunScript(scriptPath); err != nil {
return fmt.Errorf("python devcontainer install failed: %w", err) return fmt.Errorf("python devcontainer install failed: %w", err)
} }

View File

@ -2,17 +2,34 @@ package python
import ( import (
"fmt" "fmt"
"os"
"os/exec" "os/exec"
"zlh-agent/internal/state" "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 { 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 return nil
} }

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
"strings"
"zlh-agent/scripts" "zlh-agent/scripts"
) )
@ -12,7 +13,8 @@ import (
// RunEmbeddedScript executes an embedded script via bash by piping its contents to stdin. // 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. // This mirrors RunScript's stdout/stderr behavior without requiring any files on disk.
func RunEmbeddedScript(path string) error { func RunEmbeddedScript(path string) error {
data, err := scripts.FS.ReadFile(path) normalized := normalizeEmbeddedPath(path)
data, err := scripts.FS.ReadFile(normalized)
if err != nil { if err != nil {
return fmt.Errorf("embedded script not found: %s", path) return fmt.Errorf("embedded script not found: %s", path)
} }
@ -26,3 +28,7 @@ func RunEmbeddedScript(path string) error {
return cmd.Run() return cmd.Run()
} }
func normalizeEmbeddedPath(path string) string {
return strings.TrimPrefix(path, "scripts/")
}

View File

@ -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)
}
}
}

View File

@ -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
}

View File

@ -1,51 +1,27 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail 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}" mkdir -p "${RUNTIME_ROOT}"
DEST_DIR="${RUNTIME_ROOT}/${VERSION}"
SYMLINK="${RUNTIME_ROOT}/current"
# -------------------------------------------------- if [ ! -d "${DEST_DIR}" ]; then
# Guards curl -fL "${URL}" -o /tmp/${ARCHIVE_FILE}
# --------------------------------------------------
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}"
mkdir -p "${DEST_DIR}" mkdir -p "${DEST_DIR}"
tar -xf /tmp/${ARCHIVE_FILE} -C "${DEST_DIR}" --strip-components=1
tar -xzf "${ARCHIVE}" -C "${DEST_DIR}" --strip-components=1
fi fi
# -------------------------------------------------- ln -sfn "${DEST_DIR}" "${RUNTIME_ROOT}/current"
# Symlink current ln -sfn "${DEST_DIR}/bin" "${RUNTIME_ROOT}/bin"
# --------------------------------------------------
ln -sfn "${DEST_DIR}" "${SYMLINK}"
# -------------------------------------------------- cat >/etc/profile.d/zlh-go.sh <<'EOF'
# Verify export PATH="/opt/zlh/runtime/go/bin:$PATH"
# -------------------------------------------------- EOF
"${SYMLINK}/bin/go" version chmod +x /etc/profile.d/zlh-go.sh
echo "[go] go ${VERSION} install complete"

View File

@ -1,62 +1,26 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
echo "[java] starting java devcontainer install" RUNTIME="java"
ARCHIVE_FILE="jdk-${DEST_VERSION}.tar.gz"
# -------------------------------------------------- ARTIFACT_BASE_URL="${ZLH_ARTIFACT_BASE_URL:-http://10.60.0.251:8080}"
# Config DEST_DIR="${RUNTIME_ROOT}/${DEST_VERSION}"
# -------------------------------------------------- ARCHIVE_FILE="jdk-${DEST_VERSION}.tar.gz"
RUNTIME_ROOT="/opt/zlh/runtime/java" URL="${ARTIFACT_BASE_URL%/}/devcontainer/java/${DEST_VERSION}/${ARCHIVE_FILE}"
ARTIFACT_ROOT="/opt/zlh/devcontainer/java"
VERSION="${RUNTIME_VERSION:-17}" mkdir -p "${RUNTIME_ROOT}"
SRC_DIR="${ARTIFACT_ROOT}/${VERSION}" if [ ! -d "${DEST_DIR}" ]; then
DEST_DIR="${RUNTIME_ROOT}/${VERSION}" curl -fL "${URL}" -o /tmp/${ARCHIVE_FILE}
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}"
mkdir -p "${DEST_DIR}" mkdir -p "${DEST_DIR}"
tar -xf /tmp/${ARCHIVE_FILE} -C "${DEST_DIR}" --strip-components=1
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}"
fi fi
# -------------------------------------------------- ln -sfn "${DEST_DIR}" "${RUNTIME_ROOT}/current"
# Symlink current ln -sfn "${DEST_DIR}/bin" "${RUNTIME_ROOT}/bin"
# --------------------------------------------------
ln -sfn "${DEST_DIR}" "${SYMLINK}"
# -------------------------------------------------- cat >/etc/profile.d/zlh-java.sh <<'EOF'
# Verify export PATH="/opt/zlh/runtime/java/bin:$PATH"
# -------------------------------------------------- EOF
"${SYMLINK}/bin/java" -version chmod +x /etc/profile.d/zlh-java.sh
echo "[java] java ${VERSION} install complete"

View File

@ -1,40 +1,33 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
RUNTIME_ROOT="/opt/zlh/runtime/node" RUNTIME="node"
ARTIFACT_ROOT="/opt/zlh/devcontainer/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}" mkdir -p "${RUNTIME_ROOT}"
# Idempotency check if [ ! -d "${DEST_DIR}" ]; then
if [ -d "${DEST_DIR}" ]; then curl -fL "${URL}" -o /tmp/${ARCHIVE_FILE}
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}"
mkdir -p "${DEST_DIR}" 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 fi
# Update stable symlink
ln -sfn "${DEST_DIR}" "${RUNTIME_ROOT}/current" ln -sfn "${DEST_DIR}" "${RUNTIME_ROOT}/current"
ln -sfn "${DEST_DIR}/bin" "${RUNTIME_ROOT}/bin"
cat >/etc/profile.d/zlh-${RUNTIME}.sh <<EOF
export PATH="/opt/zlh/runtime/${RUNTIME}/bin:\$PATH"
EOF
chmod +x /etc/profile.d/zlh-${RUNTIME}.sh
# Permissions sanity
chmod -R 755 "${DEST_DIR}" chmod -R 755 "${DEST_DIR}"
echo "[node] Node ${VERSION} installed successfully"

View File

@ -1,72 +1,55 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
echo "[python] starting python devcontainer install" RUNTIME="python"
ARCHIVE_FILE="python-${DEST_VERSION}.tar.xz"
# --------------------------------------------------
# Config
# --------------------------------------------------
RUNTIME_ROOT="/opt/zlh/runtime/python"
ARTIFACT_ROOT="/opt/zlh/devcontainer/python"
VERSION="${RUNTIME_VERSION:-3.12}" ARTIFACT_BASE_URL="${ZLH_ARTIFACT_BASE_URL:-http://10.60.0.251:8080}"
SRC_DIR="${ARTIFACT_ROOT}/${VERSION}" DEST_DIR="${RUNTIME_ROOT}/${DEST_VERSION}"
DEST_DIR="${RUNTIME_ROOT}/${VERSION}" TMP_DIR="/tmp/zlh-python-install"
SYMLINK="${RUNTIME_ROOT}/current" ARCHIVE_FILE="python-${DEST_VERSION}.tar.xz"
URL="${ARTIFACT_BASE_URL%/}/devcontainer/python/${DEST_VERSION}/${ARCHIVE_FILE}"
# -------------------------------------------------- echo "[python] Installing Python ${DEST_VERSION}"
# Guards
# --------------------------------------------------
if [ ! -d "${SRC_DIR}" ]; then
echo "[python][ERROR] artifact directory not found: ${SRC_DIR}"
exit 1
fi
if [ -x "${DEST_DIR}/bin/python3" ]; then mkdir -p "${RUNTIME_ROOT}"
echo "[python] python ${VERSION} already installed"
# Idempotency
if [ -d "${DEST_DIR}" ]; then
echo "[python] Python ${DEST_VERSION} already installed at ${DEST_DIR}"
else else
ARCHIVE="$(ls ${SRC_DIR}/Python-*.tgz 2>/dev/null | head -n1)" echo "[python] Downloading ${URL}"
if [ -z "${ARCHIVE}" ]; then rm -rf "${TMP_DIR}"
echo "[python][ERROR] no Python archive found in ${SRC_DIR}" mkdir -p "${TMP_DIR}"
if command -v curl >/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 exit 1
fi fi
echo "[python] building python ${VERSION}" echo "[python] Extracting ${ARCHIVE_FILE} -> ${DEST_DIR}"
mkdir -p "${DEST_DIR}" mkdir -p "${DEST_DIR}"
tar -xf "${TMP_DIR}/${ARCHIVE_FILE}" -C "${DEST_DIR}" --strip-components=1
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}"
fi fi
# -------------------------------------------------- # Stable symlinks (same model as Node)
# Symlink current ln -sfn "${DEST_DIR}" "${RUNTIME_ROOT}/current"
# -------------------------------------------------- ln -sfn "${DEST_DIR}/bin" "${RUNTIME_ROOT}/bin"
ln -sfn "${DEST_DIR}" "${SYMLINK}"
# -------------------------------------------------- # System-wide PATH export
# Verify cat >/etc/profile.d/zlh-python.sh <<'EOF'
# -------------------------------------------------- export PATH="/opt/zlh/runtime/python/bin:$PATH"
"${SYMLINK}/bin/python3" --version EOF
"${SYMLINK}/bin/pip3" --version
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"

BIN
zlh-agent

Binary file not shown.