feat: add pi-gen SD image build with Gitea CI Add Docker-based pi-gen build for producing flashable Raspberry Pi OS images with Ti-Pote pre-installed. Includes Gitea Actions workflow that builds on the Act runner using docker cp (DooD-compatible), QEMU ARM emulation, and automatic Gitea release upload on tags. Also adds deploy.sh for dev SSH deployment, fixes sd-notify version, and updates .gitignore for build artifacts.
This commit is contained in:
parent
6760759cb6
commit
272d225ca8
136
.gitea/workflows/build-sd-image.yml
Normal file
136
.gitea/workflows/build-sd-image.yml
Normal file
@ -0,0 +1,136 @@
|
||||
name: Build Ti-Pote SD Image
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build-image:
|
||||
name: Build Raspberry Pi SD Image
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 180
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Install pnpm
|
||||
run: npm install -g pnpm@latest
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build robot-client
|
||||
run: pnpm --filter @ti-pote/robot-client build
|
||||
|
||||
- name: Prepare pi-gen stage files
|
||||
run: |
|
||||
STAGE_FILES="tools/pi-gen-tipote/stage-tipote/01-install-tipote/files"
|
||||
rm -rf "$STAGE_FILES"
|
||||
mkdir -p "$STAGE_FILES"
|
||||
|
||||
cp -r apps/robot-client/dist "$STAGE_FILES/dist"
|
||||
cp apps/robot-client/package.json "$STAGE_FILES/package.json"
|
||||
cp apps/robot-client/deploy/tipote.service "$STAGE_FILES/tipote.service"
|
||||
cp apps/robot-client/deploy/journald-tipote.conf "$STAGE_FILES/journald-tipote.conf"
|
||||
|
||||
- name: Register QEMU for ARM emulation
|
||||
run: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
|
||||
|
||||
- name: Build pi-gen Docker image
|
||||
run: docker build -t tipote-builder tools/pi-gen-tipote/
|
||||
|
||||
- name: Build SD image in Docker
|
||||
run: |
|
||||
# Run pi-gen build inside a privileged container
|
||||
# Use docker cp (not volume mounts) because of DooD path mapping
|
||||
docker run --privileged --name tipote-build tipote-builder
|
||||
|
||||
# Extract the output image
|
||||
mkdir -p output
|
||||
docker cp tipote-build:/output/. output/ 2>/dev/null || true
|
||||
|
||||
# Fallback: check pi-gen deploy dir directly
|
||||
if [ -z "$(ls output/*.img* 2>/dev/null)" ]; then
|
||||
docker cp tipote-build:/build/pi-gen/deploy/. output/ 2>/dev/null || true
|
||||
fi
|
||||
|
||||
docker rm tipote-build
|
||||
|
||||
echo "=== Build output ==="
|
||||
ls -lh output/ || echo "No output found"
|
||||
|
||||
- name: Determine version
|
||||
id: version
|
||||
run: |
|
||||
if [[ "$GITHUB_REF" == refs/tags/v* ]]; then
|
||||
VERSION="${GITHUB_REF#refs/tags/v}"
|
||||
else
|
||||
VERSION="dev-$(echo $GITHUB_SHA | cut -c1-7)"
|
||||
fi
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Prepare final image
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
|
||||
IMG_FILE=$(find output -name "*.img.xz" -o -name "*.img" 2>/dev/null | head -1)
|
||||
|
||||
if [ -z "$IMG_FILE" ]; then
|
||||
echo "ERROR: No image found in output/"
|
||||
ls -laR output/ || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BASENAME="tipote-${VERSION}.img"
|
||||
|
||||
if [[ "$IMG_FILE" == *.img.xz ]]; then
|
||||
mv "$IMG_FILE" "output/${BASENAME}.xz"
|
||||
else
|
||||
mv "$IMG_FILE" "output/$BASENAME"
|
||||
xz -T0 -6 "output/$BASENAME"
|
||||
fi
|
||||
|
||||
ls -lh output/
|
||||
echo "IMAGE_FILE=${BASENAME}.xz" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Upload to Gitea release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
GITEA_URL="${{ github.server_url }}"
|
||||
REPO="${{ github.repository }}"
|
||||
|
||||
# Create release
|
||||
curl -sf -X POST "${GITEA_URL}/api/v1/repos/${REPO}/releases" \
|
||||
-H "Authorization: token ${{ secrets.REGISTRY_PASSWORD }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"tag_name\": \"v${VERSION}\", \"name\": \"Ti-Pote v${VERSION}\", \"body\": \"SD image for Raspberry Pi Zero 2W\"}" \
|
||||
-o /tmp/release.json
|
||||
|
||||
RELEASE_ID=$(jq -r '.id' /tmp/release.json)
|
||||
echo "Release ID: $RELEASE_ID"
|
||||
|
||||
# Upload image as attachment
|
||||
curl -sf -X POST "${GITEA_URL}/api/v1/repos/${REPO}/releases/${RELEASE_ID}/assets?name=${IMAGE_FILE}" \
|
||||
-H "Authorization: token ${{ secrets.REGISTRY_PASSWORD }}" \
|
||||
-F "attachment=@output/${IMAGE_FILE}" \
|
||||
-o /tmp/asset.json
|
||||
|
||||
echo "Download URL: $(jq -r '.browser_download_url' /tmp/asset.json)"
|
||||
|
||||
- name: Summary (dev builds)
|
||||
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
||||
run: |
|
||||
echo "=== SD Image built successfully ==="
|
||||
echo "File: output/${IMAGE_FILE}"
|
||||
ls -lh output/
|
||||
echo ""
|
||||
echo "To create a release, push a tag: git tag v0.1.0 && git push origin v0.1.0"
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@ -5,6 +5,9 @@ node_modules/
|
||||
dist/
|
||||
build/
|
||||
.next/
|
||||
*.tsbuildinfo
|
||||
vite.config.js
|
||||
vite.config.d.ts
|
||||
|
||||
# Environment
|
||||
.env
|
||||
@ -48,3 +51,7 @@ pi-snapshot/
|
||||
|
||||
|
||||
.pio/
|
||||
|
||||
# Pi-gen
|
||||
tools/pi-gen-tipote/output/
|
||||
tools/pi-gen-tipote/pi-gen/
|
||||
@ -27,7 +27,7 @@
|
||||
"pino": "^9.6.0",
|
||||
"pino-pretty": "^13.0.0",
|
||||
"serialport": "^12.0.0",
|
||||
"sd-notify": "^3.0.2"
|
||||
"sd-notify": "^2.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.8.3",
|
||||
|
||||
46
pnpm-lock.yaml
generated
46
pnpm-lock.yaml
generated
@ -28,6 +28,9 @@ importers:
|
||||
'@nestjs/core':
|
||||
specifier: ^11.1.17
|
||||
version: 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(@nestjs/websockets@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||
'@nestjs/event-emitter':
|
||||
specifier: ^3.0.0
|
||||
version: 3.0.1(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)
|
||||
'@nestjs/jwt':
|
||||
specifier: ^11.0.2
|
||||
version: 11.0.2(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))
|
||||
@ -210,6 +213,9 @@ importers:
|
||||
pino-pretty:
|
||||
specifier: ^13.0.0
|
||||
version: 13.1.3
|
||||
sd-notify:
|
||||
specifier: ^2.8.0
|
||||
version: 2.8.0
|
||||
serialport:
|
||||
specifier: ^12.0.0
|
||||
version: 12.0.0
|
||||
@ -239,6 +245,8 @@ importers:
|
||||
specifier: ^3.2.1
|
||||
version: 3.2.4(@types/node@22.19.15)(lightningcss@1.32.0)(terser@5.46.1)
|
||||
|
||||
apps/robot-client-pi: {}
|
||||
|
||||
apps/simulator:
|
||||
dependencies:
|
||||
react:
|
||||
@ -1260,6 +1268,12 @@ packages:
|
||||
'@nestjs/websockets':
|
||||
optional: true
|
||||
|
||||
'@nestjs/event-emitter@3.0.1':
|
||||
resolution: {integrity: sha512-0Ln/x+7xkU6AJFOcQI9tIhUMXVF7D5itiaQGOyJbXtlAfAIt8gzDdJm+Im7cFzKoWkiW5nCXCPh6GSvdQd/3Dw==}
|
||||
peerDependencies:
|
||||
'@nestjs/common': ^10.0.0 || ^11.0.0
|
||||
'@nestjs/core': ^10.0.0 || ^11.0.0
|
||||
|
||||
'@nestjs/jwt@11.0.2':
|
||||
resolution: {integrity: sha512-rK8aE/3/Ma45gAWfCksAXUNbOoSOUudU0Kn3rT39htPF7wsYXtKfjALKeKKJbFrIWbLjsbqfXX5bIJNvgBugGA==}
|
||||
peerDependencies:
|
||||
@ -2479,6 +2493,9 @@ packages:
|
||||
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
bindings@1.5.0:
|
||||
resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
|
||||
|
||||
bl@4.1.0:
|
||||
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
|
||||
|
||||
@ -3055,6 +3072,9 @@ packages:
|
||||
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
eventemitter2@6.4.9:
|
||||
resolution: {integrity: sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==}
|
||||
|
||||
events@3.3.0:
|
||||
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
|
||||
engines: {node: '>=0.8.x'}
|
||||
@ -3157,6 +3177,9 @@ packages:
|
||||
resolution: {integrity: sha512-DLkUvGwep3poOV2wpzbHCOnSKGk1LzyXTv+aHFgN2VFl96wnp8YA9YjO2qPzg5PuL8q/SW9Pdi6WTkYOIh995w==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
file-uri-to-path@1.0.0:
|
||||
resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
|
||||
|
||||
fill-range@7.1.1:
|
||||
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
|
||||
engines: {node: '>=8'}
|
||||
@ -4650,6 +4673,11 @@ packages:
|
||||
resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==}
|
||||
engines: {node: '>= 10.13.0'}
|
||||
|
||||
sd-notify@2.8.0:
|
||||
resolution: {integrity: sha512-e+D1v0Y6UzmqXcPlaTkHk1QMdqk36mF/jIYv5gwry/N2Tb8/UNnpfG6ktGLpeBOR6TCC5hPKgqA+0hTl9sm2tA==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
os: [linux, darwin, win32]
|
||||
|
||||
section-matter@1.0.0:
|
||||
resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==}
|
||||
engines: {node: '>=4'}
|
||||
@ -6598,6 +6626,12 @@ snapshots:
|
||||
'@nestjs/platform-express': 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)
|
||||
'@nestjs/websockets': 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)(@nestjs/platform-socket.io@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||
|
||||
'@nestjs/event-emitter@3.0.1(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.17)':
|
||||
dependencies:
|
||||
'@nestjs/common': 11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||
'@nestjs/core': 11.1.17(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.17)(@nestjs/websockets@11.1.17)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||
eventemitter2: 6.4.9
|
||||
|
||||
'@nestjs/jwt@11.0.2(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))':
|
||||
dependencies:
|
||||
'@nestjs/common': 11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||
@ -7700,6 +7734,10 @@ snapshots:
|
||||
|
||||
binary-extensions@2.3.0: {}
|
||||
|
||||
bindings@1.5.0:
|
||||
dependencies:
|
||||
file-uri-to-path: 1.0.0
|
||||
|
||||
bl@4.1.0:
|
||||
dependencies:
|
||||
buffer: 5.7.1
|
||||
@ -8307,6 +8345,8 @@ snapshots:
|
||||
|
||||
event-target-shim@5.0.1: {}
|
||||
|
||||
eventemitter2@6.4.9: {}
|
||||
|
||||
events@3.3.0: {}
|
||||
|
||||
eventsource-parser@3.0.6: {}
|
||||
@ -8484,6 +8524,8 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
file-uri-to-path@1.0.0: {}
|
||||
|
||||
fill-range@7.1.1:
|
||||
dependencies:
|
||||
to-regex-range: 5.0.1
|
||||
@ -10077,6 +10119,10 @@ snapshots:
|
||||
ajv-formats: 2.1.1(ajv@8.18.0)
|
||||
ajv-keywords: 5.1.0(ajv@8.18.0)
|
||||
|
||||
sd-notify@2.8.0:
|
||||
dependencies:
|
||||
bindings: 1.5.0
|
||||
|
||||
section-matter@1.0.0:
|
||||
dependencies:
|
||||
extend-shallow: 2.0.1
|
||||
|
||||
33
tools/pi-gen-tipote/Dockerfile
Normal file
33
tools/pi-gen-tipote/Dockerfile
Normal file
@ -0,0 +1,33 @@
|
||||
FROM ubuntu:24.04
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Install pi-gen dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
git coreutils quilt parted \
|
||||
debootstrap zerofree zip dosfstools \
|
||||
libarchive-tools libcap2-bin grep rsync xz-utils \
|
||||
file binfmt-support qemu-user-static \
|
||||
pigz arch-test \
|
||||
curl bc xxd kmod \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
# Clone pi-gen
|
||||
RUN git clone --depth 1 https://github.com/RPi-Distro/pi-gen.git /build/pi-gen
|
||||
|
||||
# Copy config and stage
|
||||
COPY config /build/pi-gen/config
|
||||
COPY stage-tipote/ /build/pi-gen/stage-tipote/
|
||||
|
||||
# Skip stage2 image export (we export from stage-tipote)
|
||||
RUN touch /build/pi-gen/stage2/SKIP_IMAGES
|
||||
|
||||
# Patch pi-gen for Docker Desktop compatibility:
|
||||
# 1. Remove setarch linux32 calls (fails on Docker Desktop)
|
||||
# 2. Remove qemu-user-binfmt from dependency check (conflicts with qemu-user-static)
|
||||
RUN find /build/pi-gen -name "*.sh" -exec sed -i 's/setarch linux32 //g' {} + 2>/dev/null || true \
|
||||
&& sed -i '/qemu-user-binfmt/d' /build/pi-gen/depends
|
||||
|
||||
CMD ["bash", "-c", "cd /build/pi-gen && ./build.sh && mkdir -p /output && cp deploy/*.img.xz /output/ 2>/dev/null || cp deploy/*.img /output/ 2>/dev/null && ls -lh /output/"]
|
||||
@ -3,14 +3,15 @@ set -euo pipefail
|
||||
|
||||
# ──────────────────────────────────────────────────────
|
||||
# Ti-Pote SD Image Builder
|
||||
# Builds a flashable .img using pi-gen (Docker mode)
|
||||
# Builds a flashable .img using pi-gen in a Docker container.
|
||||
# Works on macOS (Docker Desktop) and Linux.
|
||||
# ──────────────────────────────────────────────────────
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
ROBOT_CLIENT_DIR="$REPO_ROOT/apps/robot-client"
|
||||
STAGE_FILES="$SCRIPT_DIR/stage-tipote/01-install-tipote/files"
|
||||
PI_GEN_DIR="$SCRIPT_DIR/pi-gen"
|
||||
OUTPUT_DIR="$SCRIPT_DIR/output"
|
||||
|
||||
echo "╔══════════════════════════════════════╗"
|
||||
echo "║ Ti-Pote SD Image Builder ║"
|
||||
@ -19,73 +20,59 @@ echo "╚═══════════════════════
|
||||
# ── Step 1: Build robot-client ──
|
||||
|
||||
echo ""
|
||||
echo "▸ Building robot-client..."
|
||||
echo "▸ [1/3] Building robot-client..."
|
||||
cd "$REPO_ROOT"
|
||||
pnpm --filter @ti-pote/robot-client build
|
||||
|
||||
# ── Step 2: Prepare files for pi-gen stage ──
|
||||
# ── Step 2: Prepare stage files ──
|
||||
|
||||
echo "▸ Preparing stage files..."
|
||||
echo "▸ [2/3] Preparing stage files..."
|
||||
rm -rf "$STAGE_FILES"
|
||||
mkdir -p "$STAGE_FILES"
|
||||
|
||||
# Copy built dist
|
||||
cp -r "$ROBOT_CLIENT_DIR/dist" "$STAGE_FILES/dist"
|
||||
cp -r "$ROBOT_CLIENT_DIR/dist" "$STAGE_FILES/dist"
|
||||
cp "$ROBOT_CLIENT_DIR/package.json" "$STAGE_FILES/package.json"
|
||||
cp "$ROBOT_CLIENT_DIR/deploy/tipote.service" "$STAGE_FILES/tipote.service"
|
||||
cp "$ROBOT_CLIENT_DIR/deploy/journald-tipote.conf" "$STAGE_FILES/journald-tipote.conf"
|
||||
|
||||
# Copy package.json (for npm install --omit=dev in chroot)
|
||||
cp "$ROBOT_CLIENT_DIR/package.json" "$STAGE_FILES/package.json"
|
||||
# ── Step 3: Build image in Docker ──
|
||||
|
||||
# Copy deploy configs
|
||||
cp "$ROBOT_CLIENT_DIR/deploy/tipote.service" "$STAGE_FILES/tipote.service"
|
||||
cp "$ROBOT_CLIENT_DIR/deploy/journald-tipote.conf" "$STAGE_FILES/journald-tipote.conf"
|
||||
echo "▸ [3/3] Building SD image in Docker (this takes 30-60 min)..."
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
# ── Step 3: Clone or update pi-gen ──
|
||||
# Register QEMU ARM interpreters in Docker Desktop's VM
|
||||
echo " → Registering ARM emulation..."
|
||||
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes 2>/dev/null || true
|
||||
|
||||
if [ ! -d "$PI_GEN_DIR" ]; then
|
||||
echo "▸ Cloning pi-gen..."
|
||||
git clone --depth 1 https://github.com/RPi-Distro/pi-gen.git "$PI_GEN_DIR"
|
||||
else
|
||||
echo "▸ pi-gen already cloned"
|
||||
fi
|
||||
# Build the builder image (force amd64 — pi-gen cross-compiles to ARM from x86)
|
||||
docker build --platform linux/amd64 -t tipote-builder .
|
||||
|
||||
# ── Step 4: Configure pi-gen ──
|
||||
|
||||
echo "▸ Configuring pi-gen..."
|
||||
cp "$SCRIPT_DIR/config" "$PI_GEN_DIR/config"
|
||||
|
||||
# Link our custom stage
|
||||
ln -sfn "$SCRIPT_DIR/stage-tipote" "$PI_GEN_DIR/stage-tipote"
|
||||
|
||||
# Skip stages 3-5 (desktop, apps — we only want Lite + our stage)
|
||||
touch "$PI_GEN_DIR/stage3/SKIP" "$PI_GEN_DIR/stage4/SKIP" "$PI_GEN_DIR/stage5/SKIP"
|
||||
touch "$PI_GEN_DIR/stage3/SKIP_IMAGES" "$PI_GEN_DIR/stage4/SKIP_IMAGES" "$PI_GEN_DIR/stage5/SKIP_IMAGES"
|
||||
|
||||
# Don't export images from stage2 (we export from stage-tipote)
|
||||
touch "$PI_GEN_DIR/stage2/SKIP_IMAGES"
|
||||
|
||||
# ── Step 5: Build image ──
|
||||
|
||||
echo "▸ Building image (this will take a while)..."
|
||||
cd "$PI_GEN_DIR"
|
||||
./build-docker.sh
|
||||
|
||||
# ── Step 6: Copy result ──
|
||||
|
||||
OUTPUT_DIR="$SCRIPT_DIR/output"
|
||||
# Run the build — mount output dir to retrieve the .img
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
docker run --rm --privileged \
|
||||
--platform linux/amd64 \
|
||||
-v /proc/sys/fs/binfmt_misc:/proc/sys/fs/binfmt_misc:rw \
|
||||
-v "$OUTPUT_DIR:/output" \
|
||||
tipote-builder
|
||||
|
||||
IMG_FILE=$(find "$PI_GEN_DIR/deploy" -name "*.img.xz" -type f | head -1)
|
||||
# ── Done ──
|
||||
|
||||
IMG_FILE=$(find "$OUTPUT_DIR" -name "*.img*" -type f | head -1)
|
||||
|
||||
if [ -n "$IMG_FILE" ]; then
|
||||
cp "$IMG_FILE" "$OUTPUT_DIR/"
|
||||
BASENAME=$(basename "$IMG_FILE")
|
||||
echo ""
|
||||
echo "════════════════════════════════════════"
|
||||
echo " Image ready: output/$BASENAME"
|
||||
echo " Flash with: xzcat output/$BASENAME | sudo dd of=/dev/sdX bs=4M status=progress"
|
||||
echo " Or use Balena Etcher"
|
||||
SIZE=$(ls -lh "$IMG_FILE" | awk '{print $5}')
|
||||
echo " Size: $SIZE"
|
||||
echo ""
|
||||
echo " Flash with:"
|
||||
echo " xzcat output/$BASENAME | sudo dd of=/dev/sdX bs=4M status=progress"
|
||||
echo " Or drag into Balena Etcher / Raspberry Pi Imager"
|
||||
echo "════════════════════════════════════════"
|
||||
else
|
||||
echo "⚠ No image found in pi-gen/deploy/ — check build logs"
|
||||
echo ""
|
||||
echo "⚠ No image found — check Docker logs above"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@ -9,7 +9,5 @@ KEYBOARD_LAYOUT="French"
|
||||
TIMEZONE_DEFAULT=Europe/Paris
|
||||
ENABLE_SSH=1
|
||||
|
||||
# Skip stages we don't need (desktop, X11, etc.)
|
||||
SKIP_STAGE3=1
|
||||
SKIP_STAGE4=1
|
||||
SKIP_STAGE5=1
|
||||
# Only run base stages + our custom stage (skip desktop/apps)
|
||||
STAGE_LIST="stage0 stage1 stage2 stage-tipote"
|
||||
|
||||
147
tools/pi-gen-tipote/deploy.sh
Executable file
147
tools/pi-gen-tipote/deploy.sh
Executable file
@ -0,0 +1,147 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# ──────────────────────────────────────────────────────
|
||||
# Ti-Pote Remote Deployment Script
|
||||
#
|
||||
# Deploys Ti-Pote to a fresh Raspberry Pi OS Lite.
|
||||
# Run from your dev machine (macOS/Linux).
|
||||
#
|
||||
# Prerequisites:
|
||||
# 1. Flash Raspberry Pi OS Lite (64-bit, Debian trixie)
|
||||
# using Raspberry Pi Imager
|
||||
# 2. Enable SSH + set user "tipote" in Imager settings
|
||||
# 3. Connect Pi to your local network (Ethernet or WiFi)
|
||||
#
|
||||
# Usage:
|
||||
# ./deploy.sh <pi-ip-or-hostname>
|
||||
# ./deploy.sh 192.168.1.124
|
||||
# ./deploy.sh ti-pote.local
|
||||
# ──────────────────────────────────────────────────────
|
||||
|
||||
if [ $# -lt 1 ]; then
|
||||
echo "Usage: $0 <pi-host> [pi-user]"
|
||||
echo " e.g. $0 192.168.1.124"
|
||||
echo " e.g. $0 ti-pote.local tipote"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PI_HOST="$1"
|
||||
PI_USER="${2:-tipote}"
|
||||
SSH="ssh ${PI_USER}@${PI_HOST}"
|
||||
SCP="scp -r"
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
ROBOT_CLIENT_DIR="$REPO_ROOT/apps/robot-client"
|
||||
|
||||
echo "╔══════════════════════════════════════╗"
|
||||
echo "║ Ti-Pote Remote Deployment ║"
|
||||
echo "╠══════════════════════════════════════╣"
|
||||
echo "║ Target: ${PI_USER}@${PI_HOST}"
|
||||
echo "╚══════════════════════════════════════╝"
|
||||
|
||||
# ── Step 1: Build robot-client ──
|
||||
|
||||
echo ""
|
||||
echo "▸ [1/7] Building robot-client..."
|
||||
cd "$REPO_ROOT"
|
||||
pnpm --filter @ti-pote/robot-client build
|
||||
|
||||
# ── Step 2: Test SSH connectivity ──
|
||||
|
||||
echo "▸ [2/7] Testing SSH connection..."
|
||||
$SSH "echo 'SSH OK — $(uname -m) — $(cat /etc/os-release | grep PRETTY_NAME | cut -d= -f2)'" || {
|
||||
echo "✗ Cannot reach ${PI_USER}@${PI_HOST} via SSH"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# ── Step 3: Install system dependencies ──
|
||||
|
||||
echo "▸ [3/7] Installing system dependencies..."
|
||||
$SSH "sudo apt-get update -qq && sudo apt-get install -y -qq \
|
||||
python3 python3-venv python3-pip \
|
||||
portaudio19-dev libatlas-base-dev alsa-utils \
|
||||
network-manager curl"
|
||||
|
||||
# Install Node.js 22 if not present
|
||||
$SSH "node --version 2>/dev/null | grep -q 'v22' || {
|
||||
echo 'Installing Node.js 22...'
|
||||
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo bash -
|
||||
sudo apt-get install -y -qq nodejs
|
||||
}"
|
||||
|
||||
# ── Step 4: Create tipote directory structure ──
|
||||
|
||||
echo "▸ [4/7] Setting up /opt/tipote..."
|
||||
$SSH "sudo mkdir -p /opt/tipote && sudo chown ${PI_USER}:${PI_USER} /opt/tipote"
|
||||
|
||||
# ── Step 5: Deploy files ──
|
||||
|
||||
echo "▸ [5/7] Uploading files..."
|
||||
|
||||
# Sync dist/ and package.json
|
||||
rsync -az --delete \
|
||||
"$ROBOT_CLIENT_DIR/dist/" \
|
||||
"${PI_USER}@${PI_HOST}:/opt/tipote/dist/"
|
||||
|
||||
$SCP "$ROBOT_CLIENT_DIR/package.json" "${PI_USER}@${PI_HOST}:/opt/tipote/package.json"
|
||||
|
||||
# Install production deps on Pi
|
||||
echo "▸ [5/7] Installing production dependencies on Pi..."
|
||||
$SSH "cd /opt/tipote && npm install --omit=dev 2>/dev/null || npm install --production"
|
||||
|
||||
# ── Step 6: Setup Python venv + openwakeword ──
|
||||
|
||||
echo "▸ [6/7] Setting up Python venv..."
|
||||
$SSH "test -d /opt/tipote/.venv || {
|
||||
python3 -m venv /opt/tipote/.venv
|
||||
/opt/tipote/.venv/bin/pip install --upgrade pip
|
||||
/opt/tipote/.venv/bin/pip install openwakeword
|
||||
}"
|
||||
|
||||
# ── Step 7: Install systemd + configs ──
|
||||
|
||||
echo "▸ [7/7] Installing systemd service..."
|
||||
|
||||
# .env (only if not exists — don't overwrite user config)
|
||||
$SSH "test -f /opt/tipote/.env || cat > /opt/tipote/.env << 'ENVEOF'
|
||||
ROBOT_MODE=physical
|
||||
CLOUD_URL=https://api.tipote.dev
|
||||
AUDIO_BACKEND=esp32
|
||||
TRIGGER_MODE=wakeword
|
||||
HARDWARE_SERIAL_ENABLED=true
|
||||
HARDWARE_SERIAL_PORT=/dev/serial0
|
||||
HARDWARE_SERIAL_BAUD_RATE=921600
|
||||
WAKEWORD_PYTHON_PATH=/opt/tipote/.venv/bin/python3
|
||||
NODE_ENV=production
|
||||
LOG_LEVEL=info
|
||||
ENVEOF
|
||||
chmod 600 /opt/tipote/.env"
|
||||
|
||||
# systemd service
|
||||
$SCP "$ROBOT_CLIENT_DIR/deploy/tipote.service" "${PI_USER}@${PI_HOST}:/tmp/tipote.service"
|
||||
$SSH "sudo mv /tmp/tipote.service /etc/systemd/system/tipote.service && sudo systemctl daemon-reload && sudo systemctl enable tipote"
|
||||
|
||||
# journald config
|
||||
$SCP "$ROBOT_CLIENT_DIR/deploy/journald-tipote.conf" "${PI_USER}@${PI_HOST}:/tmp/journald-tipote.conf"
|
||||
$SSH "sudo mkdir -p /etc/systemd/journald.conf.d && sudo mv /tmp/journald-tipote.conf /etc/systemd/journald.conf.d/tipote.conf"
|
||||
|
||||
# CLI symlink
|
||||
$SSH "sudo ln -sf /opt/tipote/dist/cli.js /usr/local/bin/tipote && sudo chmod +x /opt/tipote/dist/cli.js"
|
||||
|
||||
# Permissions
|
||||
$SSH "mkdir -p /opt/tipote/.tipote && chmod 700 /opt/tipote/.tipote"
|
||||
|
||||
# Add user to dialout + audio groups (for serial + audio)
|
||||
$SSH "sudo usermod -aG dialout,audio ${PI_USER} 2>/dev/null || true"
|
||||
|
||||
echo ""
|
||||
echo "════════════════════════════════════════"
|
||||
echo " Deployment complete!"
|
||||
echo ""
|
||||
echo " Start: ssh ${PI_USER}@${PI_HOST} 'sudo systemctl start tipote'"
|
||||
echo " Status: ssh ${PI_USER}@${PI_HOST} 'sudo systemctl status tipote'"
|
||||
echo " Logs: ssh ${PI_USER}@${PI_HOST} 'journalctl -u tipote -f'"
|
||||
echo " CLI: ssh ${PI_USER}@${PI_HOST} 'tipote doctor'"
|
||||
echo "════════════════════════════════════════"
|
||||
Loading…
x
Reference in New Issue
Block a user