pub mod archive;
pub mod shim;
use crate::error::Error;
use crate::net::github::GithubAPI;
use crate::net::http;
use crate::net::http::responses::Release;
use crate::project::manifest::Tool;
use crate::sys::paths;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
const SHORTHANDS: &[(&str, &str)] = &[
("darklua", "seaofvoices/darklua"),
("lest", "ryancundiff/lest"),
("luau-lsp", "johnnymorganz/luau-lsp"),
("rojo", "rojo-rbx/rojo"),
("stylua", "johnnymorganz/stylua"),
];
pub fn expand_shorthand(name: &str) -> String {
SHORTHANDS
.iter()
.find(|(short, _)| *short == name)
.map_or_else(|| name.to_string(), |(_, full)| full.to_string())
}
pub fn shorthand_repository(name: &str) -> Option<&'static str> {
SHORTHANDS
.iter()
.find(|(short, _)| *short == name)
.map(|(_, full)| *full)
}
pub fn storage_dir(repository: &str, version: &str) -> Result<PathBuf, Error> {
Ok(paths::tools_dir()?
.join(repository.replace('/', "_"))
.join(version))
}
fn stored_executable(repository: &str, version: &str) -> Result<PathBuf, Error> {
Ok(storage_dir(repository, version)?.join(executable_name(repo_short_name(repository))))
}
pub fn install_tool(alias: &str, tool: &Tool, github: &GithubAPI) -> Result<bool, Error> {
let stored = stored_executable(&tool.repository, &tool.version)?;
if stored.exists() {
shim::write(&shim::path(alias)?)?;
return Ok(false);
}
let release = github.get_release(&tool.repository, &tool.version)?;
install_release(alias, &tool.repository, &release)
}
pub fn install_release(alias: &str, repository: &str, release: &Release) -> Result<bool, Error> {
let version = release.tag_name.trim_start_matches('v');
let storage = storage_dir(repository, version)?;
let repo = repo_short_name(repository);
let stored = storage.join(executable_name(repo));
if stored.exists() {
shim::write(&shim::path(alias)?)?;
return Ok(false);
}
let asset = archive::select_asset(&release.assets, env::consts::OS, env::consts::ARCH)
.ok_or_else(|| Error::NoMatchingAsset {
tool: repository.to_string(),
version: version.to_string(),
})?;
let bytes = http::get_bytes(&asset.browser_download_url, &[])?;
let staging = paths::with_suffix(&storage, ".tmp");
if staging.exists() {
fs::remove_dir_all(&staging)?;
}
fs::create_dir_all(&staging)?;
let target = staging.join(executable_name(repo));
archive::extract(&asset.name, &bytes, &staging, &target)?;
let mut files = Vec::new();
archive::collect_files(&staging, &mut files)?;
let found = archive::pick_executable(&files, repo, alias, env::consts::EXE_SUFFIX)
.ok_or_else(|| Error::NoExecutableInAsset(alias.to_string()))?;
if found != target {
if target.exists() {
fs::remove_file(&target)?;
}
fs::rename(&found, &target)?;
}
if storage.exists() {
fs::remove_dir_all(&storage)?;
}
fs::create_dir_all(storage.parent().expect("storage dir has a parent"))?;
fs::rename(&staging, &storage)?;
let stored = storage.join(executable_name(repo));
make_executable(&stored)?;
shim::write(&shim::path(alias)?)?;
Ok(true)
}
fn repo_short_name(repository: &str) -> &str {
repository
.split_once('/')
.map_or(repository, |(_, repo)| repo)
}
fn executable_name(repo: &str) -> String {
format!("{repo}{}", env::consts::EXE_SUFFIX)
}
#[cfg(unix)]
fn make_executable(path: &Path) -> Result<(), Error> {
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(path, fs::Permissions::from_mode(0o755))?;
Ok(())
}
#[cfg(not(unix))]
fn make_executable(_path: &Path) -> Result<(), Error> {
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn storage_paths_include_owner_repo_and_version() {
let dir = storage_dir("rojo-rbx/rojo", "7.4.4").unwrap();
assert!(dir.starts_with(paths::tools_dir().unwrap()));
assert!(dir.ends_with(Path::new("tools").join("rojo-rbx_rojo").join("7.4.4")));
}
#[test]
fn expands_known_shorthands_only() {
assert_eq!(expand_shorthand("rojo"), "rojo-rbx/rojo");
assert_eq!(expand_shorthand("acme/tool"), "acme/tool");
assert_eq!(shorthand_repository("stylua"), Some("johnnymorganz/stylua"));
assert_eq!(shorthand_repository("acme/tool"), None);
}
}