use std::collections::BTreeMap;
use std::io::Read;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Uuid([u8; 16]);
impl Uuid {
pub fn bytes(&self) -> [u8; 16] {
self.0
}
}
impl std::str::FromStr for Uuid {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut bytes = [0u8; 16];
let mut nibbles = s.bytes().filter(|b| *b != b'-');
for byte in bytes.iter_mut() {
let hi = nibbles.next().and_then(hex_val).ok_or(())?;
let lo = nibbles.next().and_then(hex_val).ok_or(())?;
*byte = (hi << 4) | lo;
}
if nibbles.next().is_some() {
return Err(()); }
Ok(Uuid(bytes))
}
}
fn hex_val(b: u8) -> Option<u8> {
match b {
b'0'..=b'9' => Some(b - b'0'),
b'a'..=b'f' => Some(b - b'a' + 10),
b'A'..=b'F' => Some(b - b'A' + 10),
_ => None,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PackageKind {
Registered,
Dev,
Stdlib,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Package {
pub name: String,
pub uuid: Uuid,
pub version: Option<String>,
pub tree_sha1: Option<String>,
pub deps: Vec<String>,
pub kind: PackageKind,
pub source: Option<PathBuf>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EnvSource {
JuliaProject,
WorkspaceWalkUp,
DefaultEnv,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct JuliaInstall {
pub prefix: PathBuf,
pub share: PathBuf,
pub base_dir: PathBuf,
pub stdlib_dir: PathBuf,
pub version: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Environment {
pub project_file: PathBuf,
pub project_dir: PathBuf,
pub manifest_file: Option<PathBuf>,
pub name: Option<String>,
pub uuid: Option<Uuid>,
pub direct_deps: BTreeMap<String, Uuid>,
pub packages: Vec<Package>,
pub depots: Vec<PathBuf>,
pub source: EnvSource,
pub install: Option<JuliaInstall>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DevPackage {
pub name: String,
pub root: PathBuf,
}
impl Environment {
pub fn dev_package(&self) -> Option<DevPackage> {
if self.source == EnvSource::DefaultEnv {
return None;
}
let name = self.name.as_ref()?;
let entry = self.project_dir.join("src").join(format!("{name}.jl"));
if entry.is_file() {
Some(DevPackage {
name: name.clone(),
root: self.project_dir.clone(),
})
} else {
None
}
}
}
#[derive(Debug, Clone)]
pub struct EnvContext {
pub workspace_root: PathBuf,
pub julia_project: Option<String>,
pub julia_depot_path: Option<String>,
pub home: Option<PathBuf>,
pub julia_bindir: Option<String>,
pub path: Option<String>,
}
impl EnvContext {
pub fn from_process(workspace_root: PathBuf) -> Self {
Self {
workspace_root,
julia_project: std::env::var("JULIA_PROJECT").ok(),
julia_depot_path: std::env::var("JULIA_DEPOT_PATH").ok(),
home: std::env::var_os("HOME").map(PathBuf::from),
julia_bindir: std::env::var("JULIA_BINDIR").ok(),
path: std::env::var("PATH").ok(),
}
}
}
#[derive(Debug)]
pub enum EnvironmentError {
Read { path: PathBuf, message: String },
Parse { path: PathBuf, message: String },
}
impl std::fmt::Display for EnvironmentError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EnvironmentError::Read { path, message } => {
write!(f, "failed to read {}: {message}", path.display())
}
EnvironmentError::Parse { path, message } => {
write!(f, "failed to parse {}: {message}", path.display())
}
}
}
}
impl std::error::Error for EnvironmentError {}
const PROJECT_NAMES: [&str; 2] = ["JuliaProject.toml", "Project.toml"];
const MANIFEST_NAMES: [&str; 2] = ["JuliaManifest.toml", "Manifest.toml"];
pub fn is_environment_file(path: &Path) -> bool {
let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
return false;
};
PROJECT_NAMES.contains(&name)
|| MANIFEST_NAMES.contains(&name)
|| name
.strip_prefix("Manifest-v")
.and_then(|rest| rest.strip_suffix(".toml"))
.and_then(parse_version)
.is_some()
}
pub fn resolve(ctx: &EnvContext) -> Result<Option<Environment>, EnvironmentError> {
let depots = depot_roots(ctx);
let Some((project_file, source)) = locate_project(ctx, &depots) else {
return Ok(None);
};
let project_dir = project_file
.parent()
.map(Path::to_path_buf)
.unwrap_or_default();
let (name, uuid, direct_deps) = parse_project(&project_file)?;
let manifest_file = find_manifest(&project_dir);
let mut packages = match &manifest_file {
Some(path) => parse_manifest(path, &project_dir, &depots)?,
None => Vec::new(),
};
let install = locate_install(ctx, &depots);
if let Some(install) = &install {
resolve_stdlib_sources(&mut packages, install);
}
Ok(Some(Environment {
project_file,
project_dir,
manifest_file,
name,
uuid,
direct_deps,
packages,
depots,
source,
install,
}))
}
fn locate_project(ctx: &EnvContext, depots: &[PathBuf]) -> Option<(PathBuf, EnvSource)> {
if let Some(raw) = ctx.julia_project.as_deref() {
let trimmed = raw.trim();
if !trimmed.is_empty()
&& let Some(path) = from_julia_project(trimmed, ctx, depots)
{
return Some((path, EnvSource::JuliaProject));
}
}
if let Some(path) = walk_up_for_project(&ctx.workspace_root) {
return Some((path, EnvSource::WorkspaceWalkUp));
}
newest_default_env(ctx).map(|path| (path, EnvSource::DefaultEnv))
}
fn from_julia_project(value: &str, ctx: &EnvContext, depots: &[PathBuf]) -> Option<PathBuf> {
if value == "@." {
return walk_up_for_project(&ctx.workspace_root);
}
if let Some(name) = value.strip_prefix('@') {
return depots
.iter()
.find_map(|depot| project_file_in(&depot.join("environments").join(name)));
}
let path = PathBuf::from(value);
if path.is_file() {
return Some(path);
}
project_file_in(&path)
}
fn walk_up_for_project(anchor: &Path) -> Option<PathBuf> {
anchor.ancestors().find_map(project_file_in)
}
fn project_file_in(dir: &Path) -> Option<PathBuf> {
PROJECT_NAMES
.iter()
.map(|name| dir.join(name))
.find(|candidate| candidate.is_file())
}
fn find_manifest(project_dir: &Path) -> Option<PathBuf> {
if let Some(path) = MANIFEST_NAMES
.iter()
.map(|name| project_dir.join(name))
.find(|candidate| candidate.is_file())
{
return Some(path);
}
std::fs::read_dir(project_dir)
.ok()?
.filter_map(Result::ok)
.filter_map(|entry| {
let name = entry.file_name();
let name = name.to_str()?;
let version = name
.strip_prefix("Manifest-v")?
.strip_suffix(".toml")
.and_then(parse_version)?;
Some((version, entry.path()))
})
.max_by_key(|(version, _)| *version)
.map(|(_, path)| path)
}
fn newest_default_env(ctx: &EnvContext) -> Option<PathBuf> {
let envs = ctx.home.as_ref()?.join(".julia").join("environments");
let dir = std::fs::read_dir(&envs)
.ok()?
.filter_map(Result::ok)
.filter_map(|entry| {
let name = entry.file_name();
let version = parse_version(name.to_str()?.strip_prefix('v')?)?;
Some((version, entry.path()))
})
.max_by_key(|(version, _)| *version)
.map(|(_, path)| path)?;
project_file_in(&dir)
}
fn parse_version(s: &str) -> Option<(u32, u32)> {
let mut parts = s.split('.');
let major = parts.next()?.parse().ok()?;
let minor = parts.next()?.parse().ok()?;
Some((major, minor))
}
fn depot_roots(ctx: &EnvContext) -> Vec<PathBuf> {
let default = ctx.home.as_ref().map(|home| home.join(".julia"));
match ctx.julia_depot_path.as_deref() {
Some(raw) if !raw.trim().is_empty() => raw
.split(depot_separator())
.flat_map(|entry| {
if entry.is_empty() {
default.clone()
} else {
Some(PathBuf::from(entry))
}
})
.collect(),
_ => default.into_iter().collect(),
}
}
const fn depot_separator() -> char {
if cfg!(windows) { ';' } else { ':' }
}
pub fn locate_install(ctx: &EnvContext, depots: &[PathBuf]) -> Option<JuliaInstall> {
install_from_bindir(ctx)
.or_else(|| install_from_juliaup(depots))
.or_else(|| install_from_path(ctx))
}
fn install_from_bindir(ctx: &EnvContext) -> Option<JuliaInstall> {
let bindir = ctx.julia_bindir.as_deref().map(str::trim)?;
if bindir.is_empty() {
return None;
}
let prefix = Path::new(bindir).parent()?;
install_from_share(prefix.join("share").join("julia"))
}
fn install_from_juliaup(depots: &[PathBuf]) -> Option<JuliaInstall> {
for depot in depots {
let juliaup = depot.join("juliaup");
let Ok(text) = std::fs::read_to_string(juliaup.join("juliaup.json")) else {
continue;
};
let Ok(json) = serde_json::from_str::<serde_json::Value>(&text) else {
continue;
};
let Some(install) = juliaup_install_dir(&json, &juliaup) else {
continue;
};
if let Some(found) = install_from_share(install.join("share").join("julia")) {
return Some(found);
}
}
None
}
fn juliaup_install_dir(json: &serde_json::Value, juliaup: &Path) -> Option<PathBuf> {
let default = json.get("Default")?.as_str()?;
let version = json
.get("InstalledChannels")?
.get(default)?
.get("Version")?
.as_str()?;
let rel = json
.get("InstalledVersions")?
.get(version)?
.get("Path")?
.as_str()?;
Some(juliaup.join(rel))
}
fn install_from_path(ctx: &EnvContext) -> Option<JuliaInstall> {
let path = ctx.path.as_deref()?;
let exe = if cfg!(windows) { "julia.exe" } else { "julia" };
let julia = path
.split(depot_separator())
.filter(|dir| !dir.is_empty())
.map(|dir| Path::new(dir).join(exe))
.find(|candidate| candidate.is_file())?;
let real = std::fs::canonicalize(&julia).ok()?;
let real = std::fs::canonicalize(follow_wrapper(&real, 8)).unwrap_or(real);
let prefix = real.parent()?.parent()?; install_from_share(prefix.join("share").join("julia"))
}
fn follow_wrapper(path: &Path, depth: u8) -> PathBuf {
if depth == 0 || !is_shebang(path) {
return path.to_path_buf();
}
match std::fs::read_to_string(path)
.ok()
.as_deref()
.and_then(exec_target)
{
Some(target) if target != path => follow_wrapper(&target, depth - 1),
_ => path.to_path_buf(),
}
}
fn is_shebang(path: &Path) -> bool {
let mut buf = [0u8; 2];
std::fs::File::open(path)
.and_then(|mut file| file.read_exact(&mut buf))
.is_ok()
&& &buf == b"#!"
}
fn exec_target(script: &str) -> Option<PathBuf> {
let line = script
.lines()
.rev()
.find(|line| line.trim_start().starts_with("exec "))?;
let after = line.trim_start().strip_prefix("exec ")?.trim_start();
let target = match after.strip_prefix('"') {
Some(rest) => rest.split('"').next()?,
None => after.split_whitespace().next()?,
};
(!target.is_empty()).then(|| PathBuf::from(target))
}
fn install_from_share(share: PathBuf) -> Option<JuliaInstall> {
let base_dir = share.join("base");
if !base_dir.join("Base.jl").is_file() {
return None;
}
let (version, stdlib_dir) = newest_stdlib(&share.join("stdlib"))?;
let prefix = share.parent()?.parent()?.to_path_buf(); Some(JuliaInstall {
prefix,
base_dir,
stdlib_dir,
version,
share,
})
}
fn newest_stdlib(stdlib: &Path) -> Option<(String, PathBuf)> {
std::fs::read_dir(stdlib)
.ok()?
.filter_map(Result::ok)
.filter_map(|entry| {
let name = entry.file_name();
let raw = name.to_str()?.strip_prefix('v')?;
let version = parse_version(raw)?;
Some((version, raw.to_string(), entry.path()))
})
.max_by_key(|(version, _, _)| *version)
.map(|(_, raw, path)| (raw, path))
}
fn resolve_stdlib_sources(packages: &mut [Package], install: &JuliaInstall) {
for pkg in packages
.iter_mut()
.filter(|p| p.kind == PackageKind::Stdlib)
{
let dir = install.stdlib_dir.join(&pkg.name);
if dir.is_dir() {
pkg.source = Some(dir);
}
}
}
type ProjectMeta = (Option<String>, Option<Uuid>, BTreeMap<String, Uuid>);
fn parse_project(path: &Path) -> Result<ProjectMeta, EnvironmentError> {
let table = read_toml(path)?;
let name = table
.get("name")
.and_then(|v| v.as_str())
.map(str::to_string);
let uuid = table
.get("uuid")
.and_then(|v| v.as_str())
.and_then(|s| s.parse().ok());
let mut direct_deps = BTreeMap::new();
if let Some(deps) = table.get("deps").and_then(|v| v.as_table()) {
for (dep_name, value) in deps {
if let Some(uuid) = value.as_str().and_then(|s| s.parse().ok()) {
direct_deps.insert(dep_name.clone(), uuid);
}
}
}
Ok((name, uuid, direct_deps))
}
fn parse_manifest(
path: &Path,
project_dir: &Path,
depots: &[PathBuf],
) -> Result<Vec<Package>, EnvironmentError> {
let table = read_toml(path)?;
let mut packages = Vec::new();
if let Some(deps) = table.get("deps").and_then(|v| v.as_table()) {
for (name, value) in deps {
collect_entries(name, value, project_dir, depots, &mut packages);
}
} else {
for (name, value) in &table {
collect_entries(name, value, project_dir, depots, &mut packages);
}
}
packages.sort_by(|a, b| a.name.cmp(&b.name));
Ok(packages)
}
fn collect_entries(
name: &str,
value: &toml::Value,
project_dir: &Path,
depots: &[PathBuf],
packages: &mut Vec<Package>,
) {
let Some(entries) = value.as_array() else {
return;
};
for entry in entries {
if let Some(table) = entry.as_table()
&& let Some(package) = parse_entry(name, table, project_dir, depots)
{
packages.push(package);
}
}
}
fn parse_entry(
name: &str,
table: &toml::Table,
project_dir: &Path,
depots: &[PathBuf],
) -> Option<Package> {
let uuid: Uuid = table.get("uuid").and_then(|v| v.as_str())?.parse().ok()?;
let version = table
.get("version")
.and_then(|v| v.as_str())
.map(str::to_string);
let tree_sha1 = table
.get("git-tree-sha1")
.and_then(|v| v.as_str())
.map(str::to_string);
let path = table.get("path").and_then(|v| v.as_str());
let deps = extract_deps(table.get("deps"));
let kind = if path.is_some() {
PackageKind::Dev
} else if tree_sha1.is_some() {
PackageKind::Registered
} else {
PackageKind::Stdlib
};
let source = match kind {
PackageKind::Dev => Some(resolve_dev_path(project_dir, path?)),
PackageKind::Registered => resolve_registered(name, uuid, tree_sha1.as_deref()?, depots),
PackageKind::Stdlib => None,
};
Some(Package {
name: name.to_string(),
uuid,
version,
tree_sha1,
deps,
kind,
source,
})
}
fn extract_deps(value: Option<&toml::Value>) -> Vec<String> {
match value {
Some(toml::Value::Array(arr)) => arr
.iter()
.filter_map(|v| v.as_str().map(str::to_string))
.collect(),
Some(toml::Value::Table(table)) => table.keys().cloned().collect(),
_ => Vec::new(),
}
}
fn resolve_dev_path(project_dir: &Path, path: &str) -> PathBuf {
let path = Path::new(path);
if path.is_absolute() {
path.to_path_buf()
} else {
project_dir.join(path)
}
}
fn resolve_registered(
name: &str,
uuid: Uuid,
tree_sha1: &str,
depots: &[PathBuf],
) -> Option<PathBuf> {
let sha1 = parse_sha1(tree_sha1)?;
let slug = version_slug(uuid, &sha1);
depots
.iter()
.map(|depot| depot.join("packages").join(name).join(&slug))
.find(|candidate| candidate.is_dir())
}
const SLUG_CHARS: &[u8; 62] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
fn version_slug(uuid: Uuid, sha1: &[u8]) -> String {
let mut uuid_le = uuid.bytes();
uuid_le.reverse();
let crc = crc32c(&uuid_le, 0);
let crc = crc32c(sha1, crc);
slug(crc, 5)
}
fn slug(mut value: u32, len: usize) -> String {
let base = SLUG_CHARS.len() as u32;
let mut out = String::with_capacity(len);
for _ in 0..len {
let digit = (value % base) as usize;
value /= base;
out.push(SLUG_CHARS[digit] as char);
}
out
}
fn crc32c(bytes: &[u8], crc: u32) -> u32 {
const POLY: u32 = 0x82F6_3B78;
let mut c = !crc;
for &byte in bytes {
c ^= byte as u32;
for _ in 0..8 {
c = if c & 1 != 0 { (c >> 1) ^ POLY } else { c >> 1 };
}
}
!c
}
fn parse_sha1(s: &str) -> Option<[u8; 20]> {
let mut bytes = [0u8; 20];
let mut nibbles = s.bytes();
for byte in bytes.iter_mut() {
let hi = nibbles.next().and_then(hex_val)?;
let lo = nibbles.next().and_then(hex_val)?;
*byte = (hi << 4) | lo;
}
if nibbles.next().is_some() {
return None;
}
Some(bytes)
}
fn read_toml(path: &Path) -> Result<toml::Table, EnvironmentError> {
let text = std::fs::read_to_string(path).map_err(|err| EnvironmentError::Read {
path: path.to_path_buf(),
message: err.to_string(),
})?;
text.parse::<toml::Table>()
.map_err(|err| EnvironmentError::Parse {
path: path.to_path_buf(),
message: err.to_string(),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_uuid_roundtrip() {
let uuid: Uuid = "1520ce14-60c1-5f80-bbc7-55ef81b5835c".parse().unwrap();
assert_eq!(uuid.bytes()[0], 0x15);
assert_eq!(uuid.bytes()[15], 0x5c);
}
#[test]
fn rejects_malformed_uuid() {
assert!("not-a-uuid".parse::<Uuid>().is_err());
assert!("1520ce14".parse::<Uuid>().is_err());
}
#[test]
fn crc32c_empty_is_zero() {
assert_eq!(crc32c(b"", 0), 0);
}
#[test]
fn crc32c_chains() {
assert_eq!(
crc32c(b"world", crc32c(b"hello ", 0)),
crc32c(b"hello world", 0)
);
}
#[test]
fn version_slug_golden() {
let uuid: Uuid = "1520ce14-60c1-5f80-bbc7-55ef81b5835c".parse().unwrap();
let sha1 = parse_sha1("2d9c9a55f9c93e8887ad391fbae72f8ef55e1177").unwrap();
assert_eq!(version_slug(uuid, &sha1), "Ftf8W");
}
#[test]
fn extract_deps_array_and_table() {
let value: toml::Value = toml::from_str("deps = [\"A\", \"B\"]").unwrap();
assert_eq!(extract_deps(value.get("deps")), vec!["A", "B"]);
let value: toml::Value = toml::from_str("[deps]\nA = \"x\"\nB = \"y\"").unwrap();
let mut got = extract_deps(value.get("deps"));
got.sort();
assert_eq!(got, vec!["A", "B"]);
}
#[test]
fn classifies_manifest_entries() {
let text = r#"
julia_version = "1.11.7"
manifest_format = "2.0"
[[deps.AbstractTrees]]
git-tree-sha1 = "2d9c9a55f9c93e8887ad391fbae72f8ef55e1177"
uuid = "1520ce14-60c1-5f80-bbc7-55ef81b5835c"
version = "0.4.5"
[[deps.Dates]]
uuid = "ade2ca70-3891-5945-98fb-dc099432e06a"
[[deps.Local]]
deps = ["Dates"]
path = "vendor/Local"
uuid = "00000000-0000-0000-0000-000000000001"
"#;
let table: toml::Table = text.parse().unwrap();
let project_dir = Path::new("/proj");
let deps = table.get("deps").and_then(|v| v.as_table()).unwrap();
let mut packages = Vec::new();
for (name, value) in deps {
collect_entries(name, value, project_dir, &[], &mut packages);
}
packages.sort_by(|a, b| a.name.cmp(&b.name));
assert_eq!(packages.len(), 3);
let by_name = |n: &str| packages.iter().find(|p| p.name == n).unwrap();
assert_eq!(by_name("AbstractTrees").kind, PackageKind::Registered);
assert_eq!(by_name("Dates").kind, PackageKind::Stdlib);
assert_eq!(by_name("Dates").source, None);
let local = by_name("Local");
assert_eq!(local.kind, PackageKind::Dev);
assert_eq!(local.deps, vec!["Dates"]);
assert_eq!(local.source, Some(PathBuf::from("/proj/vendor/Local")));
}
#[test]
fn depot_roots_fall_back_to_home() {
let ctx = EnvContext {
workspace_root: PathBuf::from("/ws"),
julia_project: None,
julia_depot_path: None,
home: Some(PathBuf::from("/home/u")),
julia_bindir: None,
path: None,
};
assert_eq!(depot_roots(&ctx), vec![PathBuf::from("/home/u/.julia")]);
}
#[test]
fn depot_roots_expand_empty_entry_to_default() {
let sep = depot_separator();
let ctx = EnvContext {
workspace_root: PathBuf::from("/ws"),
julia_project: None,
julia_depot_path: Some(format!("/custom{sep}")),
home: Some(PathBuf::from("/home/u")),
julia_bindir: None,
path: None,
};
assert_eq!(
depot_roots(&ctx),
vec![PathBuf::from("/custom"), PathBuf::from("/home/u/.julia")]
);
}
#[test]
fn classifies_environment_files() {
for name in [
"Project.toml",
"JuliaProject.toml",
"Manifest.toml",
"JuliaManifest.toml",
"Manifest-v1.11.toml",
] {
assert!(
is_environment_file(&PathBuf::from("/ws").join(name)),
"{name} steers resolution"
);
}
for name in ["a.jl", "Cargo.toml", "Manifest-vX.toml", "Manifest-v1.11"] {
assert!(
!is_environment_file(&PathBuf::from("/ws").join(name)),
"{name} does not steer resolution"
);
}
}
#[test]
fn parse_version_orders_correctly() {
assert!(parse_version("1.11") > parse_version("1.7"));
assert!(parse_version("2.0") > parse_version("1.99"));
assert_eq!(parse_version("nope"), None);
}
}