From 943c9da841a88647d34a9974f78093bfbf06dbc4 Mon Sep 17 00:00:00 2001 From: TEC Date: Sat, 5 Oct 2024 12:22:23 +0800 Subject: [PATCH] Launch the manifest-specified Julia version When running `julia` with no extra arguments, and no explicit version, it is best to match the manifest version. This is done by implemented a limited form of the Julia executable's argument parsing and load path interpreting to determine the appropriate project to inspect, and then some light ad-hoc parsing of the manifest. We can then search the installed versions for a matching minor version, and run that. --- src/bin/julialauncher.rs | 215 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 203 insertions(+), 12 deletions(-) diff --git a/src/bin/julialauncher.rs b/src/bin/julialauncher.rs index 01975de7..2ad863f0 100644 --- a/src/bin/julialauncher.rs +++ b/src/bin/julialauncher.rs @@ -12,6 +12,8 @@ use nix::{ unistd::{fork, ForkResult}, }; use normpath::PathExt; +use semver::Version; +use std::io::BufRead; #[cfg(not(windows))] use std::os::unix::process::CommandExt; #[cfg(windows)] @@ -300,6 +302,181 @@ fn get_override_channel( } } +fn get_project(args: &Vec) -> Option { + let mut project_arg: Option = None; + for (_, arg) in args.iter().enumerate() { + if arg.starts_with("--project=") { + project_arg = Some(arg["--project=".len()..].to_string()); + // Note: You'd think this might work, but it's not actually supported. + // } else if arg == "--project" && i + 1 < args.len() { + // project_arg = Some(args[i + 1].clone()); + } else if arg == "--" { + break; + } + } + 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" { + let mut program_file: Option = None; + let no_arg_short_switches = vec!['v', 'h', 'i', 'q']; + let no_arg_long_switches = vec![ + "--version", + "--help", + "--help-hidden", + "--interactive", + "--quiet", + // Hidden options + "--lisp", + "--image-codegen", + "--rr-detach", + "--strip-metadata", + "--strip-ir", + "--permalloc-pkgimg", + "--heap-size-hint", + "--trim", + ]; + // `args` represents [switches...] -- [programfile] [programargs...] + // We want to find the first non-switch argument or the first argument after `--` + let mut skip_next = false; + for (i, arg) in args.iter().skip(1).enumerate() { + if skip_next { + skip_next = false; + } else if arg == "--" { + program_file = args.get(i + 1).cloned(); + 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(arg.clone()); + break; + } + } + if let Some(program_file) = program_file { + let mut path = PathBuf::from(program_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("Manifest.toml").exists() { + path.join("Manifest.toml") + } else if path.join("JuliaManifest.toml").exists() { + path.join("JuliaManifest.toml") + } else { + return None; + }; + let file = std::fs::File::open(manifest).ok()?; + let reader = std::io::BufReader::new(file); + // This is a somewhat bootleg way to parse the manifest, + // but since we know the format it should be fine. + for line in reader.lines() { + let line = line.ok()?; + if line.starts_with("julia_version = ") { + return Version::parse(line["julia_version = ".len()..].trim_matches('"')).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 @@ -329,6 +506,16 @@ fn run_app() -> Result { } } + let manifest_derived_julia_path = if channel_from_cmd_line.is_none() { + get_project(&args) + .and_then(julia_version_from_manifest) + .and_then(|ver| { + get_julia_path_for_version(&config_file.data, &paths.juliaupconfig, &ver).ok() + }) + } else { + None + }; + let (julia_channel_to_use, juliaup_channel_source) = if let Some(channel) = channel_from_cmd_line { (channel, JuliaupChannelSource::CmdLine) @@ -344,19 +531,23 @@ fn run_app() -> Result { )); }; - let (julia_path, julia_args) = get_julia_path_from_channel( - &versiondb_data, - &config_file.data, - &julia_channel_to_use, - &paths.juliaupconfig, - juliaup_channel_source, - ) - .with_context(|| { - format!( - "The Julia launcher failed to determine the command for the `{}` channel.", - julia_channel_to_use + let (julia_path, julia_args) = if let Some(path) = manifest_derived_julia_path { + (path, Vec::new()) + } else { + get_julia_path_from_channel( + &versiondb_data, + &config_file.data, + &julia_channel_to_use, + &paths.juliaupconfig, + juliaup_channel_source, ) - })?; + .with_context(|| { + format!( + "The Julia launcher failed to determine the command for the `{}` channel.", + julia_channel_to_use + ) + })? + }; let mut new_args: Vec = Vec::new();