use std::path::{Path, PathBuf};
use fusevm::{Op, Value};
use crate::compiler::{CompileError, Compiler};
use crate::parser::Word;
use crate::runtime::{to_tcl_string, TclError};
pub mod ext {
use crate::compiler::ext::NS_BASE;
pub const SOURCE: u16 = NS_BASE + 2;
pub const FIND_LIBRARY: u16 = NS_BASE + 3;
}
pub fn is_op(id: u16) -> bool {
id == ext::SOURCE || id == ext::FIND_LIBRARY
}
impl Compiler {
pub(crate) fn cmd_source(&mut self, args: &[Word]) -> Result<(), CompileError> {
let path = match args {
[path] => path,
[flag, encoding, path] if flag.as_literal() == Some("-encoding") => {
match encoding.as_literal() {
Some("utf-8") | Some("utf8") => path,
_ => {
return self.error(
"\"source -encoding\" is only supported for utf-8: this frontend \
reads a script as UTF-8",
)
}
}
}
[flag, ..] if flag.as_literal().is_some_and(|t| t != "-encoding") => {
let msg = format!(
"bad option \"{}\": must be -encoding",
flag.as_literal().unwrap_or_default()
);
return Err(self.deferrable_err(msg));
}
_ => {
return self
.error("wrong # args: should be \"source ?-encoding encoding? fileName\"")
}
};
self.word(path)?;
self.emit(Op::Extended(ext::SOURCE, 1), 0);
Ok(())
}
pub(crate) fn cmd_find_library(&mut self, args: &[Word]) -> Result<(), CompileError> {
if args.len() != 6 {
return self.error(
"wrong # args: should be \"tcl_findLibrary basename version patch initScript \
enVarName varName\"",
);
}
for w in args {
self.word(w)?;
}
self.emit(Op::Extended(ext::FIND_LIBRARY, 6), 1 - 6);
Ok(())
}
}
pub(crate) fn extension(
interp: &crate::runtime::Shared,
vm: &mut fusevm::VM,
id: u16,
argc: u8,
) -> Result<(), TclError> {
let mut values = Vec::with_capacity(argc as usize);
for _ in 0..argc {
values.push(vm.pop());
}
values.reverse();
let args: Vec<String> = values.iter().map(to_tcl_string).collect();
let result = crate::runtime::with_written_back(interp, vm, |interp| match id {
ext::SOURCE => source(interp, &args[0]),
_ => find_library(interp, &args).map(|_| String::new()),
})?;
vm.push(Value::Str(std::sync::Arc::new(result)));
Ok(())
}
fn posix_reason(e: &std::io::Error) -> String {
use std::io::ErrorKind::*;
match e.kind() {
NotFound => "no such file or directory".to_string(),
PermissionDenied => "permission denied".to_string(),
IsADirectory => "is a directory".to_string(),
_ => {
let text = e.to_string();
let text = text.split(" (os error").next().unwrap_or(&text).to_string();
let mut chars = text.chars();
match chars.next() {
Some(first) => first.to_lowercase().chain(chars).collect(),
None => text,
}
}
}
}
pub(crate) fn source(interp: &crate::runtime::Shared, path: &str) -> Result<String, TclError> {
let bytes = std::fs::read(path).map_err(|e| {
TclError::plain(format!(
"couldn't read file \"{path}\": {}",
posix_reason(&e)
))
})?;
let text = String::from_utf8(bytes).map_err(|_| {
TclError::plain(format!(
"couldn't read file \"{path}\": the file is not valid UTF-8"
))
})?;
crate::runtime::run_source(interp, &text).map(|v| to_tcl_string(&v))
}
fn join(parts: &[&str]) -> String {
let mut out = PathBuf::new();
for p in parts {
if p.is_empty() {
continue;
}
if p.starts_with('/') {
out = PathBuf::from(p);
} else {
out.push(p);
}
}
out.to_string_lossy().into_owned()
}
fn dirname(path: &str) -> String {
Path::new(path)
.parent()
.map(|p| p.to_string_lossy().into_owned())
.filter(|p| !p.is_empty())
.unwrap_or_else(|| ".".to_string())
}
fn normalize(path: &str) -> String {
std::fs::canonicalize(path)
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_else(|_| path.to_string())
}
pub(crate) fn find_library(
interp: &crate::runtime::Shared,
args: &[String],
) -> Result<(), TclError> {
let [basename, version, _patch, init_script, envar, varname] = args else {
return Err(TclError::plain(
"wrong # args: should be \"tcl_findLibrary basename version patch initScript \
enVarName varName\"",
));
};
let read = |name: &str| -> Option<String> {
let state = interp.lock().expect("interpreter lock");
state.globals.get(name).map(to_tcl_string)
};
let mut dirs: Vec<String> = Vec::new();
let hardwired = read(crate::cmd_namespace::store_key(
&crate::cmd_namespace::resolve("::", varname),
));
match hardwired {
Some(dir) if !dir.is_empty() => dirs.push(dir),
_ => {
if let Ok(dir) = std::env::var(envar) {
dirs.push(dir);
}
let auto_path = read("auto_path").unwrap_or_default();
let entries = crate::list::split(&auto_path).map_err(TclError::plain)?;
let darwin = cfg!(target_os = "macos");
for d in &entries {
dirs.push(join(&[d, &format!("{basename}{version}")]));
if darwin {
dirs.push(join(&[
d,
&format!("{basename}{version}"),
"Resources",
"Scripts",
]));
}
}
let exe = std::env::current_exe()
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_default();
let parent = dirname(&dirname(&exe));
let grandparent = dirname(&parent);
dirs.push(join(&[&parent, "lib", &format!("{basename}{version}")]));
dirs.push(join(&[
&grandparent,
"lib",
&format!("{basename}{version}"),
]));
dirs.push(join(&[&parent, "library"]));
}
}
let mut seen: Vec<String> = Vec::new();
let mut errors = String::new();
for dir in &dirs {
let norm = normalize(dir);
if seen.contains(&norm) {
continue;
}
seen.push(norm);
let file = join(&[dir, init_script]);
if !Path::new(&file).exists() {
continue;
}
{
let mut state = interp.lock().expect("interpreter lock");
state.globals.insert(
crate::cmd_namespace::store_key(&crate::cmd_namespace::resolve("::", varname))
.to_string(),
Value::Str(std::sync::Arc::new(dir.clone())),
);
}
match source(interp, &file) {
Ok(_) => return Ok(()),
Err(e) => errors.push_str(&format!("{file}: {}\n{}\n", e.msg, e.msg)),
}
}
{
let mut state = interp.lock().expect("interpreter lock");
state.globals.remove(crate::cmd_namespace::store_key(
&crate::cmd_namespace::resolve("::", varname),
));
}
Err(TclError::plain(format!(
"Can't find a usable {init_script} in the following directories: \n {}\n\n{errors}\n\n\
This probably means that {basename} wasn't installed properly.\n",
crate::list::join(&dirs)
)))
}
pub fn seed_library_environment(interp: &mut crate::runtime::Interp) {
let mut path: Vec<String> = Vec::new();
if let Ok(dir) = std::env::var("TCL_LIBRARY") {
if !dir.is_empty() {
path.push(dirname(&dir));
interp.set_global("tcl_library", dir);
}
}
if let Ok(exe) = std::env::current_exe() {
let exe = exe.to_string_lossy().into_owned();
let parent = dirname(&dirname(&exe));
let grandparent = dirname(&parent);
for base in [&parent, &grandparent] {
let lib = join(&[base, "lib"]);
if Path::new(&lib).is_dir() && !path.contains(&lib) {
path.push(lib);
}
}
}
if interp.global("tcl_library").is_none() {
for dir in &path {
let candidate = join(&[dir, "tcl9.0"]);
if Path::new(&join(&[&candidate, "init.tcl"])).exists() {
interp.set_global("tcl_library", candidate);
break;
}
}
}
let joined = crate::list::join(&path);
interp.set_global("tcl_libPath", joined.clone());
if interp.global("auto_path").is_none() {
interp.set_global("auto_path", joined);
}
}