This project uses Copier to manage shared infrastructure across multiple Control4 driver repositories. Shared code lives in a template repo and is synced into each driver project.
These files are owned by the template and should not be edited directly in
this repo. Change them in the
template repo instead,
then pull the change in with copier update.
Build tooling:
Makefile: build, format, docs, package, and clean targets.github/workflows/build.yml: CI build and packaging.github/workflows/release.yml: tagged GitHub releases
Common libraries (src/lib/):
bindings.lua: binding managementconditionals.lua: conditional/programming UI managementevents.lua: event firing and managementhttp.lua: HTTP client wrapperlogging.lua: structured logging with configurable levelslru.lua: LRU cache utilitypersist.lua: persistent storage abstractionutils.lua: general utilities (XML, device queries, table helpers, type coercion)values.lua: value parsing, coercion, and formattinggithub-updater.lua: GitHub Releases self-updater (non-DriverCentral builds)
Vendor libraries (vendor/):
JSON.lua: JSON encoder/decoderdeferred.lua: promises/deferred implementationdrivers-common-public/: Control4's official shared librariesxml/: XML parser (xml2lua)cloud-client-byte.lua: DriverCentral cloud licensingversion.lua: semver comparison (used by github-updater)bitn.lua: bit manipulation library
Tools (tools/):
preprocess.py: C-style#ifdef/#ifndefpreprocessor for Lua, XML, and Markdowndocs.py: documentation generation (Markdown -> HTML -> PDF, plus README)package.py: packaging helpers (driver.xml stamping, zip bundling)deps.py: build preflight, checks the.venvagainstrequirements.txtgen-squishy.lua: auto-generates squishy files from driver.c4zprojgithub-markdown.css: vendored stylesheet for the rendered docsgithub-setup: checks the GitHub repo against the standard settings and prints the drift;--applyfixes it
Other:
.gitignore,LICENSE,CONTRIBUTING.mdrequirements.txt, the build-time Python dependency listtest/c4_shim.lua,test/run_test.sh.copier-answers.yml, which Copier maintains; never edit it by hand
src/constants.lua: driver-specific constantsdrivers/*/driver.lua: main driver logicdrivers/*/driver.xml: driver XML configurationdrivers/*/driver.c4zproj: driver packaging manifestdrivers/*/www/: documentation and iconsCHANGELOG.md, seeded by the template on first render and left alone after that- Any additional
src/modules specific to this driver - Any additional
vendor/libraries specific to this driver
README.md is in neither list: it is generated. make build rewrites it
from the driver's www/documentation/index.md, so edit the source doc, not the
README.
Copier is only needed for this occasional sync. It is not a build dependency
and is intentionally not installed by make init, so run it without installing
anything:
uvx copier update --trust # using uv (https://docs.astral.sh/uv/)
# or:
pipx run copier update --trust # using pipxCopier shows diffs for any files that changed and lets you resolve conflicts. It
tracks which template version you're on via the .copier-answers.yml file
(committed to the repo). To update every driver repo, run the same command in
each one.
This project uses make for build orchestration. All tooling is Python (in a
local .venv) plus a few standalone binaries, with no Node/npm.
- Python 3.9+ (docs, formatters, preprocess, and driverpackager)
- Git, since
make initclones the driverpackager intodist/ - LuaJIT (
brew install luajit) to runtools/gen-squishy.luaand the tests - stylua (
brew install stylua), the Lua formatter - Pango (
brew install pango), WeasyPrint's PDF rendering engine swigand OpenSSL headers (brew install swig openssl), which M2Crypto compiles against
make init creates the .venv, installs everything in requirements.txt
(WeasyPrint, markdown-it-py, mdit-py-plugins, Pygments, mdformat, black, and the
driverpackager's M2Crypto + lxml), and clones the driverpackager into
dist/driverpackager. It is safe (and cheap) to re-run: the install is keyed on
requirements.txt, so a template update that adds a dependency reinstalls on
the next make init or make build rather than leaving the existing .venv
stale.
make help # List every pipeline step, plus init/build/fmt/clean
make init # Install all dependencies (safe to re-run)
make check-deps # Install if needed, then verify .venv matches requirements.txt
make build # Full build, from clean through zip (see Build Pipeline)
make build-nodocs # Build without generating docs
make fmt # Format all code (Lua, Python, Markdown)
make clean # Remove build artifacts and dist/ (including driverpackager)
make clean-all # Remove everything (build artifacts, dist, deps, venv)Every step in the pipeline below is also a target of its own, and make help
lists them all. The sub-targets those steps delegate to (fmt-lua, fmt-py,
fmt-md, docs-html, docs-pdf, docs-readme, update-xml-version,
update-xml-modified) are real targets too, but carry no ## annotation, so
make help does not print them. Note that the later steps read build/:
make docs pulls in preprocess itself, but make package and make zip fail
on a clean tree until something has populated build/.
make build runs these in order, starting from a cleaned build/:
- Format: stylua (Lua), black (Python), mdformat (Markdown)
- Preprocess: resolve
#ifdef/#ifndefdirectives per distribution - Generate squishy: create squish manifests from .c4zproj files
- Update driver.xml: stamp version date and modified timestamp
- Generate docs: Markdown -> HTML -> PDF, plus README
- Package: run driverpackager to create .c4z files
- Zip: bundle the c4z, docs and source folders per distribution
Each distribution's outputs are grouped by type under dist/<distribution>/:
c4z/ (packaged drivers), docs/ (generated PDFs) and source/ (the squished
per-driver Lua). <repo>.zip sits alongside them and contains all three.
Step 1 rewrites tracked files in place. Formatting covers tools/*.py, all Lua
under drivers/, src/, test/, tools/ and vendor/, and every Markdown
file at the repo root, under documentation/, and under
drivers/*/www/documentation/, so it reformats this file, the driver docs you
author, and the generated README.md too. CI fails the build if it finishes
with a dirty tree, so build on a clean checkout and commit whatever the
formatter changes.
Builds are configured for these distributions: drivercentral oss
Each one produces its own set of .c4z driver files, with code paths selected by
#ifdef directives (for example #ifdef DRIVERCENTRAL vs #ifdef OSS). The
preprocessor recognizes three distributions: drivercentral and
drivercentral-dev (which both define DRIVERCENTRAL, the latter also
DRIVERCENTRAL_DEV) and oss (which defines OSS). Any other token in the
answers file fails the build.
The tools/preprocess.py script supports C-style conditional compilation. It
walks every file in the build tree and matches four comment styles: Lua (--#),
XML and Markdown (<!-- # -->), C and JavaScript (// #), and bare #
directives with no comment prefix.
--#ifdef DRIVERCENTRAL
DC_PID = 1234
DC_FILENAME = "driver.c4z"
--#else
DRIVER_GITHUB_REPO = "finitelabs/control4-zigbee3"
--#endif<!-- #ifdef DRIVERCENTRAL -->
<Driver type="c4z" name="driver_dc" squishLua="true">
<!-- #else -->
<Driver type="c4z" name="driver" squishLua="true">
<!-- #endif -->It also resolves <!-- #embed-changelog -->, which splices CHANGELOG.md into
the generated documentation. That runs as a pre-pass, so directives inside the
changelog are still honored.
Drivers can define variants via a variants.json file. The preprocessor expands
these into multiple driver directories with substituted values, generating one
.c4z per variant combination. Variants can be listed flat or given as
dimensions, in which case the cross-product is expanded.
Substitute a variant value with __NAME__, or with %%NAME%% in Markdown,
where __NAME__ would render as bold instead. In .c4zproj files,
--#variant-filenames <template> emits one filename line per generated variant.