Consuming Spices Guide

A spice is a Turmeric package that your project depends on. This guide covers how to find, add, fetch, and import spices -- including pure-Turmeric spices and C library bindings via :cmake-deps.

See Developing Spices if you want to create and publish a spice of your own.


Official First-Party Spices

The canonical source for official spices is the turmeric-spices monorepo. Its packages include:

Spice Description C deps?
tur-test Testing framework: describe/it/TAP output None
tur-math 2D/3D vector and matrix math None
tur-sqlite SQLite3 bindings SQLite amalgamation
tur-raylib Raylib graphics and input Raylib
tur-json JSON parsing and serialization yyjson
tur-http Async HTTP/HTTPS client mbedTLS
tur-regex PCRE2 regular expression bindings PCRE2

The web stack (tur-httpd, tur-template, tur-tourist, tur-tls, tur-ws-client/tur-ws-server) and the data/DSP spices (tur-frame, tur-stats, tur-signal, tur-ecs, ...) live there too -- see their per-spice guides. Each spice lives in its own subdirectory and is versioned independently with a per-package tag: <spice>-vMAJOR.MINOR.PATCH.


Adding Spices

Official spices from turmeric-spices

Use tur add with --subdir to pull a spice from the monorepo:

tur add https://github.com/turmeric-lang/turmeric-spices \
  --ref test-v0.1.0 --subdir spices/test --name test

tur add https://github.com/turmeric-lang/turmeric-spices \
  --ref math-v0.1.0 --subdir spices/math --name math

tur add https://github.com/turmeric-lang/turmeric-spices \
  --ref sqlite-v0.1.0 --subdir spices/sqlite --name sqlite

tur add https://github.com/turmeric-lang/turmeric-spices \
  --ref json-v0.1.0 --subdir spices/json --name json

Each command updates build.tur and tur.lock automatically.

Third-party spices from a Git URL

tur add https://github.com/alice/tur-geom --ref v0.2.1

The spice name defaults to the last path segment with any tur- prefix stripped (tur-geom becomes geom). Override with --name:

tur add https://github.com/alice/tur-geom --ref v0.2.1 --name geometry

If --ref is omitted the tool resolves to HEAD and warns:

Warning: no --ref specified; resolved to HEAD (a1b2c3d4).
Pin with: tur add https://github.com/alice/tur-geom --ref a1b2c3d4

Always pin with an explicit tag or commit SHA to keep builds reproducible.

Local path spices (development)

tur add ../tur-utils --path

Use this while actively developing a dependency alongside your project. Local path spices are not recorded in tur.lock and are never fetched from the network.

Globally installed spices

A spice you installed with tur install can be consumed as a library by declaring it :global -- there is no tur add flag for this yet, so write the entry by hand:

:spices #map{
  "notebook" #map{:global true}
}

It resolves through the install registry rather than <project>/spices/, so nothing is fetched and no tur.lock row is written (as with a :path dep). If the spice is not installed, tur fetch says so and fails rather than letting the build reach module not found. :global takes neither :url nor :path -- those name a different resolution source. See Global Spices as Libraries for the full rules.


Using spices without a project

A one-off script or a REPL session has no build.tur to declare spices in. For those, an installed spice is importable directly:

tur install https://github.com/turmeric-lang/turmeric-spices \
  --ref json-v0.3.0 --subdir spices/json
cd ~/scratch                  # no build.tur anywhere above
tur run report.tur            # (import json/decode) resolves
tur repl                      # and at the prompt too

Any spice with modules can be installed -- one with no :bin entries is a library-only install (see Global Spices as Libraries).

Which files see the installed spices

Only a file with no build.tur above it (within 16 parent directories): tur run, tur check, tur build <file>, tur --interpret, tur jit, tur emit-c, tur expand, tur eval --file, the LSP, and libturi embedders that call turi_env_set_search_path_for. tur run - (stdin) and tur repl are anchored at the current directory.

A file inside a project sees exactly the spices its manifest declares, as always -- a project build must not depend on what happens to be installed on the machine, and a script has no lock file whose reproducibility the installed set could break. Inside a project, reach an installed spice with a :global dep, or with :spice at the REPL.

Search order

Outside a project an import is searched in:

  1. the importing file's directory,
  2. the stdlib,
  3. -I dirs, in command-line order,
  4. every installed spice's own src/, in install-name order (byte-wise),
  5. each installed spice's own dependencies, in the same order.

Steps 1-3 are what was searched before; installed spices only fill a gap. An import that resolves without them resolves to the same file with them, and the spice you installed by name beats another version of it that some other installed spice depends on. A failed import lists the installed dirs it tried, labelled (installed spice <name>), and names tur install in its hint.

Linking follows the imports: an installed spice's :c-sources, :cmake-deps and :link-libs reach a program only when the program imports one of its modules, so one broken or platform-specific installed spice does not break every script.

Turning it off

--no-global-spices (a global flag) or TUR_NO_GLOBAL_SPICES=1 removes steps 4 and 5. --no-auto-spice implies it -- it already means "add nothing to the search path on my behalf". The repository's test harnesses set TUR_NO_GLOBAL_SPICES=1, so a developer's installed spices cannot leak into the fixture suite.

Declaring spices in the script itself (defscript)

An installed spice makes a script work on your machine. To make it work on anyone's, declare what it needs in the file, in a (defscript ...) header. This is experimental, so it needs --enable=script-deps -- on the #! line, on the command line, or in ~/.config/turmeric/experiments.tur:

#!/usr/bin/env -S tur --enable=script-deps
(defscript
  :spices #map{"json" #map{:url    "https://github.com/turmeric-lang/turmeric-spices"
                           :ref    "json-v0.3.0"
                           :subdir "spices/json"}})

(defmodule report
  (import json/decode)
  (defn main [] : int
    ...))
chmod +x report.tur
./report.tur data.json       # first run fetches json-v0.3.0; later runs are offline

The rules:

Every per-file command honors the header -- tur run, tur check, tur build <file>, tur --interpret, tur jit, tur emit-c -- and the LSP resolves it from the cache without ever fetching.

Where the spices go

Fetched spices land in the script-dep cache, ~/.local/share/turmeric/cache/script-spices/ (under $TUR_HOME when set): one immutable tree per (url, subdir, commit), each set up as a fetched project, so its own :spices and :cmake-deps come with it. The cache is never your install set -- tur list does not show it, and two scripts can pin two versions of one spice side by side.

A tag or branch :ref is resolved once and remembered in the cache's index.tur; later runs need no network. A run that fetches says so on stderr (tur: fetching json (json-v0.3.0) for report.tur).

Command Effect
tur run --offline report.tur resolve from the cache only; a missing spice is TUR-E0634
tur run --update report.tur resolve tag and branch refs again
tur fetch --script report.tur warm the cache without running
tur cache list the entries, their size and when they were last used
tur cache clean [--older-than <days>] remove entries (all, or the ones not used for <days>)

Editing the header

tur --enable=script-deps add --script report.tur \
    https://github.com/turmeric-lang/turmeric-spices --ref json-v0.3.0 --subdir spices/json
tur --enable=script-deps add --script report.tur ./helpers --path --name helpers

tur add --script creates the header (after the #! and #lang lines) or adds the entry to it, written back through the formatter. Comments inside an existing header are not kept.

Locking a script

A one-off script should not grow a second file unless you want it to, so the lock is opt-in:

tur --enable=script-deps fetch --script report.tur --lock     # writes report.tur.lock

report.tur.lock uses tur.lock's format: a row per fetched spice with its commit and tree hash, and one per dependency of each, named <spice>/<dep>. While it exists:

:path and :global spices are never locked, as in build.tur.

Running a script by name

tur report.tur a b is tur run report.tur -- a b, which is what lets a #!/usr/bin/env -S tur --enable=script-deps line work: global flags before the file apply, and every word after it is the script's. A built-in command always wins (tur check.tur runs a script called check.tur; tur check is the command). Windows has no #!: run tur report.tur args there.

Running a spice's tool without installing it (tur x)

tur install makes a spice's :bin a command you keep. To run one once -- the way npx or uvx do -- use tur x:

tur x https://github.com/turmeric-lang/turmeric-spices \
      --ref notebook-v0.1.0 --subdir spices/notebook -- render hello.tur.md
tur x ~/src/my-tool -- --help        # a local checkout, built in place
tur x notebook -- new hello.tur.md   # an installed spice, run directly

Everything after -- is the program's. The source is read the way tur install reads it:

Source What tur x does
an installed name runs the binary tur install built; never builds
a path (has a /, or starts with . or ~) builds the :bin into <checkout>/build/, where tur install puts it, and again whenever a file under the checkout is newer than the binary
a URL (--ref, --subdir) fetches into the script-dep cache and builds the :bin there once

A URL lands in the same cache as a defscript header's deps -- <data>/cache/script-spices/<name>-<sha12>, set up as a fetched project, so its own :spices and :cmake-deps come with it -- and a tool and a script naming the same commit share one tree. The first run fetches and builds; later runs need no network. A --ref naming a branch is resolved once and remembered; --refresh resolves it again (an older tree stays until tur cache clean removes it). Without --ref, the default branch.

tur x registers nothing: no state.tur row, nothing in ~/.local/bin/, nothing added to the project-less search path. It also ignores any build.tur in the current directory -- inside a project or outside one it means the same thing.


What tur add Changes

Before adding any spice:

(defpackage my-app
  :name    "my-app"
  :version "0.1.0")

After tur add https://github.com/alice/tur-geom --ref v0.2.1:

(defpackage my-app
  :name    "my-app"
  :version "0.1.0"
  :spices #map{
    "geom" #map{:url "https://github.com/alice/tur-geom"
                :ref "v0.2.1"}
  })

Fetching and Updating

tur fetch               # download everything in tur.lock, at the pinned commits
tur fetch --update      # upgrade spices to the latest allowed versions
tur fetch --frozen      # what CI should run: fail if tur.lock would change

tur run and tur build invoke tur fetch automatically when any spice is missing. Pass --offline to skip the network and fail if a spice is absent:

tur run --offline

The Lock File

tur.lock records the resolved commit and a content hash of the tree for every fetched spice. Always commit it to version control; it is what makes builds reproducible across machines and CI runs.

;; tur.lock -- generated by `tur fetch`.  Do not edit by hand; commit it.
(deflockfile
  :format-version 1
  :spices #{
    "geom" #{:url "https://github.com/alice/tur-geom" :ref "v0.2.1" :resolved "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0" :sha256 "tree1:dc4d2c00b1e74a6f9c3d8e2f1a0b5c4d7e6f8a9b0c1d2e3f4a5b6c7d8e9f0a1b" :fetched-at "2026-10-21T09:00:00Z" :transitive ["math@math-v0.1.0"] }
    "math" #{:url "https://github.com/turmeric-lang/turmeric-spices" :ref "math-v0.1.0" :resolved "d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5" :sha256 "tree1:7a1c9e0d3b5f2a4c6e8d0b1f3a5c7e9d2b4f6a8c0e1d3f5b7a9c2e4d6f8b0a1c" :fetched-at "2026-10-21T09:00:03Z" }
  }
  :cmake-deps #{
    "raylib" #{:url "https://github.com/raysan5/raylib" :ref "5.0" :resolved "ae50bfa2cc569c5a9f4a3c0a4e2b7d8e9f0a1b2c" :fetched-at "2026-10-21T09:00:05Z" }
  })

tur fetch writes plain s-expressions, never the sweet-exp form. A spice row's :sha256 is a tree1: hash of the checked-out tree, not of an archive; :subdir is not recorded.

Rules:


Importing Spice Modules

Spice names map directly to import paths. A spice named geom that exports geom/vector and geom/matrix is imported like any other module:

(import geom/vector :refer [vector-2d cross-product])
(import geom/matrix :as mat)

(defn main [] :int
  (let [v (vector-2d 1.0 2.0)
        m (mat/mat4-identity)]
    (println v)
    0))

The compiler resolves geom/vector to spices/geom-v0.2.1/src/vector.tur. No extra configuration is needed.


Optional Spices

Mark a spice :optional true when it is only needed for tests or dev tooling. Optional spices that are absent do not cause a build error.

:spices #map{
  "test" #map{:url    "https://github.com/turmeric-lang/turmeric-spices"
              :ref    "test-v0.1.0"
              :subdir "spices/test"
              :optional true}
}

When tur test is run, optional spices are fetched so test files can import them. When tur build is run in production, they are skipped.

An optional spice that cannot be fetched is reported and skipped: tur fetch still writes tur.lock and exits 1, distinct from the 2 a required spice's failure earns (and the 0 of a clean fetch), so a CI job can warn on the first and fail on the second. See the exit-status table in package-management-guide.md.


C/CMake Dependencies

Some spices (like tur-sqlite or tur-raylib) wrap a C library. When you add them, tur build fetches and compiles the C library via CMake automatically -- no CMake knowledge required on your end.

You can also add a C library directly to your project without going through a spice:

tur add-cmake https://github.com/raysan5/raylib --ref 5.5 \
  --opt BUILD_SHARED_LIBS=OFF --opt BUILD_EXAMPLES=OFF

This appends a :cmake-deps entry to build.tur:

(defpackage my-app
  :name    "my-app"
  :version "0.1.0"
  :cmake-deps #map{
    "raylib" #map{:url     "https://github.com/raysan5/raylib"
                  :ref     "5.5"
                  :options #map{:BUILD_SHARED_LIBS "OFF"
                                :BUILD_EXAMPLES   "OFF"}}
  })

When tur build runs it:

  1. Generates cmake/CMakeLists.txt from the :cmake-deps block (via the automatic tur fetch).
  2. Invokes CMake to fetch and compile the C library.
  3. Reads cmake/spice-deps-manifest.json for include dirs, lib dirs, link libs, and link flags.
  4. Passes those flags to cc automatically.

Step 3's link flags are what make a Cocoa- or Objective-C-backed library (glfw, raylib) link on macOS: they carry the -framework Cocoa / -framework IOKit entries CMake records on the target, which cannot be spelled as -l. They also carry the dep's artifact by full path, so a target whose name differs from its library's (glfw's target glfw builds libglfw3.a) resolves correctly. Both are derived automatically from :targets; see :link-libs and :link-flags overrides for when you need to override them.

Declare the C symbols you need in Turmeric with extern-c (no header include is needed for the declarations themselves; an inline-C body that wants the header can hoist #include <raylib.h> to file scope with a __tur_include__ marker -- see the C integration guide):

(extern-c InitWindow        [:int :int :cstr] :void)
(extern-c CloseWindow       [] :void)
(extern-c WindowShouldClose [] :bool)
(extern-c BeginDrawing      [] :void)
(extern-c EndDrawing        [] :void)

(defn main [] :int
  (InitWindow 800 600 "Hello")
  (while (not (WindowShouldClose))
    (BeginDrawing)
    (EndDrawing))
  (CloseWindow)
  0)

No -I or -L flags are needed; tur build injects them from the manifest.

CMake deps are also tracked in tur.lock with SHA-256 hashes for integrity verification. For the generated cmake/CMakeLists.txt format, the manifest schema, security considerations, and the outbound direction (publishing a Turmeric library so CMake projects can consume it), see the CMake/CPM integration notes.


Security


Conflict Resolution

When two spices require incompatible versions of a shared dependency the build fails with a diagnostic. There is no silent multiple-version shadowing. You must either upgrade one spice or pin versions to a compatible range.


Common Error Messages

Condition Message
No build.tur in the directory tree No build.tur found. Run tur new <name> to create a project.
Spice already in the manifest 'geom' is already a dependency. To change its ref, edit the :spices entry for 'geom' in build.tur, then run tur fetch --update.
Network failure Failed to reach https://github.com/...: <reason>
Ref not found Ref 'v99.0.0' not found in https://github.com/alice/tur-geom
SHA mismatch on re-fetch Integrity check failed for 'geom'. Run tur fetch --force to re-download.

A spice can declare which compiler versions it supports with :tur-version; if your tur is below that floor the build stops with TUR-E0621 (above a declared ceiling is TUR-W0623, a warning, and a malformed range in the spice's own manifest is TUR-E0622) -- see Declaring a compiler version range.


CLI Quick Reference

# Add a Turmeric spice
tur add <url>
tur add <url> --ref <tag-or-sha>
tur add <url> --ref <ref> --subdir <path> --name <alias>
tur add <path> --path

# Add a C/CMake dependency
tur add-cmake <url> --ref <ref>
tur add-cmake <url> --ref <ref> --opt KEY=VALUE

# Fetch and update
tur fetch
tur fetch --update

# Build and run
tur build
tur build --release
tur run
tur run --release
tur run --offline
tur test

# A script's own spices (defscript header; --enable=script-deps)
tur add --script <file> <url> --ref <ref>
tur fetch --script <file> [--update] [--lock]
tur run [--offline|--update] <file>
tur <file> <args...>
tur cache list
tur cache clean [--older-than <days>]

# Run a spice's tool once, without installing it
tur x <url> [--ref <ref>] [--subdir <dir>] [--bin <name>] [-- <args...>]
tur x <path> [-- <args...>]
tur x <installed-name> [-- <args...>]

See Also