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.
This commit is contained in:
daimond113 2025-03-09 01:18:12 +01:00
parent 41337ac96a
commit 9a75ebf637
No known key found for this signature in database
GPG key ID: 640DC95EC1190354
4 changed files with 122 additions and 184 deletions

View file

@ -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

View file

@ -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)

View file

@ -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::<BTreeSet<_>>()
.into_iter()
.collect::<Vec<_>>()
.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

View file

@ -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(&current_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::<HashSet<_>>();
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::<JoinSet<_>>();
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)