A spice is a Turmeric package that other projects depend on. This guide
covers every step of authoring one: project structure, declaring exports,
wrapping C libraries with :cmake-deps, testing, versioning, and publishing.
See Consuming Spices if you only need to add an existing spice to your project.
Create a spice when you want to:
tur add.For code shared only within a single project, use the module system -- no separate package is needed.
tur new tur-mylib --lib
cd tur-mylib
This creates:
tur-mylib/
build.tur -- package manifest
tur.lock -- empty lock file (commit to VCS)
src/
lib.tur -- stub exported function
.gitignore
README.md
By convention, name the package with a tur- prefix. Consumers drop the
prefix as the import alias: tur-geom becomes geom, tur-math becomes
math.
build.tur ManifestA complete library manifest:
(defpackage tur-mylib
:name "tur-mylib"
:version "0.1.0"
:description "A brief description of my library"
:license "MIT"
:authors ["Your Name <you@example.com>"]
:repository "https://github.com/you/tur-mylib"
:exports {
"mylib/core" ["some-fn" "another-fn"]
"mylib/util" ["helper-fn"]
})
| Field | Required | Notes |
|---|---|---|
:name |
Yes | Must match [a-z][a-z0-9-]* |
:version |
Yes | Semver: MAJOR.MINOR.PATCH |
:description |
Recommended | One-line summary |
:license |
Recommended | SPDX identifier (e.g. "MIT") |
:authors |
Recommended | "Name <email>" list |
:repository |
Recommended | URL to the canonical Git repo |
:exports |
Yes (library) | Map of module path to exported symbol names |
:spices |
If needed | Turmeric package dependencies |
:cmake-deps |
If needed | C/C++ library dependencies |
:build-opts |
Rarely | Extra C compiler flags or additional link libs |
The :exports map controls what is visible to consumers. Only listed
symbols are part of the public API; everything else is private.
:exports {
"mylib/types" ["Coord" "Rect" "Color"]
"mylib/draw" ["draw-rect" "draw-circle" "draw-line"]
"mylib/io" ["read-file" "write-file"]
}
Each key ("mylib/types") becomes an importable path for consumers:
(import mylib/types :refer [Coord Rect])
(import mylib/draw :refer [draw-rect])
Internal helpers that should not be exposed are simply omitted from
:exports.
Follow this layout so that module paths and file paths align without extra configuration:
tur-mylib/
build.tur
tur.lock
src/
types.tur -- exports "mylib/types"
draw.tur -- exports "mylib/draw"
io.tur -- exports "mylib/io"
internal/
helpers.tur -- not exported; only imported by other src files
tests/
types_test.tur
draw_test.tur
The module path "mylib/types" resolves to src/types.tur. A nested path
"mylib/net/http" resolves to src/net/http.tur.
Add Turmeric spice dependencies the same way any project does:
tur add https://github.com/rjungemann/turmeric-spices \
--ref math-v0.1.0 --subdir spices/math --name math
This produces:
:spices {
"math" {:url "https://github.com/rjungemann/turmeric-spices"
:ref "math-v0.1.0"
:subdir "spices/math"}
}
Mark spices that are only needed for tests :optional true so consumers
are not forced to fetch them:
:spices {
"test" {:url "https://github.com/rjungemann/turmeric-spices"
:ref "test-v0.1.0"
:subdir "spices/test"
:optional true}
}
:cmake-depsThe :cmake-deps block tells tur build to fetch and compile a C library
via CMake, then inject the resulting include dirs and link flags into the
compilation step. You write only Turmeric; CMake is an implementation detail.
:cmake-deps {
"sqlite3" {:url "https://github.com/sqlite/sqlite"
:ref "version-3.47.2"
:options {:BUILD_SHARED_LIBS "OFF"}}
}
:options sets CMake cache variables (-DKEY=VALUE). Common patterns:
:BUILD_SHARED_LIBS "OFF":BUILD_EXAMPLES "OFF", :YYJSON_BUILD_TESTS "OFF"Use include-c to pull in the C header and extern-c to declare symbols:
;; src/db.tur
(include-c "sqlite3.h")
(extern-c sqlite3_open [:cstr :ptr] :int)
(extern-c sqlite3_close [:ptr] :int)
(extern-c sqlite3_exec [:ptr :cstr :ptr :ptr :ptr] :int)
(extern-c sqlite3_errmsg [:ptr] :cstr)
(defn db-open [path :cstr] (result :ptr)
(let [db-ptr (make-ptr)]
(let [rc (sqlite3_open path db-ptr)]
(if (= rc 0)
(ok (deref-ptr db-ptr))
(err (cstr->str (sqlite3_errmsg (deref-ptr db-ptr))))))))
extern-c trusts the signature you give it -- double-check it against the
actual C header. No -I or -L flags are needed in your source; tur build
injects them from cmake/spice-deps-manifest.json.
When a binding is simpler to write directly in C, use an inline-C block:
(defn db-last-insert-rowid [db :ptr] :int
```c
return (int)sqlite3_last_insert_rowid((sqlite3*)db);
```)
The closing ``` and its enclosing ) must be on the same line. See
the inline-C style rule for why.
:cmake-name and :targets overridesWhen the CMake find_package name or target name differs from the key in
:cmake-deps, supply overrides:
:cmake-deps {
"sqlite" {:url "https://github.com/sqlite/sqlite"
:ref "version-3.47.2"
:cmake-name "SQLite3"
:targets ["SQLite::SQLite3"]}
}
For the full :cmake-deps field reference, the generated SpiceDeps.cmake
format, the spice-deps-manifest.json schema, and hash locking, see the
CMake/CPM integration notes.
Every exported symbol should have a ;;; docstring immediately above its
definition. The standard format (from CLAUDE.md):
;;; db-open -- open an SQLite database file.
;;;
;;; Parameters:
;;; path -- filesystem path to the database file (created if absent)
;;;
;;; Returns:
;;; (ok db) on success, (err message) if the file cannot be opened
;;;
;;; Example:
;;; (match (db-open "app.db")
;;; (ok db) (println "opened")
;;; (err m) (println "failed:" m))
;;;
;;; Since: Phase P2
(defn db-open [path :cstr] (result :ptr)
...)
Exported symbols without docstrings will be omitted from just docs output.
A spice module can optionally include a module-level docstring at the very top
of the file, before the first defn, defmacro, defstruct, definstance,
or defopaque. Place a contiguous ;;; block followed by a ;; comment line
(which acts as the separator):
;;; myspice/db -- SQLite database bindings.
;;;
;;; Thin wrapper around libsqlite3; provides open/close/query/exec with
;;; result-typed error handling.
;;;
;;; Since: Phase P2
;; ---- SQLite bindings ----
(extern-c sqlite3_open ...)
Without a module docstring the page renders with no description block -- the per-symbol cards still appear normally.
cons ... 0 chains in examplesIn README quick-starts and docstring examples, do not show runtime list values
as (cons x (cons y 0)) chains. The trailing 0 is the nil-of-list footgun --
new readers have no way to tell that 0 means "end of list," and the chain
itself reads in reversed nesting order.
Use the list macro instead. It expands to the same tcons/tnil cells, so
it's a drop-in for any API that today takes a :int cons list:
;; Avoid
(group-by f (cons "g" 0))
(plot (cons (axes) (cons (function f) 0)))
;; Prefer
(group-by f (list "g"))
(plot (list (axes) (function f)))
Pair-cons ((cons key value) with no trailing 0) is fine -- a two-element
pair is a clear, idiomatic data shape. The footgun is the nil-terminated chain.
If your spice API can take a Vec instead of a cons list, prefer that and
document it with (vec-of ...). The end goal is for cons to disappear from
quick-start surfaces entirely.
Add tur-test as an optional dependency and place test files in tests/:
tur add https://github.com/rjungemann/turmeric-spices \
--ref test-v0.1.0 --subdir spices/test --name test
A test file using tur-test:
(import test/assert :refer [assert-eq assert-ok assert-err])
(import test/suite :refer [describe it])
(import test/runner :refer [run-all])
(import mylib/core :refer [some-fn])
(describe "some-fn"
(it "returns the expected value"
(assert-eq (some-fn 10) 42))
(it "returns err for invalid input"
(assert-err (some-fn nil))))
(run-all)
Run the test suite:
tur test
For the full testing API see test-runner-contract.md.
MAJOR.MINOR.PATCH.vMAJOR.MINOR.PATCH for standalone repos (v0.1.0).turmeric-spices monorepo use the per-spice tag
format <spice>-vMAJOR.MINOR.PATCH (math-v0.1.0, sqlite-v0.2.1).v0.2.0-alpha.1.MAJOR for breaking API changes, MINOR for additive changes,
PATCH for bug fixes.git tag v0.1.0
git push && git push --tags
Consumers then add your spice with:
tur add https://github.com/you/tur-mylib --ref v0.1.0
The turmeric-spices monorepo accepts spices that meet the bar for the ecosystem. To contribute:
spices/<name>/.build.tur :members list.README.md and at least one test file.After merging, tag the spice's first release:
git tag math-v0.1.0
git push --tags
Consumers use --subdir spices/<name> when adding:
tur add https://github.com/rjungemann/turmeric-spices \
--ref myspice-v0.1.0 --subdir spices/myspice --name myspice
Once pkg.turmeric-lang.org launches, tur publish will register the
package and consumers will use tur add spice/<name> without a Git URL.
tur build <dir> and tur run (project mode) know about the spice they
live in because they start by reading build.tur. The per-file
subcommands tur check, tur emit-c, tur emit-h, and tur run <file>
get the same behavior automatically -- they walk up from the input file
looking for a sibling build.tur and add that spice's src/ to the
include path. If the manifest declares :spices dependencies, those
deps' src/ directories are added too.
This means editors, format-on-save hooks, LSP clients, and quick "compile this one file" loops work without per-spice configuration:
cd spices/frame
tur check src/frame/frame.tur # resolves intra-spice imports
tur emit-c src/frame/schema.tur # same
tur run src/frame/quickstart.tur # builds and executes
The walk-up is capped at 16 ancestor directories, so a stray
build.tur far above your working tree won't accidentally win.
-I for extra directoriesExplicit -I <dir> flags still work and take priority over
auto-discovered paths -- useful for fixtures, vendored copies, or when
you want to test against a different version of a dep:
tur check -I vendor/alternate src/main.tur
-I accepts both the spaced (-I path) and concatenated (-Ipath)
forms.
--no-auto-spice escape hatchIf you need to compile a file as if no spice exists around it (rare --
typically only useful for resolver-fixture tests or when an unrelated
build.tur is in the ancestor chain), pass --no-auto-spice:
tur --no-auto-spice check tests/fixtures/resolver/input.tur
This restores the pre-auto-discovery behavior: only the input file's
own directory, the stdlib, and any explicit -I paths are searched.
tur build <dir> -- already configures itself from the directory's
build.tur.tur build <file> -- the single-file build doesn't auto-discover;
use tur run <file> if you want the same convenience for a one-off
invocation, or pass -I explicitly.tur format <file> -- the formatter doesn't resolve imports, so
include paths are irrelevant.If you want C and C++ projects to consume your spice via CMake or CPM without knowing anything about Turmeric, run:
tur emit-cmake
This reads build.tur and generates CMakeLists.txt,
<name>Config.cmake, and helper modules. Commit those files, tag the
release, and a CPM consumer can add your library with:
CPMAddPackage(
NAME tur-mylib
URL https://github.com/you/tur-mylib/archive/refs/tags/v0.1.0.tar.gz
VERSION 0.1.0
)
target_link_libraries(my_app PRIVATE tur-mylib::all)
For the complete step-by-step see Using a Turmeric library from CMake.
Once tur build --target wasm lands, all cmake-deps spices will get WASM
builds automatically when the underlying C library supports Emscripten.
To make your spice Emscripten-compatible:
#ifdef __EMSCRIPTEN__ in inline-C blocks to handle differences
(e.g. skipping native TLS when the browser provides its own TLS stack).A spice installed globally with tur install is currently usable as a
command-line tool only: its :bin entries are symlinked into
~/.local/bin/ and become available as tur-<cmd> (or via the
tur <cmd> fallthrough). The same install is not automatically
visible as a library to other projects -- the global spices/ root is
left out of the default module-resolution path to preserve build
reproducibility.
A future v2 will let a project opt in to consuming a globally-installed
spice as a library by naming it in its build.tur:
:spices {
"notebook" {:global true}
}
tur fetch would then validate the global install exists at a matching
version and record its resolved SHA in tur.lock. A project-level
:global-policy knob would decide whether a missing global install gets
auto-installed or errors out.
This is deferred; until it ships, a spice that wants to be reused as
a library should be added the normal way with tur add. See the
global-spice-install plan
for the full design sketch.
tur build --release passes with no warningstur test passes:exports;;; docstring (summary + params + returns + example)tur emit-cmake succeeds if you intend to support CMake consumersCHANGELOG.md entry writtengit tag v0.1.0 && git push --tagsbuild.tur manifest reference and tur CLIextern-c, include-c, inline-C blockstur emit-cmake