Files
StarPilot/scripts/laptop_device_build.sh
2026-05-18 11:49:28 -05:00

719 lines
24 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "${ROOT_DIR}"
HOST_ROOT_DIR="${COMMA_HOST_ROOT_DIR:-${ROOT_DIR}}"
DOCKER_RUN_USER="${COMMA_DOCKER_RUN_USER:-$(id -u):$(id -g)}"
HOST_VENV_DIR="${COMMA_HOST_VENV_DIR:-${HOST_ROOT_DIR}/.venv-linux-arm64}"
# 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}}"
HOST_SYSROOT_DIR="${COMMA_HOST_SYSROOT_DIR:-${SYSROOT_DIR}}"
HOST_CACHE_DIR="${COMMA_HOST_CACHE_DIR:-${HOST_ROOT_DIR}/.cache}"
HOST_DOCKERFILE_PATH="${COMMA_HOST_DOCKERFILE_PATH:-${HOST_ROOT_DIR}/tools/laptop_device_build/Dockerfile}"
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)."
}
build_container_image_cmd() {
local engine="$1"
shift
if [[ "${engine##*/}" == "docker" ]] && "${engine}" buildx version >/dev/null 2>&1; then
"${engine}" buildx build --load --platform linux/arm64 "$@"
return
fi
"${engine}" build --pull --platform linux/arm64 "$@"
}
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..."
build_container_image_cmd "${engine}" --pull -f "${HOST_DOCKERFILE_PATH}" -t "${IMAGE_NAME}" "${HOST_ROOT_DIR}"
fi
assert_image_arch "${engine}"
ensure_image_capnp_version "${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
}
expected_capnp_version() {
if [[ -n "${COMMA_EXPECTED_CAPNP_VERSION:-}" ]]; then
echo "${COMMA_EXPECTED_CAPNP_VERSION}"
return
fi
local dockerfile_version=""
dockerfile_version="$(sed -n 's/^ARG CAPNP_VERSION=\(.*\)$/\1/p' "${ROOT_DIR}/tools/laptop_device_build/Dockerfile" | head -n 1)"
if [[ -n "${dockerfile_version}" ]]; then
echo "${dockerfile_version}"
return
fi
local header_path="${ROOT_DIR}/cereal/gen/cpp/custom.capnp.h"
if [[ ! -f "${header_path}" ]]; then
err "Unable to determine expected Cap'n Proto version. Set COMMA_EXPECTED_CAPNP_VERSION or restore tools/laptop_device_build/Dockerfile ARG CAPNP_VERSION."
fi
local raw_version=""
raw_version="$(sed -n 's/^#elif CAPNP_VERSION != \([0-9][0-9]*\)$/\1/p' "${header_path}" | head -n 1)"
[[ -n "${raw_version}" ]] || err "Unable to determine expected Cap'n Proto version from ${header_path}."
local major=$(( raw_version / 1000000 ))
local minor=$(( (raw_version / 1000) % 1000 ))
local micro=$(( raw_version % 1000 ))
echo "${major}.${minor}.${micro}"
}
image_capnp_version() {
local engine="$1"
"${engine}" run --rm --platform linux/arm64 "${IMAGE_NAME}" bash -lc "capnp --version | awk '{print \$4}'" 2>/dev/null || true
}
ensure_image_capnp_version() {
local engine="$1"
local expected actual
expected="$(expected_capnp_version)"
actual="$(image_capnp_version "${engine}")"
if [[ "${actual}" == "${expected}" ]]; then
return
fi
echo "Container image ${IMAGE_NAME} has Cap'n Proto ${actual:-unknown}, expected ${expected}. Rebuilding it now..."
build_container_image_cmd "${engine}" --pull -f "${HOST_DOCKERFILE_PATH}" -t "${IMAGE_NAME}" "${HOST_ROOT_DIR}"
actual="$(image_capnp_version "${engine}")"
if [[ "${actual}" != "${expected}" ]]; then
err "Container image ${IMAGE_NAME} still has Cap'n Proto ${actual:-unknown} after rebuild; expected ${expected}."
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)"
build_container_image_cmd "${engine}" --pull \
-f "${HOST_DOCKERFILE_PATH}" \
-t "${IMAGE_NAME}" "${HOST_ROOT_DIR}"
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}/.sconsign.dblite" \
"${ROOT_DIR}/common/libcommon.a" \
"${ROOT_DIR}/common/params.o" \
"${ROOT_DIR}/common/params_pyx.cpp" \
"${ROOT_DIR}/cereal/libcereal.a" \
"${ROOT_DIR}/cereal/libsocketmaster.a" \
"${ROOT_DIR}/cereal/gen/cpp/"*.capnp.c++ \
"${ROOT_DIR}/cereal/gen/cpp/"*.capnp.h \
"${ROOT_DIR}/cereal/gen/cpp/"*.capnp.o \
"${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/pandad/can_list_to_can_capnp.o" \
"${ROOT_DIR}/selfdrive/pandad/libcan_list_to_can_capnp.a" \
"${ROOT_DIR}/selfdrive/modeld/models/commonmodel_pyx.o" \
"${ROOT_DIR}/selfdrive/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" "${HOST_CACHE_DIR}/agnos"
"${engine}" run --rm --platform linux/arm64 \
--user "${DOCKER_RUN_USER}" \
-v "${HOST_ROOT_DIR}:/work" \
-v "${HOST_SYSROOT_DIR}:/opt/tici-sysroot" \
-v "${HOST_CACHE_DIR}:/work/.cache" \
-w /work \
"${IMAGE_NAME}" \
/usr/bin/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" "${HOST_CACHE_DIR}/scons" "${HOST_VENV_DIR}"
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=/work/.cache/uv
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
export HOME=/tmp
export PARAMS_ROOT=/tmp/params
mkdir -p "\${UV_CACHE_DIR}"
mkdir -p "\${PARAMS_ROOT}"
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 \
--user "${DOCKER_RUN_USER}" \
-v "${HOST_ROOT_DIR}:/work" \
-v "${HOST_VENV_DIR}:/work/.venv-linux-arm64" \
-v "${HOST_SYSROOT_DIR}:/opt/tici-sysroot:ro" \
-v "${HOST_SYSROOT_DIR}/system/vendor/lib64:/system/vendor/lib64:ro" \
-v "${HOST_CACHE_DIR}:/work/.cache" \
-v "${HOST_CACHE_DIR}/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" "${HOST_CACHE_DIR}/scons" "${HOST_VENV_DIR}"
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=/work/.cache/uv
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
export HOME=/tmp
export PARAMS_ROOT=/tmp/params
mkdir -p "\${UV_CACHE_DIR}"
mkdir -p "\${PARAMS_ROOT}"
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 \
--user "${DOCKER_RUN_USER}" \
-v "${HOST_ROOT_DIR}:/work" \
-v "${HOST_VENV_DIR}:/work/.venv-linux-arm64" \
-v "${HOST_SYSROOT_DIR}:/opt/tici-sysroot:ro" \
-v "${HOST_SYSROOT_DIR}/system/vendor/lib64:/system/vendor/lib64:ro" \
-v "${HOST_CACHE_DIR}:/work/.cache" \
-v "${HOST_CACHE_DIR}/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 \
--user "${DOCKER_RUN_USER}" \
-v "${HOST_ROOT_DIR}:/work" \
-v "${HOST_VENV_DIR}:/work/.venv-linux-arm64" \
-v "${HOST_SYSROOT_DIR}:/opt/tici-sysroot:ro" \
-v "${HOST_SYSROOT_DIR}/system/vendor/lib64:/system/vendor/lib64:ro" \
-v "${HOST_CACHE_DIR}:/work/.cache" \
-v "${HOST_CACHE_DIR}/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 "$@"