diff --git a/.gitignore b/.gitignore index f68f6088..6cdca85a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,8 @@ .gems build /cbuild64/ + +# Meson build directories and local cross-files +builddir*/ +subprojects/ +cross/local/ diff --git a/README-BUILD-WITH-MESON.md b/README-BUILD-WITH-MESON.md new file mode 100644 index 00000000..7cc1075a --- /dev/null +++ b/README-BUILD-WITH-MESON.md @@ -0,0 +1,351 @@ +# Building Giac with Meson + +Meson is an alternative build system for Giac, supporting native desktop builds +and cross-compilation to Android, iOS, Mac Catalyst, Windows, and WebAssembly. + +## Prerequisites + +- [Meson](https://mesonbuild.com/) >= 1.2.0 +- [Ninja](https://ninja-build.org/) build backend +- C/C++ compiler (GCC, Clang, or MSVC) + +## Quick Start: Build All Platforms + +A convenience script builds every supported platform in one command: + +```bash +./scripts/build-all.sh +``` + +This builds: native desktop, all 4 Android ABIs, all 5 iOS/Catalyst variants, +Windows (MinGW cross), and Emscripten WASM. See [Build All Script](#build-all-script) +for details and prerequisites. + +## Native Desktop Builds + +Native builds use system-installed GMP and MPFR libraries. + +### Linux (Debian/Ubuntu) + +```bash +sudo apt install meson ninja-build libgmp-dev libmpfr-dev + +meson setup builddir +meson compile -C builddir +``` + +Outputs: +- `builddir/libgiac.a` -- static library +- `builddir/libjavagiac.so` -- JNI shared library (if JDK headers are available) +- `builddir/minigiac` -- standalone CAS shell + +### macOS + +```bash +brew install meson ninja gmp mpfr + +meson setup builddir +meson compile -C builddir +``` + +Outputs: +- `builddir/libgiac.a` -- static library +- `builddir/libjavagiac.dylib` -- JNI shared library +- `builddir/minigiac` -- standalone CAS shell + +### Windows (MSYS2 CLANG64) + +```bash +pacman -S mingw-w64-clang-x86_64-meson mingw-w64-clang-x86_64-ninja \ + mingw-w64-clang-x86_64-gmp mingw-w64-clang-x86_64-mpfr + +meson setup builddir +meson compile -C builddir +``` + +### Smoke Test + +```bash +echo '1+1' | ./builddir/minigiac +# Expected output: 2 +``` + +## Cross-Compilation + +Cross-compilation uses prebuilt static GMP/MPFR libraries bundled in +`src/jni/prebuilt/`. Each target platform has a Meson cross-file in `cross/`. + +### Android + +Requires the [Android NDK](https://developer.android.com/ndk). The cross-files +ship with `@NDK@` placeholders — configure them before first use: + +```bash +# Auto-detect NDK from ANDROID_NDK_HOME, ANDROID_HOME, or common locations +./scripts/configure-cross.sh + +# Or specify explicitly +ANDROID_NDK_HOME=/path/to/ndk ./scripts/configure-cross.sh + +# Build for a single ABI (use generated files from cross/local/) +meson setup builddir-android-arm64 --cross-file cross/local/android-arm64.ini +meson compile -C builddir-android-arm64 + +# Build for all 4 ABIs +for abi in arm arm64 x86 x86_64; do + meson setup builddir-android-$abi --cross-file cross/local/android-$abi.ini + meson compile -C builddir-android-$abi +done +``` + +#### `configure-cross.sh` + +The Android cross-files in `cross/` are **templates** containing `@NDK@` and +`@NDK_HOST@` placeholders. The `scripts/configure-cross.sh` script replaces +these placeholders with your actual NDK path and generates ready-to-use files +in `cross/local/` (which is gitignored). + +NDK detection order: +1. `ANDROID_NDK_HOME` environment variable +2. Latest version found in `$ANDROID_HOME/ndk/` +3. Common default locations: + - macOS: `~/Library/Android/sdk/ndk/` + - Linux: `~/Android/Sdk/ndk/` + - CI: `/usr/local/lib/android/sdk/ndk/` + +```bash +# Auto-detect +./scripts/configure-cross.sh + +# Explicit path +ANDROID_NDK_HOME=/opt/android-ndk-r29 ./scripts/configure-cross.sh +``` + +The `build-all.sh` script calls `configure-cross.sh` automatically if +`cross/local/` does not exist yet. + +Available cross-files (templates in `cross/`, generated in `cross/local/`): + +| Template | ABI | Outputs | +|---|---|---| +| `android-arm.ini` | armeabi-v7a | `libgiac.a`, `libjavagiac.so` | +| `android-arm64.ini` | arm64-v8a | `libgiac.a`, `libjavagiac.so` | +| `android-x86.ini` | x86 | `libgiac.a`, `libjavagiac.so` | +| `android-x86_64.ini` | x86_64 | `libgiac.a`, `libjavagiac.so` | + +### iOS / Mac Catalyst + +Requires Xcode with iOS and macOS SDKs. The cross-files assume the default +Xcode SDK paths. Adjust the `sdk_path` constant if yours differ. + +```bash +# Build for iOS device (arm64) +meson setup builddir-ios-arm64 --cross-file cross/ios-arm64.ini \ + -Dwith_simpleinterface=true +meson compile -C builddir-ios-arm64 + +# Build all 5 variants +for variant in ios-arm64 ios-sim-x86_64 ios-sim-arm64 \ + maccatalyst-x86_64 maccatalyst-arm64; do + meson setup builddir-$variant --cross-file cross/$variant.ini \ + -Dwith_simpleinterface=true + meson compile -C builddir-$variant +done + +# Assemble into Giac.xcframework +./scripts/build-xcframework.sh +``` + +Available cross-files: + +| Cross-file | Target | Outputs | +|---|---|---| +| `cross/ios-arm64.ini` | iOS device (arm64) | `libgiac.a`, `libsimpleinterface.a` | +| `cross/ios-sim-x86_64.ini` | iOS Simulator (x86_64) | `libgiac.a`, `libsimpleinterface.a` | +| `cross/ios-sim-arm64.ini` | iOS Simulator (arm64) | `libgiac.a`, `libsimpleinterface.a` | +| `cross/maccatalyst-x86_64.ini` | Mac Catalyst (x86_64) | `libgiac.a`, `libsimpleinterface.a` | +| `cross/maccatalyst-arm64.ini` | Mac Catalyst (arm64) | `libgiac.a`, `libsimpleinterface.a` | + +### Windows (MinGW cross-compilation) + +Requires the MinGW-w64 cross-compiler toolchain. + +On macOS: +```bash +brew install mingw-w64 +``` + +On Linux (Debian/Ubuntu): +```bash +sudo apt install gcc-mingw-w64-x86-64 g++-mingw-w64-x86-64 +``` + +Build: +```bash +meson setup builddir-windows --cross-file cross/windows-clang64.ini +meson compile -C builddir-windows +``` + +Outputs: +- `builddir-windows/libgiac.a` -- static library +- `builddir-windows/libjavagiac.dll` -- JNI DLL (PE32+ x86-64) + +> **Note:** The cross-file includes `-Wa,-mbig-obj` to handle `cocoa.cc`'s +> large number of PE/COFF sections from heavy template instantiation. + +### WebAssembly (Emscripten) + +Requires the [Emscripten SDK](https://emscripten.org/). + +On macOS: +```bash +brew install emscripten +``` + +Or manually: +```bash +git clone https://github.com/emscripten-core/emsdk.git +cd emsdk && ./emsdk install latest && ./emsdk activate latest +source emsdk_env.sh +``` + +Build: +```bash +meson setup builddir-wasm --cross-file cross/emscripten-wasm32.ini +meson compile -C builddir-wasm +``` + +Outputs: +- `builddir-wasm/giacggb.wasm` -- WebAssembly binary (~9.5 MB) +- `builddir-wasm/giacggb.js` -- Emscripten JS glue (~178 KB) +- `builddir-wasm/giac.wasm.js` -- single embeddable JS file (WASM inlined as base64, ~13 MB) + +Test in Node.js: + +```bash +node -e " + const Module = require('./builddir-wasm/giacggb.js'); + Module.onRuntimeInitialized = () => { + const caseval = Module.cwrap('caseval', 'string', ['string']); + console.log(caseval('1+1')); + }; +" +# Expected output: 2 +``` + +### Linux ARM (Raspberry Pi) + +Requires an ARM cross-compilation toolchain (`arm-linux-gnueabihf-gcc`). + +```bash +meson setup builddir-linux-arm --cross-file cross/linux-arm.ini +meson compile -C builddir-linux-arm +``` + +## Build All Script + +The `scripts/build-all.sh` script automates building for every supported +platform from a macOS host. It builds targets sequentially and reports a +summary at the end. + +### Prerequisites + +The script checks for required tools before starting. Install them with: + +```bash +# Core build tools +brew install meson ninja gmp mpfr + +# Cross-compilation toolchains +brew install mingw-w64 emscripten + +# Android NDK (set path in cross/android-*.ini files) +# iOS/Catalyst (requires Xcode) +``` + +### Usage + +```bash +# Build all platforms +./scripts/build-all.sh + +# Build specific platforms only +./scripts/build-all.sh native android + +# Build with verbose output +./scripts/build-all.sh --verbose + +# Clean all build directories first +./scripts/build-all.sh --clean +``` + +Available platform selectors: `native`, `android`, `ios`, `windows`, `wasm`. + +### Output Summary + +The script prints a summary table showing pass/fail status for each target, +along with the build artifacts produced. + +## Build Options + +Options are set at configure time with `-D`: + +| Option | Type | Default | Description | +|---|---|---|---| +| `giac_version` | string | `1.2.4` | Version string embedded via `-DVERSION` | +| `with_jni` | feature | `auto` | Build JNI shared library (`auto` = build if not iOS/WASM) | +| `with_minigiac` | boolean | `true` | Build minigiac executable (native desktop only) | +| `with_simpleinterface` | boolean | `false` | Build SimpleInterface static library (iOS/Catalyst) | +| `with_wasm_embed` | boolean | `true` | Post-process WASM into single embedded JS file | + +Examples: + +```bash +# Disable JNI +meson setup builddir -Dwith_jni=disabled + +# Custom version string +meson setup builddir -Dgiac_version=1.9.0-99 + +# iOS with SimpleInterface +meson setup builddir-ios --cross-file cross/ios-arm64.ini \ + -Dwith_simpleinterface=true +``` + +## Project Structure + +``` +meson.build # Main build definition +meson_options.txt # Build options +cross/ # Cross-compilation files + android-arm.ini + android-arm64.ini + android-x86.ini + android-x86_64.ini + emscripten-wasm32.ini + ios-arm64.ini + ios-sim-arm64.ini + ios-sim-x86_64.ini + linux-arm.ini + maccatalyst-arm64.ini + maccatalyst-x86_64.ini + windows-clang64.ini +scripts/ + build-all.sh # Build all platforms + configure-cross.sh # Generate Android cross-files from NDK path + embed-wasm.sh # WASM base64 post-processing + build-xcframework.sh # iOS xcframework assembly +``` + +## Notes + +- Cross-compilation uses prebuilt static GMP 6.3.0 and MPFR 4.2.1 from + `src/jni/prebuilt/` (Android, iOS, Linux ARM, Windows) or + `src/giac.js/prebuilt/` (Emscripten). +- Windows cross-compilation requires `-Wa,-mbig-obj` due to `cocoa.cc`'s + heavy template usage exceeding the PE/COFF default section limit. +- The Emscripten embed step uses Python 3 to handle base64-encoded WASM + strings that exceed shell argument length limits. +- The Node.js addon (`binding.gyp`) is not managed by Meson and remains + unchanged. +- This build system coexists with the existing CMake and Gradle builds. diff --git a/cross/android-arm.ini b/cross/android-arm.ini new file mode 100644 index 00000000..14877152 --- /dev/null +++ b/cross/android-arm.ini @@ -0,0 +1,25 @@ +# Android armeabi-v7a (arm) cross-file for Meson +# Requires: ANDROID_NDK_HOME environment variable pointing to NDK root + +[constants] +ndk = '@NDK@' +toolchain = ndk / 'toolchains/llvm/prebuilt/@NDK_HOST@' +api = '35' + +[binaries] +c = toolchain / 'bin' / 'armv7a-linux-androideabi' + api + '-clang' +cpp = toolchain / 'bin' / 'armv7a-linux-androideabi' + api + '-clang++' +ar = toolchain / 'bin' / 'llvm-ar' +strip = toolchain / 'bin' / 'llvm-strip' +c_ld = 'lld' +cpp_ld = 'lld' + +[built-in options] +c_args = ['-fPIC', '-fno-strict-aliasing', '-DPIC'] +cpp_args = ['-fPIC', '-fno-strict-aliasing', '-DPIC', '-fpermissive'] + +[host_machine] +system = 'android' +cpu_family = 'arm' +cpu = 'armv7a' +endian = 'little' diff --git a/cross/android-arm64.ini b/cross/android-arm64.ini new file mode 100644 index 00000000..615fee1e --- /dev/null +++ b/cross/android-arm64.ini @@ -0,0 +1,25 @@ +# Android arm64-v8a (aarch64) cross-file for Meson +# Requires: ANDROID_NDK_HOME environment variable pointing to NDK root + +[constants] +ndk = '@NDK@' +toolchain = ndk / 'toolchains/llvm/prebuilt/@NDK_HOST@' +api = '35' + +[binaries] +c = toolchain / 'bin' / 'aarch64-linux-android' + api + '-clang' +cpp = toolchain / 'bin' / 'aarch64-linux-android' + api + '-clang++' +ar = toolchain / 'bin' / 'llvm-ar' +strip = toolchain / 'bin' / 'llvm-strip' +c_ld = 'lld' +cpp_ld = 'lld' + +[built-in options] +c_args = ['-fPIC', '-fno-strict-aliasing', '-DPIC'] +cpp_args = ['-fPIC', '-fno-strict-aliasing', '-DPIC', '-fpermissive'] + +[host_machine] +system = 'android' +cpu_family = 'aarch64' +cpu = 'armv8' +endian = 'little' diff --git a/cross/android-x86.ini b/cross/android-x86.ini new file mode 100644 index 00000000..0cdeccd5 --- /dev/null +++ b/cross/android-x86.ini @@ -0,0 +1,25 @@ +# Android x86 cross-file for Meson +# Requires: ANDROID_NDK_HOME environment variable pointing to NDK root + +[constants] +ndk = '@NDK@' +toolchain = ndk / 'toolchains/llvm/prebuilt/@NDK_HOST@' +api = '35' + +[binaries] +c = toolchain / 'bin' / 'i686-linux-android' + api + '-clang' +cpp = toolchain / 'bin' / 'i686-linux-android' + api + '-clang++' +ar = toolchain / 'bin' / 'llvm-ar' +strip = toolchain / 'bin' / 'llvm-strip' +c_ld = 'lld' +cpp_ld = 'lld' + +[built-in options] +c_args = ['-fPIC', '-fno-strict-aliasing', '-DPIC'] +cpp_args = ['-fPIC', '-fno-strict-aliasing', '-DPIC', '-fpermissive'] + +[host_machine] +system = 'android' +cpu_family = 'x86' +cpu = 'i686' +endian = 'little' diff --git a/cross/android-x86_64.ini b/cross/android-x86_64.ini new file mode 100644 index 00000000..1c7a1a2b --- /dev/null +++ b/cross/android-x86_64.ini @@ -0,0 +1,25 @@ +# Android x86_64 cross-file for Meson +# Requires: ANDROID_NDK_HOME environment variable pointing to NDK root + +[constants] +ndk = '@NDK@' +toolchain = ndk / 'toolchains/llvm/prebuilt/@NDK_HOST@' +api = '35' + +[binaries] +c = toolchain / 'bin' / 'x86_64-linux-android' + api + '-clang' +cpp = toolchain / 'bin' / 'x86_64-linux-android' + api + '-clang++' +ar = toolchain / 'bin' / 'llvm-ar' +strip = toolchain / 'bin' / 'llvm-strip' +c_ld = 'lld' +cpp_ld = 'lld' + +[built-in options] +c_args = ['-fPIC', '-fno-strict-aliasing', '-DPIC'] +cpp_args = ['-fPIC', '-fno-strict-aliasing', '-DPIC', '-fpermissive'] + +[host_machine] +system = 'android' +cpu_family = 'x86_64' +cpu = 'x86_64' +endian = 'little' diff --git a/cross/emscripten-wasm32.ini b/cross/emscripten-wasm32.ini new file mode 100644 index 00000000..33e7d1a0 --- /dev/null +++ b/cross/emscripten-wasm32.ini @@ -0,0 +1,18 @@ +# Emscripten WebAssembly cross-file for Meson +# Requires: Emscripten SDK activated (source emsdk_env.sh) + +[binaries] +c = 'emcc' +cpp = 'em++' +ar = 'emar' +strip = 'emstrip' + +[built-in options] +c_args = ['-Oz', '-fwasm-exceptions'] +cpp_args = ['-Oz', '-fwasm-exceptions', '-fpermissive'] + +[host_machine] +system = 'emscripten' +cpu_family = 'wasm32' +cpu = 'wasm32' +endian = 'little' diff --git a/cross/ios-arm64.ini b/cross/ios-arm64.ini new file mode 100644 index 00000000..ea6c839e --- /dev/null +++ b/cross/ios-arm64.ini @@ -0,0 +1,22 @@ +# iOS device arm64 cross-file for Meson +# Requires: Xcode with iOS SDK installed + +[constants] +sdk_path = '/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk' + +[binaries] +c = 'clang' +cpp = 'clang++' +ar = 'ar' +strip = 'strip' + +[built-in options] +c_args = ['-isysroot', sdk_path, '-target', 'arm64-apple-ios9.0', '-miphoneos-version-min=9.0', '-fembed-bitcode', '-std=gnu11', '-stdlib=libc++'] +cpp_args = ['-isysroot', sdk_path, '-target', 'arm64-apple-ios9.0', '-miphoneos-version-min=9.0', '-fembed-bitcode', '-std=gnu++11', '-stdlib=libc++', '-fpermissive'] + +[host_machine] +system = 'darwin' +cpu_family = 'aarch64' +cpu = 'arm64' +endian = 'little' +subsystem = 'ios' diff --git a/cross/ios-sim-arm64.ini b/cross/ios-sim-arm64.ini new file mode 100644 index 00000000..90ed7875 --- /dev/null +++ b/cross/ios-sim-arm64.ini @@ -0,0 +1,22 @@ +# iOS Simulator arm64 cross-file for Meson +# Requires: Xcode with iOS Simulator SDK installed + +[constants] +sdk_path = '/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk' + +[binaries] +c = 'clang' +cpp = 'clang++' +ar = 'ar' +strip = 'strip' + +[built-in options] +c_args = ['-isysroot', sdk_path, '-target', 'arm64-apple-ios9.0-simulator', '-miphoneos-version-min=9.0', '-fembed-bitcode', '-std=gnu11', '-stdlib=libc++'] +cpp_args = ['-isysroot', sdk_path, '-target', 'arm64-apple-ios9.0-simulator', '-miphoneos-version-min=9.0', '-fembed-bitcode', '-std=gnu++11', '-stdlib=libc++', '-fpermissive'] + +[host_machine] +system = 'darwin' +cpu_family = 'aarch64' +cpu = 'arm64' +endian = 'little' +subsystem = 'ios' diff --git a/cross/ios-sim-x86_64.ini b/cross/ios-sim-x86_64.ini new file mode 100644 index 00000000..356fd95e --- /dev/null +++ b/cross/ios-sim-x86_64.ini @@ -0,0 +1,22 @@ +# iOS Simulator x86_64 cross-file for Meson +# Requires: Xcode with iOS Simulator SDK installed + +[constants] +sdk_path = '/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk' + +[binaries] +c = 'clang' +cpp = 'clang++' +ar = 'ar' +strip = 'strip' + +[built-in options] +c_args = ['-isysroot', sdk_path, '-target', 'x86_64-apple-ios9.0-simulator', '-miphoneos-version-min=9.0', '-fembed-bitcode', '-std=gnu11', '-stdlib=libc++'] +cpp_args = ['-isysroot', sdk_path, '-target', 'x86_64-apple-ios9.0-simulator', '-miphoneos-version-min=9.0', '-fembed-bitcode', '-std=gnu++11', '-stdlib=libc++', '-fpermissive'] + +[host_machine] +system = 'darwin' +cpu_family = 'x86_64' +cpu = 'x86_64' +endian = 'little' +subsystem = 'ios' diff --git a/cross/linux-arm.ini b/cross/linux-arm.ini new file mode 100644 index 00000000..83a946f9 --- /dev/null +++ b/cross/linux-arm.ini @@ -0,0 +1,18 @@ +# Linux ARM (Raspberry Pi) cross-file for Meson +# Requires: ARM cross-compilation toolchain installed + +[binaries] +c = 'arm-linux-gnueabihf-gcc' +cpp = 'arm-linux-gnueabihf-g++' +ar = 'arm-linux-gnueabihf-ar' +strip = 'arm-linux-gnueabihf-strip' + +[built-in options] +c_args = ['-fPIC', '-fno-strict-aliasing', '-DPIC'] +cpp_args = ['-fPIC', '-fno-strict-aliasing', '-DPIC', '-fpermissive'] + +[host_machine] +system = 'linux' +cpu_family = 'arm' +cpu = 'armv7l' +endian = 'little' diff --git a/cross/maccatalyst-arm64.ini b/cross/maccatalyst-arm64.ini new file mode 100644 index 00000000..3ddfff07 --- /dev/null +++ b/cross/maccatalyst-arm64.ini @@ -0,0 +1,22 @@ +# Mac Catalyst arm64 cross-file for Meson +# Requires: Xcode with macOS SDK installed + +[constants] +sdk_path = '/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk' + +[binaries] +c = 'clang' +cpp = 'clang++' +ar = 'ar' +strip = 'strip' + +[built-in options] +c_args = ['-isysroot', sdk_path, '-target', 'arm64-apple-ios14.0-macabi', '-std=gnu11', '-stdlib=libc++', '-fembed-bitcode'] +cpp_args = ['-isysroot', sdk_path, '-target', 'arm64-apple-ios14.0-macabi', '-std=gnu++11', '-stdlib=libc++', '-fembed-bitcode', '-fpermissive'] + +[host_machine] +system = 'darwin' +subsystem = 'macos' +cpu_family = 'aarch64' +cpu = 'arm64' +endian = 'little' diff --git a/cross/maccatalyst-x86_64.ini b/cross/maccatalyst-x86_64.ini new file mode 100644 index 00000000..fbc9be10 --- /dev/null +++ b/cross/maccatalyst-x86_64.ini @@ -0,0 +1,22 @@ +# Mac Catalyst x86_64 cross-file for Meson +# Requires: Xcode with macOS SDK installed + +[constants] +sdk_path = '/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk' + +[binaries] +c = 'clang' +cpp = 'clang++' +ar = 'ar' +strip = 'strip' + +[built-in options] +c_args = ['-isysroot', sdk_path, '-target', 'x86_64-apple-ios14.0-macabi', '-std=gnu11', '-stdlib=libc++', '-fembed-bitcode'] +cpp_args = ['-isysroot', sdk_path, '-target', 'x86_64-apple-ios14.0-macabi', '-std=gnu++11', '-stdlib=libc++', '-fembed-bitcode', '-fpermissive'] + +[host_machine] +system = 'darwin' +subsystem = 'macos' +cpu_family = 'x86_64' +cpu = 'x86_64' +endian = 'little' diff --git a/cross/windows-clang64.ini b/cross/windows-clang64.ini new file mode 100644 index 00000000..8f0fef4d --- /dev/null +++ b/cross/windows-clang64.ini @@ -0,0 +1,19 @@ +# Windows MSYS2 CLANG64 cross-file for Meson +# Requires: MSYS2 with CLANG64 environment + +[binaries] +c = 'x86_64-w64-mingw32-gcc' +cpp = 'x86_64-w64-mingw32-g++' +ar = 'x86_64-w64-mingw32-ar' +strip = 'x86_64-w64-mingw32-strip' +windres = 'x86_64-w64-mingw32-windres' + +[built-in options] +c_args = ['-Wa,-mbig-obj'] +cpp_args = ['-fpermissive', '-Wa,-mbig-obj'] + +[host_machine] +system = 'windows' +cpu_family = 'x86_64' +cpu = 'x86_64' +endian = 'little' diff --git a/meson.build b/meson.build new file mode 100644 index 00000000..577cbb53 --- /dev/null +++ b/meson.build @@ -0,0 +1,391 @@ +project('giac', 'c', 'cpp', + version : '1.9.0', + meson_version : '>= 1.2.0', + default_options : ['cpp_std=c++11'], +) + +# ---------- Options ---------- + +giac_version = get_option('giac_version') + +# ---------- Compilers ---------- + +cc = meson.get_compiler('c') +cpp = meson.get_compiler('cpp') + +# ---------- Source files (51 .cc + 1 .c from src/giac/cpp/) ---------- + +giac_sources = files( + 'src/giac/cpp/alg_ext.cc', + 'src/giac/cpp/cocoa.cc', + 'src/giac/cpp/csturm.cc', + 'src/giac/cpp/derive.cc', + 'src/giac/cpp/desolve.cc', + 'src/giac/cpp/ezgcd.cc', + 'src/giac/cpp/freeglut_stroke_roman.c', + 'src/giac/cpp/gauss.cc', + 'src/giac/cpp/gausspol.cc', + 'src/giac/cpp/gen.cc', + 'src/giac/cpp/global.cc', + 'src/giac/cpp/help.cc', + 'src/giac/cpp/identificateur.cc', + 'src/giac/cpp/ifactor.cc', + 'src/giac/cpp/index.cc', + 'src/giac/cpp/input_lexer.cc', + 'src/giac/cpp/input_parser.cc', + 'src/giac/cpp/intg.cc', + 'src/giac/cpp/intgab.cc', + 'src/giac/cpp/isom.cc', + 'src/giac/cpp/lin.cc', + 'src/giac/cpp/maple.cc', + 'src/giac/cpp/mathml.cc', + 'src/giac/cpp/misc.cc', + 'src/giac/cpp/modfactor.cc', + 'src/giac/cpp/modpoly.cc', + 'src/giac/cpp/moyal.cc', + 'src/giac/cpp/opengl.cc', + 'src/giac/cpp/pari.cc', + 'src/giac/cpp/permu.cc', + 'src/giac/cpp/plot.cc', + 'src/giac/cpp/plot3d.cc', + 'src/giac/cpp/prog.cc', + 'src/giac/cpp/quater.cc', + 'src/giac/cpp/risch.cc', + 'src/giac/cpp/rpn.cc', + 'src/giac/cpp/series.cc', + 'src/giac/cpp/solve.cc', + 'src/giac/cpp/sparse.cc', + 'src/giac/cpp/subst.cc', + 'src/giac/cpp/sym2poly.cc', + 'src/giac/cpp/symbolic.cc', + 'src/giac/cpp/tex.cc', + 'src/giac/cpp/threaded.cc', + 'src/giac/cpp/ti89.cc', + 'src/giac/cpp/tinymt32.cc', + 'src/giac/cpp/TmpFGLM.cpp', + 'src/giac/cpp/TmpLESystemSolver.cpp', + 'src/giac/cpp/unary.cc', + 'src/giac/cpp/usual.cc', + 'src/giac/cpp/vecteur.cc', +) + +# ---------- Common preprocessor defines (all platforms) ---------- + +common_defines = [ + '-DGIAC_GGB', + '-DIN_GIAC', + '-DGIAC_GENERIC_CONSTANTS', + '-DHAVE_NO_HOME_DIRECTORY', + '-DTIMEOUT', + '-DHAVE_LIBMPFR', + '-DVERSION="' + giac_version + '"', +] + +# ---------- Platform-conditional defines ---------- + +platform_defines = [] +is_ios = false +is_catalyst = false + +if host_machine.system() == 'linux' + platform_defines += [ + '-DHAVE_UNISTD_H', + '-DHAVE_SYS_TIMES_H', + '-DHAVE_SYS_TIME_H', + '-DHAVE_SYSCONF', + '-DHAVE_LIBPTHREAD', + '-DSIZEOF_VOID_P=8', + ] +elif host_machine.system() == 'darwin' + # Check if this is iOS/Catalyst via subsystem (Meson >= 1.2.0) + # iOS cross-files set subsystem='ios', Catalyst set subsystem='macos' + host_subsystem = host_machine.subsystem() + is_ios = host_subsystem == 'ios' + is_catalyst = meson.is_cross_build() and host_subsystem == 'macos' + if is_ios or is_catalyst + # iOS and Mac Catalyst share the same defines + platform_defines += [ + '-DAPPLE_SMART', + '-DNO_GETTEXT', + '-DNO_SCANDIR', + '-DOSX_10_9_CXX', + '-DHAVE_CONFIG_H', + '-D_IOS_FIX_', + '-DHAVE_UNISTD_H', + '-DHAVE_SYS_TIMES_H', + '-DHAVE_SYS_TIME_H', + ] + else + # macOS desktop (native build) + platform_defines += [ + '-DHAVE_UNISTD_H', + '-DAPPLE_SMART', + '-DNO_SCANDIR', + '-DNO_GETTEXT', + '-DHAVE_SYS_TIMES_H', + '-DHAVE_SYS_TIME_H', + ] + endif +elif host_machine.system() == 'windows' + platform_defines += [ + '-DGIAC_MPQS', + '-D__MINGW_H', + '-DMINGW32', + '-DHAVE_NO_SYS_TIMES_H', + '-DHAVE_NO_SYS_RESOURCE_WAIT_H', + '-DHAVE_NO_PWD_H', + '-DHAVE_NO_CWD', + '-DNO_CLOCK', + '-Dusleep=', + '-DYY_NO_UNISTD_H', + ] +elif host_machine.system() == 'android' + platform_defines += [ + '-DHAVE_UNISTD_H', + '-DNO_BSD', + '-DHAVE_CONFIG_H', + ] +elif host_machine.system() == 'emscripten' + platform_defines += [ + '-DHAVE_UNISTD_H', + '-DHAVE_CONFIG_H', + '-DEMCC2', + '-Dgammaf=tgammaf', + ] +endif + +add_project_arguments(common_defines + platform_defines, language : ['c', 'cpp']) + +# ---------- Common compiler flags ---------- + +common_cpp_args = [] +common_c_args = [] + +if cpp.get_id() != 'msvc' + common_cpp_args += ['-fpermissive', '-fexceptions'] + common_c_args += ['-fexceptions'] +endif + +add_project_arguments(common_cpp_args, language : 'cpp') +add_project_arguments(common_c_args, language : 'c') + +# ---------- Include directories ---------- + +giac_inc = include_directories('src/giac/headers') + +jdk_inc_dirs = [include_directories('src/jni/jdkHeaders')] +if host_machine.system() == 'darwin' + jdk_inc_dirs += include_directories('src/jni/jdkHeaders/darwin') +elif host_machine.system() == 'linux' or host_machine.system() == 'android' + jdk_inc_dirs += include_directories('src/jni/jdkHeaders/linux') +elif host_machine.system() == 'windows' + jdk_inc_dirs += include_directories('src/jni/jdkHeaders/win') +endif + +# Android architecture-specific headers +android_inc = [] +if host_machine.system() == 'android' + if host_machine.cpu_family() == 'arm' + android_inc = [include_directories('src/giac/headers/android/arm-v7')] + elif host_machine.cpu_family() == 'aarch64' + android_inc = [include_directories('src/giac/headers/android/arm-v8')] + elif host_machine.cpu_family() == 'x86' + android_inc = [include_directories('src/giac/headers/android/x86')] + elif host_machine.cpu_family() == 'x86_64' + android_inc = [include_directories('src/giac/headers/android/x86-64')] + endif +endif + +simpleinterface_inc = include_directories('src/simpleInterface/headers') + +# ---------- Dependencies: GMP and MPFR ---------- +# MPFR must precede GMP in link order (MPFR FAQ). + +prebuilt_base = meson.project_source_root() / 'src' / 'jni' / 'prebuilt' + +if meson.is_cross_build() + # Determine prebuilt directory from host_machine + if host_machine.system() == 'android' + if host_machine.cpu_family() == 'arm' + prebuilt_dir = prebuilt_base / 'android' / 'arm-v7' + elif host_machine.cpu_family() == 'aarch64' + prebuilt_dir = prebuilt_base / 'android' / 'arm-v8' + elif host_machine.cpu_family() == 'x86' + prebuilt_dir = prebuilt_base / 'android' / 'x86' + elif host_machine.cpu_family() == 'x86_64' + prebuilt_dir = prebuilt_base / 'android' / 'x86-64' + endif + elif host_machine.system() == 'darwin' + if host_machine.subsystem() == 'ios' + if host_machine.cpu_family() == 'aarch64' + prebuilt_dir = prebuilt_base / 'ios' / 'arm64' + elif host_machine.cpu_family() == 'x86_64' + prebuilt_dir = prebuilt_base / 'iphonesimulator' / 'x86_64' + endif + else + if host_machine.cpu_family() == 'aarch64' + prebuilt_dir = prebuilt_base / 'maccatalyst' / 'arm64' + elif host_machine.cpu_family() == 'x86_64' + prebuilt_dir = prebuilt_base / 'maccatalyst' / 'x86_64' + endif + endif + elif host_machine.system() == 'linux' + if host_machine.cpu_family() == 'arm' + prebuilt_dir = prebuilt_base / 'linux' / 'arm-v7' + elif host_machine.cpu_family() == 'x86_64' + prebuilt_dir = prebuilt_base / 'linux' / 'x86-64' + endif + elif host_machine.system() == 'windows' + prebuilt_dir = prebuilt_base / 'windows' / 'clang64' + elif host_machine.system() == 'emscripten' + prebuilt_dir = meson.project_source_root() / 'src' / 'giac.js' / 'prebuilt' + endif + + mpfr_dep = declare_dependency( + dependencies : cc.find_library('mpfr', dirs : prebuilt_dir, static : true), + ) + gmp_dep = declare_dependency( + dependencies : cc.find_library('gmp', dirs : prebuilt_dir, static : true), + ) +else + # Native desktop build: use system libraries + gmp_dep = dependency('gmp') + # MPFR's pkg-config emits a bare -lgmp without GMP's -L path. + # On systems where GMP and MPFR are in separate directories (e.g. Homebrew), + # this causes a linker error. We work around this by using MPFR's + # pkg-config in pure mode (no unmanaged dependencies) and depending on + # our GMP dependency explicitly. + _mpfr_pc = dependency('mpfr', method : 'pkg-config') + gmp_libdir = gmp_dep.get_variable(pkgconfig : 'libdir', default_value : '') + if gmp_libdir != '' + mpfr_dep = declare_dependency( + dependencies : [_mpfr_pc], + link_args : ['-L' + gmp_libdir], + ) + else + mpfr_dep = _mpfr_pc + endif +endif + +# ---------- Target: libgiac (static library, all platforms) ---------- + +libgiac = static_library('giac', + giac_sources, + include_directories : [giac_inc] + android_inc, + dependencies : [mpfr_dep, gmp_dep], +) + +giac_dep = declare_dependency( + link_with : libgiac, + include_directories : [giac_inc], + dependencies : [mpfr_dep, gmp_dep], +) + +# ---------- Target: libjavagiac (shared library, conditional on JNI) ---------- + +# Use vendored JDK headers from src/jni/jdkHeaders/ instead of Meson's +# dependency('jni') which requires Java as a project language. +jni_dep = declare_dependency(include_directories : jdk_inc_dirs) +build_javagiac = not get_option('with_jni').disabled() and host_machine.system() != 'emscripten' and not is_ios and not is_catalyst + +if build_javagiac + javagiac_link_args = [] + javagiac_deps = [giac_dep, jni_dep] + + if host_machine.system() == 'darwin' and not is_ios + javagiac_link_args += [ + '-framework', 'Accelerate', + '-framework', 'CoreFoundation', + '-lc++', + '-lpthread', + ] + elif host_machine.system() == 'linux' + javagiac_link_args += ['-static-libgcc', '-static-libstdc++', '-s'] + elif host_machine.system() == 'windows' + javagiac_link_args += [ + '-Wl,--add-stdcall-alias', + '-static-libgcc', + '-static-libstdc++', + '-static', + ] + elif host_machine.system() == 'android' + javagiac_link_args += ['-s'] + endif + + javagiac_kwargs = { + 'sources' : files('src/jni/cpp/giac_wrap.cxx'), + 'include_directories' : [giac_inc] + jdk_inc_dirs + android_inc, + 'dependencies' : javagiac_deps, + 'link_args' : javagiac_link_args, + } + + if host_machine.system() == 'windows' + javagiac_kwargs += { + 'vs_module_defs' : 'src/jni/giac.def', + } + endif + + libjavagiac = shared_library('javagiac', kwargs : javagiac_kwargs) +endif + +# ---------- Target: minigiac (executable, native desktop only) ---------- + +if get_option('with_minigiac') and not meson.is_cross_build() + minigiac_link_args = [] + if host_machine.system() == 'linux' + minigiac_link_args += ['-static-libgcc', '-static-libstdc++'] + endif + + minigiac = executable('minigiac', + 'src/minigiac/cpp/minigiac.cc', + include_directories : [giac_inc], + dependencies : [giac_dep], + link_args : minigiac_link_args, + ) +endif + +# ---------- Target: libsimpleinterface (iOS/Catalyst only) ---------- + +if get_option('with_simpleinterface') + libsimpleinterface = static_library('simpleinterface', + files( + 'src/simpleInterface/cpp/ContextBridge.cpp', + 'src/simpleInterface/cpp/GenBridge.cpp', + ), + include_directories : [giac_inc, simpleinterface_inc], + dependencies : [giac_dep], + ) +endif + +# ---------- Target: WASM (Emscripten only) ---------- + +if host_machine.system() == 'emscripten' + wasm_link_args = [ + '-s', 'EXPORTED_FUNCTIONS=["_caseval"]', + '-s', 'EXPORTED_RUNTIME_METHODS=["cwrap"]', + '-s', 'TOTAL_MEMORY=67108864', + '-s', 'WASM=1', + '-s', 'ALLOW_MEMORY_GROWTH=1', + '-s', 'NO_EXIT_RUNTIME=1', + '-s', 'PRECISE_I64_MATH=1', + '-fwasm-exceptions', + '--js-library', meson.project_source_root() / 'src' / 'giac.js' / 'js' / 'time.js', + ] + + giac_wasm = executable('giacggb', + giac_sources, + include_directories : [giac_inc], + dependencies : [mpfr_dep, gmp_dep], + link_args : wasm_link_args, + ) + + if get_option('with_wasm_embed') + embed_script = find_program('scripts/embed-wasm.sh') + custom_target('giac_embedded_js', + output : 'giac.wasm.js', + depends : giac_wasm, + command : [embed_script, giac_wasm.full_path(), '@OUTPUT@'], + build_by_default : true, + ) + endif +endif diff --git a/meson_options.txt b/meson_options.txt new file mode 100644 index 00000000..12661825 --- /dev/null +++ b/meson_options.txt @@ -0,0 +1,10 @@ +option('giac_version', type : 'string', value : '1.2.4', + description : 'Giac version string for -DVERSION') +option('with_jni', type : 'feature', value : 'auto', + description : 'Build JNI shared library (auto = build if JDK found)') +option('with_minigiac', type : 'boolean', value : true, + description : 'Build minigiac executable (native desktop only)') +option('with_simpleinterface', type : 'boolean', value : false, + description : 'Build SimpleInterface static library (iOS/Catalyst)') +option('with_wasm_embed', type : 'boolean', value : true, + description : 'Post-process WASM into single embedded JS file') diff --git a/scripts/build-all.sh b/scripts/build-all.sh new file mode 100755 index 00000000..dd3cf1cb --- /dev/null +++ b/scripts/build-all.sh @@ -0,0 +1,281 @@ +#!/usr/bin/env bash +# build-all.sh — Build Giac for all supported platforms +# +# Usage: +# ./scripts/build-all.sh # build everything +# ./scripts/build-all.sh native android # build specific platforms +# ./scripts/build-all.sh --clean # wipe build dirs first +# ./scripts/build-all.sh --verbose # show full compiler output +# +# Platform selectors: native, android, ios, windows, wasm +# +# Prerequisites (macOS): +# brew install meson ninja gmp mpfr mingw-w64 emscripten +# Android NDK path set in cross/android-*.ini files +# Xcode with iOS/macOS SDKs for iOS/Catalyst builds + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +cd "$ROOT_DIR" + +# ---------- Parse arguments ---------- + +CLEAN=false +VERBOSE="" +PLATFORMS=() + +for arg in "$@"; do + case "$arg" in + --clean) CLEAN=true ;; + --verbose) VERBOSE="-v" ;; + native|android|ios|windows|wasm) + PLATFORMS+=("$arg") ;; + *) + echo "Unknown argument: $arg" >&2 + echo "Usage: $0 [--clean] [--verbose] [native] [android] [ios] [windows] [wasm]" >&2 + exit 1 ;; + esac +done + +# Default: build all platforms +if [ ${#PLATFORMS[@]} -eq 0 ]; then + PLATFORMS=(native android ios windows wasm) +fi + +# ---------- Helpers ---------- + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BOLD='\033[1m' +NC='\033[0m' + +PASS=0 +FAIL=0 +SKIP=0 +declare -a RESULTS=() + +log_header() { + echo "" + echo -e "${BOLD}=== $1 ===${NC}" +} + +record_result() { + local target="$1" + local status="$2" + local details="${3:-}" + RESULTS+=("$status|$target|$details") + if [ "$status" = "PASS" ]; then + ((PASS++)) + echo -e " ${GREEN}PASS${NC} $target${details:+ — $details}" + elif [ "$status" = "FAIL" ]; then + ((FAIL++)) + echo -e " ${RED}FAIL${NC} $target${details:+ — $details}" + else + ((SKIP++)) + echo -e " ${YELLOW}SKIP${NC} $target${details:+ — $details}" + fi +} + +# Setup + compile a single target +# Arguments: builddir cross-file [extra-meson-args...] +build_target() { + local builddir="$1" + local crossfile="$2" + shift 2 + local extra_args=("$@") + + if $CLEAN && [ -d "$builddir" ]; then + rm -rf "$builddir" + fi + + local setup_cmd=(meson setup "$builddir") + if [ -n "$crossfile" ]; then + setup_cmd+=(--cross-file "$crossfile") + fi + if [ ${#extra_args[@]} -gt 0 ]; then + setup_cmd+=("${extra_args[@]}") + fi + + # Setup (reconfigure if already exists, wipe on failure) + if [ -d "$builddir" ]; then + "${setup_cmd[@]}" --reconfigure >/dev/null 2>&1 || \ + "${setup_cmd[@]}" --wipe >/dev/null 2>&1 + else + "${setup_cmd[@]}" >/dev/null 2>&1 + fi + + # Compile + if [ -n "$VERBOSE" ]; then + meson compile -C "$builddir" -v 2>&1 + else + meson compile -C "$builddir" 2>&1 + fi +} + +# Check if a command exists +require_cmd() { + if ! command -v "$1" &>/dev/null; then + return 1 + fi + return 0 +} + +# List notable files in a build directory +list_artifacts() { + local builddir="$1" + local artifacts=() + for f in "$builddir"/libgiac.a "$builddir"/libjavagiac.* \ + "$builddir"/libsimpleinterface.a "$builddir"/minigiac \ + "$builddir"/giacggb.wasm "$builddir"/giac.wasm.js; do + if [ -f "$f" ]; then + artifacts+=("$(basename "$f")") + fi + done + echo "${artifacts[*]}" +} + +# ---------- Check core prerequisites ---------- + +if ! require_cmd meson; then + echo -e "${RED}Error: meson not found. Install with: brew install meson${NC}" >&2 + exit 1 +fi + +if ! require_cmd ninja; then + echo -e "${RED}Error: ninja not found. Install with: brew install ninja${NC}" >&2 + exit 1 +fi + +echo -e "${BOLD}Giac Meson Build — All Platforms${NC}" +echo "Platforms: ${PLATFORMS[*]}" +echo "Root: $ROOT_DIR" + +# ---------- Native desktop ---------- + +if [[ " ${PLATFORMS[*]} " == *" native "* ]]; then + log_header "Native Desktop" + if build_target builddir "" >/dev/null 2>&1; then + artifacts=$(list_artifacts builddir) + record_result "native-desktop" "PASS" "$artifacts" + # Smoke test + if [ -x builddir/minigiac ]; then + result=$(echo '1+1' | ./builddir/minigiac 2>/dev/null | grep -o '^[0-9]*<<.*' | head -1 | sed 's/^[0-9]*<< *//' || true) + if [ "$result" = "2" ]; then + record_result "native-smoke-test" "PASS" "echo '1+1' -> 2" + else + record_result "native-smoke-test" "FAIL" "got: $result" + fi + fi + else + record_result "native-desktop" "FAIL" + fi +fi + +# ---------- Android (4 ABIs) ---------- + +if [[ " ${PLATFORMS[*]} " == *" android "* ]]; then + log_header "Android" + # Auto-configure cross-files if cross/local/ doesn't exist yet + if [ ! -d cross/local ] || [ ! -f cross/local/android-arm64.ini ]; then + echo " Configuring Android cross-files..." + if ! "$SCRIPT_DIR/configure-cross.sh"; then + record_result "android (all)" "SKIP" "NDK not found — run: ANDROID_NDK_HOME=/path/to/ndk ./scripts/configure-cross.sh" + PLATFORMS=("${PLATFORMS[@]/android/}") + fi + fi + for abi in arm arm64 x86 x86_64; do + crossfile="cross/local/android-${abi}.ini" + builddir="builddir-android-${abi}" + if [ ! -f "$crossfile" ]; then + record_result "android-${abi}" "SKIP" "cross-file not found" + continue + fi + if build_target "$builddir" "$crossfile" >/dev/null 2>&1; then + artifacts=$(list_artifacts "$builddir") + record_result "android-${abi}" "PASS" "$artifacts" + else + record_result "android-${abi}" "FAIL" + fi + done +fi + +# ---------- iOS / Mac Catalyst (5 variants) ---------- + +if [[ " ${PLATFORMS[*]} " == *" ios "* ]]; then + log_header "iOS / Mac Catalyst" + for variant in ios-arm64 ios-sim-x86_64 ios-sim-arm64 \ + maccatalyst-x86_64 maccatalyst-arm64; do + crossfile="cross/${variant}.ini" + builddir="builddir-${variant}" + if [ ! -f "$crossfile" ]; then + record_result "${variant}" "SKIP" "cross-file not found" + continue + fi + if build_target "$builddir" "$crossfile" -Dwith_simpleinterface=true >/dev/null 2>&1; then + artifacts=$(list_artifacts "$builddir") + record_result "${variant}" "PASS" "$artifacts" + else + record_result "${variant}" "FAIL" + fi + done +fi + +# ---------- Windows (MinGW cross) ---------- + +if [[ " ${PLATFORMS[*]} " == *" windows "* ]]; then + log_header "Windows (MinGW cross)" + if ! require_cmd x86_64-w64-mingw32-gcc; then + record_result "windows-x86_64" "SKIP" "mingw-w64 not installed (brew install mingw-w64)" + else + if build_target builddir-windows cross/windows-clang64.ini >/dev/null 2>&1; then + artifacts=$(list_artifacts builddir-windows) + record_result "windows-x86_64" "PASS" "$artifacts" + else + record_result "windows-x86_64" "FAIL" + fi + fi +fi + +# ---------- Emscripten / WASM ---------- + +if [[ " ${PLATFORMS[*]} " == *" wasm "* ]]; then + log_header "Emscripten / WASM" + if ! require_cmd emcc; then + record_result "wasm32" "SKIP" "emscripten not installed (brew install emscripten)" + else + if build_target builddir-wasm cross/emscripten-wasm32.ini >/dev/null 2>&1; then + artifacts=$(list_artifacts builddir-wasm) + record_result "wasm32" "PASS" "$artifacts" + else + record_result "wasm32" "FAIL" + fi + fi +fi + +# ---------- Summary ---------- + +echo "" +echo -e "${BOLD}========== Build Summary ==========${NC}" +printf "%-30s %-6s %s\n" "TARGET" "STATUS" "ARTIFACTS" +printf "%-30s %-6s %s\n" "------" "------" "---------" + +for entry in "${RESULTS[@]}"; do + IFS='|' read -r status target details <<< "$entry" + case "$status" in + PASS) color="$GREEN" ;; + FAIL) color="$RED" ;; + *) color="$YELLOW" ;; + esac + printf "%-30s ${color}%-6s${NC} %s\n" "$target" "$status" "$details" +done + +echo "" +echo -e "Total: ${GREEN}${PASS} passed${NC}, ${RED}${FAIL} failed${NC}, ${YELLOW}${SKIP} skipped${NC}" + +# Exit with error if any build failed +if [ "$FAIL" -gt 0 ]; then + exit 1 +fi diff --git a/scripts/build-xcframework.sh b/scripts/build-xcframework.sh new file mode 100755 index 00000000..ac862841 --- /dev/null +++ b/scripts/build-xcframework.sh @@ -0,0 +1,136 @@ +#!/usr/bin/env bash +# build-xcframework.sh — Assemble Giac.xcframework from Meson build outputs +# +# Usage: ./scripts/build-xcframework.sh [output-dir] +# +# Expects Meson build directories: +# builddir-ios-arm64/ +# builddir-ios-sim-x86_64/ +# builddir-ios-sim-arm64/ +# builddir-maccatalyst-x86_64/ +# builddir-maccatalyst-arm64/ +# +# Each must contain libgiac.a and libsimpleinterface.a + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +OUTPUT_DIR="${1:-${ROOT_DIR}/build/framework}" + +PREBUILT="${ROOT_DIR}/src/jni/prebuilt" +HEADERS="${ROOT_DIR}/src/simpleInterface/headers" +FRAMEWORK_SRC="${ROOT_DIR}/src/simpleInterface/framework" + +MERGED="${OUTPUT_DIR}/merged" +FRAMEWORKS="${OUTPUT_DIR}/frameworks" +XCFRAMEWORK="${OUTPUT_DIR}/Giac.xcframework" + +# Cleanup previous output +rm -rf "$OUTPUT_DIR" + +# ---------- Step 1: Merge static libraries per architecture ---------- + +merge_libs() { + local variant="$1" + local builddir="$2" + local prebuilt_dir="$3" + local out_dir="${MERGED}/${variant}" + + mkdir -p "$out_dir" + libtool -static -o "${out_dir}/Giac.a" \ + "${builddir}/libgiac.a" \ + "${builddir}/libsimpleinterface.a" \ + "${prebuilt_dir}/libgmp.a" \ + "${prebuilt_dir}/libmpfr.a" +} + +echo "Merging static libraries..." + +merge_libs "ios_arm64" \ + "${ROOT_DIR}/builddir-ios-arm64" \ + "${PREBUILT}/ios/arm64" + +merge_libs "iphonesimulator_arm64" \ + "${ROOT_DIR}/builddir-ios-sim-arm64" \ + "${PREBUILT}/iphonesimulator/arm64" + +merge_libs "iphonesimulator_x86_64" \ + "${ROOT_DIR}/builddir-ios-sim-x86_64" \ + "${PREBUILT}/iphonesimulator/x86_64" + +merge_libs "maccatalyst_arm64" \ + "${ROOT_DIR}/builddir-maccatalyst-arm64" \ + "${PREBUILT}/maccatalyst/arm64" + +merge_libs "maccatalyst_x86_64" \ + "${ROOT_DIR}/builddir-maccatalyst-x86_64" \ + "${PREBUILT}/maccatalyst/x86_64" + +# ---------- Step 2: Create fat binaries with lipo ---------- + +echo "Creating fat binaries..." + +mkdir -p "${MERGED}/iphonesimulator" "${MERGED}/maccatalyst" "${MERGED}/ios" + +# iOS device (single arch, but still copy through lipo for consistency) +lipo -create \ + "${MERGED}/ios_arm64/Giac.a" \ + -output "${MERGED}/ios/Giac.a" + +# iOS Simulator (arm64 + x86_64) +lipo -create \ + "${MERGED}/iphonesimulator_arm64/Giac.a" \ + "${MERGED}/iphonesimulator_x86_64/Giac.a" \ + -output "${MERGED}/iphonesimulator/Giac.a" + +# Mac Catalyst (arm64 + x86_64) +lipo -create \ + "${MERGED}/maccatalyst_arm64/Giac.a" \ + "${MERGED}/maccatalyst_x86_64/Giac.a" \ + -output "${MERGED}/maccatalyst/Giac.a" + +# ---------- Step 3: Create framework bundles ---------- + +create_framework() { + local variant="$1" + local lib_path="$2" + local plist_name="${3:-Info.plist}" + local out="${FRAMEWORKS}/${variant}/Giac.framework" + + mkdir -p "${out}/Headers" "${out}/Resources" + cp "${lib_path}" "${out}/Giac" + cp "${HEADERS}"/*.hpp "${out}/Headers/" + if [ -f "${FRAMEWORK_SRC}/${plist_name}" ]; then + cp "${FRAMEWORK_SRC}/${plist_name}" "${out}/Resources/Info.plist" + fi +} + +echo "Creating framework bundles..." + +create_framework "ios" "${MERGED}/ios/Giac.a" "Info.plist" +create_framework "iphonesimulator" "${MERGED}/iphonesimulator/Giac.a" "framework.plist" +create_framework "maccatalyst" "${MERGED}/maccatalyst/Giac.a" + +# ---------- Step 4: Assemble xcframework ---------- + +echo "Assembling Giac.xcframework..." + +rm -rf "$XCFRAMEWORK" +mkdir -p "$XCFRAMEWORK" + +# Copy framework variants into xcframework structure +cp -R "${FRAMEWORKS}/ios/Giac.framework" "${XCFRAMEWORK}/ios-arm64/Giac.framework" +mkdir -p "${XCFRAMEWORK}/ios-arm64" +cp -R "${FRAMEWORKS}/ios/Giac.framework" "${XCFRAMEWORK}/ios-arm64/Giac.framework" + +mkdir -p "${XCFRAMEWORK}/ios-arm64_x86_64-simulator" +cp -R "${FRAMEWORKS}/iphonesimulator/Giac.framework" "${XCFRAMEWORK}/ios-arm64_x86_64-simulator/Giac.framework" + +mkdir -p "${XCFRAMEWORK}/ios-arm64_x86_64-maccatalyst" +cp -R "${FRAMEWORKS}/maccatalyst/Giac.framework" "${XCFRAMEWORK}/ios-arm64_x86_64-maccatalyst/Giac.framework" + +# Copy xcframework plist +cp "${FRAMEWORK_SRC}/xcframework.plist" "${XCFRAMEWORK}/Info.plist" + +echo "Done: ${XCFRAMEWORK}" diff --git a/scripts/configure-cross.sh b/scripts/configure-cross.sh new file mode 100755 index 00000000..2771d424 --- /dev/null +++ b/scripts/configure-cross.sh @@ -0,0 +1,122 @@ +#!/usr/bin/env bash +# configure-cross.sh — Generate Android cross-compilation .ini files from templates +# +# Reads cross/android-*.ini templates (containing @NDK@ and @NDK_HOST@ placeholders) +# and writes configured files to cross/local/ (gitignored). +# +# Usage: +# ./scripts/configure-cross.sh # auto-detect everything +# ANDROID_NDK_HOME=/path/to/ndk ./scripts/configure-cross.sh # explicit NDK +# +# Environment variables: +# ANDROID_NDK_HOME — Path to Android NDK root (auto-detected if not set) +# ANDROID_HOME — Fallback: searches for NDK inside $ANDROID_HOME/ndk/ + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +CROSS_DIR="$ROOT_DIR/cross" +LOCAL_DIR="$CROSS_DIR/local" + +# ---------- Detect host platform for NDK toolchain ---------- + +detect_ndk_host() { + local os arch + os="$(uname -s)" + arch="$(uname -m)" + + case "$os" in + Linux) os="linux" ;; + Darwin) os="darwin" ;; + MINGW*|MSYS*|CYGWIN*) os="windows" ;; + *) echo "Unsupported OS: $os" >&2; return 1 ;; + esac + + case "$arch" in + x86_64|amd64) arch="x86_64" ;; + aarch64|arm64) arch="x86_64" ;; # NDK ships x86_64 toolchain on ARM Macs (Rosetta) + *) echo "Unsupported arch: $arch" >&2; return 1 ;; + esac + + echo "${os}-${arch}" +} + +# ---------- Detect Android NDK path ---------- + +detect_ndk_path() { + # 1. Explicit environment variable + if [ -n "${ANDROID_NDK_HOME:-}" ] && [ -d "$ANDROID_NDK_HOME" ]; then + echo "$ANDROID_NDK_HOME" + return + fi + + # 2. Search inside ANDROID_HOME/ndk/ (pick latest version) + if [ -n "${ANDROID_HOME:-}" ] && [ -d "$ANDROID_HOME/ndk" ]; then + local latest + latest=$(ls -1d "$ANDROID_HOME/ndk"/*/ 2>/dev/null | sort -V | tail -1) + if [ -n "$latest" ]; then + echo "${latest%/}" + return + fi + fi + + # 3. Common default locations + local search_dirs=( + "$HOME/Library/Android/sdk/ndk" # macOS default + "$HOME/Android/Sdk/ndk" # Linux default + "/usr/local/lib/android/sdk/ndk" # CI environments + ) + for dir in "${search_dirs[@]}"; do + if [ -d "$dir" ]; then + local latest + latest=$(ls -1d "$dir"/*/ 2>/dev/null | sort -V | tail -1) + if [ -n "$latest" ]; then + echo "${latest%/}" + return + fi + fi + done + + return 1 +} + +# ---------- Main ---------- + +NDK_HOST=$(detect_ndk_host) +echo "NDK host platform: $NDK_HOST" + +if NDK_PATH=$(detect_ndk_path); then + echo "NDK path: $NDK_PATH" +else + echo "Error: Android NDK not found." >&2 + echo "Set ANDROID_NDK_HOME or install the NDK via Android Studio." >&2 + exit 1 +fi + +# Verify the toolchain exists +TOOLCHAIN_DIR="$NDK_PATH/toolchains/llvm/prebuilt/$NDK_HOST" +if [ ! -d "$TOOLCHAIN_DIR" ]; then + echo "Error: Toolchain not found at $TOOLCHAIN_DIR" >&2 + echo "Check NDK installation and host platform." >&2 + exit 1 +fi + +# Generate configured cross-files in cross/local/ +mkdir -p "$LOCAL_DIR" + +count=0 +for template in "$CROSS_DIR"/android-*.ini; do + name=$(basename "$template") + output="$LOCAL_DIR/$name" + sed -e "s|@NDK@|$NDK_PATH|g" \ + -e "s|@NDK_HOST@|$NDK_HOST|g" \ + "$template" > "$output" + echo "Generated: cross/local/$name" + ((count++)) +done + +echo "" +echo "Done: $count cross-file(s) generated in cross/local/" +echo "Build with:" +echo " meson setup builddir-android-arm64 --cross-file cross/local/android-arm64.ini" diff --git a/scripts/embed-wasm.sh b/scripts/embed-wasm.sh new file mode 100755 index 00000000..ac5a0c31 --- /dev/null +++ b/scripts/embed-wasm.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# embed-wasm.sh — Post-process Emscripten output into a single embeddable JS file +# +# Usage: embed-wasm.sh /giacggb[.js] /giac.wasm.js +# +# Steps: +# 1. Base64-encode giacggb.wasm +# 2. Copy giacggb.js and replace the WASM filename with a data URI +# 3. Replace 'Module' with '__ggb__giac' (GeoGebra namespace) +# 4. Remove '"use asm";' directive + +set -euo pipefail + +if [ $# -ne 2 ]; then + echo "Usage: $0 " >&2 + exit 1 +fi + +INPUT_BASE="${1%.js}" +OUTPUT="$2" + +WASM_FILE="${INPUT_BASE}.wasm" +JS_FILE="${INPUT_BASE}.js" + +if [ ! -f "$WASM_FILE" ]; then + echo "Error: WASM file not found: $WASM_FILE" >&2 + exit 1 +fi + +if [ ! -f "$JS_FILE" ]; then + echo "Error: JS file not found: $JS_FILE" >&2 + exit 1 +fi + +# Base64-encode the WASM file to a temp file (avoids argument-length limits) +B64_FILE=$(mktemp) +trap 'rm -f "$B64_FILE"' EXIT + +if base64 --help 2>&1 | grep -q '\-w'; then + base64 -w0 "$WASM_FILE" > "$B64_FILE" +else + base64 -i "$WASM_FILE" -b0 2>/dev/null > "$B64_FILE" || base64 < "$WASM_FILE" | tr -d '\n' > "$B64_FILE" +fi + +# Use Python3 to do the replacements (handles arbitrarily large strings) +python3 - "$JS_FILE" "$B64_FILE" "$OUTPUT" << 'PYEOF' +import sys + +js_path, b64_path, out_path = sys.argv[1], sys.argv[2], sys.argv[3] + +with open(js_path, 'r') as f: + js = f.read() +with open(b64_path, 'r') as f: + b64 = f.read() + +js = js.replace('giacggb.wasm', 'data:application/wasm;base64,' + b64) +js = js.replace('Module', '__ggb__giac') +js = js.replace('"use asm";', '') + +with open(out_path, 'w') as f: + f.write(js) +PYEOF + +echo "Created: $OUTPUT"