Luban
Windows-first C++ toolchain manager + cmake/vcpkg auxiliary frontend. Single static-linked binary, zero UAC, XDG-first directories.
π δΈζη β
What problem Luban solves
If you've ever tried to set up a modern C++ toolchain on Windows, you've met the wall:
- pick LLVM-MinGW or MSVC or mingw-w64;
- install cmake, ninja, clangd, vcpkg separately;
- discover that vcpkg needs git;
- learn cmake's quirks, vcpkg manifest mode, CMakePresets, triplets;
- write a CMakeLists.txt that conditionally pulls in
find_package+target_link_librariesfor each dep; - realize clangd needs
compile_commands.jsonto give you autocomplete; - realize none of this is on
PATHuntil you run an activate script in every fresh shell.
Each piece is a half-day of yak-shaving. The full stack-up usually takes a few days, and rebuilds itself on every new machine.
Luban replaces the yak-shaving with two commands:
luban setup # one-time, installs LLVM-MinGW + cmake + ninja + mingit + vcpkg
luban env --user # one-time, registers everything on user PATH (rustup-style)
After that, fresh shells have cmake, clang++, ninja, clangd directly. Then for each project:
luban new app foo # scaffold; auto-builds; clangd ready out of the box
cd foo
luban add fmt # one-line dep add
luban build # cmake fetches the dep via vcpkg manifest mode
You never open CMakeLists.txt for the 80% case.
What Luban is not
- Not a build system. cmake + ninja still do the building. Luban only generates the cmake glue.
- Not a package manager. vcpkg still resolves and builds C++ packages. Luban edits
vcpkg.jsonfor you. - Not a replacement. You can
git clonea Luban-managed project and build it on a machine without Luban βcmake --preset defaultworks as long as cmake + vcpkg are present. Luban-generated files (luban.cmake) commit cleanly to the repo.
Design philosophy
| Principle | What it means |
|---|---|
| Auxiliary, not authoritative | cmake / vcpkg / ninja stay primary. Luban writes luban.cmake (a standard cmake module) that the user include()s. Drop one line and luban is gone. |
| No new manifest format | Deps live in vcpkg.json (vcpkg's own schema). luban.toml is optional and only holds project-level preferences (warning level, sanitizers). |
luban.cmake is git-tracked | Reproducibility: cloning the project on a Luban-free machine still builds. |
| XDG-first directories | ~/.local/share/luban/ (data), ~/.cache/luban/ (cache), ~/.local/state/luban/ (state), ~/.config/luban/ (config). XDG_* env vars override. |
| Zero UAC | All Luban toolchains land in user-writable directories. No admin prompts ever. |
| Single static binary | One luban.exe with everything. No Python, no MSI, no installer. |
Where to next
- Installation β getting
luban.exeon your machine - Quickstart β the 5-command sequence to "hello, fmt"
- The Daily Driver Loop β what a typical week with luban looks like
- Commands β Overview β full command reference
Installation
Luban is a single static-linked Windows executable (~31 MB). No installer, no PATH dance, no admin rights required.
Prerequisites
- Windows 10 (1809+) or Windows 11 β both
x64only for now - A working network connection (for
luban setupto fetch toolchains; ~250 MB total download)
That's it. No Python, no Visual Studio, no Chocolatey/Scoop, nothing pre-installed.
Pick your install style
Option A β drop the binary anywhere
- Download
luban.exefrom the GitHub Releases page. - Save it somewhere you'll remember β e.g.
%USERPROFILE%\bin\luban.exe. - From the next step you can run it via full path or have it on PATH (Option C below).
Option B β build from source
If you already have a C++ toolchain (MSVC, MinGW, Clang) with cmake + ninja:
git clone https://github.com/Coh1e/luban.git
cd luban
cmake --preset default
cmake --build --preset default
:: build/default/luban.exe
You can then use this freshly-built luban.exe to install Luban-managed toolchains.
Option C β bootstrap fully from a single binary
luban.exe setup :: installs LLVM-MinGW 22, cmake 4.3, ninja 1.13, mingit 2.54, vcpkg 2026.03
luban.exe env --user :: rustup-style HKCU PATH integration (one-time)
After this, any new shell has cmake / clang / clangd / ninja / git / vcpkg on PATH. You'll never need to source an activate script again.
Tip: After
luban env --user, copyluban.exeitself into<data>\bin\so it's also on PATH:copy luban.exe %LOCALAPPDATA%\luban\bin\luban.exe
Verify
luban doctor
Expected output:
β Canonical homes
β data C:\Users\you\.local\share\luban
β cache C:\Users\you\.cache\luban
...
β Installed components
β cmake 4.3.2
β llvm-mingw 20260421
β mingit 2.54.0
β ninja 1.13.2
β vcpkg 2026.03.18
β Tools on PATH
β clang C:\Users\you\.local\share\luban\bin\clang.cmd
β cmake ...
...
If any tool says (not found) after luban env --user, open a fresh terminal window β the change only takes effect in newly-spawned shells, not the one that ran the command.
What's installed and where
%LOCALAPPDATA%\luban\
toolchains\
cmake-4.3.2-x86_64\ # cmake.exe + share/cmake-4.3
llvm-mingw-20260421-x86_64\ # clang/clang++/clangd/lld/libc++ + sysroot
ninja-1.13.2-x86_64\
mingit-2.54.0-x86_64\
vcpkg-2026.03.18-x86_64\ # ports tree + vcpkg.exe
bin\ # rustup-style shim dir (on PATH after env --user)
cmake.cmd / cmake.ps1 / cmake # 280+ alias shims
env\ # generated activate scripts
activate.cmd / .ps1 / .sh
%USERPROFILE%\.cache\luban\downloads\ # archive cache
%USERPROFILE%\.local\state\luban\ # installed.json (component registry)
%USERPROFILE%\.config\luban\ # selection.json (which components to install)
See XDG-first directory layout for the full spec.
Uninstall
luban env --unset-user :: remove from HKCU PATH/env
rmdir /S /Q %LOCALAPPDATA%\luban :: ~250 MB of toolchains gone
rmdir /S /Q %USERPROFILE%\.cache\luban
rmdir /S /Q %USERPROFILE%\.local\state\luban
rmdir /S /Q %USERPROFILE%\.config\luban
del luban.exe :: wherever you put it
Luban never writes outside these directories. There is no Windows Registry persistence beyond HKCU PATH/env (cleaned up by luban env --unset-user).
Quickstart
From a fresh Windows 11 machine to a hello-world C++ project linked against a vcpkg library, in five commands.
Prerequisites
- Windows 10/11 x64
luban.exesomewhere reachable
The five commands
luban setup :: installs the toolchain (~250 MB, ~3 minutes)
luban env --user :: registers tools on user PATH (one-time)
:: --- open a fresh terminal here so PATH picks up changes ---
luban new app hello :: scaffolds + auto-builds; clangd ready
cd hello
luban add fmt :: edits vcpkg.json + luban.cmake
luban build :: cmake auto-fetches fmt via vcpkg, builds
build\default\src\hello\hello.exe
That's it. You should see:
hello from hello!
Now edit src/hello/main.cpp to use fmt:
#include <fmt/core.h>
#include <fmt/color.h>
int main() {
fmt::print(fg(fmt::color::cyan), "hello from luban via vcpkg-installed fmt!\n");
return 0;
}
Build again:
luban build
build\default\src\hello\hello.exe
Open in your editor
Open the project in Neovim or VS Code. Both auto-detect:
compile_commands.json(in project root afterluban build) β clangd reads it directlyclangd.exeis on PATH β no LSP config needed
If clangd is correctly attached, you should see autocomplete on fmt:: and jump-to-definition into the vcpkg-installed fmt headers.
What just happened
hello/
βββ CMakeLists.txt β user-owned, 4 lines: project + include + register_targets
βββ luban.cmake β luban-generated; find_package(fmt) + target_link_libraries
βββ vcpkg.json β {"name":"hello", "version":"0.1.0", "dependencies":["fmt"]}
βββ vcpkg-configuration.json β baseline pinned to a specific vcpkg commit
βββ CMakePresets.json β Ninja generator + vcpkg toolchain file (when VCPKG_ROOT set)
βββ compile_commands.json β copied from build/ for clangd
βββ .gitignore
βββ .clang-format
βββ .clang-tidy
βββ .vscode/
βββ src/
βββ hello/
βββ CMakeLists.txt β user-owned, 2 lines
βββ main.cpp
You never opened CMakeLists.txt. You never wrote a find_package call. You never read the vcpkg manifest mode docs. Luban did all of that.
Where to next
- The Daily Driver Loop β what
lubanuse looks like every day - Workflows β Multi-target project β when one binary is not enough
- Commands β
luban addβ version constraints, feature selection - Reference β
luban.cmakeβ what luban actually generates and why
The Daily Driver Loop
Once you've done the one-time setup (luban setup + luban env --user), here's what a typical week with luban looks like.
Monday: new project
luban new app weekly-experiment
cd weekly-experiment
nvim src/weekly-experiment/main.cpp # clangd autocomplete works immediately
luban new already ran luban build once at the end, so compile_commands.json is on disk and clangd will find it without any LSP config in your editor.
Tuesday: needed a JSON parser
luban add nlohmann-json
luban build # vcpkg fetches + caches it (~30s first time)
luban add edited vcpkg.json and regenerated luban.cmake. You did not open CMakeLists.txt. You did not look up the cmake target name (it's nlohmann_json::nlohmann_json, but luban's curated mapping handles that).
Wednesday: split out a library
The main.cpp is getting unwieldy. Move parsing logic into its own static lib:
luban target add lib parser
Now you have src/parser/{parser.h, parser.cpp, CMakeLists.txt}. Edit those three files. Then in src/weekly-experiment/CMakeLists.txt add one line:
target_link_libraries(weekly-experiment PRIVATE parser)
luban build again β both targets compile, the exe links the lib.
Thursday: tests
luban add catch2
luban target add exe test-parser
Wire test-parser to depend on parser and Catch2::Catch2WithMain. Standard cmake from there.
Friday: clone the project on a colleague's machine
The colleague has cmake + ninja + vcpkg, but no Luban:
git clone <your-repo>
cd weekly-experiment
cmake --preset default
cmake --build --preset default
Works. luban.cmake is in git, so cmake includes it; vcpkg fetches the same deps at the same baseline (locked in vcpkg-configuration.json). The colleague never knew Luban existed.
What changed every day
| Day | Files Luban touched | Files you touched |
|---|---|---|
| Mon | vcpkg.json luban.cmake (initial) | none |
| Tue | vcpkg.json luban.cmake | main.cpp |
| Wed | luban.cmake (LUBAN_TARGETS) + new src/parser/* | main.cpp, src/weekly-experiment/CMakeLists.txt (+1 line), parser.{h,cpp} |
| Thu | vcpkg.json luban.cmake + new src/test-parser/* | various |
| Fri | (none β just running cmake) | (none β just running cmake) |
You opened the root CMakeLists.txt zero times. You wrote zero find_package calls. You read the vcpkg manifest mode docs zero times.
When to escape Luban
Luban is best at the 80% case. When you need anything below, just write it directly:
- Custom compile flags per file: write them in
src/<target>/CMakeLists.txt, afterluban_apply(). - Custom find rules for a non-vcpkg dep: standard
find_package(MyLib)afterinclude(luban.cmake)βluban.cmakedoesn't fight you. - Unusual generators (Make, VS, Xcode): edit
CMakePresets.json(it's user-owned). - Header-only library target: in
src/<lib>/CMakeLists.txt, changeadd_library(<name> STATIC ...)toadd_library(<name> INTERFACE)and adjust accordingly. - External dep from somewhere not vcpkg: skip
luban add, write your ownfind_package/ FetchContent.
The escape hatch is: just delete the include(luban.cmake) line from CMakeLists.txt and you have a plain cmake project again.
Commands overview
Luban has 16 commands, organized into four groups.
Toolchain & environment (one-time per machine)
| Command | What it does |
|---|---|
luban setup | Install LLVM-MinGW + cmake + ninja + mingit + vcpkg into <data>/toolchains/ |
luban env | Show env state; rewrite activate scripts; register HKCU PATH (rustup-style) |
Per-project (run inside a project dir)
| Command | What it does |
|---|---|
| [`luban new app | lib |
luban build | cmake --preset && cmake --build; sync compile_commands.json |
| [`luban target add | remove`](./target.md) |
Dependency management (vcpkg.json + luban.cmake)
| Command | What it does |
|---|---|
luban add <pkg>[@version] | Edit vcpkg.json + regenerate luban.cmake (find_package + link auto-wired) |
luban remove <pkg> | Reverse luban add |
luban sync | Re-read vcpkg.json + luban.toml, regenerate luban.cmake |
luban search <pattern> | Search vcpkg ports (wraps vcpkg search) |
Advanced / diagnostic
| Command | What it does |
|---|---|
luban doctor | Report directories, installed components, tools on PATH |
luban run <cmd> [args...] | uv-style activate + exec; runs cmd with luban env |
luban which <alias> | Print absolute exe path that an alias resolves to |
luban describe [--json] | Dump system + project state for IDEs / scripts |
luban shim | Regenerate <data>/bin/ shims (text + .exe; repair tool) |
luban self {update,uninstall} | Self-update binary, or uninstall luban completely |
Global flags
These work before any subcommand:
| Flag | Effect |
|---|---|
-V, --version | Print luban X.Y.Z and exit |
-h, --help | Print top-level help |
-v, --verbose | Verbose log output (including stack traces on internal errors) |
Conventions
- Idempotent: every command can be re-run safely.
luban setupskips already-installed components,luban addreplaces existing dep,luban target addrefuses duplicate names. - Atomic file writes: every config/manifest write goes via
tmp + rename, so a crash leaves either the old or new file fully intact, never half-written. - Idiomatic exit codes:
0success,1runtime failure (download failed, cmake error),2user error (invalid args, refused operation). - Logs to stderr: useful info (
β,β,!,βprefixed lines) goes to stderr. stdout is for machine-readable output (e.g.,compile_commands.jsonpaths). You can pipe stdout cleanly.
luban setup
Install all enabled toolchain components. Run this once per machine.
Synopsis
luban setup [--only <name>[,<name>...]] [--force] [--dry-run] [--refresh-buckets]
Default behavior
Reads <config>/luban/selection.json (or seeds it from manifests_seed/selection.json on first run), installs every component with enabled: true. Idempotent β re-running is fast unless you pass --force.
The default selection installs:
| Component | Source | Approx. size |
|---|---|---|
| llvm-mingw | mstorsjo/llvm-mingw release | ~180 MB |
| cmake | Kitware/CMake release | ~50 MB |
| ninja | ninja-build/ninja release | ~300 KB |
| mingit | git-for-windows/git release | ~40 MB |
| vcpkg | microsoft/vcpkg release + bootstrap-vcpkg.bat | ~10 MB + (vcpkg.exe ~7 MB after bootstrap) |
Total: ~280 MB on disk after extraction (sum of ~50 MB compressed downloads in <cache>/luban/downloads).
Flags
--only <name>[,<name>...]
Install only the named component(s). Comma-separated list. Overrides the enabled flag in selection.json β useful for installing components disabled by default (like vcpkg which has its own overlay).
luban setup --only vcpkg
luban setup --only cmake,ninja
--force
Reinstall the component even if already present at the same version. Re-downloads if cache is stale, re-extracts, rewrites shims. Good for recovering after a corrupted toolchain dir.
--dry-run
Show what would be installed without doing it. Walks the selection, verifies manifests can be fetched + parsed, prints the URL each component would download. No network downloads, no extracts, no registry writes.
--refresh-buckets (experimental)
Force re-fetch of Scoop bucket mirrors. Normally luban fetches manifests on demand and caches them; this flag invalidates the cache.
Per-component pipeline
For each enabled component, luban runs:
- Resolve manifest β overlay β bucket cache β bucket remote (raw.githubusercontent)
- Validate manifest β reject if
installer,pre_install,post_install,uninstaller,persist, orpsmodulefields are present (these would require running PowerShell) - Download the archive to
<cache>/luban/downloads/, with retries + sha256 verification - Extract to a staging dir under
<data>/toolchains/.tmp-<name>-<ver>/ - Apply
extract_dirβ descend into the wrapper directory if the archive uses one - Promote staging β
<data>/toolchains/<name>-<ver>-<arch>/(atomic rename, copy fallback for cross-volume) - Special bootstrap for vcpkg: run
bootstrap-vcpkg.batonce after extract to fetch matchingvcpkg.exefrom microsoft/vcpkg-tool releases - Write shims β
.cmd,.ps1, extensionless sh, one set perbinalias, into<data>/bin/ - Update registry β
<state>/luban/installed.json
What it does NOT touch
HKCU\Environment(useluban env --userfor that, separately)HKLM(anything system-wide; never)- The
~/scoop/directory if you have Scoop installed (we read Scoop manifests, never write the Scoop layout) - Existing user PATH (until you
luban env --user)
Common workflows
Fresh machine
luban setup # ~3 min on a fast connection
luban env --user # one-time HKCU PATH registration
Add vcpkg later
If the default selection doesn't include vcpkg (depends on which seed you ship):
luban setup --only vcpkg
luban env --user # picks up VCPKG_ROOT for new shells
Reinstall a corrupted toolchain
luban setup --only ninja --force
Verify what's about to happen
luban setup --dry-run
Failure modes
- Network:
luban setupretries 3Γ with exponential backoff. Persistent failure β exits 1 with the error from the last attempt. - Hash mismatch: download is discarded, no install. Suggests upstream tampering or a corrupted CDN edge.
- Unsafe manifest: aborts with a clear message; user must provide an overlay manifest in
<data>/registry/overlay/<name>.json. - Extract failure: staging dir is wiped, the partial extract is gone, registry untouched. Safe to retry.
The pipeline is designed so a Ctrl-C or system crash never leaves a half-installed component visible to luban β either the install completed and installed.json records it, or it didn't.
See also
luban envβ the rustup-style PATH registrationluban doctorβ verify what was installed- Reference β Manifest overlay format β how to add components luban doesn't ship by default
luban env
Show or modify environment integration. With no flags, prints status.
Synopsis
luban env [--apply] [--user | --unset-user]
What each flag does
| Flag | Effect |
|---|---|
| (none) | Print env state: <data> location, activate-script paths, what's on PATH |
--apply | Rewrite <data>/env/activate.{cmd,ps1,sh} from the current registry |
--user | Register on HKCU PATH (rustup-style) + set LUBAN_* and VCPKG_ROOT user env vars |
--unset-user | Reverse --user β remove <data>/bin/ from PATH and unset the env vars |
Run luban env --help for examples.
When to use each mode
- One-time after
luban setup:luban env --userso all new shells see cmake/clang/clangd/vcpkg directly. - Already use activate scripts: don't run
--user; sourcingactivate.{cmd,ps1,sh}from<data>/env/works equivalently per-shell. - CI / container: skip
--userentirely; setPATHandVCPKG_ROOTfrom the env scripts or directly.
Behavior details
--user is idempotent: re-running it doesn't duplicate PATH entries. The check is case-insensitive normalized path.
The PATH change is broadcast via WM_SETTINGCHANGE, so File Explorer and newly-spawned processes see it immediately. Already-running shells do NOT see the change β that's a Windows constraint.
luban doctor
Print luban's view of the world. Use this when something seems off.
What it shows
β Canonical homes
β data C:\Users\you\.local\share\luban
β cache ...
β state ...
β config ...
β Sub-directories
β store ...
β toolchains ...
β bin ...
...
β Installed components
β cmake 4.3.2
β llvm-mingw 20260421
...
β Tools on PATH
β clang C:\Users\you\.local\share\luban\bin\clang.cmd
β cmake ...
Β· vcpkg (not found) β if vcpkg not yet installed or PATH not set
When to run it
- After
luban setup, before doing anything else - When a new tool you expect to be there (e.g.,
clang-tidy) fails to launch - When migrating to a new machine β confirms your
<data>/<cache>/<state>/<config>resolution is sane
Reading the output
- β green check + path = installed and resolvable
- Β· grey dot +
(not found)= not installed, or installed but not on this shell's PATH - β red cross = something's actively wrong (e.g.,
installed.jsonis corrupt)
If a tool shows (not found) but installed.json lists it: open a fresh shell after luban env --user, or call <data>\env\activate.cmd.
luban new
Scaffold a new C++ project.
Synopsis
luban new {app|lib} <name> [--at <dir>] [--no-build]
What you get
<name>/
βββ CMakeLists.txt # 4 lines, user-owned
βββ luban.cmake # luban-managed; GIT-TRACKED
βββ vcpkg.json # {"name":"<name>","version":"0.1.0","dependencies":[]}
βββ vcpkg-configuration.json # baseline pinned to specific vcpkg commit
βββ CMakePresets.json # Ninja, with vcpkg toolchain when VCPKG_ROOT set
βββ compile_commands.json # generated by initial build, used by clangd
βββ .gitignore # build/, vcpkg_installed/, .cache/, .vs/
βββ .clang-format
βββ .clang-tidy
βββ .vscode/
βββ src/
βββ <name>/
βββ CMakeLists.txt # 2 lines: add_executable + luban_apply
βββ main.cpp # std::println("hello from <name>!");
What luban new runs
- Validates the project name (lowercase, digits,
-,_; must start with a letter) - Copies the template tree to
<at>/<name>/, expanding{{name}}placeholders in both file names and contents - Auto-runs
luban buildonce unless--no-buildis passed. This producescompile_commands.jsonso clangd works the moment you open the project in Neovim or VS Code.
Flags
| Flag | Effect |
|---|---|
--at <dir> | Parent directory for the new project (default: cwd) |
--no-build | Skip the initial build (faster scaffold; clangd will be unhappy until you luban build manually) |
After luban new
cd <name>
nvim src/<name>/main.cpp # clangd attached, autocomplete works
For lib: same scaffold for now (we don't differentiate lib vs app extensively yet); luban target add lib mylib is the proper way to add a library target to an existing project.
luban build
Wraps cmake --preset && cmake --build and copies compile_commands.json to the project root.
Synopsis
luban build [--preset <name>] [--at <dir>]
Preset auto-selection
If --preset is not given (or --preset auto), luban picks based on the project's vcpkg.json:
| Project state | Picked preset |
|---|---|
vcpkg.json has dependencies | default (uses VCPKG_ROOT for vcpkg toolchain) |
vcpkg.json is empty / missing | no-vcpkg (no toolchain file, faster, no VCPKG_ROOT needed) |
Override with --preset release etc.
What it does, in order
- Configure: run
cmake --preset <P>in the project dir. vcpkg manifest mode kicks in if the toolchain file is set. - Build: run
cmake --build --preset <P>. Ninja parallelizes across cores. - Sync: copy
build/<P>/compile_commands.jsonto project root, so clangd / VS Code C/C++ extension auto-find it. - Report: print
β build complete(or the cmake error verbatim).
The cmake invocation gets <data>/toolchains/*/bin prepended to PATH (so cmake/ninja/clang are visible) plus LUBAN_* env vars set. The current process's PATH is preserved underneath, so vcpkg's child processes can still find mingit / system tools.
Common scenarios
| Goal | Command |
|---|---|
| Just build (auto preset) | luban build |
| Force release | luban build --preset release |
| Build a subproject | luban build --at ../other-project |
| Skip cmake configure (incremental rebuild) | Just run cmake --build --preset default directly |
luban add
Add a vcpkg library to the current project. Edits vcpkg.json + regenerates luban.cmake (find_package + target_link_libraries auto-wired).
Synopsis
luban add <pkg>[@<version>]
<pkg> is a vcpkg port name. <version> is optional; when given, becomes a version>= constraint in vcpkg.json.
Examples
luban add fmt # β vcpkg.json: ["fmt"]
luban add spdlog@1.13 # β version>=: 1.13.0
luban add boost-asio # Boost.Asio (Boost::asio target)
luban add catch2 # link target: Catch2::Catch2WithMain
What luban add fmt actually does
Before:
// vcpkg.json
{ "name": "myapp", "version": "0.1.0", "dependencies": [] }
After:
// vcpkg.json
{
"name": "myapp",
"version": "0.1.0",
"dependencies": ["fmt"]
}
And luban.cmake is regenerated to include:
find_package(fmt CONFIG REQUIRED)
function(luban_apply target)
target_compile_features(${target} PRIVATE cxx_std_23)
if(NOT MSVC)
target_compile_options(${target} PRIVATE -Wall -Wextra)
target_link_options(${target} PRIVATE -static -static-libgcc -static-libstdc++)
endif()
target_link_libraries(${target} PRIVATE
fmt::fmt
)
endfunction()
The find_package(fmt ...) line is unconditional. The target_link_libraries(... fmt::fmt) line is added inside luban_apply() so every target that calls luban_apply() automatically links fmt.
Version constraints
| Spec | vcpkg.json result |
|---|---|
luban add fmt | "fmt" (string form, baseline version) |
luban add fmt@10 | {"name":"fmt","version>=":"10.0.0"} |
luban add fmt@10.2 | {"name":"fmt","version>=":"10.2.0"} |
luban add fmt@10.2.1 | {"name":"fmt","version>=":"10.2.1"} |
Version padding to 3 segments is automatic β vcpkg expects X.Y.Z for version>=. Suffixes (e.g., 1.0.0-alpha) pass through unchanged.
To pin an exact version (not just a lower bound), edit vcpkg.json manually after luban add:
{
"dependencies": [{"name": "fmt", "version>=": "10.2.0"}],
"overrides": [{"name": "fmt", "version": "10.2.0"}]
}
Then run luban sync to regenerate luban.cmake (no functional change but confirms the schema is parsed).
System tools are rejected
$ luban add cmake
β 'cmake' is a system-level toolchain, not a vcpkg library.
Β· System tools live in <data>/toolchains/. Use:
Β· luban setup --only cmake
This is enforced for: cmake, ninja, clang, clang++, clangd, clang-format, clang-tidy, lld, llvm-mingw, mingit, git, vcpkg, make, gcc. See Two-tier dependency model for why.
When the cmake target name is wrong
luban add uses a curated vcpkg port β cmake target mapping (~50 popular libs). For a library not in the table, luban falls back to find_package(<port> CONFIG REQUIRED) and does not auto-link it. You'll need to:
- Look up the cmake target name in vcpkg's usage file (after first
luban buildthey're invcpkg_installed/<triplet>/share/<pkg>/usage) - Add
target_link_libraries(<your-target> PRIVATE <Mod>::<target>)in yoursrc/<target>/CMakeLists.txt
Future luban will auto-discover from usage files; in M2.5 it's manual fallback.
Does it run vcpkg install?
No. luban add only edits vcpkg.json. The actual fetching + building of fmt happens later, when cmake runs (because CMakePresets.json points CMAKE_TOOLCHAIN_FILE at vcpkg's, and vcpkg in manifest mode auto-installs deps during cmake configure).
So the sequence is:
luban add fmt # fast: just file edits
luban build # slow first time: cmake β vcpkg fetches + builds fmt β links
luban build # subsequent: vcpkg cache hit, just compile your code
Future flag idea: luban add fmt --install for "fetch right now". Not in v1.
See also
luban removeβ symmetric reverseluban syncβ regenerateluban.cmakewithout changing deps- Reference β
luban.cmakeβ what gets generated and why - vcpkg manifest mode docs
luban remove
Reverse of luban add. Removes a vcpkg library from vcpkg.json and regenerates luban.cmake.
luban remove <pkg>
vcpkg_installed/ is not cleaned up β vcpkg's binary cache may still hold the lib for other projects. To force a clean rebuild without the lib: rm -rf build/ vcpkg_installed/.
luban sync
Re-read vcpkg.json + luban.toml in the current project, regenerate luban.cmake.
luban sync
When you'd use it
- Just pulled a project from git, want to make sure
luban.cmakeis up to date with the in-treevcpkg.json - Hand-edited
vcpkg.json(e.g., to add"features"or"overrides"thatluban adddoesn't expose) - Changed
luban.toml [scaffold] warnings = "strict"and want it reflected luban.cmakegot accidentally deleted
What it changes
Only luban.cmake. vcpkg.json and luban.toml are read-only inputs.
luban search
Search vcpkg ports.
luban search <pattern>
Wraps vcpkg search <pattern>. Requires luban setup --only vcpkg first.
Examples
luban search fmt
:: fmt 12.1.0 {fmt} is an open-source formatting library...
:: log4cxx[fmt] Include log4cxx::FMTLayout class...
:: spdlog[fmt] Use fmt library
:: ...
luban search json
:: nlohmann-json 3.12.0 JSON for Modern C++
:: rapidjson 1.1.0 A fast JSON parser/generator for C++...
:: simdjson 3.10.0 Fast JSON parser
luban search boost-asio
:: port name match for Boost.Asio
The first column is the port name β pass that to luban add.
Notes
- Search matches port name + description substring (vcpkg's behavior, not fuzzy)
- For features (e.g.,
boost[asio]), the bracket syntax is informational only here; you actuallyluban add boost-asio(separate port name) - Output may include a "results may be outdated" warning from vcpkg β that's
vcpkg's voice. To refresh,
cd $VCPKG_ROOT && git pull(or runluban setup --force --only vcpkgto fully re-sync)
luban target
Add or remove a build target (library or executable) within the current project.
Synopsis
luban target add {lib|exe} <name>
luban target remove <name>
What target add lib mylib creates
src/mylib/
βββ CMakeLists.txt # add_library(mylib STATIC mylib.cpp)
β # target_include_directories(mylib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
β # luban_apply(mylib)
βββ mylib.h # namespace mylib { int hello(); }
βββ mylib.cpp # int mylib::hello() { return 42; }
LUBAN_TARGETS in luban.cmake gets appended with mylib. The root CMakeLists.txt's luban_register_targets() call automatically picks it up.
What target add exe bench creates
src/bench/
βββ CMakeLists.txt # add_executable(bench main.cpp) + luban_apply(bench)
βββ main.cpp # std::println("hello from bench!");
Linking targets together
luban does NOT abstract target_link_libraries. To link bench against mylib, edit src/bench/CMakeLists.txt and add one line:
target_link_libraries(bench PRIVATE mylib)
Then #include "mylib.h" in bench/main.cpp works β mylib's target_include_directories(... PUBLIC) makes its headers visible to dependents.
target remove
luban target remove <name>
Unregisters from LUBAN_TARGETS (so cmake stops add_subdirectory-ing it) but leaves src/<name>/ in place. Delete that directory manually if you really want it gone.
This is deliberate: luban target remove is non-destructive by design.
luban run
uv-style transparent activate-and-exec. Run any luban-managed exe with the
toolchain env injected, no call activate.cmd needed.
Synopsis
luban run <cmd> [args...]
All args after <cmd> are forwarded to the child process verbatim β
luban does not parse them. So luban run cmake --version sends --version
to cmake (not luban).
How <cmd> resolves
In order:
- Registry alias β looked up in
<state>/installed.json.cmakeβ<data>/toolchains/cmake-X/bin/cmake.exe. This bypasses the.cmdshim layer (one less cmd.exe wrapper). - Augmented PATH search β if no registry match, search luban's
toolchain bin dirs prepended to your current PATH, with PATHEXT
(
.exe β .cmd β .bat β no-ext). Useful for tools not in the registry (e.g.,where,tasklist).
If neither finds it, exits 127 with a helpful hint.
Environment injection
While the child runs, these env vars are set:
PATH=<luban toolchain dirs>+ your existing PATHLUBAN_DATA/LUBAN_CACHE/LUBAN_STATE/LUBAN_CONFIG- (
VCPKG_ROOTis not auto-injected byrunβ set it vialuban env --useronce for permanence)
Examples
luban run cmake --version
:: cmake version 4.3.2
:: ...
luban run clang -E -dM -x c nul
:: dump preprocessor macros that clang sees by default
luban run vcpkg search fmt
:: equivalent to `luban search fmt`
luban run clang-format -i src\foo.cpp
:: format a single file via Luban's clang-format
When you don't need luban run
luban env --useralready on this machine β cmake / clang / clangd are on user PATH; no need forluban runfor those.- Project-local
luban buildβ directly wraps cmake without needingluban run.
luban run is for one-off "run X with luban env" without committing to PATH
registration, or for tools you'd rather not put on PATH.
Vs alternatives
| Tool | Equivalent |
|---|---|
| uv | uv run <cmd> |
| cargo | (no direct equivalent β cargo run is project-build) |
| nix | nix-shell -p X --run "X args" |
| direnv | (sourcing on cwd entry; passive vs explicit) |
luban which
Print the absolute exe path for a luban-managed alias.
luban which <alias>
Resolves the alias via <state>/installed.json and prints to stdout. Useful
for debugging "which version of cmake am I running?" or for scripts.
Examples
luban which cmake
:: C:\Users\you\.local\share\luban\toolchains\cmake-4.3.2-x86_64\bin\cmake.exe
luban which clangd
:: C:\Users\you\.local\share\luban\toolchains\llvm-mingw-20260421-x86_64\bin\clangd.exe
luban which vcpkg
:: C:\Users\you\.local\share\luban\toolchains\vcpkg-2026.03.18-x86_64\vcpkg.exe
stdout has just the path (good for piping). Diagnostic info (which component provides this alias) goes to stderr.
Multi-component aliases
If multiple components provide the same alias (rare; e.g., if you have both mingit and a separate git overlay), the first match in installed.json wins, with a stderr warning listing all candidates.
Vs other tools
where cmake(Windows cmd) β searches PATHEXT in current PATH; finds the shim (cmake.cmdorcmake.exe), not the underlying real binarywhich cmake(Git Bash) β same as above, just Unix-styleluban which cmakeβ finds the real target exe via the registry, bypassing all shim layers
Use the right one for your need.
luban describe
Dump system + (optional) project state. Default human-readable; --json for
machines.
Synopsis
luban describe [--json]
Default output (text)
Luban 0.1.0
ββ Paths ββ
bin C:\Users\you\.local\share\luban\bin
cache C:\Users\you\.cache\luban
config C:\Users\you\.config\luban
data C:\Users\you\.local\share\luban
...
ββ Installed components (5) ββ
cmake 4.3.2 β C:\Users\you\.local\share\luban\toolchains\cmake-4.3.2-x86_64
5 alias(es), source: scoop-main
llvm-mingw 20260421 β ...
270 alias(es), source: overlay
...
ββ Project: foo 0.1.0 ββ (only when run inside a luban project)
root: C:\Users\you\projects\foo
deps (2): fmt spdlog@>=1.13
luban.toml: cpp=23 triplet=x64-mingw-static warnings=normal sanitizers=[]
builds: default(β) release(β)
The "Project" section only appears when run from inside a directory tree
containing luban.cmake or vcpkg.json.
--json output (machine-readable)
{
"luban_version": "0.1.0",
"paths": { "bin": "...", "cache": "...", ... },
"installed_components": [
{
"name": "cmake",
"version": "4.3.2",
"source": "scoop-main",
"url": "https://...",
"hash": "sha256:...",
"toolchain_dir": "C:\\Users\\...\\cmake-4.3.2-x86_64",
"architecture": "x86_64",
"installed_at": "2026-04-25T...",
"bins": [
{
"alias": "cmake",
"relative_path": "bin/cmake.exe",
"absolute_path": "C:\\Users\\...\\cmake.exe"
},
...
]
},
...
],
"project": { // when in a project
"root": "...",
"name": "foo",
"version": "0.1.0",
"dependencies": [
{ "name": "fmt" },
{ "name": "spdlog", "version_ge": "1.13.0" }
],
"luban_toml": { "cpp": 23, "triplet": "...", "warnings": "...", ... },
"builds": [
{ "preset": "default", "dir": "...", "compile_commands": true }
],
"compile_commands_root": "..."
}
}
Schema is stable for v0.x. Changes will be flagged in release notes.
Use cases
- IDE plugin backend β Neovim/VS Code/CLion plugins can shell out to
luban describe --jsonto discover toolchain paths, deps, and build state in one call. - Visualization β pipe to D3.js / vis.js to render a system+project graph in a browser (no need to compile luban to WASM; the JSON is the API).
- Debugging β "is fmt actually in this project's deps? what version does
vcpkg.json say?" β
luban describe --json | jq .project.dependencies - Scripting β
luban describe --json | jq -r '.installed_components[].name'to enumerate components in shell scripts.
Examples
luban describe :: human-readable
luban describe --json :: full JSON dump
luban describe --json | jq .paths :: just paths
luban describe --json | jq '.installed_components[] | {name, version}'
:: extract just name + version from each component
luban shim
Repair tool. Walks <state>/luban/installed.json and rewrites every shim file under <data>/luban/bin/.
luban shim
When you'd run it
<data>/bin/got accidentally deleted- A toolchain dir was moved or re-bootstrapped (e.g., vcpkg.exe re-downloaded)
- A new luban version changed shim format and you want to upgrade existing installs
What gets written
For each bins entry in each component record:
<data>/bin/<alias>.cmd # @echo off + "<exe>" %*
<data>/bin/<alias>.ps1 # PowerShell forward
<data>/bin/<alias> # POSIX sh-script (works in Git Bash, future Linux)
Plus mode-bits: the sh-script is chmod +x on POSIX, harmless on Windows.
Notes
- M3 will replace
.cmdshims with real.exeproxies (rustup-style). That'll let cmake's compiler probe accept the shim as a "real" cmake/clang. - Until then, cmake's compiler-detect uses absolute paths (we point CMakePresets at the toolchain bin directly), and the
.cmdshims are for interactive shell use.
luban self
Manage the luban binary itself.
luban self update
luban self uninstall [--yes] [--keep-data]
luban self update
Pull the latest release from github.com/Coh1e/luban, verify SHA256, atomically swap the running luban.exe.
What it does
- GET
https://api.github.com/repos/Coh1e/luban/releases/latest - Compare
tag_name(e.g.v0.1.2) against the running version - If equal β exit "already up to date"
- If newer:
- Download
luban.exetoluban.exe.new(with retry + stream-SHA) - Download
SHA256SUMS, parse, verify the new exe matches - Move running
luban.exeβluban.exe.old - Move
luban.exe.newβluban.exe - Schedule
luban.exe.oldfor delete-on-reboot viaMoveFileExW(... MOVEFILE_DELAY_UNTIL_REBOOT)
- Download
- Exit; next invocation runs the new binary
The previous binary is kept as luban.exe.old (auto-cleaned at reboot). If the swap fails partway, the original is restored.
Behavior on errors
- 4xx HTTP β fail immediately, no retry (URL-issue)
- 5xx / network β 3 retries with exponential backoff
- SHA256 mismatch β refuse swap, delete
.newfile, exit 1 - Move failures β restore original; exit 1
luban self uninstall --yes
Reverse every footprint of luban on this machine.
β οΈ Destructive. Refuses without
--yes. Print a plan, then exit 1.
What it removes
| Step | Effect |
|---|---|
| 1 | HKCU\Environment PATH entry pointing at <data>/bin/ |
| 2 | HKCU\Environment\LUBAN_DATA/LUBAN_CACHE/LUBAN_STATE/LUBAN_CONFIG |
| 3 | HKCU\Environment\VCPKG_ROOT |
| 4 | <data> (~250 MB toolchains + content store) |
| 5 | <cache> (download archives + vcpkg binary cache) |
| 6 | <state> (installed.json + logs) |
| 7 | <config> (selection.json) |
| 8 | luban.exe + luban-shim.exe (via deferred batch script) |
Self-delete on Windows
Running .exe files are file-locked by Windows. We can't del them while running. Pattern used (rustup-style):
- Write a temp
luban-uninstall.bat:@echo off ping 127.0.0.1 -n 2 >nul :: ~1.5s delay del /F /Q "<luban.exe>" :: by then we've exited; lock released del /F /Q "<luban-shim.exe>" del /F /Q "%~f0" :: batch self-deletes - Spawn the batch via
CreateProcessWwithDETACHED_PROCESS - Exit luban.exe immediately
- After ~1.5s, batch wakes up + deletes everything
You'll briefly see no luban.exe on disk; this is intentional.
Flags
| Flag | Effect |
|---|---|
--yes | Required to actually run; without it, prints plan + exits 1 |
--keep-data | Preserve <data> etc. (toolchains stay on disk); only undo HKCU env injection + delete the binaries |
Use cases
- Full uninstall:
luban self uninstall --yesβ leaves no trace - "Reset" without re-downloading toolchains:
luban self uninstall --yes --keep-data, then re-download luban.exe + runluban setup(idempotent β re-uses existing<data>/toolchains/) - Migrate to a different luban version: prefer
luban self updateover uninstall + reinstall
Why no separate luban-init.exe
rustup ships rustup-init.exe because rustup itself is a shell-script-bootstrapped Rust toolchain β there's no chicken-and-egg "first download a single binary that runs". luban already IS that single binary: download luban.exe from Releases, put it where you want, run luban setup. luban self update/uninstall round-trip the entire lifecycle within one binary, uv-style.
First-time machine setup
A complete walkthrough from a fresh Windows install to a working C++ project.
Assumptions
- Windows 10 (1809+) or Windows 11, x64
- A user account with normal (non-admin) rights
- Network access
You do not need: Visual Studio, MSYS2, Chocolatey, Scoop, Python, Git, or anything else pre-installed.
Step 1 β Get luban.exe
Any of:
- Download from GitHub Releases and put it in
%USERPROFILE%\bin\luban.exe(or anywhere) git clone+ build from source if you already have a C++ toolchain (see Installation)
Open a fresh Command Prompt or PowerShell. Verify:
where luban
:: should print full path; if not, you need to use full path or add to PATH temporarily
Step 2 β Install the toolchain
luban setup
Expected output (truncated):
β Ensuring canonical directories exist
β directories ready
β Deploying seed manifests + selection
β overlays on disk: 3
β Installing 5 component(s)
β β llvm-mingw
β download llvm-mingw-20260421-ucrt-x86_64.zip
[########################] 100.0% 178.4 MiB/178.4 MiB ...
β extract llvm-mingw-20260421-ucrt-x86_64.zip
β installed llvm-mingw 20260421 β C:\Users\you\.local\share\luban\toolchains\llvm-mingw-20260421-x86_64
β β cmake
...
β β ninja
...
β β mingit
...
β β vcpkg
β extract 2026.03.18.zip
β bootstrapping vcpkg.exe (microsoft/vcpkg-tool)
β vcpkg.exe ready
β installed vcpkg 2026.03.18 β ...
β setup complete
Time: 3β5 minutes on a typical home connection. Most of it is downloading the LLVM-MinGW archive (~180 MB compressed).
Step 3 β Register on PATH
luban env --user
Output:
β added C:\Users\you\.local\share\luban\bin to HKCU PATH
β set HKCU LUBAN_DATA = C:\Users\you\.local\share\luban
β set HKCU LUBAN_CACHE = C:\Users\you\.cache\luban
β set HKCU LUBAN_STATE = C:\Users\you\.local\state\luban
β set HKCU LUBAN_CONFIG = C:\Users\you\.config\luban
β set HKCU VCPKG_ROOT = C:\Users\you\.local\share\luban\toolchains\vcpkg-2026.03.18-x86_64
Β· open a new shell for the changes to take effect.
β οΈ Critical: Close the current cmd/PowerShell window. Open a fresh one. The PATH change affects new processes, not the one running luban.
Step 4 β Verify
In your fresh shell:
cmake --version
:: cmake version 4.3.2
clang --version
:: clang version 22.1.4 ...
ninja --version
:: 1.13.2
clangd --version
:: clangd version 22.1.4 ...
vcpkg --version
:: vcpkg package management program version 2026-03-04-...
If any of these fail with "not recognized as a command":
- Confirm you opened a new terminal (the change doesn't propagate to running shells)
luban doctorβ should show all components β and tools β on PATHecho %PATH%β<data>\luban\binshould be near the front
Step 5 β Hello, world
mkdir %USERPROFILE%\projects
cd %USERPROFILE%\projects
luban new app hello
:: β Scaffolding app 'hello' at C:\Users\you\projects\hello
:: β wrote 12 files
:: β running initial `luban build` to produce compile_commands.json
:: ...
:: β build complete
:: β next: cd hello && nvim src/hello/main.cpp (clangd ready out of the box)
cd hello
build\default\src\hello\hello.exe
:: hello from hello!
You're up. Open src\hello\main.cpp in your favorite editor; clangd should attach automatically and give autocomplete.
Where to next
- Add a vcpkg library β your second project will probably want one
- The Daily Driver Loop β what week 2 looks like
- IDE integration β Neovim, VS Code, JetBrains
Add a vcpkg library
Need fmt, spdlog, boost-asio, or any of the ~2200 vcpkg ports?
TL;DR
luban add fmt # edits vcpkg.json + luban.cmake
luban build # cmake fetches + builds fmt via vcpkg manifest mode
Step-by-step
-
In your project directory, run
luban add <port>. Replace<port>with the vcpkg port name (e.g.,fmt,nlohmann-json,boost-asio). -
Use the library in your
src/<target>/main.cpp:#include <fmt/core.h> int main() { fmt::print("hello, {}\n", "fmt"); } -
Build:
luban buildThe first build triggers vcpkg's manifest-mode install. It's slow on first add (vcpkg builds the lib from source) but cached afterward. Subsequent builds reuse the same
vcpkg_installed/and just compile your code.
Version constraints
luban add fmt # any version (uses baseline)
luban add fmt@10 # version >= 10.0.0
luban add fmt@10.2.1 # version >= 10.2.1
To pin an exact version, edit vcpkg.json manually after luban add to add an "overrides": [...] entry. See reference/vcpkg-json.md.
Boost (and other multi-port libs)
Boost in vcpkg is split per-component. Add only what you need:
luban add boost-asio
luban add boost-beast
luban add boost-program-options
Each adds the correct Boost::<sublib> cmake target via Luban's curated mapping.
When the cmake target name is wrong
Luban's table covers ~50 popular libs. For uncovered libs, Luban writes find_package(<port>) but doesn't auto-link. After first luban build, find the target name in vcpkg_installed/<triplet>/share/<pkg>/usage, then:
# in src/<your-target>/CMakeLists.txt
target_link_libraries(<your-target> PRIVATE <ModuleName>::<target>)
Removing a dep
luban remove fmt
vcpkg_installed/ is not auto-cleaned (vcpkg manages its own cache). Delete build/ and vcpkg_installed/ for a clean state.
See also
luban addβ full reference- vcpkg manifest mode
- Reference β
vcpkg.json
Multi-target project
A real C++ project usually has more than one binary: an exe + a lib it links against, or an exe + tests.
TL;DR
luban target add lib mylib # creates src/mylib/{.h,.cpp,CMakeLists.txt}
# Then in src/<your-exe>/CMakeLists.txt manually add:
# target_link_libraries(<your-exe> PRIVATE mylib)
luban build
Worked example
Start with a fresh project:
luban new app calc
cd calc
Now you have src/calc/main.cpp. Suppose you want to put your math logic in a separate library so you can test it.
luban target add lib mathlib
Result:
calc/
βββ CMakeLists.txt # untouched
βββ luban.cmake # LUBAN_TARGETS now: "calc;mathlib"
βββ src/
βββ calc/
β βββ CMakeLists.txt
β βββ main.cpp
βββ mathlib/ β NEW
βββ CMakeLists.txt
βββ mathlib.h β namespace mathlib { int hello(); }
βββ mathlib.cpp
Edit src/calc/CMakeLists.txt β add one line:
add_executable(calc main.cpp)
luban_apply(calc)
target_link_libraries(calc PRIVATE mathlib) # β add this
Edit src/calc/main.cpp to use it:
#include <print>
#include "mathlib.h"
int main() {
std::println("hello from calc, mathlib::hello() == {}", mathlib::hello());
return 0;
}
Build + run:
luban build
build\default\src\calc\calc.exe
:: hello from calc, mathlib::hello() == 42
Why one extra line?
Luban deliberately does NOT auto-link new libs to existing exes. There's no way to know which exe should depend on which lib without ambiguity (testing, plugins, etc.). cmake's target_link_libraries is the right place to express this β it's standard cmake, every C++ developer can read it.
What luban DOES handle:
- creating the dir + skeleton files
- registering the target in
LUBAN_TARGETS - making
target_include_directories(... PUBLIC)work so#include "mathlib.h"resolves
More targets
luban target add lib utils # second lib
luban target add exe bench # second exe (e.g., for benchmarks)
Each gets its own src/<name>/ dir. Inter-link as needed via target_link_libraries.
Removing
luban target remove utils # unregisters; src/utils/ left in place
rm -rf src/utils # if you really want it gone
See also
luban targetβ full reference- Architecture β Why no IR β why luban doesn't abstract
target_link_libraries
IDE integration
Luban-managed projects work in any editor that speaks LSP, with zero configuration. The contract is:
clangdis on PATH (provided byluban env --user)compile_commands.jsonis at the project root (produced byluban build)- cmake is on PATH (so
cmake --presetworks for any "configure-via-cmake-tools" extensions)
If those three are true, the IDE attaches clangd, finds the right toolchain, and gives you autocomplete + diagnostics + jump-to-definition out of the box.
Neovim
If you use nvim-lspconfig:
require('lspconfig').clangd.setup{}
That's it. clangd will find compile_commands.json automatically (project root + a few common subdirs).
VS Code / Cursor
Install clangd extension. Done.
The clangd extension auto-detects compile_commands.json. The bundled C/C++ extension by Microsoft is not needed for clangd workflow (and conflicts with it; disable for this workspace).
For cmake-tools experience (preset selector, build/run buttons), also install CMake Tools extension β it picks up CMakePresets.json automatically.
CLion / RustRover / other JetBrains
CLion auto-detects cmake projects via CMakePresets.json. Just open the project root.
What if it doesn't work?
Things to check:
luban env --userwas run in a previous shell? Open a fresh terminal and re-checkclangd --version.luban buildwas run at least once?compile_commands.jsononly exists after a build.luban newruns a build automatically; if you passed--no-build, runluban buildmanually.compile_commands.jsonis in the project root? Should be a copy ofbuild/<preset>/compile_commands.json. If not,luban buildre-syncs it.- Right clangd version?
which clangdshould point at<data>/luban/bin/clangd.cmd(or, after future M3,clangd.exe). Stale system clangd may shadow ours.
Why it works without config
clangd reads compile_commands.json (a list of {file, command, directory} JSON objects produced by cmake). Each entry has the full compile command, including -I, -D, -std=..., and the actual clang++.exe absolute path. clangd uses that compile command verbatim β no guessing.
Because the path to clang++.exe is absolute (luban-managed toolchain), clangd auto-discovers the matching system headers / libc++ via the compiler's resource directory. You never need to set --query-driver or compile_flags.txt.
Why "compile_commands.json at project root" matters
clangd searches a fixed list of locations for compile_commands.json:
<project>/compile_commands.json<project>/build/compile_commands.json<project>/build/<some-preset>/compile_commands.json
luban build always copies to (1), the most reliable location. This is plain cmake convention; any IDE with clangd integration knows about it.
Reproducing a luban project on another machine
A Luban project committed to git can be rebuilt anywhere by anyone β even on machines that don't have luban installed. This is a property of the design (see Architecture β Design philosophy Β§3), not an afterthought.
Three scenarios
A. Target machine has luban
git clone <repo>
cd <repo>
luban build # vcpkg fetches deps via manifest mode at the pinned baseline
Same fmt version, same toolchain, same binary semantics. Done.
B. Target machine has cmake + vcpkg, but no luban
git clone <repo>
cd <repo>
set VCPKG_ROOT=C:\path\to\vcpkg
cmake --preset default
cmake --build --preset default
Works. cmake includes the (committed) luban.cmake, vcpkg honors the (committed) vcpkg-configuration.json baseline. Same outputs.
This is why luban.cmake is git-tracked β it makes luban an optional convenience, not a build-time dependency.
C. Bare machine
:: install luban (one binary, one URL):
curl -O https://luban.coh1e.com/luban.exe
luban setup
luban env --user
:: open fresh shell
git clone <repo>
cd <repo>
luban build
What's pinned, what isn't
| Concern | Pinned by | Lockfile-equivalent |
|---|---|---|
| Toolchain (cmake, ninja, clang) | luban.exe version on the target machine | luban release version |
| vcpkg ports baseline | vcpkg-configuration.json baseline field (commit SHA of microsoft/vcpkg) | yes |
| Per-port version | optional dependencies[].version>= + overrides[] | yes |
| Source code | git | yes |
Generated cmake (luban.cmake) | git | yes (it's just text, regenerable but committed) |
Best practices
-
Don't
.gitignoreluban.cmakeβ it's the bridge that makes "no-luban" reproducibility possible. -
Pin a vcpkg baseline.
luban newdoes this for you (uses the latest at scaffold time). Bump it intentionally withluban sync --baseline-now(M3) or by hand-editingvcpkg-configuration.json. -
Document the toolchain. A
README.mdline like "Built with luban X.Y.Z (LLVM-MinGW 22, cmake 4.3+)" helps future-you and contributors. -
Keep
vcpkg.jsonminimal. Add only what you need. Eachluban addshould correspond to a real#includein your code β vcpkg builds everything in the manifest, so unused deps are time tax on every clean build.
Cross-platform reproducibility (future)
Today luban runs Windows-only. When the Linux/macOS port lands, the same vcpkg manifest will resolve different triplets (x64-linux, arm64-osx etc.). Project-level vcpkg.json doesn't change; only VCPKG_TARGET_TRIPLET differs in CMakePresets.json.
XDG-first directory layout
Luban respects the XDG Base Directory Specification, even on Windows. This makes container/CI setups trivial and lets a future Linux port "just work."
The four canonical homes
Every Luban file lives under one of four roles:
| Role | Purpose |
|---|---|
data | Long-lived state. Toolchain installs, the shim dir, content store, registry mirrors. |
cache | Recreatable artifacts. Downloaded archives, vcpkg binary cache. Safe to delete. |
state | Mutable runtime state. installed.json, .last_sync, logs. |
config | User preferences. selection.json, future config.toml. |
Resolution order (per role)
For each role, in this exact order, the first existing answer wins:
$LUBAN_PREFIX/<role>ifLUBAN_PREFIXis set. Useful for containers / CI / multi-tenant setups.$XDG_<ROLE>_HOME/lubanif the relevant XDG variable is set. (XDG_DATA_HOME,XDG_CACHE_HOME,XDG_STATE_HOME,XDG_CONFIG_HOME)- macOS default (
~/Library/...) if running on macOS - Windows fallback (
%LOCALAPPDATA%\lubanetc.) if running on Windows - Linux default (
~/.local/share/lubanetc.) β the spec's fallback
Note: on Linux the 3rd and 4th rows don't apply, so it's straight to the Linux default. On Windows,
XDG_*env vars take precedence over%LOCALAPPDATA%.
Default paths per platform
| Role | Linux default | Windows fallback | macOS default |
|---|---|---|---|
| data | ~/.local/share/luban | %LOCALAPPDATA%\luban | ~/Library/Application Support/luban |
| cache | ~/.cache/luban | %LOCALAPPDATA%\luban\Cache | ~/Library/Caches/luban |
| state | ~/.local/state/luban | %LOCALAPPDATA%\luban\State | ~/Library/Application Support/luban/State |
| config | ~/.config/luban | %APPDATA%\luban (roaming) | ~/Library/Preferences/luban |
Sub-locations
Within each role, Luban creates a fixed set of sub-directories on first run:
<data>/
toolchains/<name>-<ver>-<arch>/ # one dir per installed component
bin/ # rustup-style shim dir (added to user PATH)
env/activate.{cmd,ps1,sh} # generated activate scripts + default.env.json
registry/buckets/<bucket>/bucket/*.json # cached Scoop bucket manifests
registry/overlay/*.json # user-supplied overlay manifests
store/sha256/<sha>/blob # content-addressable file store (M2.5+: hardlink dedup)
<cache>/
downloads/ # archive downloads (zip etc.)
vcpkg-binary/ # vcpkg binary cache (when configured)
<state>/
installed.json # component registry
.last_sync # bucket sync timestamp
logs/ # future: install/build logs
<config>/
selection.json # which components luban setup installs
config.toml # future: user preferences
Run luban doctor to see the resolved paths on your machine.
Why XDG even on Windows
Three reasons:
- Container/CI portability β a single
LUBAN_PREFIX=/tmp/lubanenv var redirects everything for headless builds, with no Windows API quirks. - No-surprise Linux port β when Linux support lands, the same paths work; users moving cross-platform don't need to relearn anything.
- Cleanly separated concerns β
data(long-lived),cache(rm-safe),state(mutable),config(user-edited) are different categories with different backup semantics. Lumping into a single%LOCALAPPDATA%\lubanwould hide that.
Overrides for power users
:: redirect everything (good for containers, ephemeral CI)
set LUBAN_PREFIX=C:\luban-isolated
:: redirect just data dir (e.g., put toolchains on a different drive)
set XDG_DATA_HOME=D:\luban-cache\share
:: same for the others
set XDG_CACHE_HOME=...
set XDG_STATE_HOME=...
set XDG_CONFIG_HOME=...
After setting any of these, re-run luban doctor to confirm Luban sees the override.
See also
luban doctorβ print resolved paths and verify everything exists- Architecture β Design philosophy Β§4 (XDG rationale)
luban.toml schema
Project-local preferences for luban. Optional β the file does not need to exist; defaults apply.
Schema (v1)
[project]
default_preset = "default" # luban build picks this if --preset not given
triplet = "x64-mingw-static" # vcpkg target triplet
cpp = 23 # C++ standard (passed to luban_apply via cxx_std_23)
[scaffold]
warnings = "strict" # off | normal | strict
sanitizers = ["address", "ub"] # passed to -fsanitize=...
| Field | Type | Default | Effect |
|---|---|---|---|
[project] default_preset | string | "default" | which preset luban build picks if --preset not given (and not auto) |
[project] triplet | string | "x64-mingw-static" | vcpkg target triplet for manifest-mode installs |
[project] cpp | int | 23 | C++ language standard, passed to cmake cxx_std_<N> |
[scaffold] warnings | string | "normal" | off / normal / strict (more flags at higher levels) |
[scaffold] sanitizers | array | [] | comma-joined and prefixed with -fsanitize= for both compile and link |
What warnings controls
Generated in luban.cmake via target_compile_options(... PRIVATE ...):
| Level | Flags emitted |
|---|---|
off | (none) |
normal | -Wall -Wextra |
strict | -Wall -Wextra -Wpedantic -Werror=return-type |
MSVC is detected and these flags are skipped (MSVC doesn't grok GCC-style -W flags).
Regenerating after editing
luban sync # rewrites luban.cmake from current vcpkg.json + luban.toml
luban add and luban remove also implicitly regenerate.
Future fields (M3+)
[toolchain]β pin per-project toolchain versions (rust-toolchain.toml-equivalent)[scripts]β npm-style command aliases (luban run <script>)[workspace]β multi-package projects (cargo workspace-equivalent)
These are NOT in v1 β adding them would expand the file's role beyond "preferences" toward "manifest", which the design explicitly rejects.
vcpkg.json & vcpkg-configuration.json
Two files vcpkg uses for manifest mode. luban-managed projects always use manifest mode.
vcpkg.json β the manifest
This is vcpkg's schema, not luban's. luban only edits the dependencies array (and name / version on luban new).
{
"name": "myapp",
"version": "0.1.0",
"dependencies": [
"fmt", // baseline version
{"name": "spdlog", "version>=": "1.13.0"}, // version constraint
"nlohmann-json"
],
"overrides": [ // optional: pin exact versions
{"name": "fmt", "version": "10.2.1"}
],
"features": { // optional: opt-in optional feature deps
"tests": {
"description": "build with tests",
"dependencies": ["catch2"]
}
}
}
luban add writes dependencies. Other sections β overrides, features, supports, builtin-baseline, dependencies[].features β are preserved as-is when luban writes the file (extras are copied through unchanged).
See vcpkg's manifest reference for full schema.
vcpkg-configuration.json β the baseline pin
Locks the exact commit of microsoft/vcpkg ports tree that defines version-to-port-revision mappings.
{
"default-registry": {
"kind": "git",
"repository": "https://github.com/microsoft/vcpkg",
"baseline": "c3867e714dd3a51c272826eea77267876517ed99"
}
}
baseline is a 40-char git commit SHA, not a tag. luban new writes this with the most-recently-known stable vcpkg commit. Bump it intentionally:
:: M3 will provide: luban sync --baseline-now
:: Until then, hand-edit vcpkg-configuration.json:
git -C %VCPKG_ROOT% rev-parse HEAD # get current ports tree commit
:: paste that into baseline field
luban sync # noop semantically, just confirms parse
Why a baseline matters: without it, vcpkg uses HEAD which moves under you, breaking reproducibility (reproduce workflow).
What luban does NOT touch
vcpkg-configuration.jsonafterluban newβ this is yoursvcpkg.json[overrides],vcpkg.json[features]β preserved exactlyvcpkg-cache/orvcpkg_installed/β those are vcpkg's working dirs, both.gitignored
luban.cmake generated module
The cmake module luban generates and include()s into your project. Git-tracked β committed alongside source, so non-luban machines can build the project too.
Anatomy
# generated by luban β DO NOT EDIT BY HAND
# regenerate via: luban add / remove / sync / target add / target remove
# project deps come from vcpkg.json; flags come from luban.toml ([scaffold] section).
# ---- targets (LUBAN_TARGETS) ----
set(LUBAN_TARGETS calc mathlib)
# ---- find_package (auto from vcpkg.json dependencies) ----
find_package(fmt CONFIG REQUIRED)
find_package(spdlog CONFIG REQUIRED)
# ---- luban_apply: call once per target after add_executable / add_library ----
function(luban_apply target)
target_compile_features(${target} PRIVATE cxx_std_23)
if(NOT MSVC)
target_compile_options(${target} PRIVATE -Wall -Wextra)
# static-link toolchain runtime so the exe runs without toolchain DLLs on PATH.
target_link_options(${target} PRIVATE -static -static-libgcc -static-libstdc++)
endif()
target_link_libraries(${target} PRIVATE
fmt::fmt
spdlog::spdlog
)
endfunction()
# ---- luban_register_targets: call once from root CMakeLists.txt ----
function(luban_register_targets)
foreach(t IN LISTS LUBAN_TARGETS)
if(EXISTS ${CMAKE_SOURCE_DIR}/src/${t}/CMakeLists.txt)
add_subdirectory(src/${t})
else()
message(WARNING "luban: target '${t}' has no src/${t}/CMakeLists.txt; skipping")
endif()
endforeach()
endfunction()
Three concerns, three sections
| Section | Source | What it controls |
|---|---|---|
LUBAN_TARGETS | luban target add/remove | Which subdirs luban_register_targets() walks |
find_package(...) | vcpkg.json dependencies | Which vcpkg libs are imported |
luban_apply(target) body | luban.toml [scaffold] + vcpkg.json deps | Flags + library links applied per target |
Inputs
vcpkg.json β dependencies (drives find_package + target_link_libraries)
luban.toml β cpp, warnings, sanitizers (drive compile_features + compile_options)
luban.cmake itself β LUBAN_TARGETS list (read back via parse so target add/remove are atomic)
How users interact
The user's root CMakeLists.txt is just:
cmake_minimum_required(VERSION 3.25)
project(foo CXX)
include(${CMAKE_SOURCE_DIR}/luban.cmake)
luban_register_targets()
Each src/<target>/CMakeLists.txt does:
add_executable(foo main.cpp)
luban_apply(foo)
# user can add target_link_libraries / target_compile_options here
That's the full surface area. Two function calls per project.
When luban.cmake is regenerated
luban add <pkg>/luban remove <pkg>β find_package + link list updatedluban syncβ full regen from vcpkg.json + luban.tomlluban target add/luban target removeβ LUBAN_TARGETS updated, rest preserved if vcpkg.json/luban.toml unchanged
The whole file is rewritten each time (no diff/merge); user-edits to luban.cmake will be lost. Don't edit it by hand. The header comment says this; the user-facing contract is: write your custom cmake outside this file.
Why this shape
See Architecture β Why no IR for the full rationale.
Manifest overlay format
Place a JSON file at <data>/luban/registry/overlay/<name>.json (or <repo>/manifests_seed/<name>.json for ones that ship with luban). It will take priority over the corresponding Scoop bucket manifest.
Use overlays when:
- the upstream Scoop manifest uses
installer.script/pre_install/post_install/uninstaller/persist/psmodule(luban refuses these for safety) - you want a different upstream URL
- you want to lock to a specific version
Schema (subset of Scoop manifest)
{
"_comment": "Free-form. Documentation/audit only.",
"version": "2026.03.18",
"url": "https://example.com/path/to/release.zip",
"hash": "sha256:528ff8708702e296b5744d9168c3fb4343c015fa024cd3770ede8ac94d9971b9",
"extract_dir": "subdir-name-inside-the-archive",
"extract_to": "subdir-of-toolchain-dir",
"bin": [
["relative/path/to/exe.exe", "alias"],
["relative/path/to/another.exe", "another-alias", "prefix arg1 arg2"]
],
"env_set": {
"TOOL_HOME": "$dir"
},
"env_add_path": ["bin", "lib/runtime"],
"depends": ["mingit"]
}
| Field | Type | Notes |
|---|---|---|
version | string | required |
url | string | string[1] | required, plain HTTPS download |
hash | string | string[1] | required; sha256:<hex> (or just <hex>) |
extract_dir | string | optional; descend into this subdir of the extracted archive |
extract_to | string | optional; place archive contents into this subdir of the toolchain dir |
bin | array | optional; entries are string or [rel,alias] or [rel,alias,"prefix args"] |
env_set | object | optional; future env-injection (M3) |
env_add_path | array | optional; subdirs to add to PATH (auto-shimmed at install time) |
depends | array | optional; install order hint (M3) |
Refused fields (safety)
These are always rejected, even in overlay manifests:
installer(e.g.,installer.script)pre_install/post_installuninstallerpersistpsmodule
If a manifest has any of these, luban errors out at parse time and refuses to install. Overlays are the only way to get past a Scoop manifest that needs these fields β by stripping them and providing a plain url+hash alternative.
Special-case: vcpkg
The vcpkg overlay (manifests_seed/vcpkg.json) ships in luban itself. After extract, luban runs bootstrap-vcpkg.bat once to fetch the matching vcpkg.exe from microsoft/vcpkg-tool releases. This is luban's decision (hardcoded for name == "vcpkg"), not a manifest-driven script execution β the manifest itself is plain url+hash.
Example: a simple "just download a zip" overlay
{
"version": "1.0.0",
"url": "https://example.com/mytool-1.0.0.zip",
"hash": "sha256:abc...",
"bin": [["mytool.exe", "mytool"]]
}
Drop at <data>/luban/registry/overlay/mytool.json, then:
luban setup --only mytool
Environment variables
Variables luban reads or writes.
Variables luban reads
| Variable | Effect |
|---|---|
LUBAN_PREFIX | If set, all four roles resolve to $LUBAN_PREFIX/<role>. Highest priority. |
XDG_DATA_HOME | Override <data> to $XDG_DATA_HOME/luban. |
XDG_CACHE_HOME | Override <cache> to $XDG_CACHE_HOME/luban. |
XDG_STATE_HOME | Override <state> to $XDG_STATE_HOME/luban. |
XDG_CONFIG_HOME | Override <config> to $XDG_CONFIG_HOME/luban. |
LOCALAPPDATA | Windows fallback for data/cache/state. |
APPDATA | Windows fallback for config (roaming). |
USERPROFILE / HOMEDRIVE+HOMEPATH / HOME | Used to find the user's home dir. |
NO_COLOR | If set (any value), disables ANSI colors in luban's output. |
LUBAN_NO_PROGRESS | If set, disables download progress bar (CI-friendly). |
PATH | Read by luban env --user to deduplicate before adding <data>/bin. |
Variables luban writes (with luban env --user)
These are written to HKCU\Environment (current user, persistent).
| Variable | Value |
|---|---|
PATH | Prepends <data>/luban/bin (deduped, idempotent) |
LUBAN_DATA | <data> resolved at the time of luban env --user |
LUBAN_CACHE | <cache> |
LUBAN_STATE | <state> |
LUBAN_CONFIG | <config> |
VCPKG_ROOT | Path to currently-installed vcpkg toolchain dir (empty if vcpkg not installed yet) |
luban env --unset-user removes all of the above.
Variables consumed by tools, not luban itself
VCPKG_ROOTis read by vcpkg's cmake integration, NOT by luban directly. We just set it.CMAKE_TOOLCHAIN_FILEreferenced inCMakePresets.jsonuses$env{VCPKG_ROOT}to find vcpkg's toolchain file at cmake configure time.
Generated activate scripts
<data>/luban/env/activate.{cmd,ps1,sh} set the same variables (PATH + LUBAN_*) for the current shell session. Useful when:
- You can't or don't want to use
luban env --user(CI, ephemeral containers) - You need a per-shell setup (e.g., test multiple luban prefixes via
LUBAN_PREFIX)
Source as appropriate:
:: cmd
call %LOCALAPPDATA%\luban\env\activate.cmd
:: pwsh
. $env:LOCALAPPDATA\luban\env\activate.ps1
:: bash / WSL
source ~/.local/share/luban/env/activate.sh
Design summary
A single page covering the question: why does luban look the way it does?
If you read only one architecture page, read this. The other architecture
pages (philosophy, two-tier-deps, why-cmake-module, roadmap) zoom
into individual topics β this is the synthesis.
What problem luban solves
Modern C++ on Windows requires the user to assemble:
- A toolchain (LLVM-MinGW or MSVC + MinGW or mingw-w64)
- cmake β project meta-build
- ninja β actual builder
- clangd β LSP for editors
- vcpkg β package manager
- git β for vcpkg + general use
- Some glue:
compile_commands.json,find_package,target_link_libraries, triplets,CMakePresets,CMAKE_TOOLCHAIN_FILE,vcpkg-configuration.jsonbaseline pinning,target_include_directories(... PUBLIC), etc.
Each of these is half a day of yak-shaving. Stack-up usually takes days, and resets on every new machine.
luban's job: turn that into two one-time commands plus a tiny per-project loop:
luban setup :: γ3 min, ~250 MB toolchains
luban env --user :: register on HKCU PATH (rustup-style)
:: open fresh shell
luban new app foo
cd foo
luban add fmt :: edits vcpkg.json + luban.cmake
luban build :: vcpkg auto-fetches fmt during configure
User never opens CMakeLists.txt, never writes a find_package, never
reads vcpkg manifest mode docs.
What luban is NOT
- Not a build system. cmake + ninja still do the building.
- Not a package manager. vcpkg still resolves and builds C++ packages.
- Not a fork or replacement. A luban-managed project remains a standard
cmake project β clone it onto a luban-free machine,
cmake --preset default && cmake --build --preset defaultworks.
luban is glue with opinions: the right glue, glued correctly, with opinionated defaults that keep beginners on the rails.
The 8 architecture invariants
Locked-in design decisions. Breaking any of them invalidates luban's fundamental contract:
1. cmake stays primary
luban does not invent a new IR. luban does not invent a new manifest format
to replace CMakeLists.txt. luban writes a standard cmake module
(luban.cmake) that user code include()s. One line removed and luban is gone.
2. luban.cmake is git-tracked
The whole point of "auxiliary" is that the project must remain buildable
without luban. luban.cmake is a regular cmake module, committed to git,
always reproducible.
3. No new manifest format
Project deps live in vcpkg.json (vcpkg's schema). luban edits it via
luban add. There is no parallel [deps] section in luban.toml β
that would create double truth.
luban.toml exists only as optional project preferences (warning
level, sanitizers, default preset, triplet) β the kinds of things that
don't fit naturally in vcpkg.json.
4. luban.toml is optional
Many luban projects have no luban.toml at all. It only appears when the
user has actual preferences. Defaults work for the 80% case.
5. XDG-first paths, even on Windows
luban respects XDG_DATA_HOME / XDG_CACHE_HOME / XDG_STATE_HOME /
XDG_CONFIG_HOME and the LUBAN_PREFIX umbrella variable, before
falling back to %LOCALAPPDATA% / %APPDATA% defaults.
This makes container/CI/multi-user setups trivial, and makes a future Linux port a no-surprise affair.
6. Zero UAC
Every luban-managed file lives in user-writable directories. luban never
writes Program Files, never touches HKLM, never spawns elevated
installers. luban env --user writes only to HKCU (current user).
7. Single static-linked binary
luban.exe is one file: ~3 MB release / ~32 MB debug, depending on
miniz/json/toml++ vendored single-headers. No DLLs alongside, no Python,
no MSI. Drop on a USB stick, copy to a fresh VM, runs.
8. Two-tier dependency model
| Layer | What | Where | Who |
|---|---|---|---|
| System | cmake, ninja, clang, clangd, lld, git, vcpkg | <data>/toolchains/ | luban setup |
| Project | fmt, spdlog, boost, catch2 | <project>/vcpkg_installed/<triplet>/ | luban add (which edits vcpkg.json) |
Mixing them is rejected. luban add cmake errors out with a hint about
luban setup. System tools are version-locked machine-wide; project libs
live per-project with their own baseline pin.
Why this shape (the alternatives we rejected)
Alternative A: a luban DSL β lower to cmake
xmake / meson / build2 / premake all chose this path. User writes
xmake.lua or meson.build, the tool generates the actual build files.
Why we said no: C++ has decades of cmake idioms. Custom commands, target-specific flags, conditional features β every real project hits cases the DSL doesn't cover and falls through to "you have to know cmake anyway." At that point the DSL is just an extra thing to learn that doesn't hide cmake. Worse, the seam between DSL and cmake is hostile (eject rituals, one-way conversions).
Alternative B: in-place edit of CMakeLists.txt with marker blocks
Like a code generator inserting # >>> BEGIN luban / # <<< END luban
sections. Luban owns those blocks; user owns the rest.
Why we said no: edit-in-place is fragile. User's hand-edits inside the markers get clobbered. Edits near the markers race with regeneration. The boundary is a constant source of confusion.
luban.cmake solves this cleanly: luban owns the file, user include()s
it, the boundary is a hard line.
Alternative C: One unified manifest at the top
luban.toml containing everything β deps, scripts, profiles, workspace,
toolchain pin. Cargo style.
Why we said no: vcpkg's vcpkg.json already exists and is the de facto
C++ manifest. Inventing a parallel [deps] section means we maintain a
double-truth, with constant drift between the two. Better to let vcpkg
own that schema and just edit it via luban add.
Alternative D: A luban-init.exe bootstrapper
rustup-style: a tiny installer that downloads luban + runs setup.
Why we said no: luban.exe is already a single self-contained binary.
Users curl -O luban.exe from GitHub Releases, run it. The bootstrapper
adds a layer with no real value. Instead, luban self update and
luban self uninstall round-trip the lifecycle within one binary,
uv-style.
Trade-offs we accept
- Windows-first. Linux/macOS port is M3+ work; we lose multi-platform hygiene short-term but gain rapid iteration on the platform that needs the most help (Windows C++ tooling).
- Vendored deps only. No
find_package(zlib)-style consumption of system libs. Adds ~10MB to the binary but keeps "one file, runs anywhere" promise. - The
.cmake-module model assumes users willinclude(luban.cmake). If they don't, none of the auto-find_package magic happens β they fall through to plain cmake. This is by design. - Curated pkgβtarget mapping.
luban addknows ~50 popular libs by name. Unknown ports getfind_package(<port>)written but no auto-link; user fills target name in. Long tail handled by future scraping of vcpkg'susagefiles.
Operating model
luban operates as three layers, each with stable interfaces:
ββ user βββββββββββββββββββββββββββββββββββββββββββββββββ
β luban CLI (16 verbs in 4 groups) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β edits vcpkg.json / luban.toml
β runs cmake / vcpkg
βΌ
ββ luban-managed artifacts ββββββββββββββββββββββββββββββ
β luban.cmake β luban writes; git-tracked β
β <data>/bin/ β shim dir; on user PATH β
β <state>/installed.json β component registry β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β shells out to
βΌ
ββ external tools βββββββββββββββββββββββββββββββββββββββ
β cmake / ninja / clang / vcpkg / git β
β (luban downloads + manages them, but doesn't β
β invade their config β they remain "off the shelf") β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
The middle layer is what makes luban "auxiliary": it's a thin, stable, git-trackable spec that bridges user intent to standard tools. Drop the middle layer and you still have working tools at the bottom.
Where to read more
- Philosophy β the "auxiliary, not authoritative" stance expanded
- Two-tier dependency model β system vs project deps in detail
- Why no IR, why luban.cmake β alternative B (marker-block in-place edit) rejection rationale
- Roadmap β what's done, what's next
- Quickstart β the 5-command tour
Design philosophy
Luban is shaped by 6 hard rules that came out of the design discussions. They constrain a lot of seemingly-arbitrary decisions in the code; if you find a piece of luban surprising, the answer is probably here.
1. Auxiliary, not authoritative
"luban is the helper. cmake / vcpkg / ninja are the main characters."
Luban does not invent a new build system. It does not invent a new IR. It does not invent a new manifest format. cmake remains the C++ build de facto, vcpkg remains the package source. Luban writes a luban.cmake module; one include(luban.cmake) line in your CMakeLists.txt brings it in, one line removed and Luban is gone.
Concretely: the moment a user wants to do something Luban doesn't directly support, they should be able to fall through to plain cmake / vcpkg without any "ejection" ritual.
2. No new manifest format
Deps live in vcpkg.json (vcpkg's own schema). luban add fmt edits vcpkg.json directly β there is no [dependencies] table in luban.toml parallel to vcpkg's manifest. One source of truth per concern.
luban.toml is optional and only holds project-level luban preferences: warning level, sanitizers, default preset, triplet. If you have no preferences, luban.toml does not exist.
3. luban.cmake is git-tracked
The whole point of "luban as auxiliary" is that the project must remain buildable on a machine that doesn't have luban installed. luban.cmake is a standard cmake module β committed to git, always reproducible, no luban needed at consumer-end.
This is the opposite of an eject model: the generated artifacts are first-class, not staging.
4. XDG-first paths, even on Windows
Linux's XDG Base Directory spec is the cleanest model the field has produced for "where do tools put their stuff." We respect XDG_DATA_HOME, XDG_CACHE_HOME, XDG_STATE_HOME, XDG_CONFIG_HOME even on Windows. Only when those are absent do we fall back to %LOCALAPPDATA% / %APPDATA% defaults.
Why on Windows too: it makes container/CI/multi-user scenarios trivial (just set LUBAN_PREFIX=/somewhere), and a future Linux port has no surprises.
See reference/paths.md for the full resolution order.
5. Zero UAC
Every Luban-managed file lives in user-writable directories. We never write Program Files, never touch HKLM, never spawn an installer that needs admin. This is the same instinct as rustup, pipx, volta β the user owns their environment.
Concretely: luban env --user writes to HKCU\Environment (current user only). Toolchains land in ~/.local/share/luban/. Nothing requires runas.
6. Single static binary
luban.exe is one file. Static-linked libc++, no DLLs to ship alongside, no Python runtime, no MSI. You can drop it on a USB stick, copy it to a fresh VM, run it on a Windows-on-ARM laptop someday β it just works (assuming Windows 10+).
Concretely: 31 MB binary. ~14 MB of that is luban code; ~10 MB is vendored miniz + nlohmann_json + toml++; ~7 MB is statically-linked libc++ + libwinpthread + libunwind.
What this rules out
- No luban-only DSL for build description (xmake/meson territory)
- No proprietary lockfile format (vcpkg has its own, we use it)
- No global system PATH writes, no admin install
- No Python or other runtime dependency
- No "luban world" you have to live inside β every artifact is something cmake / vcpkg / git can already consume
What this admits
- Light, narrow scope. Luban does toolchain bootstrap + cmake/vcpkg glue + nothing else.
- A
luban.exeupgrade is an.exeswap, no migration needed. - A future Linux port is plausible without rewriting; the philosophy already accommodates it.
Cross-references
- Two-tier dependency model β why system tools and project libs are separate concerns
- Why no IR, why luban.cmake β the technical alternative we considered and rejected
- Roadmap β what's next
Two-tier dependency model
"Luban also needs to do system bin management. cmake doesn't have to be locked inside luban β like Python where I
pip installfor system tools or for project libs." β the conversation that sparked this design
Luban distinguishes two kinds of "dependency" that the user sees, and treats them as categorically different concerns:
| Layer | Examples | Stored in | Scope |
|---|---|---|---|
| System | cmake, ninja, clang, clangd, lld, git, vcpkg itself | <data>/toolchains/<name>-<ver>-<arch>/ | Cross-project, machine-wide (one user) |
| Project | fmt, spdlog, boost, catch2, nlohmann-json | <project>/vcpkg_installed/<triplet>/ | Per-project, in-tree |
This is the same split that mature ecosystems converge on:
| Ecosystem | System tier | Project tier |
|---|---|---|
| Rust | rustup | cargo |
| Python | pipx / uv tool | uv / pip (in venv) |
| Node | volta / nvm | npm / pnpm |
| Go | system go | go.mod |
| C++ via Luban | luban setup (LLVM-MinGW etc.) | luban add (vcpkg deps) |
Hard rules (the constraints we enforce)
-
Toolchain names not allowed in
vcpkg.json.luban add cmakeerrors out and points the user toluban setup. Same for ninja, clang, git, etc. -
Project libs not allowed in
luban setup. If youluban setup --only fmt, it'll just fail to find an overlay manifest (and Scoop'sfmtmanifest, if any, is ports-via-installer-script which we refuse). -
One source of truth per layer. System tier is
<state>/installed.json. Project tier isvcpkg.json. Never duplicated, never conflated. -
System tier upgrades are global. All projects on this machine see the same
cmakeversion. (Future: per-project pin vialuban.toml [toolchain], but not in v1.) -
Project tier installs land per-project (
vcpkg_installed/<triplet>/). They are.gitignored. Their reproducibility comes from the manifest+baseline, not the install.
Why two layers and not one
It's tempting to ask: why doesn't luban add cmake just work? After all, cmake is a dep of the project too.
The answer is failure-mode separation:
- Toolchain breakage: rare, big consequence. A bad cmake version blocks ALL your projects. You want this rare, deliberate, version-controlled.
- Library breakage: common, small consequence. A bad
fmtupgrade affects one project's link. You want this fast, throwaway.
Mixing them means a routine luban add could silently bump cmake. That's the kind of design that turns into a 3 AM incident.
What luban does NOT have (yet)
These would round out the model but aren't in v1:
- Per-project toolchain pin:
luban.toml [toolchain] cmake = "4.3.2". Today the system tier is global. - Workspaces: a single
luban.toml [workspace] members = ["foo", "bar"]describing multiple sibling projects sharing build cache + vcpkg cache. Cargo-style. - Global tool install:
luban tool install <pkg>(pipx-equivalent) for command-line C++ tools (e.g., a static analyzer). Today only toolchains are system-level.
These are open M3+ territory.
Why no IR, why luban.cmake
This page documents an alternative design path we did not take and why.
The temptation: a luban IR
The first instinct when wrapping cmake + vcpkg + ninja is: "let's invent a unified frontend." User writes luban.toml, luban lowers it to cmake/vcpkg/ninja config files. xmake, meson, build2, premake all chose this path.
Sketch:
# luban.toml as primary manifest (REJECTED)
[package]
name = "myapp"
cpp = "23"
[build]
kind = "exe"
sources = ["src/**.cpp"]
[deps]
fmt = "10"
spdlog = "1.13"
β luban generates CMakeLists.txt + vcpkg.json + CMakePresets.json from this.
Why we rejected it
1. The user has to learn cmake anyway
C++ has decades of accumulated knowledge in cmake idioms. Custom commands, target_compile_options for one file, conditional features, link_options for memory sanitizer β every real-world project hits cases where the IR doesn't have a direct equivalent and the user has to know cmake to escape.
2. The escape hatch is hostile
If luban.toml is the source of truth and we generate CMakeLists.txt, then user edits to the generated cmake either:
- get clobbered on next regen (frustrating), or
- require a complex "eject" ritual (create-react-app-style, painful).
Either way the seam is sharp.
3. Re-implementing cmake is a tax we don't owe
vcpkg's manifest mode + cmake's preset system have been evolving for years. Inventing parallel concepts in luban means tracking their evolution forever β for marginal user value.
What we did instead: the include() model
luban.cmake is a standard cmake module. The user's CMakeLists.txt does:
cmake_minimum_required(VERSION 3.25)
project(foo CXX)
include(${CMAKE_SOURCE_DIR}/luban.cmake)
luban_register_targets()
# Below this line: standard cmake. Luban does not touch.
luban.cmake exposes two functions:
luban_apply(target)β call once per target afteradd_executable/add_library. Sets cpp std, warnings, links vcpkg deps.luban_register_targets()β call once from root. Doesadd_subdirectory(src/<name>)for every entry inLUBAN_TARGETS.
That's it. Two functions, minimal API surface, no DSL.
Properties of this design
| Property | How it works |
|---|---|
User can edit CMakeLists.txt freely | Luban only writes luban.cmake; the CMakeLists.txt is theirs from day 1 (after luban new). |
| Project builds without luban | cmake --preset default works on any machine with cmake + vcpkg. luban.cmake is just a regular include. |
| User can opt out at any time | Delete the include(luban.cmake) and luban_register_targets() lines. The project becomes plain cmake. |
| Luban can update its codegen without breaking your project | We rewrite luban.cmake on each luban add/sync, but never touch the user's CMakeLists.txt after luban new. |
| Per-target customization is plain cmake | User writes target_compile_options(foo PRIVATE -O3) in src/foo/CMakeLists.txt after luban_apply(foo). No Luban DSL needed. |
What luban_apply() actually does
For a project with vcpkg.json declaring ["fmt", "spdlog"] and no luban.toml:
function(luban_apply target)
target_compile_features(${target} PRIVATE cxx_std_23)
if(NOT MSVC)
target_compile_options(${target} PRIVATE -Wall -Wextra)
target_link_options(${target} PRIVATE -static -static-libgcc -static-libstdc++)
endif()
target_link_libraries(${target} PRIVATE
fmt::fmt
spdlog::spdlog
)
endfunction()
Three concerns: language standard, warning policy, vcpkg deps. All three are derived from vcpkg.json + luban.toml (with sensible defaults). All three are things you'd write in cmake by hand anyway.
What luban does NOT generate
CMakeLists.txt(root and per-target β user-owned afterluban new)CMakePresets.json(user-owned afterluban new)vcpkg.json(vcpkg's manifest β luban only edits viaadd/remove)- Source files
- Test files
All four are written exactly once by luban new, and after that they belong to the user.
Roadmap
This is the rough order in which features land. Anything past M2.5 is open territory.
β M1 β User-facing commands (done)
C++ ports of the Python bootstrap commands: doctor, env, new, build, shim. Single static luban.exe.
β M2 β Setup pipeline ported (done)
luban setup end-to-end in C++:
- WinHTTP downloads, BCrypt SHA verification, miniz ZIP extract
- Scoop manifest parsing (with safety whitelist)
- Component install: download β verify β extract β shim β registry
- Bucket sync (per-manifest fetch from raw.githubusercontent)
- vcpkg overlay manifest +
bootstrap-vcpkg.batintegration
Python bootstrap retired.
β M2.5 β cmake/vcpkg auxiliary frontend (done)
The "user-facing" half of luban's value:
luban add/luban remove/luban sync(vcpkg.json + luban.cmake)luban target add/luban target remove(multi-target)luban.cmakegenerator (find_package, luban_apply, luban_register_targets)luban.tomlschema v1 ([project], [scaffold])- Curated pkg β cmake target mapping (~50 popular libraries)
- Scaffold improvements: subdir-from-day-1, vcpkg-configuration.json baseline
luban env --userextended to set HKCU env vars (LUBAN_*, VCPKG_ROOT)
π M3 β Daily-driver polish
| Feature | Notes |
|---|---|
luban search <pattern> | Wraps vcpkg search; adds caching |
luban which <alias> | Show absolute exe path for a registry alias |
luban describe --json | Machine-readable project + system state for IDE plugins |
luban run <cmd> [args...] | uv-style transparent activation + exec |
Real .exe shim | rustup-style native exe proxies, replacing .cmd shims |
luban-init.exe mode | Standalone bootstrapper (~5 MB) β alternative entry to luban setup |
luban toolchain {list,use,install} | Multi-version toolchain management |
π M4+ β Beyond the immediate need
| Theme | Examples |
|---|---|
| Workspace support | luban.toml [workspace] members = [...], shared build cache |
| Toolchain pin per project | rust-toolchain.toml equivalent |
| Linux/macOS port | proc.cpp POSIX, download.cpp libcurl, setup.cpp cross-platform |
| TUI / interactive mode | luban setup -i, FTXUI; first-run wizard |
Visualization (/luban describe --view) | dump JSON, open static HTML page with D3.js graph |
luban tool install <pkg> | pipx-equivalent for global C++ CLI tools |
Out of scope (for now)
- Replacing cmake or vcpkg with anything custom
- Cross-machine sync, "luban cloud", telemetry
- IDE plugins maintained by us (we generate compile_commands.json + standard cmake; let editors own integration)
- Building luban itself with anything other than LLVM-MinGW (MSVC support might come if asked, but not a priority)
Contributing
Building luban from source
git clone https://github.com/Coh1e/luban.git
cd luban
:: requires LLVM-MinGW + cmake + ninja already on PATH
:: easiest: install luban first, then `luban env --user`, then build:
cmake --preset default
cmake --build --preset default
:: build/default/luban.exe
Repo layout
src/ # luban's C++23 source
cli.{hpp,cpp} # subcommand dispatch, help renderer
paths.{hpp,cpp} # XDG-first path resolution (Win32 SHGetKnownFolderPath fallback)
registry.{hpp,cpp} # installed.json schema (1:1 with Python predecessor)
scoop_manifest.{hpp,cpp} # Scoop manifest parser with safety whitelist
vcpkg_manifest.{hpp,cpp} # vcpkg.json reader/writer
luban_toml.{hpp,cpp} # luban.toml schema v1
luban_cmake_gen.{hpp,cpp}# luban.cmake generator
lib_targets.{hpp,cpp} # curated vcpkg port β cmake target mapping
download.{hpp,cpp} # WinHTTP wrapper with retry + progress
hash.{hpp,cpp} # BCrypt SHA256/SHA512 file hashing
archive.{hpp,cpp} # miniz ZIP extract with path-traversal guard
env_snapshot.{hpp,cpp} # PATH + LUBAN_* compute
shim.{hpp,cpp} # .cmd / .ps1 / sh shim writer
proc.{hpp,cpp} # CreateProcessW wrapper
win_path.{hpp,cpp} # HKCU PATH + env var registry ops
bucket_sync.{hpp,cpp} # Scoop manifest fetcher
selection.{hpp,cpp} # selection.json reader + seed deployment
component.{hpp,cpp} # full install pipeline
log.{hpp,cpp} # ANSI logger
commands/ # one cpp per CLI verb
doctor.cpp env.cpp setup.cpp shim_cmd.cpp
new_project.cpp build_project.cpp
add.cpp target_cmd.cpp
util/win.hpp # utf8 β wstring conversions
third_party/ # vendored single-header libs
json.hpp # nlohmann/json, MIT
miniz.{h,c} # Rich Geldreich, BSD-3
toml.hpp # marzer/tomlplusplus, MIT
*.LICENSE
manifests_seed/ # default selection + overlay manifests
selection.json
llvm-mingw.json mingit.json vcpkg.json
templates/ # `luban new` scaffolding
app/
docs/ # this site (mdBook + Doxygen)
src/ # mdBook source
Doxyfile # Doxygen config
doxygen-awesome*.{css,js}# vendored modern theme
book.toml # mdBook config
.github/workflows/ # CI: docs build + (future) release
Coding conventions
- C++23,
clang++from LLVM-MinGW.-Wall -Wextra -Wpedantic. - Static link in release builds (
-static -static-libgcc -static-libstdc++). - Single binary: every dep is vendored single-header in
third_party/. No CMakefind_packagecalls in luban's own build. - Win32 native: prefer
CreateProcessW/WinHttpSendRequest/BCryptHashData/RegSetValueExWover libc abstractions, where stable behavior matters. - No dynamic allocation in hot paths. We're a CLI; one-shot allocation patterns are fine.
- Descriptive comments where the code is non-obvious; see existing files for style.
Testing
Currently smoke-test driven (luban setup --only ninja --force end-to-end). M3 will add a tests/ dir with doctest-based unit tests for pure-compute modules (paths, scoop_manifest, hash, archive).
Where to start
Look for issues tagged good-first-issue. Generally welcome:
- Curating more
lib_targetsmappings (src/lib_targets.cpp) - More overlay manifests in
manifests_seed/(e.g., for tools that need an installer.script bypass) - Documentation improvements (this site is in
docs/src/)
License
Luban itself
License: TBD (likely MIT or Apache-2.0).
Vendored third-party libraries
Luban statically links these single-header libraries. Their license texts ship in third_party/:
| Library | Path | License |
|---|---|---|
| nlohmann/json | third_party/json.hpp | MIT (see their LICENSE.MIT) |
| richgel999/miniz | third_party/miniz.{h,c} | BSD-3-Clause (third_party/miniz.LICENSE) |
| marzer/tomlplusplus | third_party/toml.hpp | MIT (third_party/toml.LICENSE) |
| jothepro/doxygen-awesome-css | docs/doxygen-awesome*.{css,js} | MIT (docs/doxygen-awesome.LICENSE) β used only for the API docs site, not linked into luban.exe |
Tools downloaded by luban setup
luban setup downloads + installs these into <data>/toolchains/. Each is the upstream's own binary, with its own license:
| Tool | License | Source |
|---|---|---|
| LLVM-MinGW | Apache-2.0 + LLVM Exception (LLVM project) | github.com/mstorsjo/llvm-mingw |
| CMake | BSD-3-Clause-like (Kitware) | github.com/Kitware/CMake |
| Ninja | Apache-2.0 | github.com/ninja-build/ninja |
| MinGit (Git for Windows minimal) | GPLv2 + various deps | github.com/git-for-windows/git |
| vcpkg | MIT (Microsoft) | github.com/microsoft/vcpkg |
These remain entirely separate processes invoked by luban; their licenses do not propagate to luban itself.