Skip to main content

cppjs.build.js — Package-author Build Hooks

Package authors only. Apps and libraries consuming @cpp.js/package-* do NOT write this file. It lives only inside cppjs-packages/cppjs-package-<name>/cppjs-package-<name>-{wasm,android,ios}/.

cppjs.build.js describes how to fetch and build the upstream C++ library that this package wraps. The CLI auto-merges its exports into config.build of the sibling cppjs.config.js at build time.

Shape

export default {
// ─────────────────────────────────────────────────────────────
// Source acquisition (pick ONE)
// ─────────────────────────────────────────────────────────────
getURL: (version) => 'https://example.com/upstream-${version}.tar.gz',
// Simplest path: return a tarball URL. The CLI fetches + extracts
// into state.config.paths.build automatically.
// `version` is the nativeVersion field from package.json.

// OR

getSource: async (state) => {
// Custom: clone, copy from another dep, generate, etc.
// state.config.paths.build is your staging dir.
// For autotools projects without a CMake fork, this is where
// you'd run `git clone` or `cp -R` from a sibling.
},

// ─────────────────────────────────────────────────────────────
// Build system selector
// ─────────────────────────────────────────────────────────────
buildType: 'cmake',
// 'cmake' — default; the CLI runs cmake configure + build.
// 'configure' — runs `./configure && make` for autotools projects.
// See cppjs-package-openssl-* for the canonical example.

// ─────────────────────────────────────────────────────────────
// Configure-step parameters
// ─────────────────────────────────────────────────────────────
getBuildParams: (state, target) => [
'-DBUILD_SHARED_LIBS=OFF',
'-DBUILD_TESTING=OFF',
],
// Extra cmake -D flags (or autotools args). Receives:
// state — full resolved config + state object
// target — current build target ({ platform, arch, runtime, … })
//
// Use `target` to branch on per-arch needs:
// target.platform === 'wasm' | 'android' | 'ios'
// target.runtime === 'st' | 'mt'

// ─────────────────────────────────────────────────────────────
// Build-time env vars (CFLAGS / CXXFLAGS / LDFLAGS as string literals)
// ─────────────────────────────────────────────────────────────
env: (target) => [
'CFLAGS="-fPIC -DSQLITE_ENABLE_FTS5"',
'LDFLAGS="-Wl,--no-undefined"',
],
// Function form receives target; can also be a plain string array.
// DIFFERENT from cppjs.config.js `env: {}` which is RUNTIME env.

// ─────────────────────────────────────────────────────────────
// Extra link libraries
// ─────────────────────────────────────────────────────────────
getExtraLibs: (target) => ['-lpthread', '-lm'],
// Returns extra libs appended to the link line beyond what
// `dependencies` already wires up. Rarely needed; most upstream
// libs declare their own link deps.

// ─────────────────────────────────────────────────────────────
// Upstream source patching (regex)
// ─────────────────────────────────────────────────────────────
replaceList: [
{
regex: /CPL_CPUID\(1, cpuinfo\);/g,
replacement: '#ifdef __wasm__\ncpuinfo[0]=0;\n#else\nCPL_CPUID(1, cpuinfo);\n#endif',
paths: ['port/cpl_cpu_features.cpp'],
},
],
// Patch the extracted upstream source before configure. Each entry:
// regex — matched against file contents
// replacement — substitution
// paths — file globs (relative to state.config.paths.build)
//
// Real users: gdal-wasm (CPU intrinsic gating), curl-wasm
// (Emscripten Fetch API swap), sqlite3-android (Makefile fixups).
//
// Function form (sourceReplaceList) gets state + depPaths if you
// need them to compute the regex/replacement:
// sourceReplaceList: (target, depPaths) => [...]

// ─────────────────────────────────────────────────────────────
// Asset copying — into source dir before build, into dist after
// ─────────────────────────────────────────────────────────────
copyToSource: {
'assets/empty.cpp': ['src/empty.cpp'], // copy assets/empty.cpp → ${build}/src/empty.cpp
},
// Inject files into the build dir BEFORE configure. Used by gdal
// to inject an empty .cpp that forces the linker to include
// missing object files.

copyToDist: {
'assets/cacert.pem': ['ssl/certs/cacert.pem'], // copy → ${output}/ssl/certs/cacert.pem
},
// Ship extra files alongside the built artifacts. Used by openssl
// to include the CA bundle.

// ─────────────────────────────────────────────────────────────
// Lifecycle overrides (advanced)
// ─────────────────────────────────────────────────────────────
setState: (state) => {
// Mutate state once at init time, after config load but before
// any target build. Used by extensions to inject data.
// Most package authors don't write this.
},

beforeRun: (cmakeDir) => [
{ program: 'autoreconf', parameters: ['-fi'] },
],
// Run shell commands before cmake configure. Returns array of
// {program, parameters}. Used by autotools projects to
// regenerate configure scripts after `replaceList` patches.

prepare: async (state) => {
// Pre-configure step (after `getSource`/`getURL` extracts the
// tarball, before cmake configure runs). Patch source, generate
// headers, fetch sub-deps. Default: no-op.
},

build: async (state) => {
// Override the entire build step. Default: cmake configure +
// build, or `./configure && make` if buildType === 'configure'.
//
// Only override when the upstream's build system can't be
// shoehorned into one of those two. Heavy lift; rare.
},
}

How the CLI uses these hooks

For each architecture sub-package (-wasm, -android, -ios), the CLI:

  1. Reads the package's nativeVersion from package.json.
  2. Calls getURL(version) (or getSource(state)) to populate state.config.paths.build.
  3. Runs prepare(state) if defined.
  4. Calls build(state) if defined; otherwise:
    • buildType: 'cmake'cmake -S <build> -B <build/build> [getBuildParams flags] && cmake --build
    • buildType: 'configure'./configure [getBuildParams flags] && make && make install
  5. Collects artifacts (.a, include/, …) into state.config.paths.output.

Example: zlib (canonical small example)

// cppjs-packages/cppjs-package-zlib/cppjs-package-zlib-wasm/cppjs.build.js
export default {
getURL: (version) => `https://zlib.net/zlib-${version}.tar.gz`,
buildType: 'cmake',
getBuildParams: () => ['-DZLIB_BUILD_SHARED=OFF', '-DZLIB_BUILD_TESTING=OFF'],
}

Example: autotools (OpenSSL)

export default {
getURL: (version) => `https://www.openssl.org/source/openssl-${version}.tar.gz`,
buildType: 'configure',
getBuildParams: (state, target) => {
const flags = ['no-shared', 'no-tests', 'no-docs']
if (target.platform === 'wasm') flags.push('linux-generic32')
if (target.platform === 'ios') flags.push('iphoneos-cross')
return flags
},
}

Example: per-target source patching

export default {
getURL: (version) => `https://github.com/upstream/proj/archive/refs/tags/${version}.tar.gz`,
buildType: 'cmake',
prepare: async (state) => {
// Patch a header to disable a problematic feature on iOS.
if (state.target.platform === 'ios') {
const fs = await import('node:fs/promises')
const file = `${state.config.paths.build}/src/proj_internal.h`
let content = await fs.readFile(file, 'utf8')
content = content.replace('#define HAVE_LOCALECONV 1', '')
await fs.writeFile(file, content)
}
},
getBuildParams: () => ['-DBUILD_TESTING=OFF', '-DENABLE_CURL=OFF'],
}

When to choose which hook

You need toUse
Download an upstream tarballgetURL
Use a git checkout, monorepo dep, or generated sourcegetSource
Inject CMake / configure flagsgetBuildParams
Patch source files between fetch and buildprepare
Replace the build runner entirelybuild

Start with the simplest hook that works. Most packages need only getURL + buildType + getBuildParams.

See also

  • cppjs-config.md — sibling config file. The CLI merges this file's exports into config.build.
  • docs/playbooks/new-package.md — full walkthrough of authoring a new cppjs-package-*.
  • ADR-0002 — pnpm topological build order driven by dependencies in package.json (NOT here).
  • Canonical examples: cppjs-packages/cppjs-package-zlib/ (smallest), cppjs-packages/cppjs-package-openssl/ (autotools), cppjs-packages/cppjs-package-gdal/ (largest aggregator).