aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/src/installer
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-05-23 15:45:33 +0900
committernsfisis <nsfisis@gmail.com>2026-05-23 15:48:00 +0900
commitbd6d0186d2c01a3e1d6324ad5a0bcdd71de53098 (patch)
tree939eb1dccbfb3341a2f618e734ca23ef84a8e5cc /crates/shirabe/src/installer
parente068a9d644fde6659a88accd55b3f1d0d9d7cf46 (diff)
downloadphp-shirabe-bd6d0186d2c01a3e1d6324ad5a0bcdd71de53098.tar.gz
php-shirabe-bd6d0186d2c01a3e1d6324ad5a0bcdd71de53098.tar.zst
php-shirabe-bd6d0186d2c01a3e1d6324ad5a0bcdd71de53098.zip
refactor(promise): drop \React\Promise
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe/src/installer')
-rw-r--r--crates/shirabe/src/installer/installation_manager.rs185
-rw-r--r--crates/shirabe/src/installer/installer_interface.rs1
-rw-r--r--crates/shirabe/src/installer/library_installer.rs80
-rw-r--r--crates/shirabe/src/installer/metapackage_installer.rs1
-rw-r--r--crates/shirabe/src/installer/noop_installer.rs1
-rw-r--r--crates/shirabe/src/installer/plugin_installer.rs5
-rw-r--r--crates/shirabe/src/installer/project_installer.rs1
7 files changed, 129 insertions, 145 deletions
diff --git a/crates/shirabe/src/installer/installation_manager.rs b/crates/shirabe/src/installer/installation_manager.rs
index ae0470a..7a05324 100644
--- a/crates/shirabe/src/installer/installation_manager.rs
+++ b/crates/shirabe/src/installer/installation_manager.rs
@@ -3,7 +3,6 @@
use crate::io::io_interface;
use anyhow::Result;
use indexmap::IndexMap;
-use shirabe_external_packages::react::promise;
use shirabe_external_packages::seld::signal::SignalHandler;
use shirabe_php_shim::{
InvalidArgumentException, PhpMixed, array_search_mixed, array_splice, array_unshift, count,
@@ -181,7 +180,10 @@ impl InstallationManager {
// @var array<callable(): ?PromiseInterface<void|null>> $cleanupPromises
let mut cleanup_promises: IndexMap<
i64,
- Box<dyn Fn() -> Option<Box<dyn PromiseInterface>>>,
+ Box<
+ dyn Fn()
+ -> Option<std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>>>>>,
+ >,
> = IndexMap::new();
let signal_handler = SignalHandler::create(
@@ -234,15 +236,17 @@ impl InstallationManager {
}
for batch_to_execute in batches {
- self.download_and_execute_batch(
- repo,
- batch_to_execute,
- &mut cleanup_promises,
- dev_mode,
- run_scripts,
- download_only,
- // TODO(phase-b): allOperations should be the original full list; would require clone
- vec![],
+ tokio::runtime::Runtime::new().unwrap().block_on(
+ self.download_and_execute_batch(
+ repo,
+ batch_to_execute,
+ &mut cleanup_promises,
+ dev_mode,
+ run_scripts,
+ download_only,
+ // TODO(phase-b): allOperations should be the original full list; would require clone
+ vec![],
+ ),
)?;
}
@@ -255,7 +259,9 @@ impl InstallationManager {
match result {
Ok(()) => {}
Err(e) => {
- self.run_cleanup(&cleanup_promises);
+ tokio::runtime::Runtime::new()
+ .unwrap()
+ .block_on(self.run_cleanup(&cleanup_promises));
return Err(e);
}
}
@@ -275,20 +281,22 @@ impl InstallationManager {
/// @param OperationInterface[] $operations List of operations to execute in this batch
/// @param OperationInterface[] $allOperations Complete list of operations to be executed in the install job, used for event listeners
/// @phpstan-param array<callable(): ?PromiseInterface<void|null>> $cleanupPromises
- fn download_and_execute_batch(
+ async fn download_and_execute_batch(
&mut self,
repo: &mut dyn InstalledRepositoryInterface,
operations: IndexMap<i64, Box<dyn OperationInterface>>,
- cleanup_promises: &mut IndexMap<i64, Box<dyn Fn() -> Option<Box<dyn PromiseInterface>>>>,
+ cleanup_promises: &mut IndexMap<
+ i64,
+ Box<
+ dyn Fn()
+ -> Option<std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>>>>>,
+ >,
+ >,
dev_mode: bool,
run_scripts: bool,
download_only: bool,
all_operations: Vec<Box<dyn OperationInterface>>,
) -> Result<()> {
- // TODO(phase-c-promise): Loop::wait-driven batch orchestration (download/cleanup promises);
- // rewrite to await collected futures once the async Loop boundary lands.
- let mut promises: Vec<Box<dyn PromiseInterface>> = vec![];
-
for (index, operation) in &operations {
let op_type = operation.get_operation_type();
@@ -316,35 +324,35 @@ impl InstallationManager {
}
let installer = self.get_installer(package.get_type())?;
- // TODO(phase-b): closure captures installer + package; needs Arc/Rc for shared state
+ // TODO(phase-b): closure captures installer + package; needs Rc-shared installer/package
let _ = installer;
let op_type_clone = op_type.clone();
- let cleanup: Box<dyn Fn() -> Option<Box<dyn PromiseInterface>>> =
- Box::new(move || -> Option<Box<dyn PromiseInterface>> {
- // avoid calling cleanup if the download was not even initialized for a package
- // as without installation source configured nothing will work
- // TODO(phase-b): if (null === $package->getInstallationSource()) return \React\Promise\resolve(null);
- let _ = &op_type_clone;
- Some(promise::resolve(None))
- });
+ let cleanup: Box<
+ dyn Fn()
+ -> Option<std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>>>>>,
+ > = Box::new(move || {
+ // avoid calling cleanup if the download was not even initialized for a package
+ // as without installation source configured nothing will work
+ // TODO(phase-b): if (null === $package->getInstallationSource()) return resolve(null);
+ let _ = &op_type_clone;
+ // TODO(phase-c-promise): build the real installer.cleanup() future once the installer
+ // can be shared into a 'static cleanup closure (Stage 2 Rc/Arc).
+ let fut: std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>>>> =
+ Box::pin(async { Ok(()) });
+ Some(fut)
+ });
cleanup_promises.insert(*index, cleanup);
if op_type != "uninstall" {
+ // TODO(phase-c-promise): PHP collects every download and runs them concurrently via
+ // Loop::wait; the single-threaded loop awaits each serially instead.
let installer = self.get_installer(package.get_type())?;
- let promise = installer.download(package, initial_package);
- if let Ok(Some(p)) = promise {
- promises.push(p);
- }
+ installer.download(package, initial_package).await?;
}
}
- // execute all downloads first
- if (promises.len() as i64) > 0 {
- self.wait_on_promises(promises);
- }
-
if download_only {
- self.run_cleanup(cleanup_promises);
+ self.run_cleanup(cleanup_promises).await;
return Ok(());
}
@@ -385,7 +393,8 @@ impl InstallationManager {
dev_mode,
run_scripts,
&all_operations,
- )?;
+ )
+ .await?;
}
Ok(())
@@ -394,18 +403,21 @@ impl InstallationManager {
/// @param OperationInterface[] $operations List of operations to execute in this batch
/// @param OperationInterface[] $allOperations Complete list of operations to be executed in the install job, used for event listeners
/// @phpstan-param array<callable(): ?PromiseInterface<void|null>> $cleanupPromises
- fn execute_batch(
+ async fn execute_batch(
&mut self,
repo: &mut dyn InstalledRepositoryInterface,
operations: IndexMap<i64, Box<dyn OperationInterface>>,
- cleanup_promises: &IndexMap<i64, Box<dyn Fn() -> Option<Box<dyn PromiseInterface>>>>,
+ cleanup_promises: &IndexMap<
+ i64,
+ Box<
+ dyn Fn()
+ -> Option<std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>>>>>,
+ >,
+ >,
dev_mode: bool,
run_scripts: bool,
all_operations: &[Box<dyn OperationInterface>],
) -> Result<()> {
- // TODO(phase-c-promise): Loop::wait-driven batch orchestration (prepare/install promises);
- // rewrite to await collected futures once the async Loop boundary lands.
- let mut promises: Vec<Box<dyn PromiseInterface>> = vec![];
let mut post_exec_callbacks: Vec<Box<dyn Fn()>> = vec![];
for (index, operation) in operations {
@@ -473,14 +485,13 @@ impl InstallationManager {
let _io = self.io.as_ref();
let installer = self.get_installer(package.get_type())?;
- let promise = installer.prepare(&op_type, package, initial_package);
- let promise = match promise {
- Ok(Some(p)) => p,
- Ok(None) => promise::resolve(None),
- Err(e) => return Err(e),
- };
+ // TODO(phase-c-promise): PHP chains prepare()->then(install/update/uninstall)->then(cleanup
+ // + repo.write); the single-threaded loop awaits prepare and leaves the rest as phase-b work.
+ installer
+ .prepare(&op_type, package, initial_package)
+ .await?;
- // TODO(phase-b): chain `.then(cb1).then(cb2)` with cleanup_promises[index], repo.write, etc.
+ // TODO(phase-b): chain the install/update/uninstall step with cleanup_promises[index], repo.write, etc.
let _ = cleanup_promises.get(&index);
let event_name_post = match op_type.as_str() {
@@ -497,13 +508,6 @@ impl InstallationManager {
// dispatcher.dispatch_package_event(event_name_post, dev_mode, repo, all_operations, operation);
}));
}
-
- promises.push(promise);
- }
-
- // execute all prepare => installs/updates/removes => cleanup steps
- if (promises.len() as i64) > 0 {
- self.wait_on_promises(promises);
}
Platform::workaround_filesystem_issues();
@@ -515,32 +519,6 @@ impl InstallationManager {
Ok(())
}
- /// @param array<PromiseInterface<void|null>> $promises
- fn wait_on_promises(&mut self, promises: Vec<Box<dyn PromiseInterface>>) {
- // TODO(phase-c-promise): thin wrapper over Loop::wait (the async boundary owned separately).
- let mut progress: Option<()> = None;
- // TODO(phase-b): self.io instanceof ConsoleIO downcast
- let io_is_console = false;
- if self.output_progress
- && io_is_console
- && Platform::get_env("CI").is_none()
- && !self.io.is_debug()
- && (promises.len() as i64) > 1
- {
- // TODO(phase-b): progress = self.io.get_progress_bar();
- progress = Some(());
- }
- // TODO(phase-b): pass actual ProgressBar when self.io.get_progress_bar() is implemented
- let _ = self.loop_.borrow_mut().wait(promises, None);
- if progress.is_some() {
- // progress.clear();
- // ProgressBar in non-decorated output does not output a final line-break and clear() does nothing
- if !self.io.is_decorated() {
- self.io.write_error3("", true, io_interface::NORMAL);
- }
- }
- }
-
/// Executes download operation.
///
/// @phpstan-return PromiseInterface<void|null>|null
@@ -653,10 +631,8 @@ impl InstallationManager {
}
pub fn notify_installs(&mut self, _io: &dyn IOInterface) {
- // TODO(phase-c-promise): collects http_downloader.add() promises and drives them via Loop::wait;
- // rewrite to await the collected futures once the async Loop boundary lands.
- let mut promises: Vec<Box<dyn PromiseInterface>> = vec![];
-
+ // TODO(phase-c-promise): PHP collects every http_downloader.add() promise and runs them via
+ // Loop::wait; the single-threaded sync bridge block_on's each notification serially instead.
let result: Result<()> = (|| -> Result<()> {
for (repo_url, packages) in &self.notifiable_packages {
// non-batch API, deprecated
@@ -699,13 +675,13 @@ impl InstallationManager {
),
);
- promises.push(
+ tokio::runtime::Runtime::new().unwrap().block_on(
self.loop_
.borrow()
.get_http_downloader()
.borrow_mut()
- .add(&url, opts)?,
- );
+ .add(&url, opts),
+ )?;
}
continue;
@@ -771,17 +747,15 @@ impl InstallationManager {
PhpMixed::Array(http.into_iter().map(|(k, v)| (k, Box::new(v))).collect()),
);
- promises.push(
+ tokio::runtime::Runtime::new().unwrap().block_on(
self.loop_
.borrow()
.get_http_downloader()
.borrow_mut()
- .add(repo_url, opts)?,
- );
+ .add(repo_url, opts),
+ )?;
}
- let _ = self.loop_.borrow_mut().wait(promises, None);
-
Ok(())
})();
// PHP swallows the exception silently here
@@ -800,28 +774,33 @@ impl InstallationManager {
}
/// @phpstan-param array<callable(): ?PromiseInterface<void|null>> $cleanupPromises
- fn run_cleanup(
+ async fn run_cleanup(
&mut self,
- cleanup_promises: &IndexMap<i64, Box<dyn Fn() -> Option<Box<dyn PromiseInterface>>>>,
+ cleanup_promises: &IndexMap<
+ i64,
+ Box<
+ dyn Fn()
+ -> Option<std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>>>>>,
+ >,
+ >,
) {
- // TODO(phase-c-promise): runs cleanup closures and drives them via Loop::wait;
- // rewrite once the async Loop boundary and async cleanup closures land.
- let mut promises: Vec<Box<dyn PromiseInterface>> = vec![];
+ let mut promises: Vec<std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>>>>> =
+ vec![];
self.loop_.borrow().abort_jobs();
for (_, cleanup) in cleanup_promises {
- // TODO(phase-b): React\Promise\Promise constructor with executor; emulate by wrapping cleanup()
+ // PHP wraps a missing cleanup promise in \React\Promise\resolve(null).
let promise = cleanup();
if let Some(p) = promise {
promises.push(p);
} else {
- promises.push(promise::resolve(None));
+ promises.push(Box::pin(async { Ok(()) }));
}
}
if (promises.len() as i64) > 0 {
- let _ = self.loop_.borrow_mut().wait(promises, None);
+ let _ = self.loop_.borrow_mut().wait(promises, None).await;
}
}
}
diff --git a/crates/shirabe/src/installer/installer_interface.rs b/crates/shirabe/src/installer/installer_interface.rs
index 29171cf..bb510b8 100644
--- a/crates/shirabe/src/installer/installer_interface.rs
+++ b/crates/shirabe/src/installer/installer_interface.rs
@@ -4,6 +4,7 @@ use crate::package::PackageInterface;
use crate::repository::InstalledRepositoryInterface;
use shirabe_php_shim::PhpMixed;
+#[async_trait::async_trait(?Send)]
pub trait InstallerInterface: std::fmt::Debug {
fn supports(&self, package_type: &str) -> bool;
diff --git a/crates/shirabe/src/installer/library_installer.rs b/crates/shirabe/src/installer/library_installer.rs
index 0629de5..29cb455 100644
--- a/crates/shirabe/src/installer/library_installer.rs
+++ b/crates/shirabe/src/installer/library_installer.rs
@@ -5,7 +5,8 @@ use std::any::Any;
use anyhow::Result;
use shirabe_external_packages::composer::pcre::Preg;
use shirabe_php_shim::{
- InvalidArgumentException, LogicException, is_link, preg_quote, realpath, rmdir, rtrim, strpos,
+ InvalidArgumentException, LogicException, PhpMixed, dirname, is_dir, is_link, preg_quote,
+ realpath, rmdir, rtrim, strpos,
};
use crate::composer::PartialComposerWeakHandle;
@@ -215,6 +216,7 @@ impl LibraryInstaller {
}
}
+#[async_trait::async_trait(?Send)]
impl InstallerInterface for LibraryInstaller {
fn supports(&self, package_type: &str) -> bool {
match &self.r#type {
@@ -314,20 +316,16 @@ impl InstallerInterface for LibraryInstaller {
self.binary_installer.remove_binaries(package);
}
- // TODO(phase-c-promise): rewrite install_code().then(installBinaries + repo.addPackage) as an await sequence.
- let promise = self.install_code(package)?;
- let promise = match promise {
- Some(p) => p,
- None => shirabe_external_packages::react::promise::resolve(None),
- };
+ let _ = self.install_code(package).await?;
- // TODO(phase-b): promise.then expects Option<Box<dyn FnOnce(Option<PhpMixed>) -> Option<PhpMixed>>>
- // arguments. The original PHP closure captures &mut self/binary_installer/repo/package;
- // restructuring required.
- let _ = promise;
- Ok(Some(todo!(
- "promise.then(...) chain to install_binaries + repo.add_package"
- )))
+ let install_path = self.get_install_path(package).unwrap();
+ self.binary_installer
+ .install_binaries(package, &install_path, true);
+ if !repo.has_package(package) {
+ repo.add_package(package.clone_package_box());
+ }
+
+ Ok(None)
}
async fn update(
@@ -348,20 +346,17 @@ impl InstallerInterface for LibraryInstaller {
// self.initialize_vendor_dir();
self.binary_installer.remove_binaries(initial);
- // TODO(phase-c-promise): rewrite update_code().then(installBinaries + repo updates) as an await sequence.
- let promise = self.update_code(initial, target)?;
- let promise = match promise {
- Some(p) => p,
- None => shirabe_external_packages::react::promise::resolve(None),
- };
+ let _ = self.update_code(initial, target).await?;
+
+ let install_path = self.get_install_path(target).unwrap();
+ self.binary_installer
+ .install_binaries(target, &install_path, true);
+ repo.remove_package(initial);
+ if !repo.has_package(target) {
+ repo.add_package(target.clone_package_box());
+ }
- // TODO(phase-b): promise.then expects Option<Box<dyn FnOnce(Option<PhpMixed>) -> Option<PhpMixed>>>
- // arguments. Closure captures &mut self/binary_installer/repo/initial/target;
- // restructuring required.
- let _ = promise;
- Ok(Some(todo!(
- "promise.then(...) chain to install_binaries + repo updates"
- )))
+ Ok(None)
}
async fn uninstall(
@@ -377,20 +372,25 @@ impl InstallerInterface for LibraryInstaller {
.into());
}
- // TODO(phase-c-promise): rewrite remove_code().then(remove_binaries/remove_package/rmdir) as an await sequence.
- let promise = self.remove_code(package)?;
- let promise = match promise {
- Some(p) => p,
- None => shirabe_external_packages::react::promise::resolve(None),
- };
+ let _ = self.remove_code(package).await?;
+
+ let download_path = self.get_package_base_path(package);
+ self.binary_installer.remove_binaries(package);
+ repo.remove_package(package);
+
+ if strpos(package.get_name(), "/").map_or(false, |pos| pos != 0) {
+ let package_vendor_dir = dirname(&download_path);
+ if is_dir(&package_vendor_dir)
+ && self.filesystem.borrow().is_dir_empty(&package_vendor_dir)
+ {
+ let _ = Silencer::call(|| {
+ rmdir(&package_vendor_dir);
+ Ok(())
+ });
+ }
+ }
- // TODO(phase-b): promise.then expects Option<Box<dyn FnOnce(Option<PhpMixed>) -> Option<PhpMixed>>>
- // arguments. Closure captures binary_installer/filesystem/download_path/package/repo;
- // restructuring required.
- let _ = promise;
- Ok(Some(todo!(
- "promise.then(...) chain to remove_binaries/remove_package/rmdir"
- )))
+ Ok(None)
}
fn get_install_path(&self, package: &dyn PackageInterface) -> Option<String> {
diff --git a/crates/shirabe/src/installer/metapackage_installer.rs b/crates/shirabe/src/installer/metapackage_installer.rs
index 23d92f2..2ea9685 100644
--- a/crates/shirabe/src/installer/metapackage_installer.rs
+++ b/crates/shirabe/src/installer/metapackage_installer.rs
@@ -22,6 +22,7 @@ impl MetapackageInstaller {
}
}
+#[async_trait::async_trait(?Send)]
impl InstallerInterface for MetapackageInstaller {
fn supports(&self, package_type: &str) -> bool {
package_type == "metapackage"
diff --git a/crates/shirabe/src/installer/noop_installer.rs b/crates/shirabe/src/installer/noop_installer.rs
index 68e4ef8..8297165 100644
--- a/crates/shirabe/src/installer/noop_installer.rs
+++ b/crates/shirabe/src/installer/noop_installer.rs
@@ -8,6 +8,7 @@ use shirabe_php_shim::{InvalidArgumentException, PhpMixed};
#[derive(Debug)]
pub struct NoopInstaller;
+#[async_trait::async_trait(?Send)]
impl InstallerInterface for NoopInstaller {
fn supports(&self, _package_type: &str) -> bool {
true
diff --git a/crates/shirabe/src/installer/plugin_installer.rs b/crates/shirabe/src/installer/plugin_installer.rs
index 4f9918e..a540677 100644
--- a/crates/shirabe/src/installer/plugin_installer.rs
+++ b/crates/shirabe/src/installer/plugin_installer.rs
@@ -41,7 +41,7 @@ impl PluginInstaller {
self.get_plugin_manager().borrow_mut().disable_plugins();
}
- fn rollback_install(
+ async fn rollback_install(
&mut self,
e: anyhow::Error,
repo: &mut dyn InstalledRepositoryInterface,
@@ -51,7 +51,7 @@ impl PluginInstaller {
"Plugin initialization failed ({}), uninstalling plugin",
e
));
- self.inner.uninstall(repo, package)?;
+ self.inner.uninstall(repo, package).await?;
Err(e)
}
@@ -61,6 +61,7 @@ impl PluginInstaller {
}
}
+#[async_trait::async_trait(?Send)]
impl InstallerInterface for PluginInstaller {
fn supports(&self, package_type: &str) -> bool {
package_type == "composer-plugin" || package_type == "composer-installer"
diff --git a/crates/shirabe/src/installer/project_installer.rs b/crates/shirabe/src/installer/project_installer.rs
index 034a07a..8960854 100644
--- a/crates/shirabe/src/installer/project_installer.rs
+++ b/crates/shirabe/src/installer/project_installer.rs
@@ -29,6 +29,7 @@ impl ProjectInstaller {
}
}
+#[async_trait::async_trait(?Send)]
impl InstallerInterface for ProjectInstaller {
fn supports(&self, _package_type: &str) -> bool {
true