API Manifest and Versioning
Home > Manifest and Versioning
tsujikiri can track the binding surface (API) of your C++ headers over time using a manifest — a JSON snapshot of what is currently exposed. Two manifests can be compared to detect breaking changes and suggest a semantic version bump.
What the Manifest Captures
The manifest is computed from the filtered and transformed IR, after all emit=False nodes have been removed. It records the binding-visible surface — names after any renames, types before format-level remapping:
Classes: their binding name, all emitted constructor signatures, all emitted methods (name, parameters, return type, is_static), all emitted fields (name, type, is_const), and nested enums
Free functions: name, parameter types, return type
Top-level enums: name and all value names with their integer values
The manifest does not capture:
Suppressed nodes (
emit=False)Template-level type remapping (from
type_mappingsin.output.yml)Code injections
Comments or generation settings
Deterministic UID
The uid field is a SHA-256 hash of the api section serialised with sorted keys. The same C++ surface always produces the same uid, regardless of file ordering or timestamp.
Manifest JSON Structure
{
"module": "myproject",
"version": "1.2.0",
"api": {
"classes": [
{
"name": "Vec3",
"constructors": [
[],
["float", "float", "float"]
],
"methods": [
{
"name": "length",
"params": [],
"return_type": "float",
"is_static": false
},
{
"name": "dot",
"params": ["const Vec3 &"],
"return_type": "float",
"is_static": false
}
],
"fields": [
{ "name": "x_", "type": "float", "is_const": false },
{ "name": "y_", "type": "float", "is_const": false },
{ "name": "z_", "type": "float", "is_const": false }
],
"enums": []
}
],
"functions": [
{
"name": "computeArea",
"params": ["double"],
"return_type": "double"
}
],
"enums": [
{
"name": "Color",
"values": [
{ "name": "Blue", "value": 2 },
{ "name": "Green", "value": 1 },
{ "name": "Red", "value": 0 }
]
}
]
}
}
Tip: Commit the manifest JSON file to version control alongside the generated bindings. This gives you a complete history of API changes.
Saving and Loading a Manifest
# First run — generate bindings and save the initial manifest
tsujikiri -i project.input.yml --target luabridge3 src/bindings.cpp \
-m api.manifest.json
# Subsequent runs — compare with existing manifest, then save updated one
tsujikiri -i project.input.yml --target luabridge3 src/bindings.cpp \
-m api.manifest.json
When -m FILE is passed:
If
FILEdoes not exist: generate bindings normally, then save the manifest.If
FILEdoes exist and the uid differs: compare the two manifests, print a report, then save the new manifest.If
FILEexists and uid is identical: no changes; keep the existing manifest unchanged.
Comparing Manifests — Breaking vs Additive
When the manifest changes, tsujikiri classifies each difference:
Breaking Changes (scripts that use the old surface may break)
What changed |
Example |
|---|---|
Class removed |
|
Constructor removed |
|
Method signature removed or changed |
|
Field removed |
|
Field type changed |
|
Field const qualifier changed |
|
Enum removed |
|
Enum value removed |
|
Enum value integer changed |
|
Additive Changes (existing scripts continue to work)
What changed |
Example |
|---|---|
Class added |
|
Constructor overload added |
|
Method added |
|
Method overload added |
|
Field added |
|
Enum added |
|
Enum value added |
|
Stderr Output
WARNING: Additive API changes:
+ Class 'Matrix4' was added
+ Method 'Vec3.normalize() -> Vec3' was added
ERROR: Breaking API changes detected:
! Method 'Vec3.dot(const Vec3 &) -> float' signature was removed or changed
! Field 'Vec3.z_' was removed
--check-compat — Fail on Breaking Changes
tsujikiri -i project.input.yml --target luabridge3 src/bindings.cpp \
-m api.manifest.json --check-compat
When --check-compat is passed:
If breaking changes are detected: exit with code
1; the manifest is not saved (the old manifest is preserved)If only additive changes: exit
0; manifest is saved normallyIf no changes: exit
0; manifest is unchanged
Use --check-compat in CI to block merges that would break existing Lua scripts.
Semantic Versioning Integration
If the existing manifest has a "version" field that is a valid MAJOR.MINOR.PATCH semver string, tsujikiri suggests a bumped version:
Change type |
Bump |
|---|---|
Breaking changes present |
Bump |
Only additive changes |
Bump |
No changes |
Version unchanged |
INFO: Suggested semver bump: 1.2.0 -> 2.0.0
The suggestion is printed to stderr. The manifest is saved with the suggested version automatically.
To seed the initial version, manually edit the saved manifest JSON and set "version": "1.0.0". On the next run, tsujikiri will pick it up and suggest bumps from there.
--embed-version — Version Hash in Generated Code
tsujikiri -i project.input.yml --target luabridge3 src/bindings.cpp \
-m api.manifest.json --embed-version
When --embed-version is passed (or embed_version: true in generation), the api version number is embedded in the generated code.
luabridge3 output:
static constexpr const char* k_myproject_api_version = "1.7.3";
const char* get_myproject_api_version()
{
return k_myproject_api_version;
}
// Inside register_myproject():
.addFunction("get_api_version", +[] () -> const char* { return k_myproject_api_version; })
luals output:
---@return string
function myproject.get_api_version() end
Runtime version check (Lua side):
local function parse(v)
local a, b, c = v:match("(%d+)%.(%d+)%.(%d+)")
return a*1e6 + b*1e3 + c
end
local expected = "1.7.3"
local actual = myproject.get_api_version()
if parse(actual) ~= parse(expected) then
error(("API version mismatch: expected %s got %s"):format(expected, actual))
end
This lets you detect at runtime when a Lua script was compiled against a different API version than the loaded library provides.
Complete CI Workflow Example
The following shell script demonstrates a full versioning workflow in a CI pipeline:
#!/bin/bash
set -euo pipefail
INPUT="project.input.yml"
MANIFEST="api.manifest.json"
OUTPUT="src/lua_bindings.cpp"
echo "--- Generating bindings ---"
tsujikiri -i "$INPUT" --target luabridge3 "$OUTPUT" -m "$MANIFEST" \
--check-compat \
--embed-version
# If we get here, either:
# a) No manifest existed yet (first run), or
# b) Changes were only additive (MINOR bump applied), or
# c) No changes at all
echo "--- Generating LuaLS annotations ---"
tsujikiri -i "$INPUT" --target luals "types/myproject.lua"
echo "--- Committing updated bindings ---"
git add "$OUTPUT" "$MANIFEST" "types/myproject.lua"
git diff --staged --quiet || git commit -m "chore: update generated bindings"
If the C++ API has breaking changes, tsujikiri exits with code 1 at the --check-compat step, the script stops (due to set -e), and CI marks the build as failed.
Sample manifest after a breaking change is resolved and MAJOR bumped:
{
"module": "myproject",
"version": "2.0.0",
"api": {
"classes": [ ... ],
"functions": [ ... ],
"enums": [ ... ]
}
}
See Also
Getting Started —
--manifest-file,--check-compat,--embed-versionCLI flagsInput File Reference —
generation.embed_versionconfig keyOutput Formats — how the API version hash appears in luabridge3 and luals templates