venusian / build
Compiles a Venusian app into a native executable. Installed by venusian install:sdk.
Requires
- php: ^8.4|^8.5|^8.6
- ext-phar: *
- ext-zip: *
- laravel/prompts: ^0.3.19
- nikic/php-parser: ^5.0
- symfony/console: ^7.4|^8.0
- symfony/filesystem: ^7.0|^8.0
- symfony/finder: ^7.0|^8.0
- symfony/process: ^7.0|^8.0
Requires (Dev)
- mockery/mockery: ^1.6
- pestphp/pest: ^4
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
venusian build, run inside a Venusian app, turns it into something another machine installs and runs: a signed .app in a .dmg for Apple Silicon Macs on macOS 14 and newer, and a .deb for Linux arm64 and x86_64 (Ubuntu 24.04 and newer, Debian 13, Raspberry Pi OS on trixie, JetPack 7).
Requirements
Everywhere:
- Composer on your
PATH - The Venusian installer
- The app's
config/app.phphas'id' => env('APP_ID', 'com.venusian.app')undername(framework 0.10 skeletons ship it)
macOS targets, built on an Apple Silicon Mac:
- The Xcode command line tools (
xcode-select --install): clang,codesign,hdiutil,notarytool cmake,autoconfandpkg-config(brew install cmake autoconf pkg-config): build tools only; nothing from Homebrew ships in an app- A macOS app's toolkit is AppKit (
jovian/venusian-appkit). GTK and Qt run on a Mac from source for testing; a macOS build leaves them out, and refuses an app that has no AppKit
Linux targets, built from any machine:
- Docker on a machine of the target's CPU: the one you run
venusian buildon, or another reached through a docker context over ssh. A Mac with Docker Desktop buildslinux-arm64in its VM - Nothing else: PHP, the SAPI and the extensions compile inside the build image
docker context create gamingpc --docker "host=ssh://angel@192.168.4.38" # an x86_64 box docker context create jetson --docker "host=ssh://angel@192.168.4.40" # an arm64 box
The context needs ssh key auth that works without a prompt (a key in the agent or the macOS keychain). Then name which context builds which target in ~/.venusian/build/config.json, a file about your machines, never committed with an app:
{"hosts": {"linux-x86_64": "gamingpc", "linux-arm64": "jetson"}}
A target left out builds on the current docker context when its CPU matches.
Installation
venusian install:sdk
That runs composer global require venusian/build. The installer finds the package by its Composer type, venusian-tool, and venusian build appears.
Usage
cd my-app
venusian build
The interview asks for the app name, version, the sketch to run (only when there are several), the author and one-line summary while they are empty, and, once per machine when a macOS target is built, how macOS builds sign (saved to ~/.venusian/build/config.json). Every answer defaults from build.json and is written back to it, so a second run is Enter all the way; --no-interaction takes the defaults outright. Output lands in build/; the build git-ignores it.
venusian build "Night Sky" # name argument over the manifest venusian build --dir=../other-app # another app venusian build --no-interaction # defaults, no terminal needed
On macOS, open build/<kebab>-<version>-macos-arm64.dmg and drag the app to Applications, or run build/<Name>.app/Contents/MacOS/<kebab> from a terminal to see its output. Started from Finder, the app's output goes to ~/Library/Logs/<kebab>/<kebab>.log. On Linux:
sudo apt install ./build/<kebab>_<version>_<arch>.deb <kebab>
apt installs the libraries the .deb depends on; a windowed app appears in the desktop's app grid under its name and icon.
build.json
Optional, at the app root, committed with the app. Every key has a default; an unknown key is refused by name. Editors validate it against schema/build.schema.json through the $schema line the build writes.
{
"$schema": "https://raw.githubusercontent.com/VenusianPHP/build/main/schema/build.schema.json",
"id": null,
"name": null,
"version": "0.1.0",
"build": 1,
"sketch": null,
"icon": null,
"summary": "",
"description": "",
"author": "",
"homepage": "",
"license": "",
"category": "Utility",
"permissions": {},
"extensions": [],
"targets": [],
"env": {},
"env_except": []
}
| Key | Meaning |
|---|---|
id |
Reverse-DNS identity. The app's identity is config/app.php app.id (APP_ID); build.json may repeat it and must then match |
name |
Display name; null uses app.name |
version |
Starts with a digit: Debian takes it as the package version |
sketch |
The sketch the packaged app runs; the only one, or asked when several |
icon |
Square PNG relative to the app, at least 16 px; 1024 px serves both: the .deb gets every hicolor size from 16 to 512 the source covers |
summary, description |
One line, and paragraphs separated by blank lines: the package description and the AppStream entry |
author |
Name <email>, the .deb maintainer; required for Linux targets |
homepage, license |
Homepage URL, SPDX identifier |
category |
freedesktop main category: AudioVideo, Development, Education, Game, Graphics, Network, Office, Science, Settings, System, Utility; the .app maps it to an App Store category |
extensions |
Extensions beyond the ext-* that composer.lock declares: a php-src or php-io-extensions name (pdo_sqlite, imgdec), or any php-ext package on Packagist named in full (phpredis/phpredis, pecl/parallel), compiled under the extension name it declares at its newest stable tag. A name found nowhere is refused with the php-ext packages Packagist finds for it |
zts |
Thread-safe PHP (Linux and macOS). Left out, the runtime matches the PHP running the build: zenusian build (ZTS) builds ZTS, venusian build on an NTS PHP builds NTS. An extension that cannot build at that thread safety (parallel is ZTS only) stops the build by name |
build |
Whole number, one more each release: the .app's CFBundleVersion |
permissions |
macOS: camera, microphone, bluetooth, each with the sentence macOS shows when it asks; the .app gets the usage string and the hardened-runtime device entitlement |
targets |
From macos-arm64, linux-arm64, linux-x86_64; [] is the machine running the build |
env, env_except |
Over the app's .env in the packaged app, and .env keys left out (DB_PASSWORD) |
When build.json and app.id disagree the build stops: build.json names {id} but config/app.php resolves app.id to {app_id}; make them match, the toolkit engines get pissy when app ids don't match.
What a build does
- Checks the directory is a Venusian app (
composer.jsonrequiresvenusian/framework,computer,bootstrap/app.php). - Reads the manifest:
build.jsonover the defaults,app.nameandapp.id, sketches underapp/Runner/Sketches, everyext-*incomposer.lock(the packages' and the app's own), the default database connection; ajovian/*toolkit package in the lock makes the app windowed. - Packs
<kebab>.pharonce: the app as git would publish it (tracked and untracked files.gitignoreleaves in, lessexport-ignorein.gitattributes), withoutvendor,storage,tests, caches and.env;composer install --no-devin a stage, each path-repository package replaced by what its own repository publishes, then an authoritative classmap; a.envof the app's.envwithbuild.envover it,build.env_exceptleft out andAPP_IDalways set (a packaged app has no other environment); the stub runsrocket <sketch>, or, named a script inside the phar as its first argument, that script (how the framework's process pools get workers). A packaged app's database is sqlite for now; another default connection stops the build. A sqlite app whose connection namesdatabase/database.sqliteships a fresh one, migrated fromdatabase/migrationsas the build runs; the developer's own file never ships. On first run it is copied to the app's data directory, where it is writable; aDB_DATABASEin the app's.envnames the developer's machine and is left out of the packaged one. - Scans the packed code for calls into PHP extensions (
resources/extension-symbols.json, regenerated withbin/extension-symbols). A call in the app's own code adds its extension and says so:Adding intl: app/Money.php uses NumberFormatter; a sqlite default connection adds pdo_sqlite. Calls invendoronly are reported,Vendor code also calls, not compiled in: …, since a package's guarded call is not a need; add one tobuild.jsonextensionswhen the app needs it. - Hands the phar to every target in
build.targetsthis machine can build, reporting the others with the reason. Each target runs the built binary once, with HOME in a temporary directory, on the packed.venusian-boot-check.php: it boots the app, builds its sketch and opens its database. An app that fails there stops the build with what it said, instead of on a user's first launch.
macOS (macos-arm64, on an Apple Silicon Mac):
- The library set, once per set of pins:
build-env/macos/libs.shbuilds OpenSSL, OpenLDAP's client libraries, Oniguruma, libpng, libjpeg-turbo, libtiff, GLFW, SDL3, libusb and libftdi static, and the Vulkan loader with MoltenVK as dylibs, at the macOS 14 floor, into~/.venusian/build/macos/prefix-14.0-<hash>. Libraries the macOS SDK carries (zlib, libxml2, sqlite3, curl, iconv, bzip2, libffi, libedit, libxslt) are the system's; the system's LDAP is too old for PHP 8.4's ldap, so OpenLDAP is built. build-env/macos/recipe.shcompiles php-src (the newest release of the PHP running the build's minor, 8.4.x for PHP 8.4) with the Venusian SAPI and the extensions compiled in (ctype, filter, mbstring, openssl, pdo, kqueue, pcurl, rasterize and what they need, plus the app's) for macOS 14, and refuses a binary that links anything but the OS and the bundle's Frameworks. The binary is kept per app under~/.venusian/build/macos/runtimes/<kebab>/<set hash>/while the set stays the same. gettext, gmp, intl, pdo_pgsql, pgsql, sodium, tidy and zip have no library on macOS and stop the build by name.<Name>.app: the binary asContents/MacOS/<kebab>, the phar inContents/Resources, the icon,Info.plist(id, name, version, build, category, permissions, macOS 14), and when the app uses Vulkan the loader and MoltenVK inContents/Frameworkswith MoltenVK's driver manifest inContents/Resources/vulkan/icd.d. Signed under the hardened runtime and checked withcodesign --verify --strict --deep.<kebab>-<version>-macos-arm64.dmg: the app beside an Applications link; with a Developer ID it is signed, notarized, stapled and assessed by Gatekeeper.
Linux (linux-arm64, linux-x86_64, on a docker context of that CPU):
- The build image
ghcr.io/venusianphp/build-env:ubuntu24.04-<sha1 of the Dockerfile, 12 hex>is pulled, or built once per host frombuild-env/when the registry has none; a changed Dockerfile is a new image everywhere. - php-src (the newest release of the PHP running the build's minor), the Venusian SAPI v0.10.2 and each extension from Packagist: a plain name is
php-io-extensions/<name>on the 0.10 line (its0.10.x-devbranch, else its newest 0.10 tag), a name with a vendor is that php-ext package at its newest stable tag, are fetched once into~/.venusian/build/sources, an extension's archive per commit. Each extension declares its system packages underextra.venusian.systemin its composer.json. An extension whosephp-extmetadata leaves Linux out (appkit, kqueue) is left out and reported. - Sources, the phar and the recipe go into a docker volume
venusian-build-<kebab>on the host.recipe.shcompiles PHP with the SAPI and the extensions compiled in (ctype, filter, mbstring, openssl, pdo, epoll, pcurl, rasterize and what they need, plus the app's), linked to Ubuntu's libraries, except GLFW 3.4 and SDL3 (24.04 ships GLFW 3.3 and no SDL3) and libjpeg-turbo (Ubuntu'slibjpeg8is not in Debian 13), which the image builds static, and the Vulkan 1.4.309 headers ext-vulkan compiles against; declared apt build packages the image lacks are installed first (and again in the packaging container). The binary carries no RPATH. A volume keeps the compiled runtime while the extension set and every extension's commit stay the same. PHP's own extensions compile in by flag; dba, enchant, odbc, opcache, pdo_dblib, pdo_firebird, pdo_odbc and snmp are not compiled in yet and stop the build by name. package.shlays out/usr/lib/<kebab>/<kebab>and<kebab>.pharbeside it,/usr/bin/<kebab>, and for a windowed app/usr/share/applications/<id>.desktop, the icon rendered to eachhicolorsize and/usr/share/metainfo/<id>.metainfo.xml;dpkg-shlibdepswrites Depends from what the binary links (at64package also accepts its plain name, as Debian 13 ships some), the extensions' declared run-time packages join it, and their recommended ones become Recommends;dpkg-debbuilds<kebab>_<version>_<arch>.deb, which comes back intobuild/.
A first Linux build on a host builds the image, which compiles GLFW, SDL3 and libjpeg-turbo, then compiles PHP: a few minutes. A new extension set recompiles: about a minute on a Ryzen gaming PC, about five on a Jetson Orin Nano. A build of the same set repackages in about half a minute per host.
The packaged app writes under ~/Library/Application Support/<Name> on macOS and $XDG_DATA_HOME/<Name> (else ~/.local/share/<Name>) on Linux: storage, database, bootstrap caches, seeded from the phar on first run. That is framework behaviour (venusian/framework packaged mode), not the build's.
Signing
How macOS builds sign is a fact about your machine, kept in ~/.venusian/build/config.json; the first interactive build asks once:
{"macos": {"sign": "Developer ID Application: Your Name (TEAMID)", "notary_profile": "venusian"}}
"sign": "adhoc"(the default) opens on the Mac that built it; the build says the.dmgis not notarized.- A Developer ID identity from your keychain signs the frameworks, the app and the
.dmgwith a secure timestamp under the hardened runtime, with JIT allowed for PCRE and the device entitlement of each declared permission. The build looks the name up before compiling and signs by the certificate's SHA-1; when a renewed certificate and the one it replaces share the name, the one that expires last signs. A SHA-1 works assigntoo. notary_profilenames a notarytool keychain profile (xcrun notarytool store-credentials venusian); with it the build submits the.dmg, waits for Apple, staples the ticket and checks it as Gatekeeper checks a download. A rejection prints Apple's issues.
The ecosystem
| Package | Role |
|---|---|
venusian/framework |
The runtime: event loop, IOPools, sketches, workflows, queue, database, console |
venusian/surface and venusian-surface/* |
Windows, drawing engines, toolkits, embedded displays |
scrapyard-io/* |
Hardware: GPIO, I2C, SPI, UART, PWM, chip adapters |
jovian/* |
Toolkit and engine drivers (AppKit, GTK, Qt, SDL3, OpenGL, Metal, Vulkan) |
php-io-extensions/* |
The C extensions those drivers bind |
venusian/installer |
venusian new, venusian install:ext, venusian install:sdk |
venusian/build |
This package: venusian build |
Next: Windows, HumanInput, DrawingSurface, TypePHP, Terran.
Testing
composer install vendor/bin/pest
Tests use temporary directories, a fake Mac (SDK path, library set, recipe, otool, codesign, hdiutil, notarytool), a fake Packagist and a fake docker CLI; nothing reaches the network, compiles, runs docker or signs anything. Every target is tested on any machine: the host is passed in.
License
MIT.