From 9a75ebf6372524fb109651cc180b96f8c8857e11 Mon Sep 17 00:00:00 2001 From: daimond113 Date: Sun, 9 Mar 2025 01:18:12 +0100 Subject: [PATCH] refactor: handle bin linkers in rust Previously, binary linkers were handled by a Luau script. This was not cross-runtime portable, and forced us to do many "hacks" in order to be able to implement them. To solve these issues, they are now handled with Rust, which allows us to use our existing infrastructure. --- CHANGELOG.md | 3 + src/cli/bin_link.luau | 81 ----------------------- src/cli/install.rs | 77 ++++++---------------- src/main.rs | 145 ++++++++++++++++++++++++++++-------------- 4 files changed, 122 insertions(+), 184 deletions(-) delete mode 100644 src/cli/bin_link.luau 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)