| Age | Commit message (Collapse) | Author |
|
shirabe-symfony-filesystem crate
Move `Symfony\Component\Filesystem` out of shirabe-external-packages and
into its own crate, so the path is
`shirabe_symfony_filesystem::Filesystem` instead of
`shirabe_external_packages::symfony::filesystem::Filesystem`.
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>
|
|
The shim raises no PHP-level errors, so error_get_last() always returned
None and every message built from it lost its trailing reason. Now that
the fs mutators and php_strip_whitespace carry an io::Error, take the
reason from the call that actually failed instead of a global last-error
slot.
Filesystem::unlinkImplementation returns that error rather than a bool so
ensureDirectoryExists, unlink and rmdir can report it, and PhpFileParser
appends it to the "following message may be helpful" hint. The wording
is Rust's io::Error text, not PHP's warning text, for the same reason
noted in symfony/filesystem.
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>
|
|
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>
|
|
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>
|
|
Composer hands the exception it caught to the one it throws in its place,
so `getPrevious()` reaches the cause and Application's renderer prints the
whole chain. Every ported site dropped it, because the flat exception
structs had nowhere to put one. `AnyThrowable::into_previous` turns the
caught error into that argument, and the 13 sites now pass it.
`getCode()` came along for the ride at the four sites that derive the new
exception's code from the caught one (PharArchiver, ArrayLoader x2), and
ComposerRepository's message now names the caught exception's class
instead of the literal "Exception".
GitHubDriver::attemptCloneFallback took the previous exception's message
and appended it to its own, which no `\RuntimeException('Fallback to git
driver disabled')` in Composer ever says; it now chains it instead.
Git::syncMirror restores what PHP's `finally` does to an exception in
flight: the `git remote set-url` that scrubs credentials back out of the
URL runs in a `finally`, and when it fails PHP propagates *its* exception
over the one already leaving, chaining the displaced one as previous. The
port discarded the finally's result, so a failure to scrub the URL was
reported as a successful mirror sync. `AnyThrowable::set_previous`
models the engine-level chaining.
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>
|
|
PHP's version_compare takes its operator as a string, so the shim's port
did too, and Constraint carried two families of operator constants plus
translation tables to convert between the string form and its own int
codes. Five copies of those tables had accumulated across Constraint,
CompilingMatcher and the plugin value bridge.
version_compare now takes a CmpOp, which makes an invalid operator
unrepresentable and removes the tables' reason to exist. Constraint
stores a CmpOp and keeps only the string parsing its constructor needs;
getOperator, compile and CompilingMatcher::match speak CmpOp as well.
PHP's OP_* numbering stays observable: a plugin reads the raw integer off
the Constraint object over RPC, so get_operator_constant and its new
inverse hold that 0..5 mapping.
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>
|
|
Call sites whose haystack was an inline array of literals (or a local
built solely to feed one) had to wrap both sides in PhpMixed just to
compare, allocating a String per element on every call. matches! does
the same test against the underlying &str/i64/Option directly, so the
PhpMixed round trip and its .to_string()/.clone()/.iter().map()
conversions are gone.
Sites whose haystack is a runtime value or a named constant array are
left on in_array_strict: inlining a named constant would duplicate its
contents at the call site.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
|
|
PHP hands back a positional array, so every caller had to reach into a
PhpMixed list by index and fall back to a default when an element was
missing or the wrong variant -- fallbacks that could never fire, since
the list shape is fixed. LicenseMetadata names the four elements and
makes the accesses total.
This also drops the crate's last use of PhpMixed, and with it the
dependency on shirabe-php-shim.
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>
|
|
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
Three fixes for the ComposerRepository/FilesystemRepository/PlatformRepository
hazards where inner-composition delegation skipped PHP's late-bound virtual
dispatch:
- ComposerRepository::has_package now builds its packageMap through the
late-bound getPackages() equivalent, so lazy-providers repos surface the
LogicException and available-packages repos load their package list, as in
PHP, instead of silently answering false from the raw array.
- RepositoryInterface::get_repo_name returns anyhow::Result<String>: PHP's
getRepoName() counts through the late-bound initialize(), which is fallible
in file-reading subclasses. FilesystemRepository and PackageRepository now
run that initialization instead of freezing the inner array repository to an
empty state (which also made a later write() truncate installed.json).
Supporting changes keep the initialization chain callable from &self:
JsonFile::read takes &self (indent moved into a RefCell), FilesystemRepository
dev_mode became a Cell, and WritableArrayRepository dev_package_names a
RefCell.
- PlatformRepository::new routes constructor packages through its own
add_package so the override handling and full platform initialization run
as they do via PHP's parent constructor; the inner find_package/add_package
delegations inside add_package (and ComposerRepository::add_package) gained
the same is_initialized guard, since the constructor path would otherwise
freeze the repository.
Same defect class as 7db937af, 97b5211a and 3e367f78.
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>
|
|
IOInterface::ask/select now return anyhow::Result<PhpMixed>, and
ConsoleIO::ask_question forwards QuestionHelper errors (validator
failures, MissingInputException) instead of collapsing them with
.expect(). In PHP these exceptions propagate from QuestionHelper
through ConsoleIO to the caller, so callers such as UpdateCommand's
interactive package selection must be able to observe them; the
MissingInputException is wrapped with its concrete type preserved so
Application's ExceptionInterface downcast keeps working. All call
sites now propagate with `?` (Perforce::query_p4_user becomes
Result-returning: PHP declares it void but exceptions still escape),
and the previously ignored
test_interactive_mode_throws_if_no_package_entered passes.
ask_confirmation/ask_and_hide_answer still collapse errors; extending
propagation to them is left as TODO(phase-c) pending a decision.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
The only callers were trivial fixed-format uses: reading the first
four hash bytes as a native int, splitting in_addr byte strings, and
building a constant ZIP EOCD record. Each site now does the byte
manipulation directly, so the general-purpose shims are no longer
needed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
The two summary templates are compile-time constants, so the runtime
sprintf shim is unnecessary; carry the tag and the "ignored " prefix
through the passes list instead of pre-built template strings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
Adopt the tar, flate2, and bzip2 crates to fill in the phar.rs and
compress.rs todos: PharData tar/zip reading, building, and whole-archive
compression, plus a native .phar reader that follows the php.net
file-format manual and verifies hash-based signatures. Callers now
propagate the constructor/extract errors PHP throws, and fwrite accepts
byte strings so gzread no longer needs lossy UTF-8.
The native .phar writing API stays todo!() (no call sites; Composer's
Compiler is not ported) and OPENSSL phar signatures are accepted
unverified (TODO(phase-c)).
This unblocks Tar::getComposerJson and the tar/phar/gzip downloaders;
tar_test (7), artifact_repository_test (2), and phar_archiver_test zip
(1) are un-ignored. The archive command itself still panics because
ArchiveManager::archive always generates glob excludes whose look-ahead
regexes the regex crate cannot compile; converting those patterns to
regex-compatible ones is a separate, still-undecided work item.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Two shim gaps made the extension-related branches of solver-problem
messages wrong:
- phpversion($ext) with a non-empty extension can't be known statically
(it's shirabe-php-shim's todo!()); Problem::get_missing_package_reason
now calls shirabe_php_rpc::phpversion, the same RPC bridge
platform::runtime::Runtime::get_extension_version already uses.
- extension_loaded's hardcoded allowlist was missing "pcre", a mandatory
always-compiled-in PHP extension, so ext-pcre was misreported as
"missing from your system" instead of "disabled by your platform
config" whenever a platform override disabled it.
Also fix XdebugHandler::getAllIniFiles() always returning `[""]`
(a php-runtime stub): create_extension_hint()'s early-return guard
(`paths[0] empty && len==1`) fired unconditionally, silently dropping
the entire "To enable extensions..." hint from every solver-problem
message that mentions missing extensions. shirabe-external-packages
can't depend on shirabe-php-rpc (shirabe-php-rpc already depends on
shirabe-external-packages), so IniHelper::get_all() queries a new
get_all_ini_files RPC command directly instead of going through the
stub.
This exposed that XdebugHandler is never constructed with a name
because bin/composer's restart-without-Xdebug bootstrap was never
ported to main.rs, making COMPOSER_ORIGINAL_INIS-driven behavior
unreachable; documented with a TODO(phase-c) and updated
ini_helper_test.rs's ignore reasons (and ignored test_with_no_ini,
which only passed before by coincidence with the old stub's constant
output) to match.
|
|
A plugin can reach ProcessExecutor::executeAsync() through the
rust-proxy stub, which resolves with a real Symfony Process instance
that can't be reconstructed on the Rust side (its state is tied to
whichever process calls proc_open(), and it refuses serialization).
Add execute_async_php() as a todo!() stub, documented as a dual-
instantiation split in plugin-class-classification.md: Rust-internal
callers keep using execute_async(), while the plugin path must forward
spawning to the PHP child once the RPC channel exists.
|
|
shirabe_php_shim::chr() returned a Rust String, which lossily
re-encodes bytes >= 0x80 as UTF-8 replacement characters. ip_get_mask,
ip_get_network, and ip_map_to_6 relied on chr() to build raw in_addr
and netmask byte arrays, corrupting IPv4-in-IPv6 mappings and CIDR
netmasks. Build the Vec<u8> byte arrays directly instead of
round-tripping through String, and un-ignore test_ip_address and
test_ip_range now that the underlying bug is fixed.
chr()'s only other caller (http_downloader.rs, an ASCII ESC byte in a
regex pattern) didn't need the indirection either, so remove the shim
function entirely.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|
Several call sites coerced PhpMixed to bool via `.as_bool()` (which
only matches a literal Bool variant) where the corresponding PHP code
does a plain `(bool)` cast or truthy check (isset()/array_key_exists()
+ implicit bool conversion). This silently dropped truthy non-bool
values (e.g. String("true"), String("1"), Int(1)) to their unwrap_or
default instead of PHP's actual truthy result. Switched these sites to
PhpMixed::to_bool(), which implements PHP's full truthy-cast rules.
|
|
The ignore reason went stale: the FilesystemMock seam added for
FileDownloaderTest already intercepts removeDirectoryAsync, and the
sibling fossil/hg testRemove ports use it. The seam now records the
removed directories instead of a bare call count so the PHP
->with($this->equalTo($this->workingDir)) argument assertion can be
reproduced faithfully.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
PHP's getRemoteContents calls file_get_contents unconditionally: the
same stream wrapper reads file:// URLs, plain local paths and network
schemes. The Rust port only handled the explicit "file" scheme, so a
scheme-less local packages.json repository failed with "file could not
be downloaded". Extend the local branch to empty schemes; the http(s)
stub is unchanged.
This clears the first blocker of the create-project functional test;
its ignore reason now documents the remaining ones (live network and
Glob::to_regex emitting PCRE lookaheads the regex crate cannot
compile), which need a user decision.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
support
ProcessExecutor::execute_async's mock branch was an unimplemented
todo!(), blocking every test whose code path calls it (feature-branch
git diffing, system-unzip/7z fallback extraction). Implement it by:
- Adding Process::__mock (mirroring the existing ZipArchive::__mock
pattern) so execute_async can resolve with a fabricated, already-
terminated Process instead of spawning a real subprocess.
- Extracting the sync mock's expectation-matching logic into a shared
ProcessExecutor::mock_match, wrapping the mock state in a RefCell so
it works from execute_async's &self/&mut self receivers.
- Setting error_output/capture_output from the mock branch, matching
PHP's ProcessExecutorMock::executeAsync sharing doExecute with the
sync path; execute_async now takes &mut self for this (safe, since
the borrow only needs to live through the synchronous setup, not
across the .await).
- Turning a strict-mode expectation mismatch from a panic!() into a
shirabe_php_shim::RuntimeException Err, mirroring PHPUnit's
AssertionFailedError extending \RuntimeException: PHP call sites
that catch (\RuntimeException $e) around a mocked git/hg/svn call
(e.g. Git::get_mirror_default_branch, GitDriver::supports) treat a
mismatch as an ordinary recoverable failure, and now so does the
port. The RefMut is dropped before firing an expectation's optional
callback so a re-entrant callback doesn't panic on double-borrow.
Also fixes two real bugs found while porting test_private_repository_
no_interaction: GitHub::authorize_oauth and GitLab::authorize_oauth
checked their domains config via PhpMixed::as_array(), which only
matches the Array (map) variant, but github-domains/gitlab-domains
default to PhpMixed::List, so the check always returned false and OAuth
token lookup was silently skipped. Use the in_array shim instead,
matching PHP's in_array() semantics. Also fix Git::run_command's
"capture credentials from git remote -v" call, which used the panic-
swallowing execute_args wrapper instead of a fallible execute(), so a
mock mismatch there couldn't reach get_mirror_default_branch's catch.
Un-ignores:
- zip_downloader_test::test_system_unzip_only_{good,failed}
- zip_downloader_test::test_non_windows_fallback_{good,failed}
- event_dispatcher_test::test_dispatcher_outputs_error_on_failed_command
- root_package_loader_test::test_feature_branch_pretty_version
- version_guesser_test::test_guess_version_reads_and_respects_non_feature_branches_configuration_for_arbitrary_naming{,_regex}
- version_guesser_test::test_remote_branches_are_selected
- github_driver_test::test_private_repository_no_interaction (also
adds the missing #[serial], since it seeds the shared Git::VERSION
static that vcs_repository_test::test_load_versions depends on for
real)
- init_command_test::test_get_git_config, made deterministic by
pointing HOME at a throwaway dir with its own .gitconfig instead of
depending on the host's global git config
Deduplicates the GitVersionGuard/RestoreEnv test-drop-guard idioms into
tests/common/test_case.rs instead of reimplementing them per file.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|
|
|
Concurrent package operations call into the same downloader instances
through Rc<RefCell<dyn DownloaderInterface>>; with &mut self methods
every call holds a RefMut across its awaits, which panics with
'already mutably borrowed' the moment two operations overlap. This is
groundwork for fanning out InstallationManager's download/install
loops (same rework HttpDownloader/CurlDownloader already got).
- DownloaderInterface/ChangeReportInterface/ArchiveDownloader/
VcsDownloader methods now take &self; as_change_report_interface
returns &dyn instead of &mut dyn.
- Implementors move their genuinely mutable state behind cells:
FileDownloader.additional_cleanup_paths, the archive downloaders'
cleanup_executed, ZipDownloader.zip_archive_object,
VcsDownloaderBase.has_cleaned_changes, GitDownloader's stash/discard/
cache maps and GitUtil, SvnDownloader.cache_credentials,
PerforceDownloader.perforce. FileDownloader.io gains a RefCell layer
so get_local_changes can keep PHP's NullIO swap under &self.
- ProcessExecutor::execute_async now returns a future that captures
everything up front instead of borrowing the executor, and call
sites build the future before awaiting, so no borrow on the shared
executor is held while a subprocess runs.
- Filesystem::remove_directory_async becomes remove_directory_async_via
taking the Rc handle: the Filesystem is only borrowed for the sync
head/tail, never across the rm subprocess await (sync borrow_mut
users like rename/ensure_directory_exists would otherwise collide).
- DownloadManager async call sites hold shared borrows only.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
execute_async was a serial pump: it queued a job, then drove it to
completion itself via wait_id()'s blocking usleep loop before
returning, so concurrent callers never overlapped even when polled
together through FuturesUnordered.
Rewrite it as a single &self async fn mirroring the CurlDownloader/
HttpDownloader rework: a tokio Semaphore sized by max_jobs
(COMPOSER_MAX_PARALLEL_PROCESSES, PHP parity) gates admission, the
child is started non-blocking, and an async 1ms sleep loop pumps
is_running()/check_timeout() while yielding to the reactor so sibling
jobs genuinely run in parallel. The Job table, STATUS_* lifecycle,
start_job/mark_job_done/count_active_jobs/wait/wait_id all had no
remaining callers and are removed.
&self also lets callers hold only a shared borrow across their awaits
(zip_downloader, version_guesser, filesystem via the new
get_process_handle), which would otherwise panic with 'already
mutably borrowed' once two async jobs overlap on the same
Rc<RefCell<ProcessExecutor>>.
The async mock branch no longer consumes the expectation before
hitting its todo!(): the panic made that bookkeeping unobservable.
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>
|
|
CurlDownloader::auth_helper was never cloned out to another owner, so
Rc<RefCell<AuthHelper>> only needed the RefCell for interior
mutability (all methods take &self). Likewise HttpDownloader::dispatch
cloned self.rfs into a local binding it only ever used synchronously
(copy/get_contents don't await), so the clone bought nothing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|
PHP's handleRedirect() throws a bare TransportException when the
Location header is missing, and the caller's single catch block always
unlinks the `~` partial file via rejectJob(). The Rust decide() loop
splits each failure path into its own branch and had unlinked on every
other one, but missed this branch, leaking the partial file.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|
Replace sync_executor::block_on's reactor-less busy-spin poller with
tokio::task::block_in_place + Handle::current().block_on(), riding a
single tokio Runtime entered once in main.rs (falling back to a
disposable one when no ambient runtime exists, e.g. in tests). This
lets HttpDownloader::dispatch await CurlDownloader::download directly
instead of bouncing through the separate curl_runtime() bridge, which
is now deleted.
Manual create-project verification against the real network caught a
concurrency bug this exposed: async_fetch_file held http_downloader's
RefMut across the await on add(), which only panics once downloads
genuinely overlap. add() only needs &self, so borrow() fixes it.
With everything now sharing one real reactor, the FuturesOrdered
fan-out added for ComposerRepository::get_security_advisories/
load_async_packages finally overlaps for real: fetching 8 packages'
metadata dropped from ~7-40s to a consistent ~3-4s in a before/after
comparison, with identical resulting lock files.
sync_executor::block_on's call sites are still synchronous rather
than async fn propagated up to Command::execute, which remains the
end goal (see the TODO(phase-e) in sync_executor.rs) - nested block_on
calls elsewhere don't get this same overlap, only prevented panics.
|
|
Loop::wait already had the target signature and a TODO(phase-c-promise)
marker noting it drove promises serially; swap the for-loop for
FuturesUnordered so all promises are polled together instead of one at
a time, keeping the "remember only the first error" semantics.
This adds the first real use of the futures dependency (already present
in Cargo.toml/Cargo.lock from earlier prep work, now finally consumed),
so those lockfile/manifest changes land in this commit.
Real overlap still doesn't happen yet: each promise (HttpDownloader::add/
add_copy etc.) resolves through a blocking bridge (curl_runtime()/
sync_executor::block_on) that fully occupies the thread until it
settles, so this is groundwork for once a single top-level Runtime
replaces those bridges. Updated the TODO(phase-c-promise) comment to
reflect that.
|
|
Replaces Job/Request/JobHandle/id_gen/running_jobs/max_jobs with a
tokio::sync::Semaphore permit held for the duration of each request.
get/add/copy/add_copy are now &self (add/add_copy are also genuinely
async); a shared execute()/dispatch() core replaces
add_job/run_rfs_job/start_job/settle_job, returning the Response
directly instead of deferring to wait()/count_active_jobs()/
get_response() (all removed — confirmed zero callers, same for the
now-unused STATUS_* constants).
get()/copy() stay synchronous rather than becoming async wrappers
around add()/add_copy(), bridging via the existing sync_executor
instead of the curl_runtime() introduced for CurlDownloader: their
callers (~35 files reaching HttpDownloader) are mostly plain sync fns
with no async boundary anywhere in the call chain, and forcing that
propagation now would pull forward the dedicated async-propagation
task. curl-eligible requests still route through curl_runtime()
inside dispatch(), same as before — nesting sync_executor::block_on
(no real reactor) around curl_runtime().block_on() (a real, separate
Runtime) is safe; it's only nesting curl_runtime() inside itself that
would panic.
CurlDownloader no longer needs Rc<RefCell<>> wrapping despite the
original design sketch: since item 2 made all of its methods &self,
a plain Option<CurlDownloader> field works fine under HttpDownloader's
own &self methods.
get/add/copy/add_copy becoming &self (rather than &mut self) requires
no changes at any of their ~35 calling files: RefMut/Ref both deref to
a type that can call &self methods just fine.
Verified manually against real network I/O (sandbox disabled): `shirabe
show -a` (get()'s sync_executor-bridged path) and `shirabe
create-project` (add_copy()'s genuinely async path via file_downloader.rs)
both complete correctly with no hang.
|
|
Freeform notes about intentionally-unported production behavior are
easy to miss on a read-through and impossible to grep for later.
|
|
Replaces the Job-table + tick()-driven polling loop with one async
download() that sends, decides (retry/redirect/fail/succeed via a new
decide() extracted from the former run_job), and loops until it
resolves — no more resolve/reject callbacks. The client switches from
reqwest::blocking::Client to the non-blocking reqwest::Client, with
body streaming now via tokio::fs.
Because real async I/O needs a live tokio reactor and none runs yet at
the process level (sync_executor::block_on is a no-reactor busy-spin
executor that only works when awaited futures resolve synchronously),
HttpDownloader::start_job drives CurlDownloader::download() through a
dedicated temporary current_thread Runtime (curl_runtime(), marked
TODO(phase-e)) instead. This keeps concurrency characteristics
unchanged for now — start_job still resolves one job at a time — real
parallel I/O lands once HttpDownloader/Loop are rearchitected on top of
FuturesUnordered.
count_active_jobs' curl.tick() polling and the Job.settled/curl_id
plumbing are removed as dead weight now that start_job settles curl
jobs synchronously, same as the rfs path already did.
abort_request is dropped: it had no caller (the PHP Promise-cancellation
flow it backs was never ported), and the job table it operated on no
longer exists.
Verified manually against real network I/O (sandbox disabled): `shirabe
show -a` (JSON metadata, in-memory body) and `shirabe create-project`
(actual dist zip download + extraction) both complete correctly with no
hang. Two unrelated pre-existing bugs surfaced during manual testing
(an event-dispatcher subscriber wiring gap during `require`, and a
RefCell reentrancy panic in `diagnose`) reproduce identically on the
pre-change code and are out of scope here.
|
|
In prep for the upcoming &self conversion of add()/get()/copy(), the
mock hook needs interior mutability too. The struct's Clone derive is
dropped since nothing clones the whole state anymore, only the shared
Rc handle.
|
|
Wrap RemoteFilesystem in Rc<RefCell<>> inside HttpDownloader, in prep
for the upcoming &self conversion of add()/get(). copy()/get_contents()
now bundle the response headers into their return value instead of
requiring a follow-up get_last_headers() call, since two separate calls
through a shared RefCell could otherwise race: nothing would guarantee
the reader observes the headers from its own request rather than one
clobbered by a concurrently borrowed call. get_last_headers() itself is
left in place, mirroring RemoteFilesystem::getLastHeaders() in PHP.
|
|
CurlDownloader's download() is about to become an &self async method
as part of the HttpDownloader async rearchitecture; its auth_helper
field needs interior mutability ahead of that change. RemoteFilesystem
keeps its own AuthHelper as a plain field since it stays &mut self.
|
|
reset() eagerly rebuilt the ProxyManager singleton immediately,
capturing env vars before a caller could set them for the next
request. PHP's reset() just nulls the static instance; getInstance()
lazily constructs on next use. Match that so proxy env vars set after
reset() are observed.
get_instance() also ensured the singleton was constructed under its
own lock, dropped that lock, and returned the bare Mutex; every caller
then took a second, independent lock. A reset() landing in that gap
would leave the caller observing None and panicking on
.as_ref().unwrap(), a state the old eager-reconstructing reset() could
not produce. Return the already-locked MutexGuard from get_instance()
instead, so construction and use happen under one lock, and update all
call sites accordingly.
Holding that guard across a loop body then deadlocked in
diagnose_command, since check_http_proxy transitively re-enters
get_instance() via HttpDownloader -> CurlDownloader, and
std::sync::Mutex is not reentrant. Re-acquire the lock fresh each
iteration with a short-lived guard instead.
Finally, Mutex::new is a const fn, so the OnceLock wrapper around it
was unnecessary indirection; a bare static Mutex<Option<ProxyManager>>
initializes to the same state without the get_or_init/get dance.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|
Implement previously-todo!() tests in auth_helper_test.rs,
process_executor_test.rs, remote_filesystem_test.rs, and
stream_context_factory_test.rs by porting the corresponding PHPUnit
test methods. Extend IOStub with writeRaw/setAuthentication call
tracking and askAndValidate/getAuthentication overrides to model the
PHPUnit mocks these tests rely on, deduping the resulting
call-recording fields into a small generic CallRecorder<T> helper
instead of repeating the same RefCell<Vec<T>> push/borrow().clone()
boilerplate five times.
testStoreAuthWithPromptInvalidAnswer and
testPromptAuthIfNeededMultipleBitbucketDownloads had initially lost
the ported PHPUnit mock's argument/call-count assertions
(askAndValidate's exact prompt string, and
hasAuthentication/getAuthentication's exactly(2) call counts),
silently narrowing what the tests verify; IOStub now records these
calls and the tests assert on them, matching upstream.
Tests left unportable (PHP set_error_handler machinery, closures in
data providers, network/subclass-mock dependencies, etc.) keep
#[ignore] with a single // TODO(phase-d) reason recorded in the
function body.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
|
|
|
|
|
Convert errors/warnings/config to RefCell so load() can satisfy the
trait's &self signature, matching upstream's `instanceof
ValidatingArrayLoader` check in VcsRepository. This makes the
InvalidPackageException downcast path in VcsRepository reachable for
the first time instead of being permanently dead code.
|