| Age | Commit message (Collapse) | Author |
|
Silencer only lowers the PHP error_reporting() level and re-throws
whatever the guarded work raises. A region that never reaches the PHP
runtime has no level to lower and emits no diagnostic on failure, so
wrapping it is indistinguishable from running it unguarded.
The pair kept in Application::hint_common_errors brackets a
getComposer() call, which loads installed plugins and dispatches
PluginEvents::INIT.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
file_get_contents() and file_get_contents_with_max_length() return
Vec<u8> instead of a from_utf8_lossy'd String.
Call sites whose consumer takes a &str still convert lossily and are
marked TODO(bytes).
file_get_contents_with_max_length() now reads at most the requested
number of bytes instead of the whole file.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
Messages that instruct the user to run a command named the Composer
binary. Shirabe ships as its own binary, so the hints now name it.
File names (composer.json, composer.lock), the "composer" repository
type and prose about Composer itself are untouched. The installer and
functional integration fixtures live in the Composer submodule and
cannot be edited, so their expected output is normalized on load.
|
|
Every call site but one passed offset 0. The remaining one, the UTF-8
chunking loop in Application, slices the subject instead: its pattern has
no anchor or lookaround, so matching a suffix is equivalent to starting
the search at that offset.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
Preg had shed everything it owned: after the last few rounds its methods
were one-line forwards to the shim's preg_*(), differing only in a
default argument or a wrapper the caller unwrapped anyway. The 460 call
sites now name the shim function, and shirabe-pcre is gone from the
workspace along with its LICENSE entry.
The forwards expand as they read: isMatch becomes
preg_match2(.., 0).is_some() (is_none() where PHP negates it), isMatch3
and match3 drop the .is_some(), matchAll counts through
preg_match_all2(..).occurrence_count(), and replace4/replace5 spell out
the limit and count arguments preg_replace2 takes. Callbacks are the one
place the shapes differ: preg_replace_callback carries an error out of
the callback, so the fourteen infallible closures wrap their result in
Ok() and expect() it back.
Config::process() is the fifteenth, and it drops the `error` cell it
captured to smuggle a failure past a closure that could only return a
String. The `?` in the closure now carries it, which is what the PHP
does -- a throw from the callback leaves preg_replace_callback at the
failing match rather than running the remaining replacements and
reporting the last error.
The module doc that explained why composer/pcre's exceptions and
*StrictGroups() variants have no counterpart moves to the shim's preg
module, where the functions it describes live.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
PregMatches keyed both forms of a capture group through CaptureKey, so
every read built one: a usize wrapped in an enum, or worse, a String
allocated to name a group that regex::Captures can look up from a &str.
It now mirrors regex::Captures instead -- get() takes the group number,
name() the group name -- and the enum drops out of the type entirely.
That is 285 call sites across 59 files, and the named ones carry most of
the win: `matches.get(&CaptureKey::ByName("host".to_string()))` reads as
`matches.name("host")`. ProcessExecutor loses a `user_key` binding that
existed only to build the key once.
CaptureKey stays as the key type of PregMatchesAll and
PregMatchesAllWithOffsets, where numbered and named entries share one
IndexMap and a key type is the point. Five files still name it.
Also retargets the two preg_match_all comments that described the
occurrence count through `matches[&CaptureKey::ByIndex(0)].len()`, an
Index impl these types no longer carry.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
is_match_named and is_match_with_indexed_captures reshaped a match into
a name-keyed map or a number-positioned vec, each allocating a String
per group up front for callers that then read one or two of them. Every
one of the eleven call sites ports a plain Preg::isMatch in PHP, so they
now call is_match3 and reach for the group they want through
get(&CaptureKey::ByIndex(N)) / get(&CaptureKey::ByName(..)), the same
way the rest of the tree already reads a match.
Falling out of that: PregNamedGroups existed only to type the first
variant; PregMatches::iter() only to build both; and PregMatches::pattern
only to give iter() the capture names. PregMatches is now a plain
wrapper over regex::Captures, so preg_replace_callback no longer clones
the resolved pattern for every match, and preg_match_map! is internal to
the shim again.
SvnDriver::get_file_content and get_change_date recover the flat
`isMatch(..) && $match[2] !== null` condition the PHP has, which the vec
shape had forced into a nested if.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
Preg::match4 and Preg::replace_callback gave callers a
PregMatchedGroups: an IndexMap rebuilt from the match with an owned
String per group, plus a second String for a named group's name key.
That is the copy PregMatches shed when it started wrapping
regex::Captures, reinstated one layer up -- and nearly every regex call
in the tree goes through Preg rather than the shim's preg_* directly, so
almost nothing saw the borrow.
PregMatchedGroups existed only to drop the null (unmatched) groups the
old PregMatches held as Option<String> values. PregMatches::get reports
a non-participating group as None on its own, so the two read alike and
the type collapses into it. Call sites still reach groups through
get(&CaptureKey::ByIndex(N)); what changes is that the value arrives as
a &str borrowed from the subject, which the signatures now carry as a
lifetime.
Three places needed the borrow reckoned with rather than a mechanical
rewrite: PhpFileCleaner::clean and Problem::get_messages read their
groups out before mutating what the match borrows, and
Git::get_authentication_failure names the lifetime of its url argument,
which the result borrows instead of self.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
`Composer\Pcre\Preg` fills `$matches` through a by-ref parameter, and the
port mirrored that with a `&mut` (or `Option<&mut>`) out-param plus a
bool or count return. Callers had to declare an empty map one line ahead
of the call, and the type never said the map is only meaningful when the
call matched. Return the matches instead:
- match3/match4/is_match3/is_match4 -> Option<PregMatchedGroups>
- is_match_named -> Option<PregNamedGroups>
- match_all2/is_match_all -> PregMatchesAll
- is_match_all_with_offsets3 -> PregMatchesAllWithOffsets
Nothing is lost: the bool is `Option::is_some()`, and the occurrence
count is the length of any one column of a PREG_PATTERN_ORDER map, now
spelled `PregMatchesAll::occurrence_count()`. is_match() still answers
the bool question directly for callers that want no groups.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
The shim carried two reporting modes for the preg_* $matches maps: PHP's
default (trailing unmatched groups dropped, interior ones ""), and the
PREG_UNMATCHED_AS_NULL form, picked by calling a *_unmatched_as_null()
variant. The regex crate hands out Option<Match>, which maps onto the
null form directly, and no caller distinguished a dropped group from a
null one -- preg_match() and preg_match_all2() already reported nulls
unconditionally. Keep only the null form; the shim API no longer mirrors
PHP's flag set, which is intended.
Preg::is_match_with_indexed_captures() modelled PHP's "unset" as a
truncated Vec<String>, and now returns Vec<Option<String>>. That is what
Composer actually does: Preg::isMatch() always sets
PREG_UNMATCHED_AS_NULL, and its callers test groups with `!== null`.
preg_match_all(), preg_match_all_set_order() and
preg_split_delim_capture() still hand back Vec<String> and keep the ""
form.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
The five IndexMap shapes that the preg_* functions and Preg fill in are
now distinct types generated by preg_match_map!, so a matches map no
longer interchanges with any other map of the same key and value type.
Index<usize> is kept alongside Index<&Q> because call sites such as
config_command and event_dispatcher reach for a group by its position in
the map rather than by its capture key.
|
|
Porting mapped every PHP `protected` member onto `pub(crate)`, which is
wider than nearly all of them need. Each item demoted here is reached
only from the module that defines it, so the crate-wide visibility
conveyed nothing.
Every `pub(crate)` that survives has at least one reader in another
module of the same crate.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
`http_build_query` ignores `numeric_prefix` because a string-keyed slice never
holds an integer key, and every caller passes `"&"` as the separator. Drop both
parameters and hard-code the separator.
Callers that pass a map of scalar literals to `http_build_query_mixed` no longer
need to build an `IndexMap<String, PhpMixed>` for it, so move them to the slice
form.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
str_contains(), str_starts_with() and str_ends_with() were thin wrappers
over the str methods of the same semantics. Call sites now use
contains()/starts_with()/ends_with() directly.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
The SignalHandler port was a no-op stub, so all four of Composer's abort
paths were dead code: nothing removed a half-created project, reverted
composer.json, or cleaned up half-installed packages.
Composer runs those handlers from pcntl callbacks, which a Rust signal
handler cannot do -- it may touch nothing beyond atomics. SignalSubscription
records the signal instead, and the abort runs from checkpoints on the normal
call stack, where the clean-up can borrow the state it needs. That also
resolves the closure-capture TODO(phase-c)s in RequireCommand and
InstallationManager, and replaces exit_with_last_signal's exit(0) with the
restore-and-re-raise Seld\Signal does.
A subscription is live only inside the four abort regions, so elsewhere the
signals keep their default disposition and kill the process at once. It is
installed without SA_RESTART so a signal interrupts an interactive prompt
rather than resuming the read. A signal reaches only the innermost
subscription, reproducing SignalHandler's single-stack dispatch.
Drop SignalRegistry, SignalableCommandInterface and the Application wiring
for them: nothing in Composer reaches that path, and SignalHandler discards
whatever they register. Signal handling from plugins and scripts is
undefined behavior; see docs/dev/signals.md.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
Move `Seld\Signal` out of shirabe-external-packages and into its own
crate, so the path is `shirabe_seld_signal::SignalHandler` instead of
`shirabe_external_packages::seld::signal::SignalHandler`.
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>
|
|
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>
|
|
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>
|
|
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>
|
|
The proxy templates are heredocs in Composer's BinaryInstaller. Writing
them as raw string literals keeps the generated PHP and shell code laid
out the way it is emitted, instead of one escaped line.
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 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>
|
|
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>
|
|
Retag every Shirabe-authored TODO comment to one of the fixed tags:
phase-c, phase-d, plugin, php-runtime, phase-e.
Upstream-authored TODO comments from Composer/Symfony are left
untouched to preserve the ported code shape.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
PHP's ArrayRepository::count()/hasPackage() call $this->initialize(),
which late-binds to the concrete repository class and lazily loads its
packages. The Rust pass-throughs skipped that: they ran ArrayRepository's
stub initialize instead, returning 0/false and marking the repository
initialized with an empty package list, which made ensure_initialized()
skip the real initialization forever after.
Take &mut self in RepositoryInterface::count/has_package so the lazy
repositories (Filesystem, Platform, Composer) can guard with their real
initialize, and return Result from has_package since that initialization
can fail (PHP propagates the exception). InstallerInterface::is_installed
and InstallationManager::is_package_installed/mark_alias_installed
propagate the same way, which also resolves the TODO(phase-d) markers on
Package/Path/Artifact/Vcs repositories about initialization errors being
swallowed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Operations are only ever constructed by the dependency resolver, so a
plugin has no way to inject an implementation of its own and the set is
closed. Modelling it as an enum, like AnyPackage, removes the
OperationInterface trait together with its two parallel downcast
mechanisms (as_any() + downcast_ref, and as_*_operation()) and the
get_package() default method that panicked on UpdateOperation.
The PHP idiom `$op instanceof UpdateOperation ? getTargetPackage() :
getPackage()`, written out at six call sites, becomes
AnyOperation::get_target_package(). InstallationManager's three blocks
that matched on the type string and then recovered the type with
expect() collapse into exhaustive matches.
SolverOperation keeps only its TYPE constant; the shared
getOperationType()/__toString() implementations move to AnyOperation,
which also drops the five Self::TYPE.to_string() allocations.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
Add the __add_installer test seam that registers an installer as a
pre-built Rc handle, so the test can reproduce PHP's object-identity
semantics (assertSame / removeInstaller) via Rc::ptr_eq; add_installer
cannot serve because Rc::from(Box) reallocates, losing the caller's
pointer identity.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
|
|
executeBatch now builds one future per operation — the PHP promise
chain prepare -> install/update/uninstall -> cleanup -> repo->write,
including the '<Op> of <pkg> failed' rejection handler covering the
chain up to cleanup — and drives the whole batch through
waitOnPromises()/Loop::wait, so archive extraction (the unzip
subprocesses gated by ProcessExecutor's semaphore) finally overlaps
across packages. Alias operations stay synchronous in the collection
loop like PHP.
The shared repository is threaded through the chains as
RefCell<&mut dyn InstalledRepositoryInterface>: execute() wraps the
incoming &mut once, and InstallerInterface::install/update/uninstall
take the cell so implementations borrow it only in their synchronous
head/tail, never across an await. InstallationManager's own
install/update/uninstall/download/get_installer/get_install_path/
mark_for_notification move to &self (cache and notifiable_packages
behind RefCell) so every chain can capture &self.
WritableRepositoryInterface::write and the InstallationManagerInterface
get_install_path it relies on lose their &mut manager requirement —
the per-op repo->write inside the chains only reads install paths.
Warm-cache create-project laravel/laravel: the package-operations
phase (109 installs) drops from ~3.0-3.8s serial to ~1.9s, on par
with real Composer (~2.1s) measured back-to-back; the resulting
vendor tree, installed.json included, stays byte-identical to
Composer's (diff -rq clean).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
InstallationManager::downloadAndExecuteBatch now matches PHP: every
update/install operation's installer->download() promise is collected
and driven concurrently through waitOnPromises()/Loop::wait instead of
being awaited one package at a time. Concurrency caps stay where PHP
puts them (HttpDownloader 12, ProcessExecutor 10 via their semaphores).
Error semantics follow PHP too: all downloads settle before the first
rejection is rethrown, rather than aborting on the first failure.
To let the collected futures and the cleanup closures own their
installer beyond the loop iteration that created them, the installer
registry becomes Vec<Rc<dyn InstallerInterface>> and get_installer
hands out clones (PHP closures capture $installer the same way), with
InstallerInterface methods taking &self across the six implementors —
the only genuinely mutable state was LibraryInstaller.vendor_dir
(canonicalized in place), now behind a RefCell.
as_plugin_installer_mut/as_binary_presence_interface lose their &mut.
The cleanup_promises entries are now the real thing: the PHP closure
including the getInstallationSource() guard and the
installer->cleanup($opType, $package, $initialPackage) call, replacing
the no-op futures (drops one TODO(phase-b) and two TODO(phase-c)).
Verified against the real network: create-project laravel/laravel
produces a vendor tree byte-identical to real Composer's (diff -rq
clean across all 109 packages including vendor/composer).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
regex::Regex::clone() does not share the underlying meta engine's
search-cache pool, so every fresh clone pays a ~10us warmup cost on
its first use. Two changes together eliminate this across nearly all
preg_* call sites:
- A php_regex! macro resolves PHP-style patterns to a per-call-site
&'static regex::Regex (via regex-macro's LazyLock), applied at the
majority of call sites throughout the codebase.
- Call sites still passing dynamic pattern strings go through
PATTERN_CACHE, which now stores Arc<(Regex, bool)> and hands out
Arc::clone()s instead of cloning the Regex itself.
PregPattern::resolve() returns a ResolvedPattern enum (Arc or
'static reference) rather than an owned Regex, so neither path ever
clones the Regex proper.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|
|
|
Platform::get_env("CI").is_some()/is_none() only checked whether the
variable was set, unlike PHP's (bool) Platform::getEnv('CI') which
treats "" and "0" as falsy. CI="0" (used by some CI providers to
explicitly disable CI mode) would previously flip behavior compared to
Composer.
|
|
Real Composer shows a ProgressBar during InstallationManager's
waitOnPromises (`Package operations: N installs` ... `0/109 [>---] 0%`
... `100%`), but this port never wired one up: output_progress was set
by callers and never consulted. This adds the same gating PHP uses
(output_progress, ConsoleIO, not CI, not debug, more than one
operation) for both the download phase and the install/extract phase.
The port runs downloads/installs serially rather than as concurrently
polled promises (see the existing TODO(phase-c-promise) notes), so
there is no active-job count to poll for intermediate snapshots.
Stepping the bar per completed operation was tried first, but it
interleaves with the "- Installing ..." lines mid-terminal-line since
both share the same overwrite/newline state; rendering a single 0% ->
100% jump after each phase avoids that garbling at the cost of the
timing-driven intermediate snapshots real Composer shows.
|
|
Wires PartialComposer.as_full() to fetch the PluginManager, replacing
the todo!() stub. Mirrors PHP's assertion that $this->composer must be
a fully-loaded Composer instance.
|
|
Add a no_banned_use linter that forbids importing anyhow::Result, and
update all call sites to reference it via its fully-qualified path so
it is never confused with std::result::Result.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
|
execute_batch only awaited prepare(), leaving the install/update/
uninstall + cleanup + repo.write promise chain as a todo!() stub, so
packages were downloaded to cache but never installed into the target
directory. Wire the operation step (mirroring PHP's promise chain),
propagate errors from the install/update/uninstall wrappers instead of
swallowing them with .ok()?, and write the repo after each op.
Un-ignore the create-project functional tests and the install/remove
command tests this unblocks. The remaining --no-install case still fails
on a separate install-path bug; its ignore reason is updated to match.
|
|
Port composer/tests/Composer/Test/InstallerTest.php. testInstaller (the
provideInstaller cases) is fully ported and passes; the three integration
tests port doTestIntegration in full (the .test fixture loader, FactoryMock,
the in-process console Application with install/update commands, and the
PHPUnit assertStringMatchesFormat matcher) and remain #[ignore]'d since the
install pipeline is not yet executable end-to-end.
Add test-only `__`-seams to the concrete types the test depends on, since
their consumers (e.g. Locker takes the concrete InstallationManager) and the
subclass-style mocks have no trait to mock: InstallationManager (recording
mock + as_any), Factory (__create_mock), VersionGuesser, and
InstalledFilesystemRepository. The production path (mock: false) is unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
|
The CurlDownloader owned a tokio runtime and block_on'd reqwest from its
sync tick(), while the repository/installer/downloader sync bridges each
created another Runtime and block_on'd async fns that reach that leaf.
Driving one Runtime::block_on from within another panics with "Cannot
start a runtime from within a runtime", hit by `require` when fetching
p2 metadata.
Switch CurlDownloader to a blocking reqwest client (its own internal
thread, never nested) and replace the per-call Runtime::new().block_on
bridges with a no-reactor sync_executor::block_on helper. No awaited
future parks on a reactor once the only async I/O is blocking, so the
helper can be nested freely.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
|
|
|
LibraryInstaller and PluginInstaller upgrade the Composer back-reference
in their constructors, so they could not be built inside Rc::new_cyclic
where the weak handle is not yet upgradeable. Defer create_default_
installers until after the cyclic Rc is established, where the weak
handle resolves, and implement it to register Library -> Plugin ->
Metapackage with a single shared BinaryInstaller.
To share one BinaryInstaller (as Composer does), LibraryInstaller's
binary_installer becomes Rc<RefCell<dyn BinaryInstallerInterface>>
instead of an owned Box; PluginInstaller and the __set_binary_installer
test seam follow.
This clears "Unknown installer type: metapackage". Un-ignores the six
remove tests that now pass; the remaining install/remove tests are
re-labeled for the next blocker (InstallationManager::execute_batch
still leaves the install/cleanup/repo.write promise chain as a todo!()
stub, so package operations do not actually execute).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
|
Port 11 categories of previously-ignored Composer tests now reachable with
the mockall crate: DownloadManager, VCS/Perforce/File downloaders,
VersionSelector, PlatformRepository, Auditor, installer/FilesystemRepository,
RootPackageLoader, util auth/http, commands, and Cache.
Extract test seams additively on concrete structs as *Interface traits
(Runtime, HhvmDetector, VersionGuesser, RepositorySet, Perforce,
BinaryInstaller) plus mock-field seams (Cache, Filesystem); consumers take
trait objects. Mocks are defined locally in the test crates via
mockall::mock!, since automock-generated mocks are cfg(test)-gated and
invisible across the integration-test boundary.
dataProviders are ported in full; tests blocked by unported shims stay
#[ignore] with documented reasons rather than reduced or weakened.
Fix product bugs surfaced by the ports:
- util/github: use the exception code, not the HTTP status, for 401/403
- advisory: serialize empty audit maps as [] to match PHP json_encode
- repository/filesystem and downloader/file: fix RefCell double-borrow panics
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
|
Composer/PartialComposer exposed its RepositoryManager, InstallationManager,
EventDispatcher, Locker, DownloadManager, AutoloadGenerator and ArchiveManager
as concrete types, but Composer's public setters (setDownloadManager() etc.)
let plugins swap in subclasses. Introduce a *Interface trait per manager and
store each as Rc<RefCell<dyn ...Interface>> so a replacement is honored.
Only Composer's slots and the sinks fed from its accessors become trait
objects; managers injected concretely at construction keep their concrete
references, matching PHP semantics. Fluent setters on the affected classes now
return () and Locker::update_hash is de-generified to a boxed FnOnce so the
traits stay object-safe.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
|
|