use std::collections::HashMap;
use std::path::Path;
use super::super::alc_toml::{self, load_alc_toml};
use super::super::lockfile::{load_lockfile, lockfile_path};
use super::super::manifest;
use super::super::project::resolve_project_root;
use super::super::resolve::is_system_package;
use super::super::source::{infer_from_legacy_source_string, PackageSource};
use super::super::AppService;
#[derive(Debug)]
enum Scope {
Project,
Global,
}
#[derive(Debug)]
struct PackageListEntry {
name: String,
scope: Scope,
source_type: Option<String>,
path: Option<String>,
source: Option<String>,
active: bool,
version: Option<String>,
installed_at: Option<String>,
updated_at: Option<String>,
install_source: Option<String>,
overrides: Option<Vec<String>>,
meta: serde_json::Value,
error: Option<String>,
linked: Option<bool>,
link_target: Option<String>,
broken: Option<bool>,
}
impl PackageListEntry {
fn into_json(self) -> serde_json::Value {
let scope_str = match self.scope {
Scope::Project => "project",
Scope::Global => "global",
};
let mut map = serde_json::Map::new();
map.insert("name".to_string(), serde_json::Value::String(self.name));
map.insert(
"scope".to_string(),
serde_json::Value::String(scope_str.to_string()),
);
if let Some(st) = self.source_type {
map.insert("source_type".to_string(), serde_json::Value::String(st));
}
if let Some(p) = self.path {
map.insert("path".to_string(), serde_json::Value::String(p));
}
if let Some(s) = self.source {
map.insert("source".to_string(), serde_json::Value::String(s));
}
map.insert("active".to_string(), serde_json::Value::Bool(self.active));
if let Some(v) = self.version {
map.insert("version".to_string(), serde_json::Value::String(v));
}
if let Some(ia) = self.installed_at {
map.insert("installed_at".to_string(), serde_json::Value::String(ia));
}
if let Some(ua) = self.updated_at {
map.insert("updated_at".to_string(), serde_json::Value::String(ua));
}
if let Some(is) = self.install_source {
map.insert("install_source".to_string(), serde_json::Value::String(is));
}
if let Some(ov) = self.overrides {
map.insert("overrides".to_string(), serde_json::json!(ov));
}
if let serde_json::Value::Object(meta_map) = self.meta {
for (k, v) in meta_map {
map.entry(k).or_insert(v);
}
}
if let Some(err) = self.error {
map.insert("error".to_string(), serde_json::Value::String(err));
}
if let Some(linked) = self.linked {
map.insert("linked".to_string(), serde_json::Value::Bool(linked));
}
if let Some(target) = self.link_target {
map.insert("link_target".to_string(), serde_json::Value::String(target));
}
if let Some(broken) = self.broken {
map.insert("broken".to_string(), serde_json::Value::Bool(broken));
}
serde_json::Value::Object(map)
}
}
impl AppService {
pub async fn pkg_list(&self, project_root: Option<String>) -> Result<String, String> {
let manifest_data = manifest::load_manifest().unwrap_or_default();
let resolved_root = resolve_project_root(project_root.as_deref());
let mut project_names: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut entries: Vec<PackageListEntry> = Vec::new();
let mut project_root_str: Option<String> = None;
let mut lockfile_path_str: Option<String> = None;
if let Some(ref root) = resolved_root {
project_root_str = Some(root.display().to_string());
lockfile_path_str = Some(lockfile_path(root).display().to_string());
let lock_map: HashMap<String, (Option<String>, PackageSource)> =
match load_lockfile(root) {
Ok(Some(lock)) => lock
.packages
.into_iter()
.map(|p| (p.name, (p.version, p.source)))
.collect(),
Ok(None) => HashMap::new(),
Err(e) => {
tracing::warn!("failed to load alc.lock: {e}");
HashMap::new()
}
};
match load_alc_toml(root) {
Ok(Some(alc_toml)) => {
for (name, dep) in &alc_toml.packages {
let (version, source_type, abs_path) =
resolve_project_pkg_info(name, dep, &lock_map, root);
project_names.insert(name.clone());
entries.push(make_project_entry(
name.clone(),
version,
source_type,
abs_path,
));
}
}
Ok(None) => {
collect_path_entries_from_lock(
&lock_map,
root,
&mut project_names,
&mut entries,
);
}
Err(e) => {
tracing::warn!("failed to load alc.toml: {e}");
}
}
}
let mut seen: HashMap<String, Vec<(usize, String)>> = HashMap::new();
let global_start_idx = entries.len();
for (idx, sp) in self.search_paths.iter().enumerate() {
if !sp.path.is_dir() {
continue;
}
let read_entries = match std::fs::read_dir(&sp.path) {
Ok(e) => e,
Err(_) => continue,
};
for dir_entry in read_entries.flatten() {
let path = dir_entry.path();
let is_symlink = path
.symlink_metadata()
.map(|m| m.file_type().is_symlink())
.unwrap_or(false);
let link_target = if is_symlink {
path.read_link().ok().map(|t| t.display().to_string())
} else {
None
};
let broken = if is_symlink {
Some(!path.exists())
} else {
None
};
if !is_symlink && !path.is_dir() {
continue;
}
if broken != Some(true) && !path.join("init.lua").exists() {
continue;
}
let name = dir_entry.file_name().to_string_lossy().to_string();
if is_system_package(&name) {
continue;
}
let source_display = sp.path.display().to_string();
seen.entry(name.clone())
.or_default()
.push((idx, source_display.clone()));
let global_active = seen[&name].len() == 1 && !project_names.contains(&name);
let (meta, eval_error) = if is_safe_pkg_name(&name) {
let code = format!(
r#"package.loaded["{name}"] = nil
local pkg = require("{name}")
return pkg.meta or {{ name = "{name}" }}"#
);
match self.executor.eval_simple(code).await {
Ok(v) => (v, None),
Err(_) => (
serde_json::Value::Object(serde_json::Map::new()),
Some("failed to load meta".to_string()),
),
}
} else {
(
serde_json::Value::Object(serde_json::Map::new()),
Some("invalid package name".to_string()),
)
};
let (source_type, installed_at, updated_at, install_source) =
if let Some(entry) = manifest_data.packages.get(&name) {
let inferred = infer_from_legacy_source_string(&entry.source);
let st = match &inferred {
PackageSource::Git { .. } => "git".to_string(),
PackageSource::Installed => {
format!("installed (from: {})", entry.source)
}
PackageSource::Path { .. } => "path".to_string(),
PackageSource::Bundled { .. } => "bundled".to_string(),
};
(
Some(st),
Some(entry.installed_at.clone()),
Some(entry.updated_at.clone()),
Some(entry.source.clone()),
)
} else {
(None, None, None, None)
};
entries.push(PackageListEntry {
name,
scope: Scope::Global,
source_type,
path: None,
source: Some(source_display),
active: global_active,
version: None,
installed_at,
updated_at,
install_source,
overrides: None,
meta,
error: eval_error,
linked: if is_symlink { Some(true) } else { None },
link_target,
broken,
});
}
}
for entry in entries[global_start_idx..].iter_mut() {
if !entry.active {
continue;
}
if let Some(occurrences) = seen.get(&entry.name) {
if occurrences.len() > 1 {
entry.overrides =
Some(occurrences.iter().skip(1).map(|(_, s)| s.clone()).collect());
}
}
}
let all_packages: Vec<serde_json::Value> =
entries.into_iter().map(|e| e.into_json()).collect();
let search_paths_json: Vec<serde_json::Value> = self
.search_paths
.iter()
.map(|sp| {
serde_json::json!({
"path": sp.path.display().to_string(),
"source": sp.source.to_string(),
})
})
.collect();
let mut result = serde_json::json!({
"packages": all_packages,
"search_paths": search_paths_json,
});
if let Some(root_str) = project_root_str {
result["project_root"] = serde_json::Value::String(root_str);
}
if let Some(lp) = lockfile_path_str {
result["lockfile_path"] = serde_json::Value::String(lp);
}
Ok(result.to_string())
}
}
fn resolve_project_pkg_info(
name: &str,
dep: &alc_toml::PackageDep,
lock_map: &HashMap<String, (Option<String>, PackageSource)>,
root: &Path,
) -> (Option<String>, Option<String>, Option<String>) {
if let Some((ver, source)) = lock_map.get(name) {
match source {
PackageSource::Path { path: raw_path } => {
let p = Path::new(raw_path);
let abs = if p.is_absolute() {
p.to_path_buf()
} else {
root.join(p)
};
(
ver.clone(),
Some("path".to_string()),
Some(abs.display().to_string()),
)
}
PackageSource::Installed => (ver.clone(), Some("installed".to_string()), None),
PackageSource::Git { .. } => (ver.clone(), Some("git".to_string()), None),
PackageSource::Bundled { .. } => (ver.clone(), Some("bundled".to_string()), None),
}
} else {
let st = match dep {
alc_toml::PackageDep::Version(_) => Some("installed".to_string()),
alc_toml::PackageDep::Path { .. } => Some("path".to_string()),
alc_toml::PackageDep::Git { .. } => Some("git".to_string()),
};
(None, st, None)
}
}
fn make_project_entry(
name: String,
version: Option<String>,
source_type: Option<String>,
abs_path: Option<String>,
) -> PackageListEntry {
PackageListEntry {
name,
scope: Scope::Project,
source_type,
path: abs_path,
source: None,
active: true,
version,
installed_at: None,
updated_at: None,
install_source: None,
overrides: None,
meta: serde_json::Value::Object(serde_json::Map::new()),
error: None,
linked: None,
link_target: None,
broken: None,
}
}
fn collect_path_entries_from_lock(
lock_map: &HashMap<String, (Option<String>, PackageSource)>,
root: &Path,
project_names: &mut std::collections::HashSet<String>,
entries: &mut Vec<PackageListEntry>,
) {
for (name, (version, source)) in lock_map {
if let PackageSource::Path { path: raw_path } = source {
let p = Path::new(raw_path);
let abs = if p.is_absolute() {
p.to_path_buf()
} else {
root.join(p)
};
project_names.insert(name.clone());
entries.push(make_project_entry(
name.clone(),
version.clone(),
Some("path".to_string()),
Some(abs.display().to_string()),
));
}
}
}
fn is_safe_pkg_name(name: &str) -> bool {
!name.is_empty()
&& name
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-')
}