use anyhow::{anyhow, Context, Result};
use semver::Version;
use std::ffi::OsStr;
use std::fs;
use std::path::{Path, PathBuf};
use toml::Value;
use crate::jsonstructs_versionsdb::JuliaupVersionDB;
use crate::utils::{print_juliaup_style, JuliaupMessageType};
const PROJECT_NAMES: &[&str] = &["JuliaProject.toml", "Project.toml"];
const MANIFEST_NAMES: &[&str] = &["JuliaManifest.toml", "Manifest.toml"];
#[cfg(windows)]
pub const LOAD_PATH_SEPARATOR: &str = ";";
#[cfg(not(windows))]
pub const LOAD_PATH_SEPARATOR: &str = ":";
fn find_named_file(dir: &Path, candidates: &[&str]) -> Option<PathBuf> {
candidates
.iter()
.map(|file| dir.join(file))
.find(|path| path.is_file())
}
fn find_project_file_in_dir(dir: &Path) -> Option<PathBuf> {
find_named_file(dir, PROJECT_NAMES)
}
fn resolve_depot_paths(depot_path: Option<&OsStr>) -> Result<Vec<PathBuf>> {
if let Some(paths) = depot_path {
let candidates: Vec<_> = std::env::split_paths(paths).collect();
if !candidates.is_empty() {
return Ok(candidates);
}
}
let home = dirs::home_dir()
.ok_or_else(|| anyhow!("Could not determine the path of the user home directory."))?;
Ok(vec![home.join(".julia")])
}
fn find_named_environment(depot_paths: &[PathBuf], env_name: &str) -> Option<PathBuf> {
depot_paths.iter().find_map(|depot| {
let env_dir = depot.join("environments").join(env_name);
if env_dir.is_dir() {
find_project_file_in_dir(&env_dir)
} else {
None
}
})
}
fn default_named_environment_path(depot_paths: &[PathBuf], env_name: &str) -> Option<PathBuf> {
depot_paths.first().map(|depot| {
depot
.join("environments")
.join(env_name)
.join(PROJECT_NAMES.last().copied().unwrap_or("Project.toml"))
})
}
fn should_skip_load_path_entry(entry: &str) -> bool {
entry.is_empty() || entry == "@" || entry == "@stdlib" || entry.starts_with("@v")
}
pub fn current_project(dir: &Path) -> Option<PathBuf> {
let home = dirs::home_dir();
let mut current = dir;
loop {
if let Some(project) = find_project_file_in_dir(current) {
return Some(project);
}
if let Some(ref home_dir) = home {
if current == home_dir.as_path() {
break;
}
}
match current.parent() {
Some(parent) if parent != current => current = parent,
_ => break,
}
}
None
}
pub fn load_path_expand_impl(
env: &str,
current_dir: &Path,
depot_path: Option<&std::ffi::OsStr>,
) -> Result<Option<PathBuf>> {
if let Some(stripped) = env.strip_prefix('@') {
match stripped {
"" => return Ok(None),
"." => return Ok(current_project(current_dir)),
"stdlib" => return Ok(None),
_ => {}
}
let depot_paths = resolve_depot_paths(depot_path)?;
if let Some(project) = find_named_environment(&depot_paths, stripped) {
return Ok(Some(project));
}
return Ok(default_named_environment_path(&depot_paths, stripped));
}
let mut path = PathBuf::from(shellexpand::tilde(env).as_ref());
if path.is_relative() {
path = current_dir.join(path);
}
if path.is_dir() {
if let Some(project_file) = find_project_file_in_dir(&path) {
return Ok(Some(project_file));
}
}
Ok(Some(path))
}
pub fn find_project_from_load_path(
load_path: &str,
current_dir: &Path,
depot_path: Option<&std::ffi::OsStr>,
) -> Result<Option<PathBuf>> {
for entry in load_path.split(LOAD_PATH_SEPARATOR).map(str::trim) {
if should_skip_load_path_entry(entry) {
continue;
}
match load_path_expand_impl(entry, current_dir, depot_path)? {
Some(project_file) => {
if project_file_manifest_path(&project_file).is_some() {
log::debug!("Found valid project in JULIA_LOAD_PATH entry: {}", entry);
return Ok(Some(project_file));
}
}
None => continue, }
}
log::debug!("No valid project with manifest found in JULIA_LOAD_PATH");
Ok(None)
}
const PROJECT_FLAGS: &[&str] = &["--project", "--projec", "--proje", "--proj"];
fn match_project_flag(arg: &str) -> Option<Option<String>> {
PROJECT_FLAGS.iter().find_map(|flag| {
if arg == *flag {
Some(None)
} else {
arg.strip_prefix(flag)
.and_then(|rest| rest.strip_prefix('='))
.map(|v| Some(v.to_string()))
}
})
}
pub fn julia_option_requires_arg(opt: &str) -> bool {
if opt.contains('=') {
return false;
}
let short_option = opt
.strip_prefix('-')
.filter(|s| !s.starts_with('-') && s.len() == 1)
.and_then(|s| s.chars().next());
if let Some(short) = short_option {
return !matches!(short, 'v' | 'h' | 'q' | 'i' | 'O' | 'g');
}
!matches!(
opt,
"--version" | "--help" | "--help-hidden" | "--interactive" | "--quiet"
| "--experimental" | "--lisp" | "--image-codegen" | "--rr-detach"
| "--strip-metadata" | "--strip-ir" | "--gc-sweep-always-full"
| "--trace-compile-timing"
| "--project" | "--code-coverage" | "--track-allocation" | "--optimize"
| "--min-optlevel" | "--debug-info" | "--worker" | "--trim" | "--trace-eval"
)
}
pub fn init_active_project_impl(
args: &[String],
current_dir: &Path,
julia_project: Option<&str>,
depot_path: Option<&std::ffi::OsStr>,
) -> Result<Option<PathBuf>> {
let mut project_cli = None;
let mut args_iter = args.iter().skip(1);
while let Some(arg) = args_iter.next() {
if arg == "--" {
break;
}
if !arg.starts_with('-') {
break;
}
if let Some(spec) = match_project_flag(arg) {
project_cli = Some(spec);
} else if julia_option_requires_arg(arg) {
args_iter.next();
}
}
let maybe_project = if let Some(spec) = project_cli {
Some(spec.unwrap_or_else(|| "@.".to_string()))
} else {
julia_project.map(|v| {
if v.trim().is_empty() {
"@.".to_string() } else {
v.to_string()
}
})
};
if let Some(project) = maybe_project {
load_path_expand_impl(&project, current_dir, depot_path)
} else {
Ok(None)
}
}
pub fn project_file_manifest_path(project_file: &Path) -> Option<PathBuf> {
let dir = project_file.parent()?;
if !project_file.exists() || !project_file.is_file() {
return None;
}
let project_content = fs::read_to_string(project_file).ok()?;
let parsed_project: Value = toml::from_str(&project_content).ok()?;
if let Some(Value::String(explicit_manifest)) = parsed_project.get("manifest") {
let manifest_file = if Path::new(explicit_manifest).is_absolute() {
PathBuf::from(explicit_manifest)
} else {
dir.join(explicit_manifest)
};
if manifest_file.exists() && manifest_file.is_file() {
return Some(manifest_file);
}
}
if let Some(versioned_manifest) = find_highest_versioned_manifest(dir) {
return Some(versioned_manifest);
}
find_named_file(dir, MANIFEST_NAMES)
}
pub fn determine_project_version_spec(args: &[String]) -> Result<Option<String>> {
determine_project_version_spec_impl(
args,
std::env::var("JULIA_PROJECT").ok(),
std::env::var("JULIA_LOAD_PATH").ok(),
&std::env::current_dir().with_context(|| "Failed to determine current directory.")?,
)
}
pub fn determine_project_version_spec_impl(
args: &[String],
julia_project: Option<String>,
julia_load_path: Option<String>,
current_dir: &Path,
) -> Result<Option<String>> {
let depot_path = std::env::var_os("JULIA_DEPOT_PATH");
let maybe_project_file =
init_active_project_impl(
args,
current_dir,
julia_project.as_deref(),
depot_path.as_deref(),
)?
.or_else(|| {
julia_load_path.as_ref().and_then(|load_path| {
find_project_from_load_path(load_path, current_dir, depot_path.as_deref())
.ok() .flatten() })
});
let Some(project_file) = maybe_project_file else {
log::debug!("No project specification found");
return Ok(None);
};
extract_version_from_project(project_file)
}
pub fn extract_version_from_project(project_file: PathBuf) -> Result<Option<String>> {
log::debug!("Using project file: {}", project_file.display());
let manifest_path = match project_file_manifest_path(&project_file) {
Some(path) => path,
None => {
log::debug!("No manifest file found for project");
return Ok(None);
}
};
log::debug!("Detected manifest file: {}", manifest_path.display());
if let Some(version) = read_manifest_julia_version(&manifest_path)? {
log::debug!("Read Julia version from manifest: {}", version);
return Ok(Some(version));
}
log::debug!("Manifest file exists but does not contain julia_version field");
Ok(None)
}
pub fn find_highest_versioned_manifest(project_root: &Path) -> Option<PathBuf> {
let entries = fs::read_dir(project_root).ok()?;
let mut highest_julia: Option<(Version, PathBuf)> = None;
let mut highest_manifest: Option<(Version, PathBuf)> = None;
for entry in entries.flatten() {
let path = entry.path();
if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
if !filename.starts_with("JuliaManifest-v") && !filename.starts_with("Manifest-v") {
continue;
}
let (prefix, target) = if filename.starts_with("JuliaManifest-v") {
("JuliaManifest-v", &mut highest_julia)
} else {
("Manifest-v", &mut highest_manifest)
};
if let Some(version) = filename
.strip_prefix(prefix)
.and_then(|s| s.strip_suffix(".toml"))
.and_then(parse_version_lenient)
{
let should_update = match target {
Some((current_version, _)) => version > *current_version,
None => true,
};
if should_update {
*target = Some((version, path));
}
}
}
}
match (highest_julia, highest_manifest) {
(Some((jv, jpath)), Some((mv, mpath))) => Some(if jv >= mv { jpath } else { mpath }),
(Some((_, jpath)), None) => Some(jpath),
(None, Some((_, mpath))) => Some(mpath),
(None, None) => None,
}
}
pub fn parse_version_lenient(version_str: &str) -> Option<Version> {
if let Ok(version) = Version::parse(version_str) {
return Some(version);
}
let parts: Vec<&str> = version_str.split('.').collect();
let normalized = match parts.len() {
1 => format!("{}.0.0", parts[0]),
2 => format!("{}.{}.0", parts[0], parts[1]),
_ => return None,
};
Version::parse(&normalized).ok()
}
pub fn read_manifest_julia_version(path: &Path) -> Result<Option<String>> {
if !path.exists() {
log::debug!(
"Manifest file `{}` not found while attempting to resolve Julia version.",
path.display()
);
return Ok(None);
}
let manifest_content = fs::read_to_string(path)
.with_context(|| format!("Failed to read manifest file `{}`.", path.display()))?;
let manifest: Value = toml::from_str(&manifest_content).with_context(|| {
format!(
"Failed to parse manifest file `{}` as TOML.",
path.display()
)
})?;
Ok(manifest
.get("julia_version")
.and_then(|v| v.as_str())
.map(|s| s.to_string()))
}
pub fn parse_db_version(version: &str) -> Result<Version> {
let base = version
.split('+')
.next()
.ok_or_else(|| anyhow!("Invalid version string `{}`.", version))?;
Version::parse(base).with_context(|| format!("Failed to parse version `{}`.", base))
}
fn versioned_nightly_channel(major: u64, minor: u64) -> String {
format!("{}.{}-nightly", major, minor)
}
impl JuliaupVersionDB {
pub fn max_available_version(&self) -> Option<Version> {
self.available_versions
.keys()
.filter_map(|key| parse_db_version(key).ok())
.max()
}
pub fn max_version_for_minor(&self, major: u64, minor: u64) -> Option<Version> {
self.available_channels
.keys()
.filter_map(|key| parse_db_version(key).ok())
.filter(|version| version.major == major && version.minor == minor)
.max()
}
pub fn has_channel(&self, version: &str) -> bool {
self.available_channels.contains_key(version)
}
}
pub fn resolve_auto_channel(required: String, versions_db: &JuliaupVersionDB) -> Result<String> {
if versions_db.has_channel(&required) {
return Ok(required);
}
let required_version = Version::parse(&required).with_context(|| {
format!(
"Failed to parse Julia version `{}` from manifest.",
required
)
})?;
if !required_version.pre.is_empty() {
let versioned_nightly =
versioned_nightly_channel(required_version.major, required_version.minor);
if versions_db.has_channel(&versioned_nightly) {
print_juliaup_style(
"Info",
&format!(
"Manifest specifies prerelease Julia {}. Using {} channel.",
required, versioned_nightly
),
JuliaupMessageType::Progress,
);
return Ok(versioned_nightly);
}
print_juliaup_style(
"Info",
&format!(
"Manifest specifies prerelease Julia {}. Using nightly channel.",
required
),
JuliaupMessageType::Progress,
);
return Ok("nightly".to_string());
}
let max_version_for_minor =
versions_db.max_version_for_minor(required_version.major, required_version.minor);
if let Some(max_minor_version) = &max_version_for_minor {
if &required_version > max_minor_version {
let channel = versioned_nightly_channel(required_version.major, required_version.minor);
print_juliaup_style(
"Info",
&format!(
"Manifest specifies Julia {} but the highest known version for {}.{} is {}. Using {} channel.",
required,
required_version.major,
required_version.minor,
max_minor_version,
channel
),
JuliaupMessageType::Progress,
);
return Ok(channel);
}
}
let max_known_version = versions_db.max_available_version();
if let Some(max_version) = &max_known_version {
if &required_version > max_version {
let versioned_nightly =
versioned_nightly_channel(required_version.major, required_version.minor);
if versions_db.has_channel(&versioned_nightly) {
print_juliaup_style(
"Info",
&format!(
"Manifest specifies Julia {} but the highest known version is {}. Using {} channel.",
required, max_version, versioned_nightly
),
JuliaupMessageType::Progress,
);
return Ok(versioned_nightly);
}
print_juliaup_style(
"Info",
&format!(
"Manifest specifies Julia {} but the highest known version is {}. Using nightly channel.",
required, max_version
),
JuliaupMessageType::Progress,
);
return Ok("nightly".to_string());
}
} else {
print_juliaup_style(
"Info",
&format!(
"Manifest specifies Julia {} but no versions are known. Using nightly channel.",
required
),
JuliaupMessageType::Progress,
);
return Ok("nightly".to_string());
}
Err(anyhow!(
"Julia version `{}` requested by Project.toml/Manifest.toml is not available in the versions database.",
required
))
}
pub fn get_auto_channel(
args: &[String],
versions_db: &JuliaupVersionDB,
manifest_version_detect: bool,
) -> Result<Option<String>> {
if !manifest_version_detect {
Ok(None)
} else if let Some(required_version) = determine_project_version_spec(args)? {
resolve_auto_channel(required_version, versions_db).map(Some)
} else {
Ok(None)
}
}