mirror of
https://github.com/hanzoai/pack.git
synced 2026-09-30 23:43:13 +00:00
* feat(mise): configure railpack to use musl-compatible mise binary - Add dependencies (bash, git) required by mise's internal logic. - Pin the mise version to match the repository's configuration. - Symlink the installed musl-compatible mise binary to the railpack cache directory to ensure binary compatibility on the host side. Generated-by: aiautocommit * feat(mise): target musl-based builds for linux - Refactor getAssetName to accept platform parameters, improving testability. - Update linux asset resolution to favor musl-based binaries for improved compatibility with alpine and other musl environments. - Add unit tests for platform-specific asset naming and error handling. Generated-by: aiautocommit
646 lines
20 KiB
Bash
Executable File
646 lines
20 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Keep this host-side orchestration in Bash so ShellCheck can analyze it;
|
|
# ShellCheck does not support zsh scripts.
|
|
#MISE description="Linux shell (Apple container machine) using host Docker"
|
|
#MISE interactive=true
|
|
#USAGE flag "--restart" help="Delete and recreate the machine from scratch"
|
|
#USAGE flag "--stop" help="Delete the machine and stop the host Docker TCP proxy (socat)"
|
|
|
|
# mise tool installation and project builds can behave differently on Linux
|
|
# than on macOS. This task provides a persistent Linux environment for
|
|
# reproducing and debugging those differences while sharing the Mac project
|
|
# tree and host Docker daemon.
|
|
|
|
set -euo pipefail
|
|
|
|
NAME=railpack-linux
|
|
# Alpine is used because Apple container machines require /sbin/init in the
|
|
# image. Stock ubuntu:24.04 does not have it (needs a custom systemd image /
|
|
# Dockerfile). Alpine ships busybox init and needs no image build.
|
|
IMAGE=alpine:latest
|
|
PORT=2375
|
|
# Keep all mise state on the machine disk. The Mac home is mounted at
|
|
# /Users/...; if mise used ~/.local/share or ~/.config there it would mix
|
|
# host (darwin) installs with guest (linux) ones.
|
|
MISE_DATA_DIR_GUEST=/var/lib/mise
|
|
MISE_CACHE_DIR_GUEST=/var/cache/mise
|
|
MISE_CONFIG_DIR_GUEST=/var/lib/mise/config
|
|
# Host path of this repo (same path inside the machine via the /Users mount).
|
|
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
RAILPACK_MISE_VERSION="$(<"${PROJECT_ROOT}/core/mise/version.txt")"
|
|
RAILPACK_MISE_DIR=/tmp/railpack/mise
|
|
RUNTIME_ROOT="${TMPDIR:-/tmp}"
|
|
PROXY_STATE_DIR="${RUNTIME_ROOT%/}/railpack-linux-shell-${UID}"
|
|
PROXY_STATE_FILE="${PROXY_STATE_DIR}/socat.state"
|
|
PROXY_LOCK_FILE="${PROXY_STATE_DIR}/socat.lock"
|
|
PROXY_LOG_FILE="${PROXY_STATE_DIR}/socat.log"
|
|
|
|
GATEWAY=""
|
|
DOCKER_HOST_TCP=""
|
|
DOCKER_SOCKET=""
|
|
PROXY_LOCK_HELD=false
|
|
PROXY_START_PID=""
|
|
MANAGED_PROXY_PID=""
|
|
MANAGED_PROXY_GATEWAY=""
|
|
MANAGED_PROXY_SOCKET=""
|
|
WAIT_PID=""
|
|
|
|
log() { echo "$*" >&2; }
|
|
|
|
sleep_ms() {
|
|
local milliseconds="$1"
|
|
local duration
|
|
printf -v duration '%d.%03d' "$((milliseconds / 1000))" "$((milliseconds % 1000))"
|
|
sleep "$duration"
|
|
}
|
|
|
|
# Retry a predicate or explicitly retryable command with capped exponential backoff.
|
|
retry_command() {
|
|
local max_attempts="$1"
|
|
local delay_ms="$2"
|
|
local max_delay_ms="$3"
|
|
local label="$4"
|
|
local report_retries="$5"
|
|
shift 5
|
|
|
|
local attempt=1
|
|
local ec=0
|
|
while ((attempt <= max_attempts)); do
|
|
if "$@"; then
|
|
return 0
|
|
else
|
|
ec=$?
|
|
fi
|
|
|
|
if ((attempt == max_attempts)); then
|
|
return "$ec"
|
|
fi
|
|
if [[ "$report_retries" == "true" ]]; then
|
|
log "==> ${label} failed (exit ${ec}); retry $((attempt + 1))/${max_attempts}"
|
|
fi
|
|
sleep_ms "$delay_ms"
|
|
delay_ms=$((delay_ms * 2))
|
|
if ((delay_ms > max_delay_ms)); then
|
|
delay_ms="$max_delay_ms"
|
|
fi
|
|
attempt=$((attempt + 1))
|
|
done
|
|
}
|
|
|
|
# Host gateway on Apple's default container network (not OrbStack, not loopback).
|
|
discover_gateway() {
|
|
local quiet="${1:-false}"
|
|
local network_json gateway
|
|
|
|
if ! command -v container >/dev/null 2>&1; then
|
|
[[ "$quiet" == "true" ]] || log "error: Apple container CLI is not installed"
|
|
return 1
|
|
fi
|
|
if ! command -v jq >/dev/null 2>&1; then
|
|
[[ "$quiet" == "true" ]] || log "error: jq is required to inspect the container network"
|
|
return 1
|
|
fi
|
|
if ! network_json="$(container network inspect default 2>&1)"; then
|
|
if [[ "$quiet" != "true" ]]; then
|
|
log "error: could not inspect the default container network (is 'container system' running?)"
|
|
log "$network_json"
|
|
fi
|
|
return 1
|
|
fi
|
|
if ! gateway="$(printf '%s\n' "$network_json" | jq -er '.[0].status.ipv4Gateway | select(type == "string" and length > 0)' 2>/dev/null)"; then
|
|
[[ "$quiet" == "true" ]] || log "error: default container network has no IPv4 gateway"
|
|
return 1
|
|
fi
|
|
|
|
GATEWAY="$gateway"
|
|
DOCKER_HOST_TCP="tcp://${GATEWAY}:${PORT}"
|
|
}
|
|
|
|
resolve_docker_socket() {
|
|
local quiet="${1:-false}"
|
|
local socket
|
|
if ! socket="$(realpath /var/run/docker.sock 2>/dev/null)" || [[ ! -S "$socket" ]]; then
|
|
[[ "$quiet" == "true" ]] || log "error: Docker socket not found at /var/run/docker.sock (is Docker/OrbStack running?)"
|
|
return 1
|
|
fi
|
|
DOCKER_SOCKET="$socket"
|
|
}
|
|
|
|
docker_api_ok() {
|
|
curl -fsS --connect-timeout 1 "http://${GATEWAY}:${PORT}/_ping" 2>/dev/null | grep -qx OK
|
|
}
|
|
|
|
# Guest commands run once so a real guest failure is not mistaken for a
|
|
# transient machine transport failure. Readiness checks are retried separately.
|
|
machine_run() {
|
|
container machine run -n "$NAME" "$@"
|
|
}
|
|
|
|
machine_is_ready() {
|
|
container machine run -n "$NAME" -- true >/dev/null 2>&1
|
|
}
|
|
|
|
wait_machine_ready() {
|
|
if retry_command 10 250 2000 "container machine readiness" false machine_is_ready; then
|
|
return 0
|
|
fi
|
|
log "error: container machine $NAME did not become ready after repeated attempts"
|
|
return 1
|
|
}
|
|
|
|
wait_docker_proxy_ready() {
|
|
retry_command 6 100 1000 "Docker proxy readiness" false docker_api_ok
|
|
}
|
|
|
|
lsof_path() {
|
|
if [[ -x /usr/sbin/lsof ]]; then
|
|
echo /usr/sbin/lsof
|
|
else
|
|
command -v lsof 2>/dev/null
|
|
fi
|
|
}
|
|
|
|
load_proxy_state() {
|
|
MANAGED_PROXY_PID=""
|
|
MANAGED_PROXY_GATEWAY=""
|
|
MANAGED_PROXY_SOCKET=""
|
|
[[ -f "$PROXY_STATE_FILE" ]] || return 1
|
|
{
|
|
IFS= read -r MANAGED_PROXY_PID
|
|
IFS= read -r MANAGED_PROXY_GATEWAY
|
|
IFS= read -r MANAGED_PROXY_SOCKET
|
|
} <"$PROXY_STATE_FILE"
|
|
[[ "$MANAGED_PROXY_PID" =~ ^[0-9]+$ ]] &&
|
|
[[ -n "$MANAGED_PROXY_GATEWAY" ]] &&
|
|
[[ -n "$MANAGED_PROXY_SOCKET" ]]
|
|
}
|
|
|
|
write_proxy_state() {
|
|
local pid="$1" gateway="$2" socket="$3"
|
|
local state_tmp
|
|
mkdir -p "$PROXY_STATE_DIR"
|
|
chmod 0700 "$PROXY_STATE_DIR"
|
|
state_tmp="$(mktemp "${PROXY_STATE_FILE}.XXXXXX")"
|
|
printf '%s\n%s\n%s\n' "$pid" "$gateway" "$socket" >"$state_tmp"
|
|
mv -f "$state_tmp" "$PROXY_STATE_FILE"
|
|
}
|
|
|
|
clear_proxy_state() {
|
|
rm -f -- "$PROXY_STATE_FILE"
|
|
}
|
|
|
|
try_acquire_proxy_lock() {
|
|
local owner=""
|
|
mkdir -p "$PROXY_STATE_DIR"
|
|
chmod 0700 "$PROXY_STATE_DIR"
|
|
if ln -s "$$" "$PROXY_LOCK_FILE" 2>/dev/null; then
|
|
PROXY_LOCK_HELD=true
|
|
return 0
|
|
fi
|
|
owner="$(readlink "$PROXY_LOCK_FILE" 2>/dev/null || true)"
|
|
if [[ "$owner" =~ ^[0-9]+$ ]] && ! kill -0 "$owner" 2>/dev/null; then
|
|
# Recheck before removing a stale lock in case another process replaced it.
|
|
if [[ "$(readlink "$PROXY_LOCK_FILE" 2>/dev/null || true)" == "$owner" ]]; then
|
|
rm -f -- "$PROXY_LOCK_FILE"
|
|
fi
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
acquire_proxy_lock() {
|
|
if retry_command 40 50 250 "Docker proxy lock" false try_acquire_proxy_lock; then
|
|
return 0
|
|
fi
|
|
log "error: timed out waiting for the Docker proxy lock: $PROXY_LOCK_FILE"
|
|
return 1
|
|
}
|
|
|
|
release_proxy_lock() {
|
|
if [[ "$PROXY_LOCK_HELD" == "true" ]] &&
|
|
[[ "$(readlink "$PROXY_LOCK_FILE" 2>/dev/null || true)" == "$$" ]]; then
|
|
rm -f -- "$PROXY_LOCK_FILE"
|
|
fi
|
|
PROXY_LOCK_HELD=false
|
|
}
|
|
|
|
socat_process_matches() {
|
|
local pid="$1" gateway="$2" socket="$3"
|
|
local lsof_bin comm args listen_arg connect_arg
|
|
[[ "$pid" =~ ^[0-9]+$ ]] || return 1
|
|
[[ -n "$gateway" && -n "$socket" ]] || return 1
|
|
if ! lsof_bin="$(lsof_path)" || [[ -z "$lsof_bin" ]]; then
|
|
return 1
|
|
fi
|
|
comm="$(ps -p "$pid" -o comm= 2>/dev/null || true)"
|
|
comm="${comm#"${comm%%[![:space:]]*}"}"
|
|
comm="${comm%"${comm##*[![:space:]]}"}"
|
|
[[ "${comm##*/}" == "socat" ]] || return 1
|
|
args="$(ps -p "$pid" -o command= 2>/dev/null || true)"
|
|
listen_arg="TCP-LISTEN:${PORT},bind=${gateway},reuseaddr,fork"
|
|
connect_arg="UNIX-CONNECT:${socket}"
|
|
[[ "$args" == *"$listen_arg"* && "$args" == *"$connect_arg"* ]] || return 1
|
|
"$lsof_bin" -a -p "$pid" -t -nP "-iTCP@${gateway}:${PORT}" -sTCP:LISTEN 2>/dev/null |
|
|
grep -qx "$pid"
|
|
}
|
|
|
|
listener_pids() {
|
|
local gateway="$1" lsof_bin
|
|
if ! lsof_bin="$(lsof_path)" || [[ -z "$lsof_bin" ]]; then
|
|
return 1
|
|
fi
|
|
"$lsof_bin" -t -nP "-iTCP@${gateway}:${PORT}" -sTCP:LISTEN 2>/dev/null
|
|
}
|
|
|
|
find_matching_socat() {
|
|
local gateway="$1" socket="$2" pid
|
|
while IFS= read -r pid; do
|
|
if socat_process_matches "$pid" "$gateway" "$socket"; then
|
|
echo "$pid"
|
|
return 0
|
|
fi
|
|
done < <(listener_pids "$gateway" || true)
|
|
return 1
|
|
}
|
|
|
|
process_is_gone() {
|
|
! kill -0 "$WAIT_PID" 2>/dev/null
|
|
}
|
|
|
|
cleanup_proxy_setup() {
|
|
if [[ -n "$PROXY_START_PID" ]] && kill -0 "$PROXY_START_PID" 2>/dev/null; then
|
|
kill "$PROXY_START_PID" 2>/dev/null || true
|
|
fi
|
|
PROXY_START_PID=""
|
|
release_proxy_lock
|
|
}
|
|
|
|
# Stop only a listener proven to be socat with the expected bridge and socket arguments.
|
|
stop_docker_proxy() {
|
|
local pid="" gateway="" socket=""
|
|
acquire_proxy_lock
|
|
trap cleanup_proxy_setup EXIT
|
|
|
|
if load_proxy_state; then
|
|
gateway="$MANAGED_PROXY_GATEWAY"
|
|
socket="$MANAGED_PROXY_SOCKET"
|
|
if socat_process_matches "$MANAGED_PROXY_PID" "$gateway" "$socket"; then
|
|
pid="$MANAGED_PROXY_PID"
|
|
else
|
|
pid="$(find_matching_socat "$gateway" "$socket" || true)"
|
|
fi
|
|
else
|
|
# Best-effort adoption supports proxies started before state tracking existed.
|
|
if discover_gateway true && resolve_docker_socket true; then
|
|
gateway="$GATEWAY"
|
|
socket="$DOCKER_SOCKET"
|
|
pid="$(find_matching_socat "$gateway" "$socket" || true)"
|
|
fi
|
|
fi
|
|
|
|
if [[ -z "$pid" ]]; then
|
|
log "==> No matching socat Docker proxy found"
|
|
clear_proxy_state
|
|
release_proxy_lock
|
|
trap - EXIT
|
|
return 0
|
|
fi
|
|
|
|
if ! socat_process_matches "$pid" "$gateway" "$socket"; then
|
|
log "error: refusing to stop PID ${pid}; it no longer matches the expected socat proxy"
|
|
clear_proxy_state
|
|
release_proxy_lock
|
|
trap - EXIT
|
|
return 1
|
|
fi
|
|
log "==> Stopping socat Docker proxy on ${gateway}:${PORT} (PID ${pid})"
|
|
kill "$pid"
|
|
WAIT_PID="$pid"
|
|
if ! retry_command 10 100 500 "socat shutdown" false process_is_gone; then
|
|
log "error: socat process ${pid} did not stop"
|
|
release_proxy_lock
|
|
trap - EXIT
|
|
return 1
|
|
fi
|
|
clear_proxy_state
|
|
log "==> Stopped socat Docker proxy (PID ${pid})"
|
|
release_proxy_lock
|
|
trap - EXIT
|
|
}
|
|
|
|
# The container machine cannot use the host's Unix Docker socket directly: the
|
|
# socket lives on macOS, while Docker commands run inside the Linux guest. socat
|
|
# bridges a TCP endpoint on Apple's container-machine gateway to that host Unix
|
|
# socket, allowing the guest Docker CLI to use it through DOCKER_HOST.
|
|
#
|
|
# Reuse an existing Docker API listener (OrbStack/etc.) when possible. Otherwise
|
|
# start our own proxy bound only to the machine bridge address, never 0.0.0.0,
|
|
# because access to this unauthenticated endpoint grants control of host Docker.
|
|
ensure_docker_proxy() {
|
|
local pid="" existing_pids="" socat_bin=""
|
|
acquire_proxy_lock
|
|
trap cleanup_proxy_setup EXIT
|
|
|
|
if docker_api_ok; then
|
|
if load_proxy_state &&
|
|
socat_process_matches "$MANAGED_PROXY_PID" "$MANAGED_PROXY_GATEWAY" "$MANAGED_PROXY_SOCKET"; then
|
|
log "==> Using managed socat Docker proxy at ${DOCKER_HOST_TCP} (PID ${MANAGED_PROXY_PID})"
|
|
elif resolve_docker_socket true &&
|
|
pid="$(find_matching_socat "$GATEWAY" "$DOCKER_SOCKET" || true)" && [[ -n "$pid" ]]; then
|
|
write_proxy_state "$pid" "$GATEWAY" "$DOCKER_SOCKET"
|
|
log "==> Using recognized socat Docker proxy at ${DOCKER_HOST_TCP} (PID ${pid})"
|
|
log "==> mise-linux-shell --stop will stop this exact proxy"
|
|
else
|
|
clear_proxy_state
|
|
log "==> Using existing Docker API listener at ${DOCKER_HOST_TCP}"
|
|
log "==> Listener is not a matching socat proxy and will not be stopped by --stop"
|
|
fi
|
|
release_proxy_lock
|
|
trap - EXIT
|
|
return 0
|
|
fi
|
|
|
|
if ! socat_bin="$(command -v socat 2>/dev/null)" || [[ -z "$socat_bin" ]]; then
|
|
log "error: host Docker API not reachable at ${DOCKER_HOST_TCP} and socat is not installed"
|
|
log ""
|
|
log " brew install socat"
|
|
log " # then re-run: mise run mise-linux-shell"
|
|
release_proxy_lock
|
|
trap - EXIT
|
|
return 1
|
|
fi
|
|
if ! resolve_docker_socket; then
|
|
release_proxy_lock
|
|
trap - EXIT
|
|
return 1
|
|
fi
|
|
|
|
pid="$(find_matching_socat "$GATEWAY" "$DOCKER_SOCKET" || true)"
|
|
if [[ -n "$pid" ]]; then
|
|
write_proxy_state "$pid" "$GATEWAY" "$DOCKER_SOCKET"
|
|
log "error: matching socat proxy is listening at ${DOCKER_HOST_TCP} (PID ${pid}), but the Docker API did not respond"
|
|
log " check that Docker is running and the socket is healthy: ${DOCKER_SOCKET}"
|
|
release_proxy_lock
|
|
trap - EXIT
|
|
return 1
|
|
fi
|
|
existing_pids="$(listener_pids "$GATEWAY" || true)"
|
|
if [[ -n "$existing_pids" ]]; then
|
|
log "error: ${GATEWAY}:${PORT} is already occupied by a non-matching listener (pids: ${existing_pids//$'\n'/ })"
|
|
release_proxy_lock
|
|
trap - EXIT
|
|
return 1
|
|
fi
|
|
|
|
# Bind only to the bridge gateway — not 0.0.0.0 — so the API stays off the LAN.
|
|
: >"$PROXY_LOG_FILE"
|
|
nohup "$socat_bin" \
|
|
"TCP-LISTEN:${PORT},bind=${GATEWAY},reuseaddr,fork" \
|
|
"UNIX-CONNECT:${DOCKER_SOCKET}" >"$PROXY_LOG_FILE" 2>&1 &
|
|
PROXY_START_PID=$!
|
|
disown 2>/dev/null || true
|
|
log "==> Starting socat Docker proxy on ${GATEWAY}:${PORT} -> ${DOCKER_SOCKET} (PID ${PROXY_START_PID})"
|
|
|
|
if ! wait_docker_proxy_ready; then
|
|
log "error: started socat but Docker API still not reachable at ${DOCKER_HOST_TCP}"
|
|
if [[ -s "$PROXY_LOG_FILE" ]]; then
|
|
log "==> socat output ($PROXY_LOG_FILE):"
|
|
sed -n '1,20p' "$PROXY_LOG_FILE" >&2
|
|
fi
|
|
cleanup_proxy_setup
|
|
trap - EXIT
|
|
return 1
|
|
fi
|
|
|
|
PROXY_START_PID="$(find_matching_socat "$GATEWAY" "$DOCKER_SOCKET" || true)"
|
|
if [[ -z "$PROXY_START_PID" ]]; then
|
|
log "error: Docker API became reachable, but the expected socat listener could not be identified"
|
|
cleanup_proxy_setup
|
|
trap - EXIT
|
|
return 1
|
|
fi
|
|
write_proxy_state "$PROXY_START_PID" "$GATEWAY" "$DOCKER_SOCKET"
|
|
log "==> Started managed socat Docker proxy at ${DOCKER_HOST_TCP} (PID ${PROXY_START_PID})"
|
|
log "==> Run 'mise run mise-linux-shell --stop' to stop it"
|
|
PROXY_START_PID=""
|
|
release_proxy_lock
|
|
trap - EXIT
|
|
return 0
|
|
}
|
|
|
|
destroy_machine() {
|
|
log "==> Stopping machine: $NAME"
|
|
container machine stop "$NAME" 2>/dev/null || true
|
|
container machine delete "$NAME" 2>/dev/null || true
|
|
}
|
|
|
|
if [[ "${usage_stop:-false}" == "true" ]]; then
|
|
if [[ "${usage_restart:-false}" == "true" ]]; then
|
|
log "error: --stop and --restart are mutually exclusive"
|
|
exit 1
|
|
fi
|
|
destroy_machine
|
|
stop_docker_proxy
|
|
log "==> Stopped"
|
|
exit 0
|
|
fi
|
|
|
|
guest_mise_env=(
|
|
-e "MISE_DATA_DIR=${MISE_DATA_DIR_GUEST}"
|
|
-e "MISE_CACHE_DIR=${MISE_CACHE_DIR_GUEST}"
|
|
-e "MISE_CONFIG_DIR=${MISE_CONFIG_DIR_GUEST}"
|
|
-e "MISE_GLOBAL_CONFIG_FILE=${MISE_CONFIG_DIR_GUEST}/config.toml"
|
|
)
|
|
|
|
ensure_mise_dirs() {
|
|
machine_run --root -- \
|
|
mkdir -p "$MISE_DATA_DIR_GUEST" "$MISE_CACHE_DIR_GUEST" "$MISE_CONFIG_DIR_GUEST"
|
|
# The machine user owns mise state; other guest users only need traversal.
|
|
machine_run --root -- \
|
|
chown "$USER" "$MISE_DATA_DIR_GUEST" "$MISE_CACHE_DIR_GUEST" "$MISE_CONFIG_DIR_GUEST"
|
|
machine_run --root -- \
|
|
chmod 0755 "$MISE_DATA_DIR_GUEST" "$MISE_CACHE_DIR_GUEST" "$MISE_CONFIG_DIR_GUEST"
|
|
machine_run -- \
|
|
touch "${MISE_CONFIG_DIR_GUEST}/config.toml"
|
|
# Empty global config used for project `mise install` so guest-only tools
|
|
# like usage (completions) are not required to be in the project lockfile.
|
|
machine_run -- \
|
|
rm -f "${MISE_CONFIG_DIR_GUEST}/empty.toml"
|
|
machine_run -- \
|
|
touch "${MISE_CONFIG_DIR_GUEST}/empty.toml"
|
|
}
|
|
|
|
# Packages + Linux mise + zsh. container machine run mangles `sh -c '...'`.
|
|
bootstrap_machine() {
|
|
wait_machine_ready
|
|
|
|
log "==> Bootstrapping machine packages"
|
|
# bash/git: mise's Python backend uses python-build to resolve versions
|
|
# docker-cli: talk to host Docker via DOCKER_HOST
|
|
# zsh: interactive shell
|
|
# libstdc++/libgcc/gcompat: many mise tools ship glibc/musl hybrids
|
|
# curl: mise installer
|
|
machine_run --root -- \
|
|
apk add bash git docker-cli zsh curl libstdc++ libgcc gcompat
|
|
|
|
log "==> Installing mise"
|
|
machine_run --root -- \
|
|
curl -fsSL -o /tmp/mise-install.sh https://mise.run
|
|
# Alpine is musl — force the musl build (see mise.run / server.sh tip).
|
|
machine_run --root \
|
|
-e MISE_INSTALL_MUSL=1 \
|
|
-e MISE_INSTALL_PATH=/usr/local/bin/mise \
|
|
-e "MISE_VERSION=${RAILPACK_MISE_VERSION}" \
|
|
-- sh /tmp/mise-install.sh
|
|
|
|
ensure_mise_dirs
|
|
setup_zsh
|
|
}
|
|
|
|
# Railpack caches a separate mise binary for host-side version resolution. Its
|
|
# generic Linux download is glibc-linked, so reuse the pinned musl binary that
|
|
# this Alpine machine already installed instead.
|
|
configure_railpack_mise() {
|
|
log "==> Configuring Railpack to use musl mise ${RAILPACK_MISE_VERSION}"
|
|
machine_run -- mkdir -p "$RAILPACK_MISE_DIR"
|
|
machine_run -- \
|
|
ln -sfn /usr/local/bin/mise "${RAILPACK_MISE_DIR}/mise-${RAILPACK_MISE_VERSION}"
|
|
}
|
|
|
|
# mise stops at the ceiling and does not load configs *at* that path. Use the
|
|
# project's parent so this repo's mise.toml loads, but never parent dirs or ~.
|
|
guest_ceiling() {
|
|
echo "${MISE_CEILING_PATHS:-$(dirname "$PROJECT_ROOT")}"
|
|
}
|
|
|
|
# Host project tree is mounted into the machine; guest mise state is fresh after
|
|
# create/restart, so trust the project before any command that loads its config.
|
|
# Trust PROJECT_ROOT only — not --all (that walks parents too).
|
|
trust_guest_mise() {
|
|
local ceiling
|
|
ceiling="$(guest_ceiling)"
|
|
machine_run \
|
|
-e "MISE_CEILING_PATHS=${ceiling}" \
|
|
"${guest_mise_env[@]}" \
|
|
-- mise trust "$PROJECT_ROOT"
|
|
}
|
|
|
|
# Install usage CLI for mise task completions without adding it to any mise
|
|
# config. Project settings.locked=true would reject a global `usage` tool that
|
|
# is not in mise.lock.
|
|
install_usage_cli() {
|
|
log "==> Installing usage (completions helper, not a project tool)"
|
|
# Isolate from project + guest global config so locked mode cannot apply.
|
|
machine_run \
|
|
-w /var/tmp \
|
|
-e MISE_CEILING_PATHS=/var/tmp \
|
|
-e "MISE_DATA_DIR=${MISE_DATA_DIR_GUEST}" \
|
|
-e "MISE_CACHE_DIR=${MISE_CACHE_DIR_GUEST}" \
|
|
-e "MISE_CONFIG_DIR=${MISE_CONFIG_DIR_GUEST}" \
|
|
-e "MISE_GLOBAL_CONFIG_FILE=${MISE_CONFIG_DIR_GUEST}/empty.toml" \
|
|
-e MISE_LOCKED=0 \
|
|
-e CI=1 \
|
|
-- mise install usage@latest
|
|
# Keep global config free of usage so project activate stays clean.
|
|
machine_run -- \
|
|
cp "${MISE_CONFIG_DIR_GUEST}/empty.toml" "${MISE_CONFIG_DIR_GUEST}/config.toml"
|
|
machine_run --root -- \
|
|
ln -sfn "${MISE_DATA_DIR_GUEST}/installs/usage/latest/usage" /usr/local/bin/usage
|
|
}
|
|
|
|
# zsh + mise (modeled on https://github.com/wintermi/zsh-mise/blob/main/zsh-mise.plugin.zsh).
|
|
# usage is a prerequisite for mise completions (same as that plugin's README).
|
|
setup_zsh() {
|
|
log "==> Configuring zsh + mise"
|
|
local zshrc_host
|
|
trust_guest_mise
|
|
install_usage_cli
|
|
|
|
machine_run --root -- mkdir -p /etc/zsh
|
|
|
|
# Write zshrc via the shared project mount (no stdin pipe into machine run).
|
|
mkdir -p "${PROJECT_ROOT}/tmp"
|
|
zshrc_host="$(mktemp "${PROJECT_ROOT}/tmp/railpack-linux-zshrc.XXXXXX")"
|
|
trap 'rm -f -- "$zshrc_host"' EXIT
|
|
printf '%s\n' \
|
|
'# railpack-linux-shell — https://github.com/wintermi/zsh-mise' \
|
|
'if (( $+commands[mise] )); then' \
|
|
' source <(mise activate zsh)' \
|
|
' source <(mise hook-env -s zsh)' \
|
|
' _mise_comp_dir="${MISE_CACHE_DIR:-${XDG_CACHE_HOME:-$HOME/.cache}/mise}/zsh-completions"' \
|
|
' mkdir -p "$_mise_comp_dir"' \
|
|
' fpath=("$_mise_comp_dir" $fpath)' \
|
|
' if [[ ! -f "$_mise_comp_dir/_mise" ]]; then' \
|
|
' typeset -g -A _comps' \
|
|
' autoload -Uz _mise' \
|
|
' _comps[mise]=_mise' \
|
|
' fi' \
|
|
' mise completion zsh >| "$_mise_comp_dir/_mise" &|' \
|
|
'fi' \
|
|
'autoload -Uz compinit && compinit' \
|
|
>"$zshrc_host"
|
|
machine_run --root -- cp "$zshrc_host" /etc/zsh/zshrc
|
|
rm -f -- "$zshrc_host"
|
|
trap - EXIT
|
|
|
|
# Alpine has neither chsh nor usermod; interactive entry always runs zsh -l.
|
|
}
|
|
|
|
run_mise_install() {
|
|
local ceiling
|
|
ceiling="$(guest_ceiling)"
|
|
log "==> mise install (project tools)"
|
|
trust_guest_mise
|
|
# Use empty global config: guest global config may list `usage` for shell
|
|
# completions, which is not in the project lockfile (settings.locked=true).
|
|
machine_run \
|
|
-e "MISE_CEILING_PATHS=${ceiling}" \
|
|
-e "MISE_DATA_DIR=${MISE_DATA_DIR_GUEST}" \
|
|
-e "MISE_CACHE_DIR=${MISE_CACHE_DIR_GUEST}" \
|
|
-e "MISE_CONFIG_DIR=${MISE_CONFIG_DIR_GUEST}" \
|
|
-e "MISE_GLOBAL_CONFIG_FILE=${MISE_CONFIG_DIR_GUEST}/empty.toml" \
|
|
-e CI=1 \
|
|
-- mise install
|
|
}
|
|
|
|
create_machine() {
|
|
log "==> Creating $NAME ($IMAGE)"
|
|
container machine create "$IMAGE" --name "$NAME" --cpus 4 --memory 8G
|
|
bootstrap_machine
|
|
run_mise_install
|
|
}
|
|
|
|
discover_gateway
|
|
ensure_docker_proxy
|
|
|
|
if [[ "${usage_restart:-false}" == "true" ]]; then
|
|
log "==> --restart: deleting $NAME"
|
|
destroy_machine
|
|
fi
|
|
|
|
if ! container machine inspect "$NAME" &>/dev/null; then
|
|
create_machine
|
|
else
|
|
wait_machine_ready
|
|
if ! machine_run -- command -v mise >/dev/null 2>&1; then
|
|
bootstrap_machine
|
|
run_mise_install
|
|
else
|
|
ensure_mise_dirs
|
|
fi
|
|
fi
|
|
|
|
wait_machine_ready
|
|
configure_railpack_mise
|
|
|
|
CEILING="$(guest_ceiling)"
|
|
|
|
log "==> DOCKER_HOST=$DOCKER_HOST_TCP"
|
|
log "==> MISE_DATA_DIR=$MISE_DATA_DIR_GUEST (machine-local, not Mac home)"
|
|
log "==> MISE_CEILING_PATHS=$CEILING"
|
|
exec container machine run -it \
|
|
-n "$NAME" \
|
|
-e "DOCKER_HOST=${DOCKER_HOST_TCP}" \
|
|
-e "MISE_CEILING_PATHS=${CEILING}" \
|
|
"${guest_mise_env[@]}" \
|
|
-- zsh -l
|