Bundle Python, Node.js, or PHP apps into a single self-contained executable.
The output runs on any same-OS machine — no Python, Node, PHP, pip,
npm, or composer required on the target. The build machine doesn't
need a global interpreter either; ubuilder's first run vendors pinned
runtime tarballs into ~/.cache/ubuilder/runtimes/ and embeds them
into every bundle.
cd my-app/ # contains main.py + (optional) requirements.txt
ubuilder # produces one executable named after the dir
./my-app # runs anywhere with no Python installedChoose your platform. Each option downloads the binary from the latest GitHub Release and puts it on your PATH.
curl -L https://github.com/developersharif/ubuilder/releases/latest/download/ubuilder-linux-amd64.tar.gz \
| tar -xz
sudo mv ubuilder /usr/local/bin/
ubuilder --versionOr unprivileged, into ~/.local/bin:
mkdir -p ~/.local/bin
curl -L https://github.com/developersharif/ubuilder/releases/latest/download/ubuilder-linux-amd64.tar.gz \
| tar -xz -C ~/.local/bin ubuilder
# add ~/.local/bin to PATH if it isn't alreadycurl -L https://github.com/developersharif/ubuilder/releases/latest/download/ubuilder-macos-amd64.tar.gz \
| tar -xz
sudo mv ubuilder /usr/local/bin/
xattr -d com.apple.quarantine /usr/local/bin/ubuilder 2>/dev/null || true
ubuilder --versionThe xattr line removes Gatekeeper's quarantine flag on the downloaded binary.
Intel Macs: the published ubuilder-macos-amd64.tar.gz is actually an
arm64 binary today (the CI macOS runner is macos-15-arm64; the
-amd64 suffix in the asset name is a naming carryover). If you're on
an Intel Mac, build from source instead.
PowerShell:
$url = "https://github.com/developersharif/ubuilder/releases/latest/download/ubuilder-windows-amd64.zip"
Invoke-WebRequest -Uri $url -OutFile "$env:TEMP\ubuilder.zip"
Expand-Archive -Path "$env:TEMP\ubuilder.zip" -DestinationPath "$env:USERPROFILE\ubuilder" -Force
$env:Path += ";$env:USERPROFILE\ubuilder"
ubuilder --versionTo persist PATH: System Properties → Environment Variables → Path → add %USERPROFILE%\ubuilder, or run once:
[Environment]::SetEnvironmentVariable("Path", $env:Path, "User")Requires CMake ≥ 3.16, a C11/C++17 compiler (GCC/Clang/MSVC), and zlib headers.
git clone https://github.com/developersharif/ubuilder.git
cd ubuilder
mkdir -p build && cd build
cmake -DCMAKE_BUILD_TYPE=Release ..
cmake --build . -j
sudo cp src/ubuilder /usr/local/bin/ # or wherever you likeSee Build from source below for CMake options.
You only need two things in your project directory:
- The app itself (
main.py,main.js,main.php, orindex.*). - A
ubuilder.jsonmanifest — or just letubuilderwrite one for you on the first build.
mkdir hello-py && cd hello-py
cat > main.py <<'EOF'
import sys
print(f"Hello from Python, args: {sys.argv[1:]}")
EOF
ubuilder # auto-writes ubuilder.json on first run
./dist/hello-py world # default output is dist/<project-dir-basename>
# → Hello from Python, args: ['world']With dependencies:
cat > requirements.txt <<'EOF'
attrs==23.2.0
EOF
ubuilder # pip-installs attrs into the bundle
./dist/hello-pymkdir hello-node && cd hello-node
cat > main.js <<'EOF'
console.log("Hello from Node, args:", process.argv.slice(2));
EOF
echo '{"runtime":"node","entry_point":"main.js"}' > ubuilder.json
ubuilder
./dist/hello-node worldWith dependencies:
cat > package.json <<'EOF'
{ "dependencies": { "picocolors": "1.1.1" } }
EOF
ubuilder # npm-installs into the staged projectmkdir hello-php && cd hello-php
cat > main.php <<'EOF'
<?php
echo "Hello from PHP, args: " . implode(",", array_slice($argv, 1)) . "\n";
EOF
echo '{"runtime":"php","entry_point":"main.php"}' > ubuilder.json
ubuilder
./dist/hello-php worldWith Composer:
cat > composer.json <<'EOF'
{
"require": { "psr/log": "^1.1" }
}
EOF
ubuilder # composer install runs in a staged copyPHP bundles built on Linux run on any target machine that has the same shared libraries the host PHP linked against (libxml2, libssl, libsodium, …) — for most servers this is a non-issue. PHP bundles built on macOS run on any Mac with no extra dependencies: the builder bundles every non-system dylib from the host PHP's transitive dep graph and rewrites Mach-O load commands to bundle-relative paths. See Status for the libxml2 SONAME caveat that still applies on Linux.
By default ubuilder uses the host machine's PHP and bundles every dylib it depends on. On macOS that means Homebrew/Herd PHP drags in ~50 libraries (libcurl + its HTTP/3 stack, libgd, libpq, libldap, …) and bundles balloon to ~280–400 MB.
--php-runtime=static switches to a curated static-php-cli build that ubuilder ships and downloads on demand:
ubuilder --runtime=php --php-runtime=static # ~50–80 MB minimal bundlesOr in ubuilder.json:
{
"runtime": "php",
"entry_point": "index.php",
"php_runtime": "static"
}What you get:
- PHP 8.4 with FFI, GD, intl, mbstring, openssl, curl, zip, phar, pdo_* (sqlite + mysql), sockets, sodium, opcache, and the usual Laravel/Symfony set.
- One ~64 MB statically-linked PHP binary, downloaded once and cached at
$XDG_CACHE_HOME/ubuilder/runtimes/php/<minor>-<target>/. - SHA256-verified against the
.sha256published with each asset.
Trade-offs:
- Extension set is fixed (we don't run
spc buildon your machine). Ifcomposer.jsonrequires an ext we don't compile in, ubuilder errors at build time with a clear message — drop the require, switch to--php-runtime=host, or open an issue. - Currently supports macOS (arm64, x86_64) and Linux x86_64. Other targets fall back to host PHP.
# Exclude tests/ docs/ and *.md files from the embedded app tree
ubuilder --exclude='tests/**' --exclude='docs/**' --exclude='*.md'Or in ubuilder.json:
{
"runtime": "python",
"entry_point": "main.py",
"exclude": ["tests/**", "docs/**", "*.md"]
}ubuilder --exclude=six # Python: filters requirements.txt
ubuilder --exclude=is-number # Node: filters package.json
ubuilder --exclude=ext-curl # PHP: drops composer ext-* + passes --ignore-platform-req# Point at a directory you control instead of the cache
ubuilder --runtime-source=/opt/my-python/
# Or use the host's installed interpreter (NOT portable — for dev only)
ubuilder --use-host-runtimeubuilder --no-install-deps # use pre-existing vendor/ or node_modules/ubuilder --verbose # show every spawned subprocessBy default the bundle goes to dist/<project-dir-basename> and the
output tree is auto-excluded from itself so you can re-run ubuilder
freely. Override:
ubuilder --output=dist/myapp # explicit path under dist/
ubuilder --output=/opt/builds/myapp # absolute path, anywhereubuilder --helpMinimum:
{ "runtime": "python", "entry_point": "main.py" }Full schema (every field optional except runtime + entry_point):
{
"schema_version": 1,
"name": "my-app",
"runtime": "python",
"entry_point": "main.py",
"output": "dist/my-app",
"exclude": ["tests/**", "*.md", "six"],
"verbose": false,
"console": true,
"compression": true,
"runtime_options": {
"python": { "source": "/opt/cpython-3.12", "use_host": false }
}
}| Value | Effect |
|---|---|
false (default) |
Output executable runs without a console window (GUI subsystem). Double-clicking shows no terminal. Right for GUI apps and background services. |
true |
Output executable keeps its console window. Right for CLI tools, scripts, or anything that prints to stdout/stderr. |
On Linux and macOS this key is ignored — terminal visibility is determined by how the process is launched, not by the binary itself.
{ "runtime": "node", "entry_point": "main.js", "console": true }- CLI flags override config keys — except
--exclude, which appends to the config'sexcludearray. - Auto-write: a successful build with no
ubuilder.jsonwrites one with the resolvedruntime,entry_point,name, andexclude. The nextubuilderrun needs no flags.
Full schema docs: docs/internals/architecture/CONFIG_FILE_SPEC.md.
The zero-flag path is the default. Pass these for non-default cases:
| Flag | Purpose |
|---|---|
--project-dir <path> |
Build from a directory other than . |
--runtime <python|php|node> |
Override the manifest's runtime |
--output <path> |
Output executable path (default: dist/<project-dir-basename>) |
--entry-point <file> |
Override the manifest's entry point |
--config <path> |
Use an explicit ubuilder.json |
--runtime-source <path> |
Use a specific vendored interpreter tree |
--use-host-runtime |
Use the host's interpreter (bundle will not be portable) |
--no-install-deps |
Skip pip / npm / composer install |
--no-auto-vendor |
Don't auto-spawn scripts/vendor-runtimes.sh on cache miss |
--exclude <pat> (repeatable) |
Drop a file glob, PHP ext, Python wheel, or Node module |
--php-runtime <host|static> |
PHP source: host (default, walks host PHP deps) or static (downloads curated static-php-cli build, ~50–80 MB bundles) |
--self-update |
Download the latest ubuilder release and replace this binary |
--verbose / -v |
Show every spawned subprocess |
--version / -V |
Print the ubuilder version |
--help / -h |
Show all flags |
Windows console window: set
"console": trueinubuilder.jsonfor CLI tools. GUI apps default to no console window. See theubuilder.jsonmanifest section above.
| Runtime | Linux | macOS | Windows | Notes |
|---|---|---|---|---|
| Python | ✅ Hermetic | ✅ Hermetic | ✅ Host | Tier-3 Docker isolation passing on Linux + macOS |
| Node.js | ✅ Hermetic | ✅ Hermetic | ✅ Host | Tier-3 Docker isolation passing on Linux + macOS |
| PHP | ✅ Host-bits hermetic | ✅ Fresh-Mac portable | ✅ Host | M1-D ships embedded bin/php + composer extensions on Linux; on macOS the builder additionally walks otool -L and rewrites every non-system dylib ref to @executable_path/../lib/... so the bundle runs on any Mac (Herd / static-php-cli is a no-op; Homebrew/MacPorts gets full dyld rewiring) |
Known limitations:
- PHP cross-distro portability: bundles include the host's
libxml2.so.<N>. Build on the same distro family as your deployment target. Ubuntu 24.10+ and Debian Trixie+ both shiplibxml2.so.16; older distros ship.so.2. - PHP on macOS bundles are now fresh-Mac portable. Statically linked
hosts (Laravel Herd,
static-php-cli) ship their binary as-is — every extension is baked in and only system frameworks are referenced. Dynamic hosts (Homebrew, MacPorts, custom builds) trigger a Mach-O rewiring pass: each non-system dylib in the host PHP's transitive dep graph is hardlinked into<bundle>/lib/, everyLC_LOAD_DYLIBreference is rewritten viainstall_name_tool -changeto@executable_path/../lib/<name>, and each modified file is ad-hoc re-signed withcodesign --force --sign -. User-installed extensions (pecl install xdebug,brew install php-imagick, etc.) follow the same path automatically. - Windows: bundles use the host's
python.exe/node.exe/php.exerather than a vendored hermetic tree. Hermetic Windows is on the roadmap.
Full roadmap: docs/internals/architecture/ROADMAP_NEXT.md.
UBuilder is one C binary with two modes, distinguished at startup:
- Build mode (no embedded payload): parse CLI +
ubuilder.json→ pick a runtime builder → write a new executable laid out as[ubuilder launcher][runtime tree][app tree][V4 trailer with SHA-256]. - Launcher mode (payload present): detect the trailer, verify the SHA-256, extract the payload to a temp dir,
execthe embedded interpreter against the embedded entry point, clean up on exit.
User dependencies (requirements.txt, package.json, composer.json) are installed into a staged copy of the runtime/project before bundling — the shared cache is never polluted. Successful installs are content-addressed by SHA-256(manifest + lockfile) and replayed from cache on the next build.
Deep dives:
docs/internals/architecture/ARCHITECTURE_AUDIT.md— engineering audit + hermeticity principlesdocs/internals/architecture/M1_HERMETIC_INTERPRETERS.md— vendoring strategy +--runtime-sourceprecedencedocs/internals/architecture/M8_USER_DEPS.md— per-runtime dep-install mechanicsdocs/internals/architecture/CONFIG_FILE_SPEC.md— fullubuilder.jsonschemadocs/internals/architecture/STATIC_LAUNCHER.md—-DUBUILDER_STATIC=ONfor a fully static launcher (musl toolchain provided)
git clone https://github.com/developersharif/ubuilder.git
cd ubuilder
mkdir -p build && cd build
cmake -DCMAKE_BUILD_TYPE=Release ..
cmake --build . -j
./tests/test_ubuilder # 184/184 unit tests
../tests/bundle/run-bundle-tests.sh # 13/13 end-to-end bundle cases
../tests/bundle/run-tier3.sh # 4/4 Docker portability cases (Python + Node hermetic; PHP host-bits-hermetic)Useful CMake options:
| Option | Purpose |
|---|---|
-DBUILD_TESTS=OFF |
Skip building the test binary |
-DENABLE_COMPRESSION=OFF |
Disable ZLIB resource compression |
-DUBUILDER_STATIC=ON -DCMAKE_TOOLCHAIN_FILE=../toolchains/musl-linux-x86_64.cmake |
Build a fully static launcher against musl |
After building, install the binary wherever you like (/usr/local/bin, ~/.local/bin, etc.).
Contributions welcome! Start with CONTRIBUTING.md for development setup, code conventions, and the PR workflow.
A typical contributor loop:
git clone https://github.com/developersharif/ubuilder.git
cd ubuilder
mkdir -p build && cd build && cmake .. && cmake --build . -j
./tests/test_ubuilder # all green before changes
# ... make a change ...
cmake --build . -j && ./tests/test_ubuilder
../tests/bundle/run-bundle-tests.sh # before PRAreas where help is most welcome:
- Hermetic PHP on Linux without the libxml2 SONAME caveat — port the macOS Mach-O rewiring approach (
src/runtimes/php_builder.cmac_make_bundle_portable) to ELF: walkldd, bundle non-system.sodeps, setDT_RUNPATH=$ORIGIN/../lib. - Hermetic Windows runtimes — vendor a portable Python / Node tree like Linux/macOS already do.
- Lockfile reproducibility for Python (
requirements.lock) and PHP (composer.lock-driven--no-deps). - More tier-3 backends — e.g.
nix-shellfor fully reproducible Tier-3 tests.
If you find a bug, please open an issue with:
- The command you ran
- Full
--verboseoutput - Your
ubuilder.json(if any) - Host OS + distro version
ubuilder --version
ubuilder/
├── src/
│ ├── core/ # ubuilder.{c,h}, platform_compat, json_mini, config, sha256, glob_match
│ ├── runtimes/ # {python,php,nodejs}_{builder,runtime}.c + runtime_embedder, install_cache
│ └── main.c # CLI entry; routes builder vs launcher mode
├── scripts/
│ └── vendor-runtimes.sh # SHA-pinned interpreter downloader (Linux + macOS)
├── tests/
│ ├── test_*.c # unit tests (test_ubuilder binary)
│ └── bundle/ # end-to-end bundle + Tier-3 isolation harness
├── examples/{python,php,nodejs}/
├── toolchains/ # CMake toolchain files (musl-linux-x86_64)
└── docs/ # architecture audit, M1, M8, config spec, roadmap, CLI reference
MIT — see LICENSE.
