//! 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::{ InvalidArgumentException, LogicException, is_link, preg_quote, realpath, rmdir, rtrim, strpos, }; 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>>, pub(crate) io: Box, pub(crate) r#type: Option, pub(crate) filesystem: std::rc::Rc>, pub(crate) binary_installer: BinaryInstaller, } impl LibraryInstaller { /// Initializes library installer. pub fn new( io: Box, composer: PartialComposer, r#type: Option, filesystem: Option>>, binary_installer: Option, ) -> Self { // PHP: $this->downloadManager = $composer instanceof Composer ? $composer->getDownloadManager() : null; let download_manager = if let Some(full_composer) = composer.as_any().downcast_ref::() { // 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(|| std::rc::Rc::new(std::cell::RefCell::new(Filesystem::new(None)))); let vendor_dir = rtrim( // TODO(phase-b): composer.get_config().borrow_mut().get("vendor-dir") returns a PhpMixed/String &composer.get_config().borrow_mut().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().borrow_mut().get("bin-dir"), Some("/"), ), composer.get_config().borrow_mut().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|null pub(crate) fn install_code( &self, package: &dyn PackageInterface, ) -> Result>> { let download_path = self.get_install_path(package).unwrap(); self.get_download_manager() .borrow() .install(package, &download_path) } /// @return PromiseInterface|null /// @phpstan-return PromiseInterface|null pub(crate) fn update_code( &self, initial: &dyn PackageInterface, target: &dyn PackageInterface, ) -> Result>> { 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> { // 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 .borrow_mut() .rename(&initial_download_path, &target_download_path); } self.get_download_manager() .borrow() .update(initial, target, &target_download_path) } /// @return PromiseInterface|null /// @phpstan-return PromiseInterface|null pub(crate) fn remove_code( &self, package: &dyn PackageInterface, ) -> Result>> { let download_path = self.get_package_base_path(package); self.get_download_manager() .borrow() .remove(package, &download_path) } pub(crate) fn initialize_vendor_dir(&mut self) { self.filesystem .borrow_mut() .ensure_directory_exists(&self.vendor_dir); // TODO(phase-b): realpath returns Option; PHP assigns to vendorDir even when false self.vendor_dir = realpath(&self.vendor_dir).unwrap(); } pub(crate) fn get_download_manager(&self) -> &std::rc::Rc> { // 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.borrow_mut().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>> { // 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() .borrow() .download(package, &download_path, prev_package) } fn prepare( &self, r#type: &str, package: &dyn PackageInterface, prev_package: Option<&dyn PackageInterface>, ) -> Result>> { // 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() .borrow() .prepare(r#type, package, &download_path, prev_package) } fn cleanup( &self, r#type: &str, package: &dyn PackageInterface, prev_package: Option<&dyn PackageInterface>, ) -> Result>> { // 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() .borrow() .cleanup(r#type, package, &download_path, prev_package) } fn install( &mut self, repo: &mut dyn InstalledRepositoryInterface, package: &dyn PackageInterface, ) -> Result>> { // 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_package_box())?; } Ok(()) })))) } fn update( &mut self, repo: &mut dyn InstalledRepositoryInterface, initial: &dyn PackageInterface, target: &dyn PackageInterface, ) -> Result>> { 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_package_box())?; } Ok(()) })))) } fn uninstall( &mut self, repo: &mut dyn InstalledRepositoryInterface, package: &dyn PackageInterface, ) -> Result>> { 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.borrow().is_dir_empty(&package_vendor_dir) { Silencer::call(|| { rmdir(&package_vendor_dir); Ok(()) })?; } } Ok(()) })))) } fn get_install_path(&self, package: &dyn PackageInterface) -> Option { // 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) } }