From 5c40acd288b05bb4f249e0d74cc422732cb0f52b Mon Sep 17 00:00:00 2001 From: kingchenc Date: Thu, 21 May 2026 21:53:49 +0200 Subject: [PATCH] ci+loader: build with --platform; ship napi-generated loader The Windows Node 18 and Node 20 CI jobs failed because: - ci.yml ran 'napi build --release' (no --platform), which produces a filename without the target triple ('wickra.node'). - The committed loader at bindings/node/index.js looked for the triple-suffixed file ('wickra.win32-x64-msvc.node') and then for the per-platform npm subpackage as a fallback. - 'wickra-win32-x64-msvc' isn't on npm yet (blocked by the spam filter), so Windows had nothing to load. Linux and macOS passed only because their optionalDependencies do exist on npm and got fetched. Two-part fix: - ci.yml now runs 'napi build --platform --release', matching release.yml. --platform encodes the target triple in the filename, so the loader's primary lookup always finds the freshly built binary on every host. - bindings/node/index.js is now the canonical napi-rs auto-generated loader (it gets regenerated by 'napi build'). It covers all the platforms napi knows about (linux glibc/musl, Android, FreeBSD, ARM, etc.) rather than just the four we publish, so unsupported platforms get a clear 'this binding isn't built for your system' error instead of a confusing ENOENT. Local sanity: 'npx napi build --platform --release' then 'node --test __tests__/' succeeds end to end on Windows; the eight existing tests pass. --- .github/workflows/ci.yml | 6 +- bindings/node/index.js | 370 ++++++++++++++++++++++++++++++++++----- 2 files changed, 335 insertions(+), 41 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8ea2b055..6638d9bb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -146,7 +146,11 @@ jobs: - name: Build native module working-directory: bindings/node - run: npx napi build --release + # --platform puts the target triple into the filename so the loader's + # `wickra..node` lookup finds the freshly built binary instead + # of falling back to the per-platform npm subpackage (which doesn't + # exist yet for win32-x64-msvc). + run: npx napi build --platform --release - name: Run Node tests working-directory: bindings/node diff --git a/bindings/node/index.js b/bindings/node/index.js index 306036fb..122c7674 100644 --- a/bindings/node/index.js +++ b/bindings/node/index.js @@ -1,50 +1,340 @@ +/* tslint:disable */ /* eslint-disable */ /* prettier-ignore */ -// Platform-aware loader for the Wickra Node native binding. -// -// In production (after `npm install wickra`) the actual `.node` binary -// lives inside a per-platform subpackage that npm installs as an -// optional dependency — e.g. `wickra-linux-x64-gnu`. In development -// (after `napi build --platform`) the binary sits next to this file. -// We try the local path first and fall back to the subpackage so the -// same loader works in both modes. -const { platform, arch } = process; -const { join } = require('node:path'); -const { existsSync } = require('node:fs'); +/* auto-generated by NAPI-RS */ -const TARGETS = { - 'linux-x64': 'linux-x64-gnu', - 'darwin-x64': 'darwin-x64', - 'darwin-arm64': 'darwin-arm64', - 'win32-x64': 'win32-x64-msvc', -}; +const { existsSync, readFileSync } = require('fs') +const { join } = require('path') -function load() { - const key = `${platform}-${arch}`; - const target = TARGETS[key]; - if (!target) { - throw new Error( - `wickra: this platform/architecture combination is not supported (${key}). ` + - 'Open an issue at https://github.com/kingchenc/wickra/issues with your platform details.' - ); - } +const { platform, arch } = process - const localBinary = join(__dirname, `wickra.${target}.node`); - if (existsSync(localBinary)) { - return require(localBinary); - } +let nativeBinding = null +let localFileExisted = false +let loadError = null - try { - return require(`wickra-${target}`); - } catch (err) { - throw new Error( - `wickra: failed to load the native binding for ${key}. ` + - `Expected either ${localBinary} or the wickra-${target} package to be installed. ` + - `Run \`npm install\` again, or build from source with \`npm run build\`. ` + - `Underlying error: ${err.message}` - ); +function isMusl() { + // For Node 10 + if (!process.report || typeof process.report.getReport !== 'function') { + try { + const lddPath = require('child_process').execSync('which ldd').toString().trim() + return readFileSync(lddPath, 'utf8').includes('musl') + } catch (e) { + return true + } + } else { + const { glibcVersionRuntime } = process.report.getReport().header + return !glibcVersionRuntime } } -module.exports = load(); +switch (platform) { + case 'android': + switch (arch) { + case 'arm64': + localFileExisted = existsSync(join(__dirname, 'wickra.android-arm64.node')) + try { + if (localFileExisted) { + nativeBinding = require('./wickra.android-arm64.node') + } else { + nativeBinding = require('wickra-android-arm64') + } + } catch (e) { + loadError = e + } + break + case 'arm': + localFileExisted = existsSync(join(__dirname, 'wickra.android-arm-eabi.node')) + try { + if (localFileExisted) { + nativeBinding = require('./wickra.android-arm-eabi.node') + } else { + nativeBinding = require('wickra-android-arm-eabi') + } + } catch (e) { + loadError = e + } + break + default: + throw new Error(`Unsupported architecture on Android ${arch}`) + } + break + case 'win32': + switch (arch) { + case 'x64': + localFileExisted = existsSync( + join(__dirname, 'wickra.win32-x64-msvc.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./wickra.win32-x64-msvc.node') + } else { + nativeBinding = require('wickra-win32-x64-msvc') + } + } catch (e) { + loadError = e + } + break + case 'ia32': + localFileExisted = existsSync( + join(__dirname, 'wickra.win32-ia32-msvc.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./wickra.win32-ia32-msvc.node') + } else { + nativeBinding = require('wickra-win32-ia32-msvc') + } + } catch (e) { + loadError = e + } + break + case 'arm64': + localFileExisted = existsSync( + join(__dirname, 'wickra.win32-arm64-msvc.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./wickra.win32-arm64-msvc.node') + } else { + nativeBinding = require('wickra-win32-arm64-msvc') + } + } catch (e) { + loadError = e + } + break + default: + throw new Error(`Unsupported architecture on Windows: ${arch}`) + } + break + case 'darwin': + localFileExisted = existsSync(join(__dirname, 'wickra.darwin-universal.node')) + try { + if (localFileExisted) { + nativeBinding = require('./wickra.darwin-universal.node') + } else { + nativeBinding = require('wickra-darwin-universal') + } + break + } catch {} + switch (arch) { + case 'x64': + localFileExisted = existsSync(join(__dirname, 'wickra.darwin-x64.node')) + try { + if (localFileExisted) { + nativeBinding = require('./wickra.darwin-x64.node') + } else { + nativeBinding = require('wickra-darwin-x64') + } + } catch (e) { + loadError = e + } + break + case 'arm64': + localFileExisted = existsSync( + join(__dirname, 'wickra.darwin-arm64.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./wickra.darwin-arm64.node') + } else { + nativeBinding = require('wickra-darwin-arm64') + } + } catch (e) { + loadError = e + } + break + default: + throw new Error(`Unsupported architecture on macOS: ${arch}`) + } + break + case 'freebsd': + if (arch !== 'x64') { + throw new Error(`Unsupported architecture on FreeBSD: ${arch}`) + } + localFileExisted = existsSync(join(__dirname, 'wickra.freebsd-x64.node')) + try { + if (localFileExisted) { + nativeBinding = require('./wickra.freebsd-x64.node') + } else { + nativeBinding = require('wickra-freebsd-x64') + } + } catch (e) { + loadError = e + } + break + case 'linux': + switch (arch) { + case 'x64': + if (isMusl()) { + localFileExisted = existsSync( + join(__dirname, 'wickra.linux-x64-musl.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./wickra.linux-x64-musl.node') + } else { + nativeBinding = require('wickra-linux-x64-musl') + } + } catch (e) { + loadError = e + } + } else { + localFileExisted = existsSync( + join(__dirname, 'wickra.linux-x64-gnu.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./wickra.linux-x64-gnu.node') + } else { + nativeBinding = require('wickra-linux-x64-gnu') + } + } catch (e) { + loadError = e + } + } + break + case 'arm64': + if (isMusl()) { + localFileExisted = existsSync( + join(__dirname, 'wickra.linux-arm64-musl.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./wickra.linux-arm64-musl.node') + } else { + nativeBinding = require('wickra-linux-arm64-musl') + } + } catch (e) { + loadError = e + } + } else { + localFileExisted = existsSync( + join(__dirname, 'wickra.linux-arm64-gnu.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./wickra.linux-arm64-gnu.node') + } else { + nativeBinding = require('wickra-linux-arm64-gnu') + } + } catch (e) { + loadError = e + } + } + break + case 'arm': + if (isMusl()) { + localFileExisted = existsSync( + join(__dirname, 'wickra.linux-arm-musleabihf.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./wickra.linux-arm-musleabihf.node') + } else { + nativeBinding = require('wickra-linux-arm-musleabihf') + } + } catch (e) { + loadError = e + } + } else { + localFileExisted = existsSync( + join(__dirname, 'wickra.linux-arm-gnueabihf.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./wickra.linux-arm-gnueabihf.node') + } else { + nativeBinding = require('wickra-linux-arm-gnueabihf') + } + } catch (e) { + loadError = e + } + } + break + case 'riscv64': + if (isMusl()) { + localFileExisted = existsSync( + join(__dirname, 'wickra.linux-riscv64-musl.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./wickra.linux-riscv64-musl.node') + } else { + nativeBinding = require('wickra-linux-riscv64-musl') + } + } catch (e) { + loadError = e + } + } else { + localFileExisted = existsSync( + join(__dirname, 'wickra.linux-riscv64-gnu.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./wickra.linux-riscv64-gnu.node') + } else { + nativeBinding = require('wickra-linux-riscv64-gnu') + } + } catch (e) { + loadError = e + } + } + break + case 's390x': + localFileExisted = existsSync( + join(__dirname, 'wickra.linux-s390x-gnu.node') + ) + try { + if (localFileExisted) { + nativeBinding = require('./wickra.linux-s390x-gnu.node') + } else { + nativeBinding = require('wickra-linux-s390x-gnu') + } + } catch (e) { + loadError = e + } + break + default: + throw new Error(`Unsupported architecture on Linux: ${arch}`) + } + break + default: + throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`) +} + +if (!nativeBinding) { + if (loadError) { + throw loadError + } + throw new Error(`Failed to load native binding`) +} + +const { version, SMA, EMA, WMA, RSI, DEMA, TEMA, HMA, ROC, TRIX, MACD, BollingerBands, ATR, Stochastic, OBV, ADX, CCI, WilliamsR, MFI, PSAR, Keltner, Donchian, VWAP, AwesomeOscillator, Aroon, KAMA } = nativeBinding + +module.exports.version = version +module.exports.SMA = SMA +module.exports.EMA = EMA +module.exports.WMA = WMA +module.exports.RSI = RSI +module.exports.DEMA = DEMA +module.exports.TEMA = TEMA +module.exports.HMA = HMA +module.exports.ROC = ROC +module.exports.TRIX = TRIX +module.exports.MACD = MACD +module.exports.BollingerBands = BollingerBands +module.exports.ATR = ATR +module.exports.Stochastic = Stochastic +module.exports.OBV = OBV +module.exports.ADX = ADX +module.exports.CCI = CCI +module.exports.WilliamsR = WilliamsR +module.exports.MFI = MFI +module.exports.PSAR = PSAR +module.exports.Keltner = Keltner +module.exports.Donchian = Donchian +module.exports.VWAP = VWAP +module.exports.AwesomeOscillator = AwesomeOscillator +module.exports.Aroon = Aroon +module.exports.KAMA = KAMA