diff --git a/Cargo.lock b/Cargo.lock index d3a3af5d..978a75f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1031,6 +1031,7 @@ dependencies = [ "tar", "tempfile", "thiserror", + "toml 0.8.19", "url", "windows", "winres", @@ -1977,6 +1978,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", + "winnow", ] [[package]] @@ -2449,6 +2451,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +dependencies = [ + "memchr", +] + [[package]] name = "winres" version = "0.1.12" diff --git a/Cargo.toml b/Cargo.toml index 06694f56..4d23a364 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,6 +60,7 @@ is-terminal = "0.4" path-absolutize = "3.1.0" human-sort = "0.2.2" regex = "1.10" +toml = "0.8.19" [target.'cfg(windows)'.dependencies] windows = { version = "0.58.0", features = ["Win32_Foundation", "Win32_UI_Shell", "Win32_Security", "Win32_System_JobObjects", "Win32_System_Console", "Win32_System_Threading", "Services_Store", "Foundation", "Foundation_Collections", "Web_Http", "Web_Http_Headers", "Storage_Streams", "Management_Deployment"] } diff --git a/src/bin/julialauncher.rs b/src/bin/julialauncher.rs index 6a63b8c5..7eb4253d 100644 --- a/src/bin/julialauncher.rs +++ b/src/bin/julialauncher.rs @@ -12,12 +12,14 @@ use nix::{ unistd::{fork, ForkResult}, }; use normpath::PathExt; +use semver::Version; #[cfg(not(windows))] use std::os::unix::process::CommandExt; #[cfg(windows)] use std::os::windows::io::{AsRawHandle, RawHandle}; use std::path::Path; use std::path::PathBuf; +use toml::Value; #[cfg(windows)] use windows::Win32::System::{ JobObjects::{AssignProcessToJobObject, SetInformationJobObject}, @@ -160,10 +162,12 @@ fn check_channel_uptodate( Ok(()) } +#[derive(PartialEq, Eq)] enum JuliaupChannelSource { CmdLine, EnvVar, Override, + Manifest, Default, } @@ -174,6 +178,12 @@ fn get_julia_path_from_channel( juliaupconfig_path: &Path, juliaup_channel_source: JuliaupChannelSource, ) -> Result<(PathBuf, Vec)> { + if juliaup_channel_source == JuliaupChannelSource::Manifest { + let path = + get_julia_path_for_version(config_data, juliaupconfig_path, &Version::parse(channel)?)?; + return Ok((path, Vec::new())); + } + let channel_info = config_data .installed_channels .get(channel) @@ -199,6 +209,7 @@ fn get_julia_path_from_channel( UserError { msg: format!("ERROR: Invalid Juliaup channel `{}` in directory override. Please run `juliaup list` to get a list of valid channels and versions.", channel) } } }.into(), + JuliaupChannelSource::Manifest => unreachable!(), JuliaupChannelSource::Default => anyhow!("The Juliaup configuration is in an inconsistent state, the currently configured default channel `{}` is not installed.", channel) })?; @@ -300,6 +311,189 @@ fn get_override_channel( } } +fn get_program_file(args: &Vec) -> Option<(usize, &String)> { + let mut program_file: Option<(usize, &String)> = None; + let no_arg_short_switches = ['v', 'h', 'i', 'q']; + let no_arg_long_switches = [ + "--version", + "--help", + "--help-hidden", + "--interactive", + "--quiet", + // Hidden options + "--lisp", + "--image-codegen", + "--rr-detach", + "--strip-metadata", + "--strip-ir", + "--permalloc-pkgimg", + "--heap-size-hint", + "--trim", + ]; + let mut skip_next = false; + for (i, arg) in args.iter().skip(1).enumerate() { + if skip_next { + skip_next = false; + } else if arg == "--" { + if i + 1 < args.len() { + program_file = Some((i + 1, args.get(i + 1).unwrap())); + } + break; + } else if arg.starts_with("--") { + if !no_arg_long_switches.contains(&arg.as_str()) && !arg.contains('=') { + skip_next = true; + } + } else if arg.starts_with("-") { + let arg: Vec = arg.chars().skip(1).collect(); + if arg.iter().all(|&c| no_arg_short_switches.contains(&c)) { + continue; + } + for (j, &c) in arg.iter().enumerate() { + if no_arg_short_switches.contains(&c) { + continue; + } else if j < arg.len() - 1 { + break; + } else { + // `j == arg.len() - 1` + skip_next = true; + } + } + } else { + program_file = Some((i, arg)); + break; + } + } + return program_file; +} + +fn get_project(args: &Vec) -> Option { + let program_file = get_program_file(args); + let recognised_proj_flags: [&str; 4] = ["--project", "--projec", "--proje", "--proj"]; + let mut project_arg: Option = None; + for arg in args + .iter() + .take(program_file.map_or(args.len(), |(i, _)| i)) + { + if arg.starts_with("--proj") { + let mut parts = arg.splitn(2, '='); + if recognised_proj_flags.contains(&parts.next().unwrap_or("")) { + project_arg = Some(parts.next().unwrap_or("@").to_string()); + } + } + } + let project = if project_arg.is_some() { + project_arg.unwrap() + } else if let Ok(val) = std::env::var("JULIA_PROJECT") { + val + } else { + return None; + }; + if project == "@" { + return None; + } else if project == "@." || project == "" { + let mut path = PathBuf::from(std::env::current_dir().unwrap()); + while !path.join("Project.toml").exists() && !path.join("JuliaProject.toml").exists() { + if !path.pop() { + return None; + } + } + return Some(path); + } else if project == "@script" { + if let Some((_, file)) = program_file { + let mut path = PathBuf::from(file); + path.pop(); + while !path.join("Project.toml").exists() && !path.join("JuliaProject.toml").exists() { + if !path.pop() { + return None; + } + } + return Some(path); + } else { + return None; + } + } else if project.starts_with('@') { + let depot = match std::env::var("JULIA_DEPOT_PATH") { + Ok(val) => match val.split(':').next() { + Some(p) => PathBuf::from(p), + None => dirs::home_dir().unwrap().join(".julia"), + }, + _ => dirs::home_dir().unwrap().join(".julia"), + }; + let path = depot.join("environments").join(&project[1..]); + if path.exists() { + return Some(path); + } else { + return None; + } + } else { + return Some(PathBuf::from(project)); + } +} + +fn julia_version_from_manifest(path: PathBuf) -> Option { + let manifest = if path.join("JuliaManifest.toml").exists() { + path.join("JuliaManifest.toml") + } else if path.join("Manifest.toml").exists() { + path.join("Manifest.toml") + } else { + return None; + }; + let content = std::fs::read_to_string(manifest) + .ok()? + .parse::() + .ok()?; + if let Some(manifest_format) = content.get("manifest_format") { + if manifest_format.as_str()?.starts_with("2.") { + if let Some(julia_version) = content.get("julia_version") { + return julia_version.as_str().and_then(|v| Version::parse(v).ok()); + } + } + } + return None; +} + +fn get_julia_path_for_version( + config_data: &JuliaupConfig, + juliaupconfig_path: &Path, + version: &Version, +) -> Result { + let mut best_match: Option<(&String, Version)> = None; + for (installed_version_str, path) in &config_data.installed_versions { + if let Ok(installed_semver) = Version::parse(installed_version_str) { + if installed_semver.major != version.major || installed_semver.minor != version.minor { + continue; + } + if let Some((_, ref best_version)) = best_match { + if installed_semver > *best_version { + best_match = Some((&path.path, installed_semver)); + } + } else { + best_match = Some((&path.path, installed_semver)); + } + } + } + if let Some((path, _)) = best_match { + let absolute_path = juliaupconfig_path + .parent() + .unwrap() + .join(path) + .join("bin") + .join(format!("julia{}", std::env::consts::EXE_SUFFIX)) + .normalize() + .with_context(|| { + format!( + "Failed to normalize path for Julia binary, starting from `{}`.", + juliaupconfig_path.display() + ) + })?; + return Ok(absolute_path.into_path_buf()); + } else { + return Err(anyhow!( + "No installed version of Julia matches the requested version." + )); + } +} + fn run_app() -> Result { if std::io::stdout().is_terminal() { // Set console title @@ -336,6 +530,8 @@ fn run_app() -> Result { (channel, JuliaupChannelSource::EnvVar) } else if let Ok(Some(channel)) = get_override_channel(&config_file) { (channel, JuliaupChannelSource::Override) + } else if let Some(version) = get_project(&args).and_then(julia_version_from_manifest) { + (version.to_string(), JuliaupChannelSource::Manifest) } else if let Some(channel) = config_file.data.default.clone() { (channel, JuliaupChannelSource::Default) } else {