Skip to content

Commit

Permalink
Launch the manifest-specified Julia version
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
tecosaur committed Oct 5, 2024
1 parent 463b45b commit 943c9da
Showing 1 changed file with 203 additions and 12 deletions.
215 changes: 203 additions & 12 deletions src/bin/julialauncher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -300,6 +302,181 @@ fn get_override_channel(
}
}

fn get_project(args: &Vec<String>) -> Option<PathBuf> {
let mut project_arg: Option<String> = 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<String> = 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<char> = 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<Version> {
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<PathBuf> {
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<i32> {
if std::io::stdout().is_terminal() {
// Set console title
Expand Down Expand Up @@ -329,6 +506,16 @@ fn run_app() -> Result<i32> {
}
}

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)
Expand All @@ -344,19 +531,23 @@ fn run_app() -> Result<i32> {
));
};

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<String> = Vec::new();

Expand Down

0 comments on commit 943c9da

Please sign in to comment.