///|
fn package_version(text : String) -> Array[Int] raise TclError {
let parts = text.split(".").to_array()
if parts.is_empty() || parts.length() > 32 {
raise Invalid("invalid package version")
}
let result = []
for part in parts {
if part.is_empty() || !part.iter().all(c => c >= '0' && c <= '9') {
raise Invalid("expected a released numeric package version")
}
result.push(integer(part.to_owned()))
}
result
}
///|
fn compare_versions(left : String, right : String) -> Int raise TclError {
let a = package_version(left)
let b = package_version(right)
for i in 0.. Bool raise TclError {
let actual = package_version(version)
let required = package_version(requirement)
actual[0] == required[0] && compare_versions(version, requirement) >= 0
}
///|
fn Interpreter::package_command(
self : Interpreter,
args : Array[TclValue],
depth : Int,
) -> TclValue raise TclError {
let values = args.map(v => v.text)
if values.length() < 2 {
raise Invalid("package requires a subcommand")
}
let n = values.length()
let value = match values[1] {
"vcompare" => {
if n != 4 {
raise Invalid("package vcompare version1 version2")
}
compare_versions(values[2], values[3]).to_string()
}
"vsatisfies" => {
if n < 4 {
raise Invalid("package vsatisfies version requirement")
}
let mut matched = false
for requirement in values[3:] {
if package_satisfies(values[2], requirement) {
matched = true
}
}
boolean_text(matched)
}
"provide" => {
if n != 3 && n != 4 {
raise Invalid("package provide name ?version?")
}
if n == 3 {
self.state.packages.get(values[2]).unwrap_or("")
} else {
ignore(package_version(values[3]))
if self.state.packages.get(values[2]) is Some(old) &&
compare_versions(old, values[3]) != 0 {
raise Invalid("conflicting package version")
}
self.state.packages[values[2]] = values[3]
""
}
}
"ifneeded" => {
if n != 4 && n != 5 {
raise Invalid("package ifneeded name version ?script?")
}
ignore(package_version(values[3]))
let versions = self.state.package_scripts.get_or_init(values[2], () => {
Map([])
})
if n == 4 {
versions.get(values[3]).unwrap_or("")
} else {
versions[values[3]] = values[4]
""
}
}
"names" => {
if n != 2 {
raise Invalid("package names")
}
let names = self.state.packages.keys().to_array()
for name in self.state.package_scripts.keys() {
if !names.contains(name) {
names.push(name)
}
}
format_list(names)
}
"versions" => {
if n != 3 {
raise Invalid("package versions name")
}
match self.state.package_scripts.get(values[2]) {
Some(versions) => format_list(versions.keys().to_array())
None => ""
}
}
"forget" => {
for name in values[2:] {
self.state.packages.remove(name)
self.state.package_scripts.remove(name)
}
""
}
"require" | "present" => {
let exact = n > 2 && values[2] == "-exact"
let start = if exact { 3 } else { 2 }
if n < start + 1 || n > start + 2 || (exact && n != start + 2) {
raise Invalid("package require ?-exact? name ?version?")
}
let name = values[start]
let requested = values.get(start + 1)
if requested is Some(version) {
ignore(package_version(version))
}
let compatible = fn(version) raise TclError {
match requested {
None => true
Some(required) =>
if exact {
compare_versions(version, required) == 0
} else {
package_satisfies(version, required)
}
}
}
if self.state.packages.get(name) is Some(version) {
if !compatible(version) {
raise Invalid("package version conflict")
}
return text_value(version)
}
if values[1] == "present" {
raise Invalid("package is not present")
}
if self.state.package_loading.contains(name) {
raise Invalid("circular package dependency")
}
let mut selected : (String, String)? = None
if self.state.package_scripts.get(name) is Some(versions) {
for version, script in versions {
if compatible(version) &&
(
selected is None ||
(selected is Some((old, _)) && compare_versions(version, old) > 0)
) {
selected = Some((version, script))
}
}
}
guard selected is Some((version, script)) else {
raise Invalid("cannot find package " + name)
}
self.state.package_loading.push(name)
defer ignore(self.state.package_loading.pop())
let global = {
..self,
frame: {
vars: Map([]),
namespace_name: "::",
procedure: false,
parent: None,
},
}
ignore(global.propagate_value(global.capture(script, depth + 1)))
guard self.state.packages.get(name) is Some(provided) else {
raise Invalid("package script did not provide " + name)
}
if compare_versions(provided, version) != 0 || !compatible(provided) {
raise Invalid("package script provided a different version")
}
provided
}
_ => raise Invalid("unsupported package subcommand")
}
text_value(value)
}