Make the R binding self-contained (fetch the C ABI at install time) (#235)

## Problem
The R package built **only** inside the dev/CI workspace. `src/Makevars` and `configure.win` hard-required `WICKRA_INCLUDE_DIR` / `WICKRA_LIB_DIR` pointing at a pre-built `libwickra` (`configure.win` literally `: "${WICKRA_LIB_DIR:?...}"`), and there was no Unix `configure`. So **r-universe** and any plain `install.packages` / `install_github` failed — R had no working end-user install path.

## Fix
Fetch the prebuilt `wickra-c-<triple>.tar.gz` release asset matching the package version at install time and bundle the library into the package (same outcome as the C# / Java bindings, which bundle per-platform native libs):

- **New POSIX `configure`** (the Unix hook R lacked): detect OS/arch → triple, download + `untar` via **base R** (no curl/wget system dep), stage `wickra.h` + `libwickra.{so,dylib}` into `src/`, generate `src/Makevars` from `Makevars.in` with an rpath (`$ORIGIN` Linux, `@loader_path` + `install_name_tool` macOS) so the bundled lib resolves post-install.
- **`configure.win`**: drop the hard `WICKRA_LIB_DIR` requirement; download the Windows triple when unset, then keep the existing `wickra_abi.dll` rename + `objdump`/`dlltool` import-lib dance (sourcing the dll/header from the asset).
- **`install.libs.R`** also bundles `libwickra.dylib` (macOS); the `*.so`/`*.dll` globs already covered Linux/Windows.
- `WICKRA_INCLUDE_DIR`/`WICKRA_LIB_DIR` stay as an optional **dev override**.
- **CI (3 OS)** keeps building against the locally built C ABI (version-independent — avoids the chicken-egg of downloading the in-flight version) but **no longer exports `LD_LIBRARY_PATH`/`DYLD_LIBRARY_PATH`**, so it now verifies the bundled rpath — the real self-contained path users and r-universe get.

`Makevars` is now generated from `Makevars.in`; `SystemRequirements` + ignore/attributes files updated.

## Validation
- The Windows `objdump`/`dlltool` import-lib dance was smoke-tested locally against the real v0.7.9 asset (2412 `wickra_` exports → import lib built).
- Linux/macOS rpath bundling can't be tested on this Windows host → **the 3-OS `r` CI job is the gate** (now without the loader-path mask).
- Follow-up (separate, after release): set up `wickra-lib/wickra-lib.r-universe.dev` (`packages.json` → `bindings/r`).

Closes the R half of the self-contained-distribution gap (`todo-10`).
This commit is contained in:
kingchenc
2026-06-09 22:11:50 +02:00
committed by GitHub
parent d188959aab
commit 5b7523265c
11 changed files with 153 additions and 26 deletions
+2 -1
View File
@@ -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
+9 -8
View File
@@ -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
+5
View File
@@ -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$
+4 -2
View File
@@ -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
Vendored Executable
+82
View File
@@ -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-<triple>.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
+28 -2
View File
@@ -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-<triple>.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'
+7 -1
View File
@@ -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
-5
View File
@@ -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)
+6
View File
@@ -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@
+3 -3
View File
@@ -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
+7 -4
View File
@@ -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)