use crate::{
prelude::{candidates_api, candidates_dir, platform, Candidate},
sdkman::{archives_dir, temp_dir},
};
use std::{ffi::OsStr, io};
pub(crate) mod results {
pub use super::default_command::{ChangedDefault, DefaultResult};
pub use super::install_command::InstallError;
pub use super::install_command::InstallResult;
pub use super::use_command::{UseResult, Using};
}
pub struct JdkCurrent;
pub struct JdkDefault;
pub struct JdkInstall;
pub struct JdkList;
pub struct JdkUse;
impl JdkCurrent {
pub fn run() -> Option<Candidate> {
current_command::run(candidates_dir())
}
}
impl JdkDefault {
pub fn run(
query: Option<impl AsRef<OsStr>>,
show_suggested_default: bool,
verbose: bool,
) -> io::Result<results::DefaultResult> {
if show_suggested_default {
Ok(results::DefaultResult::Suggest(default_command::suggested(
verbose,
candidates_api(),
)?))
} else {
default_command::run(candidates_dir(), verbose, query.as_ref().map(OsStr::new))
}
}
pub fn set(candidate: Candidate) -> io::Result<results::ChangedDefault> {
default_command::set_default(candidates_dir(), candidate)
}
}
impl JdkList {
pub fn run(verbose: bool, all: bool) -> io::Result<String> {
let current = JdkCurrent::run();
if all {
list_command::run_online(
current.as_ref().map(Candidate::name),
candidates_dir(),
candidates_api(),
platform(),
verbose,
)
} else {
list_command::run(current.as_ref().map(Candidate::name), candidates_dir())
}
}
}
impl JdkInstall {
pub fn run(
verbose: bool,
install_suggested_default: bool,
validate_version: bool,
version: &str,
) -> results::InstallResult {
install_command::run(
candidates_dir(),
archives_dir(),
temp_dir(),
candidates_api(),
platform(),
verbose,
install_suggested_default,
validate_version,
version,
)
}
}
impl JdkUse {
pub fn run(query: Option<impl AsRef<OsStr>>, verbose: bool) -> io::Result<results::UseResult> {
let current = JdkCurrent::run();
use_command::run(
candidates_dir(),
current.as_ref().map(Candidate::name),
verbose,
query.as_ref().map(OsStr::new),
)
}
pub fn set(candidate: Candidate) -> results::Using {
use_command::set_use(candidates_dir(), candidate)
}
}
mod current_command {
use crate::candidate::Candidate;
use std::{env, fs, path::Path};
pub(super) fn run(candidates_dir: &Path) -> Option<Candidate> {
let path = env::var_os("PATH").unwrap_or_default();
env::split_paths(&path)
.find_map(|path| {
path.starts_with(candidates_dir)
.then(|| {
path.ancestors()
.skip(1)
.take_while(|path| path.starts_with(candidates_dir))
.try_fold(&*path, |parent_parent, parent| {
if parent == candidates_dir {
Err(parent_parent)
} else {
Ok(parent)
}
})
.err()
.map(ToOwned::to_owned)
})
.flatten()
})
.map(fs::canonicalize)
.and_then(Result::ok)
.map(Candidate::new)
}
}
mod use_command {
use super::shared::select_candidates;
use crate::{prelude::Candidate, select::Selection};
use std::{
env,
ffi::{OsStr, OsString},
io,
path::{Path, PathBuf},
};
pub enum UseResult {
KeepCurrent,
Invalid(Option<String>),
Use(Using),
}
pub struct Using {
pub name: String,
pub java_home: PathBuf,
pub path: Option<OsString>,
}
pub(super) fn run(
candidates_dir: &Path,
current: Option<&str>,
verbose: bool,
query: Option<&OsStr>,
) -> io::Result<UseResult> {
let selected = select_candidates(query, current, candidates_dir, verbose)?;
let use_result = match selected {
Selection::Cancelled => UseResult::KeepCurrent,
Selection::NoMatch => {
UseResult::Invalid(query.and_then(|q| q.to_str()).map(String::from))
}
Selection::Selected(selection) => {
let using = set_use(candidates_dir, selection);
UseResult::Use(using)
}
};
Ok(use_result)
}
pub(super) fn set_use(candidates_dir: &Path, candidate: Candidate) -> Using {
let name = String::from(candidate.name());
let candidate_path = candidate.into_path();
let old_path = env::var_os("PATH");
let new_path = old_path.map(|path| {
let mut candidate_path = Some(candidate_path.clone());
let new_path = env::split_paths(&path).filter_map(|path| {
if path.starts_with(candidates_dir) {
candidate_path.take().map(|mut new_candidate_path| {
let previous_path = path
.strip_prefix(candidates_dir)
.expect("starts_with returned true");
for path_segment in previous_path.iter().skip(1) {
new_candidate_path.push(path_segment);
}
new_candidate_path
})
} else {
Some(path)
}
});
env::join_paths(new_path).expect("input is from existing PATH")
});
Using {
name,
java_home: candidate_path,
path: new_path,
}
}
}
mod default_command {
use super::shared::{request, select_candidates};
use crate::{prelude::Candidate, select::Selection};
use std::{
ffi::OsStr,
fs,
io::{self, ErrorKind},
path::Path,
};
#[cfg(unix)]
use std::os::unix::fs::symlink;
#[cfg(windows)]
use std::os::windows::fs::symlink_dir as symlink;
pub enum DefaultResult {
KeepCurrent,
CandidateNotFound { query: Option<String> },
Selected(ChangedDefault),
Suggest(String),
}
pub struct ChangedDefault {
pub name: String,
pub before: Option<String>,
}
pub(super) fn suggested(verbose: bool, candidates_api: &str) -> io::Result<String> {
request(format!("{candidates_api}/candidates/default/java"), verbose)
}
pub(super) fn run(
candidates_dir: &Path,
verbose: bool,
query: Option<&OsStr>,
) -> io::Result<DefaultResult> {
let selected = select_candidates(query, None, candidates_dir, verbose)?;
let default_result = match selected {
Selection::Cancelled => DefaultResult::KeepCurrent,
Selection::NoMatch => {
let query = query.and_then(|q| q.to_str()).map(String::from);
DefaultResult::CandidateNotFound { query }
}
Selection::Selected(selection) => {
let selected = set_default(candidates_dir, selection)?;
DefaultResult::Selected(selected)
}
};
Ok(default_result)
}
pub(super) fn set_default(
candidates_dir: &Path,
candidate: Candidate,
) -> io::Result<ChangedDefault> {
let name = String::from(candidate.name());
let candidate_path = candidate.into_path();
let current_path = candidates_dir.join("current");
let previous = match fs::symlink_metadata(¤t_path) {
Ok(meta) => {
if meta.is_file() || meta.is_dir() {
return Err(io::Error::new(
ErrorKind::AlreadyExists,
format!(
"The 'current' link [{}] cannot be changed since it is not a link.",
current_path.display()
),
));
}
let previous = fs::read_link(¤t_path)?;
let previous = previous.strip_prefix(candidates_dir).unwrap_or(&previous);
let previous = previous.to_str().map(String::from);
fs::remove_file(¤t_path)?;
previous
}
Err(err) if err.kind() == ErrorKind::NotFound => None,
Err(e) => return Err(e),
};
let candidate_path = candidate_path
.strip_prefix(candidates_dir)
.unwrap_or(&candidate_path);
symlink(candidate_path, ¤t_path)?;
Ok(ChangedDefault {
name,
before: previous,
})
}
}
mod validate_command {
use super::shared::request;
use std::io;
pub(super) fn run(
candidates_api: &str,
platform: &str,
verbose: bool,
version: &str,
) -> io::Result<bool> {
let url = format!("{candidates_api}/candidates/validate/java/{version}/{platform}",);
let valid = request(url, verbose)?;
match valid.trim() {
"valid" => Ok(true),
"invalid" => Ok(false),
otherwise => {
let err_msg = format!("Unexpected validation response, expected either 'valid' or 'invalid', but got {otherwise}");
Err(crate::io_err(&err_msg))
}
}
}
}
mod install_command {
use super::{default_command, validate_command};
use crate::{eprintln_color, prelude::Candidate};
use bstr::ByteSlice;
use console::Style;
use std::{
fs::{self, OpenOptions},
io::{self, BufWriter, ErrorKind},
path::{Path, PathBuf},
process::{Command, Stdio},
time::Instant,
};
pub type InstallResult = Result<Candidate, InstallError>;
pub enum InstallError {
AlreadyInstalled(String),
InvalidVersion(String),
ArchiveCorrupt(String, PathBuf),
DownloadError(reqwest::Error),
Other(io::Error),
}
impl From<io::Error> for InstallError {
fn from(val: io::Error) -> Self {
Self::Other(val)
}
}
impl From<reqwest::Error> for InstallError {
fn from(val: reqwest::Error) -> Self {
Self::DownloadError(val)
}
}
#[allow(clippy::too_many_arguments)]
pub(super) fn run(
candidates_dir: &Path,
archives_dir: &Path,
temp_dir: &Path,
candidates_api: &str,
platform: &str,
verbose: bool,
install_suggested_default: bool,
validate_version: bool,
version: &str,
) -> InstallResult {
let default_version;
let version = if install_suggested_default {
default_version = default_command::suggested(verbose, candidates_api)?;
&default_version
} else {
version
};
let candidates_path = validate_candidate(
candidates_dir,
candidates_api,
platform,
verbose,
validate_version,
version,
)?;
let zip_archive_target = archives_dir.join(format!("java-{version}.zip"));
if !matches!(fs::symlink_metadata(&zip_archive_target), Ok(meta) if meta.is_file()) {
download(
temp_dir,
candidates_api,
platform,
verbose,
version,
&zip_archive_target,
)?;
} else {
eprintln!(
"Found a previously downloaded java {version} archive. Not downloading it again..."
);
}
let zip_archive_target = validate_archive(version, zip_archive_target)?;
install_archive(temp_dir, version, candidates_path, zip_archive_target)
}
pub(super) fn validate_candidate(
candidates_dir: &Path,
candidates_api: &str,
platform: &str,
verbose: bool,
validate_version: bool,
version: &str,
) -> Result<PathBuf, InstallError> {
let candidates_path = candidates_dir.join(version);
if matches!(fs::symlink_metadata(&candidates_path), Ok(existing) if !existing.is_file()) {
return Err(InstallError::AlreadyInstalled(version.into()));
}
if validate_version && !validate_command::run(candidates_api, platform, verbose, version)? {
return Err(InstallError::InvalidVersion(version.into()));
};
Ok(candidates_path)
}
pub(super) fn download(
temp_dir: &Path,
candidates_api: &str,
platform: &str,
verbose: bool,
version: &str,
zip_archive_target: &Path,
) -> Result<(), InstallError> {
pre_install(temp_dir, candidates_api, platform, verbose, version)?;
let url = format!("{candidates_api}/broker/download/java/{version}/{platform}");
let binary_input = temp_dir.join(format!("java-{version}.bin"));
let file = OpenOptions::new()
.write(true)
.create_new(true)
.open(&binary_input)?;
let start = Instant::now();
let mut response = reqwest::blocking::get(&url)?;
if let Some(size) = response.content_length() {
file.set_len(size)?;
}
let mut wtr = BufWriter::new(file);
let bytes = response.copy_to(&mut wtr)?;
if verbose {
let took = start.elapsed();
eprintln_color!(@Style::new().for_stderr().dim(), "Downloaded {} bytes of binaryto {} in {:?}", bytes, binary_input.display(), took);
}
let zip_output = post_install(
temp_dir,
candidates_api,
platform,
verbose,
version,
binary_input,
)?;
dbg!(fs::rename(dbg!(zip_output), dbg!(&zip_archive_target)))?;
Ok(())
}
pub(super) fn validate_archive(
version: &str,
zip_archive_target: PathBuf,
) -> Result<PathBuf, InstallError> {
let output = Command::new("unzip")
.arg("-t")
.arg(&zip_archive_target)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.output()?;
if !output.status.success()
|| !output
.stdout
.contains_str("No errors detected in compressed data")
{
try_fs(|| fs::remove_file(&zip_archive_target))?;
return Err(InstallError::ArchiveCorrupt(
version.into(),
zip_archive_target,
));
}
Ok(zip_archive_target)
}
pub(super) fn install_archive(
temp_dir: &Path,
version: &str,
candidates_path: PathBuf,
zip_archive_target: PathBuf,
) -> InstallResult {
let out = temp_dir.join("out");
try_fs(|| fs::remove_dir_all(&out))?;
if !Command::new("unzip")
.arg("-oq")
.arg(&zip_archive_target)
.arg("-d")
.arg(&out)
.status()?
.success()
{
return Err(InstallError::ArchiveCorrupt(
version.into(),
zip_archive_target,
));
}
dbg!(fs::rename(dbg!(out), dbg!(&candidates_path)))?;
Ok(Candidate::new(candidates_path))
}
fn pre_install(
_temp_dir: &Path,
_candidates_api: &str,
_platform: &str,
_verbose: bool,
_version: &str,
) -> io::Result<()> {
Ok(())
}
fn post_install(
temp_dir: &Path,
_candidates_api: &str,
_platform: &str,
_verbose: bool,
version: &str,
binary_input: PathBuf,
) -> Result<PathBuf, InstallError> {
eprintln!("Running tar test");
let binary_validation = Command::new("tar")
.arg("tzf")
.arg(&binary_input)
.stdout(Stdio::null())
.status()?;
if !binary_validation.success() {
return Err(InstallError::ArchiveCorrupt(version.into(), binary_input));
}
let work_dir = temp_dir.join("out");
fs::create_dir_all(&work_dir)?;
eprintln!("Running tar extract");
if !Command::new("tar")
.arg("zxf")
.arg(&binary_input)
.arg("-C")
.arg(&work_dir)
.status()?
.success()
{
return Err(InstallError::ArchiveCorrupt(version.into(), binary_input));
}
let zip_output = binary_input.with_extension("zip");
eprintln!("Running zip");
if !Command::new("zip")
.current_dir(&work_dir)
.arg("-qyr")
.arg(&zip_output)
.arg(".")
.status()?
.success()
{
return Err(InstallError::ArchiveCorrupt(version.into(), zip_output));
}
fs::remove_file(binary_input)?;
fs::remove_dir_all(work_dir)?;
Ok(zip_output)
}
fn try_fs(f: impl FnOnce() -> io::Result<()>) -> io::Result<()> {
match f() {
Err(e) if e.kind() == ErrorKind::NotFound => Ok(()),
otherwise => otherwise,
}
}
}
mod list_command {
use super::shared::list_candidates;
use crate::commands::shared::request;
use std::{fmt::Write, io, path::Path};
pub(super) fn run(current: Option<&str>, candidates_dir: &Path) -> io::Result<String> {
let candidates = list_candidates(candidates_dir)?;
let mut output = String::with_capacity(1024);
output.push_str(
"--------------------------------------------------------------------------------\n",
);
if candidates.is_empty() && crate::use_color() {
let _ = writeln!(
&mut output,
" {}",
console::style("None installed!").yellow()
);
}
for candidate in candidates {
let candidate = candidate.into_name();
if matches!(current, Some(c) if c == candidate) {
let _ = writeln!(&mut output, " > {candidate}");
} else {
let _ = writeln!(&mut output, " * {candidate}");
}
}
output.push_str(
"--------------------------------------------------------------------------------\n",
);
output.push_str(
"* - installed \n",
);
output.push_str(
"> - currently in use \n",
);
output.push_str(
"--------------------------------------------------------------------------------\n",
);
Ok(output)
}
pub(super) fn run_online(
current: Option<&str>,
candidates_dir: &Path,
candidates_api: &str,
platform: &str,
verbose: bool,
) -> io::Result<String> {
let installed = list_candidates(candidates_dir)?;
let url = format!(
"{api}/candidates/java/{platform}/versions/list?current={current}&installed=${installed}",
api = candidates_api,
platform = platform,
current = current.unwrap_or_default(),
installed = installed.join(",")
);
request(url, verbose)
}
}
mod shared {
use crate::io_err;
use crate::{
prelude::Candidate,
select::{SelectOptions, Selection},
};
use std::{
ffi::OsStr,
fs::{self, DirEntry},
io,
path::Path,
};
pub(super) fn request(url: String, _verbose: bool) -> io::Result<String> {
static APP_USER_AGENT: &str =
concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));
let response = reqwest::blocking::Client::builder()
.user_agent(APP_USER_AGENT)
.connection_verbose(_verbose)
.build()
.map_err(io_err)?
.get(&url)
.send()
.map_err(io_err)?
.error_for_status()
.map_err(io_err)?
.text()
.map_err(io_err)?;
Ok(response)
}
pub(super) fn list_candidates(candidates_dir: &Path) -> io::Result<Vec<Candidate>> {
let mut candidates = fs::read_dir(candidates_dir)?
.filter_map(read_entry)
.collect::<Vec<_>>();
candidates.sort();
Ok(candidates)
}
pub(super) fn select_candidates(
query: Option<&OsStr>,
current: Option<&str>,
candidates_dir: &Path,
verbose: bool,
) -> io::Result<Selection<Candidate>> {
let candidates = list_candidates(candidates_dir)?;
let pre_select = current
.and_then(|current| candidates.iter().position(|c| c.name() == current))
.map(|p| p as u32);
let preview_command = format!(
"{0}/{{2}}/bin/java --version; {0}/{{2}}/bin/java -Xinternalversion",
candidates_dir.display()
);
let selected = SelectOptions::new(candidates)
.pre_select(pre_select)
.query(query)
.preview_command(preview_command.as_str())
.verbose(verbose)
.select()?;
Ok(selected)
}
fn read_entry(entry: io::Result<DirEntry>) -> Option<Candidate> {
let entry = entry.ok()?;
entry
.metadata()
.ok()?
.is_dir()
.then(|| Candidate::new(entry.path()))
}
}