StarPilot

This commit is contained in:
firestar5683
2026-03-12 01:49:47 -05:00
parent 0e9ef526f7
commit d0e1db6766
2171 changed files with 590688 additions and 247754 deletions
+86
View File
@@ -0,0 +1,86 @@
#!/usr/bin/env bash
set -euo pipefail
# Collects high-signal diagnostics for UI freeze incidents on-device.
# Usage:
# ./scripts/debug_ui_freeze.sh
# Optional env:
# OUT_DIR=/data/media/0/realdata/ui_freeze_debug
OUT_DIR="${OUT_DIR:-/data/media/0/realdata/ui_freeze_debug}"
TS="$(date +%Y%m%d_%H%M%S)"
OUT_FILE="${OUT_DIR}/ui_freeze_${TS}.log"
mkdir -p "${OUT_DIR}"
{
echo "=== UI Freeze Debug ==="
echo "timestamp: $(date -Ins)"
echo "hostname: $(hostname)"
echo "kernel: $(uname -a)"
echo
echo "=== Process Snapshot ==="
ps -eo pid,ppid,stat,ni,etime,comm,args | grep -E "(manager.py|selfdrive.ui.ui|/selfdrive/ui/ui|weston|loggerd|encoderd|modeld|dmonitoringmodeld)" || true
echo
echo "=== Manager State (tmux service output) ==="
if command -v tmux >/dev/null 2>&1; then
tmux capture-pane -pt comma:0 -S -220 2>/dev/null || true
fi
echo
echo "=== UI Watchdog Files (/dev/shm/wd_*) ==="
ls -l /dev/shm/wd_* 2>/dev/null || true
echo
echo "=== UI PID + Watchdog Timestamp Delta ==="
for ui_pid in $(pgrep -f "selfdrive.ui.ui|/selfdrive/ui/ui" 2>/dev/null || true); do
echo "ui_pid=${ui_pid}"
wd_file="/dev/shm/wd_${ui_pid}"
if [[ -f "${wd_file}" ]]; then
now_ns="$(python3 - <<'PY'
import time
print(int(time.monotonic_ns()))
PY
)"
last_ns="$(python3 - "${wd_file}" <<'PY'
import struct, sys
with open(sys.argv[1], "rb") as f:
print(struct.unpack("Q", f.read())[0])
PY
)"
python3 - "${now_ns}" "${last_ns}" <<'PY'
import sys
now_ns = int(sys.argv[1]); last_ns = int(sys.argv[2])
print(f"watchdog_age_s={(now_ns-last_ns)/1e9:.3f}")
PY
else
echo "watchdog_file_missing=${wd_file}"
fi
echo
done
echo "=== /data/log (recent ui/watchdog/weston/gpu errors) ==="
grep -RinE "Watchdog timeout for ui|Process ui crashed|ui.*exit|weston|Wayland|EGL|gbm|drm|GPU|segfault|SIGSEGV|killed process|OOM|fatal" /data/log/swaglog* 2>/dev/null | tail -n 400 || true
echo
echo "=== dmesg (recent high-signal lines) ==="
dmesg 2>/dev/null | grep -Ei "drm|gpu|msm|kgsl|weston|wayland|segfault|oom|killed process|hang" | tail -n 300 || true
echo
echo "=== systemd journal (weston / ui if available) ==="
journalctl -n 300 --no-pager 2>/dev/null | grep -Ei "weston|wayland|ui|kgsl|drm|gpu|segfault|oom" || true
echo
echo "=== open files for UI process(es) ==="
for ui_pid in $(pgrep -f "selfdrive.ui.ui|/selfdrive/ui/ui" 2>/dev/null || true); do
echo "--- lsof for pid ${ui_pid} ---"
lsof -p "${ui_pid}" 2>/dev/null | tail -n 200 || true
done
echo
echo "=== End ==="
} > "${OUT_FILE}" 2>&1
echo "Wrote: ${OUT_FILE}"
+624
View File
@@ -0,0 +1,624 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "${ROOT_DIR}"
# Make Docker Desktop binaries discoverable (docker + credential helpers) even
# when the caller shell PATH is minimal.
if [[ -d /Applications/Docker.app/Contents/Resources/bin ]]; then
export PATH="/Applications/Docker.app/Contents/Resources/bin:${PATH}"
fi
IMAGE_NAME="${COMMA_BUILD_IMAGE:-starpilot-larch64-builder:latest}"
SYSROOT_DIR_DEFAULT="${ROOT_DIR}/.comma_sysroot"
SYSROOT_DIR="${COMMA_SYSROOT_DIR:-${SYSROOT_DIR_DEFAULT}}"
usage() {
cat <<'EOF'
Usage:
scripts/laptop_device_build.sh doctor
scripts/laptop_device_build.sh setup [device-host] [device-user] [ssh-port]
scripts/laptop_device_build.sh setup-sysroot <device-host> [device-user] [ssh-port]
scripts/laptop_device_build.sh setup-sysroot-agnos [manifest-path]
scripts/laptop_device_build.sh build-image
scripts/laptop_device_build.sh build [jobs] [scons args...]
scripts/laptop_device_build.sh scons [scons args...]
scripts/laptop_device_build.sh manager [jobs] [--no-build] [-- manager args...]
scripts/laptop_device_build.sh shell
Modes:
doctor Check host/container prerequisites for laptop-side device builds.
setup One-time setup for laptop builds (venv + image + sysroot).
setup-sysroot Sync required runtime/linker libs from a comma device over SSH.
setup-sysroot-agnos Download AGNOS system image and extract sysroot locally.
build-image Build the Linux/aarch64 container image used for device-target builds.
build Run larch64 scons build in container and set prebuilt flag.
scons Run raw scons in the larch64 container (no prebuilt touch).
manager Run system/manager/manager.py in the larch64 container.
shell Open an interactive shell in the build container with all mounts.
Environment overrides:
COMMA_BUILD_IMAGE Container image tag (default: starpilot-larch64-builder:latest)
COMMA_SYSROOT_DIR Sysroot location (default: ./.comma_sysroot)
COMMA_AUTOSTART_DOCKER Auto-launch Docker Desktop on macOS when needed (default: 1)
EOF
}
err() {
echo "ERROR: $*" >&2
exit 1
}
require_cmd() {
command -v "$1" >/dev/null 2>&1 || err "Missing required command: $1"
}
detect_engine() {
local docker_cmd=""
if command -v docker >/dev/null 2>&1; then
docker_cmd="$(command -v docker)"
elif [[ -x /Applications/Docker.app/Contents/Resources/bin/docker ]]; then
docker_cmd="/Applications/Docker.app/Contents/Resources/bin/docker"
fi
if [[ -n "${docker_cmd}" ]]; then
# Needed for docker credential helpers (e.g. docker-credential-desktop).
export PATH="$(dirname "${docker_cmd}"):${PATH}"
if ! "${docker_cmd}" info >/dev/null 2>&1; then
if [[ "$(uname -s)" == "Darwin" ]] && [[ "${COMMA_AUTOSTART_DOCKER:-1}" =~ ^(1|true|yes|on)$ ]]; then
if [[ -d /Applications/Docker.app ]] && command -v open >/dev/null 2>&1; then
echo "Docker daemon is not running. Launching Docker Desktop..." >&2
open -g -a Docker >/dev/null 2>&1 || open -a Docker >/dev/null 2>&1 || true
for ((i=0; i<300; i++)); do
if "${docker_cmd}" info >/dev/null 2>&1; then
break
fi
sleep 1
done
fi
fi
if ! "${docker_cmd}" info >/dev/null 2>&1; then
err "Docker CLI found, but Docker daemon is not running. Start Docker Desktop (open -a Docker) and retry."
fi
fi
echo "${docker_cmd}"
return
fi
if command -v podman >/dev/null 2>&1; then
local podman_cmd
podman_cmd="$(command -v podman)"
if ! "${podman_cmd}" info >/dev/null 2>&1; then
err "Podman CLI found, but Podman service is not running."
fi
echo "${podman_cmd}"
return
fi
err "No container runtime found (install docker or podman)."
}
ensure_image_exists() {
local engine="$1"
if ! "${engine}" image inspect "${IMAGE_NAME}" >/dev/null 2>&1; then
echo "Container image ${IMAGE_NAME} not found. Building it now..."
"${engine}" build --pull --platform linux/arm64 -f tools/laptop_device_build/Dockerfile -t "${IMAGE_NAME}" .
fi
assert_image_arch "${engine}"
}
assert_image_arch() {
local engine="$1"
local arch
arch="$("${engine}" image inspect "${IMAGE_NAME}" --format '{{.Architecture}}' 2>/dev/null || true)"
if [[ "${arch}" != "arm64" ]]; then
err "Container image ${IMAGE_NAME} is '${arch:-unknown}', expected 'arm64'. Rebuild with: ${engine} build --pull --platform linux/arm64 -f tools/laptop_device_build/Dockerfile -t ${IMAGE_NAME} ."
fi
}
assert_runtime_machine() {
local engine="$1"
local machine
machine="$("${engine}" run --rm --platform linux/arm64 "${IMAGE_NAME}" bash -lc "readelf -h \$(command -v python3) | awk -F: '/Machine/ {gsub(/^ +/, \"\", \$2); print \$2}'" 2>/dev/null || true)"
if [[ "${machine}" != "AArch64" ]]; then
err "Container Python runtime is '${machine:-unknown}', expected 'AArch64'. This image can still build device artifacts, but cannot run manager with larch64 Python extensions. Rebuild from an arm64-capable base image before using 'manager' mode."
fi
}
default_jobs() {
if command -v nproc >/dev/null 2>&1; then
nproc
elif command -v sysctl >/dev/null 2>&1; then
sysctl -n hw.ncpu
else
echo 8
fi
}
build_container_image() {
local engine
engine="$(detect_engine)"
"${engine}" build --pull --platform linux/arm64 \
-f tools/laptop_device_build/Dockerfile \
-t "${IMAGE_NAME}" .
assert_image_arch "${engine}"
}
sanitize_sysroot_headers() {
# Device /usr/local/include can include older capnp/kj headers that conflict
# with generated cereal headers in this tree. Keep ffmpeg headers, drop capnp.
local removed=0
local path=""
for path in \
"${SYSROOT_DIR}/usr/local/include/capnp" \
"${SYSROOT_DIR}/usr/local/include/kj"; do
if [[ -e "${path}" ]]; then
rm -rf "${path}"
removed=1
fi
done
if [[ "${removed}" -eq 1 ]]; then
echo "Sanitized sysroot headers: removed capnp/kj from usr/local/include"
fi
}
ensure_sysroot_layout() {
[[ -d "${SYSROOT_DIR}/usr/local/lib" ]] || err "Missing ${SYSROOT_DIR}/usr/local/lib. Run setup-sysroot first."
[[ -d "${SYSROOT_DIR}/usr/local/include" ]] || err "Missing ${SYSROOT_DIR}/usr/local/include. Run setup-sysroot first."
[[ -d "${SYSROOT_DIR}/lib/aarch64-linux-gnu" ]] || err "Missing ${SYSROOT_DIR}/lib/aarch64-linux-gnu. Run setup-sysroot first."
[[ -d "${SYSROOT_DIR}/usr/lib/aarch64-linux-gnu" ]] || err "Missing ${SYSROOT_DIR}/usr/lib/aarch64-linux-gnu. Run setup-sysroot first."
[[ -d "${SYSROOT_DIR}/usr/include" ]] || err "Missing ${SYSROOT_DIR}/usr/include. Run setup-sysroot first."
[[ -d "${SYSROOT_DIR}/system/vendor/lib64" ]] || err "Missing ${SYSROOT_DIR}/system/vendor/lib64. Run setup-sysroot first."
sanitize_sysroot_headers
repair_sysroot_linker
}
is_aarch64_elf() {
local path="${1:-}"
[[ -f "${path}" ]] || return 1
local desc
desc="$(file -b "${path}" 2>/dev/null || true)"
[[ "${desc}" == *"ELF 64-bit LSB"* && "${desc}" == *"ARM aarch64"* ]]
}
repair_sysroot_linker() {
local lib_ld="${SYSROOT_DIR}/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1"
local usr_ld="${SYSROOT_DIR}/usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1"
# Some sysroot extraction paths can create this as an empty directory.
if [[ -d "${lib_ld}" ]]; then
rm -rf "${lib_ld}"
fi
# Ensure lib-path loader exists (symlink to usr/lib loader is fine).
if [[ ! -e "${lib_ld}" && -f "${usr_ld}" ]]; then
mkdir -p "$(dirname "${lib_ld}")"
ln -s "../../usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1" "${lib_ld}"
fi
}
resolve_ldso_source() {
local lib_ld="${SYSROOT_DIR}/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1"
local usr_ld="${SYSROOT_DIR}/usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1"
repair_sysroot_linker
if is_aarch64_elf "${lib_ld}"; then
echo "${lib_ld}"
return 0
fi
if is_aarch64_elf "${usr_ld}"; then
echo "${usr_ld}"
return 0
fi
err "Missing valid aarch64 dynamic loader in sysroot. Expected one of: ${lib_ld} or ${usr_ld}. Re-run setup-sysroot."
}
scrub_mixed_arch_artifacts() {
# Desktop launchers can leave macOS objects with the same paths as larch64 outputs.
# Remove known cross-target extension intermediates so device builds always relink.
rm -f \
"${ROOT_DIR}/cereal/messaging/"*.o \
"${ROOT_DIR}/cereal/messaging/bridge" \
"${ROOT_DIR}/msgq_repo/msgq/"*.os \
"${ROOT_DIR}/msgq_repo/msgq/"*.o \
"${ROOT_DIR}/msgq_repo/libmsgq.a" \
"${ROOT_DIR}/msgq_repo/libvisionipc.a" \
"${ROOT_DIR}/msgq_repo/msgq/ipc_pyx.so" \
"${ROOT_DIR}/msgq_repo/msgq/visionipc/"*.os \
"${ROOT_DIR}/msgq_repo/msgq/visionipc/"*.o \
"${ROOT_DIR}/msgq_repo/msgq/visionipc/visionipc_pyx.so" \
"${ROOT_DIR}/msgq/ipc_pyx.so" \
"${ROOT_DIR}/msgq/visionipc/visionipc_pyx.so" \
"${ROOT_DIR}/common/params_pyx.o" \
"${ROOT_DIR}/common/params_pyx.so" \
"${ROOT_DIR}/common/transformations/transformations.o" \
"${ROOT_DIR}/common/transformations/transformations.so" \
"${ROOT_DIR}/selfdrive/modeld/models/commonmodel_pyx.o" \
"${ROOT_DIR}/selfdrive/modeld/models/commonmodel_pyx.so" \
"${ROOT_DIR}/frogpilot/tinygrad_modeld/models/commonmodel_pyx.o" \
"${ROOT_DIR}/frogpilot/tinygrad_modeld/models/commonmodel_pyx.so"
}
sync_sysroot_from_device() {
local host="${1:-}"
local user="${2:-comma}"
local port="${3:-22}"
[[ -n "${host}" ]] || err "setup-sysroot requires <device-host>."
require_cmd rsync
mkdir -p "${SYSROOT_DIR}"
local ssh_cmd="ssh -p ${port} -o StrictHostKeyChecking=accept-new"
rsync -a --delete -e "${ssh_cmd}" "${user}@${host}:/usr/local/lib/" "${SYSROOT_DIR}/usr/local/lib/"
rsync -a --delete -e "${ssh_cmd}" "${user}@${host}:/usr/local/include/" "${SYSROOT_DIR}/usr/local/include/"
rsync -a --delete -e "${ssh_cmd}" "${user}@${host}:/lib/aarch64-linux-gnu/" "${SYSROOT_DIR}/lib/aarch64-linux-gnu/"
rsync -a --delete -e "${ssh_cmd}" "${user}@${host}:/usr/lib/aarch64-linux-gnu/" "${SYSROOT_DIR}/usr/lib/aarch64-linux-gnu/"
rsync -a --delete -e "${ssh_cmd}" "${user}@${host}:/usr/include/" "${SYSROOT_DIR}/usr/include/"
local vendor_src=""
for candidate in /system/vendor/lib64 /vendor/lib64; do
if ssh -p "${port}" -o StrictHostKeyChecking=accept-new "${user}@${host}" "test -d '${candidate}'"; then
vendor_src="${candidate}"
break
fi
done
if [[ -n "${vendor_src}" ]]; then
rsync -a --delete -e "${ssh_cmd}" "${user}@${host}:${vendor_src}/" "${SYSROOT_DIR}/system/vendor/lib64/"
else
local vendor_dst="${SYSROOT_DIR}/system/vendor/lib64"
echo "Warning: no vendor lib64 dir found on device; falling back to usr/lib/aarch64-linux-gnu"
if [[ -L "${vendor_dst}" || -f "${vendor_dst}" ]]; then
rm -f "${vendor_dst}"
elif [[ -d "${vendor_dst}" ]]; then
rm -rf "${vendor_dst}"
fi
mkdir -p "${SYSROOT_DIR}/system/vendor"
ln -s ../../usr/lib/aarch64-linux-gnu "${vendor_dst}"
fi
sanitize_sysroot_headers
echo "Sysroot synced to: ${SYSROOT_DIR}"
}
setup_sysroot_from_agnos() {
local manifest="${1:-system/hardware/tici/agnos.json}"
local engine
engine="$(detect_engine)"
ensure_image_exists "${engine}"
mkdir -p "${SYSROOT_DIR}" "${ROOT_DIR}/.cache/agnos"
"${engine}" run --rm --platform linux/arm64 \
-v "${ROOT_DIR}:/work" \
-v "${SYSROOT_DIR}:/opt/tici-sysroot" \
-v "${ROOT_DIR}/.cache:/work/.cache" \
-w /work \
"${IMAGE_NAME}" \
python3 tools/laptop_device_build/extract_sysroot_from_agnos.py \
--manifest "${manifest}" \
--output-dir /opt/tici-sysroot \
--cache-dir /work/.cache/agnos
}
run_larch64_scons() {
local jobs="$1"
shift || true
local engine
local started_at
engine="$(detect_engine)"
ensure_image_exists "${engine}"
ensure_sysroot_layout
scrub_mixed_arch_artifacts
started_at="$(date +%s)"
echo "==> Starting larch64 scons build"
echo " jobs: ${jobs}"
echo " note: warp artifact precompile can take several minutes on first run"
mkdir -p "${ROOT_DIR}/.cache/scons"
local extra_args=""
local jobs_prefix="-j${jobs}"
if [[ "$#" -gt 0 ]]; then
for arg in "$@"; do
case "${arg}" in
-j|--jobs|-j*|--jobs=*)
jobs_prefix=""
;;
esac
extra_args+=" $(printf '%q' "${arg}")"
done
fi
local cmd
cmd="$(cat <<EOF
set -euo pipefail
export UV_CACHE_DIR=/tmp/uv-cache
export SCONS_CACHE=/work/.cache/scons
export SP_FORCE_TICI=1
export SP_FORCE_ARCH=larch64
export SP_TICI_SYSROOT=/opt/tici-sysroot
export SP_BUILD_WARP_ARTIFACTS="\${SP_BUILD_WARP_ARTIFACTS:-0}"
export SP_SKIP_DM_TINYGRAD_PKL="\${SP_SKIP_DM_TINYGRAD_PKL:-1}"
export LD_LIBRARY_PATH=/usr/local/lib:/usr/lib/aarch64-linux-gnu:/lib/aarch64-linux-gnu:/opt/tici-sysroot/usr/local/lib:/opt/tici-sysroot/usr/lib/aarch64-linux-gnu:/opt/tici-sysroot/lib/aarch64-linux-gnu:/system/vendor/lib64:\${LD_LIBRARY_PATH:-}
export TMPDIR=/tmp
mkdir -p "\${UV_CACHE_DIR}"
UV_PROJECT_ENVIRONMENT=/work/.venv-linux-arm64 uv sync --frozen --all-extras
source /work/.venv-linux-arm64/bin/activate
CACHE_FLAG="--cache-disable"
if [[ "\${SP_ENABLE_SCONS_CACHE:-0}" =~ ^(1|true|yes|on)$ ]]; then
CACHE_FLAG=""
fi
scons ${jobs_prefix} \${CACHE_FLAG}${extra_args}
EOF
)"
"${engine}" run --rm --platform linux/arm64 \
-v "${ROOT_DIR}:/work" \
-v "${SYSROOT_DIR}:/opt/tici-sysroot:ro" \
-v "${SYSROOT_DIR}/system/vendor/lib64:/system/vendor/lib64:ro" \
-v "${ROOT_DIR}/.cache:/work/.cache" \
-v "${ROOT_DIR}/.cache/scons:/data/scons_cache" \
-w /work \
"${IMAGE_NAME}" bash -lc "${cmd}"
echo "==> larch64 scons completed in $(( $(date +%s) - started_at ))s"
}
setup_host_venv() {
if [[ ! -f "${ROOT_DIR}/.venv/bin/activate" ]]; then
if [[ -x "${ROOT_DIR}/tools/install_python_dependencies.sh" ]]; then
(cd "${ROOT_DIR}" && tools/install_python_dependencies.sh)
else
err "Missing host .venv and installer script."
fi
fi
}
setup_all() {
local host="${1:-}"
local user="${2:-comma}"
local port="${3:-22}"
setup_host_venv
build_container_image
if [[ -n "${host}" ]]; then
sync_sysroot_from_device "${host}" "${user}" "${port}"
else
setup_sysroot_from_agnos "system/hardware/tici/agnos.json"
fi
}
run_larch64_build() {
local jobs="${1:-$(default_jobs)}"
shift || true
echo "==> Build pass 1/2: full project build"
run_larch64_scons "${jobs}" "$@"
# Ensure prebuilt runtime compatibility probes can import these modules.
# scrub_mixed_arch_artifacts clears them at the start of each run, so
# targeted builds must always regenerate this core set.
# Do NOT regenerate dmonitoring_model_tinygrad.pkl on laptop builds:
# it is backend-captured and should come from device/QCOM-compatible artifacts.
echo "==> Build pass 2/2: required runtime artifacts"
run_larch64_scons "${jobs}" \
selfdrive/modeld/models/dmonitoring_model_metadata.pkl \
selfdrive/modeld/models/driving_vision_metadata.pkl \
selfdrive/modeld/models/driving_policy_metadata.pkl \
selfdrive/modeld/models/driving_vision_tinygrad.pkl \
selfdrive/modeld/models/driving_policy_tinygrad.pkl \
rednose/helpers/ekf_sym_pyx.so \
common/params_pyx.so \
common/transformations/transformations.so \
selfdrive/pandad/pandad_api_impl.so \
selfdrive/controls/lib/lateral_mpc_lib/c_generated_code/acados_ocp_solver_pyx.so \
selfdrive/controls/lib/lateral_mpc_lib/c_generated_code/libacados_ocp_solver_lat.so \
selfdrive/controls/lib/longitudinal_mpc_lib/c_generated_code/acados_ocp_solver_pyx.so \
selfdrive/controls/lib/longitudinal_mpc_lib/c_generated_code/libacados_ocp_solver_long.so \
selfdrive/modeld/models/commonmodel_pyx.so \
cereal/messaging/bridge \
msgq_repo/msgq/ipc_pyx.so \
msgq_repo/msgq/visionipc/visionipc_pyx.so
touch "${ROOT_DIR}/prebuilt"
}
manager_artifacts_ready() {
[[ -f "${ROOT_DIR}/msgq/ipc_pyx.so" ]] &&
[[ -f "${ROOT_DIR}/msgq/visionipc/visionipc_pyx.so" ]] &&
[[ -f "${ROOT_DIR}/common/params_pyx.so" ]] &&
[[ -f "${ROOT_DIR}/selfdrive/pandad/pandad_api_impl.so" ]] &&
[[ -f "${ROOT_DIR}/selfdrive/controls/lib/lateral_mpc_lib/c_generated_code/acados_ocp_solver_pyx.so" ]] &&
[[ -f "${ROOT_DIR}/selfdrive/controls/lib/lateral_mpc_lib/c_generated_code/libacados_ocp_solver_lat.so" ]] &&
[[ -f "${ROOT_DIR}/selfdrive/controls/lib/longitudinal_mpc_lib/c_generated_code/acados_ocp_solver_pyx.so" ]] &&
[[ -f "${ROOT_DIR}/selfdrive/controls/lib/longitudinal_mpc_lib/c_generated_code/libacados_ocp_solver_long.so" ]] &&
[[ -f "${ROOT_DIR}/selfdrive/ui/ui" ]]
}
run_manager() {
local jobs="$(default_jobs)"
local auto_build=1
local manager_args=()
if [[ "${1:-}" =~ ^[0-9]+$ ]]; then
jobs="$1"
shift || true
fi
while [[ $# -gt 0 ]]; do
case "${1}" in
--no-build)
auto_build=0
shift
;;
--)
shift || true
manager_args=("$@")
break
;;
*)
manager_args+=("$1")
shift
;;
esac
done
if [[ "${auto_build}" -eq 1 ]] && ! manager_artifacts_ready; then
echo "Missing manager runtime artifacts. Running device-target build first..."
run_larch64_build "${jobs}"
fi
local engine
engine="$(detect_engine)"
ensure_image_exists "${engine}"
assert_runtime_machine "${engine}"
ensure_sysroot_layout
mkdir -p "${ROOT_DIR}/.cache/scons"
local manager_args_q=""
if [[ "${#manager_args[@]}" -gt 0 ]]; then
for arg in "${manager_args[@]}"; do
manager_args_q+=" $(printf '%q' "${arg}")"
done
fi
local cmd
cmd="$(cat <<EOF
set -euo pipefail
export UV_CACHE_DIR=/tmp/uv-cache
export SP_FORCE_TICI=1
export SP_FORCE_ARCH=larch64
export SP_TICI_SYSROOT=/opt/tici-sysroot
export SP_BUILD_WARP_ARTIFACTS="\${SP_BUILD_WARP_ARTIFACTS:-0}"
export LD_LIBRARY_PATH=/usr/local/lib:/usr/lib/aarch64-linux-gnu:/lib/aarch64-linux-gnu:/opt/tici-sysroot/usr/local/lib:/opt/tici-sysroot/usr/lib/aarch64-linux-gnu:/opt/tici-sysroot/lib/aarch64-linux-gnu:/system/vendor/lib64:\${LD_LIBRARY_PATH:-}
export TMPDIR=/tmp
mkdir -p "\${UV_CACHE_DIR}"
UV_PROJECT_ENVIRONMENT=/work/.venv-linux-arm64 uv sync --frozen --all-extras
source /work/.venv-linux-arm64/bin/activate
cd /work
python3 system/manager/manager.py${manager_args_q}
EOF
)"
"${engine}" run --rm --platform linux/arm64 \
-v "${ROOT_DIR}:/work" \
-v "${SYSROOT_DIR}:/opt/tici-sysroot:ro" \
-v "${SYSROOT_DIR}/system/vendor/lib64:/system/vendor/lib64:ro" \
-v "${ROOT_DIR}/.cache:/work/.cache" \
-v "${ROOT_DIR}/.cache/scons:/data/scons_cache" \
-w /work \
"${IMAGE_NAME}" bash -lc "${cmd}"
}
run_shell() {
local engine
engine="$(detect_engine)"
ensure_image_exists "${engine}"
ensure_sysroot_layout
"${engine}" run --rm -it --platform linux/arm64 \
-v "${ROOT_DIR}:/work" \
-v "${SYSROOT_DIR}:/opt/tici-sysroot:ro" \
-v "${SYSROOT_DIR}/system/vendor/lib64:/system/vendor/lib64:ro" \
-v "${ROOT_DIR}/.cache:/work/.cache" \
-v "${ROOT_DIR}/.cache/scons:/data/scons_cache" \
-w /work \
"${IMAGE_NAME}" bash
}
doctor() {
local failed=0
if command -v docker >/dev/null 2>&1; then
echo "OK: docker found"
elif command -v podman >/dev/null 2>&1; then
echo "OK: podman found"
else
echo "FAIL: install docker or podman"
failed=1
fi
if command -v rsync >/dev/null 2>&1; then
echo "OK: rsync found"
else
echo "WARN: rsync missing (needed only for setup-sysroot from a physical device)"
fi
if [[ -d "${SYSROOT_DIR}" ]]; then
if [[ -d "${SYSROOT_DIR}/usr/local/lib" && -d "${SYSROOT_DIR}/usr/local/include" && -d "${SYSROOT_DIR}/lib/aarch64-linux-gnu" && -d "${SYSROOT_DIR}/usr/lib/aarch64-linux-gnu" && -d "${SYSROOT_DIR}/usr/include" && -d "${SYSROOT_DIR}/system/vendor/lib64" ]]; then
echo "OK: sysroot present at ${SYSROOT_DIR}"
else
echo "WARN: sysroot dir exists but is incomplete (${SYSROOT_DIR})"
failed=1
fi
else
echo "WARN: sysroot missing (${SYSROOT_DIR})"
failed=1
fi
if [[ -f "${ROOT_DIR}/.venv/bin/activate" ]]; then
echo "OK: host .venv present"
else
echo "WARN: host .venv missing (run scripts/laptop_device_build.sh setup)"
failed=1
fi
if [[ "${failed}" -ne 0 ]]; then
exit 1
fi
}
main() {
local mode="${1:-}"
case "${mode}" in
doctor)
doctor
;;
setup)
shift || true
setup_all "${1:-}" "${2:-comma}" "${3:-22}"
;;
setup-sysroot)
shift
sync_sysroot_from_device "${1:-}" "${2:-comma}" "${3:-22}"
;;
setup-sysroot-agnos)
shift || true
setup_sysroot_from_agnos "${1:-system/hardware/tici/agnos.json}"
;;
build-image)
build_container_image
;;
build)
shift || true
local jobs_arg="${1:-$(default_jobs)}"
if [[ "${1:-}" =~ ^[0-9]+$ ]]; then
shift || true
else
jobs_arg="$(default_jobs)"
fi
run_larch64_build "${jobs_arg}" "$@"
;;
scons)
shift || true
run_larch64_scons "$(default_jobs)" "$@"
;;
manager)
shift || true
run_manager "$@"
;;
shell)
run_shell
;;
""|-h|--help|help)
usage
;;
*)
usage
exit 1
;;
esac
}
main "$@"
+302
View File
@@ -0,0 +1,302 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "${ROOT_DIR}"
if [[ ! -f .venv/bin/activate ]]; then
echo "Missing .venv. Run tools/install_python_dependencies.sh first."
exit 1
fi
default_jobs() {
if command -v nproc >/dev/null 2>&1; then
nproc
elif command -v sysctl >/dev/null 2>&1; then
sysctl -n hw.ncpu
else
echo 8
fi
}
jobs="$(default_jobs)"
if [[ "${1:-}" =~ ^[0-9]+$ ]]; then
jobs="$1"
shift || true
fi
if [[ -d /opt/homebrew/bin ]]; then
export PATH="/opt/homebrew/bin:${PATH}"
fi
if [[ "$(uname -s)" == "Darwin" ]]; then
# Ensure desktop host extension builds stay on Apple toolchain.
export CC="/usr/bin/clang"
export CXX="/usr/bin/clang++"
fi
unset CPATH C_INCLUDE_PATH CPLUS_INCLUDE_PATH CPPFLAGS CFLAGS CXXFLAGS LDFLAGS
PY_BIN="${ROOT_DIR}/.venv/bin/python3"
if [[ ! -x "${PY_BIN}" ]]; then
echo "Missing ${PY_BIN}. Run tools/install_python_dependencies.sh first."
exit 1
fi
export PATH="${ROOT_DIR}/.venv/bin:${PATH}"
export PYTHONPATH="${ROOT_DIR}:${ROOT_DIR}/frogpilot/third_party"
for d in "${ROOT_DIR}"/*_repo; do [[ -d "$d" ]] && export PYTHONPATH="${PYTHONPATH}:$d"; done
[[ -d "${ROOT_DIR}/third_party/acados" ]] && export PYTHONPATH="${PYTHONPATH}:${ROOT_DIR}/third_party/acados"
export BIG=0
export NOBOARD=1
export SIMULATION=1
export SKIP_FW_QUERY=1
export USE_WEBCAM=1
backup_dir="$(mktemp -d /tmp/frogpilot_c4_ui_backup.XXXXXX)"
backup_manifest="${backup_dir}/.artifact_manifest"
PRE_TRACKED_DIRTY="$(mktemp /tmp/frogpilot_c4_pretracked.XXXXXX)"
POST_TRACKED_DIRTY="$(mktemp /tmp/frogpilot_c4_posttracked.XXXXXX)"
FAKE_WIFI_PID=""
runtime_artifacts=(
"common/params_pyx.so"
"msgq/ipc_pyx.so"
"msgq/visionipc/visionipc_pyx.so"
"common/transformations/transformations.so"
)
collect_tracked_dirty() {
if ! command -v git >/dev/null 2>&1; then
return 1
fi
if ! git -C "${ROOT_DIR}" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
return 1
fi
git -C "${ROOT_DIR}" status --porcelain --untracked-files=no | sed -E 's/^.. //'
}
restore_runtime_artifacts() {
if [[ -d "${backup_dir}" ]]; then
if [[ -f "${backup_manifest}" ]]; then
while IFS= read -r rel; do
[[ -z "${rel}" ]] && continue
if [[ -f "${backup_dir}/${rel}" ]]; then
cp -f "${backup_dir}/${rel}" "${ROOT_DIR}/${rel}"
else
rm -f "${ROOT_DIR}/${rel}"
fi
done < "${backup_manifest}"
fi
rm -rf "${backup_dir}"
fi
}
restore_new_tracked_changes() {
if ! collect_tracked_dirty > "${POST_TRACKED_DIRTY}"; then
return
fi
while IFS= read -r path; do
[[ -z "${path}" ]] && continue
if ! grep -Fxq "${path}" "${PRE_TRACKED_DIRTY}"; then
git -C "${ROOT_DIR}" checkout -- "${path}" >/dev/null 2>&1 || true
fi
done < "${POST_TRACKED_DIRTY}"
}
cleanup() {
if [[ -n "${FAKE_WIFI_PID}" ]]; then
kill "${FAKE_WIFI_PID}" >/dev/null 2>&1 || true
wait "${FAKE_WIFI_PID}" >/dev/null 2>&1 || true
fi
restore_runtime_artifacts
restore_new_tracked_changes
rm -f "${PRE_TRACKED_DIRTY}" "${POST_TRACKED_DIRTY}"
}
collect_tracked_dirty > "${PRE_TRACKED_DIRTY}" || true
trap cleanup EXIT
backup_if_elf() {
local rel="$1"
local src="${ROOT_DIR}/${rel}"
echo "${rel}" >> "${backup_manifest}"
if [[ -f "${src}" ]]; then
mkdir -p "${backup_dir}/$(dirname "${rel}")"
cp -f "${src}" "${backup_dir}/${rel}"
fi
}
for rel in "${runtime_artifacts[@]}"; do
backup_if_elf "${rel}"
done
archive_is_aarch64_elf() {
local path="$1"
[[ -f "${path}" ]] || return 1
if command -v readelf >/dev/null 2>&1; then
readelf -h "${path}" 2>/dev/null | grep -iq "aarch64"
else
objdump -a "${path}" 2>/dev/null | head -n 12 | grep -iqE "elf64-littleaarch64|aarch64"
fi
}
remove_if_elf() {
local rel="$1"
local path="${ROOT_DIR}/${rel}"
if [[ -f "${path}" ]] && file "${path}" | grep -q "ELF"; then
rm -f "${path}"
fi
}
prepare_common_host_artifacts() {
if archive_is_aarch64_elf "${ROOT_DIR}/common/libcommon.a"; then
rm -f \
"${ROOT_DIR}/common/libcommon.a" \
"${ROOT_DIR}/common/"*.o \
"${ROOT_DIR}/common/params_pyx.o" \
"${ROOT_DIR}/common/transformations/transformations.o"
fi
}
prepare_msgq_host_artifacts() {
# Clear mixed-arch Python extension objects from both msgq trees. These
# commonly conflict after switching between ./build (larch64) and ./c4 (macOS).
remove_if_elf "msgq/ipc_pyx.o"
remove_if_elf "msgq/visionipc/visionipc_pyx.o"
remove_if_elf "msgq_repo/msgq/ipc_pyx.o"
remove_if_elf "msgq_repo/msgq/visionipc/visionipc_pyx.o"
if archive_is_aarch64_elf "${ROOT_DIR}/msgq_repo/libmsgq.a" || archive_is_aarch64_elf "${ROOT_DIR}/msgq_repo/libvisionipc.a"; then
rm -f \
"${ROOT_DIR}/msgq_repo/libmsgq.a" \
"${ROOT_DIR}/msgq_repo/libvisionipc.a" \
"${ROOT_DIR}/msgq_repo/msgq/"*.os \
"${ROOT_DIR}/msgq_repo/msgq/visionipc/"*.os \
"${ROOT_DIR}/msgq_repo/msgq/ipc_pyx.o" \
"${ROOT_DIR}/msgq_repo/msgq/visionipc/visionipc_pyx.o"
fi
}
python_ui_runtime_ok() {
"${PY_BIN}" - <<'PY'
import pyray # noqa: F401
import openpilot.common.params_pyx # noqa: F401
import openpilot.common.transformations.transformations # noqa: F401
import msgq.ipc_pyx # noqa: F401
import msgq.visionipc.visionipc_pyx # noqa: F401
PY
}
sync_deps() {
if command -v uv >/dev/null 2>&1; then
UV_PROJECT_ENVIRONMENT=.venv uv sync --frozen --all-extras
else
echo "Missing uv. Install dependencies with tools/install_python_dependencies.sh."
exit 1
fi
}
sync_raylib() {
if "${PY_BIN}" - <<'PY' >/dev/null 2>&1
import pyray # noqa: F401
PY
then
return
fi
if command -v uv >/dev/null 2>&1; then
UV_PROJECT_ENVIRONMENT=.venv uv pip install "raylib<5.5.0.3"
else
echo "Missing uv. Install dependencies with tools/install_python_dependencies.sh."
exit 1
fi
}
run_scons() {
local jobs_arg="$1"
shift || true
local scons_bin="${ROOT_DIR}/.venv/bin/scons"
if [[ -x "${scons_bin}" ]]; then
SP_DISABLE_AUTO_DEVICE_SCONS=1 "${scons_bin}" -j"${jobs_arg}" "$@"
elif "${PY_BIN}" -m SCons --version >/dev/null 2>&1; then
SP_DISABLE_AUTO_DEVICE_SCONS=1 "${PY_BIN}" -m SCons -j"${jobs_arg}" "$@"
else
echo "SCons not found in .venv after sync."
exit 1
fi
}
kill_stale_c4_ui() {
pkill -f "selfdrive/ui/ui.py" >/dev/null 2>&1 || true
}
start_fake_wifi() {
if [[ ! "${SP_C4_FAKE_WIFI:-1}" =~ ^(1|true|yes|on)$ ]]; then
export SP_ALLOW_DESKTOP_FAKE_WIFI=0
return
fi
export SP_ALLOW_DESKTOP_FAKE_WIFI=1
"${PY_BIN}" selfdrive/debug/fake_wifi.py --network wifi --strength great --interval 0.2 &
FAKE_WIFI_PID=$!
}
if ! python_ui_runtime_ok >/dev/null 2>&1; then
echo "Preparing macOS Python UI runtime extensions..."
sync_deps
sync_raylib
prepare_common_host_artifacts
prepare_msgq_host_artifacts
remove_if_elf "common/params_pyx.so"
remove_if_elf "common/transformations/transformations.so"
remove_if_elf "msgq/ipc_pyx.so"
remove_if_elf "msgq/visionipc/visionipc_pyx.so"
run_scons "${jobs}" common/params_pyx.so common/transformations/transformations.so
(
cd "${ROOT_DIR}/msgq_repo"
local_scons_bin="${ROOT_DIR}/.venv/bin/scons"
if [[ -x "${local_scons_bin}" ]]; then
SP_DISABLE_AUTO_DEVICE_SCONS=1 "${local_scons_bin}" -j"${jobs}" msgq/ipc_pyx.so msgq/visionipc/visionipc_pyx.so
elif "${PY_BIN}" -m SCons --version >/dev/null 2>&1; then
SP_DISABLE_AUTO_DEVICE_SCONS=1 "${PY_BIN}" -m SCons -j"${jobs}" msgq/ipc_pyx.so msgq/visionipc/visionipc_pyx.so
else
echo "SCons not found in .venv after sync."
exit 1
fi
)
fi
if ! python_ui_runtime_ok >/dev/null 2>&1; then
"${PY_BIN}" - <<'PY'
import site
import sys
print("python:", sys.executable)
print("version:", sys.version)
print("site-packages:", site.getsitepackages())
PY
echo "Unable to load Python UI runtime extensions after rebuild."
exit 1
fi
if [[ "${SP_C4_COMPILE_ONLY:-0}" == "1" ]]; then
echo "C4 runtime artifacts prepared."
exit 0
fi
"${PY_BIN}" - <<'PY'
from openpilot.common.params import Params
from openpilot.system.version import terms_version, training_version
params = Params()
params.put("HasAcceptedTerms", terms_version)
params.put("CompletedTrainingVersion", training_version)
params.put_bool("OpenpilotEnabledToggle", True)
params.put_bool("IsDriverViewEnabled", False)
PY
kill_stale_c4_ui
start_fake_wifi
"${PY_BIN}" selfdrive/ui/ui.py "$@"
+231
View File
@@ -0,0 +1,231 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "${ROOT_DIR}"
if [[ ! -f .venv/bin/activate ]]; then
echo "Missing .venv. Run tools/install_python_dependencies.sh first."
exit 1
fi
default_jobs() {
if command -v nproc >/dev/null 2>&1; then
nproc
elif command -v sysctl >/dev/null 2>&1; then
sysctl -n hw.ncpu
else
echo 8
fi
}
jobs="$(default_jobs)"
if [[ "${1:-}" =~ ^[0-9]+$ ]]; then
jobs="$1"
shift || true
fi
OS_EXT="linux"
if [[ "$(uname -s)" == "Darwin" ]]; then
OS_EXT="macos"
fi
HOST_UI="${ROOT_DIR}/selfdrive/ui/ui.${OS_EXT}"
PRE_TRACKED_DIRTY="$(mktemp /tmp/frogpilot_c3_pretracked.XXXXXX)"
POST_TRACKED_DIRTY="$(mktemp /tmp/frogpilot_c3_posttracked.XXXXXX)"
BACKUP_DIR="$(mktemp -d /tmp/frogpilot_c3_backup.XXXXXX)"
BACKUP_MANIFEST="${BACKUP_DIR}/.artifact_manifest"
FAKE_WIFI_PID=""
runtime_artifacts=(
"selfdrive/ui/ui"
"common/libcommon.a"
"common/transformations/libtransformations.a"
"cereal/libsocketmaster.a"
"msgq_repo/libmsgq.a"
"msgq_repo/libvisionipc.a"
"common/params_pyx.so"
"common/transformations/transformations.so"
"msgq_repo/msgq/ipc_pyx.so"
"msgq_repo/msgq/visionipc/visionipc_pyx.so"
)
collect_tracked_dirty() {
if ! command -v git >/dev/null 2>&1; then
return 1
fi
if ! git -C "${ROOT_DIR}" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
return 1
fi
git -C "${ROOT_DIR}" status --porcelain --untracked-files=no | sed -E 's/^.. //'
}
backup_artifact() {
local rel="$1"
local src="${ROOT_DIR}/${rel}"
echo "${rel}" >> "${BACKUP_MANIFEST}"
if [[ -f "${src}" ]]; then
mkdir -p "${BACKUP_DIR}/$(dirname "${rel}")"
cp -f "${src}" "${BACKUP_DIR}/${rel}"
fi
}
restore_runtime_artifacts() {
if [[ ! -d "${BACKUP_DIR}" ]]; then
return
fi
if [[ -f "${BACKUP_MANIFEST}" ]]; then
while IFS= read -r rel; do
[[ -z "${rel}" ]] && continue
if [[ -f "${BACKUP_DIR}/${rel}" ]]; then
cp -f "${BACKUP_DIR}/${rel}" "${ROOT_DIR}/${rel}"
else
rm -f "${ROOT_DIR}/${rel}"
fi
done < "${BACKUP_MANIFEST}"
fi
rm -rf "${BACKUP_DIR}"
}
restore_new_tracked_changes() {
if ! collect_tracked_dirty > "${POST_TRACKED_DIRTY}"; then
return
fi
while IFS= read -r path; do
[[ -z "${path}" ]] && continue
if ! grep -Fxq "${path}" "${PRE_TRACKED_DIRTY}"; then
git -C "${ROOT_DIR}" checkout -- "${path}" >/dev/null 2>&1 || true
fi
done < "${POST_TRACKED_DIRTY}"
}
cleanup() {
restore_runtime_artifacts
restore_new_tracked_changes
stop_fake_wifi
rm -f "${PRE_TRACKED_DIRTY}" "${POST_TRACKED_DIRTY}"
}
start_fake_wifi() {
if [[ ! "${SP_C3_FAKE_WIFI:-1}" =~ ^(1|true|yes|on)$ ]]; then
export SP_ALLOW_DESKTOP_FAKE_WIFI=0
return
fi
export SP_ALLOW_DESKTOP_FAKE_WIFI=1
"${ROOT_DIR}/.venv/bin/python3" selfdrive/debug/fake_wifi.py --network wifi --strength great --interval 0.2 &
FAKE_WIFI_PID=$!
}
stop_fake_wifi() {
if [[ -n "${FAKE_WIFI_PID}" ]]; then
kill "${FAKE_WIFI_PID}" >/dev/null 2>&1 || true
FAKE_WIFI_PID=""
fi
}
archive_is_aarch64_elf() {
local path="$1"
[[ -f "${path}" ]] || return 1
if command -v readelf >/dev/null 2>&1; then
readelf -h "${path}" 2>/dev/null | grep -iq "aarch64"
else
objdump -a "${path}" 2>/dev/null | head -n 12 | grep -iqE "elf64-littleaarch64|aarch64"
fi
}
remove_if_elf() {
local rel="$1"
local path="${ROOT_DIR}/${rel}"
if [[ -f "${path}" ]] && file "${path}" | grep -q "ELF"; then
rm -f "${path}"
fi
}
purge_objects() {
local root="$1"
if [[ -d "${root}" ]]; then
find "${root}" -type f \( -name '*.o' -o -name '*.os' \) -delete
fi
}
prepare_host_artifacts() {
if archive_is_aarch64_elf "${ROOT_DIR}/common/libcommon.a"; then
rm -f "${ROOT_DIR}/common/libcommon.a" "${ROOT_DIR}/common/params_pyx.o"
purge_objects "${ROOT_DIR}/common"
fi
if archive_is_aarch64_elf "${ROOT_DIR}/cereal/libsocketmaster.a"; then
rm -f "${ROOT_DIR}/cereal/libsocketmaster.a"
purge_objects "${ROOT_DIR}/cereal/messaging"
fi
if archive_is_aarch64_elf "${ROOT_DIR}/common/transformations/libtransformations.a"; then
rm -f "${ROOT_DIR}/common/transformations/libtransformations.a" \
"${ROOT_DIR}/common/transformations/transformations.o"
purge_objects "${ROOT_DIR}/common/transformations"
fi
if archive_is_aarch64_elf "${ROOT_DIR}/msgq_repo/libmsgq.a" || archive_is_aarch64_elf "${ROOT_DIR}/msgq_repo/libvisionipc.a"; then
rm -f "${ROOT_DIR}/msgq_repo/libmsgq.a" "${ROOT_DIR}/msgq_repo/libvisionipc.a"
purge_objects "${ROOT_DIR}/msgq_repo/msgq"
purge_objects "${ROOT_DIR}/msgq_repo/msgq/visionipc"
fi
remove_if_elf "common/params_pyx.so"
remove_if_elf "common/transformations/transformations.so"
remove_if_elf "msgq_repo/msgq/ipc_pyx.so"
remove_if_elf "msgq_repo/msgq/visionipc/visionipc_pyx.so"
purge_objects "${ROOT_DIR}/selfdrive/ui"
purge_objects "${ROOT_DIR}/frogpilot/ui"
rm -f "${ROOT_DIR}/selfdrive/ui/ui"
}
collect_tracked_dirty > "${PRE_TRACKED_DIRTY}" || true
trap cleanup EXIT
for rel in "${runtime_artifacts[@]}"; do
backup_artifact "${rel}"
done
source .venv/bin/activate
if [[ -d /opt/homebrew/bin ]]; then
export PATH="/opt/homebrew/bin:${PATH}"
fi
if [[ "$(uname -s)" == "Darwin" ]]; then
# Keep desktop host builds on Apple toolchain even if shell exports Homebrew llvm.
export CC="/usr/bin/clang"
export CXX="/usr/bin/clang++"
fi
unset CPATH C_INCLUDE_PATH CPLUS_INCLUDE_PATH CPPFLAGS CFLAGS CXXFLAGS LDFLAGS
prepare_host_artifacts
export PYTHONPATH="${ROOT_DIR}:${ROOT_DIR}/frogpilot/third_party"
for d in "${ROOT_DIR}"/*_repo; do [[ -d "$d" ]] && export PYTHONPATH="${PYTHONPATH}:$d"; done
[[ -d "${ROOT_DIR}/third_party/acados" ]] && export PYTHONPATH="${PYTHONPATH}:${ROOT_DIR}/third_party/acados"
export SP_DISABLE_AUTO_DEVICE_SCONS=1
scons -j"${jobs}" selfdrive/ui/ui
cp -f "${ROOT_DIR}/selfdrive/ui/ui" "${HOST_UI}"
cleanup
trap - EXIT
if [[ "${SP_C3_COMPILE_ONLY:-0}" == "1" ]]; then
echo "C3 UI binary prepared."
exit 0
fi
start_fake_wifi
trap stop_fake_wifi EXIT
"${HOST_UI}" "$@"
rc=$?
stop_fake_wifi
trap - EXIT
exit "${rc}"
+302
View File
@@ -0,0 +1,302 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "${ROOT_DIR}"
if [[ ! -f .venv/bin/activate ]]; then
echo "Missing .venv. Run tools/install_python_dependencies.sh first."
exit 1
fi
default_jobs() {
if command -v nproc >/dev/null 2>&1; then
nproc
elif command -v sysctl >/dev/null 2>&1; then
sysctl -n hw.ncpu
else
echo 8
fi
}
jobs="$(default_jobs)"
if [[ "${1:-}" =~ ^[0-9]+$ ]]; then
jobs="$1"
shift || true
fi
if [[ -d /opt/homebrew/bin ]]; then
export PATH="/opt/homebrew/bin:${PATH}"
fi
if [[ "$(uname -s)" == "Darwin" ]]; then
# Ensure desktop host extension builds stay on Apple toolchain.
export CC="/usr/bin/clang"
export CXX="/usr/bin/clang++"
fi
unset CPATH C_INCLUDE_PATH CPLUS_INCLUDE_PATH CPPFLAGS CFLAGS CXXFLAGS LDFLAGS
PY_BIN="${ROOT_DIR}/.venv/bin/python3"
if [[ ! -x "${PY_BIN}" ]]; then
echo "Missing ${PY_BIN}. Run tools/install_python_dependencies.sh first."
exit 1
fi
export PATH="${ROOT_DIR}/.venv/bin:${PATH}"
export PYTHONPATH="${ROOT_DIR}:${ROOT_DIR}/frogpilot/third_party"
for d in "${ROOT_DIR}"/*_repo; do [[ -d "$d" ]] && export PYTHONPATH="${PYTHONPATH}:$d"; done
[[ -d "${ROOT_DIR}/third_party/acados" ]] && export PYTHONPATH="${PYTHONPATH}:${ROOT_DIR}/third_party/acados"
export BIG=1
export NOBOARD=1
export SIMULATION=1
export SKIP_FW_QUERY=1
export USE_WEBCAM=1
backup_dir="$(mktemp -d /tmp/frogpilot_raybig_ui_backup.XXXXXX)"
backup_manifest="${backup_dir}/.artifact_manifest"
PRE_TRACKED_DIRTY="$(mktemp /tmp/frogpilot_raybig_pretracked.XXXXXX)"
POST_TRACKED_DIRTY="$(mktemp /tmp/frogpilot_raybig_posttracked.XXXXXX)"
FAKE_WIFI_PID=""
runtime_artifacts=(
"common/params_pyx.so"
"msgq/ipc_pyx.so"
"msgq/visionipc/visionipc_pyx.so"
"common/transformations/transformations.so"
)
collect_tracked_dirty() {
if ! command -v git >/dev/null 2>&1; then
return 1
fi
if ! git -C "${ROOT_DIR}" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
return 1
fi
git -C "${ROOT_DIR}" status --porcelain --untracked-files=no | sed -E 's/^.. //'
}
restore_runtime_artifacts() {
if [[ -d "${backup_dir}" ]]; then
if [[ -f "${backup_manifest}" ]]; then
while IFS= read -r rel; do
[[ -z "${rel}" ]] && continue
if [[ -f "${backup_dir}/${rel}" ]]; then
cp -f "${backup_dir}/${rel}" "${ROOT_DIR}/${rel}"
else
rm -f "${ROOT_DIR}/${rel}"
fi
done < "${backup_manifest}"
fi
rm -rf "${backup_dir}"
fi
}
restore_new_tracked_changes() {
if ! collect_tracked_dirty > "${POST_TRACKED_DIRTY}"; then
return
fi
while IFS= read -r path; do
[[ -z "${path}" ]] && continue
if ! grep -Fxq "${path}" "${PRE_TRACKED_DIRTY}"; then
git -C "${ROOT_DIR}" checkout -- "${path}" >/dev/null 2>&1 || true
fi
done < "${POST_TRACKED_DIRTY}"
}
cleanup() {
if [[ -n "${FAKE_WIFI_PID}" ]]; then
kill "${FAKE_WIFI_PID}" >/dev/null 2>&1 || true
wait "${FAKE_WIFI_PID}" >/dev/null 2>&1 || true
fi
restore_runtime_artifacts
restore_new_tracked_changes
rm -f "${PRE_TRACKED_DIRTY}" "${POST_TRACKED_DIRTY}"
}
collect_tracked_dirty > "${PRE_TRACKED_DIRTY}" || true
trap cleanup EXIT
backup_if_elf() {
local rel="$1"
local src="${ROOT_DIR}/${rel}"
echo "${rel}" >> "${backup_manifest}"
if [[ -f "${src}" ]]; then
mkdir -p "${backup_dir}/$(dirname "${rel}")"
cp -f "${src}" "${backup_dir}/${rel}"
fi
}
for rel in "${runtime_artifacts[@]}"; do
backup_if_elf "${rel}"
done
archive_is_aarch64_elf() {
local path="$1"
[[ -f "${path}" ]] || return 1
if command -v readelf >/dev/null 2>&1; then
readelf -h "${path}" 2>/dev/null | grep -iq "aarch64"
else
objdump -a "${path}" 2>/dev/null | head -n 12 | grep -iqE "elf64-littleaarch64|aarch64"
fi
}
remove_if_elf() {
local rel="$1"
local path="${ROOT_DIR}/${rel}"
if [[ -f "${path}" ]] && file "${path}" | grep -q "ELF"; then
rm -f "${path}"
fi
}
prepare_common_host_artifacts() {
if archive_is_aarch64_elf "${ROOT_DIR}/common/libcommon.a"; then
rm -f \
"${ROOT_DIR}/common/libcommon.a" \
"${ROOT_DIR}/common/"*.o \
"${ROOT_DIR}/common/params_pyx.o" \
"${ROOT_DIR}/common/transformations/transformations.o"
fi
}
prepare_msgq_host_artifacts() {
# Clear mixed-arch Python extension objects from both msgq trees. These
# commonly conflict after switching between ./build (larch64) and ./raybig (macOS).
remove_if_elf "msgq/ipc_pyx.o"
remove_if_elf "msgq/visionipc/visionipc_pyx.o"
remove_if_elf "msgq_repo/msgq/ipc_pyx.o"
remove_if_elf "msgq_repo/msgq/visionipc/visionipc_pyx.o"
if archive_is_aarch64_elf "${ROOT_DIR}/msgq_repo/libmsgq.a" || archive_is_aarch64_elf "${ROOT_DIR}/msgq_repo/libvisionipc.a"; then
rm -f \
"${ROOT_DIR}/msgq_repo/libmsgq.a" \
"${ROOT_DIR}/msgq_repo/libvisionipc.a" \
"${ROOT_DIR}/msgq_repo/msgq/"*.os \
"${ROOT_DIR}/msgq_repo/msgq/visionipc/"*.os \
"${ROOT_DIR}/msgq_repo/msgq/ipc_pyx.o" \
"${ROOT_DIR}/msgq_repo/msgq/visionipc/visionipc_pyx.o"
fi
}
python_ui_runtime_ok() {
"${PY_BIN}" - <<'PY'
import pyray # noqa: F401
import openpilot.common.params_pyx # noqa: F401
import openpilot.common.transformations.transformations # noqa: F401
import msgq.ipc_pyx # noqa: F401
import msgq.visionipc.visionipc_pyx # noqa: F401
PY
}
sync_deps() {
if command -v uv >/dev/null 2>&1; then
UV_PROJECT_ENVIRONMENT=.venv uv sync --frozen --all-extras
else
echo "Missing uv. Install dependencies with tools/install_python_dependencies.sh."
exit 1
fi
}
sync_raylib() {
if "${PY_BIN}" - <<'PY' >/dev/null 2>&1
import pyray # noqa: F401
PY
then
return
fi
if command -v uv >/dev/null 2>&1; then
UV_PROJECT_ENVIRONMENT=.venv uv pip install "raylib<5.5.0.3"
else
echo "Missing uv. Install dependencies with tools/install_python_dependencies.sh."
exit 1
fi
}
run_scons() {
local jobs_arg="$1"
shift || true
local scons_bin="${ROOT_DIR}/.venv/bin/scons"
if [[ -x "${scons_bin}" ]]; then
SP_DISABLE_AUTO_DEVICE_SCONS=1 "${scons_bin}" -j"${jobs_arg}" "$@"
elif "${PY_BIN}" -m SCons --version >/dev/null 2>&1; then
SP_DISABLE_AUTO_DEVICE_SCONS=1 "${PY_BIN}" -m SCons -j"${jobs_arg}" "$@"
else
echo "SCons not found in .venv after sync."
exit 1
fi
}
kill_stale_raybig_ui() {
pkill -f "selfdrive/ui/ui.py" >/dev/null 2>&1 || true
}
start_fake_wifi() {
if [[ ! "${SP_RAYBIG_FAKE_WIFI:-1}" =~ ^(1|true|yes|on)$ ]]; then
export SP_ALLOW_DESKTOP_FAKE_WIFI=0
return
fi
export SP_ALLOW_DESKTOP_FAKE_WIFI=1
"${PY_BIN}" selfdrive/debug/fake_wifi.py --network wifi --strength great --interval 0.2 &
FAKE_WIFI_PID=$!
}
if ! python_ui_runtime_ok >/dev/null 2>&1; then
echo "Preparing macOS Python UI runtime extensions..."
sync_deps
sync_raylib
prepare_common_host_artifacts
prepare_msgq_host_artifacts
remove_if_elf "common/params_pyx.so"
remove_if_elf "common/transformations/transformations.so"
remove_if_elf "msgq/ipc_pyx.so"
remove_if_elf "msgq/visionipc/visionipc_pyx.so"
run_scons "${jobs}" common/params_pyx.so common/transformations/transformations.so
(
cd "${ROOT_DIR}/msgq_repo"
local_scons_bin="${ROOT_DIR}/.venv/bin/scons"
if [[ -x "${local_scons_bin}" ]]; then
SP_DISABLE_AUTO_DEVICE_SCONS=1 "${local_scons_bin}" -j"${jobs}" msgq/ipc_pyx.so msgq/visionipc/visionipc_pyx.so
elif "${PY_BIN}" -m SCons --version >/dev/null 2>&1; then
SP_DISABLE_AUTO_DEVICE_SCONS=1 "${PY_BIN}" -m SCons -j"${jobs}" msgq/ipc_pyx.so msgq/visionipc/visionipc_pyx.so
else
echo "SCons not found in .venv after sync."
exit 1
fi
)
fi
if ! python_ui_runtime_ok >/dev/null 2>&1; then
"${PY_BIN}" - <<'PY'
import site
import sys
print("python:", sys.executable)
print("version:", sys.version)
print("site-packages:", site.getsitepackages())
PY
echo "Unable to load Python UI runtime extensions after rebuild."
exit 1
fi
if [[ "${SP_RAYBIG_COMPILE_ONLY:-0}" == "1" ]]; then
echo "Raybig runtime artifacts prepared."
exit 0
fi
"${PY_BIN}" - <<'PY'
from openpilot.common.params import Params
from openpilot.system.version import terms_version, training_version
params = Params()
params.put("HasAcceptedTerms", terms_version)
params.put("CompletedTrainingVersion", training_version)
params.put_bool("OpenpilotEnabledToggle", True)
params.put_bool("IsDriverViewEnabled", False)
PY
kill_stale_raybig_ui
start_fake_wifi
"${PY_BIN}" selfdrive/ui/ui.py "$@"
+187
View File
@@ -0,0 +1,187 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "${ROOT_DIR}"
usage() {
cat <<'EOF'
Usage:
scripts/starpilot_build_flow.sh verify
scripts/starpilot_build_flow.sh mac [jobs]
scripts/starpilot_build_flow.sh device [jobs]
scripts/starpilot_build_flow.sh laptop-setup [device-host]
scripts/starpilot_build_flow.sh laptop-device [jobs]
Modes:
verify Check critical StarPilot parity build guards in-tree.
mac Run macOS developer validation build (Qt UI + Python compile checks).
device Run full on-device build (comma hardware only) and set prebuilt flag.
laptop-setup Prepare laptop device-build environment (venv + image + sysroot).
laptop-device Run full device-target build in Linux/aarch64 container on laptop.
Notes:
- mac mode does NOT produce device binaries.
- device mode must be run on comma hardware (/TICI present).
- laptop-device mode requires docker/podman and a prepared sysroot.
- prepare sysroot via scripts/laptop_device_build.sh setup-sysroot-agnos
or scripts/laptop_device_build.sh setup-sysroot <device-ip>.
EOF
}
require_venv() {
if [[ ! -f "${ROOT_DIR}/.venv/bin/activate" ]]; then
echo "Missing .venv. Initialize it first."
exit 1
fi
}
verify_tree() {
local failed=0
if ! rg -q "libs = \\['m', 'pthread'" system/camerad/SConscript; then
echo "FAIL: system/camerad/SConscript is missing pthread link guard."
failed=1
else
echo "OK: camerad link libs include pthread."
fi
if ! rg -q "TimezoneFinder = None" system/timed.py; then
echo "FAIL: system/timed.py missing TimezoneFinder fallback."
failed=1
else
echo "OK: timed.py has TimezoneFinder fallback."
fi
if ! rg -q "if tf is not None:" system/timed.py; then
echo "FAIL: system/timed.py missing guarded timezone usage."
failed=1
else
echo "OK: timed.py guards timezonefinder usage."
fi
if ! rg -q "if os.path.isfile\\(\"/TICI\"\\)" common/spinner.py; then
echo "FAIL: common/spinner.py missing device-only legacy spinner path."
failed=1
else
echo "OK: spinner uses device-only legacy fallback."
fi
if ! rg -q "if os.path.isfile\\(\"/TICI\"\\)" common/text_window.py; then
echo "FAIL: common/text_window.py missing device-only legacy text path."
failed=1
else
echo "OK: text window uses device-only legacy fallback."
fi
if ! source .venv/bin/activate >/dev/null 2>&1; then
echo "FAIL: .venv is required for loggerd import checks."
failed=1
else
if ! python - <<'PY'
import builtins
import importlib
real_import = builtins.__import__
def fake_import(name, *args, **kwargs):
if name == "xattr":
raise ModuleNotFoundError("simulated missing xattr")
return real_import(name, *args, **kwargs)
builtins.__import__ = fake_import
importlib.import_module("openpilot.system.loggerd.xattr_cache")
print("xattr_cache import fallback OK")
PY
then
echo "FAIL: loggerd xattr fallback check failed."
failed=1
else
echo "OK: loggerd xattr fallback works when xattr module is missing."
fi
fi
if [[ "${failed}" -ne 0 ]]; then
exit 1
fi
}
build_mac() {
local jobs="${1:-8}"
require_venv
source .venv/bin/activate
verify_tree
# Homebrew tools are needed for SConstruct on macOS.
export PATH="/opt/homebrew/bin:${PATH}"
export TMPDIR="${TMPDIR:-/tmp}"
python -m py_compile \
system/timed.py \
common/spinner.py \
common/text_window.py \
frogpilot/system/frogpilot_stats.py
python -m compileall -q \
selfdrive/ui \
system/ui \
frogpilot/system/the_pond \
frogpilot/system/galaxy
SP_DISABLE_AUTO_DEVICE_SCONS=1 scons -j"${jobs}" selfdrive/ui/ui
echo "mac build validation complete."
}
build_device() {
local jobs="${1:-$(nproc)}"
if [[ ! -f /TICI ]]; then
echo "device mode must run on comma hardware (/TICI not found)."
exit 1
fi
require_venv
source .venv/bin/activate
verify_tree
scons -j"${jobs}"
touch prebuilt
echo "device build complete and prebuilt flag set."
}
main() {
if [[ $# -lt 1 ]]; then
usage
exit 1
fi
local mode="$1"
shift || true
case "${mode}" in
verify)
verify_tree
;;
mac)
build_mac "${1:-8}"
;;
device)
build_device "${1:-$(nproc)}"
;;
laptop-setup)
scripts/laptop_device_build.sh setup "${1:-}" "${2:-comma}" "${3:-22}"
;;
laptop-device)
scripts/laptop_device_build.sh build "${1:-8}"
;;
*)
usage
exit 1
;;
esac
}
main "$@"