Skip to content

developersharif/ubuilder

Repository files navigation

UBuilder

UBuilder

Bundle Python, Node.js, or PHP apps into a single self-contained executable.

MIT License Latest release Linux macOS Windows

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 installed

Install

Choose your platform. Each option downloads the binary from the latest GitHub Release and puts it on your PATH.

Linux (x86_64)

curl -L https://github.com/developersharif/ubuilder/releases/latest/download/ubuilder-linux-amd64.tar.gz \
  | tar -xz
sudo mv ubuilder /usr/local/bin/
ubuilder --version

Or 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 already

macOS (Apple Silicon)

curl -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 --version

The 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.

Windows (x86_64)

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 --version

To persist PATH: System Properties → Environment Variables → Path → add %USERPROFILE%\ubuilder, or run once:

[Environment]::SetEnvironmentVariable("Path", $env:Path, "User")

From source (all platforms)

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 like

See Build from source below for CMake options.


Quick start

You only need two things in your project directory:

  1. The app itself (main.py, main.js, main.php, or index.*).
  2. A ubuilder.json manifest — or just let ubuilder write one for you on the first build.

Python

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-py

Node.js

mkdir 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 world

With dependencies:

cat > package.json <<'EOF'
{ "dependencies": { "picocolors": "1.1.1" } }
EOF
ubuilder                                # npm-installs into the staged project

PHP

mkdir 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 world

With Composer:

cat > composer.json <<'EOF'
{
  "require": { "psr/log": "^1.1" }
}
EOF
ubuilder                                # composer install runs in a staged copy

PHP 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.

Smaller PHP bundles (--php-runtime=static)

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 bundles

Or 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 .sha256 published with each asset.

Trade-offs:

  • Extension set is fixed (we don't run spc build on your machine). If composer.json requires 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.

Usage examples

Drop files from the bundle

# 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"]
}

Drop a dependency from the install

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

Use a specific vendored interpreter

# 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-runtime

Skip dependency installation entirely

ubuilder --no-install-deps              # use pre-existing vendor/ or node_modules/

Build verbose / inspect what's happening

ubuilder --verbose                      # show every spawned subprocess

Build into a specific path

By 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, anywhere

See all flags

ubuilder --help

ubuilder.json manifest

Minimum:

{ "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 }
  }
}

console — Windows console window control

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's exclude array.
  • Auto-write: a successful build with no ubuilder.json writes one with the resolved runtime, entry_point, name, and exclude. The next ubuilder run needs no flags.

Full schema docs: docs/internals/architecture/CONFIG_FILE_SPEC.md.


CLI flags

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": true in ubuilder.json for CLI tools. GUI apps default to no console window. See the ubuilder.json manifest section above.


Status

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 ship libxml2.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/, every LC_LOAD_DYLIB reference is rewritten via install_name_tool -change to @executable_path/../lib/<name>, and each modified file is ad-hoc re-signed with codesign --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.exe rather than a vendored hermetic tree. Hermetic Windows is on the roadmap.

Full roadmap: docs/internals/architecture/ROADMAP_NEXT.md.


How it works

How UBuilder works — build mode produces a bundle that contains the interpreter, dependencies, and your app; launcher mode extracts and runs it on any same-OS target machine

UBuilder is one C binary with two modes, distinguished at startup:

  1. 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].
  2. Launcher mode (payload present): detect the trailer, verify the SHA-256, extract the payload to a temp dir, exec the 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:


Build from source

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.).


Contributing

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 PR

Areas 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.c mac_make_bundle_portable) to ELF: walk ldd, bundle non-system .so deps, set DT_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-shell for fully reproducible Tier-3 tests.

If you find a bug, please open an issue with:

  • The command you ran
  • Full --verbose output
  • Your ubuilder.json (if any)
  • Host OS + distro version
  • ubuilder --version

Project layout

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

License

MIT — see LICENSE.

About

Bundle Python, PHP, and Node.js apps into a single executable that runs anywhere.

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Contributors