diff --git a/.github/workflows/compile_starpilot.yaml b/.github/workflows/compile_starpilot.yaml index 37920c3df..756fd9bb8 100644 --- a/.github/workflows/compile_starpilot.yaml +++ b/.github/workflows/compile_starpilot.yaml @@ -3,237 +3,112 @@ name: Compile StarPilot on: workflow_dispatch: inputs: - not_vetted: - description: "This branch is not vetted" - type: boolean - default: false - required: false - publish_custom_branch: - description: "Push to custom branch:" + target_ref: + description: "Branch to build and publish back to" type: string - default: "" - required: false - publish_starpilot: - description: "Push to StarPilot" - type: boolean - default: false - required: false - publish_staging: - description: "Push to StarPilot-Staging" - type: boolean - default: false - required: false - publish_testing: - description: "Push to StarPilot-Testing" - type: boolean - default: false - required: false - runner: - description: "Select runner" - type: choice - options: - - c3 - - c3x - default: "c3" + default: "master" required: true - update_translations: - description: "Update missing/outdated translations" - type: boolean - default: false - required: false - vet_existing_translations: - description: "Vet existing translations" - type: boolean - default: false + jobs: + description: "Optional parallel job count passed to ./build" + type: string + default: "12" required: false +concurrency: + group: compile-starpilot-${{ inputs.target_ref }} + cancel-in-progress: false + +permissions: + contents: write + env: - BASE_DIR: ${{ github.workspace }} - BUILD_DIR: "/data/openpilot" - CUSTOM_BRANCH: ${{ inputs.publish_custom_branch }} - GIT_EMAIL: "91348155+FrogAi@users.noreply.github.com" - GIT_NAME: "James" - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + GIT_EMAIL: "168790843+firestar5683@users.noreply.github.com" + GIT_NAME: "StarPilot Build Bot" + TARGET_REF: ${{ inputs.target_ref }} jobs: - get_branch: - runs-on: [self-hosted, "${{ inputs.runner }}"] - outputs: - branch: ${{ steps.get_branch.outputs.branch }} - python_version: ${{ steps.get_python_version.outputs.python_version }} + build_and_push: + runs-on: [self-hosted, truenas, starpilot-build] + timeout-minutes: 720 steps: - - name: Get Current Branch - id: get_branch + - name: Validate target branch + shell: bash run: | - cd "$BUILD_DIR" - echo "branch=$(git rev-parse --abbrev-ref HEAD)" >> "$GITHUB_OUTPUT" + set -euo pipefail - - name: Get Python Version - id: get_python_version - run: | - echo "python_version=$(tr -d '[:space:]' < "$BUILD_DIR/.python-version")" >> $GITHUB_OUTPUT + if [[ -z "${TARGET_REF}" ]]; then + echo "target_ref is required." >&2 + exit 1 + fi - translate: - needs: get_branch - if: inputs.update_translations - runs-on: ubuntu-latest - steps: - - name: Configure Git Identity - run: | - git config --global user.name "$GIT_NAME" - git config --global user.email "$GIT_EMAIL" + if [[ "${TARGET_REF}" == refs/heads/* ]]; then + echo "BUILD_BRANCH=${TARGET_REF#refs/heads/}" >> "$GITHUB_ENV" + else + echo "BUILD_BRANCH=${TARGET_REF}" >> "$GITHUB_ENV" + fi - - name: Checkout Required Files + - name: Checkout target branch uses: actions/checkout@v4 with: - ref: ${{ needs.get_branch.outputs.branch }} - sparse-checkout: | - starpilot/ui/ - selfdrive/controls/lib/alerts_offroad.json - selfdrive/ui/ - selfdrive/ui/translations/ - selfdrive/ui/translations/auto_translate.py - selfdrive/ui/update_translations.py + ref: ${{ env.BUILD_BRANCH }} + fetch-depth: 0 + submodules: recursive - - name: Set Up Python - uses: actions/setup-python@v4 - with: - cache: "pip" - python-version: ${{ needs.get_branch.outputs.python_version }} - - - name: Install Dependencies + - name: Configure git identity + shell: bash run: | - pip install requests - sudo apt-get update && sudo apt-get install -y --no-install-recommends qttools5-dev-tools + set -euo pipefail + git config user.name "$GIT_NAME" + git config user.email "$GIT_EMAIL" + git config http.postBuffer 104857600 - - name: Update Translations + - name: Ensure branch exists on origin + shell: bash run: | - python selfdrive/ui/update_translations.py --vanish + set -euo pipefail + git ls-remote --exit-code --heads origin "refs/heads/${BUILD_BRANCH}" >/dev/null - - name: Update Missing Translations - continue-on-error: true - timeout-minutes: 300 + - name: Link persistent sysroot + shell: bash run: | - python selfdrive/ui/translations/auto_translate.py --all-files + set -euo pipefail + ls -ld /runner /runner/sysroots /runner/sysroots/default || true + ls -ld /runner/sysroots/default/usr /runner/sysroots/default/usr/local /runner/sysroots/default/usr/local/lib || true + rm -rf .comma_sysroot + ln -s /runner/sysroots/default .comma_sysroot + ls -ld .comma_sysroot .comma_sysroot/usr .comma_sysroot/usr/local .comma_sysroot/usr/local/lib - - name: Vet Existing Translations - if: inputs.vet_existing_translations - continue-on-error: true - timeout-minutes: 300 + - name: Run StarPilot device build + shell: bash run: | - python selfdrive/ui/translations/auto_translate.py --all-files --vet-translations + set -euo pipefail - - name: Commit and Push Translations + export COMMA_HOST_ROOT_DIR="/mnt/Apparition/My_App_Data/starpilot-runner/_work/${GITHUB_REPOSITORY#*/}/${GITHUB_REPOSITORY#*/}" + export COMMA_HOST_SYSROOT_DIR="/mnt/Apparition/My_App_Data/starpilot-runner/sysroots/default" + export COMMA_HOST_CACHE_DIR="/mnt/Apparition/My_App_Data/starpilot-runner/_work/${GITHUB_REPOSITORY#*/}/${GITHUB_REPOSITORY#*/}/.cache" + export COMMA_SYSROOT_DIR="/runner/sysroots/default" + + if [[ -n "${{ inputs.jobs }}" ]]; then + ./build "${{ inputs.jobs }}" + else + ./build + fi + + - name: Commit build output + shell: bash run: | - if git diff --quiet selfdrive/ui/translations/*.ts; then - echo "No translation updates detected." + set -euo pipefail + + if [[ -z "$(git status --porcelain --untracked-files=no)" ]]; then + echo "No build output changes detected." exit 0 fi - git fetch --unshallow origin "${{ needs.get_branch.outputs.branch }}" - git checkout "${{ needs.get_branch.outputs.branch }}" - git add selfdrive/ui/translations/*.ts - git commit --amend --no-edit - git push --force origin "${{ needs.get_branch.outputs.branch }}" + git add -u + git commit -m "build" - build_and_push: - needs: [get_branch, translate] - if: ${{ !failure() && !cancelled() && needs.get_branch.result == 'success' }} - runs-on: [self-hosted, "${{ inputs.runner }}"] - permissions: - contents: write - defaults: - run: - working-directory: ${{ env.BUILD_DIR }} - steps: - - name: Configure Git + - name: Push build commit + shell: bash run: | - git config http.postBuffer 104857600 - git config user.name "$GIT_NAME" - git config user.email "$GIT_EMAIL" - git remote set-url origin "https://${{ secrets.PERSONAL_ACCESS_TOKEN }}@github.com/FrogAi/StarPilot.git" - - - name: Sync Translation Updates - if: inputs.update_translations - run: | - git fetch origin "${{ needs.get_branch.outputs.branch }}" - git reset --hard FETCH_HEAD - - - name: Take Ownership of Build Directory - run: | - sudo chown -R $(whoami):$(whoami) . - - - name: Clean Build Artifacts - run: | - find . -name "matlab.*.md" -delete - - find . -type d \( -iname "debug" -o -iname "test" -o -iname "tests" -o -name '__pycache__' \) -exec rm -rf {} + - - find . -type f \( \ - -name '*.a' -o \ - -name '*.o' -o \ - -name '*.onnx' -o \ - -name '*.os' -o \ - -name '*.pyc' -o \ - -name 'moc_*' \ - \) -delete - - find .github -mindepth 1 -maxdepth 1 ! -name 'workflows' -exec rm -rf {} + - find .github/workflows -mindepth 1 ! \( \ - -type f \( \ - -name 'compile_starpilot.yaml' -o \ - -name 'review_pull_request.yaml' -o \ - -name 'schedule_update.yaml' -o \ - -name 'update_pr_branch.yaml' -o \ - -name 'update_release_branch.yaml' -o \ - -name 'update_tinygrad.yaml' \ - \) \ - \) -exec rm -rf {} + - - find panda/board -type f \ - ! -name '__init__.py' \ - ! -name 'bootstub.panda.bin' \ - ! -name 'bootstub.panda_h7.bin' \ - ! -name 'panda.bin.signed' \ - ! -name 'panda_h7.bin.signed' \ - -delete - - find third_party/ -name '*Darwin*' -exec rm -rf {} + - find third_party/ -name '*x86*' -exec rm -rf {} + - - rm -f .gitignore .gitmodules .gitattributes .lfsconfig .overlay_init - - rm -rf .sconsign.dblite .vscode/ Jenkinsfile release/ scripts/ site_scons/ teleoprtc_repo/ - - find . -type d -empty ! -path "./.git*" -delete - - touch prebuilt - [ "${{ inputs.not_vetted }}" = "true" ] && touch not_vetted || true - - - name: Add Update Date File - if: inputs.publish_staging - continue-on-error: true - run: | - curl -fLsS https://raw.githubusercontent.com/FrogAi/StarPilot/StarPilot-Staging/.github/update_date -o .github/update_date || echo "No update_date found, skipping..." - - - name: Commit and Push Build - run: | - git add -f . - git commit -m "Compile StarPilot" - git push --force origin HEAD - - if [ "${{ inputs.publish_starpilot }}" = "true" ]; then - git push --force origin HEAD:StarPilot - fi - - if [ "${{ inputs.publish_staging }}" = "true" ]; then - git push --force origin HEAD:StarPilot-Staging - fi - - if [ "${{ inputs.publish_testing }}" = "true" ]; then - git push --force origin HEAD:StarPilot-Testing - fi - - if [ -n "$CUSTOM_BRANCH" ]; then - git push --force origin HEAD:"$CUSTOM_BRANCH" - fi + set -euo pipefail + git push origin "HEAD:refs/heads/${BUILD_BRANCH}" diff --git a/.github/workflows/publish_truenas_runner.yaml b/.github/workflows/publish_truenas_runner.yaml new file mode 100644 index 000000000..e4f83ed5d --- /dev/null +++ b/.github/workflows/publish_truenas_runner.yaml @@ -0,0 +1,35 @@ +name: Publish TrueNAS Runner Image + +on: + workflow_dispatch: + inputs: + image_tag: + description: "Container tag to publish to GHCR" + type: string + default: "latest" + required: true + +permissions: + contents: read + packages: write + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push runner image + uses: docker/build-push-action@v6 + with: + context: . + file: tools/truenas_github_runner/Dockerfile + push: true + tags: ghcr.io/${{ github.repository_owner }}/starpilot-truenas-runner:${{ inputs.image_tag }} diff --git a/SConstruct b/SConstruct index 5bdcf570b..d2354f868 100644 --- a/SConstruct +++ b/SConstruct @@ -153,7 +153,7 @@ lenv = { } # Allow callers to override cache/temp dirs used by subprocesses (e.g. tinygrad model compilation). -for key in ("HOME", "TMPDIR", "XDG_CACHE_HOME", "CACHEDB"): +for key in ("HOME", "TMPDIR", "XDG_CACHE_HOME", "CACHEDB", "PARAMS_ROOT"): if key in os.environ: lenv[key] = os.environ[key] @@ -404,12 +404,22 @@ if arch == "larch64" and os.environ.get("SP_TICI_SYSROOT"): qt_arm_moc = os.path.join(qt_tool_bin, "moc") qt_arm_uic = os.path.join(qt_tool_bin, "uic") qt_arm_rcc = os.path.join(qt_tool_bin, "rcc") + qt_host_bin = os.environ.get("SP_QT_HOST_BIN", "/usr/lib/qt5/bin") + qt_host_moc = os.environ.get("SP_QT_HOST_MOC", os.path.join(qt_host_bin, "moc")) + qt_host_uic = os.environ.get("SP_QT_HOST_UIC", os.path.join(qt_host_bin, "uic")) + qt_host_rcc = os.environ.get("SP_QT_HOST_RCC", "rcc") if platform.machine() in ("aarch64", "arm64"): - if os.path.isfile(qt_arm_moc): + if "SP_QT_HOST_MOC" in os.environ: + qt_env['QT3_MOC'] = qt_host_moc + elif os.path.isfile(qt_arm_moc): qt_env['QT3_MOC'] = qt_arm_moc - if os.path.isfile(qt_arm_uic): + if "SP_QT_HOST_UIC" in os.environ: + qt_env['QT3_UIC'] = qt_host_uic + elif os.path.isfile(qt_arm_uic): qt_env['QT3_UIC'] = qt_arm_uic - if os.path.isfile(qt_arm_rcc): + if "SP_QT_HOST_RCC" in os.environ: + qt_env['SP_QT_RCC'] = qt_host_rcc + elif os.path.isfile(qt_arm_rcc): qt_env['SP_QT_RCC'] = qt_arm_rcc else: qt_qemu = shutil.which("qemu-aarch64-static") or shutil.which("qemu-aarch64") @@ -417,19 +427,17 @@ if arch == "larch64" and os.environ.get("SP_TICI_SYSROOT"): if qt_qemu and os.path.isfile(qt_arm_moc): qt_env['QT3_MOC'] = f"{qt_qemu} -L {qt_tool_root} {qt_arm_moc}" else: - qt_host_bin = os.environ.get("SP_QT_HOST_BIN", "/usr/lib/qt5/bin") - qt_env['QT3_MOC'] = os.environ.get("SP_QT_HOST_MOC", os.path.join(qt_host_bin, "moc")) + qt_env['QT3_MOC'] = qt_host_moc if qt_qemu and os.path.isfile(qt_arm_uic): qt_env['QT3_UIC'] = f"{qt_qemu} -L {qt_tool_root} {qt_arm_uic}" else: - qt_host_bin = os.environ.get("SP_QT_HOST_BIN", "/usr/lib/qt5/bin") - qt_env['QT3_UIC'] = os.environ.get("SP_QT_HOST_UIC", os.path.join(qt_host_bin, "uic")) + qt_env['QT3_UIC'] = qt_host_uic if qt_qemu and os.path.isfile(qt_arm_rcc): qt_env['SP_QT_RCC'] = f"{qt_qemu} -L {qt_tool_root} {qt_arm_rcc}" else: - qt_env['SP_QT_RCC'] = os.environ.get("SP_QT_HOST_RCC", "rcc") + qt_env['SP_QT_RCC'] = qt_host_rcc qt_env['CPPPATH'] += qt_dirs + ["#third_party/qrcode"] qt_flags = [ diff --git a/docs/how-to/truenas-github-runner.md b/docs/how-to/truenas-github-runner.md new file mode 100644 index 000000000..c1e5d46ad --- /dev/null +++ b/docs/how-to/truenas-github-runner.md @@ -0,0 +1,113 @@ +# TrueNAS SCALE GitHub Build Runner + +This setup runs a single GitHub self-hosted runner as a TrueNAS SCALE custom app and routes the `Compile StarPilot` workflow onto that runner. + +The app container: + +- registers one repository-scoped GitHub Actions runner +- stays visible in the TrueNAS Apps UI +- mounts the host Docker socket so `./build` can launch the existing larch64 build container flow +- persists runner state and build caches in one dataset mounted at `/runner` + +## What this workflow does + +`.github/workflows/compile_starpilot.yaml` now: + +- lets an admin choose a `target_ref` branch from the Actions UI +- checks out that branch on the NAS runner +- runs `./build` +- prunes the tree with `scripts/ci_package_prebuilt_tree.sh` +- commits the result as `build` +- pushes that commit back to the selected branch without force-pushing + +Use a branch intended for built output. The prune step removes developer-facing files and is not meant for normal day-to-day development branches. + +## 1. Publish the runner image + +You need a registry image for TrueNAS to deploy. The repo includes `.github/workflows/publish_truenas_runner.yaml` for this. + +1. Open **Actions** on GitHub. +2. Run **Publish TrueNAS Runner Image**. +3. Leave `image_tag` as `latest` unless you want a versioned tag. +4. After it finishes, the image is available at: + +```text +ghcr.io//starpilot-truenas-runner:latest +``` + +If you prefer to build it locally, run: + +```bash +docker build -f tools/truenas_github_runner/Dockerfile -t ghcr.io//starpilot-truenas-runner:latest . +docker push ghcr.io//starpilot-truenas-runner:latest +``` + +## 2. Create a runner token + +Generate a repository-scoped self-hosted runner token in GitHub: + +1. Open the repository. +2. Go to **Settings > Actions > Runners**. +3. Click **New self-hosted runner**. +4. Choose **Linux** and **x64**. +5. Copy the one-time registration token from the generated setup commands. + +That token expires quickly. Paste it into TrueNAS during deployment and restart the app if you need to re-register. + +## 3. Deploy on TrueNAS SCALE + +TrueNAS custom apps support Docker Compose YAML through **Apps > Discover Apps > Custom App > Install via YAML**. + +Create a dataset for persistent runner state first, for example: + +```text +/mnt//apps/starpilot-runner +``` + +Then paste this compose YAML into TrueNAS after replacing the placeholders: + +```yaml +services: + starpilot-runner: + image: ghcr.io//starpilot-truenas-runner:latest + container_name: starpilot-runner + restart: unless-stopped + environment: + GITHUB_REPOSITORY_URL: https://github.com// + RUNNER_NAME: starpilot-truenas-runner + RUNNER_LABELS: self-hosted,truenas,starpilot-build + RUNNER_WORKDIR: /runner/_work + RUNNER_TOKEN: + DOCKER_GID: "" + REMOVE_RUNNER_ON_EXIT: "0" + volumes: + - /mnt//apps/starpilot-runner:/runner + - /var/run/docker.sock:/var/run/docker.sock +``` + +If the container starts but cannot talk to Docker, inspect the Docker socket group on TrueNAS and set `DOCKER_GID` to that numeric group id. + +## 4. Verify runner registration + +Confirm all of the following: + +- the app shows as `Running` in TrueNAS +- the container passes its healthcheck +- GitHub shows the runner as online with labels `self-hosted`, `truenas`, and `starpilot-build` + +## 5. Run a build + +1. Open **Actions > Compile StarPilot**. +2. Click **Run workflow**. +3. Enter the branch name in `target_ref`. +4. Optionally set `jobs` to the parallelism you want for `./build`. +5. Start the run. + +On success, the workflow pushes a new commit named `build` back to that branch. + +## Operational notes + +- `RUNNER_TOKEN` is only required on first registration or when you want to replace a stale runner with the same name. +- Runner state is persisted in `/runner`, so normal restarts do not need a new token. +- `REMOVE_RUNNER_ON_EXIT=1` enables best-effort deregistration on shutdown, but that only works when the provided token is still valid. +- The build cache, GitHub workspace, and `.comma_sysroot` data all persist under the runner dataset as long as the workflow runs in the same mounted work area. diff --git a/scripts/ci_package_prebuilt_tree.sh b/scripts/ci_package_prebuilt_tree.sh new file mode 100755 index 000000000..020bdc5f3 --- /dev/null +++ b/scripts/ci_package_prebuilt_tree.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "${ROOT_DIR}" + +find . -name "matlab.*.md" -delete + +find . -type d \( -iname "debug" -o -iname "test" -o -iname "tests" -o -name "__pycache__" \) -exec rm -rf {} + + +find . -type f \( \ + -name "*.a" -o \ + -name "*.o" -o \ + -name "*.onnx" -o \ + -name "*.os" -o \ + -name "*.pyc" -o \ + -name "moc_*" \ +\) -delete + +find .github -mindepth 1 -maxdepth 1 ! -name "workflows" -exec rm -rf {} + +find .github/workflows -mindepth 1 ! \( \ + -type f \( \ + -name "compile_starpilot.yaml" -o \ + -name "publish_truenas_runner.yaml" -o \ + -name "review_pull_request.yaml" -o \ + -name "schedule_update.yaml" -o \ + -name "update_pr_branch.yaml" -o \ + -name "update_release_branch.yaml" -o \ + -name "update_tinygrad.yaml" \ + \) \ +\) -exec rm -rf {} + + +find panda/board -type f \ + ! -name "__init__.py" \ + ! -name "bootstub.panda.bin" \ + ! -name "bootstub.panda_h7.bin" \ + ! -name "panda.bin.signed" \ + ! -name "panda_h7.bin.signed" \ + -delete + +find third_party/ -name "*Darwin*" -exec rm -rf {} + +find third_party/ -name "*x86*" -exec rm -rf {} + + +rm -f .gitignore .gitmodules .gitattributes .lfsconfig .overlay_init + +rm -rf .sconsign.dblite .vscode/ Jenkinsfile release/ scripts/ site_scons/ teleoprtc_repo/ + +find . -type d -empty ! -path "./.git*" -delete + +touch prebuilt diff --git a/scripts/laptop_device_build.sh b/scripts/laptop_device_build.sh index 067768898..7c1eb7ef9 100755 --- a/scripts/laptop_device_build.sh +++ b/scripts/laptop_device_build.sh @@ -5,6 +5,9 @@ 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)}" + # 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 @@ -14,6 +17,9 @@ 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' @@ -98,11 +104,23 @@ detect_engine() { 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..." - "${engine}" build --pull --platform linux/arm64 -f tools/laptop_device_build/Dockerfile -t "${IMAGE_NAME}" . + 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}" @@ -161,7 +179,7 @@ ensure_image_capnp_version() { fi echo "Container image ${IMAGE_NAME} has Cap'n Proto ${actual:-unknown}, expected ${expected}. Rebuilding it now..." - "${engine}" build --pull --platform linux/arm64 -f tools/laptop_device_build/Dockerfile -t "${IMAGE_NAME}" . + 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 @@ -191,9 +209,9 @@ default_jobs() { build_container_image() { local engine engine="$(detect_engine)" - "${engine}" build --pull --platform linux/arm64 \ - -f tools/laptop_device_build/Dockerfile \ - -t "${IMAGE_NAME}" . + build_container_image_cmd "${engine}" --pull \ + -f "${HOST_DOCKERFILE_PATH}" \ + -t "${IMAGE_NAME}" "${HOST_ROOT_DIR}" assert_image_arch "${engine}" } @@ -351,12 +369,13 @@ setup_sysroot_from_agnos() { local engine engine="$(detect_engine)" ensure_image_exists "${engine}" - mkdir -p "${SYSROOT_DIR}" "${ROOT_DIR}/.cache/agnos" + mkdir -p "${SYSROOT_DIR}" "${ROOT_DIR}/.cache/agnos" "${HOST_CACHE_DIR}/agnos" "${engine}" run --rm --platform linux/arm64 \ - -v "${ROOT_DIR}:/work" \ - -v "${SYSROOT_DIR}:/opt/tici-sysroot" \ - -v "${ROOT_DIR}/.cache:/work/.cache" \ + --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}" \ python3 tools/laptop_device_build/extract_sysroot_from_agnos.py \ @@ -380,7 +399,7 @@ run_larch64_scons() { echo " jobs: ${jobs}" echo " note: warp artifact precompile can take several minutes on first run" - mkdir -p "${ROOT_DIR}/.cache/scons" + mkdir -p "${ROOT_DIR}/.cache/scons" "${HOST_CACHE_DIR}/scons" local extra_args="" local jobs_prefix="-j${jobs}" @@ -407,7 +426,10 @@ 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" @@ -419,11 +441,12 @@ 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" \ + --user "${DOCKER_RUN_USER}" \ + -v "${HOST_ROOT_DIR}:/work" \ + -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}" @@ -537,7 +560,7 @@ run_manager() { ensure_image_exists "${engine}" assert_runtime_machine "${engine}" ensure_sysroot_layout - mkdir -p "${ROOT_DIR}/.cache/scons" + mkdir -p "${ROOT_DIR}/.cache/scons" "${HOST_CACHE_DIR}/scons" local manager_args_q="" if [[ "${#manager_args[@]}" -gt 0 ]]; then @@ -556,7 +579,10 @@ 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 @@ -565,11 +591,12 @@ 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" \ + --user "${DOCKER_RUN_USER}" \ + -v "${HOST_ROOT_DIR}:/work" \ + -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}" } @@ -580,11 +607,12 @@ run_shell() { 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" \ + --user "${DOCKER_RUN_USER}" \ + -v "${HOST_ROOT_DIR}:/work" \ + -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 } diff --git a/selfdrive/ui/SConscript b/selfdrive/ui/SConscript index e14b9e966..e51f6d161 100644 --- a/selfdrive/ui/SConscript +++ b/selfdrive/ui/SConscript @@ -1,4 +1,5 @@ import json +import os Import('env', 'qt_env', 'arch', 'common', 'messaging', 'visionipc', 'transformations') base_libs = [common, messaging, visionipc, transformations, @@ -65,7 +66,7 @@ with open(File("translations/languages.json").abspath) as f: languages = json.loads(f.read()) translation_sources = [f"#selfdrive/ui/translations/{l}.ts" for l in languages.values()] translation_targets = [src.replace(".ts", ".qm") for src in translation_sources] -lrelease_bin = 'third_party/qt5/larch64/bin/lrelease' if arch == 'larch64' else 'lrelease' +lrelease_bin = os.environ.get("SP_QT_HOST_LRELEASE", "lrelease") if arch == 'larch64' else 'lrelease' lrelease = qt_env.Command(translation_targets, translation_sources, f"{lrelease_bin} $SOURCES") qt_env.NoClean(translation_sources) diff --git a/tools/truenas_github_runner/.env.example b/tools/truenas_github_runner/.env.example new file mode 100644 index 000000000..3440770e1 --- /dev/null +++ b/tools/truenas_github_runner/.env.example @@ -0,0 +1,9 @@ +RUNNER_IMAGE=ghcr.io/REPLACE_ME/starpilot-truenas-runner:latest +GITHUB_REPOSITORY_URL=https://github.com/REPLACE_ME/REPLACE_ME +RUNNER_NAME=starpilot-truenas-runner +RUNNER_LABELS=self-hosted,truenas,starpilot-build +RUNNER_WORKDIR=/runner/_work +RUNNER_TOKEN=REPLACE_WITH_ONE_TIME_GITHUB_RUNNER_TOKEN +RUNNER_DATA_PATH=/mnt/tank/apps/starpilot-runner +DOCKER_GID= +REMOVE_RUNNER_ON_EXIT=0 diff --git a/tools/truenas_github_runner/Dockerfile b/tools/truenas_github_runner/Dockerfile new file mode 100644 index 000000000..93aea3b60 --- /dev/null +++ b/tools/truenas_github_runner/Dockerfile @@ -0,0 +1,41 @@ +FROM ubuntu:24.04 + +ARG DEBIAN_FRONTEND=noninteractive +ARG RUNNER_VERSION=2.334.0 + +ENV RUNNER_HOME=/runner +ENV RUNNER_WORKDIR=/runner/_work + +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + docker.io \ + git \ + gosu \ + jq \ + sudo \ + tar \ + unzip \ + && rm -rf /var/lib/apt/lists/* + +RUN useradd --create-home --home-dir /home/runner --shell /bin/bash runner \ + && usermod -aG sudo runner \ + && echo "runner ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/runner + +WORKDIR /opt/actions-runner + +RUN curl -fsSL -o actions-runner.tar.gz \ + "https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz" \ + && tar -xzf actions-runner.tar.gz \ + && rm actions-runner.tar.gz \ + && ./bin/installdependencies.sh \ + && chown -R runner:runner /opt/actions-runner + +COPY tools/truenas_github_runner/entrypoint.sh /usr/local/bin/truenas-runner-entrypoint +COPY tools/truenas_github_runner/healthcheck.sh /usr/local/bin/truenas-runner-healthcheck + +RUN chmod 755 /usr/local/bin/truenas-runner-entrypoint /usr/local/bin/truenas-runner-healthcheck + +HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=5 CMD ["/usr/local/bin/truenas-runner-healthcheck"] + +ENTRYPOINT ["/usr/local/bin/truenas-runner-entrypoint"] diff --git a/tools/truenas_github_runner/docker-compose.yaml b/tools/truenas_github_runner/docker-compose.yaml new file mode 100644 index 000000000..b79813fa3 --- /dev/null +++ b/tools/truenas_github_runner/docker-compose.yaml @@ -0,0 +1,16 @@ +services: + starpilot-runner: + image: ${RUNNER_IMAGE} + container_name: starpilot-runner + restart: unless-stopped + environment: + GITHUB_REPOSITORY_URL: ${GITHUB_REPOSITORY_URL} + RUNNER_NAME: ${RUNNER_NAME} + RUNNER_LABELS: ${RUNNER_LABELS} + RUNNER_WORKDIR: ${RUNNER_WORKDIR} + RUNNER_TOKEN: ${RUNNER_TOKEN} + DOCKER_GID: ${DOCKER_GID:-} + REMOVE_RUNNER_ON_EXIT: ${REMOVE_RUNNER_ON_EXIT:-0} + volumes: + - ${RUNNER_DATA_PATH}:/runner + - /var/run/docker.sock:/var/run/docker.sock diff --git a/tools/truenas_github_runner/entrypoint.sh b/tools/truenas_github_runner/entrypoint.sh new file mode 100755 index 000000000..6773119d3 --- /dev/null +++ b/tools/truenas_github_runner/entrypoint.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash + +set -euo pipefail + +RUNNER_HOME="${RUNNER_HOME:-/runner}" +RUNNER_WORKDIR="${RUNNER_WORKDIR:-${RUNNER_HOME}/_work}" +RUNNER_LABELS="${RUNNER_LABELS:-self-hosted,truenas,starpilot-build}" +RUNNER_NAME="${RUNNER_NAME:-$(hostname)}" +REMOVE_RUNNER_ON_EXIT="${REMOVE_RUNNER_ON_EXIT:-0}" +RUNNER_ALLOW_RUNASROOT=1 + +export RUNNER_ALLOW_RUNASROOT + +log() { + printf '[truenas-runner] %s\n' "$*" +} + +fail() { + printf '[truenas-runner] ERROR: %s\n' "$*" >&2 + exit 1 +} + +require_env() { + local name="$1" + [[ -n "${!name:-}" ]] || fail "Missing required environment variable: ${name}" +} + +ensure_docker_access() { + if [[ ! -S /var/run/docker.sock ]]; then + fail "Docker socket /var/run/docker.sock is not mounted." + fi + + if [[ -n "${DOCKER_GID:-}" ]]; then + if ! getent group docker-host >/dev/null 2>&1; then + groupadd --gid "${DOCKER_GID}" docker-host + fi + usermod -aG docker-host runner + fi +} + +prepare_dirs() { + mkdir -p "${RUNNER_HOME}" "${RUNNER_WORKDIR}" "${RUNNER_HOME}/.state" "${RUNNER_HOME}/.config" + + chown -R runner:runner "${RUNNER_HOME}" +} + +restore_runner_config() { + local file="" + for file in .runner .credentials .credentials_rsaparams; do + if [[ -f "${RUNNER_HOME}/.config/${file}" ]]; then + cp -f "${RUNNER_HOME}/.config/${file}" "/opt/actions-runner/${file}" + chown runner:runner "/opt/actions-runner/${file}" + else + rm -f "/opt/actions-runner/${file}" + fi + done +} + +persist_runner_config() { + local file="" + for file in .runner .credentials .credentials_rsaparams; do + if [[ -f "/opt/actions-runner/${file}" ]]; then + cp -f "/opt/actions-runner/${file}" "${RUNNER_HOME}/.config/${file}" + chown runner:runner "${RUNNER_HOME}/.config/${file}" + fi + done +} + +configure_runner() { + require_env GITHUB_REPOSITORY_URL + + if [[ -f /opt/actions-runner/.runner ]]; then + log "Existing runner configuration detected; skipping registration." + return + fi + + require_env RUNNER_TOKEN + + log "Registering GitHub runner ${RUNNER_NAME} for ${GITHUB_REPOSITORY_URL}" + + gosu runner /opt/actions-runner/config.sh \ + --url "${GITHUB_REPOSITORY_URL}" \ + --token "${RUNNER_TOKEN}" \ + --name "${RUNNER_NAME}" \ + --labels "${RUNNER_LABELS}" \ + --work "${RUNNER_WORKDIR}" \ + --unattended \ + --replace +} + +cleanup() { + local exit_code=$? + + if [[ -n "${runner_pid:-}" ]]; then + kill "${runner_pid}" >/dev/null 2>&1 || true + wait "${runner_pid}" || true + fi + + if [[ "${REMOVE_RUNNER_ON_EXIT}" == "1" && -n "${RUNNER_TOKEN:-}" && -f /opt/actions-runner/.runner ]]; then + log "Removing runner registration on exit." + gosu runner /opt/actions-runner/config.sh remove --token "${RUNNER_TOKEN}" || true + fi + + exit "${exit_code}" +} + +trap cleanup EXIT INT TERM + +ensure_docker_access +prepare_dirs +restore_runner_config +configure_runner +persist_runner_config + +touch "${RUNNER_HOME}/.state/configured" +chown runner:runner "${RUNNER_HOME}/.state/configured" + +log "Starting runner ${RUNNER_NAME}" +gosu runner /opt/actions-runner/run.sh & +runner_pid=$! +wait "${runner_pid}" diff --git a/tools/truenas_github_runner/healthcheck.sh b/tools/truenas_github_runner/healthcheck.sh new file mode 100755 index 000000000..21cb2fa90 --- /dev/null +++ b/tools/truenas_github_runner/healthcheck.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -euo pipefail + +RUNNER_HOME="${RUNNER_HOME:-/runner}" + +test -f /opt/actions-runner/.runner +test -f "${RUNNER_HOME}/.state/configured" +pgrep -f "Runner.Listener" >/dev/null