use std::sync::Mutex;
use fusevm::{Op, Value, VM};
use crate::compiler::{ext, CompileError, Compiler};
use crate::parser::Word;
use crate::runtime::{to_tcl_string, TclError};
pub const SUBCOMMANDS: &[&str] = &[
"files",
"forget",
"ifneeded",
"names",
"prefer",
"present",
"provide",
"require",
"unknown",
"vcompare",
"versions",
"vsatisfies",
];
struct Avail {
version: String,
script: String,
}
struct Package {
name: String,
version: Option<String>,
avail: Vec<Avail>,
client_data: usize,
providing: Option<String>,
}
const PREFER: [&str; 2] = ["latest", "stable"];
const PREFER_STABLE: usize = 1;
struct Registry {
packages: Vec<Package>,
unknown: Option<String>,
prefer: usize,
}
static REGISTRY: Mutex<Registry> = Mutex::new(Registry {
packages: Vec::new(),
unknown: None,
prefer: PREFER_STABLE,
});
fn registry() -> std::sync::MutexGuard<'static, Registry> {
REGISTRY.lock().expect("package registry poisoned")
}
impl Registry {
fn find_or_create(&mut self, name: &str) -> &mut Package {
if let Some(i) = self.packages.iter().position(|p| p.name == name) {
return &mut self.packages[i];
}
self.packages.push(Package {
name: name.to_string(),
version: None,
avail: Vec::new(),
client_data: 0,
providing: None,
});
self.packages.last_mut().expect("just pushed")
}
fn find(&self, name: &str) -> Option<&Package> {
self.packages.iter().find(|p| p.name == name)
}
}
pub fn provided_version(name: &str) -> Option<String> {
registry().find(name).and_then(|p| p.version.clone())
}
pub fn provided() -> Vec<(String, String)> {
registry()
.packages
.iter()
.filter_map(|p| p.version.clone().map(|v| (p.name.clone(), v)))
.collect()
}
pub fn client_data(name: &str) -> usize {
registry().find(name).map(|p| p.client_data).unwrap_or(0)
}
pub fn provide(name: &str, version: &str, client_data: usize) -> Result<(), String> {
let previous = registry().find(name).and_then(|p| p.version.clone());
let Some(previous) = previous else {
let mut reg = registry();
let pkg = reg.find_or_create(name);
pkg.version = Some(version.to_string());
pkg.client_data = client_data;
return Ok(());
};
let pvi = check_version_and_convert(&previous).ok_or_else(|| bad_version(&previous))?;
let vi = check_version_and_convert(version).ok_or_else(|| bad_version(version))?;
if compare_versions(&pvi, &vi).0 == 0 {
if client_data != 0 {
registry().find_or_create(name).client_data = client_data;
}
return Ok(());
}
Err(format!(
"conflicting versions provided for package \"{name}\": {previous}, then {version}"
))
}
fn bad_version(text: &str) -> String {
format!("expected version number but got \"{text}\"")
}
pub fn compare(a: &str, b: &str) -> Option<i32> {
let (a, b) = (check_version_and_convert(a)?, check_version_and_convert(b)?);
Some(compare_versions(&a, &b).0)
}
pub fn check_version_and_convert(version: &str) -> Option<String> {
convert(version).map(|(internal, _)| internal)
}
fn convert(version: &str) -> Option<(String, bool)> {
let b = version.as_bytes();
if b.first().is_none_or(|c| !c.is_ascii_digit()) {
return None;
}
let mut out = String::new();
out.push(b[0] as char);
let mut has_unstable = false;
let mut prev = b[0];
for &c in &b[1..] {
if c == b'+' {
break;
}
let structural = c == b'.' || c == b'a' || c == b'b';
if !c.is_ascii_digit()
&& (!structural
|| (has_unstable && (c == b'a' || c == b'b'))
|| ((prev == b'a' || prev == b'b' || prev == b'.') && c == b'.')
|| (structural && prev == b'.'))
{
return None;
}
if c == b'a' || c == b'b' {
has_unstable = true;
}
match c {
b'.' => out.push_str(" 0 "),
b'a' => out.push_str(" -2 "),
b'b' => out.push_str(" -1 "),
_ => out.push(c as char),
}
prev = c;
}
if prev == b'.' || prev == b'a' || prev == b'b' {
return None;
}
Some((out, !has_unstable))
}
pub fn compare_versions(v1: &str, v2: &str) -> (i32, bool) {
let a = v1.as_bytes();
let b = v2.as_bytes();
let (mut s1, mut s2) = (0usize, 0usize);
let mut this_is_major = true;
let res;
loop {
while s1 < a.len() && a[s1] == b'0' {
s1 += 1;
}
while s2 < b.len() && b[s2] == b'0' {
s2 += 1;
}
let neg1 = a.get(s1) == Some(&b'-');
let neg2 = b.get(s2) == Some(&b'-');
if neg1 && !neg2 {
res = -1;
break;
}
if !neg1 && neg2 {
res = 1;
break;
}
let flip = neg1 && neg2;
if flip {
s1 += 1;
s2 += 1;
}
let e1 = s1
+ a[s1..]
.iter()
.position(|c| *c == b' ')
.unwrap_or(a.len() - s1);
let e2 = s2
+ b[s2..]
.iter()
.position(|c| *c == b' ')
.unwrap_or(b.len() - s2);
let mut r: i32 = if e1 - s1 < e2 - s2 {
-1
} else if e2 - s2 < e1 - s1 {
1
} else {
match a[s1..e1].cmp(&b[s2..e2]) {
std::cmp::Ordering::Less => -1,
std::cmp::Ordering::Equal => 0,
std::cmp::Ordering::Greater => 1,
}
};
if r != 0 {
if flip {
r = -r;
}
res = r;
break;
}
s1 = e1;
s2 = e2;
if s1 < a.len() {
s1 += 1;
} else if s2 >= b.len() {
res = 0;
break;
}
if s2 < b.len() {
s2 += 1;
}
this_is_major = false;
}
(res, this_is_major)
}
fn check_requirement(req: &str) -> Result<(), String> {
let dash = match req.find('+') {
Some(_) => None,
None => req.find('-'),
};
let Some(dash) = dash else {
return match check_version_and_convert(req) {
Some(_) => Ok(()),
None => Err(bad_version(req)),
};
};
let (min, max) = (&req[..dash], &req[dash + 1..]);
if max.contains('-') {
return Err(format!("expected versionMin-versionMax but got \"{req}\""));
}
if check_version_and_convert(min).is_none() {
return Err(bad_version(min));
}
if !max.is_empty() && check_version_and_convert(max).is_none() {
return Err(bad_version(max));
}
Ok(())
}
fn check_all_requirements(reqs: &[String]) -> Result<(), String> {
reqs.iter().try_for_each(|r| check_requirement(r))
}
fn requirement_satisfied(havei: &str, req: &str) -> bool {
let Some(dash) = req.find('-') else {
let Some(mut reqi) = check_version_and_convert(req) else {
return false;
};
reqi.push_str(" -2");
let (res, this_is_major) = compare_versions(havei, &reqi);
return res == 0 || (res == 1 && !this_is_major);
};
let (min, max) = (&req[..dash], &req[dash + 1..]);
let Some(mut mini) = check_version_and_convert(min) else {
return false;
};
if max.is_empty() {
mini.push_str(" -2");
return compare_versions(havei, &mini).0 >= 0;
}
let Some(mut maxi) = check_version_and_convert(max) else {
return false;
};
if compare_versions(&mini, &maxi).0 == 0 {
return compare_versions(&mini, havei).0 == 0;
}
mini.push_str(" -2");
maxi.push_str(" -2");
compare_versions(&mini, havei).0 <= 0 && compare_versions(havei, &maxi).0 < 0
}
fn some_requirement_satisfied(havei: &str, reqs: &[String]) -> bool {
reqs.iter().any(|r| requirement_satisfied(havei, r))
}
fn add_requirements_to_result(out: &mut String, reqs: &[String]) {
for r in reqs {
let n = r.len();
let (dash, after) = (n / 2, n.div_ceil(2));
let exact = n % 2 == 1 && r.as_bytes()[dash] == b'-' && r[..dash] == r[after..];
match exact {
true => out.push_str(&format!(" exactly {}", &r[after..])),
false => out.push_str(&format!(" {r}")),
}
}
}
fn add_requirements_to_dstring(out: &mut String, reqs: &[String]) {
if reqs.is_empty() {
out.push_str(" 0-");
return;
}
for r in reqs {
out.push(' ');
out.push_str(r);
}
}
pub trait ScriptHost {
fn eval(&mut self, src: &str) -> Result<String, TclError>;
}
fn pkg_require(name: &str, reqs: &[String], host: &mut dyn ScriptHost) -> Result<String, TclError> {
check_all_requirements(reqs).map_err(TclError::plain)?;
registry().find_or_create(name);
if provided_version(name).is_none() {
select_package(name, reqs, host)?;
}
if provided_version(name).is_none() {
let script = registry().unknown.clone();
if let Some(script) = script {
let mut command = script;
command.push(' ');
command.push_str(&crate::list::quote(name, false));
add_requirements_to_dstring(&mut command, reqs);
host.eval(&command)?;
if provided_version(name).is_none() {
select_package(name, reqs, host)?;
}
}
}
if provided_version(name).is_none() {
load_native(name).map_err(TclError::plain)?;
}
let Some(have) = provided_version(name) else {
let mut msg = format!("can't find package {name}");
add_requirements_to_result(&mut msg, reqs);
return Err(TclError::plain(msg));
};
if !reqs.is_empty() {
let havei =
check_version_and_convert(&have).ok_or_else(|| TclError::plain(bad_version(&have)))?;
if !some_requirement_satisfied(&havei, reqs) {
let mut msg = format!("version conflict for package \"{name}\": have {have}, need");
add_requirements_to_result(&mut msg, reqs);
return Err(TclError::plain(msg));
}
}
Ok(have)
}
fn select_package(name: &str, reqs: &[String], host: &mut dyn ScriptHost) -> Result<(), TclError> {
if let Some(providing) = registry().find(name).and_then(|p| p.providing.clone()) {
let mut msg = format!(
"circular package dependency: attempt to provide {name} {providing} requires {name}"
);
add_requirements_to_result(&mut msg, reqs);
return Err(TclError::plain(msg));
}
let best = {
let reg = registry();
let Some(pkg) = reg.find(name) else {
return Ok(());
};
best_available(pkg, reqs, reg.prefer)
};
let Some((version, script)) = best else {
return Ok(());
};
registry().find_or_create(name).providing = Some(version.clone());
let outcome = host.eval(&script);
registry().find_or_create(name).providing = None;
if let Err(e) = outcome {
registry().find_or_create(name).version = None;
return Err(e);
}
match provided_version(name) {
None => Err(TclError::plain(format!(
"attempt to provide package {name} {version} failed: \
no version of package {name} provided"
))),
Some(got) => match compare(&got, &version) {
Some(0) => Ok(()),
_ => {
registry().find_or_create(name).version = None;
Err(TclError::plain(format!(
"attempt to provide package {name} {version} failed: \
package {name} {got} provided instead"
)))
}
},
}
}
fn best_available(pkg: &Package, reqs: &[String], prefer: usize) -> Option<(String, String)> {
let mut best: Option<(&Avail, String)> = None;
let mut best_stable: Option<(&Avail, String)> = None;
for avail in &pkg.avail {
let Some((availi, stable)) = convert(&avail.version) else {
continue;
};
if !reqs.is_empty() && !some_requirement_satisfied(&availi, reqs) {
continue;
}
if best
.as_ref()
.is_none_or(|(_, b)| compare_versions(&availi, b).0 > 0)
{
best = Some((avail, availi.clone()));
}
if stable
&& best_stable
.as_ref()
.is_none_or(|(_, b)| compare_versions(&availi, b).0 > 0)
{
best_stable = Some((avail, availi));
}
}
let chosen = match (prefer == PREFER_STABLE, &best_stable) {
(true, Some(_)) => best_stable,
_ => best,
};
chosen.map(|(a, _)| (a.version.clone(), a.script.clone()))
}
fn load_native(name: &str) -> Result<(), String> {
#[cfg(feature = "tk")]
if name == "Tk" || name == "tk" {
return crate::tk::session::load_tk();
}
let _ = name;
Ok(())
}
fn resolve(word: &str) -> Result<&'static str, String> {
if let Some(exact) = SUBCOMMANDS.iter().copied().find(|s| *s == word) {
return Ok(exact);
}
let mut hits = SUBCOMMANDS.iter().copied().filter(|s| s.starts_with(word));
match (hits.next(), hits.next()) {
(Some(only), None) if !word.is_empty() => Ok(only),
_ => Err(format!(
"bad option \"{word}\": must be {}, or {}",
SUBCOMMANDS[..SUBCOMMANDS.len() - 1].join(", "),
SUBCOMMANDS[SUBCOMMANDS.len() - 1]
)),
}
}
fn wrong_args(sub: &str, usage: &str) -> String {
match usage.is_empty() {
true => format!("wrong # args: should be \"package {sub}\""),
false => format!("wrong # args: should be \"package {sub} {usage}\""),
}
}
pub fn run(argv: &[String], host: &mut dyn ScriptHost) -> Result<String, TclError> {
if argv.len() < 2 {
return Err(TclError::plain(
"wrong # args: should be \"package option ?arg ...?\"".to_string(),
));
}
let sub = resolve(&argv[1]).map_err(TclError::plain)?;
let args = &argv[2..];
match sub {
"files" => match args.len() {
1 => Ok(String::new()),
_ => Err(TclError::plain(wrong_args("files", "package"))),
},
"forget" => {
let mut reg = registry();
reg.packages.retain(|p| !args.contains(&p.name));
Ok(String::new())
}
"ifneeded" => ifneeded(args),
"names" => match args.is_empty() {
true => Ok(crate::list::join(
®istry()
.packages
.iter()
.filter(|p| p.version.is_some() || !p.avail.is_empty())
.map(|p| p.name.clone())
.collect::<Vec<_>>(),
)),
false => Err(TclError::plain(wrong_args("names", ""))),
},
"prefer" => prefer(args),
"present" => present(args, host),
"provide" => provide_cmd(args),
"require" => require_cmd("require", args, host),
"unknown" => unknown(args),
"vcompare" => match args.len() {
2 => {
let a = check_version_and_convert(&args[0])
.ok_or_else(|| TclError::plain(bad_version(&args[0])))?;
let b = check_version_and_convert(&args[1])
.ok_or_else(|| TclError::plain(bad_version(&args[1])))?;
Ok(compare_versions(&a, &b).0.to_string())
}
_ => Err(TclError::plain(wrong_args("vcompare", "version1 version2"))),
},
"versions" => match args.len() {
1 => {
let versions: Vec<String> = registry()
.find(&args[0])
.map(|p| p.avail.iter().map(|a| a.version.clone()).collect())
.unwrap_or_default();
Ok(crate::list::join(&versions))
}
_ => Err(TclError::plain(wrong_args("versions", "package"))),
},
"vsatisfies" => match args.len() {
0 | 1 => Err(TclError::plain(wrong_args(
"vsatisfies",
"version ?requirement ...?",
))),
_ => {
let have = check_version_and_convert(&args[0])
.ok_or_else(|| TclError::plain(bad_version(&args[0])))?;
check_all_requirements(&args[1..]).map_err(TclError::plain)?;
Ok(u8::from(some_requirement_satisfied(&have, &args[1..])).to_string())
}
},
_ => unreachable!("resolve answers only with a name from SUBCOMMANDS"),
}
}
fn ifneeded(args: &[String]) -> Result<String, TclError> {
if args.len() != 2 && args.len() != 3 {
return Err(TclError::plain(wrong_args(
"ifneeded",
"package version ?script?",
)));
}
let (name, version) = (&args[0], &args[1]);
let wanted =
check_version_and_convert(version).ok_or_else(|| TclError::plain(bad_version(version)))?;
let mut reg = registry();
if args.len() == 2 {
let Some(pkg) = reg.find(name) else {
return Ok(String::new());
};
let found = pkg.avail.iter().find(|a| {
check_version_and_convert(&a.version)
.is_some_and(|avi| compare_versions(&avi, &wanted).0 == 0)
});
return Ok(found.map(|a| a.script.clone()).unwrap_or_default());
}
let pkg = reg.find_or_create(name);
let existing = pkg.avail.iter().position(|a| {
check_version_and_convert(&a.version)
.is_some_and(|avi| compare_versions(&avi, &wanted).0 == 0)
});
match existing {
Some(i) => pkg.avail[i].script = args[2].clone(),
None => pkg.avail.push(Avail {
version: version.clone(),
script: args[2].clone(),
}),
}
Ok(String::new())
}
fn prefer(args: &[String]) -> Result<String, TclError> {
if args.len() > 1 {
return Err(TclError::plain(wrong_args("prefer", "?latest|stable?")));
}
let mut reg = registry();
if let Some(word) = args.first() {
let new = PREFER.iter().position(|p| *p == word).ok_or_else(|| {
TclError::plain(format!(
"bad preference \"{word}\": must be latest or stable"
))
})?;
if new < reg.prefer {
reg.prefer = new;
}
}
Ok(PREFER[reg.prefer].to_string())
}
fn provide_cmd(args: &[String]) -> Result<String, TclError> {
match args.len() {
1 => Ok(provided_version(&args[0]).unwrap_or_default()),
2 => {
let (name, version) = (&args[0], &args[1]);
check_version_and_convert(version)
.ok_or_else(|| TclError::plain(bad_version(version)))?;
provide(name, version, 0)
.map(|()| String::new())
.map_err(TclError::plain)
}
_ => Err(TclError::plain(wrong_args("provide", "package ?version?"))),
}
}
fn present(args: &[String], host: &mut dyn ScriptHost) -> Result<String, TclError> {
if args.is_empty() {
return require_cmd("present", args, host);
}
let exact = args[0] == "-exact";
if exact && args.len() != 3 {
return Err(TclError::plain(wrong_args(
"present",
"?-exact? package ?requirement ...?",
)));
}
let name = match exact {
true => &args[1],
false => &args[0],
};
if provided_version(name).is_some() {
return require_cmd("present", args, host);
}
let version = match exact {
true => {
check_version_and_convert(&args[2])
.ok_or_else(|| TclError::plain(bad_version(&args[2])))?;
Some(args[2].clone())
}
false => {
check_all_requirements(&args[1..]).map_err(TclError::plain)?;
args.get(1)
.filter(|v| check_version_and_convert(v).is_some())
.cloned()
}
};
Err(TclError::plain(match version {
Some(v) => format!("package {name} {v} is not present"),
None => format!("package {name} is not present"),
}))
}
fn require_cmd(sub: &str, args: &[String], host: &mut dyn ScriptHost) -> Result<String, TclError> {
let syntax = || TclError::plain(wrong_args(sub, "?-exact? package ?requirement ...?"));
if args.is_empty() {
return Err(syntax());
}
if args[0] == "-exact" {
if args.len() != 3 {
return Err(syntax());
}
let version = &args[2];
check_version_and_convert(version).ok_or_else(|| TclError::plain(bad_version(version)))?;
let reqs = vec![format!("{version}-{version}")];
return pkg_require(&args[1], &reqs, host);
}
pkg_require(&args[0], &args[1..], host)
}
fn unknown(args: &[String]) -> Result<String, TclError> {
let mut reg = registry();
match args.len() {
0 => Ok(reg.unknown.clone().unwrap_or_default()),
1 => {
reg.unknown = match args[0].is_empty() {
true => None,
false => Some(args[0].clone()),
};
Ok(String::new())
}
_ => Err(TclError::plain(wrong_args("unknown", "?command?"))),
}
}
pub(crate) fn compile(c: &mut Compiler, args: &[Word]) -> Result<(), CompileError> {
let count = u8::try_from(args.len() + 2)
.map_err(|_| c.err("more than 253 arguments to the command \"package\"".to_string()))?;
c.push_value(Value::Int(c.command_line as i64));
c.push_str("package");
for arg in args {
c.word(arg)?;
}
c.emit(Op::Extended(ext::PACKAGE, count), 1 - count as i32);
Ok(())
}
pub fn take_args(vm: &mut VM, argc: u8) -> (usize, Vec<String>) {
let mut values = Vec::with_capacity(argc as usize);
for _ in 0..argc {
values.push(vm.pop());
}
values.reverse();
let line = match values.first() {
Some(Value::Int(n)) => *n as usize,
_ => 0,
};
(line, values[1..].iter().map(to_tcl_string).collect())
}
#[cfg(test)]
mod tests {
use super::*;
struct NoScripts;
impl ScriptHost for NoScripts {
fn eval(&mut self, _src: &str) -> Result<String, TclError> {
panic!("these tests do not run a script")
}
}
fn ok(argv: &[&str]) -> String {
let argv: Vec<String> = argv.iter().map(|s| (*s).to_string()).collect();
run(&argv, &mut NoScripts).expect("should succeed")
}
fn err(argv: &[&str]) -> String {
let argv: Vec<String> = argv.iter().map(|s| (*s).to_string()).collect();
run(&argv, &mut NoScripts).expect_err("should fail").msg
}
#[test]
fn versions_compare_by_value_and_not_by_spelling() {
assert_eq!(compare("9.0", "9.0.0"), Some(0));
assert_eq!(compare("010", "10"), Some(0));
assert_eq!(compare("1.2", "1.2.0.0"), Some(0));
assert_eq!(compare("1.2", "1.3"), Some(-1));
assert_eq!(compare("1.3", "1.2"), Some(1));
assert_eq!(compare("1.2a3", "1.2"), Some(-1));
assert_eq!(compare("1.2b1", "1.2"), Some(-1));
assert_eq!(compare("1.0+abc", "1.0"), Some(0));
assert_eq!(
compare("1.99999999999999999999", "1.99999999999999999998"),
Some(1)
);
assert_eq!(compare("bogus", "1"), None);
}
#[test]
fn a_requirement_admits_a_later_minor_and_refuses_a_later_major() {
assert_eq!(ok(&["package", "vsatisfies", "1.2", "1.0"]), "1");
assert_eq!(ok(&["package", "vsatisfies", "2.0", "1.0"]), "0");
assert_eq!(ok(&["package", "vsatisfies", "1.2", "1.0-2.0"]), "1");
assert_eq!(ok(&["package", "vsatisfies", "1.0", "1.0-1.0"]), "1");
assert_eq!(
err(&["package", "vsatisfies", "1.2", "bogus"]),
"expected version number but got \"bogus\""
);
}
#[test]
fn the_refusals_are_worded_as_tclsh_words_them() {
assert_eq!(
err(&["package"]),
"wrong # args: should be \"package option ?arg ...?\""
);
assert_eq!(
err(&["package", "bogus"]),
"bad option \"bogus\": must be files, forget, ifneeded, names, prefer, \
present, provide, require, unknown, vcompare, versions, or vsatisfies"
);
assert_eq!(
err(&["package", "require"]),
"wrong # args: should be \"package require ?-exact? package ?requirement ...?\""
);
assert_eq!(
err(&["package", "present"]),
"wrong # args: should be \"package present ?-exact? package ?requirement ...?\""
);
assert_eq!(
err(&["package", "names", "extra"]),
"wrong # args: should be \"package names\""
);
assert_eq!(
err(&["package", "vcompare", "1"]),
"wrong # args: should be \"package vcompare version1 version2\""
);
assert_eq!(ok(&["package", "prov", "nothing-provided-under-this"]), "");
}
#[test]
fn an_exact_requirement_is_reported_as_exactly() {
let mut msg = String::new();
add_requirements_to_result(&mut msg, &["1.3-1.3".to_string()]);
assert_eq!(msg, " exactly 1.3");
let mut msg = String::new();
add_requirements_to_result(&mut msg, &["1.0-2.0".to_string()]);
assert_eq!(msg, " 1.0-2.0");
}
}