diff --git a/.gitattributes b/.gitattributes index 5565f803..4a2c705f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -23,8 +23,9 @@ go.sum text eol=lf # R binding: `R CMD check` requires LF in sources, Makevars and shell scripts. *.R text eol=lf *.Rd text eol=lf +bindings/r/configure text eol=lf bindings/r/configure.win text eol=lf -bindings/r/src/Makevars text eol=lf +bindings/r/src/Makevars.in text eol=lf bindings/r/src/Makevars.win text eol=lf bindings/r/src/wickra.c text eol=lf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7959a4b4..c32dd04c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -868,23 +868,24 @@ jobs: - name: Install and test the R binding shell: bash - # github.workspace is a backslash path on Windows; configure.win (sh) and - # mingw need forward slashes. On Linux/macOS the rpath points at - # WICKRA_LIB_DIR; export the loader path too as a belt-and-suspenders. + # github.workspace is a backslash path on Windows; configure(.win) (sh) and + # mingw need forward slashes. WICKRA_*_DIR makes configure use the locally + # built C ABI (dev override) instead of downloading the release asset, so + # CI is version-independent. Deliberately do NOT set LD_LIBRARY_PATH / + # DYLD_LIBRARY_PATH: the lib is bundled into the package and must resolve + # via the rpath ($ORIGIN / @loader_path) baked by configure — that is the + # self-contained install path real users (and r-universe) get. run: | export WICKRA_INCLUDE_DIR="${WICKRA_INCLUDE_DIR//\\//}" export WICKRA_LIB_DIR="${WICKRA_LIB_DIR//\\//}" - export LD_LIBRARY_PATH="$WICKRA_LIB_DIR:$LD_LIBRARY_PATH" - export DYLD_LIBRARY_PATH="$WICKRA_LIB_DIR:$DYLD_LIBRARY_PATH" R CMD INSTALL bindings/r Rscript -e 'library(testthat); library(wickra); test_dir("bindings/r/tests/testthat", stop_on_failure = TRUE)' - name: Run the offline R examples shell: bash + # No loader-path exports: the installed package is self-contained (bundled + # lib + rpath), so the examples exercise exactly what end users run. run: | - export WICKRA_LIB_DIR="${WICKRA_LIB_DIR//\\//}" - export LD_LIBRARY_PATH="$WICKRA_LIB_DIR:$LD_LIBRARY_PATH" - export DYLD_LIBRARY_PATH="$WICKRA_LIB_DIR:$DYLD_LIBRARY_PATH" cd examples/r for f in streaming backtest multi_timeframe parallel_assets \ strategy_rsi_mean_reversion strategy_macd_adx strategy_bollinger_squeeze; do diff --git a/bindings/r/.Rbuildignore b/bindings/r/.Rbuildignore index d6f0c1b2..02c0d698 100644 --- a/bindings/r/.Rbuildignore +++ b/bindings/r/.Rbuildignore @@ -5,4 +5,9 @@ ^src/wickra\.dll$ ^src/wickra\.so$ ^src/symbols\.rds$ +^src/wickra\.h$ +^src/libwickra\.(so|dylib)$ +^src/wickra-c\.tar\.gz$ +^src/wickra-c$ +^src/Makevars$ ^\.gitignore$ diff --git a/bindings/r/DESCRIPTION b/bindings/r/DESCRIPTION index 043d4f28..2da9459d 100644 --- a/bindings/r/DESCRIPTION +++ b/bindings/r/DESCRIPTION @@ -12,8 +12,10 @@ URL: https://github.com/wickra-lib/wickra, https://docs.wickra.org BugReports: https://github.com/wickra-lib/wickra/issues Encoding: UTF-8 NeedsCompilation: yes -SystemRequirements: the Wickra C ABI library (libwickra); set WICKRA_INCLUDE_DIR - and WICKRA_LIB_DIR when installing from source. +SystemRequirements: the Wickra C ABI shared library, downloaded automatically at + install time from the matching GitHub release and bundled into the package. + Set WICKRA_INCLUDE_DIR and WICKRA_LIB_DIR to build against a locally built C + ABI instead (e.g. after `cargo build -p wickra-c --release`). Roxygen: list(markdown = TRUE) Suggests: testthat (>= 3.0.0) Config/testthat/edition: 3 diff --git a/bindings/r/configure b/bindings/r/configure new file mode 100755 index 00000000..76a8209d --- /dev/null +++ b/bindings/r/configure @@ -0,0 +1,82 @@ +#!/bin/sh +# Resolve the Wickra C ABI (header + shared library) for the Unix build. +# +# Self-contained by default: download the prebuilt wickra-c-.tar.gz +# release asset that matches this package's version and stage the shared library +# into src/ so the compiled wickra.so links against it and bundles it (via +# install.libs.R), found post-install through an rpath ($ORIGIN on Linux, +# @loader_path on macOS). Set WICKRA_INCLUDE_DIR + WICKRA_LIB_DIR to build +# against a locally built C ABI instead (dev override; e.g. +# `cargo build -p wickra-c --release`). Download/extract uses base R (always +# present at build time), so there is no curl/wget system dependency. +set -e + +inc="" +lib="" +if [ -n "${WICKRA_INCLUDE_DIR}" ] && [ -n "${WICKRA_LIB_DIR}" ]; then + echo "wickra: using C ABI from WICKRA_INCLUDE_DIR / WICKRA_LIB_DIR (dev override)" + inc="${WICKRA_INCLUDE_DIR}" + lib="${WICKRA_LIB_DIR}" +else + version=$(sed -n 's/^Version:[[:space:]]*//p' DESCRIPTION) + os=$(uname -s) + arch=$(uname -m) + case "${os}" in + Linux) + case "${arch}" in + x86_64) triple="x86_64-unknown-linux-gnu" ;; + aarch64 | arm64) triple="aarch64-unknown-linux-gnu" ;; + *) echo "wickra: unsupported Linux arch '${arch}' — set WICKRA_INCLUDE_DIR/WICKRA_LIB_DIR"; exit 1 ;; + esac + ;; + Darwin) + case "${arch}" in + x86_64) triple="x86_64-apple-darwin" ;; + arm64 | aarch64) triple="aarch64-apple-darwin" ;; + *) echo "wickra: unsupported macOS arch '${arch}' — set WICKRA_INCLUDE_DIR/WICKRA_LIB_DIR"; exit 1 ;; + esac + ;; + *) + echo "wickra: unsupported OS '${os}' — set WICKRA_INCLUDE_DIR/WICKRA_LIB_DIR" + exit 1 + ;; + esac + url="https://github.com/wickra-lib/wickra/releases/download/v${version}/wickra-c-${triple}.tar.gz" + echo "wickra: downloading C ABI ${triple} for v${version}" + "${R_HOME}/bin/Rscript" -e "download.file('${url}', 'src/wickra-c.tar.gz', mode = 'wb', quiet = TRUE)" \ + || { echo "wickra: failed to download ${url}"; exit 1; } + "${R_HOME}/bin/Rscript" -e "untar('src/wickra-c.tar.gz', exdir = 'src/wickra-c')" + inc="src/wickra-c/wickra-c-${triple}/include" + lib="src/wickra-c/wickra-c-${triple}/lib" +fi + +# Stage the header and the platform shared library into src/ so the package +# object links with -L. -lwickra and install.libs.R bundles the lib. +cp "${inc}/wickra.h" src/wickra.h +staged="" +for f in libwickra.so libwickra.dylib; do + if [ -f "${lib}/${f}" ]; then + cp "${lib}/${f}" "src/${f}" + staged="${f}" + fi +done +if [ -z "${staged}" ]; then + echo "wickra: no libwickra.so/.dylib found under ${lib}" + exit 1 +fi + +# rpath so the bundled lib is found next to wickra.so after install. +case "$(uname -s)" in + Darwin) + rpath="-Wl,-rpath,@loader_path" + # Normalise the dylib id to @rpath so @loader_path resolution applies. + install_name_tool -id "@rpath/${staged}" "src/${staged}" 2>/dev/null || true + ;; + *) + rpath="-Wl,-rpath,'\$\$ORIGIN'" + ;; +esac + +sed "s|@WICKRA_RPATH@|${rpath}|" src/Makevars.in > src/Makevars +echo "wickra: configured (bundled ${staged})" +exit 0 diff --git a/bindings/r/configure.win b/bindings/r/configure.win index 56ed1dae..034d940b 100644 --- a/bindings/r/configure.win +++ b/bindings/r/configure.win @@ -4,9 +4,35 @@ # itself). Stage a renamed copy, wickra_abi.dll, into src/ and build a mingw # import library that references it by that name (objdump + dlltool, both shipped # with Rtools — no gendef/pexports needed). install.libs.R then bundles the DLL. +# +# Self-contained by default: download the prebuilt wickra-c-.tar.gz +# release asset matching this package's version. Set WICKRA_INCLUDE_DIR + +# WICKRA_LIB_DIR to build against a locally built C ABI instead (dev override). set -e -: "${WICKRA_LIB_DIR:?set WICKRA_LIB_DIR to the directory containing wickra.dll}" -cp "${WICKRA_LIB_DIR}/wickra.dll" src/wickra_abi.dll + +if [ -n "${WICKRA_INCLUDE_DIR}" ] && [ -n "${WICKRA_LIB_DIR}" ]; then + echo "wickra: using C ABI from WICKRA_INCLUDE_DIR / WICKRA_LIB_DIR (dev override)" + inc="${WICKRA_INCLUDE_DIR}" + lib="${WICKRA_LIB_DIR}" +else + version=$(sed -n 's/^Version:[[:space:]]*//p' DESCRIPTION) + arch=$(uname -m) + case "${arch}" in + x86_64) triple="x86_64-pc-windows-msvc" ;; + aarch64 | arm64) triple="aarch64-pc-windows-msvc" ;; + *) echo "wickra: unsupported Windows arch '${arch}' — set WICKRA_INCLUDE_DIR/WICKRA_LIB_DIR"; exit 1 ;; + esac + url="https://github.com/wickra-lib/wickra/releases/download/v${version}/wickra-c-${triple}.tar.gz" + echo "wickra: downloading C ABI ${triple} for v${version}" + "${R_HOME}/bin/Rscript" -e "download.file('${url}', 'src/wickra-c.tar.gz', mode = 'wb', quiet = TRUE)" \ + || { echo "wickra: failed to download ${url}"; exit 1; } + "${R_HOME}/bin/Rscript" -e "untar('src/wickra-c.tar.gz', exdir = 'src/wickra-c')" + inc="src/wickra-c/wickra-c-${triple}/include" + lib="src/wickra-c/wickra-c-${triple}/lib" +fi + +cp "${inc}/wickra.h" src/wickra.h +cp "${lib}/wickra.dll" src/wickra_abi.dll { echo 'LIBRARY wickra_abi.dll' echo 'EXPORTS' diff --git a/bindings/r/src/.gitignore b/bindings/r/src/.gitignore index 665573f6..c4c79468 100644 --- a/bindings/r/src/.gitignore +++ b/bindings/r/src/.gitignore @@ -1,7 +1,13 @@ -# Build artifacts and the configure.win-generated C ABI import shims. +# Build artifacts and the configure-staged C ABI header / library / downloads. *.o *.so *.dll *.dll.a +*.dylib +wickra.h wickra_abi.def symbols.rds +wickra-c.tar.gz +wickra-c/ +# configure generates Makevars from Makevars.in (Makevars.in + Makevars.win stay tracked). +/Makevars diff --git a/bindings/r/src/Makevars b/bindings/r/src/Makevars deleted file mode 100644 index 73dd8ec9..00000000 --- a/bindings/r/src/Makevars +++ /dev/null @@ -1,5 +0,0 @@ -# The Wickra C ABI header and library are not vendored; point the build at them -# via WICKRA_INCLUDE_DIR (containing wickra.h) and WICKRA_LIB_DIR (containing -# libwickra). Build the library first with: cargo build -p wickra-c --release. -PKG_CPPFLAGS = -I$(WICKRA_INCLUDE_DIR) -PKG_LIBS = -L$(WICKRA_LIB_DIR) -lwickra -Wl,-rpath,$(WICKRA_LIB_DIR) diff --git a/bindings/r/src/Makevars.in b/bindings/r/src/Makevars.in new file mode 100644 index 00000000..617fb6d9 --- /dev/null +++ b/bindings/r/src/Makevars.in @@ -0,0 +1,6 @@ +# Generated into src/Makevars by ../configure (the @WICKRA_RPATH@ token is +# substituted per-OS: $ORIGIN on Linux, @loader_path on macOS). The C ABI header +# (wickra.h) and shared library (libwickra.so/.dylib) are staged into src/ by +# configure, so the package object links against the bundled lib. +PKG_CPPFLAGS = -I. +PKG_LIBS = -L. -lwickra @WICKRA_RPATH@ diff --git a/bindings/r/src/Makevars.win b/bindings/r/src/Makevars.win index 2cf4d569..d7dc1df6 100644 --- a/bindings/r/src/Makevars.win +++ b/bindings/r/src/Makevars.win @@ -1,5 +1,5 @@ # Link the import library built by configure.win (references wickra_abi.dll, not -# the package's own wickra.dll). The C ABI header dir is passed via -# WICKRA_INCLUDE_DIR at build time. -PKG_CPPFLAGS = -I$(WICKRA_INCLUDE_DIR) +# the package's own wickra.dll). The C ABI header (wickra.h) is staged into src/ +# by configure.win, so the include path is the source dir itself. +PKG_CPPFLAGS = -I. PKG_LIBS = libwickra_abi.dll.a diff --git a/bindings/r/src/install.libs.R b/bindings/r/src/install.libs.R index 2d16568a..a54cd8dc 100644 --- a/bindings/r/src/install.libs.R +++ b/bindings/r/src/install.libs.R @@ -1,7 +1,10 @@ -# Install the compiled package shared object plus, on Windows, the bundled -# wickra_abi.dll (matched by the *.dll glob) so the loader can resolve the import -# from the package's own libs directory (see .onLoad, which puts it on PATH). -files <- Sys.glob(paste0("*", SHLIB_EXT)) +# Install the compiled package shared object plus the bundled C ABI library so +# the package is self-contained: on Windows the wickra_abi.dll (matched by the +# *.dll glob, loaded via PATH in .onLoad); on Linux the libwickra.so (matched by +# the *.so SHLIB_EXT glob); on macOS the libwickra.dylib (added explicitly, since +# R package objects use the .so extension there too). The Unix rpath baked by +# configure ($ORIGIN / @loader_path) resolves it from this libs directory. +files <- unique(c(Sys.glob(paste0("*", SHLIB_EXT)), Sys.glob("libwickra.dylib"))) dest <- file.path(R_PACKAGE_DIR, paste0("libs", R_ARCH)) dir.create(dest, recursive = TRUE, showWarnings = FALSE) file.copy(files, dest, overwrite = TRUE)