Files
shacraft-launcher/src-tauri/src/java.rs
T
emil28092005 cc19a24e45 feat: install and launch Minecraft with NeoForge and Microsoft login
Wires up the actual game pipeline behind the existing ShaCraft manifest
sync: Mojang version resolution, Java 21 auto-provisioning via Adoptium,
headless NeoForge installation, real Microsoft/Xbox/Minecraft Services
login, and an offline account mode, then builds and spawns the java
process itself.

- mojang.rs: vanilla trust boundary, inheritsFrom version-JSON merge,
  asset/library downloading with a worker pool and per-file retries
- neoforge.rs: runs NeoForge's own installer headlessly, with live
  progress parsed from its output against its own install_profile.json
- runtime.rs / java.rs: detects a usable local Java or provisions one
  from Eclipse Temurin, with real download progress
- msa.rs: device-code OAuth -> Xbox Live -> XSTS -> Minecraft Services,
  gated on ShaCraft registering its own Azure AD app (see MSA_CLIENT_ID)
- session.rs / launch.rs: offline deterministic UUIDs and the merged
  java invocation itself
- download.rs: shared verified-download helper (temp file, hash,
  atomic rename, retries, progress) used across all of the above and
  refactored into profile.rs
- UI: account mode toggle (Microsoft/offline), login modal, and real
  per-stage install progress instead of start/done placeholders
2026-09-06 05:09:50 +03:00

127 lines
4.1 KiB
Rust

use crate::download::ProgressCallback;
use crate::runtime::{self, RuntimeError};
use reqwest::blocking::Client;
use serde::Serialize;
use std::{env, fmt, path::{Path, PathBuf}, process::Command};
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct JavaInstallation {
pub executable: String,
pub major: u8,
pub version: String,
}
#[derive(Debug)]
pub enum EnsureJavaError {
Provisioning(RuntimeError),
ProvisionedButUnrecognised(PathBuf),
}
impl fmt::Display for EnsureJavaError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Provisioning(error) => write!(formatter, "Cannot install a Java runtime: {error}"),
Self::ProvisionedButUnrecognised(path) => {
write!(formatter, "Installed a Java runtime at {path:?}, but it did not report a usable version")
}
}
}
}
/// Finds a usable Java runtime without modifying the machine.
///
/// The launcher will later use this result to decide whether Java 21 needs to
/// be bundled. We deliberately check JAVA_HOME first: it is explicit and makes
/// development installations predictable across all supported platforms.
pub fn detect() -> Option<JavaInstallation> {
candidates().into_iter().find_map(check_candidate)
}
/// Returns a Java runtime with at least `required_major`, preferring
/// whatever the user already has installed. Only downloads and extracts a
/// ShaCraft-managed Eclipse Temurin JRE under `runtime_root` (never touches
/// the user's own Java) when nothing suitable is already on the machine.
/// `on_progress` reports real download bytes when a JRE actually needs
/// fetching; it fires once with `(1, 1)` when an existing Java is reused.
pub fn ensure_java(client: &Client, runtime_root: &Path, required_major: u8, on_progress: &ProgressCallback) -> Result<JavaInstallation, EnsureJavaError> {
if let Some(installation) = detect() {
if installation.major >= required_major {
on_progress(1, 1);
return Ok(installation);
}
}
let executable = runtime::ensure_runtime(client, runtime_root, required_major, on_progress).map_err(EnsureJavaError::Provisioning)?;
check_candidate(executable.clone()).ok_or(EnsureJavaError::ProvisionedButUnrecognised(executable))
}
fn candidates() -> Vec<PathBuf> {
let executable = if cfg!(target_os = "windows") {
"java.exe"
} else {
"java"
};
let mut candidates = Vec::new();
if let Some(java_home) = env::var_os("JAVA_HOME") {
candidates.push(PathBuf::from(java_home).join("bin").join(executable));
}
candidates.push(PathBuf::from(executable));
candidates
}
fn check_candidate(candidate: PathBuf) -> Option<JavaInstallation> {
let output = Command::new(&candidate).arg("-version").output().ok()?;
if !output.status.success() {
return None;
}
let source = format!(
"{}\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
let version = parse_version(&source)?;
let major = parse_major(&version)?;
Some(JavaInstallation {
executable: candidate.display().to_string(),
major,
version,
})
}
fn parse_version(source: &str) -> Option<String> {
let marker = "version \"";
let start = source.find(marker)? + marker.len();
let remainder = &source[start..];
let end = remainder.find('"')?;
Some(remainder[..end].to_owned())
}
fn parse_major(version: &str) -> Option<u8> {
let mut segments = version.split('.');
let first = segments.next()?.parse::<u8>().ok()?;
if first == 1 {
segments.next()?.parse::<u8>().ok()
} else {
Some(first)
}
}
#[cfg(test)]
mod tests {
use super::{parse_major, parse_version};
#[test]
fn parses_modern_java_version() {
let output = "openjdk version \"21.0.8\" 2025-07-15";
assert_eq!(parse_version(output).as_deref(), Some("21.0.8"));
assert_eq!(parse_major("21.0.8"), Some(21));
}
#[test]
fn parses_legacy_java_version() {
assert_eq!(parse_major("1.8.0_452"), Some(8));
}
}