diff --git a/Cargo.lock b/Cargo.lock index 74fcd30..171e742 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -138,6 +138,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + [[package]] name = "cpufeatures" version = "0.2.1" @@ -156,6 +162,51 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aaa7bd5fb665c6864b5f963dd9097905c54125909c7aa94c9e18507cdbe6c53" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1145cf131a2c6ba0615079ab6a638f7e1973ac9c2634fcbeaaad6114246efe8c" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "lazy_static", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38" +dependencies = [ + "cfg-if", + "lazy_static", +] + [[package]] name = "crypto-common" version = "0.1.3" @@ -398,6 +449,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" +[[package]] +name = "junction" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be39922b087cecaba4e2d5592dedfc8bda5d4a5a1231f143337cca207950b61d" +dependencies = [ + "scopeguard", + "winapi", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -422,6 +483,15 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + [[package]] name = "miniz_oxide" version = "0.4.4" @@ -432,6 +502,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "ntapi" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" +dependencies = [ + "winapi", +] + [[package]] name = "num-integer" version = "0.1.44" @@ -502,6 +581,7 @@ dependencies = [ "indicatif", "indoc", "itertools", + "junction", "md5", "num_cpus", "once_cell", @@ -511,6 +591,7 @@ dependencies = [ "serde_json", "serde_with", "sha2", + "sysinfo", "tar", "tempfile", "thiserror", @@ -559,6 +640,30 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rayon" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd99e5772ead8baa5215278c9b15bf92087709e9c1b2d1f97cdb5a183c933a7d" +dependencies = [ + "autocfg", + "crossbeam-deque", + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "258bcdb5ac6dad48491bb2992db6b7cf74878b0384908af124823d118c99683f" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + [[package]] name = "redox_syscall" version = "0.2.13" @@ -626,6 +731,12 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + [[package]] name = "semver" version = "1.0.4" @@ -715,6 +826,21 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "sysinfo" +version = "0.23.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3977ec2e0520829be45c8a2df70db2bf364714d8a748316a10c3c35d4d2b01c9" +dependencies = [ + "cfg-if", + "core-foundation-sys", + "libc", + "ntapi", + "once_cell", + "rayon", + "winapi", +] + [[package]] name = "tar" version = "0.4.38" diff --git a/Cargo.toml b/Cargo.toml index 855a6e5..e5e0e68 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,5 +36,9 @@ indicatif = "0.16.2" md5 = "0.7.0" sha2 = "0.10.2" +[target.'cfg(windows)'.dependencies] +junction = "0.2.0" +sysinfo = "0.23.12" + [profile.release] strip = "symbols" diff --git a/src/commands/completions.rs b/src/commands/completions.rs index fe6542d..3a0a8a7 100644 --- a/src/commands/completions.rs +++ b/src/commands/completions.rs @@ -21,10 +21,7 @@ impl Command for Completions { type Error = Error; fn run(&self, _: &Config) -> Result<(), Error> { - let shell = self - .shell - .map_or_else(Shell::detect_shell, Ok)? - .to_clap_shell(); + let shell = self.shell.map_or_else(shell::detect, Ok)?.to_clap_shell(); let app = Cli::into_app(); let mut stdout = std::io::stdout(); shell.generate(&app, &mut stdout); diff --git a/src/commands/init.rs b/src/commands/init.rs index 77306ff..02fbb5a 100644 --- a/src/commands/init.rs +++ b/src/commands/init.rs @@ -33,7 +33,7 @@ impl Command for Init { type Error = Error; fn run(&self, config: &Config) -> Result<(), Error> { - let shell = self.shell.map_or_else(Shell::detect_shell, Ok)?; + let shell = self.shell.map_or_else(shell::detect, Ok)?; let symlink = create_symlink(); if let Some(default_path) = default_path(config) { symlink::link(&default_path, &symlink).expect("Can't create symlink!"); diff --git a/src/commands/install.rs b/src/commands/install.rs index 62134de..2909145 100644 --- a/src/commands/install.rs +++ b/src/commands/install.rs @@ -1,241 +1,5 @@ -mod make; -mod progress_reader; +mod unix; +mod windos; -use super::{Command, Config}; -use crate::curl; -use crate::decorized::Decorized; -use crate::release::{self, Hash}; -use crate::version::{self, Version}; -use clap; -use colored::Colorize; -use flate2::read::GzDecoder; -use indicatif::{ProgressBar, ProgressStyle}; -use once_cell::sync::Lazy; -use progress_reader::ProgressReader; -use std::fs; -use std::io::{BufReader, BufWriter, Read}; -use std::path::{Path, PathBuf}; -use tar::Archive; -use thiserror::Error; - -static PROGRESS_STYLE: Lazy = Lazy::new(|| { - let progress_template = - "{prefix:>12.cyan.bold} [{bar:25}] {bytes}/{total_bytes} ({eta}) {wide_msg}"; - ProgressStyle::default_bar() - .template(progress_template) - .progress_chars("=> ") -}); - -#[derive(clap::Parser, Debug)] -pub struct Install { - version: Option, - - #[clap(flatten)] - version_file: version::File, - - /// Specify configure options used by the PHP configure scripts. - /// To specify two or more options, enclose them with quotation marks. - #[clap(long, env = "PHPUP_CONFIGURE_OPTS", allow_hyphen_values = true)] - configure_opts: Option, -} - -#[derive(Error, Debug)] -pub enum Error { - #[error("Can't detect a version: {0}")] - NoVersionFromFile(#[from] version::file::Error), - - #[error("Can't specify the system version by {0}")] - SpecifiedSystemVersion(PathBuf), - - #[error("PHP3 installation is not supported yet")] - UnsupportedPHP3, - - #[error(transparent)] - FailedFetchRelease(#[from] release::FetchError), - - #[error(transparent)] - FailedDownload(#[from] curl::Error), - - #[error(transparent)] - InvalidChecksum(#[from] release::ChecksumError), - - #[error(transparent)] - FailedMake(#[from] make::Error), - - #[error(transparent)] - Io(#[from] std::io::Error), -} - -impl Command for Install { - type Error = Error; - - fn run(&self, config: &Config) -> Result<(), Error> { - let request_version = self - .version - .map_or_else(|| self.get_version_from_version_file(), Ok)?; - - if request_version.major_version() == 3 { - return Err(Error::UnsupportedPHP3); - } - - let release = release::fetch_latest(request_version)?; - let install_version = release.version.unwrap(); - - if version::latest_installed_by(&request_version, config) == Some(install_version) { - println!( - "{}: Already installed {}", - "warning".yellow().bold(), - install_version.decorized_with_prefix() - ); - return Ok(()); - } - println!( - "{:>12} {}", - "Installing".green().bold(), - install_version.decorized_with_prefix() - ); - - let install_dir = config.versions_dir().join(install_version.to_string()); - let download_dir = tempfile::Builder::new() - .prefix(".downloads-") - .tempdir_in(&config.base_dir())?; - - let (url, checksum) = release.source_url(); - - let tar_gz = download(&url, &download_dir)?; - verify(&tar_gz, checksum)?; - let source_dir = unpack(&tar_gz, &download_dir)?; - build( - &source_dir, - &install_dir, - self.configure_opts - .as_ref() - .unwrap_or(&"".to_owned()) - .split_whitespace(), - )?; - println!( - "{:>12} {}", - "Installed".green().bold(), - install_dir.display().decorized() - ); - Ok(()) - } -} - -impl Install { - fn get_version_from_version_file(&self) -> Result { - let version_info = self.version_file.get_version_info()?; - if let Some(version) = version_info.version.as_version() { - println!( - "{} has been specified from {}", - version.decorized(), - version_info.filepath.display().decorized() - ); - Ok(version) - } else { - Err(Error::SpecifiedSystemVersion(version_info.filepath)) - } - } -} - -fn download(url: &str, dir: impl AsRef) -> Result { - let curl::Header { content_length } = curl::get_header(url)?; - let progress_bar = ProgressBar::new(content_length.unwrap() as u64) - .with_style(PROGRESS_STYLE.clone()) - .with_prefix("Downloading") - .with_message(url.to_owned()); - - let (stdout, mut stderr) = curl::get_as_reader(url)?; - let mut progress_reader = ProgressReader::new(stdout, &progress_bar); - - let download_file_path = dir.as_ref().join(url.rsplit('/').next().unwrap()); - let download_file = fs::File::create(&download_file_path)?; - let mut file_writer = BufWriter::new(&download_file); - - std::io::copy(&mut progress_reader, &mut file_writer)?; - - if download_file.metadata()?.len() > 0 { - progress_bar.finish_and_clear(); - println!("{:>12} {}", "Downloaded".green().bold(), url); - Ok(download_file_path) - } else { - let mut err_msg = String::new(); - stderr.read_to_string(&mut err_msg)?; - Err(Error::FailedDownload(curl::Error::ExitFailed( - "curl".to_owned(), - err_msg, - ))) - } -} - -fn verify(filepath: impl AsRef, checksum: Option<&Hash>) -> Result<(), Error> { - if let Some(checksum) = checksum { - let hash_type = checksum.hash_type(); - let file = fs::File::open(filepath)?; - let progress_bar = ProgressBar::new(file.metadata()?.len()) - .with_style(PROGRESS_STYLE.clone()) - .with_prefix("Verifying") - .with_message(hash_type); - - checksum.verify(ProgressReader::new(file, &progress_bar))?; - progress_bar.finish_and_clear(); - println!("{:>12} {} checksum", "Verified".green().bold(), hash_type); - } else { - println!( - "{:>12} {}: No checksum", - "Verifying".cyan().bold(), - "warning".yellow().bold() - ); - } - Ok(()) -} - -fn unpack(tar_gz: impl AsRef, dst_dir: impl AsRef) -> Result { - let file = fs::File::open(&tar_gz)?; - let progress_bar = ProgressBar::new(file.metadata()?.len()) - .with_style(PROGRESS_STYLE.clone()) - .with_prefix("Unpacking") - .with_message(tar_gz.as_ref().to_str().unwrap().to_owned()); - - let file_reader = BufReader::new(file); - let progress_reader = ProgressReader::new(file_reader, &progress_bar); - let gz_decoder = GzDecoder::new(progress_reader); - let mut tar_archive = Archive::new(gz_decoder); - - tar_archive.unpack(&dst_dir)?; - progress_bar.finish_and_clear(); - - println!( - "{:>12} {}", - "Unpacked".green().bold(), - tar_gz.as_ref().display() - ); - let tar_gz_filename = tar_gz.as_ref().file_name().unwrap().to_str().unwrap(); - let unpaked_dirname = &tar_gz_filename[..tar_gz_filename.len() - ".tar.gz".len()]; - Ok(dst_dir.as_ref().join(unpaked_dirname)) -} - -#[cfg(unix)] -fn build<'a>( - src_dir: impl AsRef, - dst_dir: impl AsRef, - configure_opts: impl Iterator, -) -> Result<(), Error> { - use make::Command; - - println!( - "{:>12} {}", - "Building".cyan().bold(), - src_dir.as_ref().display() - ); - let current_dir = src_dir.as_ref(); - - make::Configure { - prefix: dst_dir.as_ref(), - opts: configure_opts.collect(), - } - .run(current_dir)?; - make::Make {}.run(current_dir)?; - make::Install {}.run(current_dir)?; - Ok(()) -} +#[cfg(windows)] +pub use windos::Install; diff --git a/src/commands/install/unix.rs b/src/commands/install/unix.rs new file mode 100644 index 0000000..4d7a70c --- /dev/null +++ b/src/commands/install/unix.rs @@ -0,0 +1,242 @@ +#![cfg(unix)] + +mod make; +mod progress_reader; + +use crate::commands::{Command, Config}; +use crate::curl; +use crate::decorized::Decorized; +use crate::release; +use crate::version::{self, Version}; +use clap; +use colored::Colorize; +use flate2::read::GzDecoder; +use indicatif::{ProgressBar, ProgressStyle}; +use once_cell::sync::Lazy; +use progress_reader::ProgressReader; +use std::fs; +use std::io::{BufReader, BufWriter, Read}; +use std::path::{Path, PathBuf}; +use tar::Archive; +use thiserror::Error; + +static PROGRESS_STYLE: Lazy = Lazy::new(|| { + let progress_template = + "{prefix:>12.cyan.bold} [{bar:25}] {bytes}/{total_bytes} ({eta}) {wide_msg}"; + ProgressStyle::default_bar() + .template(progress_template) + .progress_chars("=> ") +}); + +#[derive(clap::Parser, Debug)] +pub struct Install { + version: Option, + + #[clap(flatten)] + version_file: version::File, + + /// Specify configure options used by the PHP configure scripts. + /// To specify two or more options, enclose them with quotation marks. + #[clap(long, env = "PHPUP_CONFIGURE_OPTS", allow_hyphen_values = true)] + configure_opts: Option, +} + +#[derive(Error, Debug)] +pub enum Error { + #[error("Can't detect a version: {0}")] + NoVersionFromFile(#[from] version::file::Error), + + #[error("Can't specify the system version by {0}")] + SpecifiedSystemVersion(PathBuf), + + #[error("PHP3 installation is not supported yet")] + UnsupportedPHP3, + + #[error(transparent)] + FailedFetchRelease(#[from] release::FetchError), + + #[error(transparent)] + FailedDownload(#[from] curl::Error), + + #[error(transparent)] + InvalidChecksum(#[from] release::ChecksumError), + + #[error(transparent)] + FailedMake(#[from] make::Error), + + #[error(transparent)] + Io(#[from] std::io::Error), +} + +impl Command for Install { + type Error = Error; + + fn run(&self, config: &Config) -> Result<(), Error> { + let request_version = self + .version + .map_or_else(|| self.get_version_from_version_file(), Ok)?; + + if request_version.major_version() == 3 { + return Err(Error::UnsupportedPHP3); + } + + let release = release::fetch_latest(request_version)?; + let install_version = release.version.unwrap(); + + if version::latest_installed_by(&request_version, config) == Some(install_version) { + println!( + "{}: Already installed {}", + "warning".yellow().bold(), + install_version.decorized_with_prefix() + ); + return Ok(()); + } + println!( + "{:>12} {}", + "Installing".green().bold(), + install_version.decorized_with_prefix() + ); + + let install_dir = config.versions_dir().join(install_version.to_string()); + let download_dir = tempfile::Builder::new() + .prefix(".downloads-") + .tempdir_in(&config.base_dir())?; + + let (url, checksum) = release.source_url(); + + let tar_gz = download(&url, &download_dir)?; + verify(&tar_gz, checksum)?; + let source_dir = unpack(&tar_gz, &download_dir)?; + build( + &source_dir, + &install_dir, + self.configure_opts + .as_ref() + .unwrap_or(&"".to_owned()) + .split_whitespace(), + )?; + println!( + "{:>12} {}", + "Installed".green().bold(), + install_dir.display().decorized() + ); + Ok(()) + } +} + +impl Install { + fn get_version_from_version_file(&self) -> Result { + let version_info = self.version_file.get_version_info()?; + if let Some(version) = version_info.version.as_version() { + println!( + "{} has been specified from {}", + version.decorized(), + version_info.filepath.display().decorized() + ); + Ok(version) + } else { + Err(Error::SpecifiedSystemVersion(version_info.filepath)) + } + } +} + +fn download(url: &str, dir: impl AsRef) -> Result { + let curl::Header { content_length } = curl::get_header(url)?; + let progress_bar = ProgressBar::new(content_length.unwrap() as u64) + .with_style(PROGRESS_STYLE.clone()) + .with_prefix("Downloading") + .with_message(url.to_owned()); + + let (stdout, mut stderr) = curl::get_as_reader(url)?; + let mut progress_reader = ProgressReader::new(stdout, &progress_bar); + + let download_file_path = dir.as_ref().join(url.rsplit('/').next().unwrap()); + let download_file = fs::File::create(&download_file_path)?; + let mut file_writer = BufWriter::new(&download_file); + + std::io::copy(&mut progress_reader, &mut file_writer)?; + + if download_file.metadata()?.len() > 0 { + progress_bar.finish_and_clear(); + println!("{:>12} {}", "Downloaded".green().bold(), url); + Ok(download_file_path) + } else { + let mut err_msg = String::new(); + stderr.read_to_string(&mut err_msg)?; + Err(Error::FailedDownload(curl::Error::ExitFailed( + "curl".to_owned(), + err_msg, + ))) + } +} + +fn verify(filepath: impl AsRef, checksum: Option<&release::unix::Hash>) -> Result<(), Error> { + if let Some(checksum) = checksum { + let hash_type = checksum.hash_type(); + let file = fs::File::open(filepath)?; + let progress_bar = ProgressBar::new(file.metadata()?.len()) + .with_style(PROGRESS_STYLE.clone()) + .with_prefix("Verifying") + .with_message(hash_type); + + checksum.verify(ProgressReader::new(file, &progress_bar))?; + progress_bar.finish_and_clear(); + println!("{:>12} {} checksum", "Verified".green().bold(), hash_type); + } else { + println!( + "{:>12} {}: No checksum", + "Verifying".cyan().bold(), + "warning".yellow().bold() + ); + } + Ok(()) +} + +fn unpack(tar_gz: impl AsRef, dst_dir: impl AsRef) -> Result { + let file = fs::File::open(&tar_gz)?; + let progress_bar = ProgressBar::new(file.metadata()?.len()) + .with_style(PROGRESS_STYLE.clone()) + .with_prefix("Unpacking") + .with_message(tar_gz.as_ref().to_str().unwrap().to_owned()); + + let file_reader = BufReader::new(file); + let progress_reader = ProgressReader::new(file_reader, &progress_bar); + let gz_decoder = GzDecoder::new(progress_reader); + let mut tar_archive = Archive::new(gz_decoder); + + tar_archive.unpack(&dst_dir)?; + progress_bar.finish_and_clear(); + + println!( + "{:>12} {}", + "Unpacked".green().bold(), + tar_gz.as_ref().display() + ); + let tar_gz_filename = tar_gz.as_ref().file_name().unwrap().to_str().unwrap(); + let unpaked_dirname = &tar_gz_filename[..tar_gz_filename.len() - ".tar.gz".len()]; + Ok(dst_dir.as_ref().join(unpaked_dirname)) +} + +fn build<'a>( + src_dir: impl AsRef, + dst_dir: impl AsRef, + configure_opts: impl Iterator, +) -> Result<(), Error> { + use make::Command; + + println!( + "{:>12} {}", + "Building".cyan().bold(), + src_dir.as_ref().display() + ); + let current_dir = src_dir.as_ref(); + + make::Configure { + prefix: dst_dir.as_ref(), + opts: configure_opts.collect(), + } + .run(current_dir)?; + make::Make {}.run(current_dir)?; + make::Install {}.run(current_dir)?; + Ok(()) +} diff --git a/src/commands/install/make.rs b/src/commands/install/unix/make.rs similarity index 100% rename from src/commands/install/make.rs rename to src/commands/install/unix/make.rs diff --git a/src/commands/install/windos.rs b/src/commands/install/windos.rs new file mode 100644 index 0000000..20f9dc8 --- /dev/null +++ b/src/commands/install/windos.rs @@ -0,0 +1,48 @@ +#![cfg(windows)] + +use crate::commands::{Command, Config}; +use crate::curl; +use crate::release; +use crate::version::{self, Version}; +use clap; +use std::path::PathBuf; +use thiserror::Error; + +#[derive(clap::Parser, Debug)] +pub struct Install { + version: Option, + + #[clap(flatten)] + version_file: version::File, + + /// Specify configure options used by the PHP configure scripts. + /// To specify two or more options, enclose them with quotation marks. + #[clap(long, env = "PHPUP_CONFIGURE_OPTS", allow_hyphen_values = true)] + configure_opts: Option, +} + +#[derive(Error, Debug)] +pub enum Error { + #[error("Can't detect a version: {0}")] + NoVersionFromFile(#[from] version::file::Error), + + #[error("Can't specify the system version by {0}")] + SpecifiedSystemVersion(PathBuf), + + #[error(transparent)] + FailedFetchRelease(#[from] release::FetchError), + + #[error(transparent)] + FailedDownload(#[from] curl::Error), + + #[error(transparent)] + Io(#[from] std::io::Error), +} + +impl Command for Install { + type Error = Error; + + fn run(&self, _config: &Config) -> Result<(), Self::Error> { + todo!() + } +} diff --git a/src/commands/list_remote.rs b/src/commands/list_remote.rs index 2227e7e..a328fbd 100644 --- a/src/commands/list_remote.rs +++ b/src/commands/list_remote.rs @@ -29,7 +29,7 @@ impl Command for ListRemote { type Error = Error; fn run(&self, config: &Config) -> Result<(), Error> { - let query_versions = match &self.version { + let releases = match &self.version { Some(version) => { if self.only_latest_patch && version.patch_version().is_some() { println!( @@ -38,38 +38,26 @@ impl Command for ListRemote { version ); } - vec![*version] - } - None => { - vec![ - Version::from_major(3), - Version::from_major(4), - Version::from_major(5), - Version::from_major(7), - Version::from_major(8), - ] + release::fetch(*version)? } + None => release::fetch_all()?, }; let installed_versions = version::installed(config).collect_vec(); let current_version = Local::current(config); - for query_version in query_versions { - let releases = release::fetch_all(query_version)?; - let remote_versions = releases.keys(); - - let remote_versions = if self.only_latest_patch { - filter_latest_patch(remote_versions).collect_vec() - } else { - remote_versions.collect_vec() - }; + let remote_versions = releases.keys(); + let remote_versions = if self.only_latest_patch { + filter_latest_patch(remote_versions).collect_vec() + } else { + remote_versions.collect_vec() + }; - for &remote_version in remote_versions { - let installed = installed_versions.contains(&remote_version); - let remote_version = Local::Installed(remote_version); - let used = Some(&remote_version) == current_version.as_ref(); - println!("{}", remote_version.to_string_by(installed, used)) - } + for &remote_version in remote_versions { + let installed = installed_versions.contains(&remote_version); + let remote_version = Local::Installed(remote_version); + let used = Some(&remote_version) == current_version.as_ref(); + println!("{}", remote_version.to_string_by(installed, used)) } Ok(()) } diff --git a/src/release.rs b/src/release.rs index 932d2fa..a1227ec 100644 --- a/src/release.rs +++ b/src/release.rs @@ -1,9 +1,8 @@ +pub mod unix; +mod windows; + use crate::curl; use crate::version::Version; -use chrono::{Datelike, NaiveDate, Utc}; -use derive_more::Display; -use serde::{de, Deserialize, Serialize}; -use serde_with::serde_as; use std::collections::BTreeMap; use thiserror::Error; @@ -14,331 +13,37 @@ pub enum FetchError { #[error(transparent)] CurlError(#[from] curl::Error), - - #[error("Receive error message from release site: {0}")] - Other(String), -} - -fn fetch_and_parse( - version: Option, - max: Option, -) -> Result, FetchError> { - let base_url = "https://www.php.net/releases/index.php"; - let query = format!( - "?json=1{}{}", - version - .map(|version| format!("&version={}", version)) - .unwrap_or_default(), - max.map(|max| format!("&max={}", max)).unwrap_or_default(), - ); - let url = &format!("{}{}", base_url, query); - let json = curl::get_as_slice(url)?; - - let resp: Response = - serde_json::from_slice(&json).unwrap_or_else(|_| panic!("Can't parse json from {}", url)); - match resp { - Response::Map(releases) => Ok(releases), - Response::One(release) => Ok([(release.version.unwrap(), release)].into_iter().collect()), - Response::Error { msg } => { - if msg.starts_with("Unknown version") { - Err(FetchError::NotFoundRelease(version.unwrap())) - } else { - Err(FetchError::Other(msg.to_owned())) - } - } - } -} - -pub fn fetch_all(version: Version) -> Result, FetchError> { - fetch_and_parse(Some(version), Some(1000)) -} - -pub fn fetch_latest(version: Version) -> Result { - let mut latest = fetch_and_parse(Some(version), None)?; - let version = *latest.keys().next().unwrap(); - Ok(latest.remove(&version).unwrap()) -} - -pub fn fetch_oldest_patch(version: Version) -> Result { - let oldest_minor_version = - Version::from_numbers(version.major_version(), version.minor_version(), Some(0)); - fetch_latest(oldest_minor_version) -} - -#[derive(Deserialize, Debug)] -#[serde(untagged)] -#[serde_as] -enum Response<'a> { - Map(#[serde_as(as = "BTreeMap<_, _>")] BTreeMap), - One(Release), - Error { - #[serde(rename = "error")] - msg: &'a str, - }, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct Release { - announcement: Option, - tags: Option>, - source: Vec, - #[serde(rename = "windows")] - windows_binary: Option>, - #[serde(deserialize_with = "date_deserializer")] - pub date: NaiveDate, - museum: Option, - pub version: Option, -} - -// TODO: wait for #[serde(flatten)] for enum variant, or implement custom deserializer -// { "announcement": { English: "/releases/..." } } -// or -// { "announcement": true } -#[derive(Serialize, Deserialize, Debug)] -#[serde(untagged)] -enum Announcement { - English(English), - Flag(bool), -} -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all(deserialize = "PascalCase", serialize = "PascalCase"))] -struct English { - english: String, -} - -#[derive(Serialize, Deserialize, Debug)] -// #[serde(untagged)] -enum Tag { - #[serde(rename = "security")] - Security, - // TOOD: skip deserialize if value is "" - #[serde(rename = "")] - None, } -#[derive(Serialize, Deserialize, Debug)] -#[serde(untagged)] -enum Source { - File(File), - Link { link: String, name: String }, +#[cfg(windows)] +pub fn fetch_all() -> Result, FetchError> { + windows::fetch(None) } -#[derive(Serialize, Deserialize, Debug)] -pub struct File { - filename: String, - name: String, - #[serde(flatten)] - checksum: Option, - // TODO: Option - date: Option, +#[cfg(windows)] +pub fn fetch(version: Version) -> Result, FetchError> { + windows::fetch(Some(version)) } -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all(deserialize = "lowercase", serialize = "lowercase"))] -pub enum Hash { - SHA256(String), - MD5(String), +#[cfg(windows)] +pub fn fetch_latest(version: Version) -> Result { + windows::fetch(Some(version)).map(|rs| rs.into_iter().last().unwrap().1) } -#[derive(Error, Debug)] -pub enum ChecksumError { - #[error("Invalid checksum\nexptected: {expected}\ngot: {got}")] - InvalidChecksum { expected: String, got: String }, - - #[error(transparent)] - Io(#[from] std::io::Error), -} - -use sha2::Digest; -impl Hash { - pub fn hash_type(&self) -> &'static str { - match self { - Hash::SHA256(_) => "SHA-256", - Hash::MD5(_) => "MD5", - } - } - pub fn verify(&self, mut data: impl std::io::Read) -> Result<(), ChecksumError> { - let (checksum, hash) = match self { - Hash::SHA256(checksum) => { - let mut sha256 = sha2::Sha256::new(); - std::io::copy(&mut data, &mut sha256)?; - let hash = sha256.finalize(); - (checksum, format!("{:x}", hash)) - } - Hash::MD5(checksum) => { - let mut md5 = md5::Context::new(); - std::io::copy(&mut data, &mut md5)?; - let hash = md5.compute(); - (checksum, format!("{:x}", hash)) - } - }; - if checksum == &hash { - Ok(()) - } else { - Err(ChecksumError::InvalidChecksum { - expected: checksum.clone(), - got: hash, - }) - } - } +#[cfg(unix)] +pub fn fetch_all() -> Result, FetchError> { + let versions = vec![ + Version::from_major(3), + Version::from_major(4), + Version::from_major(5), + Version::from_major(7), + Version::from_major(8), + ]; + versions.map(|version| unix::fetch_all(version)) } - -#[derive(Debug, Clone, Copy, Display, PartialEq)] -pub enum Support { - #[display(fmt = "Active support")] - ActiveSupport, - #[display(fmt = "Security fixes only")] - SecurityFixesOnly, - #[display(fmt = "End of life")] - EndOfLife, +#[cfg(unix)] +pub fn fetch(version: Version) -> Result, FetchError> { + windows::fetch(Some(version)) } - -impl Release { - fn source_file(&self, extention: &str) -> Option<&File> { - self.source.iter().find_map(|source| match source { - Source::File(file) if file.filename.ends_with(extention) => Some(file), - _ => None, - }) - } - pub fn source_url(&self) -> (String, Option<&Hash>) { - let source_file = self.source_file(".tar.gz").unwrap(); - let url = if self.museum == Some(true) { - let major_version = self.version.unwrap().major_version(); - format!( - "https://museum.php.net/php{}/{}", - major_version, source_file.filename - ) - } else { - format!("https://www.php.net/distributions/{}", source_file.filename) - // format!("http://jp1.php.net/get/{}/from/this/mirror/", filename) - }; - (url, source_file.checksum.as_ref()) - } - pub fn calculate_support(&self) -> Support { - let release_date = self.date; - let release_year = release_date.year(); - let active_support_deadline = release_date - .with_year(release_year + 2) - .unwrap_or_else(|| NaiveDate::from_yo(release_year + 1, 1).succ()); - let security_support_deadline = release_date - .with_year(release_year + 3) - .unwrap_or_else(|| NaiveDate::from_yo(release_year + 2, 1).succ()); - let today = Utc::now().naive_local().date(); - - if today < active_support_deadline { - Support::ActiveSupport - } else if today < security_support_deadline { - Support::SecurityFixesOnly - } else { - Support::EndOfLife - } - } -} - -fn date_deserializer<'de, D>(deserializer: D) -> Result -where - D: de::Deserializer<'de>, -{ - let s = String::deserialize(deserializer)?; - NaiveDate::parse_from_str(&s, "%d %B %Y").map_err(serde::de::Error::custom) -} - -// fn option_date_deserializer<'de, D>(deserializer: D) -> Result, D::Error> -// where -// D: de::Deserializer<'de>, -// { -// #[derive(Deserialize)] -// struct Helper(#[serde(deserialize_with = "date_deserializer")] NaiveDate); -// let helper = Option::deserialize(deserializer)?; -// Ok(helper.map(|Helper(o)| o)) -// } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn deserialize() { - let json = r#" - { - "8.1.1": { - "announcement": true, - "tags": [], - "date": "16 Dec 2021", - "source": [ - { - "filename": "php-8.1.1.tar.gz", - "name": "PHP 8.1.1 (tar.gz)", - "sha256": "4e4cf3f843a5111f6c55cd21de8f26834ea3cd4a5be77c88357cbcec4a2d671d", - "date": "16 Dec 2021" - }, - { - "filename": "php-8.1.1.tar.bz2", - "name": "PHP 8.1.1 (tar.bz2)", - "sha256": "8f8bc9cad6cd124edc111f7db0a109745e2f638770a101b3c22a2953f7a9b40e", - "date": "16 Dec 2021" - }, - { - "filename": "php-8.1.1.tar.xz", - "name": "PHP 8.1.1 (tar.xz)", - "sha256": "33c09d76d0a8bbb5dd930d9dd32e6bfd44e9efcf867563759eb5492c3aff8856", - "date": "16 Dec 2021" - } - ] - }, - "8.0.14": { - "announcement": true, - "tags": [], - "date": "16 Dec 2021", - "source": [ - { - "filename": "php-8.0.14.tar.gz", - "name": "PHP 8.0.14 (tar.gz)", - "sha256": "e67ebd8c4c77247ad1fa88829e5b95d51a19edf3d87814434de261e20a63ea20", - "date": "16 Dec 2021" - }, - { - "filename": "php-8.0.14.tar.bz2", - "name": "PHP 8.0.14 (tar.bz2)", - "sha256": "bb381fdf4817ad7c24c23ea7f77cad68dceb86eb3ac1a37acedadf8ad0a0cd4b", - "date": "16 Dec 2021" - }, - { - "filename": "php-8.0.14.tar.xz", - "name": "PHP 8.0.14 (tar.xz)", - "sha256": "fbde8247ac200e4de73449d9fefc8b495d323b5be9c10cdb645fb431c91156e3", - "date": "16 Dec 2021" - } - ] - } - } - "#; - let resp: Result = serde_json::from_str(json); - assert!(resp.is_ok()); - let resp = resp.unwrap(); - assert!(if let Response::Map(map) = resp { - map.contains_key(&"8.1.1".parse().unwrap()) - } else { - false - }); - } - #[test] - fn fetch_all_major_test() { - let releases = fetch_all("3".parse().unwrap()); - assert!(releases.is_ok()); - assert!(!releases.unwrap().is_empty()); - let releases = fetch_all("5".parse().unwrap()); - assert!(releases.is_ok()); - assert!(!releases.unwrap().is_empty()); - let releases = fetch_all("8".parse().unwrap()); - assert!(releases.is_ok()); - assert!(!releases.unwrap().is_empty()); - } - #[test] - fn fetch_latest_test() { - let latest_release = fetch_latest("7.0".parse().unwrap()); - assert!(latest_release.is_ok()); - assert_eq!( - latest_release.unwrap().version.unwrap(), - "7.0.33".parse().unwrap() - ); - } +#[cfg(unix)] +pub fn fetch_latest(version: Version) -> Result { + windows::fetch(Some(version)).map(|rs| rs.into_iter().last().unwrap().1) } diff --git a/src/release/unix.rs b/src/release/unix.rs new file mode 100644 index 0000000..13c9125 --- /dev/null +++ b/src/release/unix.rs @@ -0,0 +1,338 @@ +use crate::curl; +use crate::version::Version; +use chrono::{Datelike, NaiveDate, Utc}; +use derive_more::Display; +use serde::{de, Deserialize, Serialize}; +use serde_with::serde_as; +use std::collections::BTreeMap; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum FetchError { + #[error("Can't find releases that matches {0}")] + NotFoundRelease(Version), + + #[error(transparent)] + CurlError(#[from] curl::Error), + + #[error("Receive error message from release site: {0}")] + Other(String), +} + +fn fetch_and_parse( + version: Option, + max: Option, +) -> Result, FetchError> { + let base_url = "https://www.php.net/releases/index.php"; + let query = format!( + "?json=1{}{}", + version + .map(|version| format!("&version={}", version)) + .unwrap_or_default(), + max.map(|max| format!("&max={}", max)).unwrap_or_default(), + ); + let url = &format!("{}{}", base_url, query); + let json = curl::get_as_slice(url)?; + + let resp: Response = + serde_json::from_slice(&json).unwrap_or_else(|_| panic!("Can't parse json from {}", url)); + match resp { + Response::Map(releases) => Ok(releases), + Response::One(release) => Ok([(release.version.unwrap(), release)].into_iter().collect()), + Response::Error { msg } => { + if msg.starts_with("Unknown version") { + Err(FetchError::NotFoundRelease(version.unwrap())) + } else { + Err(FetchError::Other(msg.to_owned())) + } + } + } +} + +pub fn fetch_all(version: Version) -> Result, FetchError> { + fetch_and_parse(Some(version), Some(1000)) +} + +pub fn fetch_latest(version: Version) -> Result { + let mut latest = fetch_and_parse(Some(version), None)?; + let version = *latest.keys().next().unwrap(); + Ok(latest.remove(&version).unwrap()) +} + +#[derive(Deserialize, Debug)] +#[serde(untagged)] +#[serde_as] +enum Response<'a> { + Map(#[serde_as(as = "BTreeMap<_, _>")] BTreeMap), + One(Release), + Error { + #[serde(rename = "error")] + msg: &'a str, + }, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Release { + announcement: Option, + tags: Option>, + source: Vec, + #[serde(rename = "windows")] + windows_binary: Option>, + #[serde(deserialize_with = "date_deserializer")] + pub date: NaiveDate, + museum: Option, + pub version: Option, +} + +// TODO: wait for #[serde(flatten)] for enum variant, or implement custom deserializer +// { "announcement": { English: "/releases/..." } } +// or +// { "announcement": true } +#[derive(Serialize, Deserialize, Debug)] +#[serde(untagged)] +enum Announcement { + English(English), + Flag(bool), +} +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all(deserialize = "PascalCase", serialize = "PascalCase"))] +struct English { + english: String, +} + +#[derive(Serialize, Deserialize, Debug)] +// #[serde(untagged)] +enum Tag { + #[serde(rename = "security")] + Security, + // TOOD: skip deserialize if value is "" + #[serde(rename = "")] + None, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(untagged)] +enum Source { + File(File), + Link { link: String, name: String }, +} +#[derive(Serialize, Deserialize, Debug)] +pub struct File { + filename: String, + name: String, + #[serde(flatten)] + checksum: Option, + // TODO: Option + date: Option, +} +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all(deserialize = "lowercase", serialize = "lowercase"))] +pub enum Hash { + SHA256(String), + MD5(String), +} + +#[derive(Error, Debug)] +pub enum ChecksumError { + #[error("Invalid checksum\nexptected: {expected}\ngot: {got}")] + InvalidChecksum { expected: String, got: String }, + + #[error(transparent)] + Io(#[from] std::io::Error), +} + +use sha2::Digest; +impl Hash { + pub fn hash_type(&self) -> &'static str { + match self { + Hash::SHA256(_) => "SHA-256", + Hash::MD5(_) => "MD5", + } + } + pub fn verify(&self, mut data: impl std::io::Read) -> Result<(), ChecksumError> { + let (checksum, hash) = match self { + Hash::SHA256(checksum) => { + let mut sha256 = sha2::Sha256::new(); + std::io::copy(&mut data, &mut sha256)?; + let hash = sha256.finalize(); + (checksum, format!("{:x}", hash)) + } + Hash::MD5(checksum) => { + let mut md5 = md5::Context::new(); + std::io::copy(&mut data, &mut md5)?; + let hash = md5.compute(); + (checksum, format!("{:x}", hash)) + } + }; + if checksum == &hash { + Ok(()) + } else { + Err(ChecksumError::InvalidChecksum { + expected: checksum.clone(), + got: hash, + }) + } + } +} + +#[derive(Debug, Clone, Copy, Display, PartialEq)] +pub enum Support { + #[display(fmt = "Active support")] + ActiveSupport, + #[display(fmt = "Security fixes only")] + SecurityFixesOnly, + #[display(fmt = "End of life")] + EndOfLife, +} + +impl Release { + fn source_file(&self, extention: &str) -> Option<&File> { + self.source.iter().find_map(|source| match source { + Source::File(file) if file.filename.ends_with(extention) => Some(file), + _ => None, + }) + } + pub fn source_url(&self) -> (String, Option<&Hash>) { + let source_file = self.source_file(".tar.gz").unwrap(); + let url = if self.museum == Some(true) { + let major_version = self.version.unwrap().major_version(); + format!( + "https://museum.php.net/php{}/{}", + major_version, source_file.filename + ) + } else { + format!("https://www.php.net/distributions/{}", source_file.filename) + // format!("http://jp1.php.net/get/{}/from/this/mirror/", filename) + }; + (url, source_file.checksum.as_ref()) + } + pub fn calculate_support(&self) -> Support { + let release_date = self.date; + let release_year = release_date.year(); + let active_support_deadline = release_date + .with_year(release_year + 2) + .unwrap_or_else(|| NaiveDate::from_yo(release_year + 1, 1).succ()); + let security_support_deadline = release_date + .with_year(release_year + 3) + .unwrap_or_else(|| NaiveDate::from_yo(release_year + 2, 1).succ()); + let today = Utc::now().naive_local().date(); + + if today < active_support_deadline { + Support::ActiveSupport + } else if today < security_support_deadline { + Support::SecurityFixesOnly + } else { + Support::EndOfLife + } + } +} + +fn date_deserializer<'de, D>(deserializer: D) -> Result +where + D: de::Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + NaiveDate::parse_from_str(&s, "%d %B %Y").map_err(serde::de::Error::custom) +} + +// fn option_date_deserializer<'de, D>(deserializer: D) -> Result, D::Error> +// where +// D: de::Deserializer<'de>, +// { +// #[derive(Deserialize)] +// struct Helper(#[serde(deserialize_with = "date_deserializer")] NaiveDate); +// let helper = Option::deserialize(deserializer)?; +// Ok(helper.map(|Helper(o)| o)) +// } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn deserialize() { + let json = r#" + { + "8.1.1": { + "announcement": true, + "tags": [], + "date": "16 Dec 2021", + "source": [ + { + "filename": "php-8.1.1.tar.gz", + "name": "PHP 8.1.1 (tar.gz)", + "sha256": "4e4cf3f843a5111f6c55cd21de8f26834ea3cd4a5be77c88357cbcec4a2d671d", + "date": "16 Dec 2021" + }, + { + "filename": "php-8.1.1.tar.bz2", + "name": "PHP 8.1.1 (tar.bz2)", + "sha256": "8f8bc9cad6cd124edc111f7db0a109745e2f638770a101b3c22a2953f7a9b40e", + "date": "16 Dec 2021" + }, + { + "filename": "php-8.1.1.tar.xz", + "name": "PHP 8.1.1 (tar.xz)", + "sha256": "33c09d76d0a8bbb5dd930d9dd32e6bfd44e9efcf867563759eb5492c3aff8856", + "date": "16 Dec 2021" + } + ] + }, + "8.0.14": { + "announcement": true, + "tags": [], + "date": "16 Dec 2021", + "source": [ + { + "filename": "php-8.0.14.tar.gz", + "name": "PHP 8.0.14 (tar.gz)", + "sha256": "e67ebd8c4c77247ad1fa88829e5b95d51a19edf3d87814434de261e20a63ea20", + "date": "16 Dec 2021" + }, + { + "filename": "php-8.0.14.tar.bz2", + "name": "PHP 8.0.14 (tar.bz2)", + "sha256": "bb381fdf4817ad7c24c23ea7f77cad68dceb86eb3ac1a37acedadf8ad0a0cd4b", + "date": "16 Dec 2021" + }, + { + "filename": "php-8.0.14.tar.xz", + "name": "PHP 8.0.14 (tar.xz)", + "sha256": "fbde8247ac200e4de73449d9fefc8b495d323b5be9c10cdb645fb431c91156e3", + "date": "16 Dec 2021" + } + ] + } + } + "#; + let resp: Result = serde_json::from_str(json); + assert!(resp.is_ok()); + let resp = resp.unwrap(); + assert!(if let Response::Map(map) = resp { + map.contains_key(&"8.1.1".parse().unwrap()) + } else { + false + }); + } + #[test] + fn fetch_all_major_test() { + let releases = fetch_all("3".parse().unwrap()); + assert!(releases.is_ok()); + assert!(!releases.unwrap().is_empty()); + let releases = fetch_all("5".parse().unwrap()); + assert!(releases.is_ok()); + assert!(!releases.unwrap().is_empty()); + let releases = fetch_all("8".parse().unwrap()); + assert!(releases.is_ok()); + assert!(!releases.unwrap().is_empty()); + } + #[test] + fn fetch_latest_test() { + let latest_release = fetch_latest("7.0".parse().unwrap()); + assert!(latest_release.is_ok()); + assert_eq!( + latest_release.unwrap().version.unwrap(), + "7.0.33".parse().unwrap() + ); + } +} diff --git a/src/release/windows.rs b/src/release/windows.rs new file mode 100644 index 0000000..e45665c --- /dev/null +++ b/src/release/windows.rs @@ -0,0 +1,132 @@ +#![cfg(windows)] + +use regex::{Captures, Regex}; +use std::collections::BTreeMap; + +use crate::curl; +use crate::version::Version; + +const ARCH: &str = if cfg!(target_arch = "x86") { + "x86" +} else if cfg!(target_arch = "x86_64") { + "x64" +} else { + panic!() +}; +const BASE_URL: &str = "https://windows.php.net/downloads/releases"; +const NUMBER_REGEX: &str = r"[1-9]+\d*|0"; + +fn build_regex(version: Option) -> Regex { + let major_regex = version + .map(|v| v.major_version().to_string()) + .unwrap_or(NUMBER_REGEX.to_string()); + let minor_regex = version + .and_then(|v| v.minor_version()) + .map(|minor| minor.to_string()) + .unwrap_or(NUMBER_REGEX.to_string()); + let patch_regex = version + .and_then(|v| v.patch_version()) + .map(|patch| patch.to_string()) + .unwrap_or(NUMBER_REGEX.to_string()); + Regex::new(&format!( + r"(?x) + ( + php-((?:{})\.(?:{})\.(?:{})) + (?:\-(nts))? + \-Win32 + \-(VC|vs)(\d+) + \-({}) + \.zip + ) + ", + major_regex, minor_regex, patch_regex, ARCH + )) + .unwrap() +} + +pub fn fetch(version: Option) -> Result, super::FetchError> { + let regex = build_regex(version); + + let past_url = &format!("{}{}", BASE_URL, "/archives/"); + let utf8 = curl::get_as_slice(past_url)?; + let html = std::str::from_utf8(&utf8).unwrap(); + let past_releases = regex.captures_iter(html); + + let latest_url = &format!("{}{}", BASE_URL, "/"); + let utf8 = curl::get_as_slice(latest_url)?; + let html = std::str::from_utf8(&utf8).unwrap(); + let latest_releases = regex.captures_iter(html); + + let releases: BTreeMap = past_releases + .chain(latest_releases) + .map(|cap| Release::from(cap)) + .map(|release| (release.version, release)) + .collect(); + + if let Some(version) = version { + if releases.len() == 0 { + return Err(super::FetchError::NotFoundRelease(version)); + } + } + + Ok(releases) +} + +#[derive(Debug)] +pub struct Release { + pub version: Version, + pub thread_safe: bool, + pub compiler_version: CompilerVersion, + pub arch: Arch, + pub filename: String, +} + +#[derive(Debug)] +pub enum CompilerVersion { + VisualCpp(usize), + VisualStudio(usize), +} + +#[derive(Debug, PartialEq)] +#[allow(non_camel_case_types)] +pub enum Arch { + x86, + x64, +} + +impl From> for Release { + fn from(cap: Captures) -> Self { + let compiler_version = cap.get(5).unwrap().as_str().parse().unwrap(); + let compiler_version = match cap.get(4).unwrap().as_str() { + "VC" => CompilerVersion::VisualCpp(compiler_version), + "vs" => CompilerVersion::VisualStudio(compiler_version), + _ => panic!(), + }; + let arch = match cap.get(6).unwrap().as_str() { + "x86" => Arch::x86, + "x64" => Arch::x64, + _ => panic!(), + }; + Release { + version: cap.get(2).unwrap().as_str().parse().unwrap(), + thread_safe: cap.get(3).is_none(), + compiler_version, + arch, + filename: cap.get(1).unwrap().as_str().to_owned(), + } + } +} + +#[cfg(test)] +mod test { + + use super::*; + + #[test] + fn test() { + let query_version: Version = "8.0.0".parse().unwrap(); + let release = fetch(Some(query_version)).unwrap(); + + assert_eq!(release.keys().into_iter().next(), Some(&query_version)) + } +} diff --git a/src/shell.rs b/src/shell.rs index 1a00bf7..a1aadd0 100644 --- a/src/shell.rs +++ b/src/shell.rs @@ -2,68 +2,29 @@ use crate::version; use indoc::formatdoc; use std::fmt::Display; use std::path::Path; -use std::str::FromStr; -use thiserror::Error; #[derive(Debug, Clone, Copy)] pub enum Shell { Bash, Zsh, Fish, + PowerShell, } pub const fn available_shells() -> &'static [&'static str] { - &["bash", "zsh", "fish"] + &["bash", "zsh", "fish", "powershell"] } -#[derive(Debug, Error)] -pub enum ShellDetectError { - #[error("parent process tracing count reached the limit: {MAX_SEARCH_ITERATIONS}")] - TooManyTracing, - - #[error("reached first process PID=0 when tracing processes")] - ReachedFirstProcess, - - #[error(transparent)] - FailedGetProcessInfo(#[from] ProcessInfoError), -} - -const MAX_SEARCH_ITERATIONS: u8 = 10; - use Shell::*; impl Shell { - pub fn detect_shell() -> Result { - let mut pid = std::process::id(); - let mut visited = 0; - - loop { - if visited > MAX_SEARCH_ITERATIONS { - return Err(ShellDetectError::TooManyTracing); - } - if pid == 0 { - return Err(ShellDetectError::ReachedFirstProcess); - } - let process_info = get_process_info(pid)?; - let binary = process_info - .command - .trim_start_matches('-') - .split('/') - .last() - .unwrap(); - if let Ok(shell) = Self::from_str(binary) { - return Ok(shell); - } - pid = process_info.parent_pid; - visited += 1; - } - } pub fn set_path(&self, path: impl AsRef) -> String { match &self { Bash | Zsh => { format!("export PATH={}:$PATH", path.as_ref().display()) } Fish => format!("set -gx PATH {} $PATH;", path.as_ref().display()), + PowerShell => unimplemented!(), } } pub fn set_env(&self, name: impl Display, value: impl Display) -> String { @@ -72,6 +33,7 @@ impl Shell { format!("export {}={}", name, value) } Fish => format!("set -gx {} {};", name, value), + PowerShell => unimplemented!(), } } pub fn auto_switch_hook(&self, version_file: &version::File) -> String { @@ -125,12 +87,16 @@ impl Shell { phpup_use = phpup_use ) } + PowerShell => { + unimplemented!() + } } } pub fn rehash(&self) -> Option { match &self { Bash | Fish => None, Zsh => Some("rehash".to_string()), + PowerShell => unimplemented!(), } } pub fn to_clap_shell(&self) -> clap_complete::Shell { @@ -138,92 +104,11 @@ impl Shell { Bash => clap_complete::Shell::Bash, Zsh => clap_complete::Shell::Zsh, Fish => clap_complete::Shell::Fish, + PowerShell => clap_complete::Shell::PowerShell, } } } -#[derive(Debug, Error)] -pub enum ParseShellError { - #[error("Unknown shell: {0}")] - UnknownShell(String), -} -impl FromStr for Shell { - type Err = ParseShellError; - - fn from_str(s: &str) -> Result { - match s { - "bash" | "dash" => Ok(Bash), - "zsh" => Ok(Zsh), - "fish" => Ok(Fish), - _ => Err(ParseShellError::UnknownShell(s.to_owned())), - } - } -} - -struct ProcessInfo { - parent_pid: u32, - command: String, -} -#[derive(Debug, Error)] -pub enum ProcessInfoError { - #[error("Can't execute `{command}` because {source}")] - FailedExecute { - command: String, - #[source] - source: std::io::Error, - }, - - #[error("failed to exec '{0}' command")] - ExitFailed(String), - - #[error("can't parse 'ps' command output: {0}")] - Parse(String), - - #[error(transparent)] - Io(#[from] std::io::Error), -} -#[cfg(unix)] -fn get_process_info(pid: u32) -> Result { - use std::io::{BufRead, BufReader}; - use std::process::Command; - - let ps = [ - "ps".to_owned(), - "-o".to_owned(), - "ppid=,comm=".to_owned(), - pid.to_string(), - ]; - - let mut child = Command::new(&ps[0]) - .args(&ps[1..]) - .stdout(std::process::Stdio::piped()) - .stderr(std::process::Stdio::null()) - .spawn() - .map_err(|source| ProcessInfoError::FailedExecute { - command: ps.join(" "), - source, - })?; - - match child.wait() { - Ok(status) if status.success() => {} - _ => return Err(ProcessInfoError::ExitFailed(ps.join(" "))), - } - - let mut line = String::new(); - BufReader::new(child.stdout.unwrap()).read_line(&mut line)?; - - let mut parts = line.trim().split_whitespace(); - let ppid = parts - .next() - .ok_or_else(|| ProcessInfoError::Parse(line.to_string()))?; - let command = parts - .next() - .ok_or_else(|| ProcessInfoError::Parse(line.to_string()))?; - - Ok(ProcessInfo { - parent_pid: ppid - .parse() - .map_err(|_| ProcessInfoError::Parse(line.to_string()))?, - command: command.into(), - }) -} +mod detect; +pub use detect::detect; +pub use detect::ShellDetectError; diff --git a/src/shell/detect.rs b/src/shell/detect.rs new file mode 100644 index 0000000..da3105b --- /dev/null +++ b/src/shell/detect.rs @@ -0,0 +1,45 @@ +mod unix; +mod windows; + +use std::str::FromStr; +use thiserror::Error; + +#[cfg(unix)] +pub use self::unix::detect; +#[cfg(windows)] +pub use self::windows::detect; + +const MAX_SEARCH_ITERATIONS: u8 = 10; + +#[derive(Debug, Error)] +pub enum ParseShellError { + #[error("Unknown shell: {0}")] + UnknownShell(String), +} + +impl FromStr for super::Shell { + type Err = ParseShellError; + + fn from_str(s: &str) -> Result { + match s { + "bash" | "dash" => Ok(super::Bash), + "zsh" => Ok(super::Zsh), + "fish" => Ok(super::Fish), + "powershell" => Ok(super::PowerShell), + _ => Err(ParseShellError::UnknownShell(s.to_owned())), + } + } +} + +#[derive(Debug, Error)] +pub enum ShellDetectError { + #[error("parent process tracing count reached the limit: {MAX_SEARCH_ITERATIONS}")] + TooManyTracing, + + #[error("reached first process PID=0 when tracing processes")] + ReachedFirstProcess, + + #[cfg(unix)] + #[error(transparent)] + FailedGetProcessInfo(#[from] unix::ProcessInfoError), +} diff --git a/src/shell/detect/unix.rs b/src/shell/detect/unix.rs new file mode 100644 index 0000000..d41f0c9 --- /dev/null +++ b/src/shell/detect/unix.rs @@ -0,0 +1,97 @@ +#![cfg(unix)] + +use thiserror::Error; + +pub fn detect() -> Result { + let mut pid = std::process::id(); + let mut visited = 0; + + loop { + if visited > super::MAX_SEARCH_ITERATIONS { + return Err(super::ShellDetectError::TooManyTracing); + } + if pid == 0 { + return Err(super::ShellDetectError::ReachedFirstProcess); + } + let process_info = get_process_info(pid)?; + let process_name = process_info + .command + .trim_start_matches('-') + .split('/') + .last() + .unwrap(); + if let Ok(shell) = process_name.parse() { + return Ok(shell); + } + pid = process_info.parent_pid; + visited += 1; + } +} + +struct ProcessInfo { + parent_pid: u32, + command: String, +} +#[derive(Debug, Error)] +pub enum ProcessInfoError { + #[error("Can't execute `{command}` because {source}")] + FailedExecute { + command: String, + #[source] + source: std::io::Error, + }, + + #[error("failed to exec '{0}' command")] + ExitFailed(String), + + #[error("can't parse 'ps' command output: {0}")] + Parse(String), + + #[error(transparent)] + Io(#[from] std::io::Error), +} + +fn get_process_info(pid: u32) -> Result { + use std::io::{BufRead, BufReader}; + use std::process::Command; + + let ps = [ + "ps".to_owned(), + "-o".to_owned(), + "ppid=,comm=".to_owned(), + pid.to_string(), + ]; + + let mut child = Command::new(&ps[0]) + .args(&ps[1..]) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::null()) + .spawn() + .map_err(|source| ProcessInfoError::FailedExecute { + command: ps.join(" "), + source, + })?; + + match child.wait() { + Ok(status) if status.success() => {} + _ => return Err(ProcessInfoError::ExitFailed(ps.join(" "))), + } + + let mut line = String::new(); + BufReader::new(child.stdout.unwrap()).read_line(&mut line)?; + + let mut parts = line.trim().split_whitespace(); + let ppid = parts + .next() + .ok_or_else(|| ProcessInfoError::Parse(line.to_string()))?; + let command = parts + .next() + .ok_or_else(|| ProcessInfoError::Parse(line.to_string()))?; + + Ok(ProcessInfo { + parent_pid: ppid + .parse() + .map_err(|_| ProcessInfoError::Parse(line.to_string()))?, + command: command.into(), + }) +} diff --git a/src/shell/detect/windows.rs b/src/shell/detect/windows.rs new file mode 100644 index 0000000..8ed22d8 --- /dev/null +++ b/src/shell/detect/windows.rs @@ -0,0 +1,33 @@ +#![cfg(windows)] + +use std::ffi::OsStr; +use sysinfo::{ProcessExt, System, SystemExt}; + +pub fn detect() -> Result { + let mut system = System::new(); + let mut current_pid = sysinfo::get_current_pid().ok(); + let mut visited = 0; + + while let Some(pid) = current_pid { + if visited > super::MAX_SEARCH_ITERATIONS { + return Err(super::ShellDetectError::TooManyTracing); + } + system.refresh_process(pid); + if let Some(process) = system.process(pid) { + current_pid = process.parent(); + let process_name = process + .exe() + .file_stem() + .and_then(OsStr::to_str) + .map(str::to_lowercase); + if let Some(shell) = process_name.as_deref().and_then(|x| x.parse().ok()) { + return Ok(shell); + } + } else { + current_pid = None; + } + visited += 1; + } + + Err(super::ShellDetectError::ReachedFirstProcess) +} diff --git a/src/symlink.rs b/src/symlink.rs index aad0306..fcb93dc 100644 --- a/src/symlink.rs +++ b/src/symlink.rs @@ -13,3 +13,16 @@ pub fn remove>(symlink_file: P) -> std::io::Result<()> { } Ok(()) } + +#[cfg(windows)] +pub fn link, U: AsRef>(from: P, to: U) -> std::io::Result<()> { + junction::create(from, to) +} + +#[cfg(windows)] +pub fn remove>(junction: P) -> std::io::Result<()> { + if junction::exists(&junction).is_ok() { + std::fs::remove_dir(junction)?; + } + Ok(()) +}