Skip to content

Add platform:build for single-executable binaries - #212

Open
pierredup wants to merge 24 commits into
mainfrom
app-build
Open

pierredup wants to merge 24 commits into
mainfrom
app-build

Conversation

@pierredup

Copy link
Copy Markdown
Member

Adds bin/console platform:build (alias platform:compile), which compiles any application built on the platform into a single static executable — a FrankenPHP/Caddy web server, the Symfony app embedded as a tarball, and messenger consumers, in one binary.

The capability previously lived in SolidInvoice as four bash scripts plus a Go program, all wired to that one application. It now lives here, reusable by any platform app.

How it works

PHP owns configuration, preflight checks, archiving and orchestration. The Go program and build-static.sh ship verbatim in src/Bundle/Platform/Resources/build/ and are never rewritten — application identity reaches the binary through go build -ldflags -X, applied by a shell shim that impersonates xcaddy. That keeps the upstream build script byte-identical and re-syncable (verified: it still diffs clean against upstream).

Configuration lives under platform.build; see docs/build/index.md.

Scope

Builds are host-onlybuild-static.sh has no cross-compile path, so there is no --os/--arch. The archive is the checkout as it stands: the command verifies and warns, but does not run composer install or build assets.

Note on scope of this branch

This branch also carries 7 commits of tenant-scope work from an earlier session that were never pushed. They are included here rather than split out.

Not yet done

No real build has been run. A first build downloads and compiles PHP and takes 30–60 minutes. Before merging, one end-to-end run is worth doing, checking what the binary's version command prints — the Go linker silently ignores an -X flag with an unknown target symbol, so wrong ldflags wiring yields a working binary with wrong branding that an exit-code check cannot see.

Known risks for that first run are recorded in the branch history: static-php-cli receives --with-frankenphp-app unconditionally (hence EMBED being a bounded extracted copy rather than the project root), and the default extension set is larger than documented — see below.

Upstream bug found

build-static.sh gates composer-derived extensions on ${EMBED}/vendor/installed.json, but Composer 2 writes vendor/composer/installed.json. That branch is therefore dead and every build falls back to the script's ~69-extension default list. The script stays byte-identical by design, so this is documented rather than patched — see src/Bundle/Platform/Resources/build/UPSTREAM.md.

Also in this branch

  • Fixes a container-compile failure where TenantCreationVoter injected TenantLock while only the latter was removed with multi-tenancy disabled — the default. Any app without multi-tenancy could not boot.
  • Reverts PlatformConfigBuilder::build()toArray(), a BC break that should not have happened; the build section accessor is binaryBuild() instead.
  • Skips three Rector rules that produced broken or behaviour-changing code in this repo.

Verification

595 tests / 1661 assertions · PHPStan max clean · ECS + Rector clean · gofmt clean, Go internal tests pass.

Adds the platform.build config tree (identity, PHP build
settings, exclude paths, boot hooks) that the platform:build
command reads in later tasks, plus a BuildConfigBuilder for the
PHP config format.

Renames PlatformConfigBuilder::build() to toArray() so build()
can open the new section, matching security() and the other
section accessors; updates every caller and documents the break
in UPGRADE.md.
TenantCreationVoter injects TenantLock, which MULTI_TENANCY_SERVICES
already removes when multi-tenancy is disabled (the default). The
voter itself was missing from that list, so the container kept a
service whose argument no longer existed and failed to compile.

Also ignore the throwaway test-app/ directory created by
scripts/test-app.sh.
Copilot AI lite review requested due to automatic review settings September 18, 2026 05:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Several correctness issues affect command registration, runtime paths, environment handling, and documented application hooks.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds reusable single-executable FrankenPHP builds for platform applications, including configuration, archiving, static compilation, embedded app runtime, documentation, and related tenancy/container fixes.

Changes:

  • Adds platform:build/platform:compile and build configuration.
  • Adds Go/FrankenPHP build assets, preflight checks, archiving, and runtime support.
  • Updates schema, documentation, dependencies, tests, and Rector configuration.
File summaries
File Description
tests/Bundle/PlatformBundle/DependencyInjection/SolidWorxPlatformExtensionTest.php Build configuration and tenancy tests
tests/Bundle/PlatformBundle/Console/CommandTest.php Command IO lifecycle tests
tests/Bundle/PlatformBundle/Config/PlatformConfigurationTest.php Build defaults validation
tests/Bundle/PlatformBundle/Config/Builder/BuildConfigBuilderTest.php Fluent build configuration tests
tests/Bundle/PlatformBundle/Command/BuildCommandTest.php Build command behavior tests
tests/Bundle/PlatformBundle/Build/VersionResolverTest.php Version resolution tests
tests/Bundle/PlatformBundle/Build/StaticBuilderTest.php Static builder tests
tests/Bundle/PlatformBundle/Build/PreflightTest.php Preflight validation tests
tests/Bundle/PlatformBundle/Build/BuildOptionsTest.php Build option resolution tests
tests/Bundle/PlatformBundle/Build/AppArchiverTest.php Archive behavior tests
src/Bundle/Platform/Resources/config/services.php Registers build services
src/Bundle/Platform/Resources/build/xcaddy FrankenPHP build shim
src/Bundle/Platform/Resources/build/UPSTREAM.md Upstream synchronization notes
src/Bundle/Platform/Resources/build/untar.go Embedded archive extraction
src/Bundle/Platform/Resources/build/logger.go Caddy logger adapter
src/Bundle/Platform/Resources/build/internal/serverconfig/server_name.go Server-name construction
src/Bundle/Platform/Resources/build/internal/serverconfig/server_name_test.go Server-name tests
src/Bundle/Platform/Resources/build/go.mod Go build dependencies
src/Bundle/Platform/Resources/build/build-static.sh Vendored static build script
src/Bundle/Platform/Resources/build/app.go Embedded application runtime
src/Bundle/Platform/DependencyInjection/SolidWorxPlatformExtension.php Build parameter resolution and tenancy fix
src/Bundle/Platform/Console/Command.php Corrects command IO initialization
src/Bundle/Platform/Config/PlatformConfiguration.php Adds build configuration tree
src/Bundle/Platform/Config/Builder/PlatformConfigBuilder.php Adds binary build builder entry point
src/Bundle/Platform/Config/Builder/BuildConfigBuilder.php Fluent build configuration builder
src/Bundle/Platform/Command/BuildCommand.php Build command orchestration
src/Bundle/Platform/Build/VersionResolver.php Resolves binary versions
src/Bundle/Platform/Build/StaticBuilder.php Runs static compilation
src/Bundle/Platform/Build/Severity.php Preflight severity enum
src/Bundle/Platform/Build/Problem.php Preflight problem model
src/Bundle/Platform/Build/Preflight.php Tool and application checks
src/Bundle/Platform/Build/BuildStepFailedException.php Build failure exception
src/Bundle/Platform/Build/BuildOptions.php Resolved build options
src/Bundle/Platform/Build/Archive.php Archive metadata model
src/Bundle/Platform/Build/AppArchiver.php Application tarball creation
rector.php Excludes unsafe Rector rules
platform-schema.json Adds build schema
docs/index.md Documents build section
docs/configuration/index.md Documents build configuration
docs/build/index.md Adds build guide
composer.json Adds filesystem/process dependencies
.gitignore Ignores build artifacts
Review details
  • Files reviewed: 41/43 changed files
  • Comments generated: 8
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

use function str_contains;

#[AsCommand(
name: 'platform:build|platform:compile',

// Only set if not already set
for key, value := range envVars {
if os.Getenv(key) == "" {
// phpincludedir above also points into source. Upstream's cleanup assumes a single run;
// left inherited from a real CI environment (our own documented CI usage sets one), the
// second run would fail. RELEASE is neutralised the same way, for the same reason.
$environment['CI'] = '';

$filesystem = new Filesystem();
$filesystem->mkdir($options->outputDir);
$filesystem->rename($produced, $destination, overwrite: true);
if command == "" {
continue
}
if err := runConsoleCommand(command); err != nil {
runCmd.PersistentFlags().StringVar(&sslKeyFile, "ssl-key", "", "Path to custom SSL private key file (requires --ssl-cert and --domain)")
runCmd.PersistentFlags().BoolVar(&enableWorkerMode, "worker-mode", false, "Enable FrankenPHP worker mode for improved performance (keeps PHP workers alive between requests). Recommended for SaaS/high-traffic deployments. Can also be enabled via FRANKENPHP_WORKER_MODE=1 environment variable")
runCmd.PersistentFlags().IntVar(&workerThreads, "worker-threads", 2, "Number of FrankenPHP worker threads when worker mode is enabled (default: 2)")
runCmd.PersistentFlags().IntVar(&messengerWorkers, "messenger-workers", 1, fmt.Sprintf("Number of messenger worker processes to spawn. Each worker processes async messages independently. Set to 0 to disable built-in workers entirely (recommended for Kubernetes, where a dedicated worker pod runs '%s worker'). Increase above 1 for high-traffic standalone deployments (e.g., --messenger-workers=5)", filepath.Base(os.Args[0])))

if [ "${CURRENT_OS}" = "linux" ]; then
# musl static build: GNU ld wants a dynamic symbol list and a static PIE.
EXTLDFLAGS="-static-pie -Wl,-z,stack-size=0x80000 -Wl,--dynamic-list=${DYNSYM}"
var urls string
var domainNote string

if len(domain) > 0 {
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants