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_libraries for each dep;
  • realize clangd needs compile_commands.json to give you autocomplete;
  • realize none of this is on PATH until 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.json for you.
  • Not a replacement. You can git clone a Luban-managed project and build it on a machine without Luban β€” cmake --preset default works as long as cmake + vcpkg are present. Luban-generated files (luban.cmake) commit cleanly to the repo.

Design philosophy

PrincipleWhat it means
Auxiliary, not authoritativecmake / 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 formatDeps 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-trackedReproducibility: 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 UACAll Luban toolchains land in user-writable directories. No admin prompts ever.
Single static binaryOne luban.exe with everything. No Python, no MSI, no installer.

Where to next

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 x64 only for now
  • A working network connection (for luban setup to 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

  1. Download luban.exe from the GitHub Releases page.
  2. Save it somewhere you'll remember β€” e.g. %USERPROFILE%\bin\luban.exe.
  3. 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, copy luban.exe itself 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.exe somewhere 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 after luban build) β†’ clangd reads it directly
  • clangd.exe is 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

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

DayFiles Luban touchedFiles you touched
Monvcpkg.json luban.cmake (initial)none
Tuevcpkg.json luban.cmakemain.cpp
Wedluban.cmake (LUBAN_TARGETS) + new src/parser/*main.cpp, src/weekly-experiment/CMakeLists.txt (+1 line), parser.{h,cpp}
Thuvcpkg.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, after luban_apply().
  • Custom find rules for a non-vcpkg dep: standard find_package(MyLib) after include(luban.cmake) β€” luban.cmake doesn't fight you.
  • Unusual generators (Make, VS, Xcode): edit CMakePresets.json (it's user-owned).
  • Header-only library target: in src/<lib>/CMakeLists.txt, change add_library(<name> STATIC ...) to add_library(<name> INTERFACE) and adjust accordingly.
  • External dep from somewhere not vcpkg: skip luban add, write your own find_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)

CommandWhat it does
luban setupInstall LLVM-MinGW + cmake + ninja + mingit + vcpkg into <data>/toolchains/
luban envShow env state; rewrite activate scripts; register HKCU PATH (rustup-style)

Per-project (run inside a project dir)

CommandWhat it does
[`luban new applib `](./new.md)
luban buildcmake --preset && cmake --build; sync compile_commands.json
[`luban target addremove`](./target.md)

Dependency management (vcpkg.json + luban.cmake)

CommandWhat it does
luban add <pkg>[@version]Edit vcpkg.json + regenerate luban.cmake (find_package + link auto-wired)
luban remove <pkg>Reverse luban add
luban syncRe-read vcpkg.json + luban.toml, regenerate luban.cmake
luban search <pattern>Search vcpkg ports (wraps vcpkg search)

Advanced / diagnostic

CommandWhat it does
luban doctorReport 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 shimRegenerate <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:

FlagEffect
-V, --versionPrint luban X.Y.Z and exit
-h, --helpPrint top-level help
-v, --verboseVerbose log output (including stack traces on internal errors)

Conventions

  • Idempotent: every command can be re-run safely. luban setup skips already-installed components, luban add replaces existing dep, luban target add refuses 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: 0 success, 1 runtime failure (download failed, cmake error), 2 user 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.json paths). 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:

ComponentSourceApprox. size
llvm-mingwmstorsjo/llvm-mingw release~180 MB
cmakeKitware/CMake release~50 MB
ninjaninja-build/ninja release~300 KB
mingitgit-for-windows/git release~40 MB
vcpkgmicrosoft/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:

  1. Resolve manifest β€” overlay β†’ bucket cache β†’ bucket remote (raw.githubusercontent)
  2. Validate manifest β€” reject if installer, pre_install, post_install, uninstaller, persist, or psmodule fields are present (these would require running PowerShell)
  3. Download the archive to <cache>/luban/downloads/, with retries + sha256 verification
  4. Extract to a staging dir under <data>/toolchains/.tmp-<name>-<ver>/
  5. Apply extract_dir β€” descend into the wrapper directory if the archive uses one
  6. Promote staging β†’ <data>/toolchains/<name>-<ver>-<arch>/ (atomic rename, copy fallback for cross-volume)
  7. Special bootstrap for vcpkg: run bootstrap-vcpkg.bat once after extract to fetch matching vcpkg.exe from microsoft/vcpkg-tool releases
  8. Write shims β€” .cmd, .ps1, extensionless sh, one set per bin alias, into <data>/bin/
  9. Update registry β€” <state>/luban/installed.json

What it does NOT touch

  • HKCU\Environment (use luban env --user for 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 setup retries 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

Show or modify environment integration. With no flags, prints status.

Synopsis

luban env [--apply] [--user | --unset-user]

What each flag does

FlagEffect
(none)Print env state: <data> location, activate-script paths, what's on PATH
--applyRewrite <data>/env/activate.{cmd,ps1,sh} from the current registry
--userRegister on HKCU PATH (rustup-style) + set LUBAN_* and VCPKG_ROOT user env vars
--unset-userReverse --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 --user so all new shells see cmake/clang/clangd/vcpkg directly.
  • Already use activate scripts: don't run --user; sourcing activate.{cmd,ps1,sh} from <data>/env/ works equivalently per-shell.
  • CI / container: skip --user entirely; set PATH and VCPKG_ROOT from 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.json is 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

  1. Validates the project name (lowercase, digits, -, _; must start with a letter)
  2. Copies the template tree to <at>/<name>/, expanding {{name}} placeholders in both file names and contents
  3. Auto-runs luban build once unless --no-build is passed. This produces compile_commands.json so clangd works the moment you open the project in Neovim or VS Code.

Flags

FlagEffect
--at <dir>Parent directory for the new project (default: cwd)
--no-buildSkip 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 statePicked preset
vcpkg.json has dependenciesdefault (uses VCPKG_ROOT for vcpkg toolchain)
vcpkg.json is empty / missingno-vcpkg (no toolchain file, faster, no VCPKG_ROOT needed)

Override with --preset release etc.

What it does, in order

  1. Configure: run cmake --preset <P> in the project dir. vcpkg manifest mode kicks in if the toolchain file is set.
  2. Build: run cmake --build --preset <P>. Ninja parallelizes across cores.
  3. Sync: copy build/<P>/compile_commands.json to project root, so clangd / VS Code C/C++ extension auto-find it.
  4. 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

GoalCommand
Just build (auto preset)luban build
Force releaseluban build --preset release
Build a subprojectluban 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

Specvcpkg.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:

  1. Look up the cmake target name in vcpkg's usage file (after first luban build they're in vcpkg_installed/<triplet>/share/<pkg>/usage)
  2. Add target_link_libraries(<your-target> PRIVATE <Mod>::<target>) in your src/<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

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.cmake is up to date with the in-tree vcpkg.json
  • Hand-edited vcpkg.json (e.g., to add "features" or "overrides" that luban add doesn't expose)
  • Changed luban.toml [scaffold] warnings = "strict" and want it reflected
  • luban.cmake got 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 actually luban 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 run luban setup --force --only vcpkg to 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:

  1. Registry alias β€” looked up in <state>/installed.json. cmake β†’ <data>/toolchains/cmake-X/bin/cmake.exe. This bypasses the .cmd shim layer (one less cmd.exe wrapper).
  2. 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 PATH
  • LUBAN_DATA / LUBAN_CACHE / LUBAN_STATE / LUBAN_CONFIG
  • (VCPKG_ROOT is not auto-injected by run β€” set it via luban env --user once 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 --user already on this machine β†’ cmake / clang / clangd are on user PATH; no need for luban run for those.
  • Project-local luban build β†’ directly wraps cmake without needing luban 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

ToolEquivalent
uvuv run <cmd>
cargo(no direct equivalent β€” cargo run is project-build)
nixnix-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.cmd or cmake.exe), not the underlying real binary
  • which cmake (Git Bash) β€” same as above, just Unix-style
  • luban 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 --json to 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 .cmd shims with real .exe proxies (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 .cmd shims 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

  1. GET https://api.github.com/repos/Coh1e/luban/releases/latest
  2. Compare tag_name (e.g. v0.1.2) against the running version
  3. If equal β†’ exit "already up to date"
  4. If newer:
    • Download luban.exe to luban.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.old for delete-on-reboot via MoveFileExW(... MOVEFILE_DELAY_UNTIL_REBOOT)
  5. 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 .new file, 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

StepEffect
1HKCU\Environment PATH entry pointing at <data>/bin/
2HKCU\Environment\LUBAN_DATA/LUBAN_CACHE/LUBAN_STATE/LUBAN_CONFIG
3HKCU\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)
8luban.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):

  1. 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
    
  2. Spawn the batch via CreateProcessW with DETACHED_PROCESS
  3. Exit luban.exe immediately
  4. After ~1.5s, batch wakes up + deletes everything

You'll briefly see no luban.exe on disk; this is intentional.

Flags

FlagEffect
--yesRequired to actually run; without it, prints plan + exits 1
--keep-dataPreserve <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 + run luban setup (idempotent β€” re-uses existing <data>/toolchains/)
  • Migrate to a different luban version: prefer luban self update over 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":

  1. Confirm you opened a new terminal (the change doesn't propagate to running shells)
  2. luban doctor β€” should show all components βœ“ and tools βœ“ on PATH
  3. echo %PATH% β€” <data>\luban\bin should 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

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

  1. In your project directory, run luban add <port>. Replace <port> with the vcpkg port name (e.g., fmt, nlohmann-json, boost-asio).

  2. Use the library in your src/<target>/main.cpp:

    #include <fmt/core.h>
    int main() { fmt::print("hello, {}\n", "fmt"); }
    
  3. Build:

    luban build
    

    The 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

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

IDE integration

Luban-managed projects work in any editor that speaks LSP, with zero configuration. The contract is:

  1. clangd is on PATH (provided by luban env --user)
  2. compile_commands.json is at the project root (produced by luban build)
  3. cmake is on PATH (so cmake --preset works 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:

  1. luban env --user was run in a previous shell? Open a fresh terminal and re-check clangd --version.
  2. luban build was run at least once? compile_commands.json only exists after a build. luban new runs a build automatically; if you passed --no-build, run luban build manually.
  3. compile_commands.json is in the project root? Should be a copy of build/<preset>/compile_commands.json. If not, luban build re-syncs it.
  4. Right clangd version? which clangd should 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:

  1. <project>/compile_commands.json
  2. <project>/build/compile_commands.json
  3. <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

ConcernPinned byLockfile-equivalent
Toolchain (cmake, ninja, clang)luban.exe version on the target machineluban release version
vcpkg ports baselinevcpkg-configuration.json baseline field (commit SHA of microsoft/vcpkg)yes
Per-port versionoptional dependencies[].version>= + overrides[]yes
Source codegityes
Generated cmake (luban.cmake)gityes (it's just text, regenerable but committed)

Best practices

  1. Don't .gitignore luban.cmake β€” it's the bridge that makes "no-luban" reproducibility possible.

  2. Pin a vcpkg baseline. luban new does this for you (uses the latest at scaffold time). Bump it intentionally with luban sync --baseline-now (M3) or by hand-editing vcpkg-configuration.json.

  3. Document the toolchain. A README.md line like "Built with luban X.Y.Z (LLVM-MinGW 22, cmake 4.3+)" helps future-you and contributors.

  4. Keep vcpkg.json minimal. Add only what you need. Each luban add should correspond to a real #include in 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:

RolePurpose
dataLong-lived state. Toolchain installs, the shim dir, content store, registry mirrors.
cacheRecreatable artifacts. Downloaded archives, vcpkg binary cache. Safe to delete.
stateMutable runtime state. installed.json, .last_sync, logs.
configUser preferences. selection.json, future config.toml.

Resolution order (per role)

For each role, in this exact order, the first existing answer wins:

  1. $LUBAN_PREFIX/<role> if LUBAN_PREFIX is set. Useful for containers / CI / multi-tenant setups.
  2. $XDG_<ROLE>_HOME/luban if the relevant XDG variable is set. (XDG_DATA_HOME, XDG_CACHE_HOME, XDG_STATE_HOME, XDG_CONFIG_HOME)
  3. macOS default (~/Library/...) if running on macOS
  4. Windows fallback (%LOCALAPPDATA%\luban etc.) if running on Windows
  5. Linux default (~/.local/share/luban etc.) β€” 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

RoleLinux defaultWindows fallbackmacOS 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:

  1. Container/CI portability β€” a single LUBAN_PREFIX=/tmp/luban env var redirects everything for headless builds, with no Windows API quirks.
  2. No-surprise Linux port β€” when Linux support lands, the same paths work; users moving cross-platform don't need to relearn anything.
  3. 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%\luban would 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.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=...
FieldTypeDefaultEffect
[project] default_presetstring"default"which preset luban build picks if --preset not given (and not auto)
[project] tripletstring"x64-mingw-static"vcpkg target triplet for manifest-mode installs
[project] cppint23C++ language standard, passed to cmake cxx_std_<N>
[scaffold] warningsstring"normal"off / normal / strict (more flags at higher levels)
[scaffold] sanitizersarray[]comma-joined and prefixed with -fsanitize= for both compile and link

What warnings controls

Generated in luban.cmake via target_compile_options(... PRIVATE ...):

LevelFlags 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.json after luban new β€” this is yours
  • vcpkg.json[overrides], vcpkg.json[features] β€” preserved exactly
  • vcpkg-cache/ or vcpkg_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

SectionSourceWhat it controls
LUBAN_TARGETSluban target add/removeWhich subdirs luban_register_targets() walks
find_package(...)vcpkg.json dependenciesWhich vcpkg libs are imported
luban_apply(target) bodyluban.toml [scaffold] + vcpkg.json depsFlags + 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 updated
  • luban sync β€” full regen from vcpkg.json + luban.toml
  • luban 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"]
}
FieldTypeNotes
versionstringrequired
urlstring | string[1]required, plain HTTPS download
hashstring | string[1]required; sha256:<hex> (or just <hex>)
extract_dirstringoptional; descend into this subdir of the extracted archive
extract_tostringoptional; place archive contents into this subdir of the toolchain dir
binarrayoptional; entries are string or [rel,alias] or [rel,alias,"prefix args"]
env_setobjectoptional; future env-injection (M3)
env_add_patharrayoptional; subdirs to add to PATH (auto-shimmed at install time)
dependsarrayoptional; install order hint (M3)

Refused fields (safety)

These are always rejected, even in overlay manifests:

  • installer (e.g., installer.script)
  • pre_install / post_install
  • uninstaller
  • persist
  • psmodule

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

VariableEffect
LUBAN_PREFIXIf set, all four roles resolve to $LUBAN_PREFIX/<role>. Highest priority.
XDG_DATA_HOMEOverride <data> to $XDG_DATA_HOME/luban.
XDG_CACHE_HOMEOverride <cache> to $XDG_CACHE_HOME/luban.
XDG_STATE_HOMEOverride <state> to $XDG_STATE_HOME/luban.
XDG_CONFIG_HOMEOverride <config> to $XDG_CONFIG_HOME/luban.
LOCALAPPDATAWindows fallback for data/cache/state.
APPDATAWindows fallback for config (roaming).
USERPROFILE / HOMEDRIVE+HOMEPATH / HOMEUsed to find the user's home dir.
NO_COLORIf set (any value), disables ANSI colors in luban's output.
LUBAN_NO_PROGRESSIf set, disables download progress bar (CI-friendly).
PATHRead 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).

VariableValue
PATHPrepends <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_ROOTPath 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_ROOT is read by vcpkg's cmake integration, NOT by luban directly. We just set it.
  • CMAKE_TOOLCHAIN_FILE referenced in CMakePresets.json uses $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:

  1. A toolchain (LLVM-MinGW or MSVC + MinGW or mingw-w64)
  2. cmake β€” project meta-build
  3. ninja β€” actual builder
  4. clangd β€” LSP for editors
  5. vcpkg β€” package manager
  6. git β€” for vcpkg + general use
  7. Some glue: compile_commands.json, find_package, target_link_libraries, triplets, CMakePresets, CMAKE_TOOLCHAIN_FILE, vcpkg-configuration.json baseline 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 default works.

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

LayerWhatWhereWho
Systemcmake, ninja, clang, clangd, lld, git, vcpkg<data>/toolchains/luban setup
Projectfmt, 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 will include(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 add knows ~50 popular libs by name. Unknown ports get find_package(<port>) written but no auto-link; user fills target name in. Long tail handled by future scraping of vcpkg's usage files.

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

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.exe upgrade is an .exe swap, no migration needed.
  • A future Linux port is plausible without rewriting; the philosophy already accommodates it.

Cross-references

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 install for 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:

LayerExamplesStored inScope
Systemcmake, ninja, clang, clangd, lld, git, vcpkg itself<data>/toolchains/<name>-<ver>-<arch>/Cross-project, machine-wide (one user)
Projectfmt, spdlog, boost, catch2, nlohmann-json<project>/vcpkg_installed/<triplet>/Per-project, in-tree

This is the same split that mature ecosystems converge on:

EcosystemSystem tierProject tier
Rustrustupcargo
Pythonpipx / uv tooluv / pip (in venv)
Nodevolta / nvmnpm / pnpm
Gosystem gogo.mod
C++ via Lubanluban setup (LLVM-MinGW etc.)luban add (vcpkg deps)

Hard rules (the constraints we enforce)

  1. Toolchain names not allowed in vcpkg.json. luban add cmake errors out and points the user to luban setup. Same for ninja, clang, git, etc.

  2. Project libs not allowed in luban setup. If you luban setup --only fmt, it'll just fail to find an overlay manifest (and Scoop's fmt manifest, if any, is ports-via-installer-script which we refuse).

  3. One source of truth per layer. System tier is <state>/installed.json. Project tier is vcpkg.json. Never duplicated, never conflated.

  4. System tier upgrades are global. All projects on this machine see the same cmake version. (Future: per-project pin via luban.toml [toolchain], but not in v1.)

  5. 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 fmt upgrade 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 after add_executable / add_library. Sets cpp std, warnings, links vcpkg deps.
  • luban_register_targets() β€” call once from root. Does add_subdirectory(src/<name>) for every entry in LUBAN_TARGETS.

That's it. Two functions, minimal API surface, no DSL.

Properties of this design

PropertyHow it works
User can edit CMakeLists.txt freelyLuban only writes luban.cmake; the CMakeLists.txt is theirs from day 1 (after luban new).
Project builds without lubancmake --preset default works on any machine with cmake + vcpkg. luban.cmake is just a regular include.
User can opt out at any timeDelete the include(luban.cmake) and luban_register_targets() lines. The project becomes plain cmake.
Luban can update its codegen without breaking your projectWe rewrite luban.cmake on each luban add/sync, but never touch the user's CMakeLists.txt after luban new.
Per-target customization is plain cmakeUser 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 after luban new)
  • CMakePresets.json (user-owned after luban new)
  • vcpkg.json (vcpkg's manifest β€” luban only edits via add/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.bat integration

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.cmake generator (find_package, luban_apply, luban_register_targets)
  • luban.toml schema v1 ([project], [scaffold])
  • Curated pkg β†’ cmake target mapping (~50 popular libraries)
  • Scaffold improvements: subdir-from-day-1, vcpkg-configuration.json baseline
  • luban env --user extended to set HKCU env vars (LUBAN_*, VCPKG_ROOT)

πŸ”œ M3 β€” Daily-driver polish

FeatureNotes
luban search <pattern>Wraps vcpkg search; adds caching
luban which <alias>Show absolute exe path for a registry alias
luban describe --jsonMachine-readable project + system state for IDE plugins
luban run <cmd> [args...]uv-style transparent activation + exec
Real .exe shimrustup-style native exe proxies, replacing .cmd shims
luban-init.exe modeStandalone bootstrapper (~5 MB) β€” alternative entry to luban setup
luban toolchain {list,use,install}Multi-version toolchain management

πŸŒ… M4+ β€” Beyond the immediate need

ThemeExamples
Workspace supportluban.toml [workspace] members = [...], shared build cache
Toolchain pin per projectrust-toolchain.toml equivalent
Linux/macOS portproc.cpp POSIX, download.cpp libcurl, setup.cpp cross-platform
TUI / interactive modeluban 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 CMake find_package calls in luban's own build.
  • Win32 native: prefer CreateProcessW / WinHttpSendRequest / BCryptHashData / RegSetValueExW over 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_targets mappings (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/:

LibraryPathLicense
nlohmann/jsonthird_party/json.hppMIT (see their LICENSE.MIT)
richgel999/minizthird_party/miniz.{h,c}BSD-3-Clause (third_party/miniz.LICENSE)
marzer/tomlplusplusthird_party/toml.hppMIT (third_party/toml.LICENSE)
jothepro/doxygen-awesome-cssdocs/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:

ToolLicenseSource
LLVM-MinGWApache-2.0 + LLVM Exception (LLVM project)github.com/mstorsjo/llvm-mingw
CMakeBSD-3-Clause-like (Kitware)github.com/Kitware/CMake
NinjaApache-2.0github.com/ninja-build/ninja
MinGit (Git for Windows minimal)GPLv2 + various depsgithub.com/git-for-windows/git
vcpkgMIT (Microsoft)github.com/microsoft/vcpkg

These remain entirely separate processes invoked by luban; their licenses do not propagate to luban itself.