diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b9800d..d7d8627 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fix path dependencies using project's workspace dependencies by @daimond113 +### Changed +- Binary linkers are now done in Rust to simplify their implementation and cross-runtime portability by @daimond113 + ## [0.6.0] - 2025-02-22 ### Added - Improve installation experience by @lukadev-0 diff --git a/src/cli/bin_link.luau b/src/cli/bin_link.luau deleted file mode 100644 index 1e087fa..0000000 --- a/src/cli/bin_link.luau +++ /dev/null @@ -1,81 +0,0 @@ -local process = require("@lune/process") -local fs = require("@lune/fs") -local stdio = require("@lune/stdio") -local serde = require("@lune/serde") - -local project_root = nil -local path_components = string.split(string.gsub(process.cwd, "\\", "/"), "/") -if path_components[#path_components] == "" then - table.remove(path_components) -end - -local function in_lockfile(lockfile) - if not lockfile.graph then - return false - end - - for _, versions in lockfile.graph do - for _, node in versions do - if node.direct and node.direct[1] == "{alias}" then - return true - end - end - end - - return false -end - -for i = #path_components, 1, -1 do - local path = table.concat(path_components, "/", 1, i) - if not fs.isFile(path .. "/{MANIFEST_FILE_NAME}") then - continue - end - - if project_root == nil then - project_root = path - end - - if project_root and fs.isFile(path .. "/{LOCKFILE_FILE_NAME}") then - local lockfile = serde.decode("toml", fs.readFile(path .. "/{LOCKFILE_FILE_NAME}")) - if not lockfile.workspace then - continue - end - - local search_for = string.gsub(project_root, path, "") - if string.sub(search_for, 1, 1) == "/" then - search_for = string.sub(search_for, 2) - end - - if search_for == "" then - if in_lockfile(lockfile) then - break - end - - continue - end - - for _, targets in lockfile.workspace do - for _, member_path in targets do - local path_normalized = string.gsub(member_path, "\\", "/") - if path_normalized == search_for and in_lockfile(lockfile) then - project_root = path - break - end - end - end - end -end - -if project_root ~= nil then - for _, packages_folder in {{ {all_folders} }} do - local path = `{{project_root}}/{{packages_folder}}/{alias}.bin.luau` - - if fs.isFile(path) then - require(path) - return - end - end -end - -stdio.ewrite(stdio.color("red") .. "binary `{alias}` not found. are you in the right directory?" .. stdio.color("reset") .. "\n") -process.exit(1) \ No newline at end of file diff --git a/src/cli/install.rs b/src/cli/install.rs index 856aca9..1c34854 100644 --- a/src/cli/install.rs +++ b/src/cli/install.rs @@ -14,7 +14,7 @@ use pesde::{ engine::EngineKind, graph::{DependencyGraph, DependencyGraphWithTarget}, lockfile::Lockfile, - manifest::{target::TargetKind, Alias, DependencyType, Manifest}, + manifest::{DependencyType, Manifest}, names::PackageNames, source::{ pesde::PesdePackageSource, @@ -22,7 +22,7 @@ use pesde::{ traits::{PackageRef as _, RefreshOptions}, PackageSources, }, - version_matches, Project, RefreshedSources, LOCKFILE_FILE_NAME, MANIFEST_FILE_NAME, + version_matches, Project, RefreshedSources, MANIFEST_FILE_NAME, }; use std::{ collections::{BTreeMap, BTreeSet, HashMap}, @@ -32,32 +32,6 @@ use std::{ }; use tokio::task::JoinSet; -fn bin_link_file(alias: &Alias) -> 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(", "); - - format!( - include_str!("bin_link.luau"), - alias = alias, - all_folders = all_folders, - MANIFEST_FILE_NAME = MANIFEST_FILE_NAME, - LOCKFILE_FILE_NAME = LOCKFILE_FILE_NAME - ) -} - pub struct InstallHooks { pub bin_folder: std::path::PathBuf, } @@ -85,39 +59,30 @@ impl DownloadAndLinkHooks for InstallHooks { let bin_exec_file = bin_folder .join(alias.as_str()) .with_extension(std::env::consts::EXE_EXTENSION); + let curr_exe = + std::env::current_exe().context("failed to get current executable path")?; - let impl_folder = bin_folder.join(".impl"); - fs::create_dir_all(&impl_folder) + // TODO: remove this in a major release + #[cfg(unix)] + if fs::metadata(&bin_exec_file) .await - .context("failed to create bin link folder")?; - - let bin_file = impl_folder.join(alias.as_str()).with_extension("luau"); - fs::write(&bin_file, bin_link_file(&alias)) - .await - .context("failed to write bin link file")?; - - #[cfg(windows)] - match fs::symlink_file( - std::env::current_exe().context("failed to get current executable path")?, - &bin_exec_file, - ) - .await + .is_ok_and(|m| !m.is_symlink()) { - Ok(_) => {} - Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {} - e => e.context("failed to copy bin link file")?, + fs::remove_file(&bin_exec_file) + .await + .context("failed to remove outdated bin linker")?; } - #[cfg(not(windows))] - fs::write( - &bin_exec_file, - format!( - r#"#!/bin/sh -exec lune run "$(dirname "$0")/.impl/{alias}.luau" -- "$@""# - ), - ) - .await - .context("failed to link bin link file")?; + #[cfg(windows)] + let res = fs::symlink_file(curr_exe, &bin_exec_file).await; + #[cfg(unix)] + let res = fs::symlink(curr_exe, &bin_exec_file).await; + + match res { + Ok(_) => {} + Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {} + e => e.context("failed to symlink bin link file")?, + } make_executable(&bin_exec_file) .await diff --git a/src/main.rs b/src/main.rs index 60396d3..01f2826 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,24 @@ #[cfg(feature = "version-management")] use crate::cli::version::{check_for_updates, current_version, get_or_download_engine}; -use crate::cli::{auth::get_tokens, display_err, home_dir, HOME_DIR}; +use crate::cli::{auth::get_tokens, display_err, home_dir, style::ERROR_STYLE, HOME_DIR}; use anyhow::Context as _; use clap::{builder::styling::AnsiColor, Parser}; use fs_err::tokio as fs; use indicatif::MultiProgress; -use pesde::{engine::EngineKind, find_roots, version_matches, AuthConfig, Project}; +use pesde::{ + engine::EngineKind, find_roots, manifest::target::TargetKind, version_matches, AuthConfig, + Project, +}; use semver::VersionReq; use std::{ + collections::HashSet, io, path::{Path, PathBuf}, str::FromStr as _, sync::Mutex, }; use tempfile::NamedTempFile; +use tokio::task::JoinSet; use tracing::instrument; use tracing_subscriber::{ filter::LevelFilter, fmt::MakeWriter, layer::SubscriberExt as _, util::SubscriberInitExt as _, @@ -150,51 +155,6 @@ async fn run() -> anyhow::Result<()> { .expect("exe name is not valid utf-8"); let exe_name_engine = EngineKind::from_str(exe_name); - #[cfg(windows)] - 'scripts: { - // if we're an engine, we don't want to run any scripts - if exe_name_engine.is_ok() { - break 'scripts; - } - - if let Some(bin_folder) = current_exe.parent() { - // we're not in {path}/bin/{exe} - if bin_folder.file_name().is_some_and(|parent| parent != "bin") { - break 'scripts; - } - - // we're not in {path}/.pesde/bin/{exe} - if bin_folder - .parent() - .and_then(|home_folder| home_folder.file_name()) - .is_some_and(|home_folder| home_folder != HOME_DIR) - { - break 'scripts; - } - } - - // the bin script will search for the project root itself, so we do that to ensure - // consistency across platforms, since the script is executed using a shebang - // on unix systems - let status = std::process::Command::new("lune") - .arg("run") - .arg( - current_exe - .parent() - .unwrap_or(¤t_exe) - .join(".impl") - .join(current_exe.file_name().unwrap()) - .with_extension("luau"), - ) - .arg("--") - .args(std::env::args_os().skip(1)) - .current_dir(cwd) - .status() - .expect("failed to run lune"); - - std::process::exit(status.code().unwrap_or(1i32)); - }; - let tracing_env_filter = EnvFilter::builder() .with_default_directive(LevelFilter::INFO.into()) .from_env_lossy() @@ -236,6 +196,97 @@ async fn run() -> anyhow::Result<()> { .map_or_else(|| "none".to_string(), |p| p.display().to_string()) ); + 'scripts: { + // if we're an engine, we don't want to run any scripts + if exe_name_engine.is_ok() { + break 'scripts; + } + + if let Some(bin_folder) = current_exe.parent() { + // we're not in {path}/bin/{exe} + if bin_folder.file_name().is_some_and(|parent| parent != "bin") { + break 'scripts; + } + + // we're not in {path}/.pesde/bin/{exe} + if bin_folder + .parent() + .and_then(|home_folder| home_folder.file_name()) + .is_some_and(|home_folder| home_folder != HOME_DIR) + { + break 'scripts; + } + } + + let linker_file_name = format!("{exe_name}.bin.luau"); + + let path = 'finder: { + let all_folders = TargetKind::VARIANTS + .iter() + .flat_map(|a| TargetKind::VARIANTS.iter().map(|b| a.packages_folder(*b))) + .collect::>(); + + let mut tasks = all_folders + .into_iter() + .map(|folder| { + let package_path = project_root_dir.join(&folder).join(&linker_file_name); + let workspace_path = project_workspace_dir + .as_deref() + .map(|path| path.join(&folder).join(&linker_file_name)); + + async move { + if fs::metadata(&package_path).await.is_ok() { + return Some((true, package_path)); + } + + if let Some(workspace_path) = workspace_path { + if fs::metadata(&workspace_path).await.is_ok() { + return Some((false, workspace_path)); + } + } + + None + } + }) + .collect::>(); + + let mut workspace_path = None; + + while let Some(res) = tasks.join_next().await { + if let Some((primary, path)) = res.unwrap() { + if primary { + break 'finder path; + } + + workspace_path = Some(path); + } + } + + if let Some(path) = workspace_path { + break 'finder path; + } + + eprintln!( + "{}", + ERROR_STYLE.apply_to(format!( + "binary `{exe_name}` not found. are you in the right directory?" + )) + ); + std::process::exit(1i32); + }; + + let status = std::process::Command::new("lune") + .arg("run") + .arg(path) + .arg("--") + .args(std::env::args_os().skip(1)) + .current_dir(cwd) + .status() + .expect("failed to run lune"); + + std::process::exit(status.code().unwrap_or(1i32)); + }; + let home_dir = home_dir()?; let data_dir = home_dir.join("data"); fs::create_dir_all(&data_dir)