use crate::alias::create_alias;
use crate::archive::{self, extract::Error as ExtractError, extract::Extract};
use crate::config::FrumConfig;
use crate::input_version::InputVersion;
use crate::outln;
use crate::version::Version;
use crate::version_file::get_user_version_for_directory;
use anyhow::Result;
use colored::Colorize;
use log::debug;
use reqwest::Url;
use std::io::prelude::*;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum FrumError {
#[error(transparent)]
HttpError(#[from] reqwest::Error),
#[error(transparent)]
IoError(#[from] std::io::Error),
#[error("Can't find the number of cores")]
FromUtf8Error(#[from] std::string::FromUtf8Error),
#[error("Can't extract the file: {source:?}")]
ExtractError { source: ExtractError },
#[error("The downloaded archive is empty")]
TarIsEmpty,
#[error("Can't find version: {version}")]
VersionNotFound { version: InputVersion },
#[error("Can't list the remote versions: {source:?}")]
CantListRemoteVersions { source: reqwest::Error },
#[error("Version already installed at {path:?}")]
VersionAlreadyInstalled { path: PathBuf },
#[error("Can't find version in dotfiles. Please provide a version manually to the command.")]
CantInferVersion,
#[error("The requested version is not installable: {version}")]
NotInstallableVersion { version: Version },
#[error("Can't build Ruby: {stderr}")]
CantBuildRuby { stderr: String },
}
pub struct Install {
pub version: Option<InputVersion>,
pub configure_opts: Vec<String>,
}
impl crate::command::Command for Install {
type Error = FrumError;
fn apply(&self, config: &FrumConfig) -> Result<(), Self::Error> {
let current_version = self
.version
.clone()
.or_else(|| get_user_version_for_directory(std::env::current_dir().unwrap()))
.ok_or(FrumError::CantInferVersion)?;
let version = match current_version.clone() {
InputVersion::Full(Version::Semver(v)) => Version::Semver(v),
InputVersion::Full(Version::System) => {
return Err(FrumError::NotInstallableVersion {
version: Version::System,
})
}
current_version => {
let available_versions = crate::remote_ruby_index::list(&config.ruby_build_mirror)
.map_err(|source| FrumError::CantListRemoteVersions { source })?
.drain(..)
.map(|x| x.version)
.collect::<Vec<_>>();
current_version
.to_version(&available_versions)
.ok_or(FrumError::VersionNotFound {
version: current_version,
})?
.clone()
}
};
let installations_dir = config.versions_dir();
let installation_dir = PathBuf::from(&installations_dir).join(version.to_string());
if installation_dir.exists() {
return Err(FrumError::VersionAlreadyInstalled {
path: installation_dir,
});
}
let url = package_url(config.ruby_build_mirror.clone(), &version);
outln!(config#Info, "{} Downloading {}", "==>".green(), format!("{}", url).green());
let response = reqwest::blocking::get(url)?;
if response.status() == 404 {
return Err(FrumError::VersionNotFound {
version: current_version,
});
}
outln!(config#Info, "{} Extracting {}", "==>".green(), archive(&version).green());
let temp_installations_dir = installations_dir.join(".downloads");
std::fs::create_dir_all(&temp_installations_dir).map_err(FrumError::IoError)?;
let temp_dir = tempfile::TempDir::new_in(&temp_installations_dir)
.expect("Can't generate a temp directory");
extract_archive_into(&temp_dir, response)?;
outln!(config#Info, "{} Building {}", "==>".green(), format!("Ruby {}", current_version).green());
let installed_directory = std::fs::read_dir(&temp_dir)
.map_err(FrumError::IoError)?
.next()
.ok_or(FrumError::TarIsEmpty)?
.map_err(FrumError::IoError)?;
let installed_directory = installed_directory.path();
build_package(
&installed_directory,
&installation_dir,
&self.configure_opts,
)?;
if !config.default_version_dir().exists() {
debug!("Use {} as the default version", current_version);
create_alias(&config, "default", &version).map_err(FrumError::IoError)?;
}
Ok(())
}
}
fn extract_archive_into<P: AsRef<Path>>(
path: P,
response: reqwest::blocking::Response,
) -> Result<(), FrumError> {
#[cfg(unix)]
let extractor = archive::tar_xz::TarXz::new(response);
#[cfg(windows)]
let extractor = archive::zip::Zip::new(response);
extractor
.extract_into(path)
.map_err(|source| FrumError::ExtractError { source })?;
Ok(())
}
fn package_url(mirror_url: Url, version: &Version) -> Url {
debug!("pakage url");
Url::parse(&format!(
"{}/{}/{}",
mirror_url.as_str().trim_end_matches('/'),
match version {
Version::Semver(version) => format!("{}.{}", version.major, version.minor),
_ => unreachable!(),
},
archive(version),
))
.unwrap()
}
#[cfg(unix)]
fn archive(version: &Version) -> String {
format!("ruby-{}.tar.xz", version)
}
#[cfg(windows)]
fn archive(version: &Version) -> String {
format!("ruby-{}.zip", version)
}
#[allow(clippy::unnecessary_wraps)]
fn openssl_dir() -> Result<String, FrumError> {
#[cfg(target_os = "macos")]
return Ok(String::from_utf8_lossy(
&Command::new("brew")
.arg("--prefix")
.arg("openssl@1.1")
.output()
.map_err(FrumError::IoError)?
.stdout,
)
.trim()
.to_string());
#[cfg(not(target_os = "macos"))]
return Ok("/usr/local".to_string());
}
fn build_package(
current_dir: &Path,
installed_dir: &Path,
configure_opts: &[String],
) -> Result<(), FrumError> {
debug!("./configure {}", configure_opts.join(" "));
let mut command = Command::new("sh");
command
.arg("configure")
.arg(format!("--prefix={}", installed_dir.to_str().unwrap()))
.args(configure_opts);
if !configure_opts
.iter()
.any(|opt| opt.starts_with("--with-openssl-dir"))
{
command.arg(format!("--with-openssl-dir={}", openssl_dir()?));
}
let configure = command
.current_dir(¤t_dir)
.output()
.map_err(FrumError::IoError)?;
if !configure.status.success() {
return Err(FrumError::CantBuildRuby {
stderr: format!(
"configure failed: {}",
String::from_utf8_lossy(&configure.stderr).to_string()
),
});
};
debug!("make -j {}", num_cpus::get().to_string());
let make = Command::new("make")
.arg("-j")
.arg(num_cpus::get().to_string())
.current_dir(¤t_dir)
.output()
.map_err(FrumError::IoError)?;
if !make.status.success() {
return Err(FrumError::CantBuildRuby {
stderr: format!(
"make failed: {}",
String::from_utf8_lossy(&make.stderr).to_string()
),
});
};
debug!("make install");
let make_install = Command::new("make")
.arg("install")
.current_dir(¤t_dir)
.output()
.map_err(FrumError::IoError)?;
if !make_install.status.success() {
return Err(FrumError::CantBuildRuby {
stderr: format!(
"make install: {}",
String::from_utf8_lossy(&make_install.stderr).to_string()
),
});
};
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::command::Command;
use crate::config::FrumConfig;
use crate::version::Version;
use tempfile::tempdir;
#[test]
fn test_install_second_version() {
let config = FrumConfig {
base_dir: Some(tempdir().unwrap().path().to_path_buf()),
..Default::default()
};
Install {
version: Some(InputVersion::Full(Version::Semver(
semver::Version::parse("2.7.0").unwrap(),
))),
configure_opts: vec![],
}
.apply(&config)
.expect("Can't install 2.7.0");
Install {
version: Some(InputVersion::Full(Version::Semver(
semver::Version::parse("2.6.4").unwrap(),
))),
configure_opts: vec![],
}
.apply(&config)
.expect("Can't install 2.6.4");
assert_eq!(
std::fs::read_link(&config.default_version_dir())
.unwrap()
.components()
.last(),
Some(std::path::Component::Normal(std::ffi::OsStr::new("2.7.0")))
);
}
#[test]
fn test_install_default_version() {
let config = FrumConfig {
base_dir: Some(tempdir().unwrap().path().to_path_buf()),
..Default::default()
};
Install {
version: Some(InputVersion::Full(Version::Semver(
semver::Version::parse("2.6.4").unwrap(),
))),
configure_opts: vec![],
}
.apply(&config)
.expect("Can't install");
assert!(config.versions_dir().join("2.6.4").exists());
assert!(config
.versions_dir()
.join("2.6.4")
.join("bin")
.join("ruby")
.exists());
assert!(config.default_version_dir().exists());
}
}