use super::stored_executable;
use crate::error::Error;
use crate::project::manifest::{MANIFEST_FILE, Tool};
use crate::sys::paths;
use crate::sys::process;
use serde::Deserialize;
use std::collections::BTreeMap;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
pub fn shim_alias() -> Option<String> {
let exe = env::current_exe().ok()?;
alias_from_stem(exe.file_stem()?.to_str()?)
}
fn alias_from_stem(stem: &str) -> Option<String> {
let lower = stem.to_ascii_lowercase();
if lower == "lpm" || lower.starts_with("lpm-") {
None
} else {
Some(stem.to_string())
}
}
pub fn run(alias: &str) -> Result<i32, Error> {
let tool = resolve_alias(alias, &env::current_dir()?, &paths::global_tools_file()?)?;
let stored = stored_executable(&tool.repository, &tool.version)?;
if !stored.exists() {
return Err(Error::ToolNotInstalled(alias.to_string()));
}
let mut command = std::process::Command::new(&stored);
command.args(env::args_os().skip(1));
process::exec(command)
}
pub fn path(alias: &str) -> Result<PathBuf, Error> {
Ok(paths::bin_dir()?.join(format!("{alias}{}", env::consts::EXE_SUFFIX)))
}
pub fn write(shim: &Path) -> Result<(), Error> {
fs::create_dir_all(shim.parent().expect("bin dir has a parent"))?;
fs::copy(env::current_exe()?, shim)?;
super::make_executable(shim)
}
pub fn shadowing_executable(alias: &str) -> Option<PathBuf> {
let bin = paths::bin_dir().ok()?;
let file = format!("{alias}{}", env::consts::EXE_SUFFIX);
for dir in env::split_paths(&env::var_os("PATH")?) {
if dir.as_os_str().is_empty() {
continue;
}
let candidate = dir.join(&file);
if candidate.is_file() {
return if paths::same_file(&dir, &bin) || dir == bin {
None
} else {
Some(candidate)
};
}
}
None
}
fn resolve_alias(alias: &str, start: &Path, global: &Path) -> Result<Tool, Error> {
for dir in start.ancestors() {
if let Some(tools) = tools_in(&dir.join(MANIFEST_FILE))?
&& let Some(tool) = get_tool(&tools, alias)
{
return Ok(tool.clone());
}
}
if let Some(tools) = tools_in(global)?
&& let Some(tool) = get_tool(&tools, alias)
{
return Ok(tool.clone());
}
Err(Error::ToolNotManaged(alias.to_string()))
}
pub fn project_tools(start: &Path) -> Result<BTreeMap<String, Tool>, Error> {
let mut merged = BTreeMap::new();
for dir in start.ancestors() {
if let Some(tools) = tools_in(&dir.join(MANIFEST_FILE))? {
for (alias, tool) in tools {
merged.entry(alias).or_insert(tool);
}
}
}
Ok(merged)
}
pub fn global_tools() -> Result<BTreeMap<String, Tool>, Error> {
Ok(tools_in(&paths::global_tools_file()?)?.unwrap_or_default())
}
#[derive(Deserialize)]
struct ToolsTable {
#[serde(default)]
tools: BTreeMap<String, Tool>,
}
fn tools_in(path: &Path) -> Result<Option<BTreeMap<String, Tool>>, Error> {
if !path.exists() {
return Ok(None);
}
let table: ToolsTable = toml::from_str(&fs::read_to_string(path)?)?;
Ok(Some(table.tools))
}
fn get_tool<'a>(tools: &'a BTreeMap<String, Tool>, alias: &str) -> Option<&'a Tool> {
tools.get(alias).or_else(|| {
tools
.iter()
.find_map(|(key, tool)| key.eq_ignore_ascii_case(alias).then_some(tool))
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shim_aliases_exclude_lpm_itself() {
assert_eq!(alias_from_stem("lpm"), None);
assert_eq!(alias_from_stem("LPM"), None);
assert_eq!(alias_from_stem("lpm-windows-x86_64"), None);
assert_eq!(alias_from_stem("rojo"), Some("rojo".to_string()));
assert_eq!(alias_from_stem("StyLua"), Some("StyLua".to_string()));
}
#[test]
fn resolves_aliases_from_nearest_manifest_then_global() {
let base = std::env::temp_dir().join("lpm-test-resolve-alias");
let _ = fs::remove_dir_all(&base);
let project = base.join("project");
let nested = project.join("a").join("b");
fs::create_dir_all(&nested).unwrap();
fs::write(
project.join(MANIFEST_FILE),
"[package]\nname = \"scope/name\"\nversion = \"0.1.0\"\n\n[tools]\nrojo = \"rojo-rbx/rojo@7.4.4\"\n",
)
.unwrap();
let global = base.join("tools.toml");
fs::write(
&global,
"[tools]\nstylua = \"johnnymorganz/stylua@2.0.0\"\n",
)
.unwrap();
let tool = resolve_alias("rojo", &nested, &global).unwrap();
assert_eq!(tool.repository, "rojo-rbx/rojo");
assert_eq!(tool.version, "7.4.4");
let tool = resolve_alias("stylua", &nested, &global).unwrap();
assert_eq!(tool.repository, "johnnymorganz/stylua");
assert!(resolve_alias("Rojo", &nested, &global).is_ok());
assert!(matches!(
resolve_alias("wally", &nested, &global),
Err(Error::ToolNotManaged(_))
));
let _ = fs::remove_dir_all(&base);
}
#[test]
fn project_tools_merge_with_nearest_manifest_winning() {
let base = std::env::temp_dir().join("lpm-test-project-tools");
let _ = fs::remove_dir_all(&base);
let inner = base.join("project");
let start = inner.join("src");
fs::create_dir_all(&start).unwrap();
fs::write(
base.join(MANIFEST_FILE),
"[tools]\nrojo = \"rojo-rbx/rojo@7.4.4\"\nstylua = \"johnnymorganz/stylua@2.0.0\"\n",
)
.unwrap();
fs::write(
inner.join(MANIFEST_FILE),
"[tools]\nrojo = \"rojo-rbx/rojo@7.5.1\"\n",
)
.unwrap();
let tools = project_tools(&start).unwrap();
assert_eq!(tools["rojo"].version, "7.5.1");
assert_eq!(tools["stylua"].version, "2.0.0");
let _ = fs::remove_dir_all(&base);
}
}