
Ostatnio napisałem prostą aplikację graficzną na malinę – wyświetlanie paru rzeczy na podłączonym wyświetlaczu. Do realizacji wybrałem Tauri – zużycie pamięci, w połączeniu z cage oscyluje u mnie na poziomie 200MB, a same widoki mogę pisać przy użyciu Preact, zamiast tzw. „rzeźby” z komponentami GUI. Moim zdaniem, dobry kompromis między zużyciem zasobów a szybkością developmentu.
Pewien problem pojawił się z kompilacją i uruchomieniem. Przy próbie zbudowania na Raspberry, pomijając czas budowania, najzwyczajniej… zabrakło mi pamięci. Pierwsze co przychodzi do głowy jest cross-compilation, czyli wykorzystanie komputera do zbudowania binarki na inną architekturę. Niestety, dokumentacja opisuje to wprost:
Tauri relies heavily on native libraries and toolchains, so meaningful cross-compilation is not possible at the current moment.
Nie uwierzyłem, sprawdziłem. Budując w obrazie Dockera rust:latest i instalując biblioteki :arm64, dotarłem do punktu gdzie binarkę zbudować się udało, ale…
$ ./app
/lib/x86_64-linux-gnu/libc.so.6: version 'GLIBC_2.39' not foundUżywany obraz używa Debiana Trixie jako bazy, a mój Raspbian używa Bookworma z glibc 2.36. Próba zbudowania na obrazie rust:bookworm nie poszła już gładko (prawdę mówiąc, ściana błędów i konfliktów bibliotek okazała się nie do przejścia). Niespecjalnie również miałem chęci do dokonywania zmian na urządzeniu docelowym.
Cross-kompilacja za pomocą QEMU
Istnieje całkiem sprytny sposób na uruchamianie obrazów Dockerowych z innych architektur poprzez ich emulację – za pomocą binfmt, po uruchomieniu jednej komendy:
$ docker run --privileged --rm tonistiigi/binfmt --install allNastępnie, możemy przygotować Dockerfile:
FROM node:20 AS frontend-builder
WORKDIR /build
COPY . .
RUN yarn install && yarn build
# Magia!
ARG BUILDPLATFORM="linux/arm64"
FROM --platform=${BUILDPLATFORM} rust:bookworm AS rust-builder
RUN apt-get update && apt-get install -y \
libwebkit2gtk-4.1-dev build-essential curl \
wget file libxdo-dev libssl-dev \
libayatana-appindicator3-dev librsvg2-dev
WORKDIR /build
COPY src-tauri ./src-tauri
COPY --from=frontend-builder /build/dist ./dist
WORKDIR /build/src-tauri
RUN cargo build --release
# Mały obraz do wyciągania rezultatu
FROM busybox:latest
COPY --from=rust-builder /build/src-tauri/target/release/app /appUwaga: jest tu sporo uproszczeń na potrzeby przykładu – na przykład, instrukcja COPY . . nie powinna być tak używana, lepiej skopiować tylko potrzebne pliki.
Pozostaje tylko uruchomić i wyciągnąć rezultat:
$ docker build -t app-builder .
$ docker create --name app-builder app-builder
$ docker cp app-builder:/app ./appAlternatywne podejście – jeden obraz do budowania
Powyższe rozwiązanie działa, ale wymaga zbudowania obrazu do zbudowania aplikacji. W ten sposób osiągnąłem swój pierwotny cel, ale jak wiadomo, zawsze jest pole do ulepszeń. Szczególnie że każdorazowe budowanie obrazu nie byłoby zbyt szybkie, gdyby mi przyszło do głowy robienie tego na jakimś rurociągu (ang. pipeline 😉).
Zacznijmy zatem od Dockerfile:
ARG BUILDPLATFORM="linux/arm64"
FROM --platform=${BUILDPLATFORM} rust:bookworm
RUN apt-get update && apt-get install -y \
libwebkit2gtk-4.1-dev build-essential curl \
wget file libxdo-dev libssl-dev \
libayatana-appindicator3-dev librsvg2-dev \
&& rm -rf /var/lib/apt/lists/*
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
apt-get install -y nodejs && \
npm install -g yarn
WORKDIR /workspace
RUN cat > /build.sh << 'EOF'
#!/bin/bash
set -e
yarn install
yarn build
cd src-tauri
cargo build --release
mkdir -p /workspace/dist-arm64
cp target/release/app /workspace/dist-arm64/
EOF
RUN chmod +x /build.sh
ENTRYPOINT ["/build.sh"]Obraz nie różni się znacząco od poprzedniego. Zakładamy tutaj zmapowanie katalogu z projektem do /workspace. Cały proces może być delikatnie wolniejszy z racji kompilacji front-endu w emulowanym środowisku, ale za to ponowne uruchomienia są szybsze.
Uruchomienie obrazu, dosyć standardowo:
$ docker build -t app-builder .
$ docker run --rm \
--platform linux/arm64 \
-v "$(pwd):/workspace" \
app-builderWyjście znajdzie się w katalogu dist-arm64 w projekcie. Warto rozważyć zmapowanie pewnych katalogów do wolumenów w celu przyśpieszenia następnych buildów (np. -v cargo-cache:/usr/local/cargo/registry:rw czy -v yarn-cache:/.cache/yarn:rw).
Pewnym problemem tego podejścia są uprawnienia – czy to plików wynikowych, czy używanych w procesie. Można dodać flagę -u "$(id -u):$(id -g)" co naprawi ten problem, ale wspomniany wyżej cache nie będzie działać poprawnie. Gdy problem okaże się dla mnie bardziej denerwujący to poszukam rozwiązania i zaktualizuję artykuł 😅