Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
1239134
Extract the launcher's subprocess plumbing
emmadesilva Aug 25, 2026
f14e242
Add `hyde php`, the bundled PHP runtime as a command
emmadesilva Aug 25, 2026
c79e7f1
Bundle Composer inside the executable
emmadesilva Aug 25, 2026
c034d76
Add `hyde composer`, the bundled Composer as a command
emmadesilva Aug 25, 2026
584aa06
Show what belongs to the CLI in the command list
emmadesilva Aug 25, 2026
bbbd690
Create Composer projects with the bundled Composer
emmadesilva Aug 25, 2026
1aa0953
Document the bundled programs
emmadesilva Aug 25, 2026
cfebc12
Resolve a relative path to the executable in the acceptance suites
emmadesilva Aug 25, 2026
0e79383
Add ext-session to the runtime
emmadesilva Aug 25, 2026
e42ec5a
Ask for a bundled Composer before reading the manifest for one
emmadesilva Aug 25, 2026
0c4eab0
Do not mistake a global option's value for the command name
emmadesilva Aug 25, 2026
d9da946
Bundle Composer 2.10.2 rather than 2.8.12
emmadesilva Aug 25, 2026
4c1837f
Refuse `hyde composer self-update`
emmadesilva Aug 25, 2026
8cf0aaa
Assert the runtime carries zip and session, on the artifact itself
emmadesilva Aug 25, 2026
4b426ec
Say which command falls back to a host Composer, and which does not
emmadesilva Aug 25, 2026
3f84145
Patch the bundled Composer's curl SSL parser
emmadesilva Aug 25, 2026
7e5bacc
Record and report what the bundled Composer was patched with
emmadesilva Aug 25, 2026
79363e2
Document the Composer patch and where it lives
emmadesilva Aug 25, 2026
e4899af
Clarify test naming
emmadesilva Aug 25, 2026
25ba569
Merge branch 'master' into feature/bundled-runtime-commands
emmadesilva Aug 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,16 @@ These are not preferences. A change that breaks one of them is a bug, whatever i
the embedded framework would compile the site against a different version of Hyde than the
project declares. This is the single most important guarantee in the codebase.
3. **No external PHP in Portable mode.** `RuntimeManager` is the only thing that resolves a
PHP binary, and it never looks at `PATH`. If you need to run PHP, ask it.
4. **No Composer execution during a Portable build.** Composer is invoked in exactly one
place: `hyde new --composer`.
PHP binary, and it never looks at `PATH`. If you need to run PHP, ask it. That includes
`hyde php` and `hyde composer`. In a released executable that is always the embedded
runtime; in a source checkout, which embeds none, it is the PHP process already
running the code, and never one found on the search path.
4. **Composer is never invoked implicitly.** Nothing about building or serving a project
runs it. It runs when the user asks for it and nowhere else: `hyde composer`, and
`hyde new --composer`. Both prefer the Composer bundled in the executable, and they
differ where none is bundled: `hyde new --composer` falls back to the host's, since
it needs *a* Composer to create a project, while `hyde composer` fails — it means
"the Composer Hyde supplies", and in a source checkout the developer has their own.
5. **No mixing of dependency graphs.** The embedded `vendor/` and a project's `vendor/` never
share a process. Composer projects are dispatched into a separate process.
6. **Detection and dispatch run before the autoloader.** The `app/Launcher` classes are
Expand All @@ -33,10 +40,10 @@ These are not preferences. A change that breaks one of them is a bug, whatever i
| `hyde` | The console entry point. Detection and dispatch happen here, first. |
| `app/Launcher/` | The project model, runtime management and dispatch. Plain PHP, no framework. |
| `app/Foundation/` | Overrides that let the framework boot out of a read-only executable. |
| `app/Commands/` | The commands the executable owns: `info`, `new`, `serve`, `self-update`. |
| `app/Commands/` | The commands the executable owns: `info`, `new`, `serve`, `self-update`, and the bundled programs `php` and `composer`. |
| `app/Support/` | Small helpers with no framework dependencies. |
| `bin/` | The build scripts. `build-native.sh` and `build-native.ps1` drive static-php-cli; `build-phar.php` assembles the executable. |
| `build/runtime.json` | The single build configuration: pinned PHP version and the extension set, with a reason for each. |
| `bin/` | The build scripts. `build-native.sh` and `build-native.ps1` drive static-php-cli; `build-phar.php` assembles the executable; `lib/composer-patches.php` is what the bundled Composer is patched with, and why. |
| `build/runtime.json` | The single build configuration: pinned PHP version, the extension set with a reason for each, and the pinned Composer release with its checksum. |
| `tests/System/` | Runtime acceptance in POSIX shell and PowerShell, for hosts with no PHP. |

## Testing
Expand Down Expand Up @@ -82,5 +89,4 @@ framework requirement or to a test that proves it necessary.
## Things that are deliberately not implemented

- `hyde eject`, and Portable to Composer conversion.
- Bundling Composer inside the executable.
- Hybrid autoloading, or loading Composer addons into a Portable project.
20 changes: 13 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,16 @@ These are not preferences. A change that breaks one of them is a bug, whatever i
the embedded framework would compile the site against a different version of Hyde than the
project declares. This is the single most important guarantee in the codebase.
3. **No external PHP in Portable mode.** `RuntimeManager` is the only thing that resolves a
PHP binary, and it never looks at `PATH`. If you need to run PHP, ask it.
4. **No Composer execution during a Portable build.** Composer is invoked in exactly one
place: `hyde new --composer`.
PHP binary, and it never looks at `PATH`. If you need to run PHP, ask it. That includes
`hyde php` and `hyde composer`. In a released executable that is always the embedded
runtime; in a source checkout, which embeds none, it is the PHP process already
running the code, and never one found on the search path.
4. **Composer is never invoked implicitly.** Nothing about building or serving a project
runs it. It runs when the user asks for it and nowhere else: `hyde composer`, and
`hyde new --composer`. Both prefer the Composer bundled in the executable, and they
differ where none is bundled: `hyde new --composer` falls back to the host's, since
it needs *a* Composer to create a project, while `hyde composer` fails — it means
"the Composer Hyde supplies", and in a source checkout the developer has their own.
5. **No mixing of dependency graphs.** The embedded `vendor/` and a project's `vendor/` never
share a process. Composer projects are dispatched into a separate process.
6. **Detection and dispatch run before the autoloader.** The `app/Launcher` classes are
Expand All @@ -33,10 +40,10 @@ These are not preferences. A change that breaks one of them is a bug, whatever i
| `hyde` | The console entry point. Detection and dispatch happen here, first. |
| `app/Launcher/` | The project model, runtime management and dispatch. Plain PHP, no framework. |
| `app/Foundation/` | Overrides that let the framework boot out of a read-only executable. |
| `app/Commands/` | The commands the executable owns: `info`, `new`, `serve`, `self-update`. |
| `app/Commands/` | The commands the executable owns: `info`, `new`, `serve`, `self-update`, and the bundled programs `php` and `composer`. |
| `app/Support/` | Small helpers with no framework dependencies. |
| `bin/` | The build scripts. `build-native.sh` and `build-native.ps1` drive static-php-cli; `build-phar.php` assembles the executable. |
| `build/runtime.json` | The single build configuration: pinned PHP version and the extension set, with a reason for each. |
| `bin/` | The build scripts. `build-native.sh` and `build-native.ps1` drive static-php-cli; `build-phar.php` assembles the executable; `lib/composer-patches.php` is what the bundled Composer is patched with, and why. |
| `build/runtime.json` | The single build configuration: pinned PHP version, the extension set with a reason for each, and the pinned Composer release with its checksum. |
| `tests/System/` | Runtime acceptance in POSIX shell and PowerShell, for hosts with no PHP. |

## Testing
Expand Down Expand Up @@ -82,7 +89,6 @@ framework requirement or to a test that proves it necessary.
## Things that are deliberately not implemented

- `hyde eject`, and Portable to Composer conversion.
- Bundling Composer inside the executable.
- Hybrid autoloading, or loading Composer addons into a Portable project.

<!-- This file is a copy of AGENTS.md, kept so both conventions find the same guidance. -->
40 changes: 35 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@

The HydePHP CLI is a single-file executable for the static site generator HydePHP.

It carries its own PHP runtime and its own copy of the framework, so you can build a site on a
machine with **no PHP and no Composer installed**. Point it at a directory of Markdown files
and it will build a site; point it at a full HydePHP Composer project and it will run that
project through its own dependencies.
It carries its own PHP runtime, its own copy of the framework, and its own Composer, so you can
build a site on a machine with **no PHP and no Composer installed**. Point it at a directory of
Markdown files and it will build a site; point it at a full HydePHP Composer project and it
will run that project through its own dependencies.

The runtime it carries is not kept to itself: `hyde php` and `hyde composer` hand it to you.

## The two kinds of project

Expand Down Expand Up @@ -106,7 +108,7 @@ hyde info
# Create a new site
hyde new my-site # asks which kind you want
hyde new my-site --portable # content only, nothing to install
hyde new my-site --composer # a full Composer project (requires Composer)
hyde new my-site --composer # a full Composer project, created with the bundled Composer

# Build a site using source files in the working directory
hyde build
Expand All @@ -115,6 +117,34 @@ hyde build
hyde serve
```

### The bundled runtime

The PHP and the Composer inside the executable are available as commands of their own, so a
machine that has neither still has both:

```bash
hyde php -v # the bundled PHP CLI
hyde php script.php
hyde php -r 'echo PHP_VERSION;'

hyde composer install # the bundled Composer, on the bundled PHP
hyde composer require hyde/framework
```

Arguments and exit statuses pass through untouched. This is *Hyde's* PHP rather than a general
distribution: its extensions are the ones Hyde needs, listed with their reasons in
[`build/runtime.json`](build/runtime.json), so a script that needs something outside that set
will say so.

`hyde composer` is answered before the project is even looked at, which is deliberate: a
Composer project with a missing `vendor/` is the one state the CLI refuses to build, and
`hyde composer install` is what repairs it.

Both commands mean *the programs Hyde supplies*, so neither falls back to one installed on
the machine. `hyde composer self-update` is refused for the same reason: the bundled
Composer is versioned with the executable and verified on every run, so `hyde self-update`
is what gets you a newer one.

## Resources

### Changelog
Expand Down
29 changes: 29 additions & 0 deletions app/Commands/ComposerCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace App\Commands;

use App\Commands\Internal\BundledProgramCommand;

/**
* Runs the Composer bundled inside the executable, on the bundled PHP runtime.
*
* This is what closes the last gap in the "no PHP, no Composer" promise. The CLI
* could already run a Composer project on a machine with neither; now it can
* install one's dependencies there too:
*
* hyde composer install
*
* It is answered before the project is detected, which is deliberate — a Composer
* project with no `vendor/` is precisely the state the launcher refuses to run
* anything else in, and this is the command that repairs it.
*/
class ComposerCommand extends BundledProgramCommand
{
/** @var string */
protected $signature = 'composer {args?* : The arguments to pass to Composer}';

/** @var string */
protected $description = 'Run the bundled Composer.';
}
23 changes: 23 additions & 0 deletions app/Commands/InfoCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,33 @@ protected function printCompatibilityDetails(): void
$this->line('<info>Runtime:</info> '.($runtime->hasEmbeddedRuntime()
? sprintf('PHP %s bundled for %s', $runtime->manifest()['version'], $runtime->manifest()['platform'])
: 'none bundled (running from a source checkout)'));
$this->line('<info>Composer:</info> '.$this->composer($runtime));
$this->line('<info>Extensions:</info> '.implode(', ', $this->loadedExtensions()));
$this->newLine();
}

/**
* The bundled Composer, and whether this executable modified it.
*
* The CLI distributes a package manager, so what it distributes is reported rather
* than left to be discovered: a build that carries a patch against the published
* archive says which one, and why is in `bin/lib/composer-patches.php`.
*/
protected function composer(RuntimeManager $runtime): string
{
$version = $runtime->composerVersion();

if ($version === null) {
return 'none bundled (running from a source checkout)';
}

$patches = $runtime->composerPatches();

return $patches === []
? sprintf('%s bundled', $version)
: sprintf('%s bundled, patched (%s)', $version, implode(', ', $patches));
}

/** @return list<string> */
protected function loadedExtensions(): array
{
Expand Down
44 changes: 44 additions & 0 deletions app/Commands/Internal/BundledProgramCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

namespace App\Commands\Internal;

use App\Launcher\Launcher;
use App\Launcher\RuntimeManager;
use App\Launcher\RuntimeDispatcher;
use Illuminate\Console\Command;

/**
* @internal A command that hands control to one of the programs bundled in the executable.
*
* The work is done by {@see \App\Launcher\RuntimeDispatcher}, which the launcher calls
* before the application is booted at all: that is the only way arguments can reach
* the program exactly as they were typed, since a console application would claim
* `-v`, `--version` and `--help` for itself long before a command ran.
*
* These classes exist so the CLI can describe what it bundles — `hyde list` and
* `hyde help php` need something to describe — and so the commands still work
* if the application is booted directly, without the launcher in front of it.
* They read the raw arguments rather than parsed ones for the same reason.
*/
abstract class BundledProgramCommand extends Command
{
public function __construct()
{
parent::__construct();

// The options being forwarded are the program's, and are none of our business.
$this->ignoreValidationErrors();
}

public function handle(): int
{
return $this->dispatcher()->run((string) $this->getName(), (new Launcher())->argumentsFor($_SERVER['argv'] ?? []));
}

protected function dispatcher(): RuntimeDispatcher
{
return new RuntimeDispatcher($this->laravel->make(RuntimeManager::class));
}
}
Loading
Loading