use crate::PRELUDE_REGISTRY_KEY;
use anyhow::Context;
use mlua::{Function, Lua, Table, Value};
use mlua_pkg::Resolver;
use mlua_pkg::sandbox::{FsSandbox, InitError, ReadError, SandboxedFs, SymlinkAwareSandbox};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
pub use mlua_pkg;
pub struct TealResolver {
sandbox: Box<dyn SandboxedFs>,
root: Option<PathBuf>,
path_added: AtomicBool,
module_separator: char,
expect_type: Option<String>,
require_fields: crate::config::RequireFields,
checker_paths: Vec<PathBuf>,
exclude: Vec<String>,
only_module: Option<String>,
}
impl TealResolver {
pub fn new(root: impl Into<PathBuf>) -> Result<Self, InitError> {
let root = root.into();
Ok(Self {
sandbox: Box::new(FsSandbox::new(&root)?),
root: Some(root),
path_added: AtomicBool::new(false),
module_separator: '.',
expect_type: None,
require_fields: Default::default(),
checker_paths: Vec::new(),
exclude: Vec::new(),
only_module: None,
})
}
pub fn new_symlink_aware(root: impl Into<PathBuf>) -> Result<Self, InitError> {
let root = root.into();
Ok(Self {
sandbox: Box::new(SymlinkAwareSandbox::new(&root)?),
root: Some(root),
path_added: AtomicBool::new(false),
module_separator: '.',
expect_type: None,
require_fields: Default::default(),
checker_paths: Vec::new(),
exclude: Vec::new(),
only_module: None,
})
}
pub fn with_sandbox(sandbox: impl SandboxedFs + 'static, root: Option<PathBuf>) -> Self {
Self {
sandbox: Box::new(sandbox),
root,
path_added: AtomicBool::new(false),
module_separator: '.',
expect_type: None,
require_fields: Default::default(),
checker_paths: Vec::new(),
exclude: Vec::new(),
only_module: None,
}
}
pub fn with_module_separator(mut self, sep: char) -> Self {
self.module_separator = sep;
self
}
pub fn expect_type(mut self, type_path: impl Into<String>) -> Self {
self.expect_type = Some(type_path.into());
self
}
pub fn require_fields(mut self, names: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.require_fields =
crate::config::RequireFields::Named(names.into_iter().map(Into::into).collect());
self
}
pub fn require_all_fields(mut self) -> Self {
self.require_fields = crate::config::RequireFields::All(true);
self
}
pub fn with_checker_path(mut self, dir: impl Into<PathBuf>) -> Self {
self.checker_paths.push(dir.into());
self
}
pub fn exclude_modules(mut self, names: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.exclude.extend(names.into_iter().map(Into::into));
self
}
pub fn only_module(mut self, name: impl Into<String>) -> Self {
self.only_module = Some(name.into());
self
}
fn held(&self, name: &str) -> bool {
if self.expect_type.is_none() || self.exclude.iter().any(|e| e == name) {
return false;
}
self.only_module.as_deref().is_none_or(|m| m == name)
}
pub fn for_contract(
root: &Path,
cfg: &crate::config::HtlConfig,
c: &crate::contract::Resolved,
) -> Result<Vec<Self>, InitError> {
c.dirs(root)
.into_iter()
.map(|d| Self::for_contract_dir(root, &d, cfg, c))
.collect()
}
pub fn for_contract_dir(
root: &Path,
dir: &Path,
cfg: &crate::config::HtlConfig,
c: &crate::contract::Resolved,
) -> Result<Self, InitError> {
let mut r = Self::new_symlink_aware(dir)?
.expect_type(c.type_path.clone())
.exclude_modules(c.exclude.iter().cloned());
for p in cfg.search_paths(root) {
r = r.with_checker_path(p);
}
if let Some(m) = &c.module {
r = r.only_module(m.clone());
}
r.require_fields = c.require_fields.clone();
Ok(r)
}
fn missing_fields(&self, h: &Table, value: &Value) -> mlua::Result<Vec<String>> {
let Some(tp) = &self.expect_type else {
return Ok(Vec::new());
};
if !self.require_fields.is_on() {
return Ok(Vec::new());
}
let f: Function = h.get("record_fields")?;
let declared: Option<Vec<String>> = f
.call::<Option<Table>>(tp.as_str())?
.map(|t| t.sequence_values::<String>().collect::<mlua::Result<_>>())
.transpose()?;
let Some(declared) = declared else {
return Err(mlua::Error::external(format!(
"TealResolver::require_fields: record type {tp:?} not found by the checker"
)));
};
let names = match self.require_fields.named() {
None => declared,
Some(wanted) => {
let unknown: Vec<&str> = wanted
.iter()
.filter(|w| !declared.iter().any(|d| d == *w))
.map(|w| w.as_str())
.collect();
if !unknown.is_empty() {
return Err(mlua::Error::external(format!(
"TealResolver::require_fields names field(s) that {tp} does not declare: {}",
unknown.join(", ")
)));
}
wanted.to_vec()
}
};
let Value::Table(t) = value else {
return Ok(names); };
let mut missing = Vec::new();
for n in names {
if matches!(t.get::<Value>(n.as_str())?, Value::Nil) {
missing.push(n);
}
}
Ok(missing)
}
fn expectation_errors(&self, h: &Table, name: &str) -> mlua::Result<Option<Vec<String>>> {
let Some(tp) = &self.expect_type else {
return Ok(None);
};
let (module, _) = tp.split_once('.').ok_or_else(|| {
mlua::Error::external(format!(
"TealResolver::expect_type: expected \"<module>.<Type>\", got {tp:?}"
))
})?;
if name == module {
return Ok(None);
}
let stub = format!(
"local {module} = require(\"{module}\")\nlocal m: {tp} = require(\"{name}\")\nreturn m\n"
);
let check: Function = h.get("check_stub")?;
let errors: Table =
check.call((stub.as_str(), format!("<expect {tp} for module '{name}'>")))?;
let msgs: Vec<String> = errors
.sequence_values::<String>()
.collect::<mlua::Result<_>>()?;
Ok(if msgs.is_empty() { None } else { Some(msgs) })
}
fn prelude(lua: &Lua) -> mlua::Result<Table> {
if let Ok(t) = lua.named_registry_value::<Table>(PRELUDE_REGISTRY_KEY) {
return Ok(t);
}
if let Some(c) = lua.app_data_ref::<crate::CheckerHandle>() {
return Ok(c.0.clone());
}
Err(mlua::Error::external(
"htl::pkg::TealResolver: this Lua has no htl prelude (create it with Htl::new / Htl::from_lua)",
))
}
fn ensure_checker_path(&self, lua: &Lua, h: &Table) -> mlua::Result<()> {
if self.path_added.swap(true, Ordering::Relaxed) {
return Ok(());
}
let f: Function = h.get("add_path")?;
for p in self.checker_paths.iter().rev() {
if p.is_dir() {
f.call::<()>(p.to_string_lossy().as_ref())?;
}
}
if let Some(root) = &self.root {
f.call::<()>(root.to_string_lossy().as_ref())?;
}
let _ = lua;
Ok(())
}
fn has_lua_sibling(&self, relative: &str) -> bool {
for cand in [format!("{relative}.lua"), format!("{relative}/init.lua")] {
if let Ok(Some(_)) = self.sandbox.read(Path::new(&cand)) {
return true;
}
}
false
}
fn load_teal(
&self,
lua: &Lua,
h: &Table,
src: &str,
resolved: &Path,
name: &str,
) -> mlua::Result<Value> {
let gen_fn: Function = h.get("gen_string")?;
let (code, info): (Option<String>, Table) =
gen_fn.call((src, resolved.to_string_lossy().as_ref()))?;
let Some(code) = code else {
let errors: Table = info.get("errors")?;
let msgs: Vec<String> = errors
.sequence_values::<String>()
.collect::<mlua::Result<_>>()?;
return Err(mlua::Error::external(TealResolveError::TypeCheck {
module: name.to_string(),
errors: msgs,
}));
};
if self.held(name)
&& let Some(errs) = self.expectation_errors(h, name)?
{
return Err(mlua::Error::external(TealResolveError::Expectation {
module: name.to_string(),
expected: self.expect_type.clone().unwrap_or_default(),
errors: errs,
}));
}
let chunk = lua
.load(code)
.set_name(format!("@{}", resolved.display()))
.into_function()?;
chunk.call::<Value>((name, resolved.to_string_lossy().as_ref()))
}
}
#[derive(Debug, Clone)]
pub struct Project {
pub root: PathBuf,
pub manifest: PathBuf,
pub lockfile: PathBuf,
pub pkgs_dir: PathBuf,
pub vendored: PathBuf,
pub entries: PathBuf,
pub target_dirs: Vec<PathBuf>,
pub vendored_copies: Vec<PathBuf>,
pub patches: Vec<Patched>,
}
#[derive(Debug, Clone)]
pub struct Patched {
pub name: String,
pub dir: PathBuf,
pub entry: PathBuf,
}
impl Patched {
pub fn search_dir(&self) -> PathBuf {
match (self.entry.file_name(), self.entry.parent()) {
(Some(f), Some(up)) if f == std::ffi::OsStr::new(&self.name) => up.to_path_buf(),
_ => self.entry.clone(),
}
}
}
fn patch_entry(dir: &Path, over: Option<&Path>) -> PathBuf {
if let Some(e) = over {
return mlua_pkg::lockfile::join_entry(dir, e);
}
if let Ok(m) = mlua_pkg::manifest::Manifest::from_path(dir.join(MANIFEST_NAME))
&& let Some(e) = m.package.entry
{
return mlua_pkg::lockfile::join_entry(dir, &e);
}
mlua_pkg::resolve_entry(dir, None).unwrap_or_else(|_| dir.to_path_buf())
}
#[derive(Debug, Clone)]
pub struct AddDone {
pub report: mlua_pkg::ops::AddReport,
pub kept_patch_dir: Option<PathBuf>,
}
#[derive(Debug, Clone)]
pub struct PatchDone {
pub report: mlua_pkg::ops::PatchReport,
pub dropped: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct PatchStatus {
pub name: String,
pub dir: PathBuf,
pub base: Option<String>,
pub locked: Option<String>,
pub in_use: bool,
}
pub const MANIFEST_NAME: &str = mlua_pkg::project::MANIFEST_FILE_NAME;
pub const LOCKFILE_NAME: &str = mlua_pkg::project::LOCKFILE_FILE_NAME;
pub const PATCHES_DIR: &str = "patches";
pub fn pkgs_dir(root: &Path) -> mlua_pkg::PkgDir {
mlua_pkg::PkgDir::new(root.join(".htl").join("modules"))
}
pub const ENTRIES_DIR: &str = "entries";
pub(crate) fn owning_project(dir: &Path) -> Option<PathBuf> {
let mut up = dir.to_path_buf();
while up.pop() {
if up.join(MANIFEST_NAME).is_file() && Project::at(&up).declares(dir) {
return Some(owning_project(&up).unwrap_or(up));
}
}
None
}
impl Project {
pub fn find(start: &Path) -> Option<Self> {
let mut dir = if start.is_dir() {
start.to_path_buf()
} else {
crate::parent_dir(start)
};
if let Ok(abs) = std::fs::canonicalize(&dir) {
dir = abs;
}
loop {
let manifest = dir.join(MANIFEST_NAME);
if manifest.is_file() {
let root = owning_project(&dir).unwrap_or(dir);
return Some(Self::at(&root));
}
if !dir.pop() {
return None;
}
}
}
fn declares(&self, dir: &Path) -> bool {
self.patches
.iter()
.map(|p| p.dir.clone())
.chain(self.vendored_copies.iter().cloned())
.any(|d| dir.starts_with(std::fs::canonicalize(&d).unwrap_or(d)))
}
pub fn at(root: &Path) -> Self {
let inner = mlua_pkg::Project::in_dir(root, pkgs_dir(root));
let manifest = inner.manifest_path().to_path_buf();
let mut target_dirs: Vec<PathBuf> = Vec::new();
let mut vendored_copies: Vec<PathBuf> = Vec::new();
let mut patches: Vec<Patched> = Vec::new();
if let Ok(m) = mlua_pkg::manifest::Manifest::from_path(&manifest) {
let locked = m
.deps
.values()
.any(|d| d.patch_dir.is_some())
.then(|| mlua_pkg::lockfile::Lockfile::read(inner.lock_path()).ok())
.flatten();
for (name, dep) in &m.deps {
if let Some(td) = &dep.target_dir {
let abs = root.join(td);
let parent = abs
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| root.to_path_buf());
if !target_dirs.contains(&parent) {
target_dirs.push(parent);
}
if !vendored_copies.contains(&abs) {
vendored_copies.push(abs);
}
}
if let Some(pd) = &dep.patch_dir {
let dir = root.join(pd);
let over = locked
.as_ref()
.and_then(|l| l.pkg.iter().find(|p| &p.name == name))
.map(|p| p.entry.clone())
.or_else(|| dep.entry.clone());
let entry = patch_entry(&dir, over.as_deref());
patches.push(Patched {
name: name.clone(),
dir,
entry,
});
}
}
}
Self {
root: root.to_path_buf(),
manifest,
lockfile: inner.lock_path().to_path_buf(),
vendored: inner.pkg_dir().vendored(),
entries: inner.pkg_dir().base().join(ENTRIES_DIR),
pkgs_dir: inner.pkg_dir().base().to_path_buf(),
target_dirs,
vendored_copies,
patches,
}
}
pub fn patch_dirs(&self) -> Vec<PathBuf> {
self.patches.iter().map(|p| p.dir.clone()).collect()
}
pub fn patch_search_dirs(&self) -> Vec<PathBuf> {
self.patches.iter().map(Patched::search_dir).collect()
}
pub fn installed(&self) -> bool {
self.lockfile.is_file()
}
pub fn teal_resolver(&self) -> Result<TealResolver, InitError> {
let _ = self.link_entries();
if crate::cache::scratch_root(&self.root).is_none() {
let _ = std::fs::create_dir_all(&self.entries);
}
TealResolver::new_symlink_aware(&self.entries)
}
pub fn link_entries(&self) -> anyhow::Result<Vec<String>> {
if !self.installed() {
return Ok(Vec::new());
}
let lock = mlua_pkg::lockfile::Lockfile::read(&self.lockfile)?;
if crate::cache::scratch_root(&self.root).is_some() {
return Ok(lock.pkg.iter().map(|p| p.name.clone()).collect());
}
std::fs::create_dir_all(&self.entries)
.with_context(|| format!("creating {}", self.entries.display()))?;
let mut names = Vec::new();
for p in &lock.pkg {
let mut target = PathBuf::from("..").join("vendored").join(&p.name);
if !(p.entry.as_os_str().is_empty() || p.entry == Path::new(".")) {
target.push(&p.entry);
}
let link = self.entries.join(&p.name);
match std::fs::symlink_metadata(&link) {
Ok(m) if m.file_type().is_symlink() => {
if std::fs::read_link(&link).ok().as_deref() == Some(target.as_path()) {
names.push(p.name.clone());
continue;
}
remove_link(&link)?;
}
Ok(_) => anyhow::bail!(
"{} is not a link: htl writes that directory from the lockfile, and \
something else put a file there",
link.display()
),
Err(_) => {}
}
make_link(&target, &link)?;
names.push(p.name.clone());
}
if let Ok(rd) = std::fs::read_dir(&self.entries) {
for e in rd.flatten() {
let is_link = e.file_type().map(|t| t.is_symlink()).unwrap_or(false);
let name = e.file_name().to_string_lossy().into_owned();
if is_link && !names.contains(&name) {
remove_link(&e.path())?;
}
}
}
Ok(names)
}
pub fn vendored_resolver(&self) -> anyhow::Result<mlua_pkg::resolvers::VendoredResolver> {
if self.installed() {
Ok(mlua_pkg::resolvers::VendoredResolver::from_lockfile(
&self.lockfile,
&self.vendored,
)?)
} else {
let _ = std::fs::create_dir_all(&self.vendored);
Ok(mlua_pkg::resolvers::VendoredResolver::new(&self.vendored)?)
}
}
pub fn registry(&self) -> anyhow::Result<mlua_pkg::Registry> {
let mut reg = mlua_pkg::Registry::new();
reg.add(self.teal_resolver()?);
reg.add(self.vendored_resolver()?);
for d in &self.target_dirs {
if d.is_dir() {
reg.add(TealResolver::new(d)?);
reg.add(mlua_pkg::resolvers::FsResolver::new(d)?);
}
}
Ok(reg)
}
pub fn sync_types(&self) -> anyhow::Result<TypesSync> {
let mut out = TypesSync::default();
if !self.installed() {
return Ok(out);
}
let lock = mlua_pkg::lockfile::Lockfile::read(&self.lockfile)?;
let dest = self.root.join("types");
for p in &lock.pkg {
let Some(root) = self.package_root(p) else {
continue;
};
copy_declarations(
&root.join("types"),
&dest,
&Origin {
name: p.name.clone(),
sha: p.sha.clone(),
under: PathBuf::from("types"),
},
false,
&mut out,
)?;
}
Ok(out)
}
pub fn add_types(&self, library: &str, force: bool) -> anyhow::Result<TypesSync> {
let cache = pkgs_dir(&self.root).cache();
std::fs::create_dir_all(&cache)?;
let fetcher = mlua_pkg::fetcher::GitFetcher::new(cache);
let got = mlua_pkg::fetcher::Fetcher::fetch(
&fetcher,
&mlua_pkg::manifest::Dep {
git: TEAL_TYPES_GIT.to_string(),
tag: None,
rev: None,
branch: None,
entry: None,
target_dir: None,
patch_dir: None,
patch_drift: None,
},
)?;
self.add_types_from(&got.cache_path, library, &got.sha, force)
}
pub fn add_types_from(
&self,
checkout: &Path,
library: &str,
sha: &str,
force: bool,
) -> anyhow::Result<TypesSync> {
let under = Path::new("types").join(library);
let published = checkout.join(&under);
if !published.is_dir() {
anyhow::bail!("{}", no_such_library(checkout, library));
}
let mut out = TypesSync::default();
copy_declarations(
&published,
&self.root.join("types"),
&Origin {
name: TEAL_TYPES_NAME.to_string(),
sha: sha.to_string(),
under,
},
force,
&mut out,
)?;
Ok(out)
}
fn config(&self) -> mlua_pkg::Config {
mlua_pkg::Config::new(mlua_pkg::Project::in_dir(&self.root, pkgs_dir(&self.root)))
}
pub fn install(&self) -> anyhow::Result<mlua_pkg::ops::InstallReport> {
let report = mlua_pkg::ops::install(&self.config())?;
self.link_entries()?;
Ok(report)
}
pub fn add(&self, spec: mlua_pkg::ops::AddSpec) -> anyhow::Result<AddDone> {
let name = spec.name.clone();
let previous = mlua_pkg::manifest::Manifest::from_path(&self.manifest)
.ok()
.and_then(|m| m.deps.get(&name).cloned());
let report = mlua_pkg::ops::add(&self.config(), spec)?;
let Some(dep) = previous else {
return Ok(AddDone {
report,
kept_patch_dir: None,
});
};
let Some(dir) = dep.patch_dir.clone() else {
return Ok(AddDone {
report,
kept_patch_dir: None,
});
};
set_dep_key(&self.manifest, &name, "patch_dir", &to_toml_path(&dir))?;
if let Some(drift) = dep.patch_drift {
let value = match drift {
mlua_pkg::manifest::PatchDrift::Warn => "warn",
mlua_pkg::manifest::PatchDrift::Error => "error",
};
set_dep_key(&self.manifest, &name, "patch_drift", value)?;
}
Ok(AddDone {
report,
kept_patch_dir: Some(dir),
})
}
pub fn update(
&self,
opts: mlua_pkg::ops::UpdateOpts,
) -> anyhow::Result<mlua_pkg::ops::UpdateReport> {
let mut report = mlua_pkg::ops::update(&self.config(), opts)?;
self.link_entries()?;
report.entries.sort_by(|a, b| a.0.cmp(&b.0));
Ok(report)
}
pub fn clean(&self, all: bool) -> anyhow::Result<mlua_pkg::ops::CleanReport> {
Ok(mlua_pkg::ops::clean(&self.config(), all)?)
}
pub fn patch(&self, name: &str, force: bool) -> anyhow::Result<PatchDone> {
let manifest = mlua_pkg::manifest::Manifest::from_path(&self.manifest)?;
let dep = manifest.deps.get(name).ok_or_else(|| {
anyhow::anyhow!(
"no dependency '{name}' in {}: `htl pkg patch` takes a name the manifest declares",
self.manifest.display()
)
})?;
let declared = dep.patch_dir.is_some();
let rel = match &dep.patch_dir {
Some(p) => p.clone(),
None => PathBuf::from(format!("{PATCHES_DIR}/{name}")),
};
let dir = self.root.join(&rel);
if dir.exists() && !force {
refuse_if_uncommitted(&self.root, &rel)?;
}
let before = std::fs::read_to_string(&self.manifest)?;
if !declared {
set_dep_key(&self.manifest, name, "patch_dir", &to_toml_path(&rel))?;
}
let opts = mlua_pkg::ops::PatchOpts {
name: name.to_string(),
force: true,
};
match mlua_pkg::ops::patch(&self.config(), opts) {
Ok(report) => {
let dropped = drop_dot_entries(&report.patch_dir)?;
Ok(PatchDone { report, dropped })
}
Err(e) => {
if !declared {
let _ = std::fs::write(&self.manifest, &before);
}
Err(e.into())
}
}
}
pub fn patch_status(&self) -> Vec<PatchStatus> {
let lock = mlua_pkg::lockfile::Lockfile::read(&self.lockfile).ok();
self.patches
.iter()
.map(|p| {
let locked = lock
.as_ref()
.and_then(|l| l.pkg.iter().find(|e| e.name == p.name));
let base = locked.and_then(|e| e.patch_base.clone());
let sha = locked.map(|e| e.sha.clone());
let in_use = p.dir.is_dir() && base.is_some() && base == sha;
PatchStatus {
name: p.name.clone(),
dir: p.dir.clone(),
base,
locked: sha,
in_use,
}
})
.collect()
}
fn package_root(&self, p: &mlua_pkg::lockfile::LockedPkg) -> Option<PathBuf> {
std::fs::canonicalize(self.vendored.join(&p.name)).ok()
}
}
fn make_link(target: &Path, link: &Path) -> anyhow::Result<()> {
#[cfg(unix)]
let r = std::os::unix::fs::symlink(target, link);
#[cfg(windows)]
let r = std::os::windows::fs::symlink_dir(target, link);
r.with_context(|| format!("linking {} -> {}", link.display(), target.display()))
}
fn remove_link(link: &Path) -> anyhow::Result<()> {
#[cfg(unix)]
let r = std::fs::remove_file(link);
#[cfg(windows)]
let r = std::fs::remove_dir(link).or_else(|_| std::fs::remove_file(link));
r.with_context(|| format!("removing the link {}", link.display()))
}
fn set_dep_key(manifest: &Path, name: &str, key: &str, value: &str) -> anyhow::Result<()> {
let text = std::fs::read_to_string(manifest)?;
let mut doc = text.parse::<toml_edit::DocumentMut>()?;
let deps = doc
.get_mut("deps")
.and_then(|i| i.as_table_like_mut())
.with_context(|| format!("no [deps] table in {}", manifest.display()))?;
let entry = deps
.get_mut(name)
.and_then(|i| i.as_table_like_mut())
.with_context(|| format!("[deps.{name}] is not a table"))?;
entry.insert(key, toml_edit::value(value));
std::fs::write(manifest, doc.to_string())?;
Ok(())
}
fn to_toml_path(p: &Path) -> String {
p.components()
.map(|c| c.as_os_str().to_string_lossy())
.collect::<Vec<_>>()
.join("/")
}
fn drop_dot_entries(dir: &Path) -> anyhow::Result<Vec<String>> {
let mut dropped = Vec::new();
let entries = std::fs::read_dir(dir).with_context(|| format!("reading {}", dir.display()))?;
for entry in entries {
let entry = entry.with_context(|| format!("reading {}", dir.display()))?;
let name = entry.file_name().to_string_lossy().into_owned();
if !name.starts_with('.') {
continue;
}
let path = entry.path();
let ft = entry
.file_type()
.with_context(|| format!("reading {}", path.display()))?;
if ft.is_dir() {
std::fs::remove_dir_all(&path)
} else {
std::fs::remove_file(&path)
}
.with_context(|| format!("removing {}", path.display()))?;
dropped.push(name);
}
dropped.sort();
Ok(dropped)
}
fn refuse_if_uncommitted(root: &Path, rel: &Path) -> anyhow::Result<()> {
match uncommitted(root, rel) {
Ok(changes) if changes.is_empty() => Ok(()),
Ok(changes) => {
let mut msg = format!(
"{} has uncommitted changes, and refreshing it from the pin overwrites \
them. Commit them first — git is what carries them onto the refreshed \
copy — or pass --force to discard them:",
rel.display()
);
for c in changes.iter().take(10) {
msg.push_str("\n ");
msg.push_str(c);
}
if changes.len() > 10 {
msg.push_str(&format!("\n and {} more", changes.len() - 10));
}
anyhow::bail!("{msg}")
}
Err(why) => anyhow::bail!(
"cannot tell whether {} has uncommitted changes ({why}), and refreshing it \
from the pin overwrites whatever is in it. Pass --force to refresh it anyway.",
rel.display()
),
}
}
fn uncommitted(root: &Path, rel: &Path) -> Result<Vec<String>, String> {
let out = std::process::Command::new("git")
.arg("-C")
.arg(root)
.args(["status", "--porcelain", "--"])
.arg(rel)
.output()
.map_err(|e| match e.kind() {
std::io::ErrorKind::NotFound => "no `git` on PATH".to_string(),
_ => e.to_string(),
})?;
if !out.status.success() {
let why = String::from_utf8_lossy(&out.stderr).trim().to_string();
return Err(if why.is_empty() {
format!("git exited {}", out.status)
} else {
why
});
}
Ok(String::from_utf8_lossy(&out.stdout)
.lines()
.map(|l| l.trim_end().to_string())
.collect())
}
pub const TEAL_TYPES_GIT: &str = "https://github.com/teal-language/teal-types";
const TEAL_TYPES_NAME: &str = "teal-types";
#[derive(Debug, Default)]
pub struct TypesSync {
pub written: Vec<(PathBuf, String)>,
pub taken: Vec<(PathBuf, String)>,
}
struct Origin {
name: String,
sha: String,
under: PathBuf,
}
fn copy_declarations(
from: &Path,
to: &Path,
origin: &Origin,
force: bool,
out: &mut TypesSync,
) -> anyhow::Result<()> {
if !from.is_dir() {
return Ok(());
}
let mut found: Vec<PathBuf> = walkdir::WalkDir::new(from)
.into_iter()
.filter_map(Result::ok)
.filter(|e| e.file_type().is_file())
.map(walkdir::DirEntry::into_path)
.filter(|p| crate::is_declaration(p))
.collect();
found.sort();
for src in found {
let rel = src.strip_prefix(from).unwrap_or(&src).to_path_buf();
let target = to.join(&rel);
if target.exists() && !force {
out.taken.push((target, origin.name.clone()));
continue;
}
if let Some(parent) = target.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::copy(&src, &target)?;
let mut note = target.clone().into_os_string();
note.push(".src");
std::fs::write(
PathBuf::from(note),
format!(
"{} {} {}\n",
origin.name,
origin.sha,
origin.under.join(&rel).display()
),
)?;
out.written.push((target, origin.name.clone()));
}
Ok(())
}
fn no_such_library(checkout: &Path, library: &str) -> String {
let mut names: Vec<String> = std::fs::read_dir(checkout.join("types"))
.into_iter()
.flatten()
.filter_map(|e| e.ok())
.filter(|e| e.path().is_dir())
.map(|e| e.file_name().to_string_lossy().into_owned())
.collect();
names.sort();
let near: Vec<&str> = names
.iter()
.filter(|n| n.contains(library) || library.contains(n.as_str()))
.map(String::as_str)
.collect();
if near.is_empty() {
format!(
"teal-types has no declarations for `{library}` ({} libraries there)",
names.len()
)
} else {
format!(
"teal-types has no declarations for `{library}` — it has {}",
near.join(", ")
)
}
}
pub fn contract_resolvers(
root: &Path,
cfg: &crate::config::HtlConfig,
) -> Result<Vec<TealResolver>, InitError> {
let (contracts, _) = crate::contract::resolve(root, cfg);
let mut out = Vec::new();
for c in &contracts {
out.extend(TealResolver::for_contract(root, cfg, c)?);
}
Ok(out)
}
impl crate::Htl {
pub fn apply_project(&self, p: &Project) -> anyhow::Result<()> {
let installed = p.link_entries()?;
self.set_deps(&installed)?;
for d in p.patch_search_dirs() {
self.add_path(&d)?;
}
if crate::cache::scratch_root(&p.root).is_none() {
let _ = std::fs::create_dir_all(&p.entries);
}
self.add_path(&p.entries)?;
for d in &p.target_dirs {
self.add_path(d)?;
}
let src = p.root.join("src");
if src.is_dir() {
self.add_path(&src)?;
}
Ok(())
}
}
#[derive(Debug)]
pub enum TealResolveError {
TypeCheck {
module: String,
errors: Vec<String>,
},
Expectation {
module: String,
expected: String,
errors: Vec<String>,
},
MissingFields {
module: String,
expected: String,
fields: Vec<String>,
},
Read {
module: String,
source: ReadError,
},
}
impl std::fmt::Display for TealResolveError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::TypeCheck { module, errors } => {
write!(f, "Teal type check failed for module '{module}':")?;
for e in errors {
write!(f, "\n {e}")?;
}
Ok(())
}
Self::Expectation {
module,
expected,
errors,
} => {
write!(f, "module '{module}' does not satisfy {expected}:")?;
for e in errors {
write!(f, "\n {e}")?;
}
write!(
f,
"\n hint: annotate the returned table in the module (`local m: {expected} = {{ ... }} return m`) \
to get field-level errors with line numbers"
)
}
Self::MissingFields {
module,
expected,
fields,
} => write!(
f,
"module '{module}' is missing required field(s) of {expected}: {} (every field of that record must be non-nil)",
fields.join(", ")
),
Self::Read { module, source } => write!(f, "reading module '{module}': {source}"),
}
}
}
impl std::error::Error for TealResolveError {}
fn preloaded(lua: &Lua, name: &str) -> mlua::Result<bool> {
let package: Table = lua.globals().get("package")?;
let preload: Table = package.get("preload")?;
Ok(!matches!(preload.get::<Value>(name)?, Value::Nil))
}
impl Resolver for TealResolver {
fn resolve(&self, lua: &Lua, name: &str) -> Option<mlua::Result<Value>> {
let relative = name.replace(self.module_separator, "/");
let last = relative.rsplit('/').next().unwrap_or(&relative).to_string();
let candidates = [
(format!("{relative}.tl"), false),
(format!("{relative}/init.tl"), false),
(format!("{relative}/{last}.tl"), false),
(format!("{relative}.d.tl"), true),
];
let h = match Self::prelude(lua) {
Ok(h) => h,
Err(e) => return Some(Err(e)),
};
if let Err(e) = self.ensure_checker_path(lua, &h) {
return Some(Err(e));
}
for (candidate, type_only) in &candidates {
match self.sandbox.read(Path::new(candidate)) {
Ok(Some(file)) => {
if *type_only {
if self.has_lua_sibling(&relative) {
return None;
}
match preloaded(lua, name) {
Ok(true) => return None,
Ok(false) => {}
Err(e) => return Some(Err(e)),
}
return Some(h.get::<Function>("type_only_module").and_then(|f| {
f.call::<Value>((name, file.resolved_path.to_string_lossy().as_ref()))
}));
}
let loaded =
match self.load_teal(lua, &h, &file.content, &file.resolved_path, name) {
Ok(v) => v,
Err(e) => return Some(Err(e)),
};
if !self.held(name) {
return Some(Ok(loaded));
}
match self.missing_fields(&h, &loaded) {
Ok(m) if m.is_empty() => return Some(Ok(loaded)),
Ok(missing) => {
return Some(Err(mlua::Error::external(
TealResolveError::MissingFields {
module: name.to_string(),
expected: self.expect_type.clone().unwrap_or_default(),
fields: missing,
},
)));
}
Err(e) => return Some(Err(e)),
}
}
Ok(None) => continue,
Err(source) => {
return Some(Err(mlua::Error::external(TealResolveError::Read {
module: name.to_string(),
source,
})));
}
}
}
None
}
}