lw · build orchestration for developers and CI

One build interface for every project, toolchain and runner.

lw turns your CMake, Meson and shell build variants into named, fully resolved profiles, then configures, builds, runs and tests them through one command line. It behaves the same on your laptop as on a CI runner, and it never rewrites your build system.

  • Linux x86_64 · macOS arm64 · Windows x86_64
  • Single self-contained binary
  • Signed, provenance-attested releases
~/src/app: first run from a fresh clone
$ lw init
$ lw project add ./app                # type auto-detected (cmake, meson, …)
$ lw configset create Debug app=Debug
$ lw tools                            # toolchains found on this machine
$ lw profile create Debug ninja-gcc-14
created profile: Debug:ninja-gcc-14.2.0  [local]
$ lw build Debug:ninja-gcc-14         # configures if needed, then builds
$ lw test Debug:ninja-gcc-14 --junit results.xml
$ lw publish                          # write the shared loomworks.json

Works with the build systems you already have

CMakeIncluding CMakePresets.json and CMakeUserPresets.json with full inheritance
MesonFull configure / build / test support
ShellA generic runner for self-managed builds: scripts, Make, vendor toolchains
TypeScriptEarly support

Toolchains are detected automatically: GCC, Clang, MSVC (via vswhere) and clang-cl, paired with the Ninja or Visual Studio generators. Compilers that detection can't find are declared once with lw sdk add.

How it works

Build variants in, reproducible profiles out

You describe what a build variant means across your projects. lw pairs that with a toolchain it found on the machine, producing a profile: a fully resolved, buildable unit you can name in a script, a CI matrix or a teammate's terminal.

Commit the portable part, resolve the rest per machine

Projects and configuration sets are the portable contract. They live in loomworks.json, which you can commit. Profiles pin a toolchain that exists on this machine, so they stay local by default, and each developer or CI cell creates its own from the shared sets.

  • Coarse toolchain selectors. ninja-clang-18 picks the highest installed 18.x; msvc-17 matches Visual Studio 17 without naming the edition.
  • Configures only when it must. Any change to what a configure is given (options, environment, toolchain, compiler cache) is applied by a faithful reconfigure, and lw prints why it ran.
  • Machine-specific values stay machine-local. Declare a blank variable such as an SDK path once; each machine fills it with lw profile set. Builds refuse to run while one is unfilled.
loomworks.json
{
  "projects": {
    "MyApp":    { "cmake": {} },
    "MyLib":    { "cmake": {} },
    "Frontend": { "typescript": {} }
  },
  "configuration_sets": {
    "Debug":   { "MyApp": "Debug",   "MyLib": "Debug",   "Frontend": "development" },
    "Release": { "MyApp": "Release", "MyLib": "Release", "Frontend": "production" }
  }
}
Built for teams and pipelines

Enterprise-grade where it counts: reproducible, verifiable, scriptable

Every capability below ships in lw today.

Pinned, verified launcher

lw bootstrap commits lw.sh, lw.cmd and lw.pin. A clean checkout runs the exact pinned release, with every download checked against the pinned SHA-256. No prior install is needed.

lw bootstrap · lw update

Signed, attested releases

Each release publishes a signed SHA256SUMS, and every binary carries GitHub build provenance recorded in Sigstore's public transparency log. A bundle whose signature or hash doesn't match is never executed.

gh attestation verify · openssl dgst -verify

Headless by design

--no-input (or LW_NO_INPUT / CI) never prompts: a missing value fails with the exact command to run. Results go to stdout, diagnostics to stderr, and exit codes are real.

lw --no-input build <profile>

Deterministic queries

Ask for one machine-readable fact (build directory, configuration, state, tool, compiler cache or a variable) instead of parsing build logs. The build directory is known before you build.

lw profile query <profile> <project> build-dir

Non-invasive

lw reads your CMakeLists.txt, presets and Meson files and drives the build system's own configure and build. It never modifies project files, and collaborators don't need to know it exists.

Local or committed config

Keep everything in the gitignored working copy (.nvim/loomworks.user.json), or lw publish a shared loomworks.json for your team and CI. Each item carries an intent, so private and shared settings never mix.

lw publish · --local · --shared

Git worktree aware

Create a worktree and seed it with the main checkout's config in one step, or pull config into an existing one. The merge is non-destructive and never touches build state.

lw worktree add <branch> · lw pull

Compiler caching

ccache or sccache is wired into CMake and Meson builds automatically for GCC and Clang, and on request for MSVC, where lw switches debug info to /Z7 and flags leftover /Zi targets.

lw help cache · lw status --cache-stats

lw health and self-update

Actionable, advisory checks for the workspace and the lw release itself. lw self-update verifies, then atomically replaces the bundle and binary, on a stable or unstable channel.

lw health · lw self-update --channel stable

Output-conflict protection

If building one profile would overwrite a binary that another built profile still owns, lw refuses instead of silently clobbering it. --force proceeds and marks the other profile stale.

Run under any wrapper

Build, deploy and launch a target, or run it under valgrind or gdb with its resolved working directory and environment. --print=json reports the exact invocation without running it.

lw run --prefix valgrind · lw run --print=json

Verified modules

CMake, Meson, shell and TypeScript ship inside lw. Platform modules, such as HarmonyOS / OpenHarmony, install on demand, each checked against a pinned SHA-256 and the plugin-interface version.

lw module install harmony

Continuous integration

The same lw on every runner, pinned and verified

Commit the launcher once. Every matrix cell then fetches the exact pinned release, checks its hash, creates its own profile from the shared configuration sets, and builds by explicit key.

.github/workflows/build.yml
name: build
on: [push, pull_request]

jobs:
  build:
    strategy:
      matrix:
        include:
          - { os: ubuntu-latest,  tool: ninja-gcc }
          - { os: windows-latest, tool: msvc-17 }
    runs-on: ${{ matrix.os }}
    env:
      LW_NO_INPUT: "1"          # never prompt
      PROFILE: Debug:${{ matrix.tool }}
    defaults:
      run: { shell: bash }     # Git Bash on Windows
    steps:
      - uses: actions/checkout@v4
      - if: runner.os == 'Linux'
        run: sudo apt-get install -y ninja-build

      # lw.sh fetches the pinned lw and verifies its sha256
      - run: ./lw.sh profile create Debug ${{ matrix.tool }}
      - run: ./lw.sh build "$PROFILE"
      - run: ./lw.sh test "$PROFILE" --junit results.xml

      - name: Locate the build directory
        run: echo "BUILD_DIR=$(./lw.sh profile query "$PROFILE" app build-dir)" >> "$GITHUB_ENV"
      - uses: actions/upload-artifact@v4
        with:
          name: app-${{ matrix.os }}
          path: |
            ${{ env.BUILD_DIR }}
            results.xml
  • No install step. ./lw.sh (or lw.cmd on Windows) downloads the pinned host binary into .nvim/cache/, verifies its SHA-256 against lw.pin every time, and runs it. The host then provisions the pinned bundle.
  • Never blocks on a prompt. In non-interactive mode lw build ignores the active profile and never guesses; you name the profile, or it fails with the exact command to run.
  • Matrix-friendly selectors. Name a toolchain by major version or without an edition, and each runner resolves the best installed match.
  • Reports CI can ingest. lw test --junit writes JUnit XML and exits non-zero when a test fails. lw status --check and lw migrate --check fail the job on diagnostics or outdated workspace files.
  • Proxies and air-gapped runners. The launcher honours HTTPS_PROXY; point LOOMWORKS_RELEASE_URL at a local mirror for offline runners. Hash verification is always enforced.
once, from the repo root
$ lw bootstrap                  # pin this release (or --version x.y.z)
$ git add lw.sh lw.cmd lw.pin
$ lw update                     # later: move the pin forward

What lw bootstrap commits

FilePurpose
lw.pinThe pinned release version plus the SHA-256 of every host binary and of the release bundle, taken from the release's signed SHA256SUMS after its signature is verified.
lw.shPOSIX launcher for Linux, macOS, and Git Bash / MSYS on Windows.
lw.cmdNative Windows launcher for cmd and PowerShell.

A globally installed lw also honours the pin inside a bootstrapped repo, so developers get the pinned behaviour too. Use --no-pin to bypass it.

Also in your editor

The same profiles, inside Neovim

lw is the product; the editor integration is an optional extra. loomworks.nvim reads the same workspace files, so switching between the terminal, the editor and CI never means maintaining two configurations.

  • Status page (:LoomworksInfo): profiles, build state, diagnostics and running tasks, with build, configure and clean actions.
  • clangd, wired up. loomworks generates the compilation database for every CMake generator and restarts clangd when you switch profiles.
  • Build, run and debug through overseer.nvim and nvim-dap, with a lualine component that shows the active profile.
lazy.nvim
{
  "samienne/loomworks.nvim",
  event = "VeryLazy",
}
Get started

Download, verify, then install

lw installs itself. Download the binary for your platform, verify it with GitHub build provenance, and let the verified binary put itself on your PATH and fetch its first signed bundle. You don't pipe an unread script into a shell, and you don't need admin rights.

No gh? The docs show the release-key signature route using openssl or PowerShell 7.

Read the quick start
Linux x86_64
$ gh release download --repo samienne/loomworks.nvim -p lw-linux-x86_64
$ gh attestation verify lw-linux-x86_64 --repo samienne/loomworks.nvim
$ chmod +x lw-linux-x86_64 && ./lw-linux-x86_64 install -y
macOS arm64
$ gh release download --repo samienne/loomworks.nvim -p lw-macos-arm64
$ gh attestation verify lw-macos-arm64 --repo samienne/loomworks.nvim
$ chmod +x lw-macos-arm64 && ./lw-macos-arm64 install -y
Windows x86_64 (PowerShell)
> gh release download --repo samienne/loomworks.nvim -p lw-windows-x86_64.exe
> gh attestation verify lw-windows-x86_64.exe --repo samienne/loomworks.nvim
> .\lw-windows-x86_64.exe install -y

Stop re-explaining your build

Describe your variants once. Build them the same way on any developer machine or CI runner, from one pinned, verified lw.