aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/src/installer
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-05-16 14:42:08 +0900
committernsfisis <nsfisis@gmail.com>2026-05-16 14:42:08 +0900
commita997321d92fb0dbd40febf92f605a94396f847a1 (patch)
treeb178f27cbe51efba7c7d6c27606303f6caac4dcc /crates/shirabe/src/installer
parent5998aea69f6bd1edb1117ae1ed1e2ee9e579b169 (diff)
downloadphp-shirabe-a997321d92fb0dbd40febf92f605a94396f847a1.tar.gz
php-shirabe-a997321d92fb0dbd40febf92f605a94396f847a1.tar.zst
php-shirabe-a997321d92fb0dbd40febf92f605a94396f847a1.zip
feat(port): port LibraryInstaller.php
Diffstat (limited to 'crates/shirabe/src/installer')
-rw-r--r--crates/shirabe/src/installer/library_installer.rs427
1 files changed, 427 insertions, 0 deletions
diff --git a/crates/shirabe/src/installer/library_installer.rs b/crates/shirabe/src/installer/library_installer.rs
index 6427572..cc6e7a6 100644
--- a/crates/shirabe/src/installer/library_installer.rs
+++ b/crates/shirabe/src/installer/library_installer.rs
@@ -1 +1,428 @@
//! ref: composer/src/Composer/Installer/LibraryInstaller.php
+
+use std::any::Any;
+
+use anyhow::Result;
+use shirabe_external_packages::composer::pcre::preg::Preg;
+use shirabe_external_packages::react::promise::promise_interface::PromiseInterface;
+use shirabe_php_shim::{
+ is_link, preg_quote, realpath, rmdir, rtrim, strpos, InvalidArgumentException, LogicException,
+};
+
+use crate::composer::Composer;
+use crate::downloader::download_manager::DownloadManager;
+use crate::installer::binary_installer::BinaryInstaller;
+use crate::installer::binary_presence_interface::BinaryPresenceInterface;
+use crate::installer::installer_interface::InstallerInterface;
+use crate::io::io_interface::IOInterface;
+use crate::package::package_interface::PackageInterface;
+use crate::partial_composer::PartialComposer;
+use crate::repository::installed_repository_interface::InstalledRepositoryInterface;
+use crate::util::filesystem::Filesystem;
+use crate::util::platform::Platform;
+use crate::util::silencer::Silencer;
+
+/// Package installation manager.
+#[derive(Debug)]
+pub struct LibraryInstaller {
+ pub(crate) composer: PartialComposer,
+ pub(crate) vendor_dir: String,
+ pub(crate) download_manager: Option<DownloadManager>,
+ pub(crate) io: Box<dyn IOInterface>,
+ pub(crate) r#type: Option<String>,
+ pub(crate) filesystem: Filesystem,
+ pub(crate) binary_installer: BinaryInstaller,
+}
+
+impl LibraryInstaller {
+ /// Initializes library installer.
+ pub fn new(
+ io: Box<dyn IOInterface>,
+ composer: PartialComposer,
+ r#type: Option<String>,
+ filesystem: Option<Filesystem>,
+ binary_installer: Option<BinaryInstaller>,
+ ) -> Self {
+ // PHP: $this->downloadManager = $composer instanceof Composer ? $composer->getDownloadManager() : null;
+ let download_manager = if let Some(full_composer) =
+ (composer.as_any() as &dyn Any).downcast_ref::<Composer>()
+ {
+ // TODO(phase-b): clone or borrow the DownloadManager from the full Composer
+ Some(todo!("composer.get_download_manager() as DownloadManager"))
+ } else {
+ None
+ };
+
+ let filesystem = filesystem.unwrap_or_else(Filesystem::new);
+ let vendor_dir = rtrim(
+ // TODO(phase-b): composer.get_config().get("vendor-dir") returns a PhpMixed/String
+ &composer.get_config().get("vendor-dir"),
+ Some("/"),
+ );
+ let binary_installer = binary_installer.unwrap_or_else(|| {
+ BinaryInstaller::new(
+ // TODO(phase-b): pass io by reference/clone
+ todo!("io reference"),
+ rtrim(&composer.get_config().get("bin-dir"), Some("/")),
+ composer.get_config().get("bin-compat"),
+ // TODO(phase-b): pass filesystem reference
+ todo!("filesystem reference"),
+ vendor_dir.clone(),
+ )
+ });
+
+ Self {
+ composer,
+ download_manager,
+ io,
+ r#type,
+ filesystem,
+ vendor_dir,
+ binary_installer,
+ }
+ }
+
+ /// Make sure binaries are installed for a given package.
+ pub fn ensure_binaries_presence(&self, package: &dyn PackageInterface) {
+ self.binary_installer
+ .install_binaries(package, &self.get_install_path(package).unwrap(), false);
+ }
+
+ /// Returns the base path of the package without target-dir path
+ ///
+ /// It is used for BC as getInstallPath tends to be overridden by
+ /// installer plugins but not getPackageBasePath
+ pub(crate) fn get_package_base_path(&self, package: &dyn PackageInterface) -> String {
+ let install_path = self.get_install_path(package).unwrap();
+ let target_dir = package.get_target_dir();
+
+ if let Some(target_dir) = target_dir {
+ if !target_dir.is_empty() {
+ return Preg::replace(
+ &format!(
+ "{{/*{}/?$}}",
+ preg_quote(&target_dir, None).replace('/', "/+")
+ ),
+ "",
+ &install_path,
+ );
+ }
+ }
+
+ install_path
+ }
+
+ /// @return PromiseInterface|null
+ /// @phpstan-return PromiseInterface<void|null>|null
+ pub(crate) fn install_code(
+ &self,
+ package: &dyn PackageInterface,
+ ) -> Result<Option<Box<dyn PromiseInterface>>> {
+ let download_path = self.get_install_path(package).unwrap();
+
+ self.get_download_manager().install(package, &download_path)
+ }
+
+ /// @return PromiseInterface|null
+ /// @phpstan-return PromiseInterface<void|null>|null
+ pub(crate) fn update_code(
+ &self,
+ initial: &dyn PackageInterface,
+ target: &dyn PackageInterface,
+ ) -> Result<Option<Box<dyn PromiseInterface>>> {
+ let initial_download_path = self.get_install_path(initial).unwrap();
+ let target_download_path = self.get_install_path(target).unwrap();
+ if target_download_path != initial_download_path {
+ // if the target and initial dirs intersect, we force a remove + install
+ // to avoid the rename wiping the target dir as part of the initial dir cleanup
+ if strpos(&initial_download_path, &target_download_path) == Some(0)
+ || strpos(&target_download_path, &initial_download_path) == Some(0)
+ {
+ let promise = self.remove_code(initial)?;
+ let promise = match promise {
+ Some(p) => p,
+ None => shirabe_external_packages::react::promise::resolve(None),
+ };
+
+ return Ok(Some(promise.then(Box::new(
+ move || -> Result<Box<dyn PromiseInterface>> {
+ // TODO(phase-b): capture target/self into the closure
+ let promise = self.install_code(target)?;
+ if let Some(promise) = promise {
+ return Ok(promise);
+ }
+
+ Ok(shirabe_external_packages::react::promise::resolve(None))
+ },
+ ))));
+ }
+
+ self.filesystem
+ .rename(&initial_download_path, &target_download_path);
+ }
+
+ self.get_download_manager()
+ .update(initial, target, &target_download_path)
+ }
+
+ /// @return PromiseInterface|null
+ /// @phpstan-return PromiseInterface<void|null>|null
+ pub(crate) fn remove_code(
+ &self,
+ package: &dyn PackageInterface,
+ ) -> Result<Option<Box<dyn PromiseInterface>>> {
+ let download_path = self.get_package_base_path(package);
+
+ self.get_download_manager().remove(package, &download_path)
+ }
+
+ pub(crate) fn initialize_vendor_dir(&mut self) {
+ self.filesystem.ensure_directory_exists(&self.vendor_dir);
+ // TODO(phase-b): realpath returns Option<String>; PHP assigns to vendorDir even when false
+ self.vendor_dir = realpath(&self.vendor_dir).unwrap();
+ }
+
+ pub(crate) fn get_download_manager(&self) -> &DownloadManager {
+ // PHP: assert($this->downloadManager instanceof DownloadManager, new \LogicException(...))
+ assert!(
+ self.download_manager.is_some(),
+ "{}",
+ LogicException {
+ message: format!(
+ "{} should be initialized with a fully loaded Composer instance to be able to install/... packages",
+ "LibraryInstaller",
+ ),
+ code: 0,
+ }
+ .message
+ );
+
+ self.download_manager.as_ref().unwrap()
+ }
+}
+
+impl InstallerInterface for LibraryInstaller {
+ fn supports(&self, package_type: &str) -> bool {
+ match &self.r#type {
+ Some(t) => package_type == t,
+ None => true,
+ }
+ }
+
+ fn is_installed(
+ &self,
+ repo: &dyn InstalledRepositoryInterface,
+ package: &dyn PackageInterface,
+ ) -> bool {
+ if !repo.has_package(package) {
+ return false;
+ }
+
+ let install_path = self.get_install_path(package).unwrap();
+
+ if Filesystem::is_readable(&install_path) {
+ return true;
+ }
+
+ if Platform::is_windows() && self.filesystem.is_junction(&install_path) {
+ return true;
+ }
+
+ if is_link(&install_path) {
+ if realpath(&install_path).is_none() {
+ return false;
+ }
+
+ return true;
+ }
+
+ false
+ }
+
+ fn download(
+ &self,
+ package: &dyn PackageInterface,
+ prev_package: Option<&dyn PackageInterface>,
+ ) -> Result<Option<Box<dyn PromiseInterface>>> {
+ // TODO(phase-b): initialize_vendor_dir requires &mut self
+ // self.initialize_vendor_dir();
+ let download_path = self.get_install_path(package).unwrap();
+
+ self.get_download_manager()
+ .download(package, &download_path, prev_package)
+ }
+
+ fn prepare(
+ &self,
+ r#type: &str,
+ package: &dyn PackageInterface,
+ prev_package: Option<&dyn PackageInterface>,
+ ) -> Result<Option<Box<dyn PromiseInterface>>> {
+ // TODO(phase-b): initialize_vendor_dir requires &mut self
+ // self.initialize_vendor_dir();
+ let download_path = self.get_install_path(package).unwrap();
+
+ self.get_download_manager()
+ .prepare(r#type, package, &download_path, prev_package)
+ }
+
+ fn cleanup(
+ &self,
+ r#type: &str,
+ package: &dyn PackageInterface,
+ prev_package: Option<&dyn PackageInterface>,
+ ) -> Result<Option<Box<dyn PromiseInterface>>> {
+ // TODO(phase-b): initialize_vendor_dir requires &mut self
+ // self.initialize_vendor_dir();
+ let download_path = self.get_install_path(package).unwrap();
+
+ self.get_download_manager()
+ .cleanup(r#type, package, &download_path, prev_package)
+ }
+
+ fn install(
+ &self,
+ repo: &mut dyn InstalledRepositoryInterface,
+ package: &dyn PackageInterface,
+ ) -> Result<Option<Box<dyn PromiseInterface>>> {
+ // TODO(phase-b): initialize_vendor_dir requires &mut self
+ // self.initialize_vendor_dir();
+ let download_path = self.get_install_path(package).unwrap();
+
+ // remove the binaries if it appears the package files are missing
+ if !Filesystem::is_readable(&download_path) && repo.has_package(package) {
+ self.binary_installer.remove_binaries(package);
+ }
+
+ let promise = self.install_code(package)?;
+ let promise = match promise {
+ Some(p) => p,
+ None => shirabe_external_packages::react::promise::resolve(None),
+ };
+
+ let binary_installer = &self.binary_installer;
+ let install_path = self.get_install_path(package).unwrap();
+
+ // TODO(phase-b): capture binary_installer/install_path/package/repo into the closure
+ Ok(Some(promise.then(Box::new(move || -> Result<()> {
+ binary_installer.install_binaries(package, &install_path, true);
+ if !repo.has_package(package) {
+ repo.add_package(package.clone_box())?;
+ }
+ Ok(())
+ }))))
+ }
+
+ fn update(
+ &self,
+ repo: &mut dyn InstalledRepositoryInterface,
+ initial: &dyn PackageInterface,
+ target: &dyn PackageInterface,
+ ) -> Result<Option<Box<dyn PromiseInterface>>> {
+ if !repo.has_package(initial) {
+ return Err(InvalidArgumentException {
+ message: format!("Package is not installed: {}", initial),
+ code: 0,
+ }
+ .into());
+ }
+
+ // TODO(phase-b): initialize_vendor_dir requires &mut self
+ // self.initialize_vendor_dir();
+
+ self.binary_installer.remove_binaries(initial);
+ let promise = self.update_code(initial, target)?;
+ let promise = match promise {
+ Some(p) => p,
+ None => shirabe_external_packages::react::promise::resolve(None),
+ };
+
+ let binary_installer = &self.binary_installer;
+ let install_path = self.get_install_path(target).unwrap();
+
+ // TODO(phase-b): capture binary_installer/install_path/target/initial/repo into the closure
+ Ok(Some(promise.then(Box::new(move || -> Result<()> {
+ binary_installer.install_binaries(target, &install_path, true);
+ repo.remove_package(initial)?;
+ if !repo.has_package(target) {
+ repo.add_package(target.clone_box())?;
+ }
+ Ok(())
+ }))))
+ }
+
+ fn uninstall(
+ &self,
+ repo: &mut dyn InstalledRepositoryInterface,
+ package: &dyn PackageInterface,
+ ) -> Result<Option<Box<dyn PromiseInterface>>> {
+ if !repo.has_package(package) {
+ return Err(InvalidArgumentException {
+ message: format!("Package is not installed: {}", package),
+ code: 0,
+ }
+ .into());
+ }
+
+ let promise = self.remove_code(package)?;
+ let promise = match promise {
+ Some(p) => p,
+ None => shirabe_external_packages::react::promise::resolve(None),
+ };
+
+ let binary_installer = &self.binary_installer;
+ let download_path = self.get_package_base_path(package);
+ let filesystem = &self.filesystem;
+
+ // TODO(phase-b): capture binary_installer/filesystem/download_path/package/repo into the closure
+ Ok(Some(promise.then(Box::new(move || -> Result<()> {
+ binary_installer.remove_binaries(package);
+ repo.remove_package(package)?;
+
+ if strpos(package.get_name(), "/").is_some() {
+ let package_vendor_dir = shirabe_php_shim::dirname(&download_path);
+ if shirabe_php_shim::is_dir(&package_vendor_dir)
+ && filesystem.is_dir_empty(&package_vendor_dir)
+ {
+ Silencer::call(|| {
+ rmdir(&package_vendor_dir);
+ Ok(())
+ })?;
+ }
+ }
+ Ok(())
+ }))))
+ }
+
+ fn get_install_path(&self, package: &dyn PackageInterface) -> Option<String> {
+ // TODO(phase-b): initialize_vendor_dir requires &mut self
+ // self.initialize_vendor_dir();
+
+ let base_path = format!(
+ "{}{}",
+ if !self.vendor_dir.is_empty() {
+ format!("{}/", self.vendor_dir)
+ } else {
+ String::new()
+ },
+ package.get_pretty_name(),
+ );
+ let target_dir = package.get_target_dir();
+
+ Some(if let Some(target_dir) = target_dir {
+ if !target_dir.is_empty() {
+ format!("{}/{}", base_path, target_dir)
+ } else {
+ base_path
+ }
+ } else {
+ base_path
+ })
+ }
+}
+
+impl BinaryPresenceInterface for LibraryInstaller {
+ fn ensure_binaries_presence(&self, package: &dyn PackageInterface) {
+ LibraryInstaller::ensure_binaries_presence(self, package)
+ }
+}