feat: support binary scripts in PATH

This commit is contained in:
daimond113 2024-07-25 16:32:48 +02:00
parent 986196ac67
commit 0fcddedad6
No known key found for this signature in database
GPG key ID: 3A8ECE51328B513C
7 changed files with 245 additions and 80 deletions

View file

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

View file

@ -149,15 +149,15 @@ pub fn reqwest_client(data_dir: &Path) -> anyhow::Result<reqwest::blocking::Clie
.build()?)
}
pub fn update_scripts_folder(project: &Project) -> anyhow::Result<()> {
let home_dir = directories::UserDirs::new()
pub fn home_dir() -> anyhow::Result<std::path::PathBuf> {
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")?;

View file

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

View file

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

View file

@ -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<String>,
@ -49,7 +49,7 @@ pub fn get_file_types(file: &str) -> Result<Vec<String>, Vec<full_moon::Error>>
Ok(visitor.types)
}
pub fn generate_linking_module<I: IntoIterator<Item = S>, S: AsRef<str>>(
pub fn generate_lib_linking_module<I: IntoIterator<Item = S>, S: AsRef<str>>(
path: &str,
types: I,
) -> String {
@ -64,27 +64,40 @@ pub fn generate_linking_module<I: IntoIterator<Item = S>, S: AsRef<str>>(
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::<Vec<_>>()
.join("/")
}
pub fn get_lib_require_path(
target: &TargetKind,
base_dir: &Path,
lib_file: &RelativePathBuf,
destination_dir: &Path,
use_new_structure: bool,
) -> Result<String, errors::GetRequirePathError> {
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::<Vec<_>>()
.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::<Vec<_>>()
.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))
}

View file

@ -11,6 +11,14 @@ use std::{collections::BTreeMap, fs::create_dir_all};
pub mod generator;
fn create_and_canonicalize<P: AsRef<std::path::Path>>(
path: P,
) -> std::io::Result<std::path::PathBuf> {
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<full_moon::Error>),
#[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),

View file

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