From 4051b8f71f7154ef8dc210850d25866dffc4a489 Mon Sep 17 00:00:00 2001 From: Moritz Angermann Date: Sat, 17 Oct 2026 08:20:19 +0900 Subject: [PATCH] Fix Android pthread metadata before mobile shared linking Patch crypton and direct-sqlcipher before planning: Bionic supplies pthread in libc and the NDK has no separate libpthread. Preserve compiler threading flags and the shared-link adapter. Check Android-only source selection and allow the existing Android CI group to be dispatched independently for final-link validation. --- .github/workflows/nix.yml | 6 ++-- flake.nix | 8 ++++- scripts/nix/README.md | 10 ++++-- scripts/nix/android-pthread-libraries.nix | 16 ++++++++++ scripts/nix/check-android-pthread-scope.nix | 31 +++++++++++++++++++ scripts/nix/ci-matrix.py | 4 +-- scripts/nix/crypton-android-pthread.patch | 13 ++++++++ .../direct-sqlcipher-android-pthread.patch | 12 +++++++ scripts/nix/test-ci-matrix.py | 8 +++++ 9 files changed, 101 insertions(+), 7 deletions(-) create mode 100644 scripts/nix/android-pthread-libraries.nix create mode 100644 scripts/nix/check-android-pthread-scope.nix create mode 100644 scripts/nix/crypton-android-pthread.patch create mode 100644 scripts/nix/direct-sqlcipher-android-pthread.patch diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index fb6d5e513c..621f87304c 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -8,7 +8,7 @@ on: type: choice required: true default: all - options: [all, linux-static] + options: [all, linux-static, android] push: branches: [master, stable, build/stable-ghc-9.14] paths: @@ -96,7 +96,9 @@ jobs: - name: Check Android iserv overlay scope if: ${{ matrix.target == 'android' }} timeout-minutes: 1 - run: nix eval --json --file scripts/nix/check-android-iserv-scope.nix + run: | + nix eval --json --file scripts/nix/check-android-iserv-scope.nix + nix eval --json --file scripts/nix/check-android-pthread-scope.nix - name: Check musl interpreter link policy scope if: ${{ matrix.target == 'linux-static' }} diff --git a/flake.nix b/flake.nix index 4fab21ccbf..e6d3c01c97 100644 --- a/flake.nix +++ b/flake.nix @@ -60,12 +60,17 @@ let # v2 restores Hackage description overrides after package patches. # Plan Windows x509 from its patched local source to retain crypt32. + androidPthreads = import ./scripts/nix/android-pthread-libraries.nix { + pkgs = config.evalPackages; + hostPlatform = pkgs'.stdenv.hostPlatform; + }; patchedPackageSources = pkgs.lib.optional ( config.compiler-nix-name == "ghc914-sh" && pkgs'.stdenv.buildPlatform.config != pkgs'.stdenv.hostPlatform.config ) (import ./scripts/nix/composition-source.nix { pkgs = config.evalPackages; }) ++ pkgs.lib.optional pkgs'.stdenv.hostPlatform.isWindows - (import ./scripts/nix/windows-x509-source.nix { pkgs = config.evalPackages; inherit haskellNix; }); + (import ./scripts/nix/windows-x509-source.nix { pkgs = config.evalPackages; inherit haskellNix; }) + ++ androidPthreads.packageSources; in { compiler-nix-name = "ghc914-sh"; inherit compilerSelection; @@ -127,6 +132,7 @@ else [ ./scripts/nix/direct-sqlcipher-2.3.27.patch ] ++ pkgs.lib.optional pkgs'.stdenv.hostPlatform.isAndroid ./scripts/nix/direct-sqlcipher-android-log.patch + ++ androidPthreads.directSqlcipherPatches ++ pkgs.lib.optional pkgs'.stdenv.hostPlatform.isMusl ./scripts/nix/direct-sqlcipher-static-libraries.patch; } // pkgs.lib.optionalAttrs pkgs'.stdenv.hostPlatform.isDarwin { diff --git a/scripts/nix/README.md b/scripts/nix/README.md index fec0d6e979..7b63f6a3a6 100644 --- a/scripts/nix/README.md +++ b/scripts/nix/README.md @@ -135,6 +135,11 @@ these names against the target archive and the `ghc-internal` consumer; the Bionic API35 object stays byte-identical. ARM32 source is patched before Cabal planning, removing false missing-function stubs that shadow real static Bionic startup routines. Android links libffi through the RTS package dependency. +Android preplanning patches remove the separate `pthread` library declarations +from crypton and direct-sqlcipher. Bionic supplies these functions in `libc`; +the NDK has no `libpthread`. Other targets retain their original declarations, +and compiler `-pthread` flags and the threaded RTS remain enabled. Repeat the +scope regression with `nix eval --json --file scripts/nix/check-android-pthread-scope.nix`. The minimum Android API stays at 26. For Android below API28, SplitMix's initializer uses Bionic's existing `arc4random_buf` rather than the newer `getentropy` API. The dependency patch @@ -192,8 +197,9 @@ outputs once: x86_64 Linux runs `native`, `android`, `windows`, and `linux-stati separately; ARM64 Linux runs `native`; both Darwin hosts run `all`. A failed native Linux build therefore leaves the cross-build jobs running. PRs, pushes, and default manual runs retain that complete matrix. A manual dispatch can -select only the existing three `linux-static` outputs with -`gh workflow run nix.yml --ref BRANCH -f group=linux-static`. The registered +select the existing three `linux-static` outputs with +`gh workflow run nix.yml --ref BRANCH -f group=linux-static`, or the four Android +outputs with `gh workflow run nix.yml --ref BRANCH -f group=android`. The registered workflow can run on a branch containing this workflow file; it does not require that file on the default branch. New runs for the same workflow/ref queue without canceling its active run. Legacy PR build jobs also preserve running diff --git a/scripts/nix/android-pthread-libraries.nix b/scripts/nix/android-pthread-libraries.nix new file mode 100644 index 0000000000..57534da096 --- /dev/null +++ b/scripts/nix/android-pthread-libraries.nix @@ -0,0 +1,16 @@ +# Bionic supplies pthread symbols in libc, with no separate libpthread. +# Remove the two Linux library declarations before planning so Cabal's v2 +# source and package registrations agree; keep compiler -pthread flags intact. +{ pkgs, hostPlatform }: +{ + packageSources = pkgs.lib.optional hostPlatform.isAndroid (pkgs.applyPatches { + name = "crypton-0.34-android-source"; + src = pkgs.fetchurl { + url = "https://hackage.haskell.org/package/crypton-0.34/crypton-0.34.tar.gz"; + hash = "sha256-RESEaSTKVWFfzhBJE6WmhnWhgM/q3DUKsrEk+6G8HtY="; + }; + patches = [ ./crypton-android-pthread.patch ]; + }); + directSqlcipherPatches = pkgs.lib.optional hostPlatform.isAndroid + ./direct-sqlcipher-android-pthread.patch; +} diff --git a/scripts/nix/check-android-pthread-scope.nix b/scripts/nix/check-android-pthread-scope.nix new file mode 100644 index 0000000000..d77e6ebfe1 --- /dev/null +++ b/scripts/nix/check-android-pthread-scope.nix @@ -0,0 +1,31 @@ +# nix eval --json --file scripts/nix/check-android-pthread-scope.nix +let + select = import ./android-pthread-libraries.nix; + pkgs = { + lib.optional = condition: value: if condition then [ value ] else []; + fetchurl = args: args; + applyPatches = args: args; + }; + android = select { inherit pkgs; hostPlatform.isAndroid = true; }; + # The false branch must not even evaluate Android sources on other targets. + other = select { + pkgs = pkgs // { + fetchurl = _: throw "Unexpected Android source fetch"; + applyPatches = _: throw "Unexpected Android source patch"; + }; + hostPlatform.isAndroid = false; + }; + source = builtins.head android.packageSources; +in +assert builtins.length android.packageSources == 1; +assert source.name == "crypton-0.34-android-source"; +assert source.src.url == "https://hackage.haskell.org/package/crypton-0.34/crypton-0.34.tar.gz"; +assert source.src.hash == "sha256-RESEaSTKVWFfzhBJE6WmhnWhgM/q3DUKsrEk+6G8HtY="; +assert source.patches == [ ./crypton-android-pthread.patch ]; +assert android.directSqlcipherPatches == [ ./direct-sqlcipher-android-pthread.patch ]; +assert other == { packageSources = []; directSqlcipherPatches = []; }; +{ + androidUsesBothPreplanningPatches = true; + cryptonVersionAndSourcePinned = true; + otherPlatformsUnchangedAndLazy = true; +} diff --git a/scripts/nix/ci-matrix.py b/scripts/nix/ci-matrix.py index a7baaa6c71..79566febf4 100644 --- a/scripts/nix/ci-matrix.py +++ b/scripts/nix/ci-matrix.py @@ -15,7 +15,7 @@ ROWS = ( def select(group): - if group not in ("all", "linux-static"): + if group not in ("all", "linux-static", "android"): raise ValueError(f"unknown Nix CI group: {group}") rows = ROWS if group == "all" else tuple(row for row in ROWS if row[2] == group) return {"include": [dict(system=system, runner=runner, target=target) @@ -24,5 +24,5 @@ def select(group): if __name__ == "__main__": parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--group", choices=["all", "linux-static"], default="all") + parser.add_argument("--group", choices=["all", "linux-static", "android"], default="all") print(json.dumps(select(parser.parse_args().group), separators=(",", ":"))) diff --git a/scripts/nix/crypton-android-pthread.patch b/scripts/nix/crypton-android-pthread.patch new file mode 100644 index 0000000000..32e1d54203 --- /dev/null +++ b/scripts/nix/crypton-android-pthread.patch @@ -0,0 +1,13 @@ +--- a/crypton.cabal ++++ b/crypton.cabal +@@ -310,10 +310,7 @@ + else + build-depends: base + +- if os(linux) +- extra-libraries: pthread +- + if flag(old_toolchain_inliner) + cc-options: -fgnu89-inline + + if (arch(x86_64) || arch(aarch64)) diff --git a/scripts/nix/direct-sqlcipher-android-pthread.patch b/scripts/nix/direct-sqlcipher-android-pthread.patch new file mode 100644 index 0000000000..261cc4d54c --- /dev/null +++ b/scripts/nix/direct-sqlcipher-android-pthread.patch @@ -0,0 +1,12 @@ +--- a/direct-sqlcipher.cabal ++++ b/direct-sqlcipher.cabal +@@ -91,9 +91,6 @@ + + extra-libraries: dl + +- if !os(windows) && !os(android) +- extra-libraries: pthread +- + if flag(fulltextsearch) + cc-options: -DSQLITE_ENABLE_FTS3 -DSQLITE_ENABLE_FTS3_PARENTHESIS + -DSQLITE_ENABLE_FTS4 -DSQLITE_ENABLE_FTS5 diff --git a/scripts/nix/test-ci-matrix.py b/scripts/nix/test-ci-matrix.py index 117ec001ed..89dc39a0f5 100644 --- a/scripts/nix/test-ci-matrix.py +++ b/scripts/nix/test-ci-matrix.py @@ -36,6 +36,14 @@ class Matrix(unittest.TestCase): "hydraJobs.x86_64-linux.aarch64-linux-static:exe:simplex-chat", ]) + def test_android_dispatch_is_exactly_the_existing_four_outputs(self): + self.assertEqual(outputs("android"), [ + "hydraJobs.x86_64-linux.aarch64-android:lib:simplex-chat", + "hydraJobs.x86_64-linux.aarch64-android:lib:support", + "hydraJobs.x86_64-linux.armv7a-android:lib:simplex-chat", + "hydraJobs.x86_64-linux.armv7a-android:lib:support", + ]) + def test_legacy_pr_jobs_do_not_cancel_active_runs(self): source = (HERE.parent.parent / ".github/workflows/build.yml").read_text() expressions = re.findall(r"^ cancel-in-progress: \$\{\{ (.*) \}\}$", source, re.MULTILINE)