diff --git a/.gitea/workflows/build-and-test.yml b/.gitea/workflows/build-and-test.yml new file mode 100644 index 0000000..a33b174 --- /dev/null +++ b/.gitea/workflows/build-and-test.yml @@ -0,0 +1,167 @@ +name: '🚴 Build and Test BikeControl' + +on: + push: + branches: + - master + paths-ignore: + - '**/*.md' + pull_request: + branches: + - master + paths-ignore: + - '**/*.md' + workflow_dispatch: + +env: + RUST_BACKTRACE: 1 + CARGO_TERM_COLOR: always + +jobs: + test: + name: Workspace tests + runs-on: linux/amd64 + container: + image: gitea.tourolle.paris/dtourolle/bikecontrol-builder:latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Cache Rust dependencies + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-host-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-host- + + - name: Cache Node dependencies + uses: actions/cache@v3 + with: + path: ui/node_modules + key: ${{ runner.os }}-npm-${{ hashFiles('ui/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-npm- + + - name: Install frontend dependencies + run: npm --prefix ui ci + + # Advisory, not a gate. The tree predates this workflow and `cargo fmt + # --all` currently rewrites ~2000 lines across 28 files; making that a + # blocking check would mean landing a repo-wide reformat as a side effect + # of adding CI. Run `cargo fmt --all` once, in its own commit, then drop + # the `continue-on-error` below and this becomes a real gate. + - name: Check formatting (advisory) + run: cargo fmt --all --check + continue-on-error: true + + - name: Clippy + run: cargo clippy --workspace --all-targets -- -D warnings + + - name: Run workspace tests + run: cargo test --workspace --locked + + - name: Type-check the frontend + run: npm --prefix ui run check + + - name: Frontend tests + run: npm --prefix ui test + + - name: Build the frontend + run: npm --prefix ui run build + + # Per-commit Android compile check. + # + # This deliberately does NOT build an APK. The full signed build runs only on + # tags (build-release.yml) and takes ~15 min; `cargo check` for the Android + # target is ~1 min and catches everything that actually breaks here — the JNI + # shim in src-tauri/src/android.rs, btleplug's droidplug backend, and any + # desktop-only API that has crept into a shared crate (NFR-5). + android-check: + name: Android compile check + runs-on: linux/amd64 + needs: test + container: + image: gitea.tourolle.paris/dtourolle/bikecontrol-builder:latest + env: + ANDROID_HOME: /opt/android-sdk + ANDROID_SDK_ROOT: /opt/android-sdk + NDK_HOME: /opt/android-sdk/ndk/27.0.11902837 + ANDROID_NDK_HOME: /opt/android-sdk/ndk/27.0.11902837 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Cache Rust dependencies + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-android-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-android- + + # Cheap invariants that a compiler cannot see. Both failures are silent: + # the app builds, installs, launches, and then finds no trainer. + - name: Check the Android source layout and the JNI symbol + run: | + set -e + # gen/ is generated and must stay untracked, or a `tauri android init` + # turns into a confusing diff and the sync script becomes optional. + if git ls-files --error-unmatch src-tauri/gen >/dev/null 2>&1; then + echo "❌ src-tauri/gen is tracked. It is generated — untrack it and" + echo " keep hand-written sources in src-tauri/android/." + exit 1 + fi + + KT=src-tauri/android/src/main/java/paris/tourolle/bikecontrol/MainActivity.kt + [ -f "$KT" ] || { echo "❌ $KT is missing"; exit 1; } + + # The JNI symbol in android.rs is matched by the *runtime*, by name. + # Rename the Kotlin package or the method and nothing fails to + # compile — btleplug simply never gets initialised. + SYM=$(sed -n 's/.*pub extern "system" fn \(Java_[A-Za-z0-9_]*\).*/\1/p' src-tauri/src/android.rs) + PKG=$(sed -n 's/^package \(.*\)$/\1/p' "$KT" | tr -d '\r') + METHOD=$(sed -n 's/.*external fun \([A-Za-z0-9_]*\)().*/\1/p' "$KT") + EXPECTED="Java_$(echo "$PKG" | tr '.' '_')_MainActivity_$METHOD" + if [ "$SYM" != "$EXPECTED" ]; then + echo "❌ JNI symbol mismatch:" + echo " android.rs exports: $SYM" + echo " MainActivity needs: $EXPECTED" + exit 1 + fi + echo "✅ JNI symbol $SYM matches $PKG.MainActivity.$METHOD" + + - name: Cargo check (aarch64-linux-android) + run: | + TC="$NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin" + export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="$TC/aarch64-linux-android24-clang" + export CC_aarch64_linux_android="$TC/aarch64-linux-android24-clang" + export AR_aarch64_linux_android="$TC/llvm-ar" + cargo check -p bikecontrol-app --lib --target aarch64-linux-android --locked + + # btleplug's Android backend is half Java. That half is copied out of the + # crate sources at the version Cargo.lock pins, so a bump to btleplug that + # moved or renamed those sources must fail here — loudly, in a one-minute + # job — rather than in a fifteen-minute release build, or at the first + # scan on a phone. + - name: Verify the BLE Java backend can be sourced + run: | + set -e + cargo fetch --target aarch64-linux-android + for crate in btleplug jni-utils; do + VER=$(awk -v pkg="name = \"$crate\"" \ + '$0 == pkg { f = 1; next } f && /^version = / { gsub(/[",]/, "", $3); print $3; exit }' Cargo.lock) + DIR=$(ls -d "${CARGO_HOME:-$HOME/.cargo}"/registry/src/*/"$crate-$VER" 2>/dev/null | head -1) + [ -n "$DIR" ] || { echo "❌ $crate $VER sources not in the registry"; exit 1; } + echo "✅ $crate $VER at $DIR" + done + test -d "$(ls -d "${CARGO_HOME:-$HOME/.cargo}"/registry/src/*/btleplug-*/src/droidplug/java/src/main/java/com | head -1)" + echo "✅ droidplug Java sources present" diff --git a/.gitea/workflows/build-release.yml b/.gitea/workflows/build-release.yml new file mode 100644 index 0000000..fb8ec88 --- /dev/null +++ b/.gitea/workflows/build-release.yml @@ -0,0 +1,322 @@ +name: Build & Release + +on: + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + version: + description: 'Version to build (e.g., v0.2.0)' + required: false + +env: + RUST_BACKTRACE: 1 + CARGO_TERM_COLOR: always + +jobs: + test: + name: Run tests + runs-on: linux/amd64 + container: + image: gitea.tourolle.paris/dtourolle/bikecontrol-builder:latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Cache Rust dependencies + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-host-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-host- + + - name: Install frontend dependencies + run: npm --prefix ui ci + + - name: Run workspace tests + run: cargo test --workspace --locked + + - name: Frontend tests + run: npm --prefix ui test + + - name: Type-check the frontend + run: npm --prefix ui run check + + build-linux: + name: Build Linux (deb + AppImage) + runs-on: linux/amd64 + needs: test + container: + image: gitea.tourolle.paris/dtourolle/bikecontrol-builder:latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Cache Rust dependencies + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-host-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-host- + + - name: Install frontend dependencies + run: npm --prefix ui ci + + - name: Set app version from tag + run: ./scripts/ci-set-version.sh + + - name: Build the desktop bundle + working-directory: src-tauri + run: cargo tauri build + + - name: Collect Linux artifacts + run: | + set -e + mkdir -p dist/linux + find target/release/bundle -type f \( -name '*.deb' -o -name '*.AppImage' -o -name '*.rpm' \) \ + -exec cp -v {} dist/linux/ \; + # A release that quietly ships nothing is worse than a failed build. + [ -n "$(ls -A dist/linux)" ] || { echo "❌ No Linux bundle produced"; exit 1; } + ls -lah dist/linux/ + + - name: Upload Linux build artifact + uses: actions/upload-artifact@v3 + with: + name: bikecontrol-linux + path: dist/linux/ + retention-days: 30 + + build-arch: + name: Build Arch package + runs-on: linux/amd64 + needs: test + # Arch-specific image: makepkg does not exist on the Ubuntu builder, and + # the package must be built against Arch's own webkit2gtk/gtk3. + container: + image: gitea.tourolle.paris/dtourolle/bikecontrol-arch-builder:latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + # scripts/build-arch.sh derives the version from git describe. + fetch-depth: 0 + + - name: Build the package + run: | + set -e + # makepkg refuses to run as root, and the checkout is owned by the + # job's user, so hand the tree to the image's `builder` account. + chown -R builder:builder . + git config --global --add safe.directory "$PWD" + sudo -u builder git config --global --add safe.directory "$PWD" + sudo -u builder --preserve-env=OUTPUT_DIR \ + env OUTPUT_DIR="$PWD/dist/arch" ./scripts/build-arch.sh --no-install + ls -lah dist/arch/ + + - name: Upload Arch build artifact + uses: actions/upload-artifact@v3 + with: + name: bikecontrol-arch + path: dist/arch/ + retention-days: 30 + + build-android: + name: Build Android APK + runs-on: linux/amd64 + needs: test + container: + image: gitea.tourolle.paris/dtourolle/bikecontrol-builder:latest + env: + ANDROID_HOME: /opt/android-sdk + ANDROID_SDK_ROOT: /opt/android-sdk + NDK_HOME: /opt/android-sdk/ndk/27.0.11902837 + ANDROID_NDK_HOME: /opt/android-sdk/ndk/27.0.11902837 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Cache Rust dependencies + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-android-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-android- + + - name: Install frontend dependencies + run: npm --prefix ui ci + + - name: Set app version from tag + run: ./scripts/ci-set-version.sh + + - name: Initialise the Android project + working-directory: src-tauri + run: cargo tauri android init + + - name: Pin a monotonic Android versionCode + run: ./scripts/ci-android-version-code.sh + + # Manifest, MainActivity, gradle config, and btleplug's Java backend. + # `tauri android init` above regenerated gen/android and knows about none + # of it — without this step the APK builds and then finds no trainer. + - name: Sync custom Android sources + run: ./scripts/sync-android-sources.sh + + # gen/ is not tracked, so a Kotlin file that exists only there is one git + # has never seen and the next init will delete. Catch it before it ships. + - name: Verify every hand-written Android source is tracked + run: ./scripts/check-android-sources.sh + + - name: Write the signing keystore + run: | + echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > "$RUNNER_TEMP/bikecontrol-release.jks" + cat > src-tauri/gen/android/keystore.properties <> $GITHUB_OUTPUT + + - name: Download Linux artifacts + uses: actions/download-artifact@v3 + with: + name: bikecontrol-linux + path: artifacts/linux/ + + - name: Download Arch artifacts + uses: actions/download-artifact@v3 + with: + name: bikecontrol-arch + path: artifacts/arch/ + + - name: Download Android artifacts + uses: actions/download-artifact@v3 + with: + name: bikecontrol-android + path: artifacts/android/ + + - name: Prepare release notes + run: | + VERSION="${{ steps.tag_name.outputs.VERSION }}" + cat > release_notes.md <<'EOF' + ## Downloads + + | Platform | File | Install | + |---|---|---| + | Linux (any) | `*.AppImage` | `chmod +x BikeControl*.AppImage && ./BikeControl*.AppImage` | + | Debian/Ubuntu | `*.deb` | `sudo dpkg -i bikecontrol*.deb` | + | Arch | `*.pkg.tar.zst` | `sudo pacman -U bikecontrol-*.pkg.tar.zst` | + | Android (arm64) | `bikecontrol-release.apk` | `adb install bikecontrol-release.apk`, or sideload | + + ## Requirements + + **Linux** — BlueZ running (`bluetoothd`), and a Bluetooth adapter with BLE. + + **Android** — 7.0 (API 24) or newer, and Bluetooth LE. The app asks for + the Bluetooth scan/connect permissions on first launch; on Android 11 + and older it asks for location instead, which is what the platform + required for a BLE scan at the time. + + A Zwift Click v2 must have been unlocked once in the free Zwift app — + see the README. + EOF + sed -i "1i # BikeControl $VERSION\n" release_notes.md + cat release_notes.md + + - name: Publish Gitea release & upload assets + env: + # A PAT is preferred; falls back to the auto-provided token. + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + AUTO_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -e + command -v jq >/dev/null || { echo "❌ jq is required on the runner"; exit 1; } + VERSION="${{ steps.tag_name.outputs.VERSION }}" + API="${GITHUB_SERVER_URL}/api/v1" + REPO="${GITHUB_REPOSITORY}" + TOKEN="${GITEA_TOKEN:-$AUTO_TOKEN}" + case "$VERSION" in *rc*|*beta*|*alpha*) PRE=true;; *) PRE=false;; esac + + PAYLOAD=$(jq -n \ + --arg tag "$VERSION" \ + --arg name "BikeControl $VERSION" \ + --rawfile body release_notes.md \ + --argjson pre "$PRE" \ + '{tag_name:$tag, name:$name, body:$body, draft:false, prerelease:$pre}') + + echo "đŸ“Ļ Creating release $VERSION on $REPO" + # Capture the status rather than using -f, so an existing release + # (409) is handled instead of dropping the assets on the floor. + HTTP=$(curl -sS -o resp.json -w '%{http_code}' -X POST "$API/repos/$REPO/releases" \ + -H "Authorization: token $TOKEN" \ + -H "Content-Type: application/json" \ + -d "$PAYLOAD") + if [ "$HTTP" = "201" ]; then + RELEASE_ID=$(jq -r '.id' resp.json) + elif [ "$HTTP" = "409" ]; then + echo "â„šī¸ Release $VERSION already exists; fetching its id to upload assets" + RELEASE_ID=$(curl -fsS "$API/repos/$REPO/releases/tags/$VERSION" \ + -H "Authorization: token $TOKEN" | jq -r '.id') + else + echo "❌ Failed to create release (HTTP $HTTP):"; cat resp.json; exit 1 + fi + + for f in artifacts/*/*; do + [ -f "$f" ] || continue + echo "âŦ†ī¸ Uploading $(basename "$f")" + curl -fsS -X POST \ + "$API/repos/$REPO/releases/$RELEASE_ID/assets?name=$(basename "$f")" \ + -H "Authorization: token $TOKEN" \ + -F "attachment=@$f" >/dev/null + done + echo "✅ Release $VERSION published with assets" diff --git a/Dockerfile.arch b/Dockerfile.arch new file mode 100644 index 0000000..965a1c4 --- /dev/null +++ b/Dockerfile.arch @@ -0,0 +1,38 @@ +# BikeControl Arch Linux package builder. +# +# Separate from Dockerfile.builder on purpose: makepkg is Arch-specific, so the +# .pkg.tar.zst cannot be produced from the Ubuntu image that builds everything +# else. This is an *environment* image — it carries no source, so the same tag +# serves both CI (which checks the repo out at run time) and local use. +# +# Build and push: +# docker build -f Dockerfile.arch -t gitea.tourolle.paris/dtourolle/bikecontrol-arch-builder:latest . +# docker push gitea.tourolle.paris/dtourolle/bikecontrol-arch-builder:latest +# +# Local one-shot build of the package: +# docker run --rm -v "$PWD:/app" -v "$PWD/dist:/out" \ +# gitea.tourolle.paris/dtourolle/bikecontrol-arch-builder:latest +FROM archlinux:latest + +# base-devel brings makepkg, fakeroot and the rest of the packaging toolchain. +# The remainder are the PKGBUILD's makedepends plus what Tauri and btleplug link +# against: webkit2gtk-4.1/gtk3 for the shell, and dbus for BlueZ. +RUN pacman -Syu --noconfirm \ + base-devel git sudo \ + rust nodejs npm \ + webkit2gtk-4.1 gtk3 libayatana-appindicator \ + libsoup3 pkgconf openssl dbus bluez-libs \ + && pacman -Scc --noconfirm + +# makepkg refuses to run as root. The passwordless sudo is for `makepkg -s`, +# which pacman-installs missing makedepends. +RUN useradd -m builder && \ + echo 'builder ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/builder + +WORKDIR /app +RUN mkdir -p /out && chown builder:builder /out +VOLUME ["/out"] + +# CI overrides this and runs the script itself (it must chown the checkout to +# `builder` first). The default is the local convenience path. +CMD ["bash", "-c", "chown -R builder:builder /app && sudo -u builder --preserve-env=OUTPUT_DIR OUTPUT_DIR=/out scripts/build-arch.sh --no-install"] diff --git a/Dockerfile.builder b/Dockerfile.builder new file mode 100644 index 0000000..a3a4307 --- /dev/null +++ b/Dockerfile.builder @@ -0,0 +1,104 @@ +# BikeControl builder image. +# +# One image that can do every job CI asks of it: +# - `cargo test` / `clippy` on the host target, which means the *Linux BLE* +# stack must be present: btleplug talks to BlueZ over D-Bus, so libdbus is a +# hard build dependency, not an optional extra. +# - the Tauri desktop bundle (deb / AppImage), which needs the webkit2gtk set. +# - the Android APK: SDK, NDK, JDK, and the three Android Rust targets. +# +# Build and push: +# docker build -f Dockerfile.builder -t gitea.tourolle.paris/dtourolle/bikecontrol-builder:latest . +# docker push gitea.tourolle.paris/dtourolle/bikecontrol-builder:latest +# or run scripts/build-builder-image.sh. + +FROM ubuntu:24.04 + +ENV DEBIAN_FRONTEND=noninteractive \ + ANDROID_HOME=/opt/android-sdk \ + ANDROID_SDK_ROOT=/opt/android-sdk \ + NDK_VERSION=27.0.11902837 \ + SDK_VERSION=36 \ + BUILD_TOOLS_VERSION=35.0.0 \ + RUST_BACKTRACE=1 \ + CARGO_HOME=/root/.cargo \ + PATH="/root/.cargo/bin:$PATH" + +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + curl \ + wget \ + git \ + ca-certificates \ + unzip \ + jq \ + file \ + pkg-config \ + openjdk-17-jdk-headless \ + libssl-dev \ + libclang-dev \ + llvm-dev \ + # BLE on the host target: btleplug's Linux backend is bluez-async over the + # `dbus` crate, which links against libdbus-1. Without this the workspace + # does not build at all — `crates/ble` is not optional. + libdbus-1-dev \ + # Tauri v2 desktop (needed for `cargo test`/`clippy` on the host target and + # for the deb/AppImage bundle). + libglib2.0-dev \ + libgtk-3-dev \ + libwebkit2gtk-4.1-dev \ + libjavascriptcoregtk-4.1-dev \ + libsoup-3.0-dev \ + librsvg2-dev \ + libayatana-appindicator3-dev \ + # AppImage bundling reaches for these at bundle time. + fuse3 \ + desktop-file-utils \ + && rm -rf /var/lib/apt/lists/* + +# Node 20 for the Vite/Svelte frontend. The UI is plain npm (ui/package-lock.json +# is the lockfile CI installs from), so no bun here. +RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \ + apt-get install -y --no-install-recommends nodejs && \ + rm -rf /var/lib/apt/lists/* + +# Rust, with every target the release job builds for. +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal && \ + . "$CARGO_HOME/env" && \ + rustup component add rustfmt clippy && \ + rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android + +# The Tauri CLI as a cargo subcommand. The frontend package.json deliberately +# does not carry @tauri-apps/cli — the shell is a cargo workspace member, so the +# CLI belongs to the toolchain, not to the UI's dependency tree. +RUN . "$CARGO_HOME/env" && cargo install tauri-cli --locked --version "^2" + +# --------------------------------------------------------------------------- +# Android SDK / NDK. +RUN mkdir -p "$ANDROID_HOME" /root/.android && \ + printf '### User Sources for `android` cmd line tool ###\ncount=0\n' > /root/.android/repositories.cfg && \ + wget -q https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip -O /tmp/cmdline-tools.zip && \ + unzip -q /tmp/cmdline-tools.zip -d "$ANDROID_HOME" && \ + rm /tmp/cmdline-tools.zip && \ + mkdir -p "$ANDROID_HOME/cmdline-tools/latest" && \ + mv "$ANDROID_HOME/cmdline-tools/cmdline-tools/"* "$ANDROID_HOME/cmdline-tools/latest/" && \ + rmdir "$ANDROID_HOME/cmdline-tools/cmdline-tools" + +# Licences up front so Gradle never blocks on a prompt in CI. +RUN yes | "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --sdk_root="$ANDROID_HOME" --licenses > /dev/null + +RUN "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --sdk_root="$ANDROID_HOME" \ + "platform-tools" \ + "platforms;android-$SDK_VERSION" \ + "build-tools;$BUILD_TOOLS_VERSION" \ + "ndk;$NDK_VERSION" \ + --channel=0 2>&1 | grep -v "Warning" || true + +ENV NDK_HOME=$ANDROID_HOME/ndk/$NDK_VERSION \ + ANDROID_NDK_HOME=$ANDROID_HOME/ndk/$NDK_VERSION \ + ANDROID_NDK_ROOT=$ANDROID_HOME/ndk/$NDK_VERSION \ + PATH="/opt/android-sdk/platform-tools:/opt/android-sdk/cmdline-tools/latest/bin:$PATH" + +WORKDIR /app + +ENTRYPOINT ["/bin/bash"] diff --git a/crates/core/src/gpx.rs b/crates/core/src/gpx.rs index 42f9287..fb24f79 100644 --- a/crates/core/src/gpx.rs +++ b/crates/core/src/gpx.rs @@ -191,7 +191,7 @@ pub fn haversine_m(a: TrackPoint, b: TrackPoint) -> f64 { /// 2. Resample elevation onto an even `resample_m` grid. Uneven GPS spacing /// otherwise weights a stationary cluster of fixes as heavily as a fast /// descent. -/// 2b. Reject *outliers* — single fixes metres away from their neighbours — +/// 2b. Reject *outliers* — single fixes metres away from their neighbours — /// before any averaging. A moving average does not remove an outlier, it /// smears it across the whole window, and the differentiation in step 4 /// then reads that smear as a sustained gradient. The commonest instance is diff --git a/scripts/build-builder-image.sh b/scripts/build-builder-image.sh new file mode 100755 index 0000000..ed0ae42 --- /dev/null +++ b/scripts/build-builder-image.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# Build (and optionally push) the CI builder images. +# +# scripts/build-builder-image.sh # build both, no push +# scripts/build-builder-image.sh --push # build both and push +# scripts/build-builder-image.sh --only arch # just the Arch image +# scripts/build-builder-image.sh --only main # just the Ubuntu image +# +# Two images, because the jobs genuinely need different distributions: +# main — Ubuntu: tests, Linux desktop bundle, Android APK (Dockerfile.builder) +# arch — Arch: the .pkg.tar.zst, which needs makepkg (Dockerfile.arch) +# +# The workflows in .gitea/workflows pin these by tag, so a change to either +# Dockerfile only reaches CI once this has run with --push. +set -euo pipefail + +REGISTRY="${REGISTRY:-gitea.tourolle.paris}" +NAMESPACE="${NAMESPACE:-$REGISTRY/dtourolle}" +TAG="${TAG:-latest}" + +PUSH=0 +ONLY=both +while [ $# -gt 0 ]; do + case "$1" in + --push) PUSH=1 ;; + --only) shift; ONLY="${1:-}" ;; + -h|--help) sed -n '2,16p' "${BASH_SOURCE[0]}"; exit 0 ;; + *) echo "❌ Unknown option: $1 (try --help)" >&2; exit 2 ;; + esac + shift +done + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT" + +build_one() { + local dockerfile="$1" image="$NAMESPACE/$2:$TAG" + echo "đŸŗ Building $image from $dockerfile" + docker build -f "$dockerfile" -t "$image" . + if [ "$PUSH" = 1 ]; then + echo "âŦ†ī¸ Pushing $image" + docker push "$image" + fi +} + +case "$ONLY" in + main) build_one Dockerfile.builder bikecontrol-builder ;; + arch) build_one Dockerfile.arch bikecontrol-arch-builder ;; + both) + build_one Dockerfile.builder bikecontrol-builder + build_one Dockerfile.arch bikecontrol-arch-builder + ;; + *) echo "❌ --only takes main, arch or both" >&2; exit 2 ;; +esac + +[ "$PUSH" = 1 ] || echo "â„šī¸ Not pushed. Re-run with --push when you are happy with it." diff --git a/scripts/ci-android-version-code.sh b/scripts/ci-android-version-code.sh new file mode 100755 index 0000000..c359e68 --- /dev/null +++ b/scripts/ci-android-version-code.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env sh +# Pin an explicit, monotonic Android versionCode. +# +# `tauri android init` derives one from the semver in a way that is not +# monotonic across a version series: 0.1.0 and 0.0.10 can collide, and Android +# refuses to install an APK whose versionCode is not greater than the installed +# one. A rider who cannot update is a rider who stops updating. +# +# code = 1000 + major * 10000 + minor * 100 + patch +# +# so 0.1.0 -> 1100, 0.1.3 -> 1103, 0.2.0 -> 1200, 1.0.0 -> 11000. Monotonic in +# semver order for any minor/patch below 100, which is well past where this +# project will ever get. +# +# The 1000 floor is not decoration: `tauri android init` writes versionCode=1000 +# for 0.1.0 today, so anyone carrying a locally-built APK already has that +# number installed. A bare major/minor/patch code would be 100 — a *downgrade*, +# which Android refuses outright. +# +# POSIX sh: the runner's /bin/sh is dash. No here-strings, no \s in sed. +set -e + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +PROPS="$ROOT/src-tauri/gen/android/app/tauri.properties" +CONF="$ROOT/src-tauri/tauri.conf.json" + +[ -f "$PROPS" ] || { echo "❌ $PROPS not found — run 'cargo tauri android init' first" >&2; exit 1; } + +VERSION=$(grep '"version"' "$CONF" | head -1 | sed -E 's/.*"version"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/') +MAJ=$(echo "$VERSION" | cut -d. -f1) +MIN=$(echo "$VERSION" | cut -d. -f2) +PAT=$(echo "$VERSION" | cut -d. -f3) +# Guard a malformed or short version so we can never emit code 0, which Android +# treats as "older than everything". +: "${MAJ:=0}" "${MIN:=0}" "${PAT:=0}" +CODE=$(( 1000 + MAJ * 10000 + MIN * 100 + PAT )) + +echo "version=$VERSION -> versionCode=$CODE" +if grep -q '^tauri.android.versionCode=' "$PROPS"; then + sed -i "s/^tauri.android.versionCode=.*/tauri.android.versionCode=$CODE/" "$PROPS" +else + echo "tauri.android.versionCode=$CODE" >> "$PROPS" +fi +cat "$PROPS" diff --git a/scripts/ci-set-version.sh b/scripts/ci-set-version.sh new file mode 100755 index 0000000..708b8dc --- /dev/null +++ b/scripts/ci-set-version.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env sh +# On a tag build, the tag is the single source of truth for the app version. +# +# Only src-tauri/tauri.conf.json is rewritten. That is what the Tauri bundler +# stamps onto the .deb/.AppImage and what `tauri android init` turns into the +# Android versionName, so it is the version a user ever sees. The workspace +# Cargo.toml is deliberately left alone: editing it would invalidate Cargo.lock +# and break every `--locked` build in the same run, to change a number that +# appears nowhere but the binary's own metadata. +# +# On a non-tag run this is a no-op, so it is safe to call unconditionally. +# +# POSIX sh: the runner's /bin/sh is dash. +set -e + +CONF="$(dirname "$0")/../src-tauri/tauri.conf.json" + +case "${GITHUB_REF:-}" in + refs/tags/v*) + VERSION="${GITHUB_REF#refs/tags/v}" + echo "Setting app version to $VERSION (from $GITHUB_REF)" + sed -i "s/\"version\": \"[^\"]*\"/\"version\": \"$VERSION\"/" "$CONF" + ;; + *) + echo "Not a tag build — keeping the version in tauri.conf.json" + ;; +esac + +grep '"version"' "$CONF" diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index bb3ae4d..3192c04 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -424,7 +424,7 @@ pub fn spawn_ride_loop(app: AppHandle) { .record(&frame.snapshot, altitude_m); } n += 1; - if n % TICK_HZ == 0 { + if n.is_multiple_of(TICK_HZ) { let s = &frame.snapshot; tracing::debug!( elapsed_ms = s.elapsed_ms,