diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml new file mode 100644 index 000000000..fc974160e --- /dev/null +++ b/.github/workflows/build-android.yml @@ -0,0 +1,163 @@ +name: Reusable Android + +on: + workflow_call: + inputs: + abi: + required: true + type: string + +permissions: + contents: read + +env: + CMAKE_BUILD_TYPE: ${{ vars.CMAKE_BUILD_TYPE || 'RelWithDebInfo' }} + +jobs: + build: + runs-on: ubuntu-24.04 + steps: + - name: checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: uname -a + run: uname -a + + # The NDK comes with the runner image, so nothing is downloaded here. The image is pinned to + # ubuntu-24.04 rather than ubuntu-latest because which NDK a build used is part of what it is. + # + # The image's *latest* NDK, not its default one: the default is r27d (clang 18), while fastfetch + # is built and shipped on Termux with clang 21. Testing three majors behind the compiler the + # supported path uses buys nothing and rejects code clang 21 accepts. + - name: print NDK version + run: | + echo "default: ${ANDROID_NDK_HOME}" + echo "latest: ${ANDROID_NDK_LATEST_HOME}" + test -f "${ANDROID_NDK_LATEST_HOME}/build/cmake/android.toolchain.cmake" + cat "${ANDROID_NDK_LATEST_HOME}/source.properties" + "${ANDROID_NDK_LATEST_HOME}"/toolchains/llvm/prebuilt/*/bin/clang --version + + # freetype is the one library this build wants that the NDK does not ship: its headers are not + # in the sysroot and the runner image has no libfreetype-dev, so without this step + # ff_lib_enable(FREETYPE ...) prints "Library: missing: FREETYPE" and + # src/detection/terminalfont/terminalfont_android.c compiles only its #else branch. That is the + # branch the shipped Termux build never takes, so the freetype code would rot untested here. + # + # Handing it the host's copy is sound, for two reasons. fastfetch never links freetype -- + # BINARY_LINK_TYPE is dlopen, so ff_lib_enable skips its target_link_libraries and only the + # include path and FF_HAVE_FREETYPE are needed. And freetype's public headers carry no host + # assumptions: FT_SIZEOF_LONG and FT_SIZEOF_INT in freetype/config/integer-types.h are derived + # from FT_ULONG_MAX/FT_UINT_MAX, which come from the *target's* , so the same copy + # describes FT_FaceRec -- and the offset of the family_name that terminalfont_android.c reads -- + # correctly for aarch64, armv7 and x86_64 alike. + # + # pkg-config is the only route that can find it: the NDK toolchain leaves PKG_CONFIG_PATH, + # PKG_CONFIG_LIBDIR and PKG_CONFIG_SYSROOT_DIR untouched, so the ordinary host search path is + # what gets used. + # + # The .pc is written here rather than taken from libfreetype-dev, whose own copy carries + # `Requires.private: zlib, bzip2, libpng, harfbuzz, libbrotlidec`. pkgconf -- which is what + # `pkg-config` has been on Ubuntu since 22.10 -- fails outright on a private requirement it + # cannot resolve, even for --exists, and libfreetype-dev only pulls in libpng-dev and + # zlib1g-dev. Nothing is linked, so the include directory is the whole contract. A Version + # field is mandatory (pkgconf treats a .pc without one as not found), hence the sed. + - name: install freetype headers + run: | + sudo apt-get install -y --no-install-recommends libfreetype-dev + test -f /usr/include/freetype2/ft2build.h + VERSION=$(awk '/^#define FREETYPE_(MAJOR|MINOR|PATCH)/ {printf "%s%s", sep, $3; sep="."}' /usr/include/freetype2/freetype/freetype.h) + test -n "${VERSION}" + mkdir -p freetype-pc + cat > freetype-pc/freetype2.pc <<'EOF' + prefix=/usr + includedir=${prefix}/include + + Name: FreeType 2 + Description: A free, high-quality, and portable font engine. + Version: 0 + Cflags: -I${includedir}/freetype2 + EOF + sed -i "s/^Version: .*/Version: ${VERSION}/" freetype-pc/freetype2.pc + PKG_CONFIG_PATH="$PWD/freetype-pc" pkg-config --cflags freetype2 + + # android-24 (Android 7.0) is Termux's own floor, and it is the floor for a second, independent + # reason: libcamera2ndk.so only exists from API 24, fastfetch links it unconditionally, and the + # NDK has no such thing as a weak *library* reference -- a build aimed lower would fail to load + # on exactly the devices it was aimed at. The level is also what makes this job check anything: + # at 24 the NDK marks AImageDecoder_* / AndroidBitmap_compress (30) and AMediaCodec_getName / + # AMediaCodec_releaseName (28) unavailable, which is what CMakeLists.txt's + # -D__ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__ -Werror=unguarded-availability pair is built on. + # Building against a newer platform would quietly stop enforcing it. + # + # BUILD_TESTS stays off. The test binaries are host programs, there is no device here to run + # them on, and `ctest` therefore cannot follow -- unlike every other job in this workflow set. + # The platform branch of the build is what this job covers. + - name: configure project + env: + # Picks up the freetype2.pc written by the step above. The rest of the search path is left + # alone, so zlib and every other host .pc file is still found exactly as before. + PKG_CONFIG_PATH: ${{ github.workspace }}/freetype-pc + run: cmake -DSET_TWEAK=Off -DBUILD_TESTS=Off -DCMAKE_TOOLCHAIN_FILE="${ANDROID_NDK_LATEST_HOME}/build/cmake/android.toolchain.cmake" -DANDROID_ABI=${{ inputs.abi }} -DANDROID_PLATFORM=android-24 -DCMAKE_INSTALL_PREFIX=/data/data/com.termux/files/usr . + + # A silent regression back to "Library: missing: FREETYPE" would look exactly like a passing + # build -- the module would simply stop being compiled, and the only trace would be a line in + # the configure log nobody diffs. Assert that the define actually reached the compile line. + - name: check freetype was detected + run: | + grep -q -- '-DFF_HAVE_FREETYPE=1' CMakeFiles/libfastfetch.dir/flags.make || { + echo "::error::freetype was not detected, so terminalfont_android.c compiled its #else branch only" + grep -o -- '-DFF_HAVE_[A-Z0-9_]*' CMakeFiles/libfastfetch.dir/flags.make | sort -u + exit 1 + } + + - name: build project + run: cmake --build . --target package --verbose -j4 + + - name: print ELF header + run: readelf --wide -h fastfetch + + - name: print dynamic dependencies + run: readelf --wide -d fastfetch + + # libjnigraphics.so, libmediandk.so and libcamera2ndk.so are what provide the entry points + # CMakeLists.txt weakens. A weak reference resolves to null instead of failing the load, and + # FF_ANDROID_API_AT_LEAST() reads the platform level rather than the symbol -- so a library + # dropped from the link line still passes the guard on a device new enough, and the call goes + # through a null pointer. DT_NEEDED is the only place that mistake is visible. + - name: check linked libraries + run: | + for lib in libjnigraphics.so libmediandk.so libcamera2ndk.so; do + readelf --wide -d fastfetch | grep -q "NEEDED.*\[${lib}\]" || { echo "::error::${lib} is not in DT_NEEDED"; exit 1; } + done + + # Every reference to an entry point the NDK marks unavailable at API 24 has to be weak: a strong + # one turns a device below API 28/30 into a load failure instead of a graceful downgrade, which + # is the entire reason those three files are compiled with -D__ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__. + # `nm -D` prints a weak undefined symbol as `w` and a strong undefined one as `U`. + # + # The families are whole prefixes on purpose: every AImageDecoder* entry point is API 30 or 31, + # but the frame-info and header-info ones spell the prefix AImageDecoderFrameInfo / HeaderInfo + # without an underscore, and matching only `AImageDecoder_` would leave them unchecked. The + # AMediaCodec ones are named one by one because the rest of that API -- createDecoderByType, + # createEncoderByType, delete -- is API 21 and is correctly a strong reference. + - name: check weak references + run: | + nm -D fastfetch | grep -E ' (AImageDecoder|AndroidBitmap_compress|AMediaCodec_getName$|AMediaCodec_releaseName$)' > weak.txt + cat weak.txt + test -s weak.txt || { echo "::error::none of these entry points is referenced any more -- if the last use is gone on purpose, this check and the -Werror=unguarded-availability pair it guards should go with it"; exit 1; } + if grep -qv '^ *w ' weak.txt; then + echo "::error::expected every one of these to be a weak reference:" + grep -v '^ *w ' weak.txt + exit 1 + fi + + - name: print size + run: | + file fastfetch + ls -l fastfetch + + - name: upload artifacts + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: fastfetch-android-${{ inputs.abi }} + path: ./fastfetch-*.* diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0c67cfb1b..99e92778b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,6 +72,21 @@ jobs: contents: read uses: ./.github/workflows/build-musl-amd64.yml + android: + needs: no-features-test + name: Android-${{ matrix.abi }} + permissions: + contents: read + strategy: + matrix: + abi: + - arm64-v8a + - armeabi-v7a + - x86_64 + uses: ./.github/workflows/build-android.yml + with: + abi: ${{ matrix.abi }} + macos-hosts: needs: no-features-test name: macOS-${{ matrix.arch }} @@ -190,6 +205,7 @@ jobs: - linux-loong64 - linux-vms - musl-amd64 + - android - macos-hosts - freebsd-amd64 - openbsd-amd64 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a88d68721..985bb15b2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -680,7 +680,7 @@ cmake --build build cd build && ctest --output-on-failure ``` -Coverage focuses on the core data structures and the formatting engine in `common/`. The `detection/` layer has no automated tests, because it depends on the state of a running system; it is instead covered by the CI matrix — 20 workflows under `.github/workflows/` spanning Linux (including musl, loong64, armv7l, i686), macOS, Windows, FreeBSD, NetBSD, OpenBSD, DragonFly, Solaris, OmniOS and Haiku, plus spellcheck and benchmark jobs. +Coverage focuses on the core data structures and the formatting engine in `common/`. The `detection/` layer has no automated tests, because it depends on the state of a running system; it is instead covered by the CI matrix — 20 workflows under `.github/workflows/` spanning Linux (including musl, loong64, armv7l, i686), Android, macOS, Windows, FreeBSD, NetBSD, OpenBSD, DragonFly, Solaris, OmniOS and Haiku, plus a benchmark job. The Android job is the one that does not run what it builds: it cross-compiles for arm64-v8a, armeabi-v7a and x86_64 with the NDK at API 24 and inspects the result instead, because CI has no device or emulator. Its dynamic-symbol checks are what keep the weak references the platform branch depends on from turning into load failures on older devices. **If you modify `common/FFstrbuf.h`, `common/format.h` or `common/color.h`, extend the corresponding test.**