diff --git a/.github/workflows/armv7-termux.yml b/.github/workflows/armv7-termux.yml new file mode 100644 index 00000000000..ec52da84d13 --- /dev/null +++ b/.github/workflows/armv7-termux.yml @@ -0,0 +1,55 @@ +name: ARMv7 Termux binary + +on: + workflow_dispatch: + pull_request: + paths: + - '.github/workflows/armv7-termux.yml' + - 'build-armv7-termux' + - 'build/termux-armv7/**' + - 'code/**' + push: + branches: + - trunk + paths: + - '.github/workflows/armv7-termux.yml' + - 'build-armv7-termux' + - 'build/termux-armv7/**' + - 'code/**' + +permissions: + contents: read + +concurrency: + group: armv7-termux-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + build: + runs-on: ubuntu-24.04-arm + timeout-minutes: 360 + + steps: + - name: Check out the exact source + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + + - name: Build and execute the ARMv7 Termux package + env: + IR_TERMUX_ARTIFACT_DIR: ${{ runner.temp }}/ir-armv7-artifacts + IR_TERMUX_FREE_DISK: 1 + IR_TERMUX_KEEP_WORK: 1 + run: ./build-armv7-termux + + - name: Show the acceptance record + run: cat "${{ runner.temp }}/ir-armv7-artifacts/build-record.txt" + + - name: Upload the installable package and provenance + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f + with: + name: ir-termux-armv7-${{ github.event.pull_request.head.sha || github.sha }} + path: ${{ runner.temp }}/ir-armv7-artifacts/ + if-no-files-found: error + compression-level: 0 + retention-days: 30 diff --git a/README.md b/README.md index a47f532b49e..1b8d9014ba3 100644 --- a/README.md +++ b/README.md @@ -70,3 +70,18 @@ Code that used a single `=` as assignment must use an arrow in this R. The comma ## You do not have to give up ordinary R The tested Linux instructions install this build in its own folder and use a separate package library. They do not remove ordinary R, alter your projects, rewrite your scripts, or upload your work. Close this R and launch ordinary R as before whenever you want to switch back. + + +## ARMv7 Android / Termux + +The `ARMv7 Termux binary` workflow builds an installable 32-bit `arm` package. +Download its `ir_*.deb` artifact and install it in Termux: + +```sh +apt install ./ir_*.deb +ir +``` + +The package installs `ir` and `irscript` with a private runtime. It does not +replace the ordinary `R` or `Rscript` commands. The exact reproducible entry +point is [`build-armv7-termux`](build-armv7-termux). diff --git a/build-armv7-termux b/build-armv7-termux new file mode 100755 index 00000000000..a1cad254edf --- /dev/null +++ b/build-armv7-termux @@ -0,0 +1,177 @@ +#!/usr/bin/env bash + +set -euo pipefail + +readonly TUR_REVISION=4c751a5f7a05d62acd0f1d64aa6dd7fa17a79539 +readonly TUR_REPOSITORY=https://github.com/termux-user-repository/tur.git +readonly TERMUX_PACKAGES_REVISION=13391c04904a245c9bdbbbc2c02a3756cae47113 +readonly TERMUX_PACKAGES_REPOSITORY=https://github.com/termux/termux-packages.git +readonly container_repository_root=/data/data/com.termux/files/home/termux-packages + +die() { + printf 'build-armv7-termux: %s\n' "$*" >&2 + exit 1 +} + +require_command() { + command -v "$1" >/dev/null 2>&1 || die "required command not found: $1" +} + +readonly repository_root="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +readonly source_root="$repository_root/code" +readonly recipe_root="$repository_root/build/termux-armv7/package" + +test -f "$source_root/VERSION" || die "run this file from a complete ir checkout containing code/VERSION" +test -f "$recipe_root/build.sh" || die "Termux package recipe is missing" + +case "$(uname -m)" in + aarch64|arm64) + ;; + *) + die "the native ARMv7 build requires an ARM64 Linux host; use the ARMv7 Termux GitHub workflow" + ;; +esac + +require_command docker +require_command dpkg-deb +require_command git +require_command sed +require_command sha256sum + +source_revision="$(git -C "$repository_root" rev-parse HEAD)" +case "$source_revision" in + *[!0-9a-f]*|'') die "could not determine an immutable ir source revision" ;; +esac + +upstream_version="$(sed -n '1s/ .*//p' "$source_root/VERSION")" +case "$upstream_version" in + [0-9]*.[0-9]*.[0-9]*) ;; + *) die "unexpected code/VERSION value: $upstream_version" ;; +esac +readonly source_revision upstream_version +readonly package_version="${upstream_version}~devel+git${source_revision:0:12}" + +work_root="${IR_TERMUX_WORK_DIR:-}" +remove_work_root=false +if test -z "$work_root"; then + work_root="$(mktemp -d "${TMPDIR:-/tmp}/ir-termux-armv7.XXXXXX")" + remove_work_root=true +else + mkdir -p "$work_root" + work_root="$(CDPATH= cd -- "$work_root" && pwd)" +fi +readonly work_root + +cleanup() { + if $remove_work_root && test "${IR_TERMUX_KEEP_WORK:-0}" != 1; then + rm -rf -- "$work_root" + else + printf 'kept build directory: %s\n' "$work_root" + fi +} +trap cleanup EXIT + +readonly termux_packages="$work_root/termux-packages" +test ! -e "$termux_packages" || die "build directory is not empty: $termux_packages" + +git clone --filter=blob:none --no-checkout --depth=1 \ + "$TUR_REPOSITORY" "$termux_packages" +git -C "$termux_packages" fetch --depth=1 origin "$TUR_REVISION" +git -C "$termux_packages" checkout --detach "$TUR_REVISION" +test "$(git -C "$termux_packages" rev-parse HEAD)" = "$TUR_REVISION" || die "TUR revision mismatch" + +git clone --filter=blob:none --no-checkout --depth=1 \ + "$TERMUX_PACKAGES_REPOSITORY" "$termux_packages/termux-packages" +git -C "$termux_packages/termux-packages" fetch --depth=1 origin \ + "$TERMUX_PACKAGES_REVISION" +git -C "$termux_packages/termux-packages" checkout --detach \ + "$TERMUX_PACKAGES_REVISION" +test "$(git -C "$termux_packages/termux-packages" rev-parse HEAD)" = \ + "$TERMUX_PACKAGES_REVISION" || die "termux-packages revision mismatch" + +# TUR's setup normally advances an existing termux-packages checkout. Keep +# this build on the reviewed revision above. +sed -i \ + -e "s/git reset --hard origin\/master/git reset --hard $TERMUX_PACKAGES_REVISION/" \ + -e '/git pull --rebase/d' \ + "$termux_packages/setup-environment.sh" +grep -q "git reset --hard $TERMUX_PACKAGES_REVISION" \ + "$termux_packages/setup-environment.sh" || die "could not pin termux-packages setup" + +mkdir -p "$termux_packages/sources/ir" +cp -a "$source_root/." "$termux_packages/sources/ir/" +cp -a "$recipe_root" "$termux_packages/tur-on-device/ir" +sed -i "s/@IR_PACKAGE_VERSION@/$package_version/g" \ + "$termux_packages/tur-on-device/ir/build.sh" +if grep -q '@IR_PACKAGE_VERSION@' "$termux_packages/tur-on-device/ir/build.sh"; then + die "package-version substitution failed" +fi + +cd "$termux_packages" +./setup-environment.sh + +export ARCH=arm +./common-files/build-termux-docker.sh true +if test "${IR_TERMUX_FREE_DISK:-0}" = 1; then + ./scripts/free-space.sh +fi +./common-files/build-termux-docker.sh login ./scripts/setup-termux.sh + +if command -v sudo >/dev/null 2>&1; then + sudo chown -R 1000:1000 "$termux_packages" +elif test "$(id -u)" = 0; then + chown -R 1000:1000 "$termux_packages" +fi + +./common-files/build-termux-docker.sh login ./build-package.sh -I ir + +mapfile -t packages < <(find tur/output tur-on-device/output output \ + -maxdepth 1 -type f -name 'ir_*.deb' -print 2>/dev/null | sort) +test "${#packages[@]}" = 1 || die "expected exactly one ir .deb; found ${#packages[@]}" +readonly package_path="${packages[0]}" +readonly package_in_container="$container_repository_root/$package_path" + +./common-files/build-termux-docker.sh login -lc " + set -euo pipefail + test \"\$(dpkg --print-architecture)\" = arm + test \"\$(dpkg-deb --field '$package_in_container' Architecture)\" = arm + dpkg -i '$package_in_container' + test -x \"\$PREFIX/bin/ir\" + test -x \"\$PREFIX/bin/irscript\" + test ! -e \"\$PREFIX/bin/R\" + test ! -e \"\$PREFIX/bin/Rscript\" + runtime=\"\$PREFIX/lib/ir/R/bin/exec/R\" + test -x \"\$runtime\" + readelf -h \"\$runtime\" | grep -Eq 'Class:[[:space:]]+ELF32' + readelf -h \"\$runtime\" | grep -Eq 'Machine:[[:space:]]+ARM' + ir --vanilla --slave -e 'x ← 8 ÷ 2; stopifnot((x = 4)); cat(\"ir-armv7-ok\\n\")' + dpkg-query -W -f='\${Package}=\${Version}\\n' | sort > \ + '$container_repository_root/build-environment-packages.txt' +" + +artifact_root="${IR_TERMUX_ARTIFACT_DIR:-$repository_root/artifacts/termux-armv7}" +mkdir -p "$artifact_root" +artifact_root="$(CDPATH= cd -- "$artifact_root" && pwd)" +cp "$package_path" "$artifact_root/" +cp build-environment-packages.txt "$artifact_root/" +readonly artifact_path="$artifact_root/$(basename -- "$package_path")" + +( + cd "$artifact_root" + sha256sum "$(basename -- "$artifact_path")" > SHA256SUMS +) + +{ + printf 'ir-source=%s\n' "$source_revision" + printf 'ir-version=%s\n' "$upstream_version" + printf 'package-version=%s\n' "$package_version" + printf 'tur-source=%s\n' "$TUR_REVISION" + printf 'termux-packages-source=%s\n' "$TERMUX_PACKAGES_REVISION" + printf 'architecture=%s\n' "$(dpkg-deb --field "$artifact_path" Architecture)" + printf 'package=%s\n' "$(basename -- "$artifact_path")" + printf 'sha256=%s\n' "$(sha256sum "$artifact_path" | cut -d ' ' -f 1)" + printf 'container-acceptance=%s\n' ir-armv7-ok +} > "$artifact_root/build-record.txt" + +printf 'ARMv7 Termux package: %s\n' "$artifact_path" +printf 'Checksum: %s\n' "$artifact_root/SHA256SUMS" diff --git a/build/termux-armv7/README.md b/build/termux-armv7/README.md new file mode 100644 index 00000000000..af124d3e6d7 --- /dev/null +++ b/build/termux-armv7/README.md @@ -0,0 +1,25 @@ +# ARMv7 Termux build + +Run `./build-armv7-termux` at the repository root. It produces an `arm` +Termux `.deb`, `SHA256SUMS`, and `build-record.txt` under +`artifacts/termux-armv7/`. + +This is a native 32-bit Android build inside TUR's ARM environment. R runs +newly built R code during its own build, so an ordinary NDK cross-build is not +an equivalent test. + +The package installs `ir` and `irscript`. Its runtime lives under +`$PREFIX/lib/ir/R`; it does not install `R` or `Rscript` and therefore does not +replace an ordinary R installation. + +The TUR and `termux-packages` source revisions are pinned in +`build-armv7-termux`. The two Android patches are derived from TUR's +`tur-on-device/r-base` recipe at that same TUR revision. The build also fixes +its locale to `C.UTF-8`, because ir's source contains Unicode grammar symbols. + +Acceptance requires all of the following inside the ARM Termux environment: + +- package architecture is `arm`; +- the runtime is an ELF32 ARM executable; +- `ir` and `irscript` exist while `R` and `Rscript` do not; +- `x ← 8 ÷ 2; stopifnot((x = 4))` succeeds. diff --git a/build/termux-armv7/package/0001-wctrans-impl.patch b/build/termux-armv7/package/0001-wctrans-impl.patch new file mode 100644 index 00000000000..f91998c7972 --- /dev/null +++ b/build/termux-armv7/package/0001-wctrans-impl.patch @@ -0,0 +1,90 @@ +--- a/src/main/Makefile.in ++++ b/src/main/Makefile.in +@@ -36,6 +36,7 @@ SOURCES_C = \ + times.c \ + unique.c util.c \ + version.c \ ++ wctrans-impl.c \ + g_alab_her.c g_cntrlify.c g_fontdb.c g_her_glyph.c + + SOURCES_F = xxxpr.f +--- /dev/null ++++ b/src/main/wctrans-impl.c +@@ -0,0 +1,48 @@ ++/* ++ * Copyright (C) 2008 The Android Open Source Project ++ * All rights reserved. ++ * ++ * Redistribution and use in source and binary forms, with or without ++ * modification, are permitted provided that the following conditions ++ * are met: ++ * * Redistributions of source code must retain the above copyright ++ * notice, this list of conditions and the following disclaimer. ++ * * Redistributions in binary form must reproduce the above copyright ++ * notice, this list of conditions and the following disclaimer in ++ * the documentation and/or other materials provided with the ++ * distribution. ++ * ++ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ++ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT ++ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS ++ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE ++ * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, ++ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, ++ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS ++ * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED ++ * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, ++ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT ++ * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF ++ * SUCH DAMAGE. ++ */ ++ ++#include ++#include ++#include ++#include ++ ++static const wctrans_t wctrans_tolower = (wctrans_t)(1); ++static const wctrans_t wctrans_toupper = (wctrans_t)(2); ++ ++wctrans_t wctrans(const char* name) { ++ if (strcmp(name, "tolower") == 0) return wctrans_tolower; ++ if (strcmp(name, "toupper") == 0) return wctrans_toupper; ++ return NULL; ++} ++ ++wint_t towctrans(wint_t c, wctrans_t t) { ++ if (t == wctrans_tolower) return towlower(c); ++ if (t == wctrans_toupper) return towupper(c); ++ errno = EINVAL; ++ return 0; ++} +--- a/src/main/character.c ++++ b/src/main/character.c +@@ -89,6 +89,8 @@ + #include + #include // overrides iswxxxx on some platforms. + ++#include "wctrans.h" ++ + /* We use a shared buffer here to avoid reallocing small buffers, and + keep a standard-size (MAXELTSIZE = 8192) buffer allocated shared + between the various functions. +--- /dev/null ++++ b/src/main/wctrans.h +@@ -0,0 +1,4 @@ ++#include ++ ++wint_t towctrans(wint_t __wc, wctrans_t __transform); ++wctrans_t wctrans(const char* __name); +--- a/src/main/grep.c ++++ b/src/main/grep.c +@@ -68,6 +68,8 @@ + #include + #include /* for wctrans_t */ + ++#include "wctrans.h" ++ + /* As from TRE 0.8.0, tre.h replaces regex.h */ + #include + diff --git a/build/termux-armv7/package/0002-fix-link-libraries.patch b/build/termux-armv7/package/0002-fix-link-libraries.patch new file mode 100644 index 00000000000..672df793c61 --- /dev/null +++ b/build/termux-armv7/package/0002-fix-link-libraries.patch @@ -0,0 +1,46 @@ +--- a/configure.ac ++++ b/configure.ac +@@ -1675,6 +1675,8 @@ case "${host_os}" in + ## Could also use -rdynamic, at least for gcc and clang. + main_ldflags="-Wl,--export-dynamic" + STATICR1="-Wl,--whole-archive" + STATICR2="-Wl,--no-whole-archive" ++ ## Android needs libm linked explicitly into shared modules. ++ SHLIB_LIBADD="\$(LIBM)" + ;; + mingw*|msys) + SHLIB_EXT=".dll" +--- a/configure ++++ b/configure +@@ -30094,6 +30094,8 @@ case "${host_os}" in + ## Could also use -rdynamic, at least for gcc and clang. + main_ldflags="-Wl,--export-dynamic" + STATICR1="-Wl,--whole-archive" + STATICR2="-Wl,--no-whole-archive" ++ ## Android needs libm linked explicitly into shared modules. ++ SHLIB_LIBADD="\$(LIBM)" + ;; + mingw*|msys) + SHLIB_EXT=".dll" +--- a/src/library/utils/src/Makefile.in ++++ b/src/library/utils/src/Makefile.in +@@ -25,6 +25,7 @@ PKG_CFLAGS = $(C_VISIBILITY) + PKG_CPPFLAGS =-I../../../include -I$(top_srcdir)/src/include -DHAVE_CONFIG_H \ + -I$(top_srcdir)/src/main + ++PKG_LIBS = -llzma + + all: Makedeps + @$(MAKE) shlib +--- a/src/modules/internet/Makefile.in ++++ b/src/modules/internet/Makefile.in +@@ -22,7 +22,7 @@ DISTFILES = \ + dllversion.rc + + internet_la = internet$(SHLIB_EXT) +-internet_la_LIBADD = $(CURL_LIBS) $(LIBR1) @INTERNET_LIBS@ @DYLIB_UNDEFINED_ALLOWED_FALSE@ $(LIBINTL) ++internet_la_LIBADD = $(LIBM) $(CURL_LIBS) $(LIBR1) @INTERNET_LIBS@ @DYLIB_UNDEFINED_ALLOWED_FALSE@ $(LIBINTL) + + ALL_CPPFLAGS = $(R_XTRA_CPPFLAGS) $(CURL_CPPFLAGS) $(CPPFLAGS) $(DEFS) + ALL_CFLAGS = $(ALL_CFLAGS_LO) @C_VISIBILITY@ + diff --git a/build/termux-armv7/package/build.sh b/build/termux-armv7/package/build.sh new file mode 100644 index 00000000000..23b8668e1a5 --- /dev/null +++ b/build/termux-armv7/package/build.sh @@ -0,0 +1,77 @@ +TERMUX_PKG_HOMEPAGE=https://github.com/isomorphisms/ir +TERMUX_PKG_DESCRIPTION="R with ir's mathematical notation, isolated from ordinary R" +TERMUX_PKG_LICENSE="GPL-2.0-or-later, LGPL-2.1" +TERMUX_PKG_LICENSE_FILE="COPYING" +TERMUX_PKG_MAINTAINER="@isomorphisms" +TERMUX_PKG_VERSION="@IR_PACKAGE_VERSION@" +TERMUX_PKG_SRCURL=file:///data/data/com.termux/files/home/termux-packages/sources/ir +TERMUX_PKG_SHA256=SKIP_CHECKSUM +TERMUX_PKG_DEPENDS="libandroid-glob, libdeflate, libiconv, libbz2, libcurl, liblzma, pcre2, readline, zlib, zstd" +TERMUX_PKG_BUILD_DEPENDS="binutils, gcc-15, which" +TERMUX_PKG_AUTO_UPDATE=false +TERMUX_PKG_EXTRA_CONFIGURE_ARGS=" +ac_cv_have_decl_wctrans=yes +--with-x=no +--without-tcltk +--without-recommended-packages +--disable-java +--enable-R-shlib +--with-readline=yes +--libdir=$TERMUX_PREFIX/lib/ir +" + +# R's build invokes the R executable it has just compiled. Build it inside +# TUR's native 32-bit Android environment rather than pretending it can be +# cross-compiled by an ordinary NDK job. +TERMUX_PKG_MAKE_PROCESSES=1 + +termux_step_pre_configure() { + if [ "${TERMUX_ON_DEVICE_BUILD}" = false ]; then + termux_error_exit "ir requires TUR's native Android build environment." + fi + + CFLAGS="${CFLAGS/-Oz/-O0}" + CXXFLAGS="${CXXFLAGS/-Oz/-O0}" + # Android's 32-bit wchar_t stores Unicode code points, but bionic does not + # advertise that with the ISO 10646 feature macro expected by R. + CPPFLAGS+=" -D__STDC_ISO_10646__=201706L" + LDFLAGS="${LDFLAGS/-static-openmp/}" + LDFLAGS+=" -landroid-glob" + export LANG=C.UTF-8 + export LC_ALL=C.UTF-8 + CROSS_PREFIX=$TERMUX_ARCH-linux-android + if [ "$TERMUX_ARCH" = arm ]; then + CROSS_PREFIX=arm-linux-androideabi + fi + + export AR=$CROSS_PREFIX-ar + export AS=$CROSS_PREFIX-as + export LD=$CROSS_PREFIX-ld + export NM=$CROSS_PREFIX-nm + export CC=$CROSS_PREFIX-gcc-15 + export FC=$CROSS_PREFIX-gfortran-15 + export CXX=$CROSS_PREFIX-g++-15 + unset CPP CXXCPP STRINGS + export STRIP=$CROSS_PREFIX-strip + export RANLIB=$CROSS_PREFIX-ranlib + + if [ "$TERMUX_ARCH" = arm ]; then + export MAKEFLAGS="-j1 --jobserver-style=pipe" + fi +} + +termux_step_post_make_install() { + test -f "$TERMUX_PREFIX/bin/R" + test -f "$TERMUX_PREFIX/bin/Rscript" + mv "$TERMUX_PREFIX/bin/R" "$TERMUX_PREFIX/bin/ir" + mv "$TERMUX_PREFIX/bin/Rscript" "$TERMUX_PREFIX/bin/irscript" + + if test -f "$TERMUX_PREFIX/share/man/man1/R.1"; then + mv "$TERMUX_PREFIX/share/man/man1/R.1" \ + "$TERMUX_PREFIX/share/man/man1/ir.1" + fi + if test -f "$TERMUX_PREFIX/share/man/man1/Rscript.1"; then + mv "$TERMUX_PREFIX/share/man/man1/Rscript.1" \ + "$TERMUX_PREFIX/share/man/man1/irscript.1" + fi +}