diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 2a7c4c9..b996e9d 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -34,6 +34,12 @@ jobs: runner: ubuntu-24.04-arm - appimage_arch: aarch64 runner: ubuntu-24.04-arm + - appimage_arch: riscv64 + runner: ubuntu-24.04 + qemu: true + - appimage_arch: ppc64le + runner: ubuntu-24.04 + qemu: true runs-on: ${{ matrix.runner }} @@ -41,6 +47,11 @@ jobs: - name: Checkout uses: actions/checkout@v7 + # GitHub has no RISC-V and POWER runners: the build container runs emulated + - name: Set up QEMU + if: ${{ matrix.qemu }} + uses: docker/setup-qemu-action@v4 + - name: Get git hash run: | echo -n "https://github.com/${GITHUB_REPOSITORY}/commit/" > src/runtime/version @@ -102,11 +113,84 @@ jobs: run: | scripts/chroot/chroot_build.sh + # Architectures without Alpine Linux (big endian ppc64): built in Debian ports + # with glibc (scripts/debian-ports/build-in-debian.sh), emulated by QEMU + build-debian-ports: + strategy: + fail-fast: false + matrix: + include: + - appimage_arch: ppc64 + debian_arch: ppc64 + platform: linux/ppc64 + + runs-on: ubuntu-24.04 + timeout-minutes: 240 + + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Get git hash + run: | + echo -n "https://github.com/${GITHUB_REPOSITORY}/commit/" > src/runtime/version + git rev-parse --short HEAD | xargs >> src/runtime/version + + # debian-ports has no Docker images: the root file system is made with + # debootstrap (its second stage runs emulated) and imported. The binfmt + # image of setup-qemu-action has no big endian ppc64: qemu-user-static of Ubuntu + - name: Make the Debian ports image + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends debootstrap qemu-user-static + # the keyring of Ubuntu is older than the key that signs sid today. The pool of + # Debian keeps only the current version of the package: its file name comes + # from the package index, so that a new upload of the keyring breaks nothing + POOL=$(curl -fsSL https://deb.debian.org/debian/dists/sid/main/binary-all/Packages.xz | xz -d \ + | awk '/^Package: debian-ports-archive-keyring$/ { f = 1 } f && /^Filename:/ { print $2; f = 0 }') + test -n "$POOL" + KEYRING=$(basename "$POOL") + curl -fsSLO "https://deb.debian.org/debian/$POOL" + dpkg-deb -x $KEYRING keyring + # the keyring goes into the system too, for its apt-get update + sudo debootstrap --arch=${{ matrix.debian_arch }} --variant=minbase \ + --include=debian-ports-archive-keyring \ + --keyring=$PWD/keyring/usr/share/keyrings/debian-ports-archive-keyring.gpg \ + sid rootfs http://ftp.ports.debian.org/debian-ports + sudo tar -C rootfs -c . | docker import --platform ${{ matrix.platform }} - debian-ports + sudo rm -rf rootfs keyring $KEYRING + + - name: Build + run: | + docker run --rm --platform ${{ matrix.platform }} -v "$PWD":/src -w /src debian-ports \ + bash scripts/debian-ports/build-in-debian.sh + sudo chown -R "$(id -u):$(id -g)" out + + - name: Sign + env: + SIGNING_KEY: ${{ secrets.SIGNING_KEY }} + # as in the build job: skipped without the secret (pull requests from forks) + if: ${{ env.SIGNING_KEY != '' }} + run: | + find out + scripts/sign.sh out/runtime-* + cp signing-pubkey.asc out/ + + - uses: actions/upload-artifact@v7 + with: + name: artifacts ${{ matrix.appimage_arch }} + path: out/* + upload: name: Create release and upload artifacts runs-on: ubuntu-latest needs: - build + - build-debian-ports + # Debian ports is unstable by nature and its job runs emulated for hours: when + # it fails, the release still goes out with the other architectures (the + # artifacts of a failed job are not there to upload) + if: ${{ !cancelled() && needs.build.result == 'success' }} steps: - name: Download artifacts uses: actions/download-artifact@v8 diff --git a/BUILD.md b/BUILD.md index fbee62b..67b58ff 100644 --- a/BUILD.md +++ b/BUILD.md @@ -46,6 +46,21 @@ The build process has been automated completely. As a user, you just need to run The resulting AppImages will end up in your current working directory. +`` is one of `x86_64`, `i686`, `armhf`, `aarch64`, `riscv64` and `ppc64le`. There are no GitHub runners for the last two: the CI runs their Alpine containers emulated by QEMU (see [Emulate foreign architectures](#emulate-foreign-architectures)), which is much slower. + + +### Architectures without Alpine Linux: big endian ppc64 + +Alpine Linux has no big endian 64-bit PowerPC, so `ppc64` is built in a Debian ports system (the `sid` release, the only one with this architecture), with glibc and gcc instead of musl and clang, and without static PIE (`make STATIC_PIE=`). libfuse and squashfuse are built from source as for the other architectures (`scripts/common/install-dependencies.sh`); zstd, zlib and, when Debian has its static library, mimalloc come from Debian: + +```sh +# in a Debian ports (sid) ppc64 system, as root, in the repository: +> bash scripts/debian-ports/build-in-debian.sh +``` + +The runtime ends up in `out/runtime-ppc64`. Debian ports has no Docker images: the CI makes the system with `debootstrap` and imports it into Docker (job `build-debian-ports` of `.github/workflows/build.yaml`), and runs it with the `qemu-user-static` of Ubuntu, as the binfmt image of setup-qemu-action has no big endian ppc64. +The keyring of Debian ports is the current one of the Debian package index (the pool keeps only the latest version), the runtime is signed as the others, and the release does not wait for this job: when the emulated Debian ports build fails, the other architectures are published without `runtime-ppc64`. + ### Interactive environment diff --git a/scripts/build-runtime.sh b/scripts/build-runtime.sh index 3ce3b91..63ed9ad 100755 --- a/scripts/build-runtime.sh +++ b/scripts/build-runtime.sh @@ -17,7 +17,8 @@ cp -R src "$build_dir"/ pushd "$build_dir" pushd src/runtime/ -make -j"$(nproc)" runtime +# RUNTIME_MAKE_ARGS: extra make variables, e.g. CC=gcc STATIC_PIE= for glibc builds +make -j"$(nproc)" runtime ${RUNTIME_MAKE_ARGS:-} file runtime @@ -42,6 +43,12 @@ elif [[ "$runtime" =~ EABI5 ]]; then architecture=armhf elif [[ "$runtime" =~ x86-64 ]]; then architecture=x86_64 +elif [[ "$runtime" =~ RISC-V ]]; then + architecture=riscv64 +elif [[ "$runtime" =~ "64-bit MSB" ]] && [[ "$runtime" =~ PowerPC ]]; then + architecture=ppc64 +elif [[ "$runtime" =~ "64-bit LSB" ]] && [[ "$runtime" =~ PowerPC ]]; then + architecture=ppc64le else echo "Unsupported architecture: ${runtime#* }" exit 2 diff --git a/scripts/debian-ports/build-in-debian.sh b/scripts/debian-ports/build-in-debian.sh new file mode 100755 index 0000000..14acd41 --- /dev/null +++ b/scripts/debian-ports/build-in-debian.sh @@ -0,0 +1,38 @@ +#! /bin/bash + +# Builds the runtime in a Debian (ports) system, for the architectures that +# have no Alpine Linux, e.g. big endian ppc64: glibc instead of musl, gcc +# instead of clang, and no static PIE (glibc has none on every architecture). +# libfuse and squashfuse come from source as for the other architectures +# (scripts/common/install-dependencies.sh), zstd and zlib from Debian. +# +# (systemd-dev: the udev.pc that the build of libfuse reads, without systemd itself, +# whose maintainer scripts crash under QEMU) +# +# Usage (as root, in the repository, e.g. in a container): +# bash scripts/debian-ports/build-in-debian.sh +# The runtime is written into out/. + +set -euxo pipefail + +export DEBIAN_FRONTEND=noninteractive +apt-get update +apt-get install -y --no-install-recommends \ + bash ca-certificates wget file patch xz-utils make gcc libc6-dev pkg-config \ + meson ninja-build autoconf automake libtool \ + libzstd-dev zlib1g-dev libmimalloc-dev systemd-dev + +# meson installs libfuse into /usr/lib64, which pkg-config and gcc of Debian +# (multiarch: /usr/lib/) do not search +export PKG_CONFIG_PATH=/usr/lib64/pkgconfig${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH} +export LIBRARY_PATH=/usr/lib64${LIBRARY_PATH:+:$LIBRARY_PATH} + +bash scripts/common/install-dependencies.sh + +# mimalloc only when Debian has its static library +make_args="CC=gcc STATIC_PIE=" +if ! find /usr/lib -name libmimalloc.a | grep -q .; then + make_args="$make_args MIMALLOC=" +fi + +RUNTIME_MAKE_ARGS="$make_args" bash scripts/build-runtime.sh diff --git a/scripts/docker/create-build-container.sh b/scripts/docker/create-build-container.sh index 8991688..2d75101 100755 --- a/scripts/docker/create-build-container.sh +++ b/scripts/docker/create-build-container.sh @@ -23,6 +23,12 @@ case "${ARCH}" in aarch64) docker_platform=linux/arm64/v8 ;; + riscv64) + docker_platform=linux/riscv64 + ;; + ppc64le) + docker_platform=linux/ppc64le + ;; *) echo "Unsupported architecture: $ARCH" exit 3 diff --git a/src/runtime/Makefile b/src/runtime/Makefile index 9fd4165..9323fe6 100644 --- a/src/runtime/Makefile +++ b/src/runtime/Makefile @@ -1,7 +1,10 @@ GIT_COMMIT := $(shell cat version) CC = clang -CFLAGS = -std=gnu99 -Os -D_FILE_OFFSET_BITS=64 -DGIT_COMMIT=\"$(GIT_COMMIT)\" -T data_sections.ld -ffunction-sections -fdata-sections -Wl,--gc-sections -static -Wall -Werror -static-pie -LIBS = -lsquashfuse -lsquashfuse_ll -lzstd -lz -lfuse3 -lmimalloc +# both can be emptied on the command line, e.g. for glibc targets without static PIE (make STATIC_PIE=) +STATIC_PIE = -static-pie +MIMALLOC = -lmimalloc +CFLAGS = -std=gnu99 -Os -D_FILE_OFFSET_BITS=64 -DGIT_COMMIT=\"$(GIT_COMMIT)\" -T data_sections.ld -ffunction-sections -fdata-sections -Wl,--gc-sections -static -Wall -Werror $(STATIC_PIE) +LIBS = -lsquashfuse -lsquashfuse_ll -lzstd -lz -lfuse3 $(MIMALLOC) all: runtime diff --git a/src/runtime/runtime.c b/src/runtime/runtime.c index a073f60..bc8b0fe 100644 --- a/src/runtime/runtime.c +++ b/src/runtime/runtime.c @@ -724,14 +724,17 @@ void portable_option(const char* arg, const char* appimage_path, const char* nam char portable_dir[PATH_MAX]; char fullpath[PATH_MAX]; - ssize_t length = readlink(appimage_path, fullpath, sizeof(fullpath)); + ssize_t length = readlink(appimage_path, fullpath, sizeof(fullpath) - 1); if (length < 0) { fprintf(stderr, "Error getting realpath for %s\n", appimage_path); exit(EXIT_FAILURE); } fullpath[length] = '\0'; - sprintf(portable_dir, "%s.%s", fullpath, name); + if (snprintf(portable_dir, sizeof(portable_dir), "%s.%s", fullpath, name) >= (int) sizeof(portable_dir)) { + fprintf(stderr, "Path of the portable %s directory too long: %s.%s\n", name, fullpath, name); + exit(EXIT_FAILURE); + } if (!mkdir(portable_dir, S_IRWXU)) fprintf(stderr, "Portable %s directory created at %s\n", name, portable_dir); else @@ -1511,7 +1514,7 @@ int main(int argc, char* argv[]) { if (arg && strcmp(arg, "appimage-help") == 0) { char fullpath[PATH_MAX]; - ssize_t length = readlink(appimage_path, fullpath, sizeof(fullpath)); + ssize_t length = readlink(appimage_path, fullpath, sizeof(fullpath) - 1); if (length < 0) { fprintf(stderr, "Error getting realpath for %s\n", appimage_path); exit(EXIT_EXECERROR); @@ -1561,7 +1564,7 @@ int main(int argc, char* argv[]) { if (getenv("TARGET_APPIMAGE") == NULL) { // If we are operating on this file itself - ssize_t len = readlink(appimage_path, fullpath, sizeof(fullpath)); + ssize_t len = readlink(appimage_path, fullpath, sizeof(fullpath) - 1); if (len < 0) { perror("Failed to obtain absolute path"); exit(EXIT_EXECERROR);