feat: add cross runtime script execution

Adds a new mechanism which simplifies executing
scripts for different runtimes. Additionally,
supports Lune's 0.9.0 change of removing "--" from
the args.
This commit is contained in:
daimond113 2025-04-26 23:46:56 +02:00
parent 6aeedd8602
commit 5c74a3762b
No known key found for this signature in database
GPG key ID: 640DC95EC1190354
19 changed files with 434 additions and 147 deletions

View file

@ -1,5 +1,7 @@
use crate::cli::{ use crate::cli::{
compatible_runtime,
config::read_config, config::read_config,
get_project_engines,
reporters::{self, CliReporter}, reporters::{self, CliReporter},
VersionedPackageName, VersionedPackageName,
}; };
@ -28,7 +30,6 @@ use std::{
env::current_dir, env::current_dir,
ffi::OsString, ffi::OsString,
io::{Stderr, Write as _}, io::{Stderr, Write as _},
process::Command,
sync::Arc, sync::Arc,
}; };
@ -57,7 +58,7 @@ impl ExecuteCommand {
let refreshed_sources = RefreshedSources::new(); let refreshed_sources = RefreshedSources::new();
let (tempdir, bin_path) = reporters::run_with_reporter_and_writer( let (tempdir, runtime, bin_path) = reporters::run_with_reporter_and_writer(
std::io::stderr(), std::io::stderr(),
|multi_progress, root_progress, reporter| async { |multi_progress, root_progress, reporter| async {
let multi_progress = multi_progress; let multi_progress = multi_progress;
@ -152,6 +153,8 @@ impl ExecuteCommand {
project: project.clone(), project: project.clone(),
path: tempdir.path().into(), path: tempdir.path().into(),
id: id.clone(), id: id.clone(),
// HACK: the pesde package source doesn't use the engines, so we can just use an empty map
engines: Default::default(),
}, },
) )
.await .await
@ -175,7 +178,7 @@ impl ExecuteCommand {
project project
.download_and_link( .download_and_link(
&graph, &graph,
DownloadAndLinkOptions::<CliReporter<Stderr>, ()>::new(reqwest) DownloadAndLinkOptions::<CliReporter<Stderr>, ()>::new(reqwest.clone())
.reporter(reporter) .reporter(reporter)
.refreshed_sources(refreshed_sources) .refreshed_sources(refreshed_sources)
.install_dependencies_mode(InstallDependenciesMode::Prod), .install_dependencies_mode(InstallDependenciesMode::Prod),
@ -183,7 +186,18 @@ impl ExecuteCommand {
.await .await
.context("failed to download and link dependencies")?; .context("failed to download and link dependencies")?;
anyhow::Ok((tempdir, bin_path.to_relative_path_buf())) let manifest = project
.deser_manifest()
.await
.context("failed to deserialize manifest")?;
let engines = get_project_engines(&manifest, &reqwest).await?;
anyhow::Ok((
tempdir,
compatible_runtime(target.kind(), &engines)?,
bin_path.to_relative_path_buf(),
))
}, },
) )
.await?; .await?;
@ -200,13 +214,11 @@ impl ExecuteCommand {
) )
.context("failed to write to tempfile")?; .context("failed to write to tempfile")?;
let status = Command::new("lune") let status = runtime
.arg("run") .prepare_command(caller.path().as_os_str(), self.args)
.arg(caller.path())
.arg("--")
.args(&self.args)
.current_dir(current_dir().context("failed to get current directory")?) .current_dir(current_dir().context("failed to get current directory")?)
.status() .status()
.await
.context("failed to run script")?; .context("failed to run script")?;
drop(caller); drop(caller);

View file

@ -225,6 +225,8 @@ impl InitCommand {
// HACK: the pesde package source doesn't use the path, so we can just use an empty one // HACK: the pesde package source doesn't use the path, so we can just use an empty one
path: Path::new("").into(), path: Path::new("").into(),
id: id.clone(), id: id.clone(),
// HACK: the pesde package source doesn't use the engines, so we can just use an empty map
engines: Default::default(),
}, },
) )
.await?; .await?;
@ -268,6 +270,8 @@ impl InitCommand {
.to_string(), .to_string(),
); );
} }
// TODO: add engines
} else { } else {
println!( println!(
"{ERROR_PREFIX}: no scripts package configured, this can cause issues with Roblox compatibility" "{ERROR_PREFIX}: no scripts package configured, this can cause issues with Roblox compatibility"

View file

@ -105,7 +105,7 @@ impl Subcommand {
Subcommand::Update(update) => update.run(project, reqwest).await, Subcommand::Update(update) => update.run(project, reqwest).await,
Subcommand::Outdated(outdated) => outdated.run(project).await, Subcommand::Outdated(outdated) => outdated.run(project).await,
Subcommand::List(list) => list.run(project).await, Subcommand::List(list) => list.run(project).await,
Subcommand::Run(run) => run.run(project).await, Subcommand::Run(run) => run.run(project, reqwest).await,
Subcommand::Publish(publish) => publish.run(project, reqwest).await, Subcommand::Publish(publish) => publish.run(project, reqwest).await,
Subcommand::Yank(yank) => yank.run(project, reqwest).await, Subcommand::Yank(yank) => yank.run(project, reqwest).await,
Subcommand::Deprecate(deprecate) => deprecate.run(project, reqwest).await, Subcommand::Deprecate(deprecate) => deprecate.run(project, reqwest).await,

View file

@ -127,6 +127,9 @@ impl PublishCommand {
match up_to_date_lockfile(project).await? { match up_to_date_lockfile(project).await? {
Some(lockfile) => { Some(lockfile) => {
let engines =
Arc::new(crate::cli::get_project_engines(&manifest, &reqwest).await?);
let mut tasks = lockfile let mut tasks = lockfile
.graph .graph
.iter() .iter()
@ -142,6 +145,7 @@ impl PublishCommand {
let id = Arc::new(id.clone()); let id = Arc::new(id.clone());
let node = node.clone(); let node = node.clone();
let refreshed_sources = refreshed_sources.clone(); let refreshed_sources = refreshed_sources.clone();
let engines = engines.clone();
async move { async move {
let source = node.pkg_ref.source(); let source = node.pkg_ref.source();
@ -161,6 +165,7 @@ impl PublishCommand {
project, project,
path: container_folder.into(), path: container_folder.into(),
id, id,
engines,
}, },
) )
.await?; .await?;

View file

@ -1,20 +1,21 @@
use crate::cli::{style::WARN_STYLE, up_to_date_lockfile}; use crate::cli::{compatible_runtime, get_project_engines, style::WARN_STYLE, up_to_date_lockfile};
use anyhow::Context as _; use anyhow::Context as _;
use clap::Args; use clap::Args;
use fs_err::tokio as fs; use fs_err::tokio as fs;
use futures::{StreamExt as _, TryStreamExt as _}; use futures::{StreamExt as _, TryStreamExt as _};
use pesde::{ use pesde::{
engine::runtime::Runtime,
errors::{ManifestReadError, WorkspaceMembersError}, errors::{ManifestReadError, WorkspaceMembersError},
linking::generator::generate_bin_linking_module, linking::generator::generate_bin_linking_module,
manifest::Alias, manifest::{Alias, Manifest},
names::{PackageName, PackageNames}, names::{PackageName, PackageNames},
scripts::parse_script,
source::traits::{GetTargetOptions, PackageRef as _, PackageSource as _, RefreshOptions}, source::traits::{GetTargetOptions, PackageRef as _, PackageSource as _, RefreshOptions},
Project, MANIFEST_FILE_NAME, Project, MANIFEST_FILE_NAME,
}; };
use relative_path::RelativePathBuf; use relative_path::RelativePathBuf;
use std::{ use std::{
collections::HashSet, env::current_dir, ffi::OsString, io::Write as _, path::Path, collections::HashSet, env::current_dir, ffi::OsString, io::Write as _, path::Path, sync::Arc,
process::Command,
}; };
#[derive(Debug, Args)] #[derive(Debug, Args)]
@ -29,8 +30,15 @@ pub struct RunCommand {
} }
impl RunCommand { impl RunCommand {
pub async fn run(self, project: Project) -> anyhow::Result<()> { pub async fn run(self, project: Project, reqwest: reqwest::Client) -> anyhow::Result<()> {
let run = |root: &Path, file_path: &Path| -> ! { let manifest = project
.deser_manifest()
.await
.context("failed to deserialize manifest")?;
let engines = Arc::new(get_project_engines(&manifest, &reqwest).await?);
let run = async |runtime: Runtime, root: &Path, file_path: &Path| -> ! {
let mut caller = tempfile::NamedTempFile::new().expect("failed to create tempfile"); let mut caller = tempfile::NamedTempFile::new().expect("failed to create tempfile");
caller caller
.write_all( .write_all(
@ -42,13 +50,11 @@ impl RunCommand {
) )
.expect("failed to write to tempfile"); .expect("failed to write to tempfile");
let status = Command::new("lune") let status = runtime
.arg("run") .prepare_command(caller.path().as_os_str(), self.args)
.arg(caller.path())
.arg("--")
.args(&self.args)
.current_dir(current_dir().expect("failed to get current directory")) .current_dir(current_dir().expect("failed to get current directory"))
.status() .status()
.await
.expect("failed to run script"); .expect("failed to run script");
drop(caller); drop(caller);
@ -57,11 +63,13 @@ impl RunCommand {
}; };
let Some(package_or_script) = self.package_or_script else { let Some(package_or_script) = self.package_or_script else {
if let Some(script_path) = project.deser_manifest().await?.target.bin_path() { if let Some(script_path) = manifest.target.bin_path() {
run( run(
compatible_runtime(manifest.target.kind(), &engines)?,
project.package_dir(), project.package_dir(),
&script_path.to_path(project.package_dir()), &script_path.to_path(project.package_dir()),
); )
.await;
} }
anyhow::bail!("no package or script specified, and no bin path found in manifest") anyhow::bail!("no package or script specified, and no bin path found in manifest")
@ -130,6 +138,7 @@ impl RunCommand {
project, project,
path: container_folder.as_path().into(), path: container_folder.as_path().into(),
id: id.into(), id: id.into(),
engines: engines.clone(),
}, },
) )
.await?; .await?;
@ -140,15 +149,20 @@ impl RunCommand {
let path = bin_path.to_path(&container_folder); let path = bin_path.to_path(&container_folder);
run(&path, &path); run(compatible_runtime(target.kind(), &engines)?, &path, &path).await;
} }
if let Ok(manifest) = project.deser_manifest().await { if let Ok(mut manifest) = project.deser_manifest().await {
if let Some(script_path) = manifest.scripts.get(&package_or_script) { if let Some(script) = manifest.scripts.remove(&package_or_script) {
let (runtime, script_path) =
parse_script(script, &engines).context("failed to get script info")?;
run( run(
runtime,
project.package_dir(), project.package_dir(),
&script_path.to_path(project.package_dir()), &script_path.to_path(project.package_dir()),
); )
.await;
} }
} }
@ -212,6 +226,17 @@ impl RunCommand {
project.package_dir().to_path_buf() project.package_dir().to_path_buf()
}; };
run(&root, &path); let manifest = fs::read_to_string(root.join(MANIFEST_FILE_NAME))
.await
.context("failed to read manifest at root")?;
let manifest = toml::de::from_str::<Manifest>(&manifest)
.context("failed to deserialize manifest at root")?;
run(
compatible_runtime(manifest.target.kind(), &engines)?,
&root,
&path,
)
.await;
} }
} }

View file

@ -17,6 +17,11 @@ pub struct SelfInstallCommand {
impl SelfInstallCommand { impl SelfInstallCommand {
pub async fn run(self) -> anyhow::Result<()> { pub async fn run(self) -> anyhow::Result<()> {
let bin_dir = crate::cli::bin_dir()?;
let bin_dir = bin_dir
.to_str()
.context("bin directory path contains invalid characters")?;
#[cfg(windows)] #[cfg(windows)]
{ {
if !self.skip_add_to_path { if !self.skip_add_to_path {
@ -24,17 +29,11 @@ impl SelfInstallCommand {
use anyhow::Context as _; use anyhow::Context as _;
use windows_registry::CURRENT_USER; use windows_registry::CURRENT_USER;
let bin_dir = crate::cli::bin_dir()?;
let env = CURRENT_USER let env = CURRENT_USER
.create("Environment") .create("Environment")
.context("failed to open Environment key")?; .context("failed to open Environment key")?;
let path = env.get_string("Path").context("failed to get Path value")?; let path = env.get_string("Path").context("failed to get Path value")?;
let bin_dir = bin_dir
.to_str()
.context("bin directory path contains invalid characters")?;
let exists = path.split(';').any(|part| part == bin_dir); let exists = path.split(';').any(|part| part == bin_dir);
if !exists { if !exists {
@ -68,7 +67,7 @@ and then restart your shell.
", ",
CLI_STYLE.apply_to(env!("CARGO_BIN_NAME")), CLI_STYLE.apply_to(env!("CARGO_BIN_NAME")),
ADDED_STYLE.apply_to(env!("CARGO_PKG_VERSION")), ADDED_STYLE.apply_to(env!("CARGO_PKG_VERSION")),
style(format!(r#"export PATH="$PATH:$HOME/{HOME_DIR}/bin""#)).green(), style(format!(r#"export PATH="$PATH:{bin_dir}""#)).green(),
); );
}; };

View file

@ -1,7 +1,7 @@
use super::files::make_executable; use super::files::make_executable;
use crate::cli::{ use crate::cli::{
bin_dir, dep_type_to_key, bin_dir, dep_type_to_key,
reporters::{self, run_with_reporter, CliReporter}, reporters::{self, CliReporter},
resolve_overrides, run_on_workspace_members, resolve_overrides, run_on_workspace_members,
style::{ADDED_STYLE, REMOVED_STYLE, WARN_PREFIX}, style::{ADDED_STYLE, REMOVED_STYLE, WARN_PREFIX},
up_to_date_lockfile, up_to_date_lockfile,
@ -25,7 +25,7 @@ use pesde::{
version_matches, Project, RefreshedSources, MANIFEST_FILE_NAME, version_matches, Project, RefreshedSources, MANIFEST_FILE_NAME,
}; };
use std::{ use std::{
collections::{BTreeMap, BTreeSet, HashMap, HashSet}, collections::{BTreeMap, BTreeSet, HashSet},
num::NonZeroUsize, num::NonZeroUsize,
path::Path, path::Path,
sync::Arc, sync::Arc,
@ -181,49 +181,7 @@ pub async fn install(
} }
}; };
let progress_prefix = format!("{} {}: ", manifest.name, manifest.target); let resolved_engine_versions = Arc::new(super::get_project_engines(&manifest, &reqwest).await?);
#[cfg(feature = "version-management")]
let resolved_engine_versions = run_with_reporter(|_, root_progress, reporter| async {
let root_progress = root_progress;
let reporter = reporter;
root_progress.set_prefix(progress_prefix.clone());
root_progress.reset();
root_progress.set_message("update engines");
let mut tasks = manifest
.engines
.iter()
.map(|(engine, req)| {
let engine = *engine;
let req = req.clone();
let reqwest = reqwest.clone();
let reporter = reporter.clone();
async move {
let version = crate::cli::version::get_or_download_engine(
&reqwest, engine, req, reporter,
)
.await?
.1;
crate::cli::version::make_linker_if_needed(engine).await?;
Ok::<_, anyhow::Error>((engine, version))
}
})
.collect::<JoinSet<_>>();
let mut resolved_engine_versions = HashMap::new();
while let Some(task) = tasks.join_next().await {
let (engine, version) = task.unwrap()?;
resolved_engine_versions.insert(engine, version);
}
Ok::<_, anyhow::Error>(resolved_engine_versions)
})
.await?;
let overrides = resolve_overrides(&manifest)?; let overrides = resolve_overrides(&manifest)?;
@ -232,7 +190,7 @@ pub async fn install(
let multi = multi; let multi = multi;
let root_progress = root_progress; let root_progress = root_progress;
root_progress.set_prefix(progress_prefix); root_progress.set_prefix(format!("{} {}: ", manifest.name, manifest.target));
root_progress.reset(); root_progress.reset();
root_progress.set_message("resolve"); root_progress.set_message("resolve");
@ -318,7 +276,8 @@ pub async fn install(
.refreshed_sources(refreshed_sources.clone()) .refreshed_sources(refreshed_sources.clone())
.install_dependencies_mode(options.install_dependencies_mode) .install_dependencies_mode(options.install_dependencies_mode)
.network_concurrency(options.network_concurrency) .network_concurrency(options.network_concurrency)
.force(options.force || has_irrecoverable_changes), .force(options.force || has_irrecoverable_changes)
.engines(resolved_engine_versions.clone()),
) )
.await .await
.context("failed to download and link dependencies")?; .context("failed to download and link dependencies")?;

View file

@ -5,6 +5,10 @@ use crate::cli::{
use anyhow::Context as _; use anyhow::Context as _;
use futures::StreamExt as _; use futures::StreamExt as _;
use pesde::{ use pesde::{
engine::{
runtime::{Runtime, RuntimeKind},
EngineKind,
},
errors::ManifestReadError, errors::ManifestReadError,
lockfile::Lockfile, lockfile::Lockfile,
manifest::{ manifest::{
@ -19,8 +23,10 @@ use pesde::{
Project, DEFAULT_INDEX_NAME, Project, DEFAULT_INDEX_NAME,
}; };
use relative_path::RelativePathBuf; use relative_path::RelativePathBuf;
use reporters::run_with_reporter;
use semver::Version;
use std::{ use std::{
collections::{BTreeMap, HashSet}, collections::{BTreeMap, HashMap, HashSet},
future::Future, future::Future,
path::PathBuf, path::PathBuf,
str::FromStr, str::FromStr,
@ -361,3 +367,88 @@ pub fn dep_type_to_key(dep_type: DependencyType) -> &'static str {
DependencyType::Peer => "peer_dependencies", DependencyType::Peer => "peer_dependencies",
} }
} }
#[cfg(feature = "version-management")]
pub async fn get_project_engines(
manifest: &Manifest,
reqwest: &reqwest::Client,
) -> anyhow::Result<HashMap<EngineKind, Version>> {
use tokio::task::JoinSet;
run_with_reporter(|_, root_progress, reporter| async {
let root_progress = root_progress;
let reporter = reporter;
root_progress.set_prefix(format!("{} {}: ", manifest.name, manifest.target));
root_progress.reset();
root_progress.set_message("update engines");
let mut tasks = manifest
.engines
.iter()
.map(|(engine, req)| {
let engine = *engine;
let req = req.clone();
let reqwest = reqwest.clone();
let reporter = reporter.clone();
async move {
let version = crate::cli::version::get_or_download_engine(
&reqwest, engine, req, reporter,
)
.await
.context("failed to install engine")?
.1;
crate::cli::version::make_linker_if_needed(engine)
.await
.context("failed to make engine linker")?;
Ok::<_, anyhow::Error>((engine, version))
}
})
.collect::<JoinSet<_>>();
let mut resolved_engine_versions = HashMap::new();
while let Some(task) = tasks.join_next().await {
let (engine, version) = task.unwrap()?;
resolved_engine_versions.insert(engine, version);
}
Ok::<_, anyhow::Error>(resolved_engine_versions)
})
.await
}
#[cfg(not(feature = "version-management"))]
pub async fn get_project_engines(
_manifest: &Manifest,
_reqwest: &reqwest::Client,
) -> anyhow::Result<HashMap<EngineKind, Version>> {
Ok(Default::default())
}
pub fn compatible_runtime(
target: TargetKind,
engines: &HashMap<EngineKind, Version>,
) -> anyhow::Result<Runtime> {
let runtime = match target {
TargetKind::Lune => RuntimeKind::Lune,
TargetKind::Luau => engines
.keys()
.find_map(|e| e.as_runtime())
.context("no runtime available")?,
TargetKind::Roblox | TargetKind::RobloxServer => {
anyhow::bail!("roblox targets cannot be ran!")
}
};
Ok(Runtime::new(
runtime,
engines
.get(&runtime.into())
.with_context(|| format!("{runtime} not available!"))?
.clone(),
))
}

View file

@ -1,6 +1,7 @@
use crate::{ use crate::{
all_packages_dirs, all_packages_dirs,
download::DownloadGraphOptions, download::DownloadGraphOptions,
engine::runtime::Engines,
graph::{ graph::{
DependencyGraph, DependencyGraphNode, DependencyGraphNodeWithTarget, DependencyGraph, DependencyGraphNode, DependencyGraphNodeWithTarget,
DependencyGraphWithTarget, DependencyGraphWithTarget,
@ -104,6 +105,8 @@ pub struct DownloadAndLinkOptions<Reporter = (), Hooks = ()> {
pub network_concurrency: NonZeroUsize, pub network_concurrency: NonZeroUsize,
/// Whether to re-install all dependencies even if they are already installed /// Whether to re-install all dependencies even if they are already installed
pub force: bool, pub force: bool,
/// The engines this project is using
pub engines: Arc<Engines>,
} }
impl<Reporter, Hooks> DownloadAndLinkOptions<Reporter, Hooks> impl<Reporter, Hooks> DownloadAndLinkOptions<Reporter, Hooks>
@ -122,6 +125,7 @@ where
install_dependencies_mode: InstallDependenciesMode::All, install_dependencies_mode: InstallDependenciesMode::All,
network_concurrency: NonZeroUsize::new(16).unwrap(), network_concurrency: NonZeroUsize::new(16).unwrap(),
force: false, force: false,
engines: Default::default(),
} }
} }
@ -169,6 +173,13 @@ where
self.force = force; self.force = force;
self self
} }
/// Sets the engines this project is using
#[must_use]
pub fn engines(mut self, engines: impl Into<Arc<Engines>>) -> Self {
self.engines = engines.into();
self
}
} }
impl Clone for DownloadAndLinkOptions { impl Clone for DownloadAndLinkOptions {
@ -181,6 +192,7 @@ impl Clone for DownloadAndLinkOptions {
install_dependencies_mode: self.install_dependencies_mode, install_dependencies_mode: self.install_dependencies_mode,
network_concurrency: self.network_concurrency, network_concurrency: self.network_concurrency,
force: self.force, force: self.force,
engines: self.engines.clone(),
} }
} }
} }
@ -209,6 +221,7 @@ impl Project {
install_dependencies_mode, install_dependencies_mode,
network_concurrency, network_concurrency,
force, force,
engines,
} = options; } = options;
let reqwest = reqwest.clone(); let reqwest = reqwest.clone();
@ -346,6 +359,7 @@ impl Project {
project: &Project, project: &Project,
manifest_target_kind: TargetKind, manifest_target_kind: TargetKind,
downloaded_graph: HashMap<PackageId, DependencyGraphNode>, downloaded_graph: HashMap<PackageId, DependencyGraphNode>,
engines: &Arc<Engines>,
) -> Result<(), errors::DownloadAndLinkError<Hooks::Error>> { ) -> Result<(), errors::DownloadAndLinkError<Hooks::Error>> {
let mut tasks = downloaded_graph let mut tasks = downloaded_graph
.into_iter() .into_iter()
@ -357,6 +371,7 @@ impl Project {
.into(); .into();
let id = Arc::new(id); let id = Arc::new(id);
let project = project.clone(); let project = project.clone();
let engines = engines.clone();
async move { async move {
let target = source let target = source
@ -366,6 +381,7 @@ impl Project {
project, project,
path, path,
id: id.clone(), id: id.clone(),
engines,
}, },
) )
.await?; .await?;
@ -392,11 +408,12 @@ impl Project {
self, self,
manifest.target.kind(), manifest.target.kind(),
other_graph_to_download, other_graph_to_download,
&engines,
) )
.instrument(tracing::debug_span!("get targets (non-wally)")) .instrument(tracing::debug_span!("get targets (non-wally)"))
.await?; .await?;
self.link_dependencies(&graph, false) self.link_dependencies(&graph, &engines, false)
.instrument(tracing::debug_span!("link (non-wally)")) .instrument(tracing::debug_span!("link (non-wally)"))
.await?; .await?;
@ -418,6 +435,7 @@ impl Project {
self, self,
manifest.target.kind(), manifest.target.kind(),
wally_graph_to_download, wally_graph_to_download,
&engines,
) )
.instrument(tracing::debug_span!("get targets (wally)")) .instrument(tracing::debug_span!("get targets (wally)"))
.await?; .await?;
@ -461,7 +479,7 @@ impl Project {
} }
// step 4. link ALL dependencies. do so with types // step 4. link ALL dependencies. do so with types
self.link_dependencies(&graph, true) self.link_dependencies(&graph, &engines, true)
.instrument(tracing::debug_span!("link (all)")) .instrument(tracing::debug_span!("link (all)"))
.await?; .await?;

View file

@ -1,3 +1,5 @@
/// Engines as runtimes
pub mod runtime;
/// Sources of engines /// Sources of engines
pub mod source; pub mod source;

92
src/engine/runtime.rs Normal file
View file

@ -0,0 +1,92 @@
use std::{
collections::HashMap,
ffi::OsStr,
fmt::{Debug, Display},
};
use semver::Version;
use serde::{Deserialize, Serialize};
use tokio::process::Command;
use super::EngineKind;
pub(crate) type Engines = HashMap<EngineKind, Version>;
/// A runtime
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[cfg_attr(test, derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum RuntimeKind {
/// The Lune runtime
Lune,
}
impl Display for RuntimeKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Lune => write!(f, "lune"),
}
}
}
/// Supported runtimes
#[derive(Debug, Clone)]
pub enum Runtime {
/// The [EngineKind::Lune] runtime
Lune(Version),
}
impl Runtime {
/// Prepares a [Command] to execute the given script with the given arguments
pub fn prepare_command<A: IntoIterator<Item = S> + Debug, S: AsRef<OsStr> + Debug>(
&self,
script_path: &OsStr,
args: A,
) -> Command {
let mut command = Command::new(match self {
Self::Lune(..) => "lune",
});
match self {
Self::Lune(version) => {
command.arg("run");
command.arg(script_path);
if *version < Version::new(0, 9, 0) {
command.arg("--");
}
command.args(args);
}
}
command
}
}
impl Runtime {
/// Creates a [Runtime] from the [RuntimeKind] and [Version]
#[must_use]
pub fn new(kind: RuntimeKind, version: Version) -> Self {
match kind {
RuntimeKind::Lune => Runtime::Lune(version),
}
}
}
impl EngineKind {
/// Returns this engine as a [RuntimeKind], if it is one
#[must_use]
pub fn as_runtime(self) -> Option<RuntimeKind> {
Some(match self {
EngineKind::Pesde => return None,
EngineKind::Lune => RuntimeKind::Lune,
})
}
}
impl From<RuntimeKind> for EngineKind {
fn from(value: RuntimeKind) -> Self {
match value {
RuntimeKind::Lune => EngineKind::Lune,
}
}
}

View file

@ -377,7 +377,7 @@ impl RefreshedSources {
async fn deser_manifest(path: &Path) -> Result<Manifest, errors::ManifestReadError> { async fn deser_manifest(path: &Path) -> Result<Manifest, errors::ManifestReadError> {
let string = fs::read_to_string(path.join(MANIFEST_FILE_NAME)).await?; let string = fs::read_to_string(path.join(MANIFEST_FILE_NAME)).await?;
toml::from_str(&string).map_err(|e| errors::ManifestReadError::Serde(path.to_path_buf(), e)) toml::from_str(&string).map_err(|e| errors::ManifestReadError::Serde(path.into(), e))
} }
/// Find the project & workspace directory roots /// Find the project & workspace directory roots
@ -398,7 +398,7 @@ pub async fn find_roots(
.await .await
.map_err(errors::ManifestReadError::Io)?; .map_err(errors::ManifestReadError::Io)?;
let manifest: Manifest = toml::from_str(&manifest) let manifest: Manifest = toml::from_str(&manifest)
.map_err(|e| errors::ManifestReadError::Serde(path.to_path_buf(), e))?; .map_err(|e| errors::ManifestReadError::Serde(path.into(), e))?;
if manifest.workspace_members.is_empty() { if manifest.workspace_members.is_empty() {
return Ok(HashSet::new()); return Ok(HashSet::new());
@ -479,7 +479,7 @@ pub(crate) fn all_packages_dirs() -> HashSet<String> {
/// Errors that can occur when using the pesde library /// Errors that can occur when using the pesde library
pub mod errors { pub mod errors {
use std::path::PathBuf; use std::path::Path;
use thiserror::Error; use thiserror::Error;
/// Errors that can occur when reading the manifest file /// Errors that can occur when reading the manifest file
@ -492,7 +492,7 @@ pub mod errors {
/// An error occurred while deserializing the manifest file /// An error occurred while deserializing the manifest file
#[error("error deserializing manifest file at {0}")] #[error("error deserializing manifest file at {0}")]
Serde(PathBuf, #[source] toml::de::Error), Serde(Box<Path>, #[source] toml::de::Error),
} }
/// Errors that can occur when reading the lockfile /// Errors that can occur when reading the lockfile

View file

@ -1,4 +1,5 @@
use crate::{ use crate::{
engine::runtime::Engines,
graph::{DependencyGraphNodeWithTarget, DependencyGraphWithTarget}, graph::{DependencyGraphNodeWithTarget, DependencyGraphWithTarget},
linking::generator::get_file_types, linking::generator::get_file_types,
manifest::{Alias, Manifest}, manifest::{Alias, Manifest},
@ -15,6 +16,7 @@ use std::{
collections::HashMap, collections::HashMap,
ffi::OsStr, ffi::OsStr,
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::Arc,
}; };
use tokio::task::{spawn_blocking, JoinSet}; use tokio::task::{spawn_blocking, JoinSet};
use tracing::{instrument, Instrument as _}; use tracing::{instrument, Instrument as _};
@ -64,6 +66,7 @@ impl Project {
pub(crate) async fn link_dependencies( pub(crate) async fn link_dependencies(
&self, &self,
graph: &DependencyGraphWithTarget, graph: &DependencyGraphWithTarget,
engines: &Arc<Engines>,
with_types: bool, with_types: bool,
) -> Result<(), errors::LinkingError> { ) -> Result<(), errors::LinkingError> {
let manifest = self.deser_manifest().await?; let manifest = self.deser_manifest().await?;
@ -88,6 +91,7 @@ impl Project {
let package_id = package_id.clone(); let package_id = package_id.clone();
let node = node.clone(); let node = node.clone();
let project = self.clone(); let project = self.clone();
let engines = engines.clone();
async move { async move {
let Some(lib_file) = node.target.lib_path() else { let Some(lib_file) = node.target.lib_path() else {
@ -131,6 +135,7 @@ impl Project {
execute_script( execute_script(
ScriptName::RobloxSyncConfigGenerator, ScriptName::RobloxSyncConfigGenerator,
&project, &project,
&engines,
LinkingExecuteScriptHooks, LinkingExecuteScriptHooks,
std::iter::once(container_folder.as_os_str()) std::iter::once(container_folder.as_os_str())
.chain(build_files.iter().map(OsStr::new)), .chain(build_files.iter().map(OsStr::new)),

View file

@ -3,16 +3,16 @@ use crate::cli::version::{check_for_updates, current_version, get_or_download_en
use crate::cli::{auth::get_tokens, display_err, style::ERROR_STYLE, PESDE_DIR}; use crate::cli::{auth::get_tokens, display_err, style::ERROR_STYLE, PESDE_DIR};
use anyhow::Context as _; use anyhow::Context as _;
use clap::{builder::styling::AnsiColor, Parser}; use clap::{builder::styling::AnsiColor, Parser};
use cli::data_dir; use cli::{compatible_runtime, data_dir, get_project_engines};
use fs_err::tokio as fs; use fs_err::tokio as fs;
use indicatif::MultiProgress; use indicatif::MultiProgress;
use pesde::{ use pesde::{
engine::EngineKind, find_roots, manifest::target::TargetKind, version_matches, AuthConfig, engine::EngineKind, find_roots, manifest::target::TargetKind, version_matches, AuthConfig,
Project, Project, MANIFEST_FILE_NAME,
}; };
use semver::VersionReq; use semver::VersionReq;
use std::{ use std::{
collections::HashSet, collections::HashMap,
io, io,
path::{Path, PathBuf}, path::{Path, PathBuf},
str::FromStr as _, str::FromStr as _,
@ -197,6 +197,14 @@ async fn run() -> anyhow::Result<()> {
.map_or_else(|| "none".to_string(), |p| p.display().to_string()) .map_or_else(|| "none".to_string(), |p| p.display().to_string())
); );
let reqwest = reqwest::Client::builder()
.user_agent(concat!(
env!("CARGO_PKG_NAME"),
"/",
env!("CARGO_PKG_VERSION")
))
.build()?;
'scripts: { 'scripts: {
// if we're an engine, we don't want to run any scripts // if we're an engine, we don't want to run any scripts
if exe_name_engine.is_ok() { if exe_name_engine.is_ok() {
@ -212,15 +220,23 @@ async fn run() -> anyhow::Result<()> {
let linker_file_name = format!("{exe_name}.bin.luau"); let linker_file_name = format!("{exe_name}.bin.luau");
let path = 'finder: { let (path, target) = 'finder: {
let all_folders = TargetKind::VARIANTS let all_folders = TargetKind::VARIANTS
.iter() .iter()
.flat_map(|a| TargetKind::VARIANTS.iter().map(|b| a.packages_folder(*b))) .copied()
.collect::<HashSet<_>>(); .filter(|t| t.has_bin())
.flat_map(|a| {
TargetKind::VARIANTS
.iter()
.copied()
.filter(|t| t.has_bin())
.map(move |b| (a.packages_folder(b), b))
})
.collect::<HashMap<_, _>>();
let mut tasks = all_folders let mut tasks = all_folders
.into_iter() .into_iter()
.map(|folder| { .map(|(folder, target)| {
let package_path = project_root_dir.join(&folder).join(&linker_file_name); let package_path = project_root_dir.join(&folder).join(&linker_file_name);
let workspace_path = project_workspace_dir let workspace_path = project_workspace_dir
.as_deref() .as_deref()
@ -228,12 +244,12 @@ async fn run() -> anyhow::Result<()> {
async move { async move {
if fs::metadata(&package_path).await.is_ok() { if fs::metadata(&package_path).await.is_ok() {
return Some((true, package_path)); return Some((true, package_path, target));
} }
if let Some(workspace_path) = workspace_path { if let Some(workspace_path) = workspace_path {
if fs::metadata(&workspace_path).await.is_ok() { if fs::metadata(&workspace_path).await.is_ok() {
return Some((false, workspace_path)); return Some((false, workspace_path, target));
} }
} }
@ -245,12 +261,12 @@ async fn run() -> anyhow::Result<()> {
let mut workspace_path = None; let mut workspace_path = None;
while let Some(res) = tasks.join_next().await { while let Some(res) = tasks.join_next().await {
if let Some((primary, path)) = res.unwrap() { if let Some((primary, path, target)) = res.unwrap() {
if primary { if primary {
break 'finder path; break 'finder (path, target);
} }
workspace_path = Some(path); workspace_path = Some((path, target));
} }
} }
@ -267,13 +283,18 @@ async fn run() -> anyhow::Result<()> {
std::process::exit(1i32); std::process::exit(1i32);
}; };
let status = std::process::Command::new("lune") let manifest = fs::read_to_string(project_root_dir.join(MANIFEST_FILE_NAME))
.arg("run") .await
.arg(path) .context("failed to read manifest")?;
.arg("--") let manifest = toml::de::from_str(&manifest).context("failed to deserialize manifest")?;
.args(std::env::args_os().skip(1))
let engines = get_project_engines(&manifest, &reqwest).await?;
let status = compatible_runtime(target, &engines)?
.prepare_command(path.as_os_str(), std::env::args_os().skip(1))
.current_dir(cwd) .current_dir(cwd)
.status() .status()
.await
.expect("failed to run lune"); .expect("failed to run lune");
std::process::exit(status.code().unwrap_or(1i32)); std::process::exit(status.code().unwrap_or(1i32));
@ -294,26 +315,6 @@ async fn run() -> anyhow::Result<()> {
AuthConfig::new().with_tokens(get_tokens().await?.0), AuthConfig::new().with_tokens(get_tokens().await?.0),
); );
let reqwest = {
let mut headers = reqwest::header::HeaderMap::new();
headers.insert(
reqwest::header::ACCEPT,
"application/json"
.parse()
.context("failed to create accept header")?,
);
reqwest::Client::builder()
.user_agent(concat!(
env!("CARGO_PKG_NAME"),
"/",
env!("CARGO_PKG_VERSION")
))
.default_headers(headers)
.build()?
};
#[cfg(feature = "version-management")] #[cfg(feature = "version-management")]
'engines: { 'engines: {
let Ok(engine) = exe_name_engine else { let Ok(engine) = exe_name_engine else {

View file

@ -1,5 +1,5 @@
use crate::{ use crate::{
engine::EngineKind, engine::{runtime::RuntimeKind, EngineKind},
manifest::{ manifest::{
overrides::{OverrideKey, OverrideSpecifier}, overrides::{OverrideKey, OverrideSpecifier},
target::Target, target::Target,
@ -51,8 +51,7 @@ pub struct Manifest {
pub private: bool, pub private: bool,
/// The scripts of the package /// The scripts of the package
#[serde(default, skip_serializing)] #[serde(default, skip_serializing)]
#[cfg_attr(test, schemars(with = "BTreeMap<String, std::path::PathBuf>"))] pub scripts: BTreeMap<String, Script>,
pub scripts: BTreeMap<String, RelativePathBuf>,
/// The indices to use for the package /// The indices to use for the package
#[serde( #[serde(
default, default,
@ -210,6 +209,24 @@ impl Alias {
} }
} }
/// A script
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(untagged)]
#[cfg_attr(test, derive(schemars::JsonSchema))]
pub enum Script {
/// A path only script
#[cfg_attr(test, schemars(with = "std::path::PathBuf"))]
Path(RelativePathBuf),
/// A script which specifies both its path and its runtime
RuntimePath {
/// The runtime to execute this script with
runtime: RuntimeKind,
/// The path of the script to run
#[cfg_attr(test, schemars(with = "std::path::PathBuf"))]
path: RelativePathBuf,
},
}
/// A dependency type /// A dependency type
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]

View file

@ -76,6 +76,12 @@ impl TargetKind {
pub fn is_roblox(self) -> bool { pub fn is_roblox(self) -> bool {
matches!(self, TargetKind::Roblox | TargetKind::RobloxServer) matches!(self, TargetKind::Roblox | TargetKind::RobloxServer)
} }
/// Returns whether this target supports bin exports
#[must_use]
pub fn has_bin(self) -> bool {
!self.is_roblox()
}
} }
/// A target of a package /// A target of a package

View file

@ -1,15 +1,17 @@
use crate::Project; use crate::{
engine::runtime::{Engines, Runtime},
manifest::Script,
Project,
};
use futures::FutureExt as _; use futures::FutureExt as _;
use relative_path::RelativePathBuf;
use std::{ use std::{
ffi::OsStr, ffi::OsStr,
fmt::{Debug, Display, Formatter}, fmt::{Debug, Display, Formatter},
path::PathBuf, path::PathBuf,
process::Stdio, process::Stdio,
}; };
use tokio::{ use tokio::io::{AsyncBufReadExt as _, BufReader};
io::{AsyncBufReadExt as _, BufReader},
process::Command,
};
use tracing::instrument; use tracing::instrument;
/// Script names used by pesde /// Script names used by pesde
@ -32,33 +34,63 @@ impl Display for ScriptName {
} }
} }
/// Extracts a script and a runtime out of a [Script]
pub fn parse_script(
script: Script,
engines: &Engines,
) -> Result<(Runtime, RelativePathBuf), errors::FindScriptError> {
Ok(match script {
Script::Path(path) => {
let runtime = engines
.iter()
.filter_map(|(engine, ver)| engine.as_runtime().map(|rt| (rt, ver)))
.collect::<Vec<_>>();
if runtime.len() != 1 {
return Err(errors::FindScriptError::AmbiguousRuntime);
}
let (runtime, version) = runtime[0];
(Runtime::new(runtime, version.clone()), path)
}
Script::RuntimePath { runtime, path } => {
let Some(version) = engines.get(&runtime.into()) else {
return Err(errors::FindScriptError::SpecifiedRuntimeUnknown(runtime));
};
(Runtime::new(runtime, version.clone()), path)
}
})
}
/// Finds a script in the project, whether it be in the current package or it's workspace /// Finds a script in the project, whether it be in the current package or it's workspace
pub async fn find_script( pub async fn find_script(
project: &Project, project: &Project,
engines: &Engines,
script_name: ScriptName, script_name: ScriptName,
) -> Result<Option<PathBuf>, errors::FindScriptError> { ) -> Result<Option<(Runtime, PathBuf)>, errors::FindScriptError> {
let script_name_str = script_name.to_string(); let script_name_str = script_name.to_string();
let script_path = match project let (script, base) = match project
.deser_manifest() .deser_manifest()
.await? .await?
.scripts .scripts
.remove(&script_name_str) .remove(&script_name_str)
{ {
Some(script) => script.to_path(project.package_dir()), Some(script) => (script, project.package_dir()),
None => match project None => match project
.deser_workspace_manifest() .deser_workspace_manifest()
.await? .await?
.and_then(|mut manifest| manifest.scripts.remove(&script_name_str)) .and_then(|mut manifest| manifest.scripts.remove(&script_name_str))
{ {
Some(script) => script.to_path(project.workspace_dir().unwrap()), Some(script) => (script, project.workspace_dir().unwrap()),
None => { None => {
return Ok(None); return Ok(None);
} }
}, },
}; };
Ok(Some(script_path)) parse_script(script, engines).map(|(rt, path)| Some((rt, path.to_path(base))))
} }
#[allow(unused_variables)] #[allow(unused_variables)]
@ -74,20 +106,18 @@ pub(crate) async fn execute_script<
>( >(
script_name: ScriptName, script_name: ScriptName,
project: &Project, project: &Project,
engines: &Engines,
hooks: H, hooks: H,
args: A, args: A,
return_stdout: bool, return_stdout: bool,
) -> Result<Option<String>, errors::ExecuteScriptError> { ) -> Result<Option<String>, errors::ExecuteScriptError> {
let Some(script_path) = find_script(project, script_name).await? else { let Some((runtime, script_path)) = find_script(project, engines, script_name).await? else {
hooks.not_found(script_name); hooks.not_found(script_name);
return Ok(None); return Ok(None);
}; };
match Command::new("lune") match runtime
.arg("run") .prepare_command(script_path.as_os_str(), args)
.arg(script_path.as_os_str())
.arg("--")
.args(args)
.current_dir(project.package_dir()) .current_dir(project.package_dir())
.stdin(Stdio::inherit()) .stdin(Stdio::inherit())
.stdout(Stdio::piped()) .stdout(Stdio::piped())
@ -146,6 +176,8 @@ pub(crate) async fn execute_script<
pub mod errors { pub mod errors {
use thiserror::Error; use thiserror::Error;
use crate::engine::runtime::RuntimeKind;
/// Errors that can occur when finding a script /// Errors that can occur when finding a script
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum FindScriptError { pub enum FindScriptError {
@ -156,6 +188,14 @@ pub mod errors {
/// An IO error occurred /// An IO error occurred
#[error("IO error")] #[error("IO error")]
Io(#[from] std::io::Error), Io(#[from] std::io::Error),
/// Ambiguous runtime
#[error("don't know which runtime to use. use specific form and specify the runtime")]
AmbiguousRuntime,
/// Runtime specified in script not in engines
#[error("runtime `{0}` was specified in the script, but it is not present in engines")]
SpecifiedRuntimeUnknown(RuntimeKind),
} }
/// Errors which can occur while executing a script /// Errors which can occur while executing a script

View file

@ -1,4 +1,5 @@
use crate::{ use crate::{
engine::runtime::Engines,
manifest::{ manifest::{
target::{Target, TargetKind}, target::{Target, TargetKind},
Alias, DependencyType, Alias, DependencyType,
@ -71,6 +72,8 @@ pub struct GetTargetOptions {
pub path: Arc<Path>, pub path: Arc<Path>,
/// The package ID of the package to be downloaded /// The package ID of the package to be downloaded
pub id: Arc<PackageId>, pub id: Arc<PackageId>,
/// The engines this project is using
pub engines: Arc<Engines>,
} }
/// A source of packages /// A source of packages

View file

@ -4,6 +4,7 @@ use relative_path::RelativePathBuf;
use serde::Deserialize; use serde::Deserialize;
use crate::{ use crate::{
engine::runtime::Engines,
manifest::target::Target, manifest::target::Target,
scripts::{execute_script, ExecuteScriptHooks, ScriptName}, scripts::{execute_script, ExecuteScriptHooks, ScriptName},
source::{ source::{
@ -33,11 +34,13 @@ impl ExecuteScriptHooks for CompatExecuteScriptHooks {
async fn find_lib_path( async fn find_lib_path(
project: &Project, project: &Project,
engines: &Engines,
package_dir: &Path, package_dir: &Path,
) -> Result<Option<RelativePathBuf>, errors::GetTargetError> { ) -> Result<Option<RelativePathBuf>, errors::GetTargetError> {
let Some(result) = execute_script( let Some(result) = execute_script(
ScriptName::SourcemapGenerator, ScriptName::SourcemapGenerator,
project, project,
engines,
CompatExecuteScriptHooks, CompatExecuteScriptHooks,
[package_dir], [package_dir],
true, true,
@ -60,9 +63,14 @@ pub(crate) const WALLY_MANIFEST_FILE_NAME: &str = "wally.toml";
pub(crate) async fn get_target( pub(crate) async fn get_target(
options: &GetTargetOptions, options: &GetTargetOptions,
) -> Result<Target, errors::GetTargetError> { ) -> Result<Target, errors::GetTargetError> {
let GetTargetOptions { project, path, .. } = options; let GetTargetOptions {
project,
path,
engines,
..
} = options;
let lib = find_lib_path(project, path) let lib = find_lib_path(project, engines, path)
.await? .await?
.or_else(|| Some(RelativePathBuf::from(LINK_LIB_NO_FILE_FOUND))); .or_else(|| Some(RelativePathBuf::from(LINK_LIB_NO_FILE_FOUND)));
let build_files = Default::default(); let build_files = Default::default();