diff --git a/packages/oneclient_app/src/bridge/runtime.rs b/packages/oneclient_app/src/bridge/runtime.rs index 7729bd0c..595da137 100644 --- a/packages/oneclient_app/src/bridge/runtime.rs +++ b/packages/oneclient_app/src/bridge/runtime.rs @@ -432,11 +432,7 @@ impl CoreBridgeRuntime { ) .await { - Ok(runtime) => { - notifier.send_info( - "Java installed", - &format!("Java {} ({})", runtime.major, runtime.vendor), - ); + Ok(_) => { notifier.invalidate_java(); } Err(err) => { diff --git a/packages/oneclient_app/src/components/dropdown.rs b/packages/oneclient_app/src/components/dropdown.rs index ad16b2ec..a8cd3e7b 100644 --- a/packages/oneclient_app/src/components/dropdown.rs +++ b/packages/oneclient_app/src/components/dropdown.rs @@ -59,18 +59,50 @@ impl Component for Dropdown { let mut open = use_state(|| false); let mut hovering = use_state(|| false); + let mut button_area = use_state(|| None::); + let mut list_size = use_state(|| None::); + let selected = self.selected.clone(); let options = self.options.clone(); let on_select = self.on_select.clone(); - let list_width = self.width.clone(); let is_open = open(); + if !is_open && list_size().is_some() { + let _ = list_size.take(); + } + let trigger_bg = if hovering() { colors::ghost_overlay_hover() } else { colors::ghost_overlay() }; + let visible = options.len().min(MAX_VISIBLE_OPTIONS); + let list_h = visible as f32 * OPTION_HEIGHT + + visible.saturating_sub(1) as f32 * OPTION_SPACING; + + let offset_y = match (button_area(), list_size()) { + (Some(button), Some(list)) => { + let root_height = Platform::get().root_size.peek().height; + let space_below = root_height - button.max_y(); + let space_above = button.min_y(); + if list.height > space_below && list.height <= space_above { + -(button.height() + list.height + 8.) + } else { + 0. + } + } + _ => 0., + }; + + let list_width = button_area() + .map(|b| Size::px(b.width())) + .unwrap_or_else(|| self.width.clone()); + + let on_global_pointer_press = move |_: Event| { + open.set_if_modified(false); + }; + rect() .width(self.width.clone()) .child( @@ -85,6 +117,10 @@ impl Component for Dropdown { .background(trigger_bg) .on_pointer_enter(move |_| hovering.set(true)) .on_pointer_leave(move |_| hovering.set(false)) + .on_global_pointer_press(on_global_pointer_press) + .on_sized(move |e: Event| { + button_area.set_if_modified(Some(e.area)); + }) .on_press(move |e: Event| { e.prevent_default(); e.stop_propagation(); @@ -103,44 +139,41 @@ impl Component for Dropdown { ), ) .maybe_child(is_open.then(|| { - let visible = options.len().min(MAX_VISIBLE_OPTIONS); - let list_h = visible as f32 * OPTION_HEIGHT - + visible.saturating_sub(1) as f32 * OPTION_SPACING; - - rect().height(Size::px(0.)).width(list_width) - .layer(Layer::Overlay) - .child( - rect() - .layer(Layer::RelativeOverlay(10)) - .position(Position::new_global().top(0.).left(0.)) - .width(Size::window_percent(100.)) - .height(Size::window_percent(100.)) - .on_press(move |_| open.set(false)), - ) - .child( + rect().width(Size::px(0.)).height(Size::px(0.)).child( rect() - .width(Size::fill()) + .layer(Layer::Overlay) + .width(list_width) .margin(Gaps::new(4., 0., 0., 0.)) - .layer(Layer::RelativeOverlay(12)) - .padding(4.) - .corner_radius(CornerRadius::new_all(8.)) - .border(border_all_color(1., colors::component_border())) - .background(colors::page_elevated()) - .overflow(Overflow::Clip) + .offset_y(offset_y) + .content(Content::Fit) + .on_sized(move |e: Event| { + list_size.set_if_modified(Some(e.area.size)); + }) .child( - ScrollArea::new() - .height(Size::px(list_h)) - .spacing(OPTION_SPACING) - .children(options.into_iter().enumerate().map(|(idx, option)| { - let on_select = on_select.clone(); - DropdownOption::new(option, move |_| { - if let Some(handler) = &on_select { - handler.call(idx); - } - open.set(false); - }) - .into_element() - })), + rect() + .width(Size::fill()) + .padding(4.) + .corner_radius(CornerRadius::new_all(8.)) + .border(border_all_color(1., colors::component_border())) + .background(colors::page_elevated()) + .overflow(Overflow::Clip) + .child( + ScrollArea::new() + .height(Size::px(list_h)) + .spacing(OPTION_SPACING) + .children(options.into_iter().enumerate().map( + |(idx, option)| { + let on_select = on_select.clone(); + DropdownOption::new(option, move |_| { + if let Some(handler) = &on_select { + handler.call(idx); + } + open.set(false); + }) + .into_element() + }, + )), + ), ), ) })) diff --git a/packages/oneclient_app/src/components/scrollview.rs b/packages/oneclient_app/src/components/scrollview.rs index 8e054209..b33b207a 100644 --- a/packages/oneclient_app/src/components/scrollview.rs +++ b/packages/oneclient_app/src/components/scrollview.rs @@ -42,7 +42,7 @@ pub(crate) fn scrollbar_pos_and_size(inner: f32, viewport: f32, scroll: f32) -> let scroll_range = inner - viewport; let thumb_range = viewport - thumb; let normalized = -scroll / scroll_range; - + (normalized * thumb_range, thumb) } @@ -307,7 +307,17 @@ impl ScrollArea { let us_thumb = on_user_scroll.clone(); let us_h_thumb = on_user_scroll; + let can_scroll_v = vp_h > 0. && vp_h < ct_h; + let can_scroll_h = horizontal && vp_w > 0. && vp_w < content_w; + let on_wheel = move |e: Event| { + let wants_h = horizontal && *shift_held.read(); + + // check if scrollable + if (wants_h && !can_scroll_h) || !can_scroll_v { + return; + } + e.stop_propagation(); if (e.delta_y != 0.0 || e.delta_x != 0.0) @@ -316,7 +326,7 @@ impl ScrollArea { cb.call(()); } - if horizontal && *shift_held.read() { + if wants_h { let delta = if e.delta_x != 0.0 { e.delta_x } else { e.delta_y }; if delta != 0.0 { let cur_x = corrected_scroll(content_w, vp_w, *scroll_x.read()); diff --git a/packages/oneclient_app/src/view/app/settings/java.rs b/packages/oneclient_app/src/view/app/settings/java.rs index fe7277b0..5a9c3e63 100644 --- a/packages/oneclient_app/src/view/app/settings/java.rs +++ b/packages/oneclient_app/src/view/app/settings/java.rs @@ -15,6 +15,8 @@ fn providers() -> Vec<(JavaVendor, &'static str)> { vec![ (JavaVendor::Zulu, "Azul Zulu"), (JavaVendor::Adoptium, "Eclipse Temurin"), + (JavaVendor::Corretto, "Amazon Corretto"), + (JavaVendor::Liberica, "BellSoft Liberica"), ] } @@ -209,18 +211,31 @@ impl Component for RuntimeRow { .child( rect() .width(Size::flex(1.0)) + .overflow(Overflow::Clip) .child( - label() - .text(runtime.absolute_path.clone()) - .font_size(12.) - .max_lines(1) - .color(colors::fg_secondary()), + ScrollArea::new() + .horizontal(path_content_width(&runtime.absolute_path)) + .width(Size::fill()) + .height(Size::px(18.)) + .show_scrollbar(false) + .child( + label() + .text(runtime.absolute_path.clone()) + .font_size(12.) + .max_lines(1) + .color(colors::fg_secondary()) + .into_element(), + ), ), ) .child(remove_button(dispatch, path)) } } +fn path_content_width(path: &str) -> f32 { + (path.chars().count() as f32 * 7.0).max(1.0) +} + fn remove_button(dispatch: BridgeDispatch, path: String) -> impl IntoElement { Button::new() .ghost() diff --git a/packages/oneclient_core/examples/java_install_provider.rs b/packages/oneclient_core/examples/java_install_provider.rs index 9f30cbe6..49f82916 100644 --- a/packages/oneclient_core/examples/java_install_provider.rs +++ b/packages/oneclient_core/examples/java_install_provider.rs @@ -4,7 +4,9 @@ use std::env; use oneclient_core::dev; use oneclient_core::java::check_java_runtime; use oneclient_core::java::vendors::AdoptiumRuntimeProvider; +use oneclient_core::java::vendors::CorrettoRuntimeProvider; use oneclient_core::java::vendors::JavaRuntimeProvider; +use oneclient_core::java::vendors::LibericaRuntimeProvider; use oneclient_core::java::vendors::ZuluRuntimeProvider; use oneclient_core::logger; use oneclient_core::LauncherResult; @@ -18,7 +20,7 @@ async fn main() -> LauncherResult<()> { let vendor = args .next() .unwrap_or_else(|| { - eprintln!("usage: java_install_provider [major]"); + eprintln!("usage: java_install_provider [major]"); std::process::exit(1); }); @@ -33,13 +35,15 @@ async fn main() -> LauncherResult<()> { let provider: Box = match vendor.as_str() { "zulu" => Box::new(ZuluRuntimeProvider), "adoptium" => Box::new(AdoptiumRuntimeProvider), + "corretto" => Box::new(CorrettoRuntimeProvider), + "liberica" => Box::new(LibericaRuntimeProvider), other => { - eprintln!("unknown vendor '{other}', use zulu, adoptium, or corretto"); + eprintln!("unknown vendor '{other}', use zulu, adoptium, corretto, or liberica"); std::process::exit(1); } }; - let packages = provider.list_packages_by_major(major, &services).await?; + let packages = provider.list_packages(Some(major), &services).await?; let package = packages .into_iter() .find(|p| p.java_version.contains(&major)) diff --git a/packages/oneclient_core/examples/java_list_providers.rs b/packages/oneclient_core/examples/java_list_providers.rs index d10f6a6a..669c0d2d 100644 --- a/packages/oneclient_core/examples/java_list_providers.rs +++ b/packages/oneclient_core/examples/java_list_providers.rs @@ -10,19 +10,25 @@ use oneclient_core::logger; async fn main() -> LauncherResult<()> { logger::init_debug()?; - let major: u32 = env::args() - .nth(1) - .unwrap_or_else(|| "21".to_string()) - .parse() - .expect("usage: java_list_providers [major_version]"); + let arg = env::args().nth(1).unwrap_or_else(|| "21".to_string()); + let major: Option = if arg == "all" { + None + } else { + Some(arg.parse().expect("usage: java_list_providers [major|all]")) + }; let services = dev::ephemeral_services().await?; - println!("Listing JRE packages for Java {major} (metadata only):\n"); + match major { + Some(major) => println!("Listing JRE packages for Java {major} (metadata only):\n"), + None => println!("Listing JRE packages for all majors (metadata only):\n"), + } for provider in runtime_providers() { - let packages = provider.list_packages_by_major(major, &services).await?; - print_vendor(provider.vendor(), &packages); + match provider.list_packages(major, &services).await { + Ok(packages) => print_vendor(provider.vendor(), &packages), + Err(err) => println!("{:?}: ERROR {err}", provider.vendor()), + } } Ok(()) diff --git a/packages/oneclient_core/src/http/response.rs b/packages/oneclient_core/src/http/response.rs index 65a23477..99d7b993 100644 --- a/packages/oneclient_core/src/http/response.rs +++ b/packages/oneclient_core/src/http/response.rs @@ -13,6 +13,8 @@ pub struct ResponseOptions { pub struct ResponseNotifyOptions { child: Option, standalone_label: Option, + standalone_id: Option, + done_label: Option, } impl ResponseNotifyOptions { @@ -20,6 +22,8 @@ impl ResponseNotifyOptions { Self { child: Some(child), standalone_label: None, + standalone_id: None, + done_label: None, } } @@ -27,8 +31,20 @@ impl ResponseNotifyOptions { Self { child: None, standalone_label: Some(label.into()), + standalone_id: None, + done_label: None, } } + + pub fn with_id(mut self, id: Uuid) -> Self { + self.standalone_id = Some(id); + self + } + + pub fn done_label(mut self, label: impl Into) -> Self { + self.done_label = Some(label.into()); + self + } } #[async_trait::async_trait] @@ -59,7 +75,14 @@ impl ResponseExt for Response { .notify .as_ref() .and_then(|n| n.standalone_label.clone()); - let standalone_id = standalone_label.as_ref().map(|_| Uuid::new_v4()); + let done_label = options + .notify + .as_ref() + .and_then(|n| n.done_label.clone()); + + let standalone_id = standalone_label + .as_ref() + .map(|_| options.notify.as_ref().and_then(|n| n.standalone_id).unwrap_or_else(Uuid::new_v4)); if let Some(ref child) = grouped_child { child.set_progress(0, Some(total)); @@ -67,6 +90,7 @@ impl ResponseExt for Response { notifier.send_progress(id, label, 0, total); } + let notifier = notifier.clone(); let stream = futures_lite::StreamExt::map(self.bytes_stream(), move |item| { match item { Ok(chunk) => { @@ -75,6 +99,12 @@ impl ResponseExt for Response { if let Some(ref child) = grouped_child { child.set_progress(current, Some(total)); } else if let (Some(id), Some(label)) = (&standalone_id, &standalone_label) { + let done = current >= total; + let label = if done { + done_label.as_deref().unwrap_or(label) + } else { + label + }; notifier.send_progress(id, label, current, total); } diff --git a/packages/oneclient_core/src/java/install.rs b/packages/oneclient_core/src/java/install.rs index 65bf5475..6673b5e6 100644 --- a/packages/oneclient_core/src/java/install.rs +++ b/packages/oneclient_core/src/java/install.rs @@ -22,16 +22,19 @@ pub async fn install_package( let archive_path = java_dir.join(polyio::sanitize_path(&package.name)); + let major = package.java_version.first().copied().unwrap_or(0); services.requester .download_file( reqwest::Request::new(Method::GET, Url::parse(&package.download_url)?), &archive_path, ResponseOptions { - notify: Some(ResponseNotifyOptions::standalone(format!( - "Installing {} {}", - package.vendor, - package.java_version.first().copied().unwrap_or(0) - ))), + notify: Some( + ResponseNotifyOptions::standalone(format!( + "Installing {} {}", + package.vendor, major + )) + .done_label(format!("Installed {} {}", package.vendor, major)), + ), }, services, ) diff --git a/packages/oneclient_core/src/java/manager.rs b/packages/oneclient_core/src/java/manager.rs index 7e6271cd..fb957867 100644 --- a/packages/oneclient_core/src/java/manager.rs +++ b/packages/oneclient_core/src/java/manager.rs @@ -1,3 +1,4 @@ +use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use std::sync::Arc; @@ -92,23 +93,23 @@ impl JavaManager { return Ok(Vec::new()); }; - let results = futures_util::future::join_all(INSTALLABLE_MAJORS.iter().map(|&major| { - let provider = &provider; - async move { - (major, provider.latest_package_by_major(major, services).await) - } - })) - .await; + let packages = provider.list_packages(None, services).await?; - let mut available = Vec::new(); - for (major, result) in results { - match result { - Ok(Some(package)) => available.push(AvailableJava { major, package }), - Ok(None) => {} - Err(err) => tracing::warn!(?vendor, major, "failed to query packages: {err}"), - } + let mut by_major = + BTreeMap::::new(); + + for package in packages { + let Some(&major) = package.java_version.first() else { + continue; + }; + by_major.entry(major).or_insert(package); } + let mut available: Vec = by_major + .into_iter() + .map(|(major, package)| AvailableJava { major, package }) + .collect(); + available.sort_by_key(|b| std::cmp::Reverse(b.major)); Ok(available) @@ -171,10 +172,10 @@ async fn download_and_register( for provider in vendors::runtime_providers() { let vendor = provider.vendor(); - let packages = provider.list_packages_by_major(major, services).await; + let packages = provider.list_packages(Some(major), services).await; let package = match &packages { - Ok(packages) => match packages.first() { + Ok(packages) => match packages.iter().find(|p| p.java_version.first() == Some(&major)).or_else(|| packages.first()) { Some(package) => package, None => { tracing::warn!(?vendor, major, "no packages found"); diff --git a/packages/oneclient_core/src/java/vendors/adoptium.rs b/packages/oneclient_core/src/java/vendors/adoptium.rs index 8b94f85e..ed02a4e9 100644 --- a/packages/oneclient_core/src/java/vendors/adoptium.rs +++ b/packages/oneclient_core/src/java/vendors/adoptium.rs @@ -10,6 +10,12 @@ use crate::state::LauncherServices; pub struct AdoptiumRuntimeProvider; +// `/v3/info/available_releases` +#[derive(Debug, Deserialize)] +struct AdoptiumAvailableReleases { + available_releases: Vec, +} + #[derive(Debug, Deserialize)] struct AdoptiumRelease { version_data: AdoptiumVersionData, @@ -19,6 +25,7 @@ struct AdoptiumRelease { #[derive(Debug, Deserialize)] struct AdoptiumVersionData { semver: String, + major: u32, } #[derive(Debug, Deserialize)] @@ -38,30 +45,50 @@ impl JavaRuntimeProvider for AdoptiumRuntimeProvider { JavaVendor::Adoptium } - async fn list_packages_by_major( + async fn list_packages( &self, - major: u32, + major: Option, services: &LauncherServices, ) -> LauncherResult> { - let url = adoptium_url(major)?; + // fetched from `/v3/info/available_releases`. + let (low, high) = match major { + Some(major) => (major, major), + None => { + let info = services + .requester + .send_as::(Request::new( + Method::GET, + Url::parse("https://api.adoptium.net/v3/info/available_releases")?, + )) + .await?; + let low = info.available_releases.iter().copied().min().unwrap_or(8); + let high = info.available_releases.iter().copied().max().unwrap_or(low); + (low, high) + } + }; + + let url = adoptium_url(low, high)?; let releases = services .requester .send_as::>(Request::new(Method::GET, url)) .await?; let mut packages = Vec::new(); - + for release in releases { - let java_version: Vec = release + let mut java_version: Vec = release .version_data .semver .split(|c: char| !c.is_numeric()) .filter_map(|s| s.parse::().ok()) .collect(); + if java_version.first() != Some(&release.version_data.major) { + java_version.insert(0, release.version_data.major); + } for binary in release.binaries { packages.push(JavaPackage { - archive: PackageArchive::Zip, + archive: PackageArchive::from_filename(&binary.package.name), download_url: binary.package.link, java_version: java_version.clone(), name: binary.package.name, @@ -74,10 +101,29 @@ impl JavaRuntimeProvider for AdoptiumRuntimeProvider { } } -fn adoptium_url(major: u32) -> LauncherResult { - Ok(Url::parse(&format!( - "https://api.adoptium.net/v3/assets/feature_releases/{major}/ga?os={ADOPTIUM_OS}&architecture={ADOPTIUM_ARCH}&image_type=jre&jvm_impl=hotspot&project=jdk&heap_size=normal&vendor=eclipse&page=0&page_size=5" - ))?) +// One request spanning every GA major via a version range (`[low,high]`), +// newest build per major, instead of one request per major. +fn adoptium_url(low: u32, high: u32) -> LauncherResult { + let range = format!("%5B{low}%2C{}%5D", high + 1); + + let mut url = Url::parse(&format!( + "https://api.adoptium.net/v3/assets/version/{range}" + ))?; + + url.query_pairs_mut() + .append_pair("os", ADOPTIUM_OS) + .append_pair("architecture", ADOPTIUM_ARCH) + .append_pair("image_type", "jre") + .append_pair("jvm_impl", "hotspot") + .append_pair("project", "jdk") + .append_pair("heap_size", "normal") + .append_pair("vendor", "eclipse") + .append_pair("release_type", "ga") + .append_pair("sort_method", "DATE") + .append_pair("sort_order", "DESC") + .append_pair("page", "0") + .append_pair("page_size", "50"); + Ok(url) } const ADOPTIUM_ARCH: &str = cfg_select! { @@ -89,7 +135,7 @@ const ADOPTIUM_ARCH: &str = cfg_select! { const ADOPTIUM_OS: &str = cfg_select! { target_os = "windows" => "windows", - target_os = "macos" => "mac", + target_os = "macos" => "mac", target_os = "linux" => cfg_select! { target_env = "musl" => "alpine-linux", _ => "linux", diff --git a/packages/oneclient_core/src/java/vendors/corretto.rs b/packages/oneclient_core/src/java/vendors/corretto.rs new file mode 100644 index 00000000..26aba9c8 --- /dev/null +++ b/packages/oneclient_core/src/java/vendors/corretto.rs @@ -0,0 +1,103 @@ +use reqwest::{Method, Request}; +use serde_json::Value; +use url::Url; + +use crate::LauncherResult; +use crate::java::PackageArchive; +use crate::java::data::JavaPackage; +use crate::java::vendors::{JavaRuntimeProvider, JavaVendor}; +use crate::state::LauncherServices; + +pub struct CorrettoRuntimeProvider; + +// { os: { arch: { "jdk": { major: { ext: ... } } } } } +const INDEX_URL: &str = + "https://corretto.github.io/corretto-downloads/latest_links/indexmap_with_checksum.json"; + +#[async_trait::async_trait] +impl JavaRuntimeProvider for CorrettoRuntimeProvider { + fn vendor(&self) -> JavaVendor { + JavaVendor::Corretto + } + + async fn list_packages( + &self, + major: Option, + services: &LauncherServices, + ) -> LauncherResult> { + let index = services + .requester + .send_as::(Request::new(Method::GET, Url::parse(INDEX_URL)?)) + .await?; + + // Corretto ships JDK only + let Some(by_major) = index + .get(CORRETTO_OS) + .and_then(|by_arch| by_arch.get(CORRETTO_ARCH)) + .and_then(|by_type| by_type.get("jdk")) + .and_then(Value::as_object) + else { + return Ok(Vec::new()); + }; + + let mut packages = Vec::new(); + for (major_str, by_ext) in by_major { + let Ok(this_major) = major_str.parse::() else { + continue; + }; + if let Some(filter) = major + && filter != this_major + { + continue; + } + + let has_ext = by_ext + .as_object() + .is_some_and(|exts| exts.contains_key(CORRETTO_EXT.0)); + if !has_ext { + continue; + } + + let download_url = latest_url(this_major, CORRETTO_EXT.0); + let name = format!( + "amazon-corretto-{this_major}-{CORRETTO_ARCH}-{CORRETTO_OS}-jdk.{}", + CORRETTO_EXT.0 + ); + + packages.push(JavaPackage { + archive: CORRETTO_EXT.1, + download_url, + java_version: vec![this_major], + name, + vendor: JavaVendor::Corretto, + }); + } + + packages.sort_by_key(|p| std::cmp::Reverse(p.java_version.first().copied().unwrap_or(0))); + Ok(packages) + } +} + +fn latest_url(major: u32, ext: &str) -> String { + format!( + "https://corretto.aws/downloads/latest/amazon-corretto-{major}-{CORRETTO_ARCH}-{CORRETTO_OS}-jdk.{ext}" + ) +} + +const CORRETTO_ARCH: &str = cfg_select! { + target_arch = "x86" => "x86", + target_arch = "x86_64" => "x64", + target_arch = "arm" => "arm", + target_arch = "aarch64" => "aarch64", +}; + +const CORRETTO_OS: &str = cfg_select! { + target_os = "windows" => "windows", + target_os = "macos" => "macos", + target_os = "linux" => "linux", +}; + +const CORRETTO_EXT: (&str, PackageArchive) = cfg_select! { + target_os = "windows" => ("zip", PackageArchive::Zip), + _ => ("tar.gz", PackageArchive::TarGz), +}; diff --git a/packages/oneclient_core/src/java/vendors/liberica.rs b/packages/oneclient_core/src/java/vendors/liberica.rs new file mode 100644 index 00000000..8f5b1c5e --- /dev/null +++ b/packages/oneclient_core/src/java/vendors/liberica.rs @@ -0,0 +1,123 @@ +use reqwest::{Method, Request}; +use serde::Deserialize; +use url::Url; + +use crate::LauncherResult; +use crate::java::PackageArchive; +use crate::java::data::JavaPackage; +use crate::java::vendors::{JavaRuntimeProvider, JavaVendor}; +use crate::state::LauncherServices; + +pub struct LibericaRuntimeProvider; + +#[derive(Debug, Deserialize)] +struct LibericaRelease { + #[serde(rename = "downloadUrl")] + download_url: String, + filename: String, + #[serde(rename = "featureVersion")] + feature_version: u32, + #[serde(rename = "updateVersion", default)] + update_version: u32, + #[serde(rename = "buildVersion", default)] + build_version: u32, + version: String, +} + +#[async_trait::async_trait] +impl JavaRuntimeProvider for LibericaRuntimeProvider { + fn vendor(&self) -> JavaVendor { + JavaVendor::Liberica + } + + async fn list_packages( + &self, + major: Option, + services: &LauncherServices, + ) -> LauncherResult> { + let url = liberica_url(major)?; + let releases = services + .requester + .send_as::>(Request::new(Method::GET, url)) + .await?; + + let mut releases = releases; + releases.sort_by_key(|r| { + std::cmp::Reverse((r.feature_version, r.update_version, r.build_version)) + }); + + let packages: Vec = releases + .into_iter() + .map(|r| { + let mut java_version: Vec = r + .version + .split(|c: char| !c.is_numeric()) + .filter_map(|s| s.parse::().ok()) + .collect(); + if java_version.first() != Some(&r.feature_version) { + java_version.insert(0, r.feature_version); + } + + JavaPackage { + archive: PackageArchive::from_filename(&r.filename), + download_url: r.download_url, + java_version, + name: r.filename, + vendor: JavaVendor::Liberica, + } + }) + .collect(); + + Ok(packages) + } +} + +fn liberica_url(major: Option) -> LauncherResult { + let mut url = Url::parse("https://api.bell-sw.com/v1/liberica/releases")?; + { + let mut q = url.query_pairs_mut(); + q.append_pair("os", LIBERICA_OS) + .append_pair("arch", LIBERICA_ARCH) + .append_pair("bundle-type", "jre") + .append_pair("bitness", LIBERICA_BITNESS) + .append_pair("package-type", LIBERICA_PACKAGE) + .append_pair("installation-type", "archive") + .append_pair( + "fields", + "downloadUrl,filename,featureVersion,updateVersion,buildVersion,version", + ) + .append_pair("output", "json"); + if let Some(major) = major { + q.append_pair("version-feature", &major.to_string()); + } + } + Ok(url) +} + +const LIBERICA_ARCH: &str = cfg_select! { + target_arch = "x86" => "x86", + target_arch = "x86_64" => "x86", + target_arch = "arm" => "arm", + target_arch = "aarch64" => "arm", +}; + +const LIBERICA_BITNESS: &str = cfg_select! { + target_arch = "x86" => "32", + target_arch = "x86_64" => "64", + target_arch = "arm" => "32", + target_arch = "aarch64" => "64", +}; + +const LIBERICA_OS: &str = cfg_select! { + target_os = "windows" => "windows", + target_os = "macos" => "macos", + target_os = "linux" => cfg_select! { + target_env = "musl" => "linux-musl", + _ => "linux" + }, +}; + +const LIBERICA_PACKAGE: &str = cfg_select! { + target_os = "windows" => "zip", + _ => "tar.gz", +}; diff --git a/packages/oneclient_core/src/java/vendors/mod.rs b/packages/oneclient_core/src/java/vendors/mod.rs index 49d8d171..6c21fbab 100644 --- a/packages/oneclient_core/src/java/vendors/mod.rs +++ b/packages/oneclient_core/src/java/vendors/mod.rs @@ -5,9 +5,13 @@ use crate::state::LauncherServices; use crate::LauncherResult; mod adoptium; +mod corretto; +mod liberica; mod zulu; pub use adoptium::AdoptiumRuntimeProvider; +pub use corretto::CorrettoRuntimeProvider; +pub use liberica::LibericaRuntimeProvider; use serde::{Deserialize, Deserializer, Serialize}; pub use zulu::ZuluRuntimeProvider; @@ -15,6 +19,8 @@ pub fn runtime_providers() -> Vec> { vec![ Box::new(ZuluRuntimeProvider), Box::new(AdoptiumRuntimeProvider), + Box::new(CorrettoRuntimeProvider), + Box::new(LibericaRuntimeProvider), ] } @@ -22,9 +28,9 @@ pub fn runtime_providers() -> Vec> { pub trait JavaRuntimeProvider: Send + Sync { fn vendor(&self) -> JavaVendor; - async fn list_packages_by_major( + async fn list_packages( &self, - major: u32, + major: Option, services: &LauncherServices, ) -> LauncherResult>; @@ -33,8 +39,8 @@ pub trait JavaRuntimeProvider: Send + Sync { major: u32, services: &LauncherServices ) -> LauncherResult> { - let packages = self.list_packages_by_major(major, services).await?; - Ok(packages.first().cloned()) + let packages = self.list_packages(Some(major), services).await?; + Ok(packages.into_iter().find(|p| p.java_version.first() == Some(&major))) } async fn install_package( @@ -53,6 +59,7 @@ pub enum JavaVendor { Oracle, Microsoft, Adoptium, + Liberica, OpenJDK, Other(String), } @@ -62,6 +69,7 @@ impl std::fmt::Display for JavaVendor { match self { JavaVendor::Zulu => f.write_str("Zulu"), JavaVendor::Adoptium => f.write_str("Temurin"), + JavaVendor::Liberica => f.write_str("Liberica"), JavaVendor::Corretto => f.write_str("Corretto"), JavaVendor::Microsoft => f.write_str("Microsoft"), JavaVendor::Oracle => f.write_str("Oracle"), @@ -77,8 +85,10 @@ impl FromStr for JavaVendor { fn from_str(s: &str) -> Result { let v = s.to_lowercase(); - Ok(if v.contains("adoptium") || v.contains("adoptopenjdk") { + Ok(if v.contains("adoptium") || v.contains("adoptopenjdk") || v.contains("temurin") { Self::Adoptium + } else if v.contains("liberica") || v.contains("bellsoft") { + Self::Liberica } else if v.contains("microsoft") { Self::Microsoft } else if v.contains("openjdk") { diff --git a/packages/oneclient_core/src/java/vendors/zulu.rs b/packages/oneclient_core/src/java/vendors/zulu.rs index e0699fe9..5fb4504d 100644 --- a/packages/oneclient_core/src/java/vendors/zulu.rs +++ b/packages/oneclient_core/src/java/vendors/zulu.rs @@ -23,7 +23,7 @@ impl JavaRuntimeProvider for ZuluRuntimeProvider { JavaVendor::Zulu } - async fn list_packages_by_major(&self, major: u32, services: &LauncherServices) -> LauncherResult> { + async fn list_packages(&self, major: Option, services: &LauncherServices) -> LauncherResult> { let url = zulu_url(major)?; let packages = services.requester.send_as::>(Request::new(Method::GET, url)).await?; let packages = packages.into_iter().map(map_zulu_package).collect(); @@ -42,8 +42,26 @@ fn map_zulu_package(pkg: ZuluPackage) -> JavaPackage { } } -fn zulu_url(major: u32) -> LauncherResult { - Ok(Url::parse(&format!("https://api.azul.com/metadata/v1/zulu/packages/?java_version={major}&os={ZULU_OS}&arch={ZULU_ARCH}&archive_type=zip&java_package_type=jre&javafx_bundled=false&release_status=ga&availability_types=CA&certifications=tck&page=1&page_size=5"))?) +fn zulu_url(major: Option) -> LauncherResult { + let mut url = Url::parse("https://api.azul.com/metadata/v1/zulu/packages/")?; + { + let mut q = url.query_pairs_mut(); + q.append_pair("os", ZULU_OS) + .append_pair("arch", ZULU_ARCH) + .append_pair("archive_type", "zip") + .append_pair("java_package_type", "jre") + .append_pair("javafx_bundled", "false") + .append_pair("release_status", "ga") + .append_pair("availability_types", "CA") + .append_pair("certifications", "tck") + .append_pair("latest", "true") + .append_pair("page", "1") + .append_pair("page_size", "100"); + if let Some(major) = major { + q.append_pair("java_version", &major.to_string()); + } + } + Ok(url) } const ZULU_ARCH: &str = cfg_select! {