diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-05-20 08:33:49 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-05-20 08:33:57 +0900 |
| commit | f31b101ce1e921a026ba234b1f0a83b0392bc118 (patch) | |
| tree | b7ac2aa84d71ebd162cc21aeab0240e7e0544988 /crates/shirabe/src/downloader | |
| parent | 5e31fa33c3b5cf726a57a063b8e7a070869250fe (diff) | |
| download | php-shirabe-f31b101ce1e921a026ba234b1f0a83b0392bc118.tar.gz php-shirabe-f31b101ce1e921a026ba234b1f0a83b0392bc118.tar.zst php-shirabe-f31b101ce1e921a026ba234b1f0a83b0392bc118.zip | |
fix(compile): fix all remaining compile errors
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe/src/downloader')
17 files changed, 926 insertions, 458 deletions
diff --git a/crates/shirabe/src/downloader/archive_downloader.rs b/crates/shirabe/src/downloader/archive_downloader.rs index 937add0..45121ee 100644 --- a/crates/shirabe/src/downloader/archive_downloader.rs +++ b/crates/shirabe/src/downloader/archive_downloader.rs @@ -9,6 +9,7 @@ use shirabe_php_shim::{ }; use crate::dependency_resolver::operation::install_operation::InstallOperation; +use crate::downloader::downloader_interface::DownloaderInterface; use crate::downloader::file_downloader::FileDownloader; use crate::package::package_interface::PackageInterface; use crate::util::platform::Platform; @@ -69,7 +70,14 @@ pub trait ArchiveDownloader { )); } - let vendor_dir = self.inner().config.borrow_mut().get("vendor-dir"); + let vendor_dir = self + .inner() + .config + .borrow_mut() + .get("vendor-dir") + .as_string() + .unwrap_or("") + .to_string(); // clean up the target directory, unless it contains the vendor dir, as the vendor dir contains // the archive to be extracted. This is the case when installing with create-project in the current directory @@ -90,21 +98,20 @@ pub trait ArchiveDownloader { self.inner_mut() .filesystem .borrow_mut() - .empty_directory(path); + .empty_directory(path, true); } - let temporary_dir; - loop { - temporary_dir = format!("{}/composer/{}", vendor_dir, bin2hex(&random_bytes(4))); - if !is_dir(&temporary_dir) { - break; + let temporary_dir = loop { + let candidate = format!("{}/composer/{}", vendor_dir, bin2hex(&random_bytes(4))); + if !is_dir(&candidate) { + break candidate; } - } + }; self.inner_mut().add_cleanup_path(package, &temporary_dir); // avoid cleaning up $path if installing in "." for eg create-project as we can not // delete the directory we are currently in on windows - if !is_dir(path) || realpath(path) != Platform::get_cwd(false).unwrap_or_default() { + if !is_dir(path) || realpath(path) != Some(Platform::get_cwd(false).unwrap_or_default()) { self.inner_mut().add_cleanup_path(package, path); } @@ -114,136 +121,21 @@ pub trait ArchiveDownloader { .ensure_directory_exists(&temporary_dir); let file_name = self.inner().get_file_name(package, path); - let filesystem = &self.inner().filesystem; - - let cleanup = move || { - // remove cache if the file was corrupted - self.inner_mut().clear_last_cache_write(package); - - // clean up - filesystem.borrow_mut().remove_directory(&temporary_dir); - if is_dir(path) && realpath(path) != Platform::get_cwd(false).unwrap_or_default() { - filesystem.borrow_mut().remove_directory(path); - } - self.inner_mut() - .remove_cleanup_path(package, &temporary_dir); - let realpath_result = realpath(path); - if let Some(realpath_val) = realpath_result { - self.inner_mut().remove_cleanup_path(package, &realpath_val); - } - }; - - let promise = match self.extract(package, &file_name, &temporary_dir) { - Ok(p) => p, - Err(e) => { - cleanup(); - return Err(e); - } - }; - - Ok(promise.then( - Box::new(move || -> Result<Box<dyn PromiseInterface>> { - if file_exists(&file_name) { - filesystem.borrow_mut().unlink(&file_name); - } - - let get_folder_content = |dir: &str| -> Vec<std::path::PathBuf> { - let finder = Finder::create() - .ignore_vcs(false) - .ignore_dot_files(false) - .not_name(".DS_Store") - .depth(0) - .in_(dir); - - finder.into_iter().collect() - }; - - let mut rename_recursively: Option<Box<dyn Fn(&str, &str) -> Result<()>>> = None; - // Renames (and recursively merges if needed) a folder into another one - // - // For custom installers, where packages may share paths, and given Composer 2's parallelism, we need to make sure - // that the source directory gets merged into the target one if the target exists. Otherwise rename() by default would - // put the source into the target e.g. src/ => target/src/ (assuming target exists) instead of src/ => target/ - rename_recursively = Some(Box::new(move |from: &str, to: &str| -> Result<()> { - let content_dir = get_folder_content(from); - - // move files back out of the temp dir - for file in &content_dir { - let file = file.to_string_lossy().to_string(); - let file_basename = shirabe_php_shim::basename(&file); - if is_dir(&format!("{}/{}", to, file_basename)) { - if !is_dir(&file) { - return Err(RuntimeException { - message: format!("Installing {} would lead to overwriting the {}/{} directory with a file from the package, invalid operation.", package, to, file_basename), - code: 0, - }.into()); - } - rename_recursively.as_ref().unwrap()( - &file, - &format!("{}/{}", to, file_basename), - )?; - } else { - filesystem.borrow_mut().rename(&file, &format!("{}/{}", to, file_basename)); - } - } - - Ok(()) - })); - - let mut rename_as_one = false; - if !file_exists(path) { - rename_as_one = true; - } else if filesystem.borrow().is_dir_empty(path) { - match filesystem.borrow_mut().remove_directory_php(path) { - Ok(true) => { - rename_as_one = true; - } - _ => { - // ignore error, and simply do not renameAsOne - } - } - } - - let content_dir = get_folder_content(&temporary_dir); - let single_dir_at_top_level = - content_dir.len() == 1 - && is_dir(&content_dir[0].to_string_lossy().to_string()); - - if rename_as_one { - // if the target $path is clear, we can rename the whole package in one go instead of looping over the contents - let extracted_dir = if single_dir_at_top_level { - content_dir[0].to_string_lossy().to_string() - } else { - temporary_dir.clone() - }; - filesystem.borrow_mut().rename(&extracted_dir, path); - } else { - // only one dir in the archive, extract its contents out of it - let from = if single_dir_at_top_level { - content_dir[0].to_string_lossy().to_string() - } else { - temporary_dir.clone() - }; - - rename_recursively.as_ref().unwrap()(&from, path)?; - } + let _ = file_name; - let promise = filesystem.borrow_mut().remove_directory_async(&temporary_dir); + let promise = self.extract(package, "", &temporary_dir)?; - Ok(promise.then( - Box::new(move || -> Result<()> { - self.inner_mut().remove_cleanup_path(package, &temporary_dir); - self.inner_mut().remove_cleanup_path(package, path); - Ok(()) - }), - None, - )) - }), - Box::new(move |e: anyhow::Error| -> Result<()> { - cleanup(); - Err(e) - }), - )) + // TODO(phase-b): the original PHP chains React promise `.then(onFulfilled, onRejected)` + // callbacks that capture `$this`, `$filesystem`, `$package`, `$path`, `$temporaryDir`, + // `$fileName`, and a recursive `$renameRecursively` closure. PromiseInterface::then in + // Rust expects `FnOnce(Option<PhpMixed>) -> Option<PhpMixed>` and the callbacks here + // need both `&mut self` access and to return another promise. This needs a structural + // rework (likely splitting the trait or adding a `then_boxed_result` adapter), plus a + // way to share `&mut self` with the closure (probably `Rc<RefCell<...>>`). + let _ = (&promise, &temporary_dir, package, path); + todo!( + "ArchiveDownloader::install: rewire .then(onFulfilled, onRejected) chain to match PromiseInterface signature" + ) } /// @inheritDoc diff --git a/crates/shirabe/src/downloader/download_manager.rs b/crates/shirabe/src/downloader/download_manager.rs index ea2ee1e..630f16d 100644 --- a/crates/shirabe/src/downloader/download_manager.rs +++ b/crates/shirabe/src/downloader/download_manager.rs @@ -158,7 +158,7 @@ impl DownloadManager { message: sprintf( "Downloader \"%s\" is a %s type downloader and can not be used to download %s for package %s", &[ - PhpMixed::String(get_class(downloader)), + PhpMixed::String(shirabe_php_shim::get_class_obj(downloader)), PhpMixed::String(downloader.get_installation_source()), PhpMixed::String(installation_source.unwrap_or("").to_string()), PhpMixed::String(package.to_string()), @@ -273,9 +273,12 @@ impl DownloadManager { // PHP: $result->then(static fn ($res) => $res, $handleError); // TODO(phase-b): chain $handleError as the rejection handler on the promise - let res = result.then(Box::new(move |res: PhpMixed| -> Result<PhpMixed> { - Ok(res) - })); + let res = result.then( + Some(Box::new(move |res: Option<PhpMixed>| -> Option<PhpMixed> { + res + })), + None, + ); return Ok(res); } @@ -384,12 +387,15 @@ impl DownloadManager { let promise = initial_downloader.unwrap().remove2(initial, &target_dir)?; let target_dir_owned = target_dir.clone(); - // TODO(phase-b): capture self and target into the closure - Ok(promise.then(Box::new( - move |_res: PhpMixed| -> Result<Box<dyn PromiseInterface>> { + // TODO(phase-b): capture self and target into the closure; type mismatch with then signature. + let _ = target_dir_owned; + Ok(promise.then( + Some(Box::new(move |res: Option<PhpMixed>| -> Option<PhpMixed> { + let _ = res; todo!("self.install(target, &target_dir_owned)") - }, - ))) + })), + None, + )) } /// Removes package from target dir. diff --git a/crates/shirabe/src/downloader/downloader_interface.rs b/crates/shirabe/src/downloader/downloader_interface.rs index b72d80e..11ec928 100644 --- a/crates/shirabe/src/downloader/downloader_interface.rs +++ b/crates/shirabe/src/downloader/downloader_interface.rs @@ -79,4 +79,27 @@ pub trait DownloaderInterface: std::fmt::Debug { path: &str, prev_package: Option<&dyn PackageInterface>, ) -> anyhow::Result<Box<dyn PromiseInterface>>; + + /// TODO(phase-b): runtime downcast helpers for PHP `instanceof` checks. + fn as_change_report_interface( + &self, + ) -> Option<&dyn crate::downloader::change_report_interface::ChangeReportInterface> { + None + } + + /// TODO(phase-b): runtime downcast helpers for PHP `instanceof` checks. + fn as_vcs_capable_downloader_interface( + &self, + ) -> Option< + &dyn crate::downloader::vcs_capable_downloader_interface::VcsCapableDownloaderInterface, + > { + None + } + + /// TODO(phase-b): runtime downcast helpers for PHP `instanceof` checks. + fn as_dvcs_downloader_interface( + &self, + ) -> Option<&dyn crate::downloader::dvcs_downloader_interface::DvcsDownloaderInterface> { + None + } } diff --git a/crates/shirabe/src/downloader/file_downloader.rs b/crates/shirabe/src/downloader/file_downloader.rs index c814baa..48160e6 100644 --- a/crates/shirabe/src/downloader/file_downloader.rs +++ b/crates/shirabe/src/downloader/file_downloader.rs @@ -67,7 +67,7 @@ pub struct FileDownloader { /// @var ?Cache pub(crate) cache: Option<Cache>, /// @var ?EventDispatcher - pub(crate) event_dispatcher: Option<EventDispatcher>, + pub(crate) event_dispatcher: Option<std::rc::Rc<std::cell::RefCell<EventDispatcher>>>, /// @var ProcessExecutor pub(crate) process: std::rc::Rc<std::cell::RefCell<ProcessExecutor>>, /// @var array<string, string> Map of package name to cache key @@ -77,19 +77,29 @@ pub struct FileDownloader { } impl FileDownloader { + /// TODO(phase-b): `$downloadMetadata` is a static property in PHP; not yet mapped to Rust. + pub fn reset_download_metadata() { + todo!("FileDownloader::reset_download_metadata") + } + + /// TODO(phase-b): `$downloadMetadata` is a static property in PHP; not yet mapped to Rust. + pub fn download_metadata() -> indexmap::IndexMap<String, shirabe_php_shim::PhpMixed> { + todo!("FileDownloader::download_metadata") + } + /// Constructor. pub fn new( io: Box<dyn IOInterface>, config: std::rc::Rc<std::cell::RefCell<Config>>, http_downloader: std::rc::Rc<std::cell::RefCell<HttpDownloader>>, - event_dispatcher: Option<EventDispatcher>, + event_dispatcher: Option<std::rc::Rc<std::cell::RefCell<EventDispatcher>>>, cache: Option<Cache>, filesystem: Option<std::rc::Rc<std::cell::RefCell<Filesystem>>>, process: Option<std::rc::Rc<std::cell::RefCell<ProcessExecutor>>>, ) -> Self { let process = process.unwrap_or_else(|| { std::rc::Rc::new(std::cell::RefCell::new(ProcessExecutor::new(Some( - Box::new(&*io), + io.clone_box(), )))) }); let filesystem = filesystem.unwrap_or_else(|| { @@ -185,7 +195,7 @@ impl DownloaderInterface for FileDownloader { let file_name = self.get_file_name(package, path); self.filesystem.borrow_mut().ensure_directory_exists(path)?; - let dir_of_file = shirabe_php_shim::dirname(&file_name, 1); + let dir_of_file = shirabe_php_shim::dirname(&file_name); self.filesystem .borrow_mut() .ensure_directory_exists(&dir_of_file)?; @@ -209,7 +219,7 @@ impl DownloaderInterface for FileDownloader { _path: &str, _prev_package: Option<&dyn PackageInterface>, ) -> Result<Box<dyn PromiseInterface>> { - Ok(react_promise_resolve(PhpMixed::Null)) + Ok(react_promise_resolve(Some(PhpMixed::Null))) } /// @inheritDoc @@ -257,14 +267,14 @@ impl DownloaderInterface for FileDownloader { for dir in &dirs_to_clean_up { if is_dir(dir) - && self.filesystem.borrow_mut().is_dir_empty(dir)? + && self.filesystem.borrow_mut().is_dir_empty(dir) && realpath(dir).as_deref() != Some(&Platform::get_cwd(false).unwrap_or_default()) { self.filesystem.borrow_mut().remove_directory_php(dir)?; } } - Ok(react_promise_resolve(PhpMixed::Null)) + Ok(react_promise_resolve(Some(PhpMixed::Null))) } /// @inheritDoc @@ -379,8 +389,11 @@ impl ChangeReportInterface for FileDownloader { let mut null_io = NullIO::new(); null_io.load_configuration(&mut *self.config.borrow_mut())?; - let mut e: Option<anyhow::Error> = None; - let mut output: String = String::new(); + // TODO(phase-b): `e` is captured by both the inner closure (assignment in error handler) + // and the outer block (read after the closure). PHP closures capture by reference (`use (&$e)`); + // emulate via Rc<RefCell> or restructure when proper async/promise types land. + let e: std::cell::RefCell<Option<anyhow::Error>> = std::cell::RefCell::new(None); + let output_cell: std::cell::RefCell<String> = std::cell::RefCell::new(String::new()); let target_dir = Filesystem::trim_trailing_slash(path); let result: Result<()> = (|| -> Result<()> { @@ -400,8 +413,8 @@ impl ChangeReportInterface for FileDownloader { })), ); self.http_downloader.borrow_mut().wait()?; - if e.is_some() { - return Err(e.unwrap()); + if e.borrow().is_some() { + return Err(e.borrow_mut().take().unwrap()); } let promise = self.install(package, &format!("{}_compare", target_dir), false)?; promise.then_with( @@ -412,23 +425,25 @@ impl ChangeReportInterface for FileDownloader { })), ); self.process.borrow_mut().wait()?; - if e.is_some() { - return Err(e.unwrap()); + if e.borrow().is_some() { + return Err(e.borrow_mut().take().unwrap()); } let mut comparer = Comparer::new(); comparer.set_source(format!("{}_compare", target_dir)); comparer.set_update(target_dir.clone()); comparer.do_compare(); - output = comparer.get_changed_as_string(true, false); + *output_cell.borrow_mut() = comparer.get_changed_as_string(true, false); self.filesystem .borrow_mut() .remove_directory(&format!("{}_compare", target_dir))?; Ok(()) })(); if let Err(err) = result { - e = Some(err); + *e.borrow_mut() = Some(err); } + let e = e.into_inner(); + let output = output_cell.into_inner(); // TODO(phase-b): restore self.io = prev_io @@ -474,24 +489,26 @@ impl FileDownloader { .to_string() } - fn clear_last_cache_write(&mut self, package: &dyn PackageInterface) { + pub(crate) fn clear_last_cache_write(&mut self, package: &dyn PackageInterface) { if self.cache.is_some() && self.last_cache_writes.contains_key(package.get_name()) { - self.cache - .as_ref() + let key = self + .last_cache_writes + .get(package.get_name()) .unwrap() - .remove(self.last_cache_writes.get(package.get_name()).unwrap()); + .clone(); + self.cache.as_mut().unwrap().remove(&key); self.last_cache_writes.shift_remove(package.get_name()); } } - fn add_cleanup_path(&mut self, package: &dyn PackageInterface, path: &str) { + pub(crate) fn add_cleanup_path(&mut self, package: &dyn PackageInterface, path: &str) { self.additional_cleanup_paths .entry(package.get_name().to_string()) .or_insert_with(Vec::new) .push(path.to_string()); } - fn remove_cleanup_path(&mut self, package: &dyn PackageInterface, path: &str) { + pub(crate) fn remove_cleanup_path(&mut self, package: &dyn PackageInterface, path: &str) { if let Some(paths) = self.additional_cleanup_paths.get_mut(package.get_name()) { // PHP: array_search($path, ..., true) let idx = paths.iter().position(|p| p == path); @@ -503,7 +520,7 @@ impl FileDownloader { } /// Gets file name for specific package - fn get_file_name(&self, package: &dyn PackageInterface, _path: &str) -> String { + pub(crate) fn get_file_name(&self, package: &dyn PackageInterface, _path: &str) -> String { let extension = self.get_dist_path(package, PATHINFO_EXTENSION); let extension = if extension.is_empty() { package.get_dist_type().unwrap_or("").to_string() @@ -539,7 +556,7 @@ impl FileDownloader { } /// Process the download url - fn process_url(&self, package: &dyn PackageInterface, url: &str) -> Result<String> { + pub(crate) fn process_url(&self, package: &dyn PackageInterface, url: &str) -> Result<String> { if !shirabe_php_shim::extension_loaded("openssl") && Some(0) == strpos(url, "https:") { return Err(RuntimeException { message: "You must enable the openssl extension to download files via https" @@ -553,7 +570,7 @@ impl FileDownloader { if package.get_dist_reference().is_some() { url = UrlUtil::update_dist_reference( &*self.config.borrow(), - &url, + url, package.get_dist_reference().unwrap(), ); } @@ -571,7 +588,7 @@ struct UrlEntry { // Suppress unused-import warnings for items kept for parity with the PHP source. #[allow(dead_code)] -const _USE_PARITY: () = { +fn _use_parity() { let _ = filesize; let _ = hash_file; let _ = in_array; @@ -581,4 +598,4 @@ const _USE_PARITY: () = { message: String::new(), code: 0, }; -}; +} diff --git a/crates/shirabe/src/downloader/fossil_downloader.rs b/crates/shirabe/src/downloader/fossil_downloader.rs index 53b4315..8842a3a 100644 --- a/crates/shirabe/src/downloader/fossil_downloader.rs +++ b/crates/shirabe/src/downloader/fossil_downloader.rs @@ -1,7 +1,12 @@ //! ref: composer/src/Composer/Downloader/FossilDownloader.php +use crate::config::Config; +use crate::downloader::downloader_interface::DownloaderInterface; use crate::downloader::vcs_downloader::VcsDownloaderBase; +use crate::io::io_interface::IOInterface; use crate::package::package_interface::PackageInterface; +use crate::util::filesystem::Filesystem; +use crate::util::process_executor::ProcessExecutor; use anyhow::Result; use shirabe_external_packages::composer::pcre::preg::Preg; use shirabe_external_packages::react::promise::promise_interface::PromiseInterface; @@ -13,6 +18,17 @@ pub struct FossilDownloader { } impl FossilDownloader { + pub fn new( + io: Box<dyn IOInterface>, + config: std::rc::Rc<std::cell::RefCell<Config>>, + process: std::rc::Rc<std::cell::RefCell<ProcessExecutor>>, + fs: std::rc::Rc<std::cell::RefCell<Filesystem>>, + ) -> Self { + Self { + inner: VcsDownloaderBase::new(io, config, Some(process), Some(fs)), + } + } + pub(crate) fn do_download( &self, _package: &dyn PackageInterface, @@ -31,7 +47,7 @@ impl FossilDownloader { ) -> Result<Box<dyn PromiseInterface>> { self.inner.config.borrow_mut().prohibit_url_by_config( &url, - Some(&self.inner.io), + Some(self.inner.io.as_ref()), &indexmap::IndexMap::new(), )?; @@ -71,7 +87,10 @@ impl FossilDownloader { "fossil".to_string(), "update".to_string(), "--".to_string(), - package.get_source_reference().unwrap_or_default(), + package + .get_source_reference() + .unwrap_or_default() + .to_string(), ], real_path, &mut output, @@ -89,7 +108,7 @@ impl FossilDownloader { ) -> Result<Box<dyn PromiseInterface>> { self.inner.config.borrow_mut().prohibit_url_by_config( &url, - Some(&self.inner.io), + Some(self.inner.io.as_ref()), &indexmap::IndexMap::new(), )?; @@ -120,7 +139,10 @@ impl FossilDownloader { "fossil".to_string(), "up".to_string(), "--".to_string(), - target.get_source_reference().unwrap_or_default(), + target + .get_source_reference() + .unwrap_or_default() + .to_string(), ], real_path, &mut output, @@ -204,7 +226,7 @@ impl FossilDownloader { .inner .process .borrow_mut() - .execute(&command, output, cwd) + .execute(&command, output, cwd)? != 0 { return Err(RuntimeException { @@ -225,3 +247,69 @@ impl FossilDownloader { || std::path::Path::new(&format!("{}/_FOSSIL_", path)).is_file() } } + +// TODO(phase-b): wire up VcsDownloader trait properly. FossilDownloader extends VcsDownloader +// which implements DownloaderInterface in PHP. Delegating each trait method to todo!() until the +// inner VcsDownloaderBase exposes the matching impl surface. +impl DownloaderInterface for FossilDownloader { + fn get_installation_source(&self) -> String { + todo!() + } + + fn download( + &self, + _package: &dyn PackageInterface, + _path: &str, + _prev_package: Option<&dyn PackageInterface>, + _output: bool, + ) -> Result<Box<dyn PromiseInterface>> { + todo!() + } + + fn prepare( + &self, + _type: &str, + _package: &dyn PackageInterface, + _path: &str, + _prev_package: Option<&dyn PackageInterface>, + ) -> Result<Box<dyn PromiseInterface>> { + todo!() + } + + fn install( + &self, + _package: &dyn PackageInterface, + _path: &str, + _output: bool, + ) -> Result<Box<dyn PromiseInterface>> { + todo!() + } + + fn update( + &self, + _initial: &dyn PackageInterface, + _target: &dyn PackageInterface, + _path: &str, + ) -> Result<Box<dyn PromiseInterface>> { + todo!() + } + + fn remove( + &self, + _package: &dyn PackageInterface, + _path: &str, + _output: bool, + ) -> Result<Box<dyn PromiseInterface>> { + todo!() + } + + fn cleanup( + &self, + _type: &str, + _package: &dyn PackageInterface, + _path: &str, + _prev_package: Option<&dyn PackageInterface>, + ) -> Result<Box<dyn PromiseInterface>> { + todo!() + } +} diff --git a/crates/shirabe/src/downloader/git_downloader.rs b/crates/shirabe/src/downloader/git_downloader.rs index d451727..519f48a 100644 --- a/crates/shirabe/src/downloader/git_downloader.rs +++ b/crates/shirabe/src/downloader/git_downloader.rs @@ -93,7 +93,10 @@ impl GitDownloader { &format!( " - Syncing <info>{}</info> (<comment>{}</comment>) into cache", package.get_name(), - package.get_full_pretty_version(), + package.get_full_pretty_version( + true, + <dyn PackageInterface>::DISPLAY_SOURCE_REF_IF_DEV, + ), ), true, io_interface::NORMAL, @@ -112,7 +115,7 @@ impl GitDownloader { &cache_path, r#ref.unwrap_or(""), Some(package.get_pretty_version()), - ) && is_dir(&cache_path) + )? && is_dir(&cache_path) { self.cached_packages .entry(package.get_id()) @@ -736,7 +739,7 @@ impl GitDownloader { let changes: Vec<String> = array_map( |elem: &String| format!(" {}", elem), - &Preg::split(r"{\s*\r?\n\s*}", &changes), + &Preg::split(r"{\s*\r?\n\s*}", &changes)?, ); self.inner.io.write_error3( &format!( @@ -747,16 +750,10 @@ impl GitDownloader { io_interface::NORMAL, ); let slice_end = 10_usize.min(changes.len()); - self.inner.io.write_error3( - PhpMixed::List( - changes[..slice_end] - .iter() - .map(|s| Box::new(PhpMixed::String(s.clone()))) - .collect(), - ), - true, - io_interface::NORMAL, - ); + // TODO(phase-b): PHP passes the list directly to writeError; joined here so write_error3 takes &str + self.inner + .io + .write_error3(&changes[..slice_end].join("\n"), true, io_interface::NORMAL); if (changes.len() as i64) > 10 { self.inner.io.write_error3( &format!( @@ -804,16 +801,10 @@ impl GitDownloader { .into()); } Some("v") => { - self.inner.io.write_error3( - PhpMixed::List( - changes - .iter() - .map(|s| Box::new(PhpMixed::String(s.clone()))) - .collect(), - ), - true, - io_interface::NORMAL, - ); + // TODO(phase-b): PHP passes list directly; joined here for &str arg + self.inner + .io + .write_error3(&changes.join("\n"), true, io_interface::NORMAL); } Some("d") => { self.view_diff(&path); @@ -826,21 +817,21 @@ impl GitDownloader { if do_help { // help: + // TODO(phase-b): PHP passes list directly; joined here for &str arg self.inner.io.write_error3( - PhpMixed::List(vec![ - Box::new(PhpMixed::String(format!( + &[ + format!( " y - discard changes and apply the {}", if update { "update" } else { "uninstall" } - ))), - Box::new(PhpMixed::String(format!( + ), + format!( " n - abort the {} and let you manually clean things up", if update { "update" } else { "uninstall" } - ))), - Box::new(PhpMixed::String(" v - view modified files".to_string())), - Box::new(PhpMixed::String( - " d - view local modifications (diff)".to_string(), - )), - ]), + ), + " v - view modified files".to_string(), + " d - view local modifications (diff)".to_string(), + ] + .join("\n"), true, io_interface::NORMAL, ); @@ -925,7 +916,7 @@ impl GitDownloader { // If the non-existent branch is actually the name of a file, the file // is checked out. - let mut branch = Preg::replace(r"{(?:^dev-|(?:\.x)?-dev$)}i", "", &pretty_version); + let mut branch = Preg::replace(r"{(?:^dev-|(?:\.x)?-dev$)}i", "", &pretty_version)?; // Closure equivalent: $execute = function(array $command) use (&$output, $path) { ... }; // Inlined below at each call site. diff --git a/crates/shirabe/src/downloader/gzip_downloader.rs b/crates/shirabe/src/downloader/gzip_downloader.rs index 4ee9d33..43d174a 100644 --- a/crates/shirabe/src/downloader/gzip_downloader.rs +++ b/crates/shirabe/src/downloader/gzip_downloader.rs @@ -31,7 +31,7 @@ impl GzipDownloader { io: Box<dyn IOInterface>, config: std::rc::Rc<std::cell::RefCell<Config>>, http_downloader: std::rc::Rc<std::cell::RefCell<HttpDownloader>>, - event_dispatcher: Option<EventDispatcher>, + event_dispatcher: Option<std::rc::Rc<std::cell::RefCell<EventDispatcher>>>, cache: Option<Cache>, filesystem: std::rc::Rc<std::cell::RefCell<Filesystem>>, process: std::rc::Rc<std::cell::RefCell<ProcessExecutor>>, @@ -88,7 +88,7 @@ impl GzipDownloader { .collect(), ), Some(&mut process_output), - None, + (), )? == 0 { return Ok(shirabe_external_packages::react::promise::resolve(None)); @@ -129,3 +129,66 @@ impl GzipDownloader { fclose(target_file); } } + +impl crate::downloader::downloader_interface::DownloaderInterface for GzipDownloader { + fn get_installation_source(&self) -> String { + self.inner.get_installation_source() + } + + fn download( + &self, + package: &dyn PackageInterface, + path: &str, + prev_package: Option<&dyn PackageInterface>, + output: bool, + ) -> Result<Box<dyn PromiseInterface>> { + self.inner.download(package, path, prev_package, output) + } + + fn prepare( + &self, + r#type: &str, + package: &dyn PackageInterface, + path: &str, + prev_package: Option<&dyn PackageInterface>, + ) -> Result<Box<dyn PromiseInterface>> { + self.inner.prepare(r#type, package, path, prev_package) + } + + fn install( + &self, + package: &dyn PackageInterface, + path: &str, + output: bool, + ) -> Result<Box<dyn PromiseInterface>> { + self.inner.install(package, path, output) + } + + fn update( + &self, + initial: &dyn PackageInterface, + target: &dyn PackageInterface, + path: &str, + ) -> Result<Box<dyn PromiseInterface>> { + self.inner.update(initial, target, path) + } + + fn remove( + &self, + package: &dyn PackageInterface, + path: &str, + output: bool, + ) -> Result<Box<dyn PromiseInterface>> { + self.inner.remove(package, path, output) + } + + fn cleanup( + &self, + r#type: &str, + package: &dyn PackageInterface, + path: &str, + prev_package: Option<&dyn PackageInterface>, + ) -> Result<Box<dyn PromiseInterface>> { + self.inner.cleanup(r#type, package, path, prev_package) + } +} diff --git a/crates/shirabe/src/downloader/hg_downloader.rs b/crates/shirabe/src/downloader/hg_downloader.rs index 161eb7e..4ccb150 100644 --- a/crates/shirabe/src/downloader/hg_downloader.rs +++ b/crates/shirabe/src/downloader/hg_downloader.rs @@ -1,8 +1,13 @@ //! ref: composer/src/Composer/Downloader/HgDownloader.php +use crate::config::Config; +use crate::downloader::downloader_interface::DownloaderInterface; use crate::downloader::vcs_downloader::VcsDownloaderBase; +use crate::io::io_interface::IOInterface; use crate::package::package_interface::PackageInterface; +use crate::util::filesystem::Filesystem; use crate::util::hg::Hg as HgUtils; +use crate::util::process_executor::ProcessExecutor; use anyhow::Result; use shirabe_external_packages::react::promise::promise_interface::PromiseInterface; use shirabe_php_shim::RuntimeException; @@ -13,6 +18,17 @@ pub struct HgDownloader { } impl HgDownloader { + pub fn new( + io: Box<dyn IOInterface>, + config: std::rc::Rc<std::cell::RefCell<Config>>, + process: std::rc::Rc<std::cell::RefCell<ProcessExecutor>>, + fs: std::rc::Rc<std::cell::RefCell<Filesystem>>, + ) -> Self { + Self { + inner: VcsDownloaderBase::new(io, config, Some(process), Some(fs)), + } + } + pub(crate) fn do_download( &self, package: &dyn PackageInterface, @@ -59,7 +75,10 @@ impl HgDownloader { "hg".to_string(), "up".to_string(), "--".to_string(), - package.get_source_reference().unwrap_or_default(), + package + .get_source_reference() + .unwrap_or_default() + .to_string(), ]; let mut ignored_output = String::new(); if self.inner.process.borrow_mut().execute_args( @@ -95,7 +114,10 @@ impl HgDownloader { &self.inner.process, ); - let ref_ = target.get_source_reference().unwrap_or_default(); + let ref_ = target + .get_source_reference() + .unwrap_or_default() + .to_string(); self.inner.io.write_error(&format!( " Updating to {}", target.get_source_reference().unwrap_or_default() @@ -195,3 +217,69 @@ impl HgDownloader { std::path::Path::new(&format!("{}/.hg", path)).is_dir() } } + +// TODO(phase-b): wire up VcsDownloader trait properly. HgDownloader extends VcsDownloader which +// implements DownloaderInterface in PHP. Delegating each trait method to todo!() until the inner +// VcsDownloaderBase exposes the matching impl surface. +impl DownloaderInterface for HgDownloader { + fn get_installation_source(&self) -> String { + todo!() + } + + fn download( + &self, + _package: &dyn PackageInterface, + _path: &str, + _prev_package: Option<&dyn PackageInterface>, + _output: bool, + ) -> Result<Box<dyn PromiseInterface>> { + todo!() + } + + fn prepare( + &self, + _type: &str, + _package: &dyn PackageInterface, + _path: &str, + _prev_package: Option<&dyn PackageInterface>, + ) -> Result<Box<dyn PromiseInterface>> { + todo!() + } + + fn install( + &self, + _package: &dyn PackageInterface, + _path: &str, + _output: bool, + ) -> Result<Box<dyn PromiseInterface>> { + todo!() + } + + fn update( + &self, + _initial: &dyn PackageInterface, + _target: &dyn PackageInterface, + _path: &str, + ) -> Result<Box<dyn PromiseInterface>> { + todo!() + } + + fn remove( + &self, + _package: &dyn PackageInterface, + _path: &str, + _output: bool, + ) -> Result<Box<dyn PromiseInterface>> { + todo!() + } + + fn cleanup( + &self, + _type: &str, + _package: &dyn PackageInterface, + _path: &str, + _prev_package: Option<&dyn PackageInterface>, + ) -> Result<Box<dyn PromiseInterface>> { + todo!() + } +} diff --git a/crates/shirabe/src/downloader/path_downloader.rs b/crates/shirabe/src/downloader/path_downloader.rs index 26795f3..56ecf0f 100644 --- a/crates/shirabe/src/downloader/path_downloader.rs +++ b/crates/shirabe/src/downloader/path_downloader.rs @@ -11,10 +11,14 @@ use shirabe_php_shim::{ RuntimeException, file_exists, function_exists, is_dir, realpath, }; +use crate::cache::Cache; +use crate::config::Config; use crate::dependency_resolver::operation::install_operation::InstallOperation; use crate::dependency_resolver::operation::uninstall_operation::UninstallOperation; +use crate::downloader::downloader_interface::DownloaderInterface; use crate::downloader::file_downloader::FileDownloader; use crate::downloader::vcs_capable_downloader_interface::VcsCapableDownloaderInterface; +use crate::event_dispatcher::event_dispatcher::EventDispatcher; use crate::io::io_interface::IOInterface; use crate::package::archiver::archivable_files_finder::ArchivableFilesFinder; use crate::package::dumper::array_dumper::ArrayDumper; @@ -22,7 +26,9 @@ use crate::package::package_interface::PackageInterface; use crate::package::version::version_guesser::VersionGuesser; use crate::package::version::version_parser::VersionParser; use crate::util::filesystem::Filesystem; +use crate::util::http_downloader::HttpDownloader; use crate::util::platform::Platform; +use crate::util::process_executor::ProcessExecutor; #[derive(Debug)] pub struct PathDownloader { @@ -33,6 +39,28 @@ impl PathDownloader { const STRATEGY_SYMLINK: i64 = 10; const STRATEGY_MIRROR: i64 = 20; + pub fn new( + io: Box<dyn IOInterface>, + config: std::rc::Rc<std::cell::RefCell<Config>>, + http_downloader: std::rc::Rc<std::cell::RefCell<HttpDownloader>>, + event_dispatcher: Option<std::rc::Rc<std::cell::RefCell<EventDispatcher>>>, + cache: Option<Cache>, + filesystem: std::rc::Rc<std::cell::RefCell<Filesystem>>, + process: std::rc::Rc<std::cell::RefCell<ProcessExecutor>>, + ) -> Self { + Self { + inner: FileDownloader::new( + io, + config, + http_downloader, + event_dispatcher, + cache, + Some(filesystem), + Some(process), + ), + } + } + pub fn download( &mut self, package: &dyn PackageInterface, @@ -140,7 +168,7 @@ impl PathDownloader { let (mut current_strategy, allowed_strategies) = self.compute_allowed_strategies(&transport_options)?; - let symfony_filesystem = SymfonyFilesystem::new(None); + let symfony_filesystem = SymfonyFilesystem::new(); self.inner.filesystem.borrow_mut().remove_directory(&path); if output { @@ -153,58 +181,63 @@ impl PathDownloader { let mut is_fallback = false; if Self::STRATEGY_SYMLINK == current_strategy { - let symlink_result: Result<Result<(), IOException>> = (|| { - if Platform::is_windows() { - // Implement symlinks as NTFS junctions on Windows - if output { - self.inner.io.write_error3( - &format!("Junctioning from {}", url), - false, - io_interface::NORMAL, - ); - } - Ok(self - .inner - .filesystem - .borrow_mut() - .junction(&real_url, &path)) - } else { - let path = path.trim_end_matches('/').to_string(); - if output { - self.inner.io.write_error3( - &format!("Symlinking from {}", url), - false, - io_interface::NORMAL, - ); - } - if transport_options - .get("relative") - .and_then(|v| v.as_bool()) - .unwrap_or(false) - { - let absolute_path = - if !self.inner.filesystem.borrow_mut().is_absolute_path(&path) { - format!( - "{}{}{}", - Platform::get_cwd(false), - DIRECTORY_SEPARATOR, - path - ) - } else { - path.clone() - }; - let shortest_path = self.inner.filesystem.borrow_mut().find_shortest_path( - &absolute_path, - &real_url, - false, - true, - ); - Ok(symfony_filesystem.symlink(&format!("{}/", shortest_path), &path)) + // TODO(phase-b): PHP catches IOException; shim symfony filesystem returns anyhow::Result. + let symlink_result: Result<anyhow::Result<()>> = + (|| { + if Platform::is_windows() { + // Implement symlinks as NTFS junctions on Windows + if output { + self.inner.io.write_error3( + &format!("Junctioning from {}", url), + false, + io_interface::NORMAL, + ); + } + Ok(self + .inner + .filesystem + .borrow_mut() + .junction(&real_url, &path)) } else { - Ok(symfony_filesystem.symlink(&format!("{}/", real_url), &path)) + let path = path.trim_end_matches('/').to_string(); + if output { + self.inner.io.write_error3( + &format!("Symlinking from {}", url), + false, + io_interface::NORMAL, + ); + } + if transport_options + .get("relative") + .and_then(|v| v.as_bool()) + .unwrap_or(false) + { + let absolute_path = + if !self.inner.filesystem.borrow_mut().is_absolute_path(&path) { + format!( + "{}{}{}", + Platform::get_cwd(false)?, + DIRECTORY_SEPARATOR, + path + ) + } else { + path.clone() + }; + let shortest_path = self + .inner + .filesystem + .borrow_mut() + .find_shortest_path(&absolute_path, &real_url, false, true); + Ok(symfony_filesystem.symlink( + &format!("{}/", shortest_path), + &path, + false, + )) + } else { + Ok(symfony_filesystem.symlink(&format!("{}/", real_url), &path, false)) + } } - } - })(); + })(); match symlink_result? { Ok(()) => {} @@ -249,8 +282,9 @@ impl PathDownloader { io_interface::NORMAL, ); } - let iterator = ArchivableFilesFinder::new(&real_url, vec![], false)?; - symfony_filesystem.mirror(&real_url, &path, Some(&iterator)); + let _iterator = ArchivableFilesFinder::new(&real_url, vec![], false)?; + // TODO(phase-b): pass iterator as PhpMixed; ArchivableFilesFinder iterator wrapping not modelled yet. + symfony_filesystem.mirror(&real_url, &path, None, &IndexMap::new())?; } if output { @@ -325,12 +359,12 @@ impl PathDownloader { let abs_path = if fs.is_absolute_path(&path) { path.clone() } else { - format!("{}/{}", Platform::get_cwd(false), path) + format!("{}/{}", Platform::get_cwd(false)?, path) }; let abs_dist_url = if fs.is_absolute_path(&url) { - url.clone() + url.to_string() } else { - format!("{}/{}", Platform::get_cwd(false), url) + format!("{}/{}", Platform::get_cwd(false)?, url) }; if fs.normalize_path(&abs_path) == fs.normalize_path(&abs_dist_url) { if output { @@ -354,7 +388,7 @@ impl PathDownloader { pub fn get_vcs_reference(&self, package: &dyn PackageInterface, path: &str) -> Option<String> { let path = Filesystem::trim_trailing_slash(path); let parser = VersionParser::new(); - let guesser = VersionGuesser::new( + let mut guesser = VersionGuesser::new( std::rc::Rc::clone(&self.inner.config), std::rc::Rc::clone(&self.inner.process), parser.clone(), @@ -364,11 +398,8 @@ impl PathDownloader { let package_config = dumper.dump(package); let package_version = guesser.guess_version(&package_config, &path); - if let Some(version) = package_version { - return version - .get("commit") - .and_then(|v| v.as_string()) - .map(|s| s.to_owned()); + if let Ok(Some(version)) = package_version { + return version.commit; } None @@ -502,3 +533,70 @@ impl VcsCapableDownloaderInterface for PathDownloader { PathDownloader::get_vcs_reference(self, package, &path) } } + +// TODO(phase-b): wire up PathDownloader trait properly. PathDownloader extends FileDownloader and +// overrides download/install/remove with &mut self signatures that diverge from the trait. The +// trait methods here delegate to the inner FileDownloader; the bespoke overrides on the struct +// itself are not yet routed through the trait. +impl DownloaderInterface for PathDownloader { + fn get_installation_source(&self) -> String { + self.inner.get_installation_source() + } + + fn download( + &self, + package: &dyn PackageInterface, + path: &str, + prev_package: Option<&dyn PackageInterface>, + output: bool, + ) -> Result<Box<dyn PromiseInterface>> { + self.inner.download(package, path, prev_package, output) + } + + fn prepare( + &self, + r#type: &str, + package: &dyn PackageInterface, + path: &str, + prev_package: Option<&dyn PackageInterface>, + ) -> Result<Box<dyn PromiseInterface>> { + self.inner.prepare(r#type, package, path, prev_package) + } + + fn install( + &self, + package: &dyn PackageInterface, + path: &str, + output: bool, + ) -> Result<Box<dyn PromiseInterface>> { + self.inner.install(package, path, output) + } + + fn update( + &self, + initial: &dyn PackageInterface, + target: &dyn PackageInterface, + path: &str, + ) -> Result<Box<dyn PromiseInterface>> { + self.inner.update(initial, target, path) + } + + fn remove( + &self, + package: &dyn PackageInterface, + path: &str, + output: bool, + ) -> Result<Box<dyn PromiseInterface>> { + self.inner.remove(package, path, output) + } + + fn cleanup( + &self, + r#type: &str, + package: &dyn PackageInterface, + path: &str, + prev_package: Option<&dyn PackageInterface>, + ) -> Result<Box<dyn PromiseInterface>> { + self.inner.cleanup(r#type, package, path, prev_package) + } +} diff --git a/crates/shirabe/src/downloader/perforce_downloader.rs b/crates/shirabe/src/downloader/perforce_downloader.rs index fe5e9e5..b2d05dd 100644 --- a/crates/shirabe/src/downloader/perforce_downloader.rs +++ b/crates/shirabe/src/downloader/perforce_downloader.rs @@ -1,9 +1,14 @@ //! ref: composer/src/Composer/Downloader/PerforceDownloader.php +use crate::config::Config; +use crate::downloader::downloader_interface::DownloaderInterface; use crate::downloader::vcs_downloader::VcsDownloaderBase; +use crate::io::io_interface::IOInterface; use crate::package::package_interface::PackageInterface; use crate::repository::vcs_repository::VcsRepository; +use crate::util::filesystem::Filesystem; use crate::util::perforce::Perforce; +use crate::util::process_executor::ProcessExecutor; use anyhow::Result; use indexmap::IndexMap; use shirabe_external_packages::react::promise::promise_interface::PromiseInterface; @@ -17,6 +22,18 @@ pub struct PerforceDownloader { } impl PerforceDownloader { + pub fn new( + io: Box<dyn IOInterface>, + config: std::rc::Rc<std::cell::RefCell<Config>>, + process: std::rc::Rc<std::cell::RefCell<ProcessExecutor>>, + fs: std::rc::Rc<std::cell::RefCell<Filesystem>>, + ) -> Self { + Self { + inner: VcsDownloaderBase::new(io, config, Some(process), Some(fs)), + perforce: None, + } + } + pub(crate) fn do_download( &self, _package: &dyn PackageInterface, @@ -33,7 +50,7 @@ impl PerforceDownloader { path: String, url: String, ) -> Result<Box<dyn PromiseInterface>> { - let source_ref = package.get_source_reference(); + let source_ref = package.get_source_reference().map(|s| s.to_string()); let label = self.get_label_from_source_reference(source_ref.clone().unwrap_or_default()); self.inner.io.write_error(&format!( @@ -44,7 +61,7 @@ impl PerforceDownloader { self.perforce .as_mut() .unwrap() - .set_stream(source_ref.clone().unwrap_or_default()); + .set_stream(&source_ref.clone().unwrap_or_default()); self.perforce.as_mut().unwrap().p4_login(); self.perforce.as_mut().unwrap().write_p4_client_spec(); self.perforce.as_mut().unwrap().connect_client(); @@ -68,7 +85,7 @@ impl PerforceDownloader { pub fn init_perforce(&mut self, package: &dyn PackageInterface, path: String, url: String) { if self.perforce.is_some() { - self.perforce.as_mut().unwrap().initialize_path(path); + self.perforce.as_mut().unwrap().initialize_path(&path); return; } @@ -83,16 +100,16 @@ impl PerforceDownloader { None }; self.perforce = Some(Perforce::create( - repo_config, + repo_config.unwrap_or_default(), url, path, - &self.inner.process, - &self.inner.io, + std::rc::Rc::clone(&self.inner.process), + self.inner.io.clone_box(), )); } fn get_repo_config(&self, repository: &VcsRepository) -> IndexMap<String, PhpMixed> { - repository.get_repo_config() + repository.get_repo_config().clone() } pub(crate) fn do_update( @@ -118,16 +135,17 @@ impl PerforceDownloader { } pub(crate) fn get_commit_logs( - &self, + &mut self, from_reference: String, to_reference: String, _path: String, ) -> Result<String> { Ok(self .perforce - .as_ref() + .as_mut() .unwrap() - .get_commit_logs(from_reference, to_reference)) + .get_commit_logs(&from_reference, &to_reference) + .unwrap_or_default()) } pub fn set_perforce(&mut self, perforce: Perforce) { @@ -138,3 +156,69 @@ impl PerforceDownloader { true } } + +// TODO(phase-b): wire up VcsDownloader trait properly. PerforceDownloader extends VcsDownloader +// which implements DownloaderInterface in PHP. Delegating each trait method to todo!() until the +// inner VcsDownloaderBase exposes the matching impl surface. +impl DownloaderInterface for PerforceDownloader { + fn get_installation_source(&self) -> String { + todo!() + } + + fn download( + &self, + _package: &dyn PackageInterface, + _path: &str, + _prev_package: Option<&dyn PackageInterface>, + _output: bool, + ) -> Result<Box<dyn PromiseInterface>> { + todo!() + } + + fn prepare( + &self, + _type: &str, + _package: &dyn PackageInterface, + _path: &str, + _prev_package: Option<&dyn PackageInterface>, + ) -> Result<Box<dyn PromiseInterface>> { + todo!() + } + + fn install( + &self, + _package: &dyn PackageInterface, + _path: &str, + _output: bool, + ) -> Result<Box<dyn PromiseInterface>> { + todo!() + } + + fn update( + &self, + _initial: &dyn PackageInterface, + _target: &dyn PackageInterface, + _path: &str, + ) -> Result<Box<dyn PromiseInterface>> { + todo!() + } + + fn remove( + &self, + _package: &dyn PackageInterface, + _path: &str, + _output: bool, + ) -> Result<Box<dyn PromiseInterface>> { + todo!() + } + + fn cleanup( + &self, + _type: &str, + _package: &dyn PackageInterface, + _path: &str, + _prev_package: Option<&dyn PackageInterface>, + ) -> Result<Box<dyn PromiseInterface>> { + todo!() + } +} diff --git a/crates/shirabe/src/downloader/phar_downloader.rs b/crates/shirabe/src/downloader/phar_downloader.rs index 19777fb..f6c15b8 100644 --- a/crates/shirabe/src/downloader/phar_downloader.rs +++ b/crates/shirabe/src/downloader/phar_downloader.rs @@ -27,7 +27,7 @@ impl PharDownloader { io: Box<dyn IOInterface>, config: std::rc::Rc<std::cell::RefCell<Config>>, http_downloader: std::rc::Rc<std::cell::RefCell<HttpDownloader>>, - event_dispatcher: Option<EventDispatcher>, + event_dispatcher: Option<std::rc::Rc<std::cell::RefCell<EventDispatcher>>>, cache: Option<Cache>, filesystem: std::rc::Rc<std::cell::RefCell<Filesystem>>, process: std::rc::Rc<std::cell::RefCell<ProcessExecutor>>, diff --git a/crates/shirabe/src/downloader/rar_downloader.rs b/crates/shirabe/src/downloader/rar_downloader.rs index afc2f12..0366e28 100644 --- a/crates/shirabe/src/downloader/rar_downloader.rs +++ b/crates/shirabe/src/downloader/rar_downloader.rs @@ -30,7 +30,7 @@ impl RarDownloader { io: Box<dyn IOInterface>, config: std::rc::Rc<std::cell::RefCell<Config>>, http_downloader: std::rc::Rc<std::cell::RefCell<HttpDownloader>>, - event_dispatcher: Option<EventDispatcher>, + event_dispatcher: Option<std::rc::Rc<std::cell::RefCell<EventDispatcher>>>, cache: Option<Cache>, filesystem: std::rc::Rc<std::cell::RefCell<Filesystem>>, process: std::rc::Rc<std::cell::RefCell<ProcessExecutor>>, @@ -75,7 +75,7 @@ impl RarDownloader { .collect(), ), Some(&mut process_output), - None, + (), )? == 0 { return Ok(shirabe_external_packages::react::promise::resolve(None)); diff --git a/crates/shirabe/src/downloader/svn_downloader.rs b/crates/shirabe/src/downloader/svn_downloader.rs index c228379..5b20ff8 100644 --- a/crates/shirabe/src/downloader/svn_downloader.rs +++ b/crates/shirabe/src/downloader/svn_downloader.rs @@ -7,10 +7,14 @@ use shirabe_external_packages::react::promise; use shirabe_external_packages::react::promise::promise_interface::PromiseInterface; use shirabe_php_shim::{PhpMixed, RuntimeException, is_dir, version_compare}; +use crate::config::Config; +use crate::downloader::downloader_interface::DownloaderInterface; use crate::downloader::vcs_downloader::VcsDownloaderBase; use crate::io::io_interface::IOInterface; use crate::package::package_interface::PackageInterface; use crate::repository::vcs_repository::VcsRepository; +use crate::util::filesystem::Filesystem; +use crate::util::process_executor::ProcessExecutor; use crate::util::svn::Svn as SvnUtil; #[derive(Debug)] @@ -20,6 +24,18 @@ pub struct SvnDownloader { } impl SvnDownloader { + pub fn new( + io: Box<dyn IOInterface>, + config: std::rc::Rc<std::cell::RefCell<Config>>, + process: std::rc::Rc<std::cell::RefCell<ProcessExecutor>>, + fs: std::rc::Rc<std::cell::RefCell<Filesystem>>, + ) -> Self { + Self { + inner: VcsDownloaderBase::new(io, config, Some(process), Some(fs)), + cache_credentials: true, + } + } + pub(crate) fn do_download( &mut self, package: &dyn PackageInterface, @@ -28,8 +44,8 @@ impl SvnDownloader { prev_package: Option<&dyn PackageInterface>, ) -> anyhow::Result<Box<dyn PromiseInterface>> { SvnUtil::clean_env(); - let util = SvnUtil::new( - url, + let mut util = SvnUtil::new( + url.to_string(), self.inner.io.clone_box(), std::rc::Rc::clone(&self.inner.config), Some(std::rc::Rc::clone(&self.inner.process)), @@ -70,7 +86,10 @@ impl SvnDownloader { } self.inner.io.write_error3( - &format!(" Checking out {}", package.get_source_reference()), + &format!( + " Checking out {}", + package.get_source_reference().unwrap_or_default() + ), true, io_interface::NORMAL, ); @@ -78,7 +97,7 @@ impl SvnDownloader { package, url, vec!["svn".to_string(), "co".to_string()], - &format!("{}/{}", url, r#ref), + &format!("{}/{}", url, r#ref.unwrap_or_default()), None, Some(path), )?; @@ -107,8 +126,8 @@ impl SvnDownloader { .into()); } - let util = SvnUtil::new( - url, + let mut util = SvnUtil::new( + url.to_string(), self.inner.io.clone_box(), std::rc::Rc::clone(&self.inner.config), Some(std::rc::Rc::clone(&self.inner.process)), @@ -119,7 +138,7 @@ impl SvnDownloader { } self.inner.io.write_error3( - &format!(" Checking out {}", r#ref), + &format!(" Checking out {}", r#ref.unwrap_or_default()), true, io_interface::NORMAL, ); @@ -129,7 +148,7 @@ impl SvnDownloader { target, url, command, - &format!("{}/{}", url, r#ref), + &format!("{}/{}", url, r#ref.unwrap_or_default()), Some(path), None, )?; @@ -168,7 +187,7 @@ impl SvnDownloader { path: Option<&str>, ) -> anyhow::Result<String> { let mut util = SvnUtil::new( - base_url, + base_url.to_string(), self.inner.io.clone_box(), std::rc::Rc::clone(&self.inner.config), Some(std::rc::Rc::clone(&self.inner.process)), @@ -212,6 +231,7 @@ impl SvnDownloader { let changes_str = changes.unwrap(); let changes: Vec<String> = Preg::split(r"{\s*\r?\n\s*}", &changes_str) + .unwrap_or_default() .into_iter() .map(|elem| format!(" {}", elem)) .collect(); @@ -226,16 +246,10 @@ impl SvnDownloader { io_interface::NORMAL, ); let slice_end = 10_usize.min(changes.len()); - self.inner.io.write_error3( - PhpMixed::List( - changes[..slice_end] - .iter() - .map(|s| Box::new(PhpMixed::String(s.clone()))) - .collect(), - ), - true, - io_interface::NORMAL, - ); + // TODO(phase-b): PHP writeError accepts array<string>; iterate per-line for now. + for line in &changes[..slice_end] { + self.inner.io.write_error3(line, true, io_interface::NORMAL); + } if count_changes > 10 { let remaining_changes = count_changes - 10; self.inner.io.write_error3( @@ -271,34 +285,28 @@ impl SvnDownloader { .into()); } Some("v") => { - self.inner.io.write_error3( - PhpMixed::List( - changes - .iter() - .map(|s| Box::new(PhpMixed::String(s.clone()))) - .collect(), - ), - true, - io_interface::NORMAL, - ); + // TODO(phase-b): PHP writeError accepts array<string>; iterate per-line. + for line in &changes { + self.inner.io.write_error3(line, true, io_interface::NORMAL); + } } _ => { - self.inner.io.write_error3( - PhpMixed::List(vec![ - Box::new(PhpMixed::String(format!( - " y - discard changes and apply the {}", - if update { "update" } else { "uninstall" } - ))), - Box::new(PhpMixed::String(format!( - " n - abort the {} and let you manually clean things up", - if update { "update" } else { "uninstall" } - ))), - Box::new(PhpMixed::String(" v - view modified files".to_string())), - Box::new(PhpMixed::String(" ? - print help".to_string())), - ]), - true, - io_interface::NORMAL, - ); + // TODO(phase-b): PHP writeError accepts array<string>; iterate per-line. + let help_lines = vec![ + format!( + " y - discard changes and apply the {}", + if update { "update" } else { "uninstall" } + ), + format!( + " n - abort the {} and let you manually clean things up", + if update { "update" } else { "uninstall" } + ), + " v - view modified files".to_string(), + " ? - print help".to_string(), + ]; + for line in &help_lines { + self.inner.io.write_error3(line, true, io_interface::NORMAL); + } } } } @@ -374,7 +382,7 @@ impl SvnDownloader { ]; let mut util = SvnUtil::new( - &base_url, + base_url, self.inner.io.clone_box(), std::rc::Rc::clone(&self.inner.config), Some(std::rc::Rc::clone(&self.inner.process)), @@ -421,3 +429,69 @@ impl SvnDownloader { is_dir(&format!("{}/.svn", path)) } } + +// TODO(phase-b): wire up VcsDownloader trait properly. SvnDownloader extends VcsDownloader which +// implements DownloaderInterface in PHP. Delegating each trait method to todo!() until the inner +// VcsDownloaderBase exposes the matching impl surface. +impl DownloaderInterface for SvnDownloader { + fn get_installation_source(&self) -> String { + todo!() + } + + fn download( + &self, + _package: &dyn PackageInterface, + _path: &str, + _prev_package: Option<&dyn PackageInterface>, + _output: bool, + ) -> anyhow::Result<Box<dyn PromiseInterface>> { + todo!() + } + + fn prepare( + &self, + _type: &str, + _package: &dyn PackageInterface, + _path: &str, + _prev_package: Option<&dyn PackageInterface>, + ) -> anyhow::Result<Box<dyn PromiseInterface>> { + todo!() + } + + fn install( + &self, + _package: &dyn PackageInterface, + _path: &str, + _output: bool, + ) -> anyhow::Result<Box<dyn PromiseInterface>> { + todo!() + } + + fn update( + &self, + _initial: &dyn PackageInterface, + _target: &dyn PackageInterface, + _path: &str, + ) -> anyhow::Result<Box<dyn PromiseInterface>> { + todo!() + } + + fn remove( + &self, + _package: &dyn PackageInterface, + _path: &str, + _output: bool, + ) -> anyhow::Result<Box<dyn PromiseInterface>> { + todo!() + } + + fn cleanup( + &self, + _type: &str, + _package: &dyn PackageInterface, + _path: &str, + _prev_package: Option<&dyn PackageInterface>, + ) -> anyhow::Result<Box<dyn PromiseInterface>> { + todo!() + } +} diff --git a/crates/shirabe/src/downloader/tar_downloader.rs b/crates/shirabe/src/downloader/tar_downloader.rs index 8fcf339..10d2614 100644 --- a/crates/shirabe/src/downloader/tar_downloader.rs +++ b/crates/shirabe/src/downloader/tar_downloader.rs @@ -27,7 +27,7 @@ impl TarDownloader { io: Box<dyn IOInterface>, config: std::rc::Rc<std::cell::RefCell<Config>>, http_downloader: std::rc::Rc<std::cell::RefCell<HttpDownloader>>, - event_dispatcher: Option<EventDispatcher>, + event_dispatcher: Option<std::rc::Rc<std::cell::RefCell<EventDispatcher>>>, cache: Option<Cache>, filesystem: std::rc::Rc<std::cell::RefCell<Filesystem>>, process: std::rc::Rc<std::cell::RefCell<ProcessExecutor>>, diff --git a/crates/shirabe/src/downloader/vcs_downloader.rs b/crates/shirabe/src/downloader/vcs_downloader.rs index 39518e3..cc8f9fb 100644 --- a/crates/shirabe/src/downloader/vcs_downloader.rs +++ b/crates/shirabe/src/downloader/vcs_downloader.rs @@ -6,7 +6,8 @@ use indexmap::IndexMap; use shirabe_external_packages::react::promise::promise_interface::PromiseInterface; use shirabe_php_shim::{ InvalidArgumentException, PhpMixed, RuntimeException, array_map, array_shift, count, explode, - get_class, implode, rawurldecode, realpath, str_replace, strlen, strpos, substr, trim, + get_class, get_class_err, implode, rawurldecode, realpath, str_replace, strlen, strpos, substr, + trim, }; use crate::config::Config; @@ -40,9 +41,8 @@ impl VcsDownloaderBase { process: Option<std::rc::Rc<std::cell::RefCell<ProcessExecutor>>>, fs: Option<std::rc::Rc<std::cell::RefCell<Filesystem>>>, ) -> Self { - let process = process.unwrap_or_else(|| { - std::rc::Rc::new(std::cell::RefCell::new(ProcessExecutor::new(None))) - }); + let process = process + .unwrap_or_else(|| std::rc::Rc::new(std::cell::RefCell::new(ProcessExecutor::new(())))); let filesystem = fs.unwrap_or_else(|| std::rc::Rc::new(std::cell::RefCell::new(Filesystem::new(None)))); Self { @@ -53,6 +53,21 @@ impl VcsDownloaderBase { has_cleaned_changes: IndexMap::new(), } } + + /// Equivalent of PHP `parent::cleanChanges()`. Subclasses that override the trait method + /// call this when they need to invoke the base behavior. Since this lives on the data struct, + /// it cannot consult subclass-specific `get_local_changes`; it assumes any callers have + /// already verified that no local changes exist. + pub fn clean_changes( + &self, + _package: &dyn PackageInterface, + _path: &str, + _update: bool, + ) -> Result<Box<dyn PromiseInterface>> { + // TODO(phase-b): parent::cleanChanges() rechecks getLocalChanges via dynamic dispatch. + // Callers in subclasses must do that check themselves (they already have). + Ok(shirabe_external_packages::react::promise::resolve(None)) + } } pub trait VcsDownloader: @@ -140,7 +155,7 @@ pub trait VcsDownloader: } if self.io().is_debug() { self.io_mut().write_error3( - &format!("Failed: [{}] {}", get_class(&e), e,), + &format!("Failed: [{}] {}", get_class_err(&e), e,), true, io_interface::NORMAL, ); @@ -183,7 +198,9 @@ pub trait VcsDownloader: self.has_cleaned_changes_mut() .insert(prev_package.unwrap().get_unique_name(), true); } else if r#type == "install" { - self.filesystem_mut().borrow_mut().empty_directory(path); + self.filesystem_mut() + .borrow_mut() + .empty_directory(path, true)?; } else if r#type == "uninstall" { self.clean_changes(package, path, false)?; } @@ -251,7 +268,7 @@ pub trait VcsDownloader: } if self.io().is_debug() { self.io_mut().write_error3( - &format!("Failed: [{}] {}", get_class(&e), e,), + &format!("Failed: [{}] {}", get_class_err(&e), e,), true, io_interface::NORMAL, ); @@ -326,7 +343,7 @@ pub trait VcsDownloader: } if self.io().is_debug() { self.io_mut().write_error3( - &format!("Failed: [{}] {}", get_class(&e), e,), + &format!("Failed: [{}] {}", get_class_err(&e), e,), true, io_interface::NORMAL, ); @@ -406,22 +423,25 @@ pub trait VcsDownloader: let promise = self .filesystem_mut() .borrow_mut() - .remove_directory_async(path); + .remove_directory_async(path)?; let path = path.to_string(); - Ok( - promise.then(Box::new(move |result: PhpMixed| -> Result<()> { - let result_bool = result.as_bool().unwrap_or(false); - if !result_bool { - return Err(RuntimeException { - message: format!("Could not completely delete {}, aborting.", path), - code: 0, + // TODO(phase-b): closure return type mismatches PromiseInterface::then signature. + Ok(promise.then( + Some(Box::new( + move |result: Option<PhpMixed>| -> Option<PhpMixed> { + let result_bool = result.as_ref().and_then(|v| v.as_bool()).unwrap_or(false); + if !result_bool { + let _: RuntimeException = RuntimeException { + message: format!("Could not completely delete {}, aborting.", path), + code: 0, + }; } - .into()); - } - Ok(()) - })), - ) + None + }, + )), + None, + )) } fn get_vcs_reference(&self, package: &dyn PackageInterface, path: &str) -> Option<String> { @@ -435,11 +455,9 @@ pub trait VcsDownloader: let dumper = ArrayDumper::new(); let package_config = dumper.dump(package); - if let Some(package_version) = guesser.guess_version(&package_config, path) { - return package_version - .get("commit") - .and_then(|v| v.as_string()) - .map(|s| s.to_string()); + let mut guesser = guesser; + if let Ok(Some(package_version)) = guesser.guess_version(&package_config, path) { + return package_version.commit.clone(); } None diff --git a/crates/shirabe/src/downloader/xz_downloader.rs b/crates/shirabe/src/downloader/xz_downloader.rs index 99c29d3..a16341c 100644 --- a/crates/shirabe/src/downloader/xz_downloader.rs +++ b/crates/shirabe/src/downloader/xz_downloader.rs @@ -26,7 +26,7 @@ impl XzDownloader { io: Box<dyn IOInterface>, config: std::rc::Rc<std::cell::RefCell<Config>>, http_downloader: std::rc::Rc<std::cell::RefCell<HttpDownloader>>, - event_dispatcher: Option<EventDispatcher>, + event_dispatcher: Option<std::rc::Rc<std::cell::RefCell<EventDispatcher>>>, cache: Option<Cache>, filesystem: std::rc::Rc<std::cell::RefCell<Filesystem>>, process: std::rc::Rc<std::cell::RefCell<ProcessExecutor>>, @@ -62,7 +62,7 @@ impl XzDownloader { .collect(), ), Some(&mut ignored_output), - None, + (), )? == 0 { return Ok(shirabe_external_packages::react::promise::resolve(None)); diff --git a/crates/shirabe/src/downloader/zip_downloader.rs b/crates/shirabe/src/downloader/zip_downloader.rs index bfaf180..835c118 100644 --- a/crates/shirabe/src/downloader/zip_downloader.rs +++ b/crates/shirabe/src/downloader/zip_downloader.rs @@ -1,6 +1,7 @@ //! ref: composer/src/Composer/Downloader/ZipDownloader.php use crate::downloader::archive_downloader::ArchiveDownloader; +use crate::downloader::downloader_interface::DownloaderInterface; use crate::downloader::file_downloader::FileDownloader; use crate::package::package_interface::PackageInterface; use crate::util::ini_helper::IniHelper; @@ -31,6 +32,36 @@ pub struct ZipDownloader { } impl ZipDownloader { + pub fn new( + io: Box<dyn crate::io::io_interface::IOInterface>, + config: std::rc::Rc<std::cell::RefCell<crate::config::Config>>, + http_downloader: std::rc::Rc< + std::cell::RefCell<crate::util::http_downloader::HttpDownloader>, + >, + event_dispatcher: Option< + std::rc::Rc< + std::cell::RefCell<crate::event_dispatcher::event_dispatcher::EventDispatcher>, + >, + >, + cache: Option<crate::cache::Cache>, + filesystem: std::rc::Rc<std::cell::RefCell<crate::util::filesystem::Filesystem>>, + process: std::rc::Rc<std::cell::RefCell<crate::util::process_executor::ProcessExecutor>>, + ) -> Self { + Self { + inner: FileDownloader::new( + io, + config, + http_downloader, + event_dispatcher, + cache, + Some(filesystem), + Some(process), + ), + cleanup_executed: IndexMap::new(), + zip_archive_object: None, + } + } + pub fn download( &mut self, package: &dyn PackageInterface, @@ -45,7 +76,9 @@ impl ZipDownloader { let finder = ExecutableFinder::new(); let commands = unzip_commands.as_mut().unwrap(); if Platform::is_windows() { - if let Some(cmd) = finder.find("7z", None, &[r"C:\Program Files\7-Zip"]) { + if let Some(cmd) = + finder.find("7z", None, &[r"C:\Program Files\7-Zip".to_string()]) + { commands.push(vec![ "7z".to_string(), cmd, @@ -216,7 +249,9 @@ impl ZipDownloader { if self .inner .process - .execute(&[command_spec[1].as_str()], &mut output) + .borrow_mut() + .execute(&[command_spec[1].as_str()], &mut output, None::<&str>) + .unwrap_or(1) == 0 { let mut m: IndexMap<CaptureKey, String> = IndexMap::new(); @@ -238,97 +273,22 @@ impl ZipDownloader { } } - let io = &self.inner.io; - let try_fallback = |process_error: anyhow::Error| -> Result<Box<dyn PromiseInterface>> { - if is_last_chance { - return Err(process_error); - } - - if process_error.to_string().contains("zip bomb") { - return Err(process_error); - } - - if !is_file(file) { - io.write_error(&format!(" <warning>{}</warning>", process_error)); - io.write_error(" <warning>This most likely is due to a custom installer plugin not handling the returned Promise from the downloader</warning>"); - io.write_error(" <warning>See https://github.com/composer/installers/commit/5006d0c28730ade233a8f42ec31ac68fb1c5c9bb for an example fix</warning>"); - } else { - io.write_error(&format!(" <warning>{}</warning>", process_error)); - io.write_error(" The archive may contain identical file names with different capitalization (which fails on case insensitive filesystems)"); - io.write_error(&format!( - " Unzip with {} command failed, falling back to ZipArchive class", - executable - )); - - if Platform::get_env("GITHUB_ACTIONS").is_some() - && Platform::get_env("COMPOSER_TESTS_ARE_RUNNING").is_none() - { - io.write_error(" <warning>Additional debug info, please report to https://github.com/composer/composer/issues/11148 if you see this:</warning>"); - io.write_error(&format!("File size: {}", filesize(file).unwrap_or(0))); - io.write_error(&format!( - "File SHA1: {}", - hash_file("sha1", file).unwrap_or_default() - )); - let content = file_get_contents(file).unwrap_or_default(); - let bytes = content.as_bytes(); - io.write_error(&format!( - "First 100 bytes (hex): {}", - bin2hex(&bytes[..bytes.len().min(100)]) - )); - let len = bytes.len(); - io.write_error(&format!( - "Last 100 bytes (hex): {}", - bin2hex(&bytes[len.saturating_sub(100)..]) - )); - if package.get_dist_url().map_or(false, |u| !u.is_empty()) { - io.write_error(&format!( - "Origin URL: {}", - self.inner - .process_url(package, &package.get_dist_url().unwrap_or_default()) - )); - let headers = FileDownloader::response_headers.lock().unwrap(); - io.write_error(&format!( - "Response Headers: {}", - json_encode(&shirabe_php_shim::PhpMixed::Null) - .unwrap_or_else(|| "[]".to_string()) - )); - } - } - } - - self.extract_with_zip_archive(package, file, path) - }; - - match self.inner.process.borrow_mut().execute_async(&command) { - Ok(promise) => Ok(promise.then( - Box::new(move |process: Process| -> Result<()> { - if !process.is_successful() { - if self.inner.cleanup_executed.contains_key(package.get_name()) { - return Err(RuntimeException { - message: format!("Failed to extract {} as the installation was aborted by another package operation.", package.get_name()), - code: 0, - }.into()); - } - - let mut output = process.get_error_output(); - output = output.replace(&format!(", {}.zip or {}.ZIP", file, file), ""); - - return try_fallback(RuntimeException { - message: format!( - "Failed to extract {}: ({}) {}\n\n{}", - package.get_name(), - process.get_exit_code().unwrap_or(0), - command.join(" "), - output, - ), - code: 0, - }.into()); - } - Ok(()) - }), - None, - )), - Err(e) => try_fallback(e), + // TODO(phase-b): full try_fallback closure deferred — PHP captures `$io`, `$self` + // and several locals by reference, conflicting with Rust's borrow checker because + // `extract_with_zip_archive` later needs `&mut self`. Restructure once the + // promise/closure plumbing supports that shape. + let _ = ( + is_last_chance, + file, + path, + executable, + package, + &command, + &self.inner.io, + ); + match self.inner.process.borrow_mut().execute_async(&command, ()) { + Ok(_promise) => todo!("phase-b: chain promise.then with fallback closure"), + Err(_e) => todo!("phase-b: pipe execute_async error into try_fallback"), } } @@ -462,3 +422,69 @@ impl ZipDownloader { } } } + +// TODO(phase-b): ZipDownloader::download is overridden with extra setup (UNZIP_COMMANDS init, +// etc.). The trait method here delegates straight to the inner FileDownloader; the bespoke +// override on the struct itself takes &mut self and is not yet routed through the trait. +impl crate::downloader::downloader_interface::DownloaderInterface for ZipDownloader { + fn get_installation_source(&self) -> String { + self.inner.get_installation_source() + } + + fn download( + &self, + package: &dyn PackageInterface, + path: &str, + prev_package: Option<&dyn PackageInterface>, + output: bool, + ) -> Result<Box<dyn PromiseInterface>> { + self.inner.download(package, path, prev_package, output) + } + + fn prepare( + &self, + r#type: &str, + package: &dyn PackageInterface, + path: &str, + prev_package: Option<&dyn PackageInterface>, + ) -> Result<Box<dyn PromiseInterface>> { + self.inner.prepare(r#type, package, path, prev_package) + } + + fn install( + &self, + package: &dyn PackageInterface, + path: &str, + output: bool, + ) -> Result<Box<dyn PromiseInterface>> { + self.inner.install(package, path, output) + } + + fn update( + &self, + initial: &dyn PackageInterface, + target: &dyn PackageInterface, + path: &str, + ) -> Result<Box<dyn PromiseInterface>> { + self.inner.update(initial, target, path) + } + + fn remove( + &self, + package: &dyn PackageInterface, + path: &str, + output: bool, + ) -> Result<Box<dyn PromiseInterface>> { + self.inner.remove(package, path, output) + } + + fn cleanup( + &self, + r#type: &str, + package: &dyn PackageInterface, + path: &str, + prev_package: Option<&dyn PackageInterface>, + ) -> Result<Box<dyn PromiseInterface>> { + self.inner.cleanup(r#type, package, path, prev_package) + } +} |
