Skip to content

CMake Reference

Source: tools/cmake/PulpUtils.cmake

pulp_add_plugin

Status: usable

Create plugin targets for multiple formats from a single declaration.

pulp_add_plugin(<target>
    FORMATS         <format-list>
    PLUGIN_NAME     <string>
    BUNDLE_ID       <string>
    MANUFACTURER    <string>
    VERSION         <string>
    CATEGORY        <Effect|Instrument|MidiEffect>
    ACCEPTS_MIDI
    NATIVE_UI
    NO_EDITOR
    LITE_EDITOR
    GPU_UI
    FULL_EDITOR
    PLUGIN_CODE     <4-char>
    MANUFACTURER_CODE <4-char>
    AAX_PRODUCT_CODE <4-char>
    AAX_NATIVE_CODE <4-char>
    SOURCES         <file-list>
    PROCESSOR_FACTORY <function-name>
    UI              <ui-package-target|none>
    UI_SCRIPT       <path>
    ICON            <1024x1024 png>
    ICNS            <path>
    CONTENT_CAPABILITIES <capability-list>
    CONTENT_KINDS   <presets|themes|samples|sample-banks|wavetables...>
    CONTENT_HOT_RELOAD_KINDS <presets|themes|samples|sample-banks|wavetables...>
    CONTENT_MANUAL_RESCAN_KINDS <presets|themes|samples|sample-banks|wavetables...>
    SOURCE_GIT_SHA  <commit>
    SOURCE_GIT_DIRTY <TRUE|FALSE>
)

Parameters

An argument pulp_add_plugin() does not recognise (a misspelled keyword, a keyword newer than the SDK in use, or a stray value) stops configuration with unknown argument(s): …, and so does a FORMATS value outside the list below (names are case-sensitive). PRODUCES_MIDI keeps its own warning. pulp_add_plugin_bundle() applies the same checks.

Parameter Required Default Description
FORMATS Yes -- Space-separated list: VST3, AU, AUv3, CLAP, LV2, AAX, Standalone
PLUGIN_NAME No <target> Display name for the plugin
BUNDLE_ID No -- Reverse-DNS identifier (e.g., com.company.plugin)
MANUFACTURER No "Unknown" Manufacturer name
VERSION No "1.0.0" Plugin version string
CATEGORY No "Effect" Plugin category: Effect, Instrument, or MidiEffect
ACCEPTS_MIDI No Off Mirrors PluginDescriptor::accepts_midi for package metadata. AU v2/AUv3 effects that accept MIDI need this so Pulp emits a MIDI-routable component type (aumf) instead of an audio-only effect (aufx). MIDI output is controlled by PluginDescriptor::produces_midi; there is no pulp_add_plugin(PRODUCES_MIDI) flag.
NATIVE_UI No Off Link the native scripted Skia/Dawn view composition (Pulp::view-native and, for Standalone, Pulp::standalone-native) without WebView/WebKit. ScriptedUiSession, WidgetBridge, and @pulp/react remain available. Omit this for editors that instantiate WebViewPanel; the existing WebView-capable targets remain the default.
NO_EDITOR No Off Editor-less (audio-only) plug-in. VST3, CLAP, AU v2 and LV2 modules link Pulp::format-core (CLAP also Pulp::format-clap-core, the adapter without its GUI extension) instead of the format + view umbrella, so the image carries no view, canvas, render, Skia, Dawn or wgpu code and stages no GPU runtime. The plug-in exposes no editor (Pulp's generated AutoUI editor is not built in); hosts show their generic parameter UI. The processor must not override create_view(). Cannot be combined with NATIVE_UI or UI_SCRIPT. UI <package> implies it (see UI Packages). Standalone, AUv3 and AAX outputs keep the default view-linked path.
GPU_UI No Off The plug-in's C++ editor needs the GPU host (a view whose requires_gpu_host() is true): link the full editor stack. Without it, macOS plug-ins get the lite editor stack (see LITE_EDITOR).
FULL_EDITOR No Off Link the full editor stack for any other reason (the escape hatch from the lite default).
LITE_EDITOR No Default on macOS States the default explicitly. Unless the plug-in declares UI_SCRIPT, GPU_UI or FULL_EDITOR, its macOS VST3, CLAP, AU v2 and Standalone binaries leave out the GPU plug-in and window hosts and link lite replacements for the few library functions that reach Skia unconditionally (text measurement, font resolution, the emoji fallback, native GPU textures), so the VST3, CLAP and AU v2 modules carry no Skia, Dawn or Tint (the Standalone app still links Dawn through the standalone host). The editor opens on the CoreGraphics host, which an AutoUi or CPU-painted C++ editor already uses, and measures text through CoreText, which it already paints with. An editor that asks for the GPU host anyway logs one warning naming GPU_UI and opens on the CoreGraphics host. Combining LITE_EDITOR with UI_SCRIPT, GPU_UI or FULL_EDITOR is a configure error. Other platforms build the full stack. See Binary Footprint.
PLUGIN_CODE AU/AUv3 only -- 4-character AU plugin code
MANUFACTURER_CODE AU/AUv3/AAX only -- 4-character manufacturer code used by AU, AUv3, and AAX
AAX_PRODUCT_CODE AAX only -- 4-character stable AAX product identifier
AAX_NATIVE_CODE AAX only -- 4-character stable AAX Native identifier
SOURCES No -- Source files for the core object library
PROCESSOR_FACTORY No -- Factory function name (for generated entry points)
UI No -- A UI package target that supplies the plug-in's default editor through the editor-host contract (pulp/format/editor_host.hpp). Every created format and standalone target links the package and compiles a generated source that installs the package's factory as that binary's default, so Processor::editor_factory() returns it unless the processor overrides it. A package implies NO_EDITOR: the VST3, CLAP, AU v2 and LV2 modules link no Pulp view, canvas or render code, and combining it with NATIVE_UI, UI_SCRIPT, GPU_UI, FULL_EDITOR or LITE_EDITOR is a configure error. none, or no UI argument, generates and links nothing, and editor_factory() returns null. See UI Packages.
UI_SCRIPT No -- Path to a JavaScript UI entry file. Its bytes, the theme.json beside it, and the image, font, data and shader files beside it (png jpg jpeg gif webp bmp ico svg ttf otf woff woff2 json wgsl glsl sksl frag vert, skipping dot-directories, node_modules and nested build trees; the script must sit in its own directory, and one holding a CMakeLists.txt is refused at configure) are compiled into every created format target and registered with pulp::format at load, so the shipped image carries its editor and the files it loads relative to itself, and never reads the developer's source tree. A dev build (PULP_DEV_BUILD: defaults to PULP_BUILD_TESTS in the Pulp checkout and OFF for an installed-SDK consumer, where the pulp CLI's configure passes -DPULP_DEV_BUILD=ON) also records the absolute path; a hot-reloading editor (the Standalone host, or a DAW launched with PULP_DEV_HOT_RELOAD=1) then reads that file while it exists, which is what keeps the edit-and-see loop live. Every other open evaluates the embedded bytes, and pulp ship builds a build-ship/ that carries the dev build's whole configuration with the option off rather than shipping a dev build. A file added beside the script after configure re-runs CMake on the next build; an edit to an embedded file re-encodes it at build time.
ICON No -- A 1024x1024 PNG, converted to an .icns and bundled into every macOS bundle the call creates (Standalone, VST3, AU, CLAP, AAX). Mutually exclusive with ICNS.
ICNS No -- A finished .icns, bundled as-is into the same set. Prefer this when the asset is laid out per size; the ICON path downscales with sips, whose kernel rings small tiles. Scope: this fills CFBundleIconFile and ships the asset. macOS does not render that key for a non-.app bundle, so no icon appears in Finder for the plug-in formats; see the product-icon comment in PulpUtils.cmake.
CONTENT_CAPABILITIES No -- Runtime content capabilities the plugin actually consumes, such as content.presets.v1. Use this when the plugin can load installed content packs. Must be paired with CONTENT_KINDS.
CONTENT_KINDS No -- Content kinds accepted by the plugin: presets, themes, samples, sample-banks, wavetables. Use capability content.sample-banks.v1 for the generic pulp.sample-bank.v1 contract. This lets users and agents reject mismatched packs before install. Must be paired with CONTENT_CAPABILITIES.
CONTENT_HOT_RELOAD_KINDS No -- Accepted content kinds the plugin can refresh immediately after install/update. Each value must also appear in CONTENT_KINDS.
CONTENT_MANUAL_RESCAN_KINDS No -- Accepted content kinds that require an in-app rescan action but not a full restart. Each value must also appear in CONTENT_KINDS.
SOURCE_GIT_SHA No git HEAD at build time Product source commit recorded in every bundle's pulp-build-info.json. Overrides the project-wide PULP_PRODUCT_GIT_SHA; without either, the commit of the git checkout containing the calling CMakeLists.txt is read when the bundle is built, and "unknown" is recorded outside a checkout. Pass it explicitly for release builds from a source archive or a CI checkout without history.
SOURCE_GIT_DIRTY No git status at build time Whether the product source had uncommitted tracked changes. Overrides PULP_PRODUCT_GIT_DIRTY; recorded as null when unknown.

When CONTENT_CAPABILITIES and CONTENT_KINDS are present, pulp_add_plugin() generates a pulp.plugin-runtime.json resource containing the pulp.plugin-runtime.v1 manifest. Reload-policy fields are optional: accepted kinds listed in CONTENT_HOT_RELOAD_KINDS can update immediately, accepted kinds listed in CONTENT_MANUAL_RESCAN_KINDS should surface an in-app rescan action, and all other accepted kinds are restart-required in content preview/install UX. VST3/AU-style desktop bundles receive the manifest under Contents/Resources/; AUv3/iOS flat bundles receive it under Resources/; LV2 bundles receive it at the .lv2 bundle root; single file non-bundle formats receive a sibling <plugin-stem>.pulp.plugin-runtime.json sidecar. Validate the emitted artifact with ValidationHarness::validate_plugin_runtime_manifest(...).

Every bundle pulp_add_plugin() and pulp_add_plugin_bundle() create — VST3, AU, CLAP, LV2, AAX, the AUv3 extension and its host app, and the Standalone app — also carries a pulp-build-info.json build-identity record (pulp.build-info.v1). Its layout, schema, and the SDK's share/pulp/runtime-pins.json it embeds are documented in the shipping guide.

Reviewed UI kits are consumed explicitly after pulp kit apply has generated cmake/pulp-kits.cmake. Include that file, declare the plugin, then attach the chosen kit script:

include(cmake/pulp-kits.cmake OPTIONAL)

pulp_add_plugin(MyPlugin
    FORMATS CLAP Standalone
    ...)

pulp_use_kit_ui(MyPlugin pulp_kit_dev_pulp_fixtures_basic_ui_kit)

pulp_use_kit_ui(<plugin-target> <kit-target> [SCRIPT <exported-path>] [TOKENS <exported-path>]) reads the reviewed kit target's PULP_UI_SCRIPTS, PULP_DESIGN_TOKENS, and PULP_ASSETS properties. The selected script and token/theme file, and the files under the declared asset directories, are embedded into the existing format targets the same way UI_SCRIPT is, and the kit's selection replaces the plug-in's own UI_SCRIPT on those targets; a dev build (PULP_DEV_BUILD) also records the on-disk paths as the PULP_UI_SCRIPT_PATH, PULP_UI_THEME_PATH, and PULP_UI_ASSET_ROOTS target properties, which each target's generated editor-asset source registers at load time (pulp/format/editor_assets.hpp). Scripted editors can then use __loadAssetSync__ with relative paths into those reviewed asset roots, embedded or on disk. Kits with multiple scripts or token files require the matching SCRIPT / TOKENS argument so the project makes the choice intentionally. The helper does not apply kits automatically and does not run package code.

UI Packages

A UI package is any CMake target that declares two target properties:

Property Value
PULP_EDITOR_FACTORY_FUNCTION Fully qualified name of a function ::pulp::format::EditorFactory* name() that returns the package's editor factory. The factory must outlive every editor it creates; a function-local static is the usual shape. It is called on the host's main thread, the first time a host asks for the editor, never at load.
PULP_EDITOR_FACTORY_HEADER The header, on the package's include path, that declares that function.
add_library(my-ui STATIC my_ui_editor.cpp)
target_link_libraries(my-ui PUBLIC Pulp::format-core)
target_include_directories(my-ui PUBLIC include)
set_target_properties(my-ui PROPERTIES
    PULP_EDITOR_FACTORY_FUNCTION "my_ui::editor_factory"
    PULP_EDITOR_FACTORY_HEADER "my_ui/editor_factory.hpp")

pulp_add_plugin(MyPlugin FORMATS CLAP VST3 Standalone UI my-ui ...)

An installed package lists both properties in its targets' EXPORT_PROPERTIES so they survive install(EXPORT). The package target must exist when pulp_add_plugin() runs; a missing target or property stops configuration.

A plug-in that declares a package gets NO_EDITOR's view-free link without saying so: its VST3, CLAP, AU v2 and LV2 modules link Pulp::format-core instead of the format and view umbrella, so they carry no Pulp view, canvas, render, Skia or Dawn code. Its Standalone links Pulp::standalone-contract, the standalone app without Pulp's view layer, so it also carries no Pulp view, render or Dawn code and a package that draws with Dawn brings the only copy; that app opens only the package's editor (no Settings tab, musical typing keyboard or Audio Inspector, which are Pulp views). AUv3 and AAX outputs still link the view stack. NATIVE_UI, UI_SCRIPT, GPU_UI, FULL_EDITOR and LITE_EDITOR select Pulp's own view editor, so combining any of them with a package stops configuration with UI <package> cannot be combined with .... UI none selects no package and leaves the link as it would be without UI.

The choice belongs to the plug-in binary, not to a process-wide registry. The generated source defines the hook with internal linkage and installs it, at static init, into a slot in pulp-format-core that has internal linkage and hidden accessors (pulp/format/editor_package.hpp). pulp-format-core is linked statically into each plug-in module and executable, so two plug-ins in one host process each read their own package's factory and neither can replace the other's. Every processor in one binary that does not override editor_factory() shares that binary's package.

A format whose adapter does not open contract editors yet still builds, but without an editor; pulp_add_plugin() names those formats in a configure warning.

VST3 and CLAP open the editor through the contract whenever editor_factory() is non-null, and fall back to create_view() otherwise. A UI-package plug-in's AU v2 component serves a view-free Cocoa view factory (au_v2_contract_view.mm) that opens the package's editor. The standalone app hosts the contract editor in its own native window (macOS today; other platforms fail the run), where a Space the editor forwards plays or stops the standalone's transport.

pulp_add_plugin_bundle

Status: usable

Package MANY plugins into ONE binary per format — the multi-plugin counterpart to pulp_add_plugin. One .clap / .vst3 exposes N distinct plugins (Expert Sleepers Silent Way style). The single-plugin path is unchanged and remains the default; see PULP_PLUGIN_PACKAGING.

pulp_add_plugin_bundle(<target>
    FORMATS      CLAP VST3       # bundle-capable formats
    BUNDLE_NAME  <string>        # binary/bundle name (default <target>)
    BUNDLE_ID    <string>        # reverse-DNS id for the bundle
    VERSION      <string>        # default "1.0.0"
    MANUFACTURER <string>        # default "Unknown"
    SOURCES      <file-list>     # optional; union of the bundled plugins' sources
)

Each format's entry translation unit — clap_bundle_entry.cpp / vst3_bundle_entry.cpp in the target's source dir — uses the bundle macros (PULP_CLAP_BUNDLE_PLUGIN + PULP_CLAP_BUNDLE_ENTRY; PULP_VST3_BUNDLE_PLUGIN + PULP_VST3_FACTORY_BEGIN/_BUNDLE_CLASS/_FACTORY_END) to register N plugins into one binary. Per-plugin metadata (category, ids, and — for AU — component codes) lives in those macro calls, not in CMake args. The format functions automatically prefer a <fmt>_bundle_entry.cpp over the single-plugin <fmt>_entry.cpp.

Supported formats: CLAP and VST3 (each emits one entry symbol whose factory enumerates N plugins internally). AU/AUv3/AAX bundles additionally need a multi-component Info.plist and multi-symbol export and are not yet supported here — requesting one is a configure-time error rather than a silently-single-plugin binary.

See the plugin-bundles guide for a full walk-through and the examples/combined-bundle-demo project.

pulp_enable_midi_tuning_provider

Attach Pulp's provider-neutral optional microtuning wrappers to a plugin target. Use this after pulp_add_plugin() and after including any package CMake emitted by pulp add:

include(cmake/pulp-packages.cmake OPTIONAL)

pulp_add_plugin(MySynth
    FORMATS CLAP Standalone
    CATEGORY Instrument
    ACCEPTS_MIDI
    SOURCES src/PluginProcessor.cpp)

pulp_enable_midi_tuning_provider(MySynth MTS_ESP SCALA)

Supported providers are MTS_ESP and SCALA. MTS_ESP requires the mts_esp_client target created by pulp add mts-esp; SCALA requires the sst::tuning-library target created by pulp add sst-tuning-library. When a project uses both, MtsEspFallbackTuningProvider can prefer the active MTS-ESP session or prefer loaded local Scala tuning via MtsEspFallbackPolicy::PreferLocalTuning.

If the Pulp SDK was already built with the matching provider enabled, the helper only links pulp::midi and exposes the compile definition to the target. If an installed SDK was built without the optional provider, the helper compiles the small Pulp-owned wrapper source into your plugin target and links the package target. The third-party package remains opt-in either way.

Created Targets

Target Type Description
${target}_Core OBJECT or INTERFACE Shared processor code
${target}_VST3 MODULE VST3 bundle (.vst3)
${target}_AU MODULE AU v2 component (.component), macOS only
${target}_AUv3Framework SHARED FRAMEWORK AUv3 implementation framework, macOS only
${target}_AUv3 EXECUTABLE/BUNDLE AUv3 app extension (.appex), Apple platforms only
${target}_AUv3Host EXECUTABLE/BUNDLE macOS container app embedding the AUv3 extension
${target}_CLAP MODULE CLAP bundle (.clap)
${target}_LV2 MODULE LV2 bundle (.lv2)
${target}_AAX MODULE AAX Native bundle (.aaxplugin), macOS/Windows with developer-supplied SDK
${target}_Standalone EXECUTABLE Standalone app
pulp-install-${target} CUSTOM Copies built plugins to system folders

File Conventions

Each format target looks for an entry-point source file by convention:

Format Expected file
VST3 vst3_entry.cpp
CLAP clap_entry.cpp
AU v2 au_v2_entry.cpp
AUv3 au_v3_entry.cpp
LV2 lv2_entry.cpp
AAX aax_entry.cpp
Standalone main.cpp

Info.plist files can be provided per-plugin or generated from templates:

Format Custom plist Template
VST3 Info.plist.vst3 tools/cmake/PulpInfoPlist.vst3.in
AU Info.plist.au tools/cmake/PulpInfoPlist.au.in
AAX generated tools/cmake/PulpInfoPlist.aax.in

Metadata Resolution

pulp_add_plugin() is the public entry point for plugin metadata. It resolves per-format bundle metadata from the same declaration instead of asking projects to include format-specific helper files directly. AU v2 and AUv3 share the same component-type rules:

Declaration AU component type Host tag
CATEGORY Instrument aumu Synthesizer
CATEGORY MidiEffect aumi MIDI
CATEGORY Effect plus ACCEPTS_MIDI aumf Effects
CATEGORY Effect without ACCEPTS_MIDI aufx Effects

The component type has to agree with the AU entry macro in the plugin's au_v2_entry.cpp, or the component loads and then silently ignores every MIDI message the host sends it:

AU component type Entry macro Header
aufx PULP_AU_PLUGIN pulp/format/au_v2_entry.hpp
aumf PULP_AU_MIDI_PLUGIN pulp/format/au_v2_entry.hpp
aumi PULP_AU_MIDI_EFFECT pulp/format/au_v2_midi_effect_entry.hpp
aumu PULP_AU_INSTRUMENT pulp/format/au_v2_instrument_entry.hpp

PLUGIN_CODE and MANUFACTURER_CODE must be exactly four characters for AU and AUv3 targets. VERSION is converted to the AU integer form from numeric major[.minor[.patch]] components with an optional - or + suffix, so 1.2.3-beta.1 keeps the same AU version number as 1.2.3. Generated plist and entitlement templates consume these resolved values; downstream projects should keep using pulp_add_plugin() rather than calling the internal format helpers.

Each AU/AUv3 target must claim a unique Audio Unit identity — the (component type, PLUGIN_CODE, MANUFACTURER_CODE) triple macOS keys the AudioComponent registry on. Two targets in one project that share it are one component as far as a host is concerned, so only one of them can ever load; configure fails with the conflicting target named. Declaring both AU and AUv3 for the same target is fine — one plugin publishes one identity.

Format Availability

Format targets are only created when the corresponding SDK is available:

  • VST3: requires PULP_HAS_VST3 (set when external/vst3sdk/ exists)
  • AU: requires APPLE and PULP_HAS_AUSDK (set when external/AudioUnitSDK/ exists)
  • AUv3: requires APPLE, PLUGIN_CODE, and MANUFACTURER_CODE; macOS creates a framework, .appex, and container .app, while iOS creates the .appex
  • CLAP: requires PULP_HAS_CLAP (fetched via FetchContent)
  • LV2: requires PULP_HAS_LV2 (set when LV2 headers are available)
  • AAX: requires APPLE or WIN32, PULP_ENABLE_AAX=ON, and PULP_AAX_SDK_DIR pointing to a developer-supplied out-of-tree AAX SDK
  • Standalone: always available

On Linux and Ubuntu, requesting AAX in FORMATS is a configure-time error.

Shipped Module Size

Three defaults keep shipped plug-in modules small. Each changes what a shipped binary contains, so each has an opt-out.

Dead stripping. In every non-Debug Apple build the VST3, CLAP, AU v2, LV2 and AAX modules link with -dead_strip, so code no entry point reaches is removed. AU v3 targets are not dead-stripped. The linker keeps everything reachable from an exported symbol (the format entry points), every static initializer and what it reaches (self-registering factories survive), Objective-C class and category metadata, and anything marked __attribute__((used)). What it can drop is code reached only by a runtime name lookup it cannot see: dlsym() of a symbol the module does not export, or an Objective-C class found only through NSClassFromString(). Export such a symbol (__attribute__((visibility("default")))), mark it used, or reference it from code that is reached.

Opt out for every plug-in with -DPULP_DEAD_STRIP_PLUGINS=OFF, or for one module after pulp_add_plugin():

set_target_properties(MyPlugin_VST3 PROPERTIES PULP_DEAD_STRIP OFF)

Development-only shared runtime. -DPULP_DEV_SHARED_UI=ON (macOS, off by default; pulp dev and pulp loop turn it on in SDK projects) links the VST3, CLAP, AU and Standalone binaries of plug-ins with the default view stack against one libpulp-dev-shared-ui.<version>.dylib in the build tree instead of the static Pulp libraries, so a one-file edit relinks a module of about a megabyte. Those artifacts load only from that build tree, and every ship path refuses them. See Fast plug-in relinks.

Coverage: plugin-module-dead-strip-PulpGain-<FORMAT> checks the flag on PulpGain's modules, cmake-plugin-dead-strip-switch checks that the option and the property each remove it, and test/test_host.cpp loads and processes the dead-stripped Release PulpGain.clap. Opening a dead-stripped module's editor in every format is not covered by an automated test.

No script runtime unless the editor runs a script. Editor glue that every plug-in links reaches a scripted editor only through view::ScriptedEditorSession's virtual calls and a prewarm runner slot, so a plug-in whose editor runs no script (the generated AutoUI editor, or native views) links none of the JS engine, the widget bridge or the session code; about 5 MB of a stripped module, plus the Skia text and canvas code the widget bridge draws through. Linking follows code references, not a flag. A target that declares UI_SCRIPT (or gets one from pulp_use_kit_ui()) calls pulp::format::install_scripted_editor_builder() from its generated editor-assets source, which links the code that builds the scripted editor and with it the runtime. Any code in the plug-in that constructs a view::ScriptedUiSession links the runtime too, and the session's static initializer installs the prewarm runner; a plug-in that loads a script at run time without declaring UI_SCRIPT opts back in that way. A multi-plug-in bundle whose scripts come only from PluginRegistration::ui_script entries calls install_scripted_editor_builder() itself; without it the editor logs that the builder is missing and opens AutoUi. Reaching the runtime only by name at run time (dlsym()) cannot work, because nothing links it. Coverage: plugin-module-no-script-runtime-PulpGain-<FORMAT>.

Local symbols stripped at packaging. pulp ship package and tools/scripts/build_combined_installer.sh ship a copy of each AU, VST3 and CLAP bundle with strip -x applied to its main executable; the build output keeps its symbols, and AAX is never touched. Before stripping, each writes a dSYM of the unstripped executable (artifacts/symbols/<FORMAT>/<bundle>.dSYM, or <out>/symbols/<kind>/ for the installer script) under the UUID the stripped copy keeps, so crash reports from installed plug-ins still symbolicate; archive it with the release. Stripping happens at packaging, not at build time, so the opt-out is the packaging flag --keep-symbols, not a CMake option. See the ship skill for the details.

Install Target

pulp-install-${target} copies supported built bundles to platform-standard locations:

Platform VST3 CLAP AU AUv3 AAX
macOS ~/Library/Audio/Plug-Ins/VST3/ ~/Library/Audio/Plug-Ins/CLAP/ ~/Library/Audio/Plug-Ins/Components/ ~/Applications/ plus PlugInKit registration /Library/Application Support/Avid/Audio/Plug-Ins/
Windows %COMMONPROGRAMFILES%/VST3/ %COMMONPROGRAMFILES%/CLAP/ -- -- %COMMONPROGRAMFILES%/Avid/Audio/Plug-Ins/
Linux ~/.vst3/ ~/.clap/ -- -- --

LV2 build output is created under build/LV2/<plugin>.lv2/; pulp-install-${target} does not currently copy LV2 bundles to a user plugin folder.

Binary Footprint

Skia's libskunicode_icu.a embeds a full ICU data package (10.9 MB), the largest single part of a plug-in with Pulp's editor. With PULP_ICU_DATA_SUBSET (default ON, Apple only), FindSkia links a copy whose data keeps only what the linked ICU code reads: every break-iterator rule file and dictionary, normalization, properties, the converter alias table and locale resolution data. Collation, time-zone, currency, display-name, unit, transliteration, number spell-out, IDNA, codepage-converter and spoof-check data are removed (4.9 MB remains). The copy is generated at configure time under build/pulp-skia-icu-subset/ by tools/cmake/scripts/icu_data_subset.py, for every Skia consumer and for installed-SDK builds alike; any structural surprise falls back to the unchanged archive with a warning, and the footprint tests then fail on the size. Turn the option off only if your plug-in calls ICU services Pulp does not use itself.

On macOS a plug-in gets the lite editor stack unless it declares UI_SCRIPT, GPU_UI or FULL_EDITOR; this suits every editor that paints on the CoreGraphics host (Pulp's generated AutoUi editor, or C++ views that never ask for the GPU host). Its binaries compile their Objective-C cluster without the GPU hosts and link pulp-canvas-lite-objects and pulp-view-lite-objects (tools/cmake/PulpLiteEditorObjects.cmake): copies of text_shaper.cpp, font_resolver.cpp, canvas_widget.cpp and asset_manager.cpp built with PULP_LITE_EDITOR=1, which measure through CoreText and skip the Skia-only paths. Objects on the link line take precedence over the library's archive members, so the Skia versions are never loaded, and the binary links with -dead_strip so nothing they would have reached survives. The editor opens on the CoreGraphics host in both builds and draws the same glyphs, and captured line breaks are reused in both, because the lite build resolves a face's identity through CoreText. Text measured without a capture can differ by up to 1 logical pixel for a few system families at non-400 weights: the lite build measures with CoreText, the face it paints, while the full build measures with Skia, which can resolve a different face ("Helvetica Neue" 600 at 12 px: ascent 13.92 and width 116.7 in Skia, 13.69 and 120.0 for the painted face), and for a face a design asks for that Pulp does not bundle, where Skia and CoreText fall back to different faces (a bold JetBrains Mono: Menlo-Bold in Skia, the bundled Regular in CoreText). AutoUi, Inter and design-imported panels whose faces are bundled render the same in both builds.

A plug-in without a custom editor that does not need Pulp's generated parameter editor should use NO_EDITOR: it links none of the view, canvas, Skia, Dawn or JavaScript code. Measured on the pulp-gain examples (arm64, Release, Skia m153), binary bytes per format:

Build VST3 CLAP AU
PulpGain, full ICU data 44.5 MB 44.3 MB 44.3 MB
PulpGain, ICU subset 38.4 MB 38.2 MB 38.3 MB
PulpGain (lite editor stack, the default) 2.58 MB 2.46 MB 2.54 MB
PulpEffect (FULL_EDITOR) 28.7 MB
PulpGainHeadless (NO_EDITOR) 0.83 MB 0.61 MB 0.67 MB

tools/scripts/plugin_footprint_check.py measures a built binary against the budgets in tools/scripts/plugin_footprint_budget.json (mapped code and data, which strip settings do not move; ICU data; and, for an editor-less binary, the symbols it must not contain). The plugin-footprint-PulpEffect (FULL_EDITOR, profile editor), plugin-footprint-PulpGain (lite stack, profile lite-editor), plugin-footprint-PulpGainHeadless and icu-data-subset-linked tests run it, and pulp-test-icu-data-breaks pins the word, line, sentence and grapheme boundaries the full data produces, so a subset that drops a rule file or dictionary still in use fails.

pulp_add_app

Status: partial

Create a standalone application target.

pulp_add_app(<target>
    APP_NAME    <string>
    BUNDLE_ID   <string>
    VERSION     <string>
)

Creates a basic executable target with compile definitions for the app name and bundle ID. Minimal functionality compared to pulp_add_plugin().

On Apple platforms the target is created as a bundle-capable executable so it can accept pulp_app_icon(...).

pulp_declare_standalone_document_type

Status: usable

Declare a file type an app owns, so macOS can launch it by double-clicking one of its documents.

Call it after the target exists — after pulp_add_plugin(... FORMATS standalone) or pulp_add_app() — and name the app target itself (MyPlugin_Standalone, not MyPlugin).

pulp_add_plugin(MyApp
    FORMATS standalone
    PLUGIN_NAME "MyApp"
    BUNDLE_ID "com.example.myapp"
)

pulp_declare_standalone_document_type(MyApp_Standalone
    UTI            com.example.myapp.project
    EXTENSION      myproj
    MIME           application/vnd.example.myapp+zip
    DESCRIPTION    "MyApp Project"
    CONFORMS_TO    public.data public.archive com.pkware.zip-archive
    ROLE           Editor
    RANK           Owner
)

Parameters

Parameter Required Default Description
UTI Yes -- Reverse-DNS uniform type identifier for the file type
EXTENSION Yes -- One or more bare filename extensions; a leading . is normalized away
DESCRIPTION No the UTI Human-readable type name shown in Finder and Open panels
MIME No -- One or more MIME types published in the exported declaration
CONFORMS_TO No public.data UTIs this type conforms to
ROLE No Editor Editor, Viewer, Shell, or None
RANK No Owner Owner, Default, Alternate, or None

RANK None is the shape a helper or secondary app needs: it can open the type but must never become the system's default handler for it.

What it emits

Each call adds one entry to both CFBundleDocumentTypes (the types this app opens) and UTExportedTypeDeclarations (this app defines the type). The pair is not separable here: an app that declares a document type for a UTI nothing exports gets no document icon and unreliable Launch Services routing.

Call it more than once on one target to declare more than one file type; the declarations accumulate. Declaring the same UTI twice is an error — pass every extension of a single type in one call (EXTENSION proj projx).

Behavior notes

  • Apple only. On Windows, Linux, and Android the call validates its arguments and then does nothing, so a cross-platform CMakeLists.txt needs no if(APPLE) guard. Argument checking still runs on every platform, so a typo fails the same way everywhere.
  • It fails configure rather than dropping a declaration. A missing UTI or EXTENSION, a non-reverse-DNS UTI, an unknown ROLE/RANK, a target that is not a macOS application bundle, and a duplicate UTI are all FATAL_ERRORs — a silently ignored document type builds, installs, and then simply does nothing on double-click.
  • CMake cannot append keys to the Info.plist it synthesizes, so the function points MACOSX_BUNDLE_INFO_PLIST at a generated plist built from PulpInfoPlist.standalone.in. That template carries CMake's default key set unchanged, still as ${MACOSX_BUNDLE_*} placeholders, so every existing bundle value — including the icon file pulp_app_icon() sets, in either call order — is preserved. An app that never calls this function is untouched.
  • If the target already has a hand-authored MACOSX_BUNDLE_INFO_PLIST, the call fails rather than overwriting it.

Declaring the type is only half of opening a document

This function makes the OS route the file: Finder gives the app a document icon, the double-click launches it, and the Open panel offers the type. It does not make the app receive anything. macOS reports the file through the NSApplicationDelegate message application:openURLs:, so an app that declares the type and installs no handler launches and drops the file — which looks exactly like the declaration not working.

The runtime half is WindowHost::set_open_files_handler(), or StandaloneApp::set_open_files_handler() for a standalone plug-in target:

app.set_open_files_handler([&](const std::vector<std::string>& paths) {
    for (const auto& path : paths) open_project(path);
});
app.run_with_editor();

Install it before the event loop starts. The file that launched the app is reported from inside the loop's own startup, so a handler wired up afterwards misses the common case; paths that arrive before a handler exists are held and delivered as soon as one is installed.

pulp_app_icon

Status: usable

Attach an icon source to an application, standalone, or plugin-format bundle target. The plugin bundles pulp_add_plugin() generates — <target>_VST3, <target>_AU, <target>_CLAP, <target>_AAX — are CFBundles and accept an icon the same way an app bundle does.

Targets build normally if you never call pulp_app_icon(...). The helper is an optional overlay, so removing the call cleanly removes the custom-icon behavior.

SOURCE takes one PNG of at least 1024x1024 and derives every size from it with sips. That is fine for soft artwork and poor for a mark with fine detail: sips uses a Lanczos kernel, which overshoots on hard edges, so a 1-2px feature at 16x16 smears and a tile edge picks up a visible halo. When the icon has crisp geometry, render each size on its own pixel grid, build the .icns yourself, and pass it as ICNS — it is bundled verbatim. ICNS applies to macOS only; keep SOURCE alongside it to cover Windows, Android, and Linux.

pulp_app_icon(MyPlugin_VST3 ICNS assets/MyPlugin.icns)
pulp_app_icon(<target>
    SOURCE       assets/app-icon.png
    MACOS        assets/app-icon-mac.png
    WINDOWS      assets/app-icon-win.png
    IOS          assets/app-icon-ios.png
    ANDROID      assets/app-icon-android.png
    LINUX        assets/app-icon-linux.png
    DEBUG_ICON   assets/app-icon-debug.png
    RELEASE_ICON assets/app-icon-release.png
)

Parameters

Parameter Required Default Description
SOURCE Yes, unless a platform override is present -- Default PNG used when a platform-specific override is absent; must be at least 1024×1024
MACOS No SOURCE macOS override
WINDOWS No SOURCE Windows override
IOS No SOURCE iOS override, recorded for downstream packaging
ANDROID No SOURCE Android override
LINUX No SOURCE Linux override
DEBUG_ICON No -- Debug-build variant, used when the active configure/build type is Debug
RELEASE_ICON No -- Release-build variant

Current Platform Outputs

  • macOS: generates AppIcon.icns and bundles it into the app
  • Windows: generates an .ico plus .rc and links it into the executable
  • Android: writes launcher PNGs under android/app/src/main/res-generated/
  • Linux: records the selected PNG as the target's canonical app-icon asset via the PULP_LINUX_APP_ICON target property
  • iOS: records the selected PNG as PULP_IOS_APP_ICON and warns that asset-catalog emission is not implemented

The selected source must be a PNG that is at least 1024×1024. Smaller inputs fail at configure time so a release doesn’t silently ship blurry assets.

Example:

pulp_add_plugin(MyPlugin
    FORMATS Standalone
    PLUGIN_NAME "MyPlugin"
    BUNDLE_ID "com.example.myplugin"
)

pulp_app_icon(MyPlugin_Standalone
    SOURCE assets/app-icon.png
)

pulp_add_sparkle

Embeds Sparkle 2 in a macOS standalone app so it can offer signed updates. Call it after the app target exists, on the .app target only; it refuses any target that is not a MACOSX_BUNDLE executable, so an updater never ends up inside an AU, VST3 or CLAP bundle.

pulp_add_sparkle(MyPlugin_Standalone
    FEED_URL      "https://github.com/me/myplugin/releases/latest/download/appcast.xml"
    PUBLIC_ED_KEY "<44-character base64 Ed25519 public key>"
    [AUTOMATIC_CHECKS ON|OFF]       # SUEnableAutomaticChecks; omit to let Sparkle ask
    [CHECK_INTERVAL 86400]          # SUScheduledCheckInterval, seconds
    [RELEASES_URL <https page>]     # linked in the Settings "Updates" note
    [INSTALLER package|app]         # what an update installs; the note says so
    [AUTOMATIC_INSTALL ON|OFF]      # SUAllowsAutomaticUpdates; default OFF
    [KEEP_XPC_SERVICES]             # keep Installer/Downloader.xpc (sandboxed apps only)
    [VERSION 2.10.0 SHA256 <hash>]  # override the pinned release
    [DIST_DIR <extracted-dist>])    # offline: an already-extracted distribution

At configure time it downloads the pinned Sparkle-<version>.tar.xz release asset (verifying its SHA-256) into <build>/_deps/sparkle-<version>/. After each link it copies Sparkle.framework into Contents/Frameworks with ditto, removes the XPC services a non-sandboxed app does not use, links the framework with an @executable_path/../Frameworks rpath. At the end of configure it writes SUFeedURL, SUPublicEDKey and the optional keys into the app's Info.plist template, so they survive regeneration and combine with pulp_declare_standalone_document_type() in either order.

Offline configures: DIST_DIR, or the cache variable PULP_SPARKLE_DIST_DIR, names an extracted distribution, and PULP_SPARKLE_ARCHIVE names a local copy of the release archive, which is verified against the same pinned SHA-256 before it is extracted. With FETCHCONTENT_FULLY_DISCONNECTED ON and none of these, configure stops with an error instead of downloading.

FEED_URL must be https://, or http://127.0.0.1:<port>/… for a local practice feed served from loopback (Sparkle 2 refuses file:// feeds at run time), and PUBLIC_ED_KEY must be the public half of the key pair. The private key never belongs in a build file: keep it outside the repository and pass it to pulp ship appcast --sign-key-file when publishing.

At run time the standalone host finds Sparkle through the Objective-C runtime and, with no further code, adds Check for Updates… directly under About in the application menu, an Updates tab to Pulp's Settings panel (automatic-check toggle, Check for Updates…, version, last check and a note generated from RELEASES_URL, INSTALLER and AUTOMATIC_INSTALL), and the pulp::format::app_update_status() service a custom editor's own Settings can read (see In-app updates). AUTOMATIC_INSTALL is off unless set, so Sparkle never installs an update the user did not choose. Scheduled background checks start only when the app carries a Developer ID signature, so local and CI launches stay quiet; PULP_STANDALONE_UPDATER=on starts them in a development build, and =off disables the updater entirely. build_combined_installer.sh signs the embedded framework inside-out with the Developer ID identity before it signs the app.

pulp_add_binary_data

Status: usable

Embed binary assets as C++ arrays.

pulp_add_binary_data(MyResources
    SOURCES   logo.png preset.json font.ttf
    NAMESPACE myresources
)

Generates a static library with:

namespace myresources {
    extern const unsigned char logo_png[];
    extern const std::size_t logo_png_size;
}

Parameters

Parameter Required Default Description
SOURCES Yes -- Files to embed
NAMESPACE No <target> C++ namespace for the generated symbols
COMPRESS No Off Store each asset as a zlib stream (see below)

Compressed embeds

COMPRESS stores each asset as a zlib (RFC 1950) stream at level 9. The stream is produced by the build host's Python zlib, so it is deterministic per zlib build: two hosts whose Python links different zlib builds (zlib 1.2.x and zlib-ng, for example) can embed different, equally valid streams, and they inflate to the same bytes. Leave COMPRESS off for a target whose binaries are byte-compared across hosts or built as a reproducible release. Switching COMPRESS on or off in an existing build directory needs no clean reconfigure: CMake reruns the encoder because its command line changed, with Makefile generators as well as Ninja. Verified with Unix Makefiles on CMake 3.24.3 (the minimum Pulp supports) and 4.3.3; cmake-pulp-add-binary-data-compress-toggle checks it on every run. Text assets shrink the most: a materialized design's runtime bundle to about a tenth, its document JSON to about two fifths. Already-compressed formats (PNG, JPEG, WOFF2) gain nothing, so embed those in a separate call without COMPRESS. The symbols change, so code still reading the raw array fails to compile instead of reading compressed bytes:

namespace myresources {
    extern const unsigned char runtime_js_zlib[];   // compressed bytes
    extern const std::size_t runtime_js_zlib_size;
    extern const std::size_t runtime_js_size;       // inflated bytes
}

// Inflate where the asset is used, and drop the copy once it is consumed.
// zlib_decompress_exact inflates into a string of exactly the recorded size,
// and returns nullopt for a truncated, oversized or corrupt stream.
auto text = pulp::runtime::zlib_decompress_exact(myresources::runtime_js_zlib,
                                                 myresources::runtime_js_zlib_size,
                                                 myresources::runtime_js_size);

The trade is a smaller binary against an inflate on each use and a transient heap copy. zlib_decompress_exact allocates its output, so never call it on the audio thread: inflate on the message thread or a background thread and hand the result over. Inflating costs about 1 ms per megabyte of text on an Apple silicon core. COMPRESS is not the default: the raw array is a public symbol that consumers read directly, so a default change would break their builds.

Implementation notes

The function delegates the byte-to-C-array encoding to the tools/cmake/scripts/encode_binary_data.py helper through add_custom_command(OUTPUT … DEPENDS …). As a result:

  • Encoding runs at build time, not configure time. Editing a source asset and re-running cmake --build regenerates the embedded .cpp automatically — no cmake -S/-B reconfigure required.
  • Encoding is fast: a 1 MB asset finishes in well under a second. The older in-CMake hex loop was effectively O(n²) and could pin a single reconfigure at 100 % CPU for 10–22 minutes.
  • Python (find_package(Python3 COMPONENTS Interpreter REQUIRED)) is the only added build-time dependency; Python is already required elsewhere in the Pulp build.

pulp_add_js_bytecode

Status: usable

Compile scripted-UI JavaScript to QuickJS bytecode at build time and embed it, so a fresh process reads the bytecode instead of parsing the script before its editor can open.

pulp_add_js_bytecode(my_ui_bytecode
    SOURCES   ui/runtime.js ui/design.js
    NAMESPACE my_ui_bytecode
    WIDGET_BRIDGE_SCRIPT)
target_link_libraries(MyPlugin PRIVATE my_ui_bytecode)

Generates a static library with, per script:

namespace my_ui_bytecode {
    extern const unsigned char runtime_js_pjsbc[];
    extern const std::size_t runtime_js_pjsbc_size;
}

Register each blob before the editor opens (the processor's constructor or editor_prewarm()):

pulp::view::register_embedded_script_bytecode(my_ui_bytecode::runtime_js_pjsbc,
                                              my_ui_bytecode::runtime_js_pjsbc_size);

Parameters

Parameter Required Default Description
SOURCES Yes -- Scripts to compile
NAMESPACE No <target> C++ namespace for the generated symbols
WIDGET_BRIDGE_SCRIPT No off Compile each script as WidgetBridge::load_script() evaluates it

Behavior

  • A blob is used only for the exact source text it was compiled from (length and SHA-256) and only by the engine build that compiled it. A changed script, another Pulp version, or a damaged blob falls back to compiling from source.
  • The generator is built from the same Pulp libraries the product links. When cross-compiling it cannot run; the library then holds placeholder blobs that registration rejects, and the scripts compile at run time.
  • script_bytecode_cache_stats().embedded counts scripts served from embedded bytecode.

pulp_add_wgsl_shaders

Status: usable

Embed .wgsl files in a target as std::string_view constants and validate them at build time.

pulp_add_wgsl_shaders(MyPlugin
    FILES shaders/glow.wgsl shaders/blur.wgsl
    NAMESPACE my_plugin::shaders)

Each file becomes <stem>_wgsl.hpp on the target's private include path:

#include "glow_wgsl.hpp"
auto shader = device.create_shader_module(my_plugin::shaders::glow_wgsl, "glow.wgsl");

Parameters

Parameter Required Default Description
FILES Yes -- WGSL files, relative to the current source directory
NAMESPACE No <target>_wgsl C++ namespace for the constants
NO_VALIDATE No off Embed only; skip build-time validation

Behavior

  • Headers are generated at build time and regenerated only when a shader changes. The source is stored as a byte array, so a shader of any length compiles on every toolchain; the array is NUL-terminated.
  • Validation runs the pulp-wgsl-validate host tool, which compiles each file on Dawn's Null adapter. The Null backend draws nothing but runs the full WGSL compiler, so a failing shader stops the build with file:line:column: error: message. The compiler is the one inside the pinned Dawn (BSD-3-Clause), reached only through Dawn's public WebGPU API.
  • Validation is skipped, with a configure-time status message, where the tool does not exist: cross-compiled builds (iOS, Android, the browser), builds without Dawn, and projects built against an installed SDK. If the host cannot create a Null adapter, the tool prints a warning and the build goes on unvalidated.
  • At run time GpuDevice::create_shader_module() reports the same diagnostics as a GpuError (see Your First WGSL Pass in a View).

pulp_add_cargo_staticlib

Status: experimental

Build a Rust (Cargo) staticlib and expose it as an IMPORTED static-library target, for an opt-in native-component DSP core written in Rust (or any language that compiles to a C-ABI staticlib). Available via include(PulpCargoStaticlib). It requires a Rust toolchain (cargo + rustc, install from rustup.rs) and is intended for the opt-in lane gated behind PULP_BUILD_NATIVE_COMPONENT_RUST_TESTS — OFF by default, so a default Pulp build never needs Rust.

include(PulpCargoStaticlib)

pulp_add_cargo_staticlib(
    NAME      MyGain_Core               # IMPORTED target name to create
    MANIFEST  ${CMAKE_CURRENT_SOURCE_DIR}/rust-core/Cargo.toml
    LIB_NAME  my_gain_core              # crate's [lib] name → libmy_gain_core.a
    PROFILE   release                   # optional: dev (default) | release
    FEATURES  simd                      # optional: forwarded as --features
)

# Link it into your component target; a direct symbol reference pulls the
# archive into the final module.
target_link_libraries(MyPlugin_Core PRIVATE MyGain_Core)

Parameters

Parameter Required Default Description
NAME Yes -- Name of the IMPORTED target to create
MANIFEST Yes -- Absolute path to the crate's Cargo.toml
LIB_NAME Yes -- The crate's [lib] name (the archive is lib<LIB_NAME>.a)
PROFILE No dev dev or release cargo profile
FEATURES No -- Cargo features, forwarded to --features

Implementation notes

  • The archive is built -C relocation-model=pic -C panic=abort (PIC so it links into Pulp's globally-PIC build; panic=abort so a Rust panic can never unwind across the extern "C" boundary — also pinned in the crate's Cargo.toml as belt-and-suspenders).
  • The custom command DEPENDS on a CONFIGURE_DEPENDS glob of the crate's src/*.rs plus its Cargo.toml, so editing Rust sources re-invokes cargo on the next build (cargo is itself incremental, so a no-op re-invoke is cheap). Without this, the OUTPUT rule would only fire when the archive was missing and could silently link a stale archive.
  • cargo runs with --locked: the build uses the crate's committed Cargo.lock exactly and fails rather than rewriting it or resolving a crate version nobody reviewed. The crate must commit a Cargo.lock next to its Cargo.toml; configure refuses a crate without one (create it with cargo generate-lockfile --manifest-path <Cargo.toml>). With FETCHCONTENT_FULLY_DISCONNECTED ON, cargo also runs --offline and resolves only from the local Cargo registry cache. The Rust CLI (experimental/pulp-rs) follows the same rule for its build and its cargo test ctests.
  • On Linux/Android the IMPORTED target's INTERFACE_LINK_LIBRARIES pulls in the system libraries the Rust std archive needs (Threads::Threads, ${CMAKE_DL_LIBS}, m); macOS resolves these through libSystem automatically.

pulp_faust_generate

Generate a developer-selected C++ artifact from a Faust .dsp source file with an optional external compiler. When faust is unavailable, no codegen command is registered. Pulp reference DSPs remain buildable and are not claimed to match compiler output.

include(${PULP_ROOT}/tools/cmake/PulpFaust.cmake)

pulp_faust_generate(
    ${CMAKE_CURRENT_BINARY_DIR}/my_gain_faust.hpp
    ${CMAKE_CURRENT_SOURCE_DIR}/gain.dsp
    FaustGainDsp
)

Parameters

Parameter Required Description
output_hpp Yes Artifact path to generate; prefer the build tree unless provenance is pinned and verified
input_dsp Yes FAUST .dsp source file
class_name Yes C++ class name passed to FAUST via -cn

pulp_add_faust_test

Add a Catch2 test target for a FAUST-backed processor.

pulp_add_faust_test(faust-gain-test
    test_faust_gain.cpp
    SOURCES faust_gain.hpp
)

Parameters

Parameter Required Description
target Yes Test executable target name
test_source Yes Main test source file
SOURCES No Additional source/header files needed by the test target

pulp_add_wam_plugin

Build a Pulp Processor into a WAMv2 (Web Audio Modules v2) WebAssembly AudioWorklet plugin. Experimental — targets a stereo, single-instance canary, not full WAM-host conformance.

Requires the Emscripten toolchain (configure with emcmake cmake ...); under any other toolchain the helper is a no-op, so it never breaks native builds.

include(${PULP_ROOT}/tools/cmake/PulpWam.cmake)

pulp_add_wam_plugin(PulpGain
    ENTRY    pulp_gain_wasm.cpp                 # required: the wam_* factory TU
    INCLUDES ${PULP_ROOT}/examples/pulp-gain    # plugin headers
    # SOURCES extra_dsp.cpp                      # optional: extra DSP sources
    # SINGLE_FILE                                # optional: BASE64-embed for AudioWorklet
)

Parameters

Parameter Required Description
ENTRY Yes The plugin's factory translation unit. It defines std::unique_ptr<pulp::format::Processor> pulp_wam_make_processor(); the shared wam_* C ABI lives in core/format/src/wasm/wam_entry.cpp.
SOURCES No Additional plugin DSP source files.
INCLUDES No Additional include directories (plugin headers).
SINGLE_FILE No Emit a BASE64-embedded ES-module factory for the AudioWorklet (sync compile, no fetch in worklet scope). Without it, a separate .wasm is emitted for the Node runner and export inspection.
NATIVE_EDITOR No Record in the web-build report that the plugin's native create_view() editor is replaced by generated controls in the headless web build.

Created targets

  • ${NAME}-wam — emits ${NAME}.js (+ ${NAME}.wasm unless SINGLE_FILE).

Notes

  • The helper compiles the headless DSP subset once into a shared pulp-wam-dsp object library and owns the wam_* EXPORTED_FUNCTIONS table and Emscripten link flags. It does not link the desktop pulp::format / pulp::state libraries, which publicly pull pulp::view/GPU and crypto/HTTP — none of which belongs in a headless browser DSP module.
  • choc headers are located via -DPULP_WAM_CHOC_INCLUDE=<dir containing choc/> (or a populated sibling build tree).

pulp_stage_runtime_dependencies

Stage every runtime file a plug-in or app module needs beside its binary.

Defined in PulpRuntimeStaging.cmake, which is included by both Pulp's own source build (root CMakeLists.txt) and by PulpConfig.cmake for find_package(Pulp) consumers — so it is always available in either. Pulp's format helpers call it for every target they create, so a plug-in built with pulp_add_plugin / pulp_add_app needs nothing extra. Call it directly only when you create a loadable module yourself.

It delegates the wgpu runtime copy to target_copy_webgpu_binaries() — upstream's wgpu FetchContent package supplies that in a source build, and PulpWebGpuImportedTarget.cmake supplies a fallback for consumers. Pulp never redefines or shadows the upstream function; it adds the parts upstream has no concept of.

find_package(Pulp REQUIRED)
add_library(MyCustomModule MODULE my_module.cpp)
target_link_libraries(MyCustomModule PRIVATE Pulp::view)
pulp_stage_runtime_dependencies(MyCustomModule)
pulp_verify_runtime_dependencies_staged(MyCustomModule)   # optional but cheap

What gets staged

File When Why it is not optional
wgpu-native runtime (libwgpu_native.* / wgpu_native.dll) whenever the webgpu imported target exists the module links against it
icudtl.dat Windows, Skia builds SkParagraph's ICU-backed SkUnicode returns null without it and deliberately traps on its fUnicode check during the first label render — the host process dies on first paint, it does not fall back to simpler text

On Apple the helper also sets BUILD_WITH_INSTALL_RPATH and appends @loader_path to INSTALL_RPATH, so a signed Developer ID bundle resolves the staged runtime from inside itself rather than from the SDK prefix used at build time (library validation rejects the latter).

ICU data resolution

One canonical Pulp-side variable, PULP_SKIA_ICUDTL_FILE, with SKIA_ICUDTL_FILE (what FindSkia sets) as the fallback. A Windows Skia build that resolves neither is a hard FATAL_ERROR rather than a silent omission — a module that configures cleanly and then kills its host on first render is the worse outcome.

Function Checks
pulp_stage_runtime_dependencies(<target>) adds the staging commands
pulp_verify_runtime_dependencies_staged(<target>) POST_BUILD: the files really landed beside the built binary
pulp_assert_runtime_dependencies_staged(<target>) configure time: the staging call was made at all
target_copy_webgpu_binaries(<target>) upstream/SDK primitive — copies only the wgpu runtime; pulp_stage_runtime_dependencies calls it

Call pulp_stage_runtime_dependencies rather than target_copy_webgpu_binaries in new code. The latter is a real upstream function that does exactly what its name says — copy the wgpu runtime — and nothing else: no @loader_path rpath on Apple, no ICU data on Windows. Using it alone produces a module that builds clean, runs on the build machine, and fails once shared or once it renders text.