| Age | Commit message (Collapse) | Author |
|
Output buffering captures whatever the PHP interpreter would echo to
stdout. The shim routes no output through a buffer, so these functions
could never capture anything and stayed todo!(). Record the gap in
docs/known-incompatibilities.md instead.
|
|
Composer's Composer\Util\TlsHelper is marked deprecated for removal in
Composer 3.0 and has no caller in composer/composer outside its own
test: PHP's stream layer verifies certificate hostnames itself, and the
one surviving method delegates to composer/ca-bundle.
The Rust port had no caller either, so it, its test, and the
openssl_x509_parse shim it was the sole user of are removed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
`Application::do_run` registers a `composer.json` script whose value names a
`Symfony\Component\Console\Command\Command` subclass as a live command. The
class checks and `new $dummy($script)` need a real PHP runtime, so they run in
the worker: the command object lives there and this side keeps a metadata
mirror for `list`/`help`, forwarding a run to the worker-side console
application it is added to. The name and description fixups are applied to the
worker-side object, so both sides carry the same values.
Loading the Composer PHP runtime into the worker is gated on the Rust-side
`ClassLoader`s resolving the class to a file, keeping that load out of every
run whose scripts are plain shell commands.
The worker-side console application handoff now accepts commands registered
after it was published, since the scripts scan runs after plugin commands are
collected. Whether it was published is tracked per application: the handoff is
process-wide, so a second application must replace it rather than extend it.
`shirabe_php_shim::is_subclass_of` has no callers left.
|
|
Every parent module in the symfony-* crates already re-exported its leaf
modules with `pub use`, so each item was reachable by two paths. Make the
leaf modules private and route all callers through the single re-exported
path.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
Composer restarts itself with Xdebug unloaded because Xdebug makes PHP
several times slower. Xdebug is never loaded into this process, so what
needs dealing with is the PHP worker: it is spawned with
`-d xdebug.mode=off` and `XDEBUG_MODE=off` (Xdebug reads the environment
variable first and lets it override every ini setting), which makes its
module init return before it installs any executor, compile, error or
opcode hook.
Rewriting the ini files and re-executing, the way xdebug-handler does,
would additionally cover Xdebug 2, which hooks unconditionally and has no
equivalent setting. That is not worth its machinery here: Xdebug 2 caps
out at PHP 7.4, while every PHP version Composer supports can run
Xdebug 3.
What remains of XdebugHandler is small enough to live beside the worker
it governs, so its crate is gone and its callers inline it. isXdebugActive
answers false without asking PHP whenever the worker is switched off, so a
command that needs no PHP does not spawn one just for the Xdebug warning;
diagnose reports what the worker measures instead, which still surfaces an
Xdebug that ignores the setting. PlatformRepository has no unloaded
extension to restore, since switching the mode off leaves it loaded.
COMPOSER_ORIGINAL_INIS is neither written nor read: it exists so a
restarted process can name the ini files it replaced, and IniHelper can
report the worker's own. IniHelperTest injects through that variable, so
none of its cases are ported.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
PluginManager::getPluginCapability hands the plugin itself to the
capability constructor. Only a PHP-implemented plugin had an entity the
child could receive, so a Rust-implemented one bailed out; it now crosses
as a handle to an R-table entity behind Shirabe\RustPluginStub, or its
Capable flavour, since `$plugin instanceof Capable` is what decides
whether Composer asks a plugin for capabilities at all.
This is what the two capability tests of PluginInstallerTest were waiting
on: the mocked Capable plugin is Rust-side, and one of them asserts the
identity of the plugin read back out of $capability->args.
|
|
doRemove() renames a directory to a random hidden name before emptying
it, and undoes that rename when the final rmdir fails, so a concurrent
process cannot recreate the path mid-removal. It also walks one level at
a time through FilesystemIterator instead of flattening the whole tree,
and lets an inner rmdir failure pass, both as upstream does.
copy() keeps the mode fopen($targetFile, 'w') would have left rather
than the origin's, symlink() and mirror() call readlink() and
getLinkTarget() where upstream does, and a directory iterator that
cannot be opened propagates its UnexpectedValueException instead of
being swallowed or flattened into an IOException.
Error message text is out of scope per docs/known-incompatibilities.md,
so the three TODO(phase-c) markers that only tracked wording are gone,
along with linkException()'s Windows-only branch. So are the arguments
no caller varies -- symlink()'s copyOnWindows, mirror()'s iterator and
options, copy()'s overwriteNewerFiles -- which removes the last
TODO(phase-c) in the file.
New shim functions: readlink, filesystem_iterator, stream_is_local,
strrev and SplFileInfo::getLinkTarget. base64_encode takes bytes so
random_bytes() can feed it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
shirabe-seld-json-lint crate
Move `Seld\JsonLint` out of shirabe-external-packages and into its own
crate, so the path is `shirabe_seld_json_lint::ParsingException` instead
of `shirabe_external_packages::seld::json_lint::ParsingException`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
shirabe-symfony-console crate
Move `Symfony\Component\Console` out of shirabe-external-packages and
into its own crate, so the path is
`shirabe_symfony_console::application::Application` instead of
`shirabe_external_packages::symfony::console::application::Application`.
The `delegate_to_inner!` and `delegate_command_trait_impls_to_inner!`
macros move with it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
shirabe-symfony-process crate
Move `Symfony\Component\Process` out of shirabe-external-packages and
into its own crate, so the path is `shirabe_symfony_process::Process`
instead of `shirabe_external_packages::symfony::process::Process`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
Move `Composer\Pcre` out of shirabe-external-packages and into its own
crate, so the path is `shirabe_pcre::preg::Preg` instead of
`shirabe_external_packages::composer::pcre::preg::Preg`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
mkdir, rmdir, unlink and symlink each had a bool version and a _result
twin returning the io::Error, which left two names for one call. Keep
only the Result form and let the callers that want a boolean spell out
.is_ok(). Call sites that discard the outcome, as their PHP originals
do, are unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
InstalledVersions is a runtime API for plugins and project code; Composer
itself never reads it. Its consumers run in the PHP worker against the copy
FilesystemRepository dumps to vendor/composer/InstalledVersions.php, whose
static state is already kept in sync by __shirabe_installed_versions_reload.
Nothing in Rust read the mirrored statics, so reload() and the reflection
setters were no-ops.
The tests covered only the Rust port, not the PHP class the worker loads, so
they assert nothing about compatibility; they are left as todo!() skeletons.
This also removes the shim functions method_exists, php_dir and
require_php_file, whose only caller was the deleted module.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
shirabe never runs on HHVM, so the HHVM_VERSION_ID branches are dead.
Dropping them leaves the shim's unimplemented `constant()` without a
caller, so remove it as well.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
The shim's PATH_SEPARATOR was hardcoded to ":", so every PATH list was
split and joined on the wrong character off Unix. std::env::split_paths
and join_paths use the platform's separator, which also lets the bin-dir
membership test in EventDispatcher compare list entries instead of
regex-matching the raw PATH string.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
Concatenating a directory, MAIN_SEPARATOR and a component reimplements
what Path::join already does. Joining directly also drops the separator
from the intermediate values it used to be baked into, such as
ClassLoader's $pathEnd.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
MAIN_SEPARATOR is the path separator; PHP's `'\\' === DIRECTORY_SEPARATOR`
uses it as an OS test only because PHP has no dedicated one. cfg!(windows)
says what the branch actually selects on, and leaves MAIN_SEPARATOR to the
sites that really join or split paths.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
The shim's DIRECTORY_SEPARATOR was hardcoded to "/", so every ported
`'\\' === DIRECTORY_SEPARATOR` check compared against a constant that
does not track the target platform. std::path::MAIN_SEPARATOR and
MAIN_SEPARATOR_STR carry the same meaning as PHP's constant and resolve
per platform, so the Windows branches are selected on Windows targets.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
The deadline is a constant baked in at build time, so PHP's define() of
COMPOSER_DEV_WARNING_TIME cannot make Application take that branch.
Application now holds the deadline in a field that __set_dev_warning_time
overrides, letting testDevWarning run instead of staying ignored. The
define() shim, whose only caller was that test, goes away with it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
Ported exceptions were flat structs reached with `downcast_ref`, so
Composer's `catch (\RuntimeException $e)` only matched the exact leaf
type and `get_class($e)` had nothing to report. Each exception now
embeds an instance of the class it extends and travels inside an
`AnyThrowable`; `Catch::catch`/`catch_mut` walk that chain, and
`PhpClass::php_class_name` yields the PHP FQCN.
Dropping the `std::error::Error` impls from the exception types leaves
`AnyThrowable` as the only route into an `anyhow::Error`, so the walk
cannot be bypassed. A `no_exception_downcast` linter catches the
`downcast::<X>()` calls that would now silently answer `None`.
Three sites change behavior as a result: the `TransportException`
exit-code override reaches `MaxFileSizeExceededException`, the
`catch (\LogicException)` in findSimilar() reaches its subclasses, and
rendered exception titles carry the real class name rather than a
guess. `get_class_err()` is no longer a `todo!()`, which re-enables
FilesystemRepositoryTest::testCorruptedRepositoryFile.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
Composer defines COMPOSER_DEV_WARNING_TIME from its phar stub when the
compiled version is a commit hash rather than a tag, so the value exists
only in the build artifact. It was a todo!() in the PHP shim, leaving the
warning branch in Application::do_run unreachable.
A build script now derives it the way Compiler does, from git describe
and the HEAD commit date, and composer::COMPOSER_DEV_WARNING_TIME holds
the result as a Rust constant instead of a runtime-defined one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
InstallationManager left both PRE_PACKAGE_* and POST_PACKAGE_* as empty
stubs, so a subscriber never ran at all and the difference from upstream was
silent rather than an explicit error.
Operations cross the boundary as R-table entities with generated proxy
stubs. Materializing them the way a Link crosses is not possible: a
materialized value is revived by unserialize() on the child side, so its
properties never pass through the wire decoder and a nested handle
descriptor would not come back as a stub — and an operation always holds a
PackageInterface. execute() now shares one Rc per operation through the
whole batch pipeline, so a plugin sees one object for both the pre- and the
post-event of an operation, as it does in PHP.
POST_PACKAGE_* also moves out of the operation's promise chain into the
post-exec callback list PHP runs after waitOnPromises().
The stub generator materializes non-public class constants verbatim now,
which the operation classes need for their `protected const TYPE`: a
constant has no entity behind it, so a copy in the worker cannot diverge,
and keeping the declared visibility exposes nothing the real class hides.
The E2E fixture added here compares the recorded events against upstream
Composer. It also surfaced that upstream starts an operation's chain where
it is built (a null prepare() becomes an already-fulfilled React promise
whose handlers run through the immediately drained queue) while this port
only drives its futures in wait_on_promises, so the repository state a
pre-event observes differs; that half of the comparison is a separate
`#[ignore]`d test.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
AutoloadGenerator::dump dispatches PRE_AUTOLOAD_DUMP / POST_AUTOLOAD_DUMP, and
EventDispatcher::make_autoloader answers those by walking back into the Composer
graph -- the local repository, the installation manager, and the generator
itself. Installer::run held mutable borrows of all three across the dump call,
so any plugin subscribing to either event panicked with "RefCell already
borrowed" before its listener ran.
dump() and build_package_map() now take the local repository, the installation
manager and the locker as shared handles instead of &mut dyn, and the borrow is
taken where it is used.
The generator itself is re-entered, not just borrowed twice: PHP listeners reach
it through Composer::getAutoloadGenerator() and call setDevMode() while dump()
is running, and make_autoloader asks the same object for a package map. That is
not expressible behind &mut self, so the mutable state moves into Cell/RefCell
fields and the whole AutoloadGeneratorInterface takes &self.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
AliasPackage, CompleteAliasPackage and RootAliasPackage now have generated
proxy stubs, so a package handed to a plugin no longer has to be a real
package: it crosses as the stub matching its concrete variant, answers
getAliasOf / setRootPackageAlias / isRootPackageAlias /
hasSelfVersionRequires, and can be constructed from plugin code.
The setters RootPackageInterface declares are routed through that interface
for every root package instead of through the base Package state. Only the
alias variant needs it -- RootAliasPackage overrides all nine to write
through to the package it aliases -- but a real RootPackage delegates to the
same base state either way, so both take one path.
An alias of an alias has no representation here, so narrowing the
constructor argument to a real package is an explicit error rather than a
silent demotion.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
The RuntimeInterface seam asked the worker one question at a time: a round
trip per loaded extension, per ReflectionExtension::info() output and per
constant, so a single `show --platform` cost 70 to 100 of them. A `platform`
dispatch entry now answers all of it as one PHP array, which shirabe-php-rpc
decodes into a OnceLock-cached PlatformInfo, the way the diagnose command
already works.
Composer\Platform\Runtime therefore has no Rust counterpart any more. Its
work belongs to the running interpreter, and invoke()/construct() could only
be ported as a whitelist that panicked on anything unlisted; it is ported as
PHP into the worker instead, and PlatformRepository reads the answers off
PlatformInfo. Accessors panic on a name the payload does not carry, so the
worker and its consumers cannot drift apart unnoticed.
The tests describe the runtime as payload data where they used to mock the
seam, with the datasets unchanged. The one loss is the call-count assertion
of test_inet_pton_regression: the payload reports the result of
`@inet_pton('::')` rather than answering a call.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
|
|
Replace the todo!() bodies with real ports. Four autoload-generator
tests now run for real; the rest stay #[ignore]d, but each ignore reason
now names the concrete missing symbol instead of a vague subsystem.
Production additions the ports need: the deprecated
AuthHelper::addAuthenticationHeader wrapper,
EventDispatcher::__set_dispatch_script_override as the seam for PHPUnit
onlyMethods(['dispatchScript']), and a define() stub in the shim.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
PlatformRepository probes ResourceBundle, IntlChar and Imagick to derive
lib-icu-cldr, lib-icu-unicode and lib-imagick-imagemagick. Those probes ran
against the shim's hard-coded class_exists allowlist, which never names
them, so the packages were silently missing: on a machine with intl,
`show --platform` listed fewer libraries than upstream Composer does.
The runtime seam now asks the real PHP: hasClass over RPC, and construct /
invoke through the worker for the three classes PlatformRepository reaches.
A live PHP object has no PhpMixed counterpart, so the seam answers with the
entries the caller reads off it. The seam's own callers read those entries
instead of returning null and the empty string.
Two addLibrary calls also had replaces and provides swapped, dropping
`lib-libxslt replaces lib-xsl` and `lib-zip-libzip replaces lib-zip`.
`show --platform` now matches upstream Composer byte for byte, and all 59
provideLibraryTestCases datasets pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
getDownloaderForPackage reports get_class($downloader) when the resolved
downloader's installation source does not match. Rust has no runtime class
name, so the message was built from a shim stub that panicked instead —
the error could never be returned.
DownloaderInterface now requires PhpClass, the trait already used for the
same purpose on Command, and each downloader states the name PHP reports.
That leaves get_class_obj without callers, so it is gone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
The reason string blamed shirabe_php_shim::runtime::constant(), which
PlatformRepository stopped reaching once its constant lookups went through
the RuntimeInterface seam. All four pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
The link getters and setters and the release date accessors were explicit
errors for every non-empty value, because an immutable value has no entity
to point a handle at. They now cross as materialized values: the descriptor
names the real class and the constructor arguments, and each side builds a
genuine instance of its own.
The semver constraint a link holds is encoded structurally rather than
re-parsed from its string form, so the pretty strings and the conjunctive
flag survive the crossing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
The ported docblocks copied @param and @return straight from the PHP
source. When such a tag carries nothing but a type and an argument name,
the Rust signature already states it, so the line is noise. Tags whose
text adds prose beyond the type are kept.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
The shim's filesystem entry points took `&str` even though each one resolves
to a local path through `std::fs` or a syscall, so callers holding a `PathBuf`
had to stringify it at the call site. They now take `impl AsRef<Path>`, the
form `file_exists`, `is_dir`, `unlink` and the rest of the already-converted
set use.
`Phar`, `PharData` and `ZipArchive` keep their archive path as a `PathBuf`.
`PharData::compress` names the compressed sibling by appending the suffix to
the file name rather than formatting the path into a `String`.
Arguments PHP resolves through a stream wrapper (`fopen`, `file_put_contents`,
`include`) still take `&str`, as do the byte-string operations (`dirname`,
`basename`, `pathinfo`) and archive-internal entry names, which are `/`-joined
logical names rather than OS paths.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
A single install leaves the installer contract half tested: it never reaches
`update`, never runs over an already-installed tree, and never reaches the
plugin's own `uninstall()` override — the one chaining onto the promise
`LibraryInstaller::uninstall` returns. Nor does it touch the configuration
surface real projects use, `installer-paths` and `installer-name`, which the
plugin reads back through the package proxy.
All three now compare command output as well as the resulting tree against
upstream Composer. Progress bar frames are dropped from that comparison:
Shirabe renders them differently for every install, including projects with
no plugin at all, so they say nothing about the plugin under test.
With those covered the plugin joins the verified list in the README.
|
|
The fixture project pins composer/installers 2.3.0 and requires two packages
of framework-specific types, so the plugin's LibraryInstaller subclass decides
where they land. Upstream Composer and Shirabe install the same project and
the whole resulting tree is compared.
The plugin tarball is fetched by `fixtures/e2e-installers/fetch` into a
git-ignored directory and pinned by a digest over the extracted files, so the
third-party source never enters this repository and the test skips itself
while the directory is absent. The tree is staged and only moved into place
once verified, so an unverified tree is never observable under the name the
test looks for.
|
|
The fixture plugin registers an InstallerInterface implementation of its own
and installs a package of a custom type with it, recording every contract
call it receives. A fresh install produces a byte-identical project tree on
both implementations, trace included.
A second test pins the divergence a re-run and a `remove` expose: the two
implementations consult getInstaller at different points, so the trace
order — and its length once `remove` re-creates the Composer instance —
differs. It is written in full and marked ignored rather than trimmed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
A plugin can now hand an InstallerInterface implementation to
InstallationManager::addInstaller across the wire, and a legacy
composer-installer package is loaded as one; both are backed by a
PhpInstallerProxy forwarding the whole installer contract to the entity in
the PHP worker. An installer returning a real promise is an explicit error
until promises can cross the boundary.
InstallationManager takes installers as shared handles instead of boxes, so
the object identity removeInstaller and PluginManager's registeredPlugins
compare against survives registration, and holds them in a RefCell:
Installer::run keeps a shared borrow of the manager for the whole run, and a
plugin activated inside it registers its installer from there. The type
cache keys on the installer itself, like upstream, so re-entrant
registration cannot leave a stale index behind. InstallerInterface::supports
is fallible for the same reason getCapabilities and getCommands are: it
answers over RPC.
Cloning a proxy stub clones the Rust-side entity and rebinds the copy to the
fresh handle. Previously only the classes declaring __clone got a throwing
body, and the rest let two stubs share (and twice release) one handle.
Package entities answer with AnyPackage::dup, which already carries
BasePackage::__clone and the RootAliasPackage override; the others are an
explicit error.
The package proxy covers the whole PackageInterface surface; only the link
maps and the release date still lack a wire image for their value objects.
PluginManager gains a test-only seam for the reported Plugin API version,
and the three PluginInstallerTest cases that need it are ported.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
The pinned plugin and its real dependency tree (nine packages, fetched by
commit and verified by tree hash into the git-ignored ext/) install under
both implementations, and the list/help renderings of the plugin-provided
normalize command must match upstream byte for byte. The execution
comparison is written but ignored: NormalizeCommand builds a second,
in-process Composer instance, and the worker's proxy stubs reject native
construction of the classes that path instantiates. Stale comments about
the missing worker-side application are updated to the current facts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
A Shirabe-authored CommandProvider fixture plugin (greet) runs under both
implementations: exit codes, the command's own output, help/list rendering,
alias resolution, a validation failure, and a file recording the shared
object graph the command observed (root package, strict application FQCN,
and the exit code of the built-in about command it invoked) must match.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
Port Application::getPluginCommands: resolve the local composer with
plugins force-enabled, fall back to Factory::createGlobal, and collect
commands from CommandProvider capability adapters. PhpCommandProxy now
mirrors name/description/aliases/hidden over RPC so `list` output
matches upstream; input definitions remain TODO(plugin).
PhpClass::php_class_name returns an owned String because PHP-backed
proxies only know their class at runtime (the override-skip warning
prints get_class). CommandProvider::getCommands hands out shared
Rc<RefCell> handles since the commands are stored in the application.
Factory::createGlobal now propagates createConfig errors instead of
swallowing them, and Application::getComposer only catches the
exception classes upstream catches, so a ParsingException reaches
doRun's GithubActionError path as in Composer.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
getPluginCapability was a no-op returning None. Now it runs the real
flow: class_exists in the worker, new $capabilityClass($ctorArgs) with
the plugin's own phandle spliced in as $ctorArgs['plugin'], both
instanceof checks answered by is_a in the child, and a per-interface
Rust adapter over the resulting entity (CommandProvider and the plain
Capability marker; anything else is an explicit error).
getPluginCapabilities now propagates errors instead of swallowing them.
Capable::get_capabilities widens from IndexMap<String, String> to
IndexMap<String, PhpMixed>: upstream casts the return with (array) and
validates only the queried key, so the narrow type both rejected maps
Composer accepts and made the invalidImplementationClassNames data
provider unrepresentable. The old code also returned ' 0 ' (trimmed to
the falsy '0') as a valid class name where upstream throws; the
rewritten validation follows upstream's empty/is_string/trim sequence.
CommandProvider::get_commands returns BaseCommand adapters whose name is
read back over RPC after the PHP constructor ran configure(); executing
one still needs the PHP-side Symfony Application and stays an explicit
error. The is_array / instanceof BaseCommand checks that upstream's
Application::getPluginCommands performs on the raw getCommands value
live in the adapter, because Vec<Box<dyn BaseCommand>> asserts every
element up front.
Ports testCommandProviderCapability (plugin-v8 end to end against the
real worker) and testQueryingWithInvalidCapabilityClassNameThrows (all
eight provider cases); the two tests that pass a PHPUnit mock plugin
into PHP stay ignored — a Rust-native mock has no PHP-side entity to
cross the boundary as $ctorArgs['plugin'].
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
Run upstream Composer and Shirabe over pristine copies of a fixture
project at the same path and require composer.lock, the whole vendor tree
(including the plugin-generated GeneratedConfig.php), the plugin's IO
lines and the exit code to match byte for byte.
The plugin under test (phpstan/extension-installer 1.4.3) is downloaded by
fixtures/e2e/fetch — pinned to an upstream commit and hash-verified — into
a git-ignored directory rather than committed; the test skips while it is
absent, like the other real-PHP prerequisites. Its dependencies are
minimal stand-ins resolved from local repositories, so test runs stay
offline. A zip dist would exercise the known lossy-string byte-precision
debt in RemoteFilesystem, so the fixture serves the plugin through a path
dist until that is resolved.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
The PHP worker is shared across the test binary, so whichever subscriber
test runs first owns the bare class name and every later install registers
the plugin under a _composer_tmpN rename (as upstream does). The exact-name
lookup made the removal test depend on lock acquisition order.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
Wire removePlugin to EventDispatcher::removeListener now that subscriber
listeners carry the plugin's P-table handle: PHP's $candidate[0] ===
$listener identity check maps to phandle equality on Callable::PhpMethod.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
Wires the addPlugin subscriber branch end to end: EventSubscriberInterface
and Capable become fallible and dyn-compatible (their sole implementor is
the PHP plugin proxy, which answers getSubscribedEvents over RPC), listeners
register as Callable::PhpMethod and are invoked with a per-call event handle,
and the R table now drops entries when a child-side stub destructs. The R
table keeps its IndexMap with monotonically increasing handles, so released
handles are never reused and no generation counter is needed.
Upstream has no subscriber-plugin test, so the path is covered by a
Shirabe-owned fixture exercising all three getSubscribedEvents shapes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
InstallationManager::execute now takes &self (the mock recorder moved
into a RefCell) and its callers hold only shared borrows: plugin
registration inside a batch re-enters the same manager handle through
Composer::getInstallationManager()->getInstallPath(), which panicked
on the RefCell re-borrow under the &mut shape — the same re-entrancy
the repository side already fixed, unreachable from the ported tests
because they call PluginInstaller::install directly like PHPUnit does.
The worker-side InstalledVersions mirror now matches the full tail of
FilesystemRepository::write: unconditional reload plus the
reflection-based selfDir/installedIsLocalDir restore. The previous
class_exists(false) guard rested on a lazy-load assumption that does
not hold in the worker (its real ClassLoader only knows the Composer
checkout's vendor dir, so a later lazy load would read the checkout's
installed.php, not the project's); the mirror is now skipped only when
the class is not autoloadable at all, i.e. no plugin runtime and hence
no observer code. Boot-time seeding stays TODO(plugin).
Also from the review: registered_plugins entries are removed only
after the deactivate/uninstall loop (PHP unsets last, and a throw must
leave the entry observable); extra.class keeps associative-array
values and fails loudly on non-strings instead of silently dropping
them; the two discarded write() results now propagate (they carry the
reload-push failure); register_package's allow-plugins skip message is
DEBUG like the addPlugin side; the loader-eviction divergence of
REGISTERED_LOADERS and the lossy UTF-8 spots carry searchable
markers; the test-only proxy downcast follows the __ naming rule; the
R-table dispatch clones the entity out instead of holding the table
borrow across the handler; the IO/PartialComposer stubs turn a
plugin-side `new NullIO()` into an explicit error instead of an
ArgumentCountError; and the empty() emulation covers float 0.0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
Implement the remainder of PluginManager::registerPackage: the plugin
autoload map is built by the ported createLoader/parseAutoloads and
served to the worker over the existing reverse-RPC autoloader, files
entries go through a composerRequire-equivalent glue call, and
already-defined classes take the upstream _composer_tmp rename/eval
path. Instantiation uses the new NewObject/CallPhpMethod lanes backed
by a P table in the worker; PhpPluginProxy adapts the resulting handle
to PluginInterface, with $composer/$io exposed to plugin callbacks via
an R table (unsupported methods stay explicit errors). Hand-written
proxy stubs cover Composer, PartialComposer and the IO hierarchy, and
the stub autoloader is re-prepended after loading the Composer PHP
runtime so its vendor autoloader cannot shadow proxied FQCNs.
FilesystemRepository::write now mirrors InstalledVersions::reload into
a running worker (class_exists-guarded, so an unloaded class keeps its
upstream lazy-load behavior), removing the previously undefined
observation window.
The installer pipeline passes the installed repository as a shared
handle instead of a long-lived `&mut dyn`: plugin registration runs
inside InstallationManager::execute and re-enters the same local
repository through the RepositoryManager, which would panic on the
RefCell re-borrow under the old shape.
PluginInterface lifecycle methods now take an owned ComposerHandle
(plugins retain $composer past the call) and return anyhow::Result
(PHP plugin code may throw); the plugin list uses shared ownership so
the identity comparison of removePlugin survives the dual storage in
registeredPlugins, matching PHP reference semantics.
Ports the activate/upgrade/uninstall tests of PluginInstallerTest,
serialized across the shared worker process whose persistent class
table is exactly what exercises the rename path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
Implement the two script execution paths that previously stopped at
todo!(): a Class::method listener is invoked as CallStaticMethod with the
event crossing the boundary as a proxy-stub handle, and a Command-class
listener runs inside a throwaway bare Symfony Application hosted by the
worker via a generated snippet, its BufferedOutput written back through
the dispatcher's IO. makeAutoloader is ported for real (canonical-package
hash, setDevMode, buildPackageMap/parseAutoloads/createLoader), and the
class_exists/is_callable/is_a/defined guards now query the worker, whose
script autoloader resolves classes by asking the Rust-side ClassLoader
over the reverse channel. EventInterface gains as_any (the IOInterface
downcast pattern) so the concrete event type is reachable behind the
trait object. Application::do_run now registers ScriptAliasCommand
entries as typed commands, unblocking the run-script --list/alias tests;
the dev-mode-to-generator test is ported with local mockall mocks. The
remaining ignored tests carry re-verified reasons: the listener methods
live on the PHPUnit test class itself (unloadable in the worker), or the
test needs live import of a user PHP Command class into the Application.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
test_preserve_indentation_after_read and test_overwrites_indentation_by_default
copy to and delete the same fixture path; PHPUnit runs them serially, the
parallel Rust harness let them race and flake.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|