diff --git a/src/cli/install.rs b/src/cli/install.rs index e23cf39..55b264b 100644 --- a/src/cli/install.rs +++ b/src/cli/install.rs @@ -1,9 +1,13 @@ -use crate::cli::{reqwest_client, IsUpToDate}; +use crate::cli::{home_dir, reqwest_client, IsUpToDate}; use anyhow::Context; use clap::Args; use indicatif::MultiProgress; use pesde::{lockfile::Lockfile, manifest::target::TargetKind, Project}; -use std::{collections::HashSet, sync::Arc, time::Duration}; +use std::{ + collections::{BTreeSet, HashSet}, + sync::Arc, + time::Duration, +}; #[derive(Debug, Args)] pub struct InstallCommand { @@ -12,6 +16,44 @@ pub struct InstallCommand { threads: u64, } +fn bin_link_file(alias: &str) -> String { + let mut all_combinations = BTreeSet::new(); + + for a in TargetKind::VARIANTS { + for b in TargetKind::VARIANTS { + all_combinations.insert((a, b)); + } + } + + let all_folders = all_combinations + .into_iter() + .map(|(a, b)| format!("{:?}", a.packages_folder(b))) + .collect::>() + .into_iter() + .collect::>() + .join(", "); + + #[cfg(windows)] + let prefix = String::new(); + #[cfg(not(windows))] + let prefix = "#!/usr/bin/env -S lune run\n"; + + format!( + r#"{prefix}local process = require("@lune/process") +local fs = require("@lune/fs") + +for _, packages_folder in {{ {all_folders} }} do + local path = `{{process.cwd}}/{{packages_folder}}/{alias}.bin.luau` + + if fs.isFile(path) then + require(path) + break + end +end + "#, + ) +} + impl InstallCommand { pub fn run(self, project: Project, multi: MultiProgress) -> anyhow::Result<()> { let mut refreshed_sources = HashSet::new(); @@ -120,6 +162,47 @@ impl InstallCommand { .apply_patches(&downloaded_graph) .context("failed to apply patches")?; + let bin_folder = home_dir()?.join("bin"); + + for versions in downloaded_graph.values() { + for node in versions.values() { + if node.target.bin_path().is_none() { + continue; + } + + let Some((alias, _)) = &node.node.direct else { + continue; + }; + + let bin_file = bin_folder.join(format!("{alias}.luau")); + std::fs::write(&bin_file, bin_link_file(alias)) + .context("failed to write bin link file")?; + + // TODO: test if this actually works + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + + let mut perms = std::fs::metadata(&bin_file) + .context("failed to get bin link file metadata")? + .permissions(); + perms.set_mode(perms.mode() | 0o111); + std::fs::set_permissions(&bin_file, perms) + .context("failed to set bin link file permissions")?; + } + + #[cfg(windows)] + { + let bin_file = bin_file.with_extension(std::env::consts::EXE_EXTENSION); + std::fs::copy( + std::env::current_exe().context("failed to get current executable path")?, + &bin_file, + ) + .context("failed to copy bin link file")?; + } + } + } + project .write_lockfile(Lockfile { name: manifest.name, diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 64bbb6f..373a92c 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -149,15 +149,15 @@ pub fn reqwest_client(data_dir: &Path) -> anyhow::Result anyhow::Result<()> { - let home_dir = directories::UserDirs::new() +pub fn home_dir() -> anyhow::Result { + Ok(directories::UserDirs::new() .context("failed to get home directory")? .home_dir() - .to_owned(); + .join(concat!(".", env!("CARGO_PKG_NAME")))) +} - let scripts_dir = home_dir - .join(concat!(".", env!("CARGO_PKG_NAME"))) - .join("scripts"); +pub fn update_scripts_folder(project: &Project) -> anyhow::Result<()> { + let scripts_dir = home_dir()?.join("scripts"); if scripts_dir.exists() { let repo = gix::open(&scripts_dir).context("failed to open scripts repository")?; diff --git a/src/cli/publish.rs b/src/cli/publish.rs index 99b8980..ebfe36b 100644 --- a/src/cli/publish.rs +++ b/src/cli/publish.rs @@ -1,7 +1,9 @@ use anyhow::Context; use clap::Args; use colored::Colorize; -use pesde::{manifest::target::Target, Project, MANIFEST_FILE_NAME, MAX_ARCHIVE_SIZE}; +use pesde::{ + manifest::target::Target, scripts::ScriptName, Project, MANIFEST_FILE_NAME, MAX_ARCHIVE_SIZE, +}; use std::path::Component; #[derive(Debug, Args)] @@ -66,6 +68,26 @@ impl PublishCommand { ); } + if !manifest.includes.iter().any(|f| { + matches!( + f.to_lowercase().as_str(), + "readme" | "readme.md" | "readme.txt" + ) + }) { + println!( + "{}: no README file in includes, consider adding one", + "warn".yellow().bold() + ); + } + + if manifest.includes.remove("default.project.json") { + println!( + "{}: default.project.json was in includes, this should be generated by the {} script upon dependants installation", + "warn".yellow().bold(), + ScriptName::RobloxSyncConfigGenerator + ); + } + for (name, path) in [("lib path", lib_path), ("bin path", bin_path)] { let Some(export_path) = path else { continue }; diff --git a/src/cli/self_install.rs b/src/cli/self_install.rs index 44d35a3..b50cd81 100644 --- a/src/cli/self_install.rs +++ b/src/cli/self_install.rs @@ -1,6 +1,8 @@ -use crate::cli::update_scripts_folder; +use crate::cli::{home_dir, update_scripts_folder}; +use anyhow::Context; use clap::Args; use pesde::Project; +use std::fs::create_dir_all; #[derive(Debug, Args)] pub struct SelfInstallCommand {} @@ -9,6 +11,10 @@ impl SelfInstallCommand { pub fn run(self, project: Project) -> anyhow::Result<()> { update_scripts_folder(&project)?; + create_dir_all(home_dir()?.join("bin")).context("failed to create bin folder")?; + + // TODO: add the bin folder to the PATH + Ok(()) } } diff --git a/src/linking/generator.rs b/src/linking/generator.rs index 2f2ed27..65db82e 100644 --- a/src/linking/generator.rs +++ b/src/linking/generator.rs @@ -1,8 +1,8 @@ use std::path::{Component, Path}; +use crate::manifest::target::TargetKind; use full_moon::{ast::luau::ExportedTypeDeclaration, visitors::Visitor}; - -use crate::manifest::target::Target; +use relative_path::RelativePathBuf; struct TypeVisitor { types: Vec, @@ -49,7 +49,7 @@ pub fn get_file_types(file: &str) -> Result, Vec> Ok(visitor.types) } -pub fn generate_linking_module, S: AsRef>( +pub fn generate_lib_linking_module, S: AsRef>( path: &str, types: I, ) -> String { @@ -64,27 +64,40 @@ pub fn generate_linking_module, S: AsRef>( output } -pub fn get_require_path( - target: &Target, +fn luau_style_path(path: &Path) -> String { + path.components() + .enumerate() + .filter_map(|(i, ct)| match ct { + Component::ParentDir => Some(if i == 0 { + ".".to_string() + } else { + "..".to_string() + }), + Component::Normal(part) => Some(format!("{}", part.to_string_lossy())), + _ => None, + }) + .collect::>() + .join("/") +} + +pub fn get_lib_require_path( + target: &TargetKind, base_dir: &Path, + lib_file: &RelativePathBuf, destination_dir: &Path, use_new_structure: bool, -) -> Result { - let Some(lib_file) = target.lib_path() else { - return Err(errors::GetRequirePathError::NoLibPath); - }; - +) -> String { let path = pathdiff::diff_paths(destination_dir, base_dir).unwrap(); - let path = if !use_new_structure { - log::debug!("using old structure for require path"); + let path = if use_new_structure { + log::debug!("using new structure for require path"); lib_file.to_path(path) } else { - log::debug!("using new structure for require path"); + log::debug!("using old structure for require path"); path }; #[cfg(feature = "roblox")] - if matches!(target, Target::Roblox { .. }) { + if matches!(target, TargetKind::Roblox) { let path = path .components() .filter_map(|component| match component { @@ -102,29 +115,23 @@ pub fn get_require_path( .collect::>() .join(""); - return Ok(format!("script{path}")); + return format!("script{path}"); }; - let path = path - .components() - .filter_map(|ct| match ct { - Component::ParentDir => Some("..".to_string()), - Component::Normal(part) => Some(format!("{}", part.to_string_lossy())), - _ => None, - }) - .collect::>() - .join("/"); - - Ok(format!("./{path}")) + format!("{:?}", luau_style_path(&path)) } -pub mod errors { - use thiserror::Error; - - #[derive(Debug, Error)] - #[non_exhaustive] - pub enum GetRequirePathError { - #[error("get require path called for target without a lib path")] - NoLibPath, - } +pub fn generate_bin_linking_module(path: &str) -> String { + format!("return require({path})") +} + +pub fn get_bin_require_path( + base_dir: &Path, + bin_file: &RelativePathBuf, + destination_dir: &Path, +) -> String { + let path = pathdiff::diff_paths(destination_dir, base_dir).unwrap(); + let path = bin_file.to_path(path); + + format!("{:?}", luau_style_path(&path)) } diff --git a/src/linking/mod.rs b/src/linking/mod.rs index d5685d3..7fdcf65 100644 --- a/src/linking/mod.rs +++ b/src/linking/mod.rs @@ -11,6 +11,14 @@ use std::{collections::BTreeMap, fs::create_dir_all}; pub mod generator; +fn create_and_canonicalize>( + path: P, +) -> std::io::Result { + let p = path.as_ref(); + create_dir_all(p)?; + p.canonicalize() +} + impl Project { pub fn link_dependencies(&self, graph: &DownloadedGraph) -> Result<(), errors::LinkingError> { let manifest = self.deser_manifest()?; @@ -89,12 +97,12 @@ impl Project { for (name, versions) in graph { for (version_id, node) in versions { - let base_folder = self.path().join( - self.path() - .join(node.node.base_folder(manifest.target.kind(), true)), - ); - create_dir_all(&base_folder)?; - let base_folder = base_folder.canonicalize()?; + let base_folder = create_and_canonicalize( + self.path().join( + self.path() + .join(node.node.base_folder(manifest.target.kind(), true)), + ), + )?; let packages_container_folder = base_folder.join(PACKAGES_CONTAINER_NAME); let container_folder = node.node.container_folder( @@ -108,17 +116,36 @@ impl Project { .and_then(|v| v.get(version_id)) .and_then(|types| node.node.direct.as_ref().map(|(alias, _)| (alias, types))) { - let module = generator::generate_linking_module( - &generator::get_require_path( - &node.target, - &base_folder, - &container_folder, - node.node.pkg_ref.use_new_structure(), - )?, - types, - ); + if let Some(lib_file) = node.target.lib_path() { + let linker_file = base_folder.join(format!("{alias}.luau")); - std::fs::write(base_folder.join(format!("{alias}.luau")), module)?; + let module = generator::generate_lib_linking_module( + &generator::get_lib_require_path( + &node.target.kind(), + &linker_file, + lib_file, + &container_folder, + node.node.pkg_ref.use_new_structure(), + ), + types, + ); + + std::fs::write(linker_file, module)?; + }; + + if let Some(bin_file) = node.target.bin_path() { + let linker_file = base_folder.join(format!("{alias}.bin.luau")); + + let module = generator::generate_bin_linking_module( + &generator::get_bin_require_path( + &linker_file, + bin_file, + &container_folder, + ), + ); + + std::fs::write(linker_file, module)?; + } } for (dependency_name, (dependency_version_id, dependency_alias)) in @@ -134,26 +161,28 @@ impl Project { )); }; - let dependency_container_folder = dependency_node.node.container_folder( - &packages_container_folder, - dependency_name, - dependency_version_id.version(), - ); + let Some(lib_file) = dependency_node.target.lib_path() else { + continue; + }; - let linker_folder = container_folder - .join(dependency_node.node.base_folder(node.target.kind(), false)); - create_dir_all(&linker_folder)?; - let linker_folder = linker_folder.canonicalize()?; + let linker_file = create_and_canonicalize( + container_folder + .join(dependency_node.node.base_folder(node.target.kind(), false)), + )? + .join(format!("{dependency_alias}.luau")); - let linker_file = linker_folder.join(format!("{dependency_alias}.luau")); - - let module = generator::generate_linking_module( - &generator::get_require_path( - &dependency_node.target, + let module = generator::generate_lib_linking_module( + &generator::get_lib_require_path( + &dependency_node.target.kind(), &linker_file, - &dependency_container_folder, + lib_file, + &dependency_node.node.container_folder( + &packages_container_folder, + dependency_name, + dependency_version_id.version(), + ), node.node.pkg_ref.use_new_structure(), - )?, + ), package_types .get(dependency_name) .and_then(|v| v.get(dependency_version_id)) @@ -190,9 +219,6 @@ pub mod errors { #[error("error parsing Luau script at {0}")] FullMoon(String, Vec), - #[error("error generating require path")] - GetRequirePath(#[from] crate::linking::generator::errors::GetRequirePathError), - #[cfg(feature = "roblox")] #[error("error generating roblox sync config for {0}")] GenerateRobloxSyncConfig(String, #[source] std::io::Error), diff --git a/src/main.rs b/src/main.rs index b3fa408..b53e9f8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,6 +22,27 @@ struct Cli { } fn main() { + #[cfg(windows)] + { + let exe = std::env::current_exe().expect("failed to get current executable path"); + let exe_name = exe.with_extension(""); + let exe_name = exe_name.file_name().unwrap(); + + if exe_name != env!("CARGO_BIN_NAME") { + let args = std::env::args_os(); + + let status = std::process::Command::new("lune") + .arg("run") + .arg(exe.with_extension("luau")) + .args(args.skip(1)) + .current_dir(std::env::current_dir().unwrap()) + .status() + .expect("failed to run lune"); + + std::process::exit(status.code().unwrap()); + } + } + let multi = { let logger = pretty_env_logger::formatted_builder() .parse_env(pretty_env_logger::env_logger::Env::default().default_filter_or("info"))