1459 lines
42 KiB
Bash
Executable File
1459 lines
42 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# sloptrap
|
|
set -euo pipefail
|
|
|
|
IS_MAC=false
|
|
if [[ $(uname -s 2>/dev/null) == "Darwin" ]]; then
|
|
IS_MAC=true
|
|
mac_gnu_bins=(
|
|
/opt/homebrew/opt/coreutils/libexec/gnubin
|
|
/usr/local/opt/coreutils/libexec/gnubin
|
|
/opt/homebrew/opt/gnu-tar/libexec/gnubin
|
|
/usr/local/opt/gnu-tar/libexec/gnubin
|
|
)
|
|
for bin_dir in "${mac_gnu_bins[@]}"; do
|
|
[[ -d $bin_dir ]] || continue
|
|
case ":$PATH:" in
|
|
*":$bin_dir:"*)
|
|
;;
|
|
*)
|
|
path_head=${PATH%%:*}
|
|
path_rest=""
|
|
if [[ $PATH == *:* ]]; then
|
|
path_rest=${PATH#*:}
|
|
fi
|
|
case "$path_head" in
|
|
/tmp/*|/var/folders/*|"$HOME"/*)
|
|
if [[ -n $path_rest ]]; then
|
|
PATH="$path_head:$bin_dir:$path_rest"
|
|
else
|
|
PATH="$path_head:$bin_dir"
|
|
fi
|
|
;;
|
|
*)
|
|
PATH="$bin_dir:$PATH"
|
|
;;
|
|
esac
|
|
unset -v path_head path_rest
|
|
;;
|
|
esac
|
|
done
|
|
export PATH
|
|
fi
|
|
|
|
REQUIRED_BREW_PKGS="coreutils gnu-tar jq"
|
|
|
|
require_cmd() {
|
|
if command -v "$1" >/dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
if $IS_MAC; then
|
|
error "'$1' is required; install with: brew install $REQUIRED_BREW_PKGS"
|
|
else
|
|
error "'$1' is required"
|
|
fi
|
|
}
|
|
for c in curl tar sha256sum realpath jq; do require_cmd "$c"; done
|
|
|
|
COLOR_TEXT=$'\033[38;5;247m'
|
|
COLOR_HIGHLIGHT=$'\033[38;5;202m'
|
|
COLOR_ERROR=$'\033[38;5;160m'
|
|
COLOR_COMMENT=$'\033[38;5;242m'
|
|
RESET=$'\033[0m'
|
|
BOLD=$'\033[1m'
|
|
|
|
PREFIX_TEXT=$'\033[38;5;247m░\033[0m '
|
|
PREFIX_HIGHLIGHT=$'\033[38;5;202m█\033[0m '
|
|
PREFIX_ERROR=$'\033[38;5;160m▒\033[0m '
|
|
PREFIX_COMMENT=$'\033[38;5;242m▒\033[0m '
|
|
|
|
print_styled() {
|
|
local color=$1
|
|
local prefix=$2
|
|
local fmt=$3
|
|
shift 3
|
|
printf '%s' "$prefix"
|
|
printf '%b' "$color"
|
|
if (($# > 0)); then
|
|
local -a colored_args=()
|
|
local arg
|
|
for arg in "$@"; do
|
|
colored_args+=("$COLOR_HIGHLIGHT$arg$color")
|
|
done
|
|
# Format strings are defined within sloptrap, not user input.
|
|
# shellcheck disable=SC2059
|
|
printf "$fmt" "${colored_args[@]}"
|
|
else
|
|
printf '%b' "$fmt"
|
|
fi
|
|
printf '%b' "$RESET"
|
|
}
|
|
|
|
print_styled_err() {
|
|
local color=$1
|
|
local prefix=$2
|
|
local fmt=$3
|
|
shift 3
|
|
printf '%s' "$prefix" >&2
|
|
printf '%b' "$color" >&2
|
|
if (($# > 0)); then
|
|
local -a colored_args=()
|
|
local arg
|
|
for arg in "$@"; do
|
|
colored_args+=("$COLOR_HIGHLIGHT$arg$color")
|
|
done
|
|
# Format strings are defined within sloptrap, not user input.
|
|
# shellcheck disable=SC2059
|
|
printf "$fmt" "${colored_args[@]}" >&2
|
|
else
|
|
printf '%b' "$fmt" >&2
|
|
fi
|
|
printf '%b' "$RESET" >&2
|
|
}
|
|
|
|
info_line() {
|
|
print_styled "$COLOR_TEXT" "$PREFIX_TEXT" "$@"
|
|
}
|
|
|
|
highlight_line() {
|
|
print_styled "$COLOR_HIGHLIGHT" "$PREFIX_TEXT" "$@"
|
|
}
|
|
|
|
status_line() {
|
|
print_styled "$COLOR_COMMENT" "$PREFIX_TEXT" "$@"
|
|
}
|
|
|
|
comment_line() {
|
|
print_styled "$COLOR_COMMENT" "$PREFIX_COMMENT" "$@"
|
|
}
|
|
|
|
warn_line() {
|
|
print_styled_err "$COLOR_HIGHLIGHT" "$PREFIX_HIGHLIGHT" "$@"
|
|
}
|
|
|
|
error_line() {
|
|
print_styled_err "$COLOR_ERROR" "$PREFIX_ERROR" "$@"
|
|
}
|
|
|
|
print_banner() {
|
|
printf '%b' "${COLOR_HIGHLIGHT}${BOLD}"
|
|
cat <<'EOF'
|
|
██████ ██▓ ▒█████ ██▓███ ▄▄▄█████▓ ██▀███ ▄▄▄ ██▓███
|
|
▒██ ▒ ▓██▒ ▒██▒ ██▒▓██░ ██▒▓ ██▒ ▓▒▓██ ▒ ██▒▒████▄ ▓██░ ██▒
|
|
░ ▓██▄ ▒██░ ▒██░ ██▒▓██░ ██▓▒▒ ▓██░ ▒░▓██ ░▄█ ▒▒██ ▀█▄ ▓██░ ██▓▒
|
|
▒ ██▒▒██░ ▒██ ██░▒██▄█▓▒ ▒░ ▓██▓ ░ ▒██▀▀█▄ ░██▄▄▄▄██ ▒██▄█▓▒ ▒
|
|
▒██████▒▒░██████▒░ ████▓▒░▒██▒ ░ ░ ▒██▒ ░ ░██▓ ▒██▒ ▓█ ▓██▒▒██▒ ░ ░
|
|
▒ ▒▓▒ ▒ ░░ ▒░▓ ░░ ▒░▒░▒░ ▒▓▒░ ░ ░ ▒ ░░ ░ ▒▓ ░▒▓░ ▒▒ ▓▒█░▒▓▒░ ░
|
|
░ ░▒ ░ ░░ ░ ▒░ https://git.sk4.nz/sk4nz/skz-sloptrap ▒ ▒▒ ░░▒ ░
|
|
░ ░ ░ ░ ░ ░ ░ ▒ ░░ ░ ░░ ░ ░ ▒ ░░
|
|
░ ░ ░ ░ ░ ░ ░ ░
|
|
EOF
|
|
printf '%b' "$RESET"
|
|
}
|
|
|
|
MANIFEST_BASENAME=".sloptrap"
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
VALID_NAME_REGEX='^[A-Za-z0-9_.-]+$'
|
|
DEFAULT_CODEX_ARGS=(--sandbox workspace-write)
|
|
SLOPTRAP_IMAGE_LABEL_KEY="net.sk4nz.sloptrap.managed"
|
|
SLOPTRAP_IMAGE_LABEL="${SLOPTRAP_IMAGE_LABEL_KEY}=1"
|
|
|
|
usage() {
|
|
print_banner
|
|
info_line "Usage: %s [options] <code-directory> [target ...]\n" "$0"
|
|
info_line "Options:\n"
|
|
comment_line " --dry-run Show planned container command(s) and exit\n"
|
|
comment_line " --print-config Display resolved manifest values\n"
|
|
comment_line " -h, --help Show this message\n"
|
|
info_line "\n"
|
|
comment_line "Each project supplies configuration via a %s file in its root.\n" "$MANIFEST_BASENAME"
|
|
info_line "Example manifest entries:\n"
|
|
comment_line " name=my-project\n"
|
|
comment_line " packages_extra=kubectl helm\n"
|
|
comment_line " default_targets=run\n"
|
|
comment_line " codex_args=--sandbox workspace-write\n"
|
|
info_line "\n"
|
|
info_line "Example targets:\n"
|
|
comment_line " run Build if needed, then launch Codex\n"
|
|
comment_line " resume <id> Build if needed, then run 'codex resume <id>'\n"
|
|
comment_line " shell Drop into an interactive /bin/bash session\n"
|
|
comment_line " clean Remove the project container/image cache\n"
|
|
comment_line " prune Remove dangling/unused sloptrap images\n"
|
|
}
|
|
|
|
error() {
|
|
error_line "error: %s\n" "$1"
|
|
exit 1
|
|
}
|
|
|
|
warn() {
|
|
warn_line "warning: %s\n" "$1"
|
|
}
|
|
|
|
trim() {
|
|
local value=$1
|
|
value="${value#"${value%%[![:space:]]*}"}"
|
|
value="${value%"${value##*[![:space:]]}"}"
|
|
printf '%s' "$value"
|
|
}
|
|
|
|
resolve_path_relaxed() {
|
|
local candidate=$1
|
|
local resolved
|
|
if resolved=$(realpath -m "$candidate" 2>/dev/null); then
|
|
printf '%s' "$resolved"
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
resolve_path_strict() {
|
|
local candidate=$1
|
|
local resolved
|
|
if resolved=$(resolve_path_relaxed "$candidate"); then
|
|
printf '%s' "$resolved"
|
|
return 0
|
|
fi
|
|
error "failed to resolve path '$candidate'; install GNU coreutils for realpath support"
|
|
}
|
|
|
|
declare -A MANIFEST=()
|
|
declare -a SLOPTRAP_IGNORE_ENTRIES=()
|
|
declare -a IGNORE_MOUNT_ARGS=()
|
|
MANIFEST_PRESENT=false
|
|
|
|
CURRENT_IGNORE_FILE=""
|
|
CONTAINER_ENGINE=""
|
|
CODEX_HOME_HOST=""
|
|
CODEX_HOME_BOOTSTRAP=false
|
|
NEED_LOGIN=false
|
|
IGNORE_STUB_BASE=""
|
|
IGNORE_HELPER_ROOT=""
|
|
ALLOW_HOST_NETWORK=false
|
|
|
|
declare -a SLOPTRAP_TEMP_PATHS=()
|
|
|
|
register_temp_path() {
|
|
SLOPTRAP_TEMP_PATHS+=("$1")
|
|
}
|
|
|
|
cleanup_temp_paths() {
|
|
local path
|
|
for path in "${SLOPTRAP_TEMP_PATHS[@]}"; do
|
|
[[ -n ${path:-} ]] || continue
|
|
rm -rf "$path" >/dev/null 2>&1 || true
|
|
done
|
|
}
|
|
|
|
cleanup_ignore_stub_dir() {
|
|
local helper_root=${IGNORE_HELPER_ROOT:-}
|
|
local stub_base=${IGNORE_STUB_BASE:-}
|
|
[[ -n $helper_root && -n $stub_base ]] || return 0
|
|
[[ -d $stub_base ]] || return 0
|
|
local resolved_helper resolved_stub
|
|
if ! resolved_helper=$(resolve_path_relaxed "$helper_root"); then
|
|
warn "failed to resolve helper root '$helper_root' during cleanup"
|
|
return 0
|
|
fi
|
|
if ! resolved_stub=$(resolve_path_relaxed "$stub_base"); then
|
|
warn "failed to resolve helper stub '$stub_base' during cleanup"
|
|
return 0
|
|
fi
|
|
case "$resolved_stub" in
|
|
"$resolved_helper"/session-*|"$resolved_helper"/fallback)
|
|
rm -rf "$resolved_stub"
|
|
;;
|
|
*)
|
|
warn "refusing to remove unexpected helper path '$resolved_stub'"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
sloptrap_exit_trap() {
|
|
cleanup_temp_paths
|
|
cleanup_ignore_stub_dir
|
|
}
|
|
|
|
trap sloptrap_exit_trap EXIT INT TERM HUP
|
|
|
|
create_temp_dir() {
|
|
local label=${1:-tmp}
|
|
local template="${TMPDIR:-/tmp}/sloptrap.${label}.XXXXXXXX"
|
|
local dir
|
|
if ! dir=$(mktemp -d "$template"); then
|
|
error "failed to create temporary directory under ${TMPDIR:-/tmp}"
|
|
fi
|
|
register_temp_path "$dir"
|
|
printf '%s' "$dir"
|
|
}
|
|
|
|
write_embedded_dockerfile() {
|
|
cat <<'EOF'
|
|
# Dockerfile.sloptrap
|
|
ARG BASE_IMAGE=debian:trixie-slim
|
|
FROM ${BASE_IMAGE}
|
|
|
|
ENV DEBIAN_FRONTEND=noninteractive
|
|
|
|
ARG BASE_PACKAGES="curl bash ca-certificates libstdc++6 ripgrep xxd file procps"
|
|
ARG EXTRA_PACKAGES=""
|
|
RUN apt-get update \
|
|
&& apt-get install -y --no-install-recommends apt-utils ${BASE_PACKAGES} ${EXTRA_PACKAGES} \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
ARG CODEX_UID=1337
|
|
ARG CODEX_GID=1337
|
|
RUN groupadd --gid ${CODEX_GID} sloptrap \
|
|
&& useradd --create-home --home-dir /home/sloptrap \
|
|
--gid sloptrap --uid ${CODEX_UID} --shell /bin/bash sloptrap
|
|
|
|
ARG CODEX_BIN=codex
|
|
ARG CODEX_CONF=config/config.toml
|
|
COPY ${CODEX_BIN} /usr/local/bin/codex
|
|
|
|
RUN chown -R sloptrap:sloptrap /home/sloptrap
|
|
USER sloptrap
|
|
|
|
WORKDIR /workspace
|
|
|
|
ENV SHELL=/bin/bash HOME=/home/sloptrap
|
|
ENTRYPOINT ["codex"]
|
|
EOF
|
|
}
|
|
|
|
populate_dockerfile() {
|
|
local destination=$1
|
|
mkdir -p "$(dirname "$destination")"
|
|
if [[ -n $SLOPTRAP_DOCKERFILE_SOURCE ]]; then
|
|
if [[ ! -f $SLOPTRAP_DOCKERFILE_SOURCE ]]; then
|
|
error "container recipe '$SLOPTRAP_DOCKERFILE_SOURCE' not found"
|
|
fi
|
|
cp "$SLOPTRAP_DOCKERFILE_SOURCE" "$destination"
|
|
else
|
|
write_embedded_dockerfile >"$destination"
|
|
fi
|
|
}
|
|
|
|
validate_basename() {
|
|
local name=$1
|
|
[[ $name =~ ^[A-Za-z0-9._+-]+$ ]] || error "invalid basename '$name'"
|
|
}
|
|
|
|
sanitize_engine_name() {
|
|
local name=$1
|
|
local lowered=${name,,}
|
|
if [[ $lowered != "$name" ]]; then
|
|
warn "normalizing name '$name' to '$lowered' for container engine compatibility"
|
|
fi
|
|
if [[ ! $lowered =~ ^[a-z0-9_.-]+$ ]]; then
|
|
error "engine name '$name' is invalid after normalization"
|
|
fi
|
|
printf '%s' "$lowered"
|
|
}
|
|
|
|
prepare_build_context() {
|
|
if [[ -n $SLOPTRAP_BUILD_CONTEXT && -d $SLOPTRAP_BUILD_CONTEXT ]]; then
|
|
return 0
|
|
fi
|
|
SLOPTRAP_BUILD_CONTEXT=$(create_temp_dir "context")
|
|
SLOPTRAP_DOCKERFILE_PATH="$SLOPTRAP_BUILD_CONTEXT/Dockerfile.sloptrap"
|
|
populate_dockerfile "$SLOPTRAP_DOCKERFILE_PATH"
|
|
validate_basename "$SLOPTRAP_CODEX_BIN_NAME"
|
|
CODEX_BIN_PATH="$SLOPTRAP_BUILD_CONTEXT/$SLOPTRAP_CODEX_BIN_NAME"
|
|
}
|
|
|
|
select_codex_home() {
|
|
local preferred="$HOME/.codex"
|
|
if [[ -L $preferred ]]; then
|
|
error "Codex home '$preferred' must not be a symlink"
|
|
fi
|
|
if [[ -e $preferred && ! -d $preferred ]]; then
|
|
error "expected Codex home '$preferred' to be a directory"
|
|
fi
|
|
|
|
CODEX_HOME_HOST="$preferred"
|
|
if [[ -d $CODEX_HOME_HOST ]]; then
|
|
CODEX_HOME_HOST="$(cd "$CODEX_HOME_HOST" && pwd -P)"
|
|
CODEX_HOME_BOOTSTRAP=false
|
|
else
|
|
CODEX_HOME_BOOTSTRAP=true
|
|
fi
|
|
}
|
|
|
|
assert_path_within_code_dir() {
|
|
local candidate=$1
|
|
local resolved
|
|
resolved=$(resolve_path_strict "$candidate")
|
|
if [[ $resolved != "$CODE_DIR" && $resolved != "$CODE_DIR/"* ]]; then
|
|
error "path '$candidate' escapes project root '$CODE_DIR'"
|
|
fi
|
|
}
|
|
|
|
ensure_ignore_helper_root() {
|
|
local helper_root="$CODE_DIR/.sloptrap-ignores"
|
|
if [[ -L $helper_root ]]; then
|
|
error "$helper_root: helper directory may not be a symlink"
|
|
fi
|
|
if [[ -e $helper_root && ! -d $helper_root ]]; then
|
|
error "$helper_root: expected a directory"
|
|
fi
|
|
assert_path_within_code_dir "$helper_root"
|
|
IGNORE_HELPER_ROOT="$helper_root"
|
|
}
|
|
|
|
sanitize_ignore_rel() {
|
|
local rel=$1
|
|
local original=$rel
|
|
local source=${CURRENT_IGNORE_FILE:-$MANIFEST_PATH}
|
|
while [[ $rel == ./* ]]; do
|
|
rel=${rel:2}
|
|
done
|
|
if [[ -z $rel || $rel == "." ]]; then
|
|
error "$source: .sloptrapignore entry '$original' may not target the project root"
|
|
fi
|
|
if [[ ${rel:0:1} == "/" ]]; then
|
|
error "$source: .sloptrapignore entry '$original' must be relative"
|
|
fi
|
|
local IFS='/'
|
|
local -a segments=()
|
|
read -r -a segments <<< "$rel"
|
|
if [[ ${#segments[@]} -eq 0 ]]; then
|
|
error "$source: .sloptrapignore entry '$original' is invalid"
|
|
fi
|
|
local segment
|
|
for segment in "${segments[@]}"; do
|
|
if [[ -z $segment || $segment == "." || $segment == ".." ]]; then
|
|
error "$source: .sloptrapignore entry '$original' uses disallowed path components"
|
|
fi
|
|
done
|
|
if [[ ${rel//$'\n'/} != "$rel" || ${rel//$'\r'/} != "$rel" ]]; then
|
|
error "$source: .sloptrapignore entry '$original' contains control characters"
|
|
fi
|
|
local resolved
|
|
resolved=$(resolve_path_strict "$CODE_DIR/$rel")
|
|
if [[ $resolved != "$CODE_DIR" && $resolved != "$CODE_DIR/"* ]]; then
|
|
error "$source: .sloptrapignore entry '$original' resolves outside the project root"
|
|
fi
|
|
printf '%s' "$rel"
|
|
}
|
|
|
|
resolve_sloptrap_ignore() {
|
|
local root=$1
|
|
local ignore_file="$root/.sloptrapignore"
|
|
SLOPTRAP_IGNORE_ENTRIES=()
|
|
[[ -f $ignore_file ]] || return 0
|
|
|
|
CURRENT_IGNORE_FILE="$ignore_file"
|
|
local -a patterns=()
|
|
local line trimmed
|
|
while IFS= read -r line || [[ -n $line ]]; do
|
|
trimmed="$(trim "$line")"
|
|
[[ -z $trimmed ]] && continue
|
|
[[ ${trimmed:0:1} == "#" ]] && continue
|
|
patterns+=("$trimmed")
|
|
done <"$ignore_file"
|
|
[[ ${#patterns[@]} -gt 0 ]] || return 0
|
|
|
|
local -A selected=()
|
|
local pattern negate anchored dir_only raw had_match
|
|
local -a matches
|
|
local match rel
|
|
local prev_gs prev_ng prev_dg
|
|
prev_gs=$(shopt -p globstar 2>/dev/null || true)
|
|
prev_ng=$(shopt -p nullglob 2>/dev/null || true)
|
|
prev_dg=$(shopt -p dotglob 2>/dev/null || true)
|
|
shopt -s globstar nullglob dotglob
|
|
for pattern in "${patterns[@]}"; do
|
|
negate=false
|
|
anchored=false
|
|
dir_only=false
|
|
raw="$pattern"
|
|
had_match=false
|
|
if [[ ${raw:0:1} == "!" ]]; then
|
|
negate=true
|
|
raw=${raw:1}
|
|
fi
|
|
if [[ ${raw:0:1} == "/" ]]; then
|
|
anchored=true
|
|
raw=${raw:1}
|
|
fi
|
|
if [[ ${raw: -1} == "/" ]]; then
|
|
dir_only=true
|
|
raw=${raw%/}
|
|
fi
|
|
[[ -n $raw ]] || raw="."
|
|
matches=()
|
|
if $anchored; then
|
|
while IFS= read -r match; do
|
|
matches+=("$root/$match")
|
|
done < <(
|
|
cd "$root" && { compgen -G "$raw" || true; }
|
|
)
|
|
else
|
|
while IFS= read -r match; do
|
|
matches+=("$root/$match")
|
|
done < <(
|
|
cd "$root" && { compgen -G "$raw" || true; }
|
|
)
|
|
while IFS= read -r match; do
|
|
matches+=("$root/$match")
|
|
done < <(
|
|
cd "$root" && { compgen -G "**/$raw" || true; }
|
|
)
|
|
fi
|
|
local -A seen=()
|
|
for match in "${matches[@]}"; do
|
|
[[ -e $match ]] || continue
|
|
if [[ $match != "$root" && $match != $root/* ]]; then
|
|
continue
|
|
fi
|
|
rel=${match#"$root"/}
|
|
[[ -n $rel ]] || rel="."
|
|
if $dir_only && [[ ! -d $match ]]; then
|
|
continue
|
|
fi
|
|
if [[ -n ${seen[$rel]-} ]]; then
|
|
continue
|
|
fi
|
|
seen[$rel]=1
|
|
rel=$(sanitize_ignore_rel "$rel")
|
|
had_match=true
|
|
if $negate; then
|
|
unset 'selected[$rel]'
|
|
else
|
|
if [[ -d $match ]]; then
|
|
selected[$rel]="dir"
|
|
else
|
|
selected[$rel]="file"
|
|
fi
|
|
fi
|
|
done
|
|
if ! $had_match && ! $negate; then
|
|
warn "${CURRENT_IGNORE_FILE:-$MANIFEST_PATH}: .sloptrapignore entry '$pattern' matched no files"
|
|
fi
|
|
done
|
|
eval "$prev_gs"
|
|
eval "$prev_ng"
|
|
eval "$prev_dg"
|
|
|
|
if [[ ${#selected[@]} -eq 0 ]]; then
|
|
CURRENT_IGNORE_FILE=""
|
|
return 0
|
|
fi
|
|
SLOPTRAP_IGNORE_ENTRIES=()
|
|
for rel in "${!selected[@]}"; do
|
|
SLOPTRAP_IGNORE_ENTRIES+=("${selected[$rel]}"$'\t'"$rel")
|
|
done
|
|
mapfile -t SLOPTRAP_IGNORE_ENTRIES < <(printf '%s\n' "${SLOPTRAP_IGNORE_ENTRIES[@]}" | sort)
|
|
CURRENT_IGNORE_FILE=""
|
|
}
|
|
|
|
escape_mount_value() {
|
|
local value=$1
|
|
value=${value//\\/\\\\}
|
|
value=${value//,/\\,}
|
|
value=${value//=/\\=}
|
|
printf '%s' "$value"
|
|
}
|
|
|
|
prepare_ignore_mounts() {
|
|
IGNORE_MOUNT_ARGS=()
|
|
[[ ${#SLOPTRAP_IGNORE_ENTRIES[@]} -gt 0 ]] || return 0
|
|
local container_root="${SLOPTRAP_WORKDIR:-/workspace}"
|
|
if [[ $container_root != "/" ]]; then
|
|
container_root="${container_root%/}"
|
|
[[ -n $container_root ]] || container_root="/"
|
|
fi
|
|
local entry type rel target host_path stub_base target_mount host_path_mount
|
|
stub_base="$IGNORE_STUB_BASE"
|
|
assert_path_within_code_dir "$stub_base"
|
|
rm -rf "$stub_base" 2>/dev/null || true
|
|
if ! mkdir -p "$stub_base/files" 2>/dev/null; then
|
|
stub_base="$CODE_DIR/.sloptrap-ignores/fallback"
|
|
assert_path_within_code_dir "$stub_base"
|
|
rm -rf "$stub_base" 2>/dev/null || true
|
|
mkdir -p "$stub_base/files"
|
|
fi
|
|
IGNORE_STUB_BASE="$stub_base"
|
|
for entry in "${SLOPTRAP_IGNORE_ENTRIES[@]}"; do
|
|
type=${entry%%$'\t'*}
|
|
rel=${entry#*$'\t'}
|
|
if [[ $container_root == "/" ]]; then
|
|
target="/$rel"
|
|
else
|
|
target="$container_root/$rel"
|
|
fi
|
|
target_mount=$(escape_mount_value "$target")
|
|
case "$type" in
|
|
dir)
|
|
IGNORE_MOUNT_ARGS+=("--mount" "type=tmpfs,target=$target_mount")
|
|
;;
|
|
file)
|
|
host_path="$stub_base/files/$rel"
|
|
assert_path_within_code_dir "$host_path"
|
|
mkdir -p "$(dirname "$host_path")"
|
|
: > "$host_path"
|
|
host_path_mount=$(escape_mount_value "$host_path")
|
|
IGNORE_MOUNT_ARGS+=("--mount" "type=bind,source=$host_path_mount,target=$target_mount,readonly")
|
|
;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
ensure_safe_for_make() {
|
|
local key=$1
|
|
local value=$2
|
|
if [[ $value == *'$'* || $value == *'`'* ]]; then
|
|
error "$MANIFEST_PATH: value for '$key' must not contain \$ or \` characters"
|
|
fi
|
|
if [[ $value == *$'\n'* ]]; then
|
|
error "$MANIFEST_PATH: value for '$key' must not span multiple lines"
|
|
fi
|
|
}
|
|
|
|
validate_package_list() {
|
|
local key=$1
|
|
local raw=$2
|
|
local source=${3:-$MANIFEST_PATH}
|
|
[[ -z $raw ]] && return 0
|
|
local token
|
|
for token in $raw; do
|
|
if [[ ! $token =~ ^[A-Za-z0-9+.-]+$ ]]; then
|
|
error "$source: invalid package name '$token' in '$key'"
|
|
fi
|
|
done
|
|
}
|
|
|
|
detect_container_engine() {
|
|
local override=${SLOPTRAP_CONTAINER_ENGINE-}
|
|
if [[ -n $override ]]; then
|
|
local engine="${override,,}"
|
|
if ! command -v "$engine" >/dev/null 2>&1; then
|
|
error "container engine '$engine' not found in PATH"
|
|
fi
|
|
printf '%s' "$engine"
|
|
return 0
|
|
fi
|
|
if command -v podman >/dev/null 2>&1; then
|
|
printf 'podman'
|
|
return 0
|
|
fi
|
|
if command -v docker >/dev/null 2>&1; then
|
|
printf 'docker'
|
|
return 0
|
|
fi
|
|
error "container engine not found in PATH; install podman (preferred) or docker, or set SLOPTRAP_CONTAINER_ENGINE explicitly"
|
|
}
|
|
|
|
parse_manifest() {
|
|
local manifest_path=$1
|
|
local line key value
|
|
while IFS= read -r line || [[ -n $line ]]; do
|
|
line="$(trim "$line")"
|
|
[[ -z $line ]] && continue
|
|
[[ ${line:0:1} == "#" ]] && continue
|
|
if [[ $line != *"="* ]]; then
|
|
error "$manifest_path: expected KEY=VALUE entry (got '$line')"
|
|
fi
|
|
key="$(trim "${line%%=*}")"
|
|
value="$(trim "${line#*=}")"
|
|
if [[ -z $key ]]; then
|
|
error "$manifest_path: blank key in line '$line'"
|
|
fi
|
|
if [[ ( ${value:0:1} == '"' && ${value: -1} == '"' ) || ( ${value:0:1} == "'" && ${value: -1} == "'" ) ]]; then
|
|
value="${value:1:-1}"
|
|
fi
|
|
if [[ $key == make.* ]]; then
|
|
error "$manifest_path: make.* overrides are no longer supported; use packages_extra instead"
|
|
fi
|
|
MANIFEST["$key"]=$value
|
|
done < "$manifest_path"
|
|
}
|
|
|
|
print_config() {
|
|
local manifest_path=$1
|
|
info_line "manifest_path=%s\n" "$manifest_path"
|
|
info_line "manifest_present=%s\n" "$MANIFEST_PRESENT"
|
|
info_line "project_name=%s\n" "$PROJECT_NAME"
|
|
info_line "project_dir=%s\n" "$CODE_DIR"
|
|
info_line "resolved_targets=%s\n" "${DEFAULT_TARGETS[*]}"
|
|
info_line "container_engine=%s\n" "$CONTAINER_ENGINE"
|
|
info_line "image_name=%s\n" "$SLOPTRAP_IMAGE_NAME"
|
|
info_line "container_name=%s\n" "$SLOPTRAP_CONTAINER_NAME"
|
|
info_line "codex_home=%s\n" "$CODEX_HOME_HOST"
|
|
info_line "codex_home_bootstrap=%s\n" "$CODEX_HOME_BOOTSTRAP"
|
|
info_line "codex_archive=%s\n" "$SLOPTRAP_CODEX_ARCHIVE"
|
|
info_line "codex_url=%s\n" "$SLOPTRAP_CODEX_URL"
|
|
info_line "needs_login=%s\n" "$NEED_LOGIN"
|
|
info_line "codex_args=%s\n" "$CODEX_ARGS_DISPLAY"
|
|
info_line "ignore_stub_base=%s\n" "$IGNORE_STUB_BASE"
|
|
if [[ ${#SLOPTRAP_IGNORE_ENTRIES[@]} -gt 0 ]]; then
|
|
local ignore_paths
|
|
ignore_paths=$(printf '%s ' "${SLOPTRAP_IGNORE_ENTRIES[@]}")
|
|
ignore_paths=${ignore_paths% }
|
|
info_line "ignore_paths=%s\n" "$ignore_paths"
|
|
else
|
|
info_line "ignore_paths=\n"
|
|
fi
|
|
if [[ -n $SLOPTRAP_DOCKERFILE_SOURCE ]]; then
|
|
info_line "dockerfile_source=%s\n" "$SLOPTRAP_DOCKERFILE_SOURCE"
|
|
else
|
|
info_line "dockerfile_source=embedded\n"
|
|
fi
|
|
for key in "${!MANIFEST[@]}"; do
|
|
info_line "%s=%s\n" "$key" "${MANIFEST[$key]}"
|
|
done
|
|
}
|
|
|
|
declare -a CONTAINER_SHARED_OPTS=()
|
|
declare -a BASE_CONTAINER_CMD=()
|
|
SLOPTRAP_IMAGE_NAME=""
|
|
SLOPTRAP_CONTAINER_NAME=""
|
|
SLOPTRAP_DOCKERFILE_PATH=""
|
|
SLOPTRAP_BUILD_CONTEXT=""
|
|
SLOPTRAP_DOCKERFILE_SOURCE=""
|
|
CODEX_BIN_PATH=""
|
|
SLOPTRAP_SHARED_DIR_ABS=""
|
|
SLOPTRAP_PACKAGES_BASE=""
|
|
SLOPTRAP_PACKAGES_EXTRA_RESOLVED=""
|
|
SLOPTRAP_CODEX_BIN_NAME=""
|
|
SLOPTRAP_CODEX_URL=""
|
|
SLOPTRAP_CODEX_ARCHIVE=""
|
|
SLOPTRAP_CODEX_HOME_CONT=""
|
|
SLOPTRAP_SECURITY_OPTS_EXTRA=""
|
|
SLOPTRAP_VOLUME_LABEL=""
|
|
SLOPTRAP_WORKDIR=${SLOPTRAP_WORKDIR-}
|
|
SLOPTRAP_NETWORK_NAME=""
|
|
SLOPTRAP_LIMITS_PID=""
|
|
SLOPTRAP_LIMITS_RAM=""
|
|
SLOPTRAP_LIMITS_SWP=""
|
|
SLOPTRAP_LIMITS_SHM=""
|
|
SLOPTRAP_LIMITS_CPU=""
|
|
SLOPTRAP_TMPFS_PATHS=""
|
|
SLOPTRAP_ROOTFS_READONLY=""
|
|
|
|
get_env_default() {
|
|
local var=$1
|
|
local default=$2
|
|
local value
|
|
if value=$(printenv "$var" 2>/dev/null); then
|
|
printf '%s' "$value"
|
|
else
|
|
printf '%s' "$default"
|
|
fi
|
|
}
|
|
|
|
validate_codex_archive_name() {
|
|
local name=$1
|
|
[[ $name =~ ^codex-[A-Za-z0-9_.-]+$ ]] || error "invalid Codex archive name '$name'"
|
|
}
|
|
|
|
detect_codex_archive_name() {
|
|
local os arch codex_os codex_arch
|
|
os=$(uname -s 2>/dev/null || true)
|
|
arch=$(uname -m 2>/dev/null || true)
|
|
[[ -n $os ]] || error "failed to detect host OS for Codex download"
|
|
[[ -n $arch ]] || error "failed to detect host architecture for Codex download"
|
|
case "$os" in
|
|
Linux|Darwin) codex_os="unknown-linux-gnu" ;; # Codex runs inside a Debian-based image
|
|
*) error "unsupported host OS '$os' for Codex download" ;;
|
|
esac
|
|
case "$arch" in
|
|
x86_64|amd64) codex_arch="x86_64" ;;
|
|
arm64|aarch64) codex_arch="aarch64" ;;
|
|
*) error "unsupported host architecture '$arch' for Codex download" ;;
|
|
esac
|
|
printf 'codex-%s-%s' "$codex_arch" "$codex_os"
|
|
}
|
|
|
|
resolve_container_workdir() {
|
|
if [[ -z ${SLOPTRAP_WORKDIR:-} ]]; then
|
|
SLOPTRAP_WORKDIR=$(get_env_default "SLOPTRAP_WORKDIR" "/workspace")
|
|
fi
|
|
[[ -n $SLOPTRAP_WORKDIR ]] || SLOPTRAP_WORKDIR="/workspace"
|
|
if [[ $SLOPTRAP_WORKDIR == "/" ]]; then
|
|
error "SLOPTRAP_WORKDIR may not be '/'; use a subdirectory like /workspace"
|
|
fi
|
|
}
|
|
|
|
print_command() {
|
|
local rendered=""
|
|
if [[ $# -gt 0 ]]; then
|
|
rendered=$(printf '%q ' "$@")
|
|
rendered=${rendered% }
|
|
fi
|
|
comment_line "%s\n" "$rendered"
|
|
}
|
|
|
|
run_or_print() {
|
|
if $DRY_RUN; then
|
|
print_command "$@"
|
|
return 0
|
|
fi
|
|
"$@"
|
|
}
|
|
|
|
ensure_codex_home_dir() {
|
|
if [[ -d $CODEX_HOME_HOST ]]; then
|
|
return 0
|
|
fi
|
|
if $DRY_RUN; then
|
|
print_command mkdir -p "$CODEX_HOME_HOST"
|
|
return 0
|
|
fi
|
|
mkdir -p "$CODEX_HOME_HOST"
|
|
}
|
|
|
|
fetch_latest_codex_digest() {
|
|
local api_url="https://api.github.com/repos/openai/codex/releases/latest"
|
|
local target_asset="${SLOPTRAP_CODEX_ARCHIVE}.tar.gz"
|
|
[[ -n $SLOPTRAP_CODEX_ARCHIVE ]] || error "Codex archive name is not set"
|
|
if ! command -v jq >/dev/null 2>&1; then
|
|
error "jq is required to verify the Codex binary digest"
|
|
fi
|
|
local response
|
|
if ! response=$(curl -fsSL "$api_url"); then
|
|
error "failed to download Codex release metadata from GitHub"
|
|
fi
|
|
local digest_line
|
|
digest_line=$(jq -r --arg name "$target_asset" '.assets[] | select(.name == $name) | .digest' <<<"$response" | head -n 1)
|
|
if [[ -z $digest_line || $digest_line == "null" ]]; then
|
|
error "failed to resolve Codex digest from GitHub response"
|
|
fi
|
|
digest_line=${digest_line#sha256:}
|
|
printf '%s' "$digest_line"
|
|
}
|
|
|
|
ensure_codex_binary() {
|
|
prepare_build_context
|
|
if [[ -x $CODEX_BIN_PATH ]]; then
|
|
return 0
|
|
fi
|
|
local tar_transform="s/${SLOPTRAP_CODEX_ARCHIVE}/${SLOPTRAP_CODEX_BIN_NAME}/"
|
|
local download_dir
|
|
download_dir=$(create_temp_dir "codex")
|
|
local tmp_archive="$download_dir/codex.tar.gz"
|
|
if $DRY_RUN; then
|
|
print_command curl -Lso "$tmp_archive" "$SLOPTRAP_CODEX_URL"
|
|
print_command sha256sum -c -
|
|
print_command tar -xzf "$tmp_archive" --transform="$tar_transform" -C "$SLOPTRAP_BUILD_CONTEXT"
|
|
print_command chmod 0755 "$CODEX_BIN_PATH"
|
|
return 0
|
|
fi
|
|
if ! curl -Lso "$tmp_archive" "$SLOPTRAP_CODEX_URL"; then
|
|
rm -rf "$download_dir"
|
|
error "failed to download Codex binary from '$SLOPTRAP_CODEX_URL'"
|
|
fi
|
|
local expected_digest
|
|
expected_digest=$(fetch_latest_codex_digest)
|
|
if ! printf "%s %s\n" "$expected_digest" "$tmp_archive" | sha256sum -c - >/dev/null 2>&1; then
|
|
rm -rf "$download_dir" "$CODEX_BIN_PATH"
|
|
error "checksum verification failed for $SLOPTRAP_CODEX_BIN_NAME"
|
|
fi
|
|
if ! tar -xzf "$tmp_archive" --transform="$tar_transform" -C "$SLOPTRAP_BUILD_CONTEXT"; then
|
|
rm -rf "$download_dir"
|
|
error "failed to extract Codex binary"
|
|
fi
|
|
rm -rf "$download_dir"
|
|
chmod 0755 "$CODEX_BIN_PATH"
|
|
}
|
|
|
|
ensure_safe_sandbox() {
|
|
local -a args=("$@")
|
|
local sandbox_mode=""
|
|
local i=0
|
|
while [[ $i -lt ${#args[@]} ]]; do
|
|
if [[ ${args[$i]} == "--sandbox" ]]; then
|
|
if (( i + 1 >= ${#args[@]} )); then
|
|
error "$MANIFEST_PATH: '--sandbox' flag requires a mode (workspace-write or workspace-read-only)"
|
|
fi
|
|
sandbox_mode="${args[$((i + 1))]}"
|
|
fi
|
|
((i+=1))
|
|
done
|
|
if [[ -z $sandbox_mode ]]; then
|
|
error "$MANIFEST_PATH: codex_args must include '--sandbox <mode>' (workspace-write or workspace-read-only)"
|
|
fi
|
|
case "$sandbox_mode" in
|
|
workspace-write|workspace-read-only)
|
|
;;
|
|
*)
|
|
error "$MANIFEST_PATH: sandbox mode '$sandbox_mode' is not allowed (expected workspace-write or workspace-read-only)"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
normalize_package_list() {
|
|
local raw=$1
|
|
raw="$(trim "$raw")"
|
|
[[ -z $raw ]] && return 0
|
|
local -a tokens=()
|
|
read -r -a tokens <<< "$raw"
|
|
printf '%s' "${tokens[*]}"
|
|
}
|
|
|
|
prepare_container_runtime() {
|
|
resolve_container_workdir
|
|
SLOPTRAP_PACKAGES_EXTRA_RESOLVED=$PACKAGES_EXTRA
|
|
SLOPTRAP_SHARED_DIR_ABS="$CODE_DIR"
|
|
if [[ ! -d $SLOPTRAP_SHARED_DIR_ABS ]]; then
|
|
error "shared directory '$SLOPTRAP_SHARED_DIR_ABS' does not exist"
|
|
fi
|
|
SLOPTRAP_SHARED_DIR_ABS="$(cd "$SLOPTRAP_SHARED_DIR_ABS" && pwd -P)"
|
|
|
|
local dockerfile_override
|
|
dockerfile_override=$(get_env_default "SLOPTRAP_DOCKERFILE_PATH" "")
|
|
if [[ -n $dockerfile_override ]]; then
|
|
if [[ $dockerfile_override != /* ]]; then
|
|
dockerfile_override="$SCRIPT_DIR/$dockerfile_override"
|
|
fi
|
|
if [[ ! -f $dockerfile_override ]]; then
|
|
error "container recipe '$dockerfile_override' not found"
|
|
fi
|
|
SLOPTRAP_DOCKERFILE_SOURCE="$dockerfile_override"
|
|
elif [[ -f "$SCRIPT_DIR/Dockerfile.sloptrap" ]]; then
|
|
SLOPTRAP_DOCKERFILE_SOURCE="$SCRIPT_DIR/Dockerfile.sloptrap"
|
|
else
|
|
SLOPTRAP_DOCKERFILE_SOURCE=""
|
|
fi
|
|
|
|
SLOPTRAP_PACKAGES_BASE=$(get_env_default "SLOPTRAP_PACKAGES" "curl bash ca-certificates libstdc++6 git ripgrep xxd file procps")
|
|
validate_package_list "SLOPTRAP_PACKAGES" "$SLOPTRAP_PACKAGES_BASE" "SLOPTRAP_PACKAGES"
|
|
local default_codex_archive
|
|
default_codex_archive=$(detect_codex_archive_name)
|
|
local env_codex_archive
|
|
env_codex_archive=$(printenv "SLOPTRAP_CODEX_ARCHIVE" 2>/dev/null || true)
|
|
if [[ -n $env_codex_archive ]]; then
|
|
SLOPTRAP_CODEX_ARCHIVE=$env_codex_archive
|
|
else
|
|
SLOPTRAP_CODEX_ARCHIVE=$default_codex_archive
|
|
fi
|
|
validate_codex_archive_name "$SLOPTRAP_CODEX_ARCHIVE"
|
|
local default_codex_url="https://github.com/openai/codex/releases/latest/download/${SLOPTRAP_CODEX_ARCHIVE}.tar.gz"
|
|
SLOPTRAP_CODEX_URL=$(get_env_default "SLOPTRAP_CODEX_URL" "$default_codex_url")
|
|
if [[ -z $env_codex_archive ]]; then
|
|
local inferred_archive
|
|
inferred_archive=$(basename "${SLOPTRAP_CODEX_URL%%\?*}")
|
|
inferred_archive=${inferred_archive%.tar.gz}
|
|
if [[ $inferred_archive == codex-* ]]; then
|
|
validate_codex_archive_name "$inferred_archive"
|
|
SLOPTRAP_CODEX_ARCHIVE=$inferred_archive
|
|
fi
|
|
fi
|
|
SLOPTRAP_CODEX_BIN_NAME=$(get_env_default "SLOPTRAP_CODEX_BIN" "codex")
|
|
SLOPTRAP_CODEX_HOME_CONT=$(get_env_default "SLOPTRAP_CODEX_HOME_CONT" "/codex")
|
|
SLOPTRAP_CODEX_UID=$(get_env_default "SLOPTRAP_CODEX_UID" "1337")
|
|
SLOPTRAP_CODEX_GID=$(get_env_default "SLOPTRAP_CODEX_GID" "1337")
|
|
SLOPTRAP_SECURITY_OPTS_EXTRA=$(get_env_default "SLOPTRAP_SECURITY_OPTS_EXTRA" "")
|
|
local default_network="bridge"
|
|
if [[ $CONTAINER_ENGINE == "podman" ]]; then
|
|
if command -v slirp4netns >/dev/null 2>&1; then
|
|
default_network="slirp4netns"
|
|
else
|
|
warn "podman detected but 'slirp4netns' is missing; falling back to 'bridge' networking"
|
|
fi
|
|
fi
|
|
SLOPTRAP_NETWORK_NAME=$(get_env_default "SLOPTRAP_NETWORK_NAME" "$default_network")
|
|
if $ALLOW_HOST_NETWORK; then
|
|
SLOPTRAP_NETWORK_NAME="host"
|
|
elif [[ $SLOPTRAP_NETWORK_NAME == "host" ]]; then
|
|
error "$MANIFEST_PATH: host networking requires allow_host_network=true"
|
|
fi
|
|
SLOPTRAP_LIMITS_PID=$(get_env_default "SLOPTRAP_LIMITS_PID" "1024")
|
|
SLOPTRAP_LIMITS_RAM=$(get_env_default "SLOPTRAP_LIMITS_RAM" "1024m")
|
|
SLOPTRAP_LIMITS_SWP=$(get_env_default "SLOPTRAP_LIMITS_SWP" "1024m")
|
|
SLOPTRAP_LIMITS_SHM=$(get_env_default "SLOPTRAP_LIMITS_SHM" "1024m")
|
|
SLOPTRAP_LIMITS_CPU=$(get_env_default "SLOPTRAP_LIMITS_CPU" "8")
|
|
SLOPTRAP_TMPFS_PATHS=$(get_env_default "SLOPTRAP_TMPFS_PATHS" "/tmp:exec /run /run/lock")
|
|
SLOPTRAP_ROOTFS_READONLY=$(get_env_default "SLOPTRAP_ROOTFS_READONLY" "1")
|
|
SLOPTRAP_IMAGE_NAME=$(get_env_default "SLOPTRAP_IMAGE_NAME" "${PROJECT_NAME}-sloptrap-image")
|
|
SLOPTRAP_CONTAINER_NAME=$(get_env_default "SLOPTRAP_CONTAINER_NAME" "${PROJECT_NAME}-sloptrap-container")
|
|
SLOPTRAP_IMAGE_NAME=$(sanitize_engine_name "$SLOPTRAP_IMAGE_NAME")
|
|
SLOPTRAP_CONTAINER_NAME=$(sanitize_engine_name "$SLOPTRAP_CONTAINER_NAME")
|
|
|
|
local -a network_opts=(--network "$SLOPTRAP_NETWORK_NAME" --init)
|
|
local -a security_opts=(--cap-drop=ALL --security-opt no-new-privileges)
|
|
if [[ -n $SLOPTRAP_SECURITY_OPTS_EXTRA ]]; then
|
|
local -a extra_opts=()
|
|
read -r -a extra_opts <<< "$SLOPTRAP_SECURITY_OPTS_EXTRA"
|
|
security_opts+=("${extra_opts[@]}")
|
|
fi
|
|
local -a resource_opts=(
|
|
--pids-limit "$SLOPTRAP_LIMITS_PID"
|
|
--memory "$SLOPTRAP_LIMITS_RAM"
|
|
--memory-swap "$SLOPTRAP_LIMITS_SWP"
|
|
--shm-size "$SLOPTRAP_LIMITS_SHM"
|
|
--cpus "$SLOPTRAP_LIMITS_CPU"
|
|
)
|
|
local -a tmpfs_opts=()
|
|
if [[ -n $SLOPTRAP_TMPFS_PATHS ]]; then
|
|
local -a tmpfs_paths=()
|
|
read -r -a tmpfs_paths <<< "$SLOPTRAP_TMPFS_PATHS"
|
|
local path
|
|
for path in "${tmpfs_paths[@]}"; do
|
|
tmpfs_opts+=(--tmpfs "$path")
|
|
done
|
|
fi
|
|
|
|
local rootfs_flag=()
|
|
case "${SLOPTRAP_ROOTFS_READONLY,,}" in
|
|
1|true|yes)
|
|
rootfs_flag=(--read-only)
|
|
;;
|
|
esac
|
|
|
|
if [[ $CONTAINER_ENGINE == "podman" ]]; then
|
|
SLOPTRAP_VOLUME_LABEL=":Z"
|
|
else
|
|
SLOPTRAP_VOLUME_LABEL=""
|
|
fi
|
|
|
|
local -a volume_opts=(
|
|
-v "$SLOPTRAP_SHARED_DIR_ABS:$SLOPTRAP_WORKDIR$SLOPTRAP_VOLUME_LABEL"
|
|
-v "$CODEX_HOME_HOST:$SLOPTRAP_CODEX_HOME_CONT$SLOPTRAP_VOLUME_LABEL"
|
|
)
|
|
|
|
local -a env_args=(
|
|
-e "HOME=$SLOPTRAP_CODEX_HOME_CONT"
|
|
-e "XDG_CONFIG_HOME=$SLOPTRAP_CODEX_HOME_CONT/config"
|
|
-e "XDG_CACHE_HOME=$SLOPTRAP_CODEX_HOME_CONT/cache"
|
|
-e "XDG_STATE_HOME=$SLOPTRAP_CODEX_HOME_CONT/state"
|
|
-e "CODEX_HOME=$SLOPTRAP_CODEX_HOME_CONT"
|
|
)
|
|
|
|
local uid gid
|
|
uid=$(id -u)
|
|
gid=$(id -g)
|
|
local -a user_opts=("--user" "$uid:$gid")
|
|
if [[ $CONTAINER_ENGINE == "podman" ]]; then
|
|
user_opts=(--userns="keep-id:uid=$uid,gid=$gid" "${user_opts[@]}")
|
|
fi
|
|
|
|
CONTAINER_SHARED_OPTS=(
|
|
"${network_opts[@]}"
|
|
"${security_opts[@]}"
|
|
"${resource_opts[@]}"
|
|
"${rootfs_flag[@]}"
|
|
"${tmpfs_opts[@]}"
|
|
"${volume_opts[@]}"
|
|
"${env_args[@]}"
|
|
"${IGNORE_MOUNT_ARGS[@]}"
|
|
"${user_opts[@]}"
|
|
-w "$SLOPTRAP_WORKDIR"
|
|
)
|
|
|
|
BASE_CONTAINER_CMD=(
|
|
"$CONTAINER_ENGINE" run --rm -it
|
|
--name "$SLOPTRAP_CONTAINER_NAME"
|
|
"${CONTAINER_SHARED_OPTS[@]}"
|
|
)
|
|
}
|
|
|
|
build_image() {
|
|
ensure_codex_binary
|
|
print_banner
|
|
local extra_packages_arg
|
|
extra_packages_arg=$(normalize_package_list "$SLOPTRAP_PACKAGES_EXTRA_RESOLVED")
|
|
if ! $DRY_RUN; then
|
|
status_line "Building %s\n" "$SLOPTRAP_IMAGE_NAME"
|
|
fi
|
|
local -a cmd=(
|
|
"$CONTAINER_ENGINE" build --quiet
|
|
-t "$SLOPTRAP_IMAGE_NAME"
|
|
-f "$SLOPTRAP_DOCKERFILE_PATH"
|
|
--label "$SLOPTRAP_IMAGE_LABEL"
|
|
--build-arg "BASE_PACKAGES=$SLOPTRAP_PACKAGES_BASE"
|
|
--build-arg "CODEX_BIN=$SLOPTRAP_CODEX_BIN_NAME"
|
|
--build-arg "CODEX_UID=$SLOPTRAP_CODEX_UID"
|
|
--build-arg "CODEX_GID=$SLOPTRAP_CODEX_GID"
|
|
)
|
|
if [[ -n $extra_packages_arg ]]; then
|
|
cmd+=(--build-arg "EXTRA_PACKAGES=$extra_packages_arg")
|
|
fi
|
|
cmd+=("$SLOPTRAP_BUILD_CONTEXT")
|
|
if $DRY_RUN; then
|
|
run_or_print "${cmd[@]}"
|
|
return
|
|
fi
|
|
|
|
local build_output
|
|
if ! build_output=$("${cmd[@]}"); then
|
|
return 1
|
|
fi
|
|
build_output=$(trim "$build_output")
|
|
if [[ -n $build_output ]]; then
|
|
comment_line "Image %s\n" "$build_output"
|
|
fi
|
|
}
|
|
|
|
rebuild_image() {
|
|
ensure_codex_binary
|
|
if ! $DRY_RUN; then
|
|
status_line "Rebuilding %s (no cache)\n" "$SLOPTRAP_IMAGE_NAME"
|
|
fi
|
|
local extra_packages_arg
|
|
extra_packages_arg=$(normalize_package_list "$SLOPTRAP_PACKAGES_EXTRA_RESOLVED")
|
|
local -a cmd=(
|
|
"$CONTAINER_ENGINE" build --no-cache
|
|
-t "$SLOPTRAP_IMAGE_NAME"
|
|
-f "$SLOPTRAP_DOCKERFILE_PATH"
|
|
--label "$SLOPTRAP_IMAGE_LABEL"
|
|
--build-arg "BASE_PACKAGES=$SLOPTRAP_PACKAGES_BASE"
|
|
--build-arg "CODEX_BIN=$SLOPTRAP_CODEX_BIN_NAME"
|
|
--build-arg "CODEX_UID=$SLOPTRAP_CODEX_UID"
|
|
--build-arg "CODEX_GID=$SLOPTRAP_CODEX_GID"
|
|
)
|
|
if [[ -n $extra_packages_arg ]]; then
|
|
cmd+=(--build-arg "EXTRA_PACKAGES=$extra_packages_arg")
|
|
fi
|
|
cmd+=("$SLOPTRAP_BUILD_CONTEXT")
|
|
if $DRY_RUN; then
|
|
run_or_print "${cmd[@]}"
|
|
return
|
|
fi
|
|
|
|
local build_output
|
|
if ! build_output=$("${cmd[@]}"); then
|
|
return 1
|
|
fi
|
|
build_output=$(trim "$build_output")
|
|
if [[ -n $build_output ]]; then
|
|
comment_line "Image %s\n" "$build_output"
|
|
fi
|
|
}
|
|
|
|
build_if_missing() {
|
|
if $DRY_RUN; then
|
|
print_command "$CONTAINER_ENGINE" image inspect "$SLOPTRAP_IMAGE_NAME"
|
|
return 0
|
|
fi
|
|
if "$CONTAINER_ENGINE" image inspect "$SLOPTRAP_IMAGE_NAME" >/dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
build_image
|
|
}
|
|
|
|
stop_container() {
|
|
if $DRY_RUN; then
|
|
print_command "$CONTAINER_ENGINE" stop "$SLOPTRAP_CONTAINER_NAME"
|
|
return 0
|
|
fi
|
|
"$CONTAINER_ENGINE" stop "$SLOPTRAP_CONTAINER_NAME" >/dev/null 2>&1 || true
|
|
}
|
|
|
|
clean_environment() {
|
|
if ! $DRY_RUN; then
|
|
status_line "Cleaning %s\n" "$PROJECT_NAME"
|
|
fi
|
|
local helper_root="$CODE_DIR/.sloptrap-ignores"
|
|
if [[ -L $helper_root ]]; then
|
|
error "$helper_root: helper directory may not be a symlink"
|
|
fi
|
|
assert_path_within_code_dir "$helper_root"
|
|
stop_container
|
|
if $DRY_RUN; then
|
|
print_command "$CONTAINER_ENGINE" rm -f "$SLOPTRAP_CONTAINER_NAME"
|
|
print_command "$CONTAINER_ENGINE" rmi "$SLOPTRAP_IMAGE_NAME"
|
|
print_command rm -rf "$helper_root"
|
|
return 0
|
|
fi
|
|
"$CONTAINER_ENGINE" rm -f "$SLOPTRAP_CONTAINER_NAME" >/dev/null 2>&1 || true
|
|
"$CONTAINER_ENGINE" rmi "$SLOPTRAP_IMAGE_NAME" >/dev/null 2>&1 || true
|
|
rm -rf "$helper_root"
|
|
}
|
|
|
|
prune_sloptrap_images() {
|
|
if ! $DRY_RUN; then
|
|
status_line "Pruning unused sloptrap images\n"
|
|
fi
|
|
local -a cmd=(
|
|
"$CONTAINER_ENGINE" image prune --force --all
|
|
--filter "label=$SLOPTRAP_IMAGE_LABEL"
|
|
)
|
|
run_or_print "${cmd[@]}"
|
|
}
|
|
|
|
run_codex_command() {
|
|
local -a extra_args=("$@")
|
|
local -a cmd=("${BASE_CONTAINER_CMD[@]}" "$SLOPTRAP_IMAGE_NAME")
|
|
if [[ ${#CODEX_ARGS_ARRAY[@]} -gt 0 ]]; then
|
|
cmd+=("${CODEX_ARGS_ARRAY[@]}")
|
|
fi
|
|
if [[ ${#extra_args[@]} -gt 0 ]]; then
|
|
cmd+=("${extra_args[@]}")
|
|
fi
|
|
run_or_print "${cmd[@]}"
|
|
}
|
|
|
|
run_codex() {
|
|
if ! $DRY_RUN; then
|
|
status_line "Running %s\n" "$SLOPTRAP_IMAGE_NAME"
|
|
fi
|
|
run_codex_command
|
|
}
|
|
|
|
run_login_target() {
|
|
if ! $DRY_RUN; then
|
|
status_line "Login %s\n" "$SLOPTRAP_IMAGE_NAME"
|
|
fi
|
|
local -a cmd=("${BASE_CONTAINER_CMD[@]}" "$SLOPTRAP_IMAGE_NAME" login)
|
|
run_or_print "${cmd[@]}"
|
|
}
|
|
|
|
run_shell_target() {
|
|
if ! $DRY_RUN; then
|
|
status_line "Shell %s\n" "$SLOPTRAP_IMAGE_NAME"
|
|
fi
|
|
local -a cmd=("${BASE_CONTAINER_CMD[@]}" --entrypoint /bin/bash "$SLOPTRAP_IMAGE_NAME")
|
|
run_or_print "${cmd[@]}"
|
|
}
|
|
|
|
run_resume_target() {
|
|
local session_id=$1
|
|
if ! $DRY_RUN; then
|
|
status_line "Resume %s (%s)\n" "$SLOPTRAP_IMAGE_NAME" "$session_id"
|
|
fi
|
|
run_codex_command resume "$session_id"
|
|
}
|
|
|
|
process_resume_target() {
|
|
local session_id=$1
|
|
if [[ -z $session_id ]]; then
|
|
error "target 'resume' requires a session identifier"
|
|
fi
|
|
build_if_missing
|
|
run_resume_target "$session_id"
|
|
}
|
|
|
|
dispatch_target() {
|
|
local target=$1
|
|
case "$target" in
|
|
build)
|
|
build_image
|
|
;;
|
|
rebuild)
|
|
rebuild_image
|
|
;;
|
|
build-if-missing)
|
|
build_if_missing
|
|
;;
|
|
run)
|
|
build_if_missing
|
|
run_codex
|
|
;;
|
|
login)
|
|
build_if_missing
|
|
ensure_codex_home_dir
|
|
run_login_target
|
|
;;
|
|
shell)
|
|
build_if_missing
|
|
run_shell_target
|
|
;;
|
|
stop)
|
|
stop_container
|
|
;;
|
|
clean)
|
|
clean_environment
|
|
;;
|
|
prune)
|
|
prune_sloptrap_images
|
|
;;
|
|
*)
|
|
error "unknown target '$target'"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
DRY_RUN=false
|
|
PRINT_CONFIG=false
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--dry-run)
|
|
DRY_RUN=true
|
|
shift
|
|
;;
|
|
--print-config)
|
|
PRINT_CONFIG=true
|
|
shift
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
--)
|
|
shift
|
|
break
|
|
;;
|
|
-*)
|
|
usage >&2
|
|
error "unknown flag '$1'"
|
|
;;
|
|
*)
|
|
break
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [[ $# -lt 1 ]]; then
|
|
usage >&2
|
|
exit 1
|
|
fi
|
|
|
|
CODE_DIR_INPUT=$1
|
|
shift
|
|
|
|
if [[ ! -d $CODE_DIR_INPUT ]]; then
|
|
error "code directory '$CODE_DIR_INPUT' does not exist"
|
|
fi
|
|
|
|
CODE_DIR="$(cd "$CODE_DIR_INPUT" && pwd -P)"
|
|
if [[ $CODE_DIR == "/" ]]; then
|
|
error "project root may not be '/'"
|
|
fi
|
|
MANIFEST_PATH="$CODE_DIR/$MANIFEST_BASENAME"
|
|
|
|
if [[ -f $MANIFEST_PATH ]]; then
|
|
MANIFEST_PRESENT=true
|
|
parse_manifest "$MANIFEST_PATH"
|
|
fi
|
|
|
|
PROJECT_NAME=${MANIFEST[name]-$(basename "$CODE_DIR")}
|
|
if [[ -z $PROJECT_NAME ]]; then
|
|
error "$MANIFEST_PATH: project name resolved to empty string"
|
|
fi
|
|
if [[ ! $PROJECT_NAME =~ $VALID_NAME_REGEX ]]; then
|
|
error "$MANIFEST_PATH: invalid project name '$PROJECT_NAME' (allowed: letters, digits, ., _, -)"
|
|
fi
|
|
|
|
select_codex_home "$CODE_DIR"
|
|
ensure_ignore_helper_root
|
|
IGNORE_STUB_BASE="$IGNORE_HELPER_ROOT/session-${BASHPID:-$$}"
|
|
resolve_sloptrap_ignore "$CODE_DIR"
|
|
resolve_container_workdir
|
|
NEED_LOGIN=false
|
|
if [[ $CODEX_HOME_BOOTSTRAP == true ]]; then
|
|
NEED_LOGIN=true
|
|
elif [[ ! -f "$CODEX_HOME_HOST/auth.json" ]]; then
|
|
NEED_LOGIN=true
|
|
fi
|
|
|
|
TARGETS=("$@")
|
|
if [[ ${#TARGETS[@]} -eq 0 ]]; then
|
|
if [[ -n ${MANIFEST[default_targets]-} ]]; then
|
|
read -r -a TARGETS <<< "${MANIFEST[default_targets]}"
|
|
elif [[ -n ${MANIFEST[default_target]-} ]]; then
|
|
read -r -a TARGETS <<< "${MANIFEST[default_target]}"
|
|
else
|
|
TARGETS=("run")
|
|
fi
|
|
fi
|
|
|
|
DEFAULT_TARGETS=("${TARGETS[@]}")
|
|
|
|
PACKAGES_EXTRA=${MANIFEST[packages_extra]-}
|
|
if [[ -n ${MANIFEST[allow_host_network]-} ]]; then
|
|
case "${MANIFEST[allow_host_network],,}" in
|
|
1|true|yes)
|
|
ALLOW_HOST_NETWORK=true
|
|
;;
|
|
0|false|no)
|
|
ALLOW_HOST_NETWORK=false
|
|
;;
|
|
*)
|
|
error "$MANIFEST_PATH: allow_host_network must be true or false (got '${MANIFEST[allow_host_network]}')"
|
|
;;
|
|
esac
|
|
fi
|
|
|
|
forbidden_keys=(container_opts_extra security_opts_extra env_extra env_passthrough)
|
|
for forbidden_key in "${forbidden_keys[@]}"; do
|
|
if [[ -n ${MANIFEST[$forbidden_key]-} ]]; then
|
|
error "$MANIFEST_PATH: key '$forbidden_key' has been removed for security reasons"
|
|
fi
|
|
done
|
|
|
|
if [[ -n $PACKAGES_EXTRA ]]; then
|
|
ensure_safe_for_make "packages_extra" "$PACKAGES_EXTRA"
|
|
validate_package_list "packages_extra" "$PACKAGES_EXTRA"
|
|
fi
|
|
CONTAINER_ENGINE="$(detect_container_engine)"
|
|
CODEX_ARGS_ARRAY=("${DEFAULT_CODEX_ARGS[@]}")
|
|
if [[ -n ${MANIFEST[codex_args]-} ]]; then
|
|
ensure_safe_for_make "codex_args" "${MANIFEST[codex_args]}"
|
|
declare -a manifest_codex_args=()
|
|
read -r -a manifest_codex_args <<< "${MANIFEST[codex_args]}"
|
|
CODEX_ARGS_ARRAY+=("${manifest_codex_args[@]}")
|
|
unset -v manifest_codex_args
|
|
fi
|
|
declare -a sanitized_codex_args=()
|
|
declare -a sandbox_pair=()
|
|
codex_args_index=0
|
|
while [[ $codex_args_index -lt ${#CODEX_ARGS_ARRAY[@]} ]]; do
|
|
if [[ ${CODEX_ARGS_ARRAY[$codex_args_index]} == "--sandbox" ]]; then
|
|
if (( codex_args_index + 1 >= ${#CODEX_ARGS_ARRAY[@]} )); then
|
|
error "$MANIFEST_PATH: '--sandbox' flag requires a mode (workspace-write or workspace-read-only)"
|
|
fi
|
|
sandbox_pair=(--sandbox "${CODEX_ARGS_ARRAY[$((codex_args_index + 1))]}")
|
|
((codex_args_index+=2))
|
|
continue
|
|
fi
|
|
sanitized_codex_args+=("${CODEX_ARGS_ARRAY[$codex_args_index]}")
|
|
((codex_args_index+=1))
|
|
done
|
|
if [[ ${#sandbox_pair[@]} -gt 0 ]]; then
|
|
sanitized_codex_args+=("${sandbox_pair[@]}")
|
|
fi
|
|
CODEX_ARGS_ARRAY=("${sanitized_codex_args[@]}")
|
|
unset -v sanitized_codex_args sandbox_pair codex_args_index
|
|
ensure_safe_sandbox "${CODEX_ARGS_ARRAY[@]}"
|
|
if [[ ${#CODEX_ARGS_ARRAY[@]} -gt 0 ]]; then
|
|
CODEX_ARGS_DISPLAY=$(printf '%s ' "${CODEX_ARGS_ARRAY[@]}")
|
|
CODEX_ARGS_DISPLAY=${CODEX_ARGS_DISPLAY% }
|
|
else
|
|
CODEX_ARGS_DISPLAY=""
|
|
fi
|
|
|
|
prepare_ignore_mounts "$CODE_DIR"
|
|
prepare_container_runtime
|
|
|
|
if $PRINT_CONFIG; then
|
|
print_config "$MANIFEST_PATH"
|
|
exit 0
|
|
fi
|
|
|
|
AUTO_LOGIN=false
|
|
if [[ $NEED_LOGIN == true ]]; then
|
|
AUTO_LOGIN=true
|
|
for tgt in "${TARGETS[@]}"; do
|
|
if [[ $tgt == "login" ]]; then
|
|
AUTO_LOGIN=false
|
|
break
|
|
fi
|
|
done
|
|
fi
|
|
|
|
if $AUTO_LOGIN; then
|
|
if ! $DRY_RUN; then
|
|
status_line "Codex login required (%s)\n" "$CODEX_HOME_HOST"
|
|
fi
|
|
ensure_codex_home_dir
|
|
dispatch_target login
|
|
fi
|
|
|
|
target_index=0
|
|
while (( target_index < ${#TARGETS[@]} )); do
|
|
current_target="${TARGETS[$target_index]}"
|
|
if [[ $current_target == "resume" ]]; then
|
|
if (( target_index + 1 >= ${#TARGETS[@]} )); then
|
|
error "target 'resume' requires a session identifier"
|
|
fi
|
|
process_resume_target "${TARGETS[$((target_index + 1))]}"
|
|
((target_index+=2))
|
|
continue
|
|
fi
|
|
dispatch_target "$current_target"
|
|
((target_index+=1))
|
|
done
|