Options:
--vm <name> Parallels VM name. Default: "Ubuntu 24.04.3 ARM64"
Falls back to the closest Ubuntu VM when omitted and unavailable.
--snapshot-hint <name> Snapshot name substring/fuzzy match. Default: "fresh"
--mode <fresh|upgrade|both>
--provider <openai|anthropic|minimax>
Provider auth/model lane. Default: openai
--api-key-env <var> Host env var name for provider API key.
Default: OPENAI_API_KEY for openai, ANTHROPIC_API_KEY for anthropic
--openai-api-key-env <var> Alias for --api-key-env (backward compatible)
--install-url <url> Installer URL for latest release. Default: https://openclaw.ai/install.sh
--host-port <port> Host HTTP port for current-main tgz. Default: 18427
--host-ip <ip> Override Parallels host IP.
--latest-version <ver> Override npm latest version lookup.
--install-version <ver> Pin site-installer version/dist-tag for the baseline lane.
--target-package-spec <npm-spec>
Install this npm package tarball instead of packing current main.
Example: openclaw@2026.3.13-beta.1
--keep-server Leave temp host HTTP server running.
--json Print machine-readable JSON summary.
-h, --help Show help.
EOF
}
API_KEY_VALUE="${!API_KEY_ENV:-}"
[[ -n "$API_KEY_VALUE" ]] || die "$API_KEY_ENV is required"
resolve_vm_name() {
local json requested explicit
json="$(prlctl list --all --json)"
requested="$VM_NAME"
explicit="$VM_NAME_EXPLICIT"
PRL_VM_JSON="$json" REQUESTED_VM_NAME="$requested" VM_NAME_EXPLICIT="$explicit" python3 - <<'PY' import difflib import json import os import re import sys
from typing import Optional
payload = json.loads(os.environ["PRL_VM_JSON"])
requested = os.environ["REQUESTED_VM_NAME"].strip()
requested_lower = requested.lower()
explicit = os.environ["VM_NAME_EXPLICIT"] == "1"
names = [str(item.get("name", "")).strip() for item in payload if str(item.get("name", "")).strip()]
def parse_ubuntu_version(name: str) -> Optional[tuple[int, ...]]:
match = re.search(r"ubuntu\s+(\d+(?:\.\d+)*)", name, re.IGNORECASE) if not match:
return None
return tuple(int(part) for part in match.group(1).split("."))
def version_distance(version: tuple[int, ...], target: tuple[int, ...]) -> tuple[int, ...]:
width = max(len(version), len(target))
padded_version = version + (0,) * (width - len(version))
padded_target = target + (0,) * (width - len(target))
return tuple(abs(a - b) for a, b in zip(padded_version, padded_target))
if requested in names:
print(requested)
raise SystemExit(0)
if explicit:
sys.exit(f"vm not found: {requested}")
ubuntu_names = [name for name in names if"ubuntu" in name.lower()] if not ubuntu_names:
sys.exit(f"default vm not found and no Ubuntu fallback available: {requested}")
requested_version = parse_ubuntu_version(requested) or (24,)
ubuntu_with_versions = [
(name, parse_ubuntu_version(name)) for name in ubuntu_names
]
ubuntu_ge_24 = [
(name, version) for name, version in ubuntu_with_versions if version and version[0] >= 24
] if ubuntu_ge_24:
best_name = min(
ubuntu_ge_24,
key=lambda item: (
version_distance(item[1], requested_version),
-len(item[1]),
item[0].lower(),
),
)[0]
print(best_name)
raise SystemExit(0)
def aliases(name: str) -> list[str]:
values = [name] for pattern in (
r"^(.*)-poweroff$",
r"^(.*)-poweroff-\d{4}-\d{2}-\d{2}$",
):
match = re.match(pattern, name) if match:
values.append(match.group(1))
return values
for snapshot_id, meta in payload.items():
name = str(meta.get("name", "")).strip()
lowered = name.lower()
score = 0.0 for alias in aliases(lowered): if alias == hint:
score = max(score, 10.0) elif hint and hint in alias:
score = max(score, 5.0 + len(hint) / max(len(alias), 1)) else:
score = max(score, difflib.SequenceMatcher(None, hint, alias).ratio()) if str(meta.get("state", "")).lower() == "poweroff":
score += 0.5 if score > best_score:
best_score = score
best_id = snapshot_id
best_meta = meta if not best_id:
sys.exit("no snapshot matched")
print( "\t".join(
[
best_id,
str(best_meta.get("state", "")).strip(),
str(best_meta.get("name", "")).strip(),
]
)
)
PY
}
resolve_host_ip() { if [[ -n "$HOST_IP" ]]; then
printf '%s\n'"$HOST_IP"
return fi
local detected
detected="$(ifconfig | awk '/inet 10\.211\./ { print $2; exit }')"
[[ -n "$detected" ]] || die "failed to detect Parallels host IP; pass --host-ip"
printf '%s\n'"$detected"
}
resolve_host_port() { if is_host_port_free "$HOST_PORT"; then
printf '%s\n'"$HOST_PORT"
return fi if [[ "$HOST_PORT_EXPLICIT" -eq 1 ]]; then
die "host port $HOST_PORT already in use" fi
HOST_PORT="$(allocate_host_port)"
warn "host port 18427 busy; using $HOST_PORT"
printf '%s\n'"$HOST_PORT"
}
wait_for_vm_status() {
local expected="$1"
local deadline status
deadline=$((SECONDS + TIMEOUT_SNAPSHOT_S)) while (( SECONDS < deadline )); do
status="$(prlctl status "$VM_NAME" 2>/dev/null || true)" if [[ "$status" == *" $expected" ]]; then
return 0 fi
sleep 1 done
return 1
}
wait_for_guest_ready() {
local deadline
deadline=$((SECONDS + TIMEOUT_SNAPSHOT_S)) while (( SECONDS < deadline )); do if guest_exec /bin/true >/dev/null 2>&1; then
return 0 fi
sleep 2 done
return 1
}
restore_snapshot() {
local snapshot_id="$1"
say "Restore snapshot $SNAPSHOT_HINT ($snapshot_id)"
prlctl snapshot-switch "$VM_NAME" --id "$snapshot_id" >/dev/null if [[ "$SNAPSHOT_STATE" == "poweroff" ]]; then
wait_for_vm_status "stopped" || die "restored poweroff snapshot did not reach stopped state in $VM_NAME"
say "Start restored poweroff snapshot $SNAPSHOT_NAME"
prlctl start "$VM_NAME" >/dev/null fi
wait_for_guest_ready || die "guest did not become ready in $VM_NAME"
}
resolve_latest_version() { if [[ -n "$LATEST_VERSION" ]]; then
printf '%s\n'"$LATEST_VERSION"
return fi
npm view openclaw version --userconfig "$(mktemp)"
}
ensure_current_build() {
local head build_commit rc lock_owned
lock_owned=0 if [[ "${OPENCLAW_PARALLELS_BUILD_LOCK_HELD:-0}" != "1" ]]; then
acquire_build_lock
lock_owned=1 fi
head="$(git rev-parse HEAD)"
build_commit="$(current_build_commit)" if [[ "$build_commit" == "$head" ]] && ! source_tree_dirty_for_build; then if [[ "$lock_owned" -eq 1 ]]; then
release_build_lock fi
return fi
say "Build dist for current head"
set +e
pnpm build
rc=$? if [[ $rc -eq 0 ]]; then
parallels_package_assert_no_generated_drift
rc=$? fi
build_commit="$(current_build_commit)"
set -e if [[ "$lock_owned" -eq 1 ]]; then
release_build_lock fi
[[ $rc -eq 0 ]] || return "$rc" if [[ "$build_commit" != "$head" ]]; then
warn "dist/build-info.json still does not match HEAD after build"
return 1 fi
}
pack_main_tgz() {
local short_head pkg packed_commit rc if [[ -n "$TARGET_PACKAGE_SPEC" ]]; then
say "Pack target package tgz: $TARGET_PACKAGE_SPEC"
pkg="$(
npm pack "$TARGET_PACKAGE_SPEC" --ignore-scripts --json --pack-destination "$MAIN_TGZ_DIR" \
| python3 -c 'import json, sys; data = json.load(sys.stdin); print(data[-1]["filename"])'
)"
MAIN_TGZ_PATH="$MAIN_TGZ_DIR/$(basename "$pkg")"
TARGET_EXPECT_VERSION="$(extract_package_version_from_tgz "$MAIN_TGZ_PATH")"
say "Packed $MAIN_TGZ_PATH"
say "Target package version: $TARGET_EXPECT_VERSION"
return fi
say "Pack current main tgz"
acquire_build_lock
set +e
{
OPENCLAW_PARALLELS_BUILD_LOCK_HELD=1 ensure_current_build &&
write_package_dist_inventory &&
short_head="$(git rev-parse --short HEAD)" &&
pkg="$(
npm pack --ignore-scripts --json --pack-destination "$MAIN_TGZ_DIR" \
| python3 -c 'import json, sys; data = json.load(sys.stdin); print(data[-1]["filename"])'
)"
}
rc=$?
set -e
release_build_lock
[[ $rc -eq 0 ]] || return "$rc"
MAIN_TGZ_PATH="$MAIN_TGZ_DIR/openclaw-main-$short_head.tgz" cp"$MAIN_TGZ_DIR/$pkg""$MAIN_TGZ_PATH"
packed_commit="$(extract_package_build_commit_from_tgz "$MAIN_TGZ_PATH")"
[[ -n "$packed_commit" ]] || die "failed to read packed build commit from $MAIN_TGZ_PATH"
PACKED_MAIN_COMMIT_SHORT="${packed_commit:0:7}"
say "Packed $MAIN_TGZ_PATH"
tar -xOf "$MAIN_TGZ_PATH" package/dist/build-info.json
}
verify_target_version() { if [[ -n "$TARGET_PACKAGE_SPEC" ]]; then
verify_version_contains "$TARGET_EXPECT_VERSION"
return fi
[[ -n "$PACKED_MAIN_COMMIT_SHORT" ]] || die "packed main commit not captured"
verify_version_contains "$PACKED_MAIN_COMMIT_SHORT"
}
start_server() {
local host_ip="$1"
local artifact probe_url attempt
artifact="$(basename "$MAIN_TGZ_PATH")"
attempt=0 while :; do
attempt=$((attempt + 1))
say "Serve $(artifact_label) on $host_ip:$HOST_PORT"
(
cd "$MAIN_TGZ_DIR"
exec python3 -m http.server "$HOST_PORT" --bind 0.0.0.0
) >/tmp/openclaw-parallels-linux-http.log 2>&1 &
SERVER_PID=$!
sleep 1
probe_url="http://127.0.0.1:$HOST_PORT/$artifact" if kill -0 "$SERVER_PID" >/dev/null 2>&1 && curl -fsSI "$probe_url" >/dev/null 2>&1; then
return 0 fi
kill "$SERVER_PID" >/dev/null 2>&1 || true
wait "$SERVER_PID" >/dev/null 2>&1 || true
SERVER_PID="" if [[ "$HOST_PORT_EXPLICIT" -eq 1 || $attempt -ge 3 ]]; then
die "failed to start reachable host HTTP server on port $HOST_PORT" fi
HOST_PORT="$(allocate_host_port)"
warn "retrying host HTTP server on port $HOST_PORT" done
}
# On the Ubuntu guest the backgrounded process can bind a few seconds after # the launch command returns. Keep the race inside gateway-start instead of # failing the next phase with a false-negative RPC probe.
local deadline
deadline=$((SECONDS + TIMEOUT_GATEWAY_S)) while (( SECONDS < deadline )); do if show_gateway_status_compat >/dev/null 2>&1; then
return 0 fi
sleep 2 done
return 1
}
show_gateway_status_compat() { if guest_exec openclaw gateway status --help | grep -Fq -- "--require-rpc"; then
guest_exec openclaw gateway status --deep --require-rpc
return fi
guest_exec openclaw gateway status --deep
}
phase_run() {
local phase_id="$1"
local timeout_s="$2"
shift 2
local log_path pid start rc timed_out next_warn summary
log_path="$(phase_log_path "$phase_id")"
say "$phase_id"
start=$SECONDS
next_warn=$((start + PHASE_STALE_WARN_S))
timed_out=0
( "$@"
) >"$log_path" 2>&1 &
pid=$!
while kill -0 "$pid" >/dev/null 2>&1; do if (( SECONDS >= next_warn )); then
summary="$(parallels_log_progress_extract python3 "$log_path")"
[[ -n "$summary" ]] || summary="waiting for first log line"
warn "$phase_id still running after $((SECONDS - start))s: $summary"
next_warn=$((SECONDS + PHASE_STALE_WARN_S)) fi if (( SECONDS - start >= timeout_s )); then
timed_out=1
kill "$pid" >/dev/null 2>&1 || true
sleep 2
kill -9 "$pid" >/dev/null 2>&1 || true
break fi
sleep 1 done
set +e
wait "$pid"
rc=$?
set -e
if (( timed_out )); then
warn "$phase_id timed out after ${timeout_s}s"
printf 'timeout after %ss\n'"$timeout_s" >>"$log_path"
show_log_excerpt "$log_path"
return 124 fi
if [[ $rc -ne 0 ]]; then
warn "$phase_id failed (rc=$rc)"
show_log_excerpt "$log_path"
return "$rc" fi
return 0
}
write_summary_json() {
local summary_path="$RUN_DIR/summary.json"
python3 - "$summary_path" <<'PY' import json import os import sys
RESOLVED_VM_NAME="$(resolve_vm_name)" if [[ "$RESOLVED_VM_NAME" != "$VM_NAME" ]]; then
warn "requested VM $VM_NAME not found; using $RESOLVED_VM_NAME"
VM_NAME="$RESOLVED_VM_NAME" fi
say "VM: $VM_NAME"
say "Snapshot hint: $SNAPSHOT_HINT"
say "Resolved snapshot: $SNAPSHOT_NAME [$SNAPSHOT_STATE]"
say "Latest npm version: $LATEST_VERSION"
say "Current head: $(git rev-parse --short HEAD)"
say "Run logs: $RUN_DIR"
pack_main_tgz
start_server "$HOST_IP"
if [[ "$MODE" == "fresh" || "$MODE" == "both" ]]; then
set +e
run_fresh_main_lane "$SNAPSHOT_ID""$HOST_IP"
fresh_rc=$?
set -e if [[ $fresh_rc -eq 0 ]]; then
FRESH_MAIN_STATUS="pass" else
FRESH_MAIN_STATUS="fail" fi fi
if [[ "$MODE" == "upgrade" || "$MODE" == "both" ]]; then
set +e
run_upgrade_lane "$SNAPSHOT_ID""$HOST_IP"
upgrade_rc=$?
set -e if [[ $upgrade_rc -eq 0 ]]; then
UPGRADE_STATUS="pass" else
UPGRADE_STATUS="fail" fi fi
if [[ "$KEEP_SERVER" -eq 0 && -n "${SERVER_PID:-}" ]]; then
kill "$SERVER_PID" >/dev/null 2>&1 || true
SERVER_PID="" fi
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.