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::{
compatible_runtime,
config::read_config,
get_project_engines,
reporters::{self, CliReporter},
VersionedPackageName,
};
@ -28,7 +30,6 @@ use std::{
env::current_dir,
ffi::OsString,
io::{Stderr, Write as _},
process::Command,
sync::Arc,
};
@ -57,7 +58,7 @@ impl ExecuteCommand {
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(),
|multi_progress, root_progress, reporter| async {
let multi_progress = multi_progress;
@ -152,6 +153,8 @@ impl ExecuteCommand {
project: project.clone(),
path: tempdir.path().into(),
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
@ -175,7 +178,7 @@ impl ExecuteCommand {
project
.download_and_link(
&graph,
DownloadAndLinkOptions::<CliReporter<Stderr>, ()>::new(reqwest)
DownloadAndLinkOptions::<CliReporter<Stderr>, ()>::new(reqwest.clone())
.reporter(reporter)
.refreshed_sources(refreshed_sources)
.install_dependencies_mode(InstallDependenciesMode::Prod),
@ -183,7 +186,18 @@ impl ExecuteCommand {
.await
.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?;
@ -200,13 +214,11 @@ impl ExecuteCommand {
)
.context("failed to write to tempfile")?;
let status = Command::new("lune")
.arg("run")
.arg(caller.path())
.arg("--")
.args(&self.args)
let status = runtime
.prepare_command(caller.path().as_os_str(), self.args)
.current_dir(current_dir().context("failed to get current directory")?)
.status()
.await
.context("failed to run script")?;
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
path: Path::new("").into(),
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?;
@ -268,6 +270,8 @@ impl InitCommand {
.to_string(),
);
}
// TODO: add engines
} else {
println!(
"{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::Outdated(outdated) => outdated.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::Yank(yank) => yank.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? {
Some(lockfile) => {
let engines =
Arc::new(crate::cli::get_project_engines(&manifest, &reqwest).await?);
let mut tasks = lockfile
.graph
.iter()
@ -142,6 +145,7 @@ impl PublishCommand {
let id = Arc::new(id.clone());
let node = node.clone();
let refreshed_sources = refreshed_sources.clone();
let engines = engines.clone();
async move {
let source = node.pkg_ref.source();
@ -161,6 +165,7 @@ impl PublishCommand {
project,
path: container_folder.into(),
id,
engines,
},
)
.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 clap::Args;
use fs_err::tokio as fs;
use futures::{StreamExt as _, TryStreamExt as _};
use pesde::{
engine::runtime::Runtime,
errors::{ManifestReadError, WorkspaceMembersError},
linking::generator::generate_bin_linking_module,
manifest::Alias,
manifest::{Alias, Manifest},
names::{PackageName, PackageNames},
scripts::parse_script,
source::traits::{GetTargetOptions, PackageRef as _, PackageSource as _, RefreshOptions},
Project, MANIFEST_FILE_NAME,
};
use relative_path::RelativePathBuf;
use std::{
collections::HashSet, env::current_dir, ffi::OsString, io::Write as _, path::Path,
process::Command,
collections::HashSet, env::current_dir, ffi::OsString, io::Write as _, path::Path, sync::Arc,
};
#[derive(Debug, Args)]
@ -29,8 +30,15 @@ pub struct RunCommand {
}
impl RunCommand {
pub async fn run(self, project: Project) -> anyhow::Result<()> {
let run = |root: &Path, file_path: &Path| -> ! {
pub async fn run(self, project: Project, reqwest: reqwest::Client) -> anyhow::Result<()> {
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");
caller
.write_all(
@ -42,13 +50,11 @@ impl RunCommand {
)
.expect("failed to write to tempfile");
let status = Command::new("lune")
.arg("run")
.arg(caller.path())
.arg("--")
.args(&self.args)
let status = runtime
.prepare_command(caller.path().as_os_str(), self.args)
.current_dir(current_dir().expect("failed to get current directory"))
.status()
.await
.expect("failed to run script");
drop(caller);
@ -57,11 +63,13 @@ impl RunCommand {
};
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(
compatible_runtime(manifest.target.kind(), &engines)?,
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")
@ -130,6 +138,7 @@ impl RunCommand {
project,
path: container_folder.as_path().into(),
id: id.into(),
engines: engines.clone(),
},
)
.await?;
@ -140,15 +149,20 @@ impl RunCommand {
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 Some(script_path) = manifest.scripts.get(&package_or_script) {
if let Ok(mut manifest) = project.deser_manifest().await {
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(
runtime,
project.package_dir(),
&script_path.to_path(project.package_dir()),
);
)
.await;
}
}
@ -212,6 +226,17 @@ impl RunCommand {
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 {
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)]
{
if !self.skip_add_to_path {
@ -24,17 +29,11 @@ impl SelfInstallCommand {
use anyhow::Context as _;
use windows_registry::CURRENT_USER;
let bin_dir = crate::cli::bin_dir()?;
let env = CURRENT_USER
.create("Environment")
.context("failed to open Environment key")?;
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);
if !exists {
@ -68,7 +67,7 @@ and then restart your shell.
",
CLI_STYLE.apply_to(env!("CARGO_BIN_NAME")),
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 crate::cli::{
bin_dir, dep_type_to_key,
reporters::{self, run_with_reporter, CliReporter},
reporters::{self, CliReporter},
resolve_overrides, run_on_workspace_members,
style::{ADDED_STYLE, REMOVED_STYLE, WARN_PREFIX},
up_to_date_lockfile,
@ -25,7 +25,7 @@ use pesde::{
version_matches, Project, RefreshedSources, MANIFEST_FILE_NAME,
};
use std::{
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
collections::{BTreeMap, BTreeSet, HashSet},
num::NonZeroUsize,
path::Path,
sync::Arc,
@ -181,49 +181,7 @@ pub async fn install(
}
};
let progress_prefix = format!("{} {}: ", manifest.name, manifest.target);
#[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 resolved_engine_versions = Arc::new(super::get_project_engines(&manifest, &reqwest).await?);
let overrides = resolve_overrides(&manifest)?;
@ -232,7 +190,7 @@ pub async fn install(
let multi = multi;
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.set_message("resolve");
@ -318,7 +276,8 @@ pub async fn install(
.refreshed_sources(refreshed_sources.clone())
.install_dependencies_mode(options.install_dependencies_mode)
.network_concurrency(options.network_concurrency)
.force(options.force || has_irrecoverable_changes),
.force(options.force || has_irrecoverable_changes)
.engines(resolved_engine_versions.clone()),
)
.await
.context("failed to download and link dependencies")?;

View file

@ -5,6 +5,10 @@ use crate::cli::{
use anyhow::Context as _;
use futures::StreamExt as _;
use pesde::{
engine::{
runtime::{Runtime, RuntimeKind},
EngineKind,
},
errors::ManifestReadError,
lockfile::Lockfile,
manifest::{
@ -19,8 +23,10 @@ use pesde::{
Project, DEFAULT_INDEX_NAME,
};
use relative_path::RelativePathBuf;
use reporters::run_with_reporter;
use semver::Version;
use std::{
collections::{BTreeMap, HashSet},
collections::{BTreeMap, HashMap, HashSet},
future::Future,
path::PathBuf,
str::FromStr,
@ -361,3 +367,88 @@ pub fn dep_type_to_key(dep_type: DependencyType) -> &'static str {
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::{
all_packages_dirs,
download::DownloadGraphOptions,
engine::runtime::Engines,
graph::{
DependencyGraph, DependencyGraphNode, DependencyGraphNodeWithTarget,
DependencyGraphWithTarget,
@ -104,6 +105,8 @@ pub struct DownloadAndLinkOptions<Reporter = (), Hooks = ()> {
pub network_concurrency: NonZeroUsize,
/// Whether to re-install all dependencies even if they are already installed
pub force: bool,
/// The engines this project is using
pub engines: Arc<Engines>,
}
impl<Reporter, Hooks> DownloadAndLinkOptions<Reporter, Hooks>
@ -122,6 +125,7 @@ where
install_dependencies_mode: InstallDependenciesMode::All,
network_concurrency: NonZeroUsize::new(16).unwrap(),
force: false,
engines: Default::default(),
}
}
@ -169,6 +173,13 @@ where
self.force = force;
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 {
@ -181,6 +192,7 @@ impl Clone for DownloadAndLinkOptions {
install_dependencies_mode: self.install_dependencies_mode,
network_concurrency: self.network_concurrency,
force: self.force,
engines: self.engines.clone(),
}
}
}
@ -209,6 +221,7 @@ impl Project {
install_dependencies_mode,
network_concurrency,
force,
engines,
} = options;
let reqwest = reqwest.clone();
@ -346,6 +359,7 @@ impl Project {
project: &Project,
manifest_target_kind: TargetKind,
downloaded_graph: HashMap<PackageId, DependencyGraphNode>,
engines: &Arc<Engines>,
) -> Result<(), errors::DownloadAndLinkError<Hooks::Error>> {
let mut tasks = downloaded_graph
.into_iter()
@ -357,6 +371,7 @@ impl Project {
.into();
let id = Arc::new(id);
let project = project.clone();
let engines = engines.clone();
async move {
let target = source
@ -366,6 +381,7 @@ impl Project {
project,
path,
id: id.clone(),
engines,
},
)
.await?;
@ -392,11 +408,12 @@ impl Project {
self,
manifest.target.kind(),
other_graph_to_download,
&engines,
)
.instrument(tracing::debug_span!("get targets (non-wally)"))
.await?;
self.link_dependencies(&graph, false)
self.link_dependencies(&graph, &engines, false)
.instrument(tracing::debug_span!("link (non-wally)"))
.await?;
@ -418,6 +435,7 @@ impl Project {
self,
manifest.target.kind(),
wally_graph_to_download,
&engines,
)
.instrument(tracing::debug_span!("get targets (wally)"))
.await?;
@ -461,7 +479,7 @@ impl Project {
}
// 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)"))
.await?;

View file

@ -1,3 +1,5 @@
/// Engines as runtimes
pub mod runtime;
/// Sources of engines
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> {
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
@ -398,7 +398,7 @@ pub async fn find_roots(
.await
.map_err(errors::ManifestReadError::Io)?;
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() {
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
pub mod errors {
use std::path::PathBuf;
use std::path::Path;
use thiserror::Error;
/// Errors that can occur when reading the manifest file
@ -492,7 +492,7 @@ pub mod errors {
/// An error occurred while deserializing the manifest file
#[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

View file

@ -1,4 +1,5 @@
use crate::{
engine::runtime::Engines,
graph::{DependencyGraphNodeWithTarget, DependencyGraphWithTarget},
linking::generator::get_file_types,
manifest::{Alias, Manifest},
@ -15,6 +16,7 @@ use std::{
collections::HashMap,
ffi::OsStr,
path::{Path, PathBuf},
sync::Arc,
};
use tokio::task::{spawn_blocking, JoinSet};
use tracing::{instrument, Instrument as _};
@ -64,6 +66,7 @@ impl Project {
pub(crate) async fn link_dependencies(
&self,
graph: &DependencyGraphWithTarget,
engines: &Arc<Engines>,
with_types: bool,
) -> Result<(), errors::LinkingError> {
let manifest = self.deser_manifest().await?;
@ -88,6 +91,7 @@ impl Project {
let package_id = package_id.clone();
let node = node.clone();
let project = self.clone();
let engines = engines.clone();
async move {
let Some(lib_file) = node.target.lib_path() else {
@ -131,6 +135,7 @@ impl Project {
execute_script(
ScriptName::RobloxSyncConfigGenerator,
&project,
&engines,
LinkingExecuteScriptHooks,
std::iter::once(container_folder.as_os_str())
.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 anyhow::Context as _;
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 indicatif::MultiProgress;
use pesde::{
engine::EngineKind, find_roots, manifest::target::TargetKind, version_matches, AuthConfig,
Project,
Project, MANIFEST_FILE_NAME,
};
use semver::VersionReq;
use std::{
collections::HashSet,
collections::HashMap,
io,
path::{Path, PathBuf},
str::FromStr as _,
@ -197,6 +197,14 @@ async fn run() -> anyhow::Result<()> {
.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: {
// if we're an engine, we don't want to run any scripts
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 path = 'finder: {
let (path, target) = 'finder: {
let all_folders = TargetKind::VARIANTS
.iter()
.flat_map(|a| TargetKind::VARIANTS.iter().map(|b| a.packages_folder(*b)))
.collect::<HashSet<_>>();
.copied()
.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
.into_iter()
.map(|folder| {
.map(|(folder, target)| {
let package_path = project_root_dir.join(&folder).join(&linker_file_name);
let workspace_path = project_workspace_dir
.as_deref()
@ -228,12 +244,12 @@ async fn run() -> anyhow::Result<()> {
async move {
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 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;
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 {
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);
};
let status = std::process::Command::new("lune")
.arg("run")
.arg(path)
.arg("--")
.args(std::env::args_os().skip(1))
let manifest = fs::read_to_string(project_root_dir.join(MANIFEST_FILE_NAME))
.await
.context("failed to read manifest")?;
let manifest = toml::de::from_str(&manifest).context("failed to deserialize manifest")?;
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)
.status()
.await
.expect("failed to run lune");
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),
);
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")]
'engines: {
let Ok(engine) = exe_name_engine else {

View file

@ -1,5 +1,5 @@
use crate::{
engine::EngineKind,
engine::{runtime::RuntimeKind, EngineKind},
manifest::{
overrides::{OverrideKey, OverrideSpecifier},
target::Target,
@ -51,8 +51,7 @@ pub struct Manifest {
pub private: bool,
/// The scripts of the package
#[serde(default, skip_serializing)]
#[cfg_attr(test, schemars(with = "BTreeMap<String, std::path::PathBuf>"))]
pub scripts: BTreeMap<String, RelativePathBuf>,
pub scripts: BTreeMap<String, Script>,
/// The indices to use for the package
#[serde(
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
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[serde(rename_all = "snake_case")]

View file

@ -76,6 +76,12 @@ impl TargetKind {
pub fn is_roblox(self) -> bool {
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

View file

@ -1,15 +1,17 @@
use crate::Project;
use crate::{
engine::runtime::{Engines, Runtime},
manifest::Script,
Project,
};
use futures::FutureExt as _;
use relative_path::RelativePathBuf;
use std::{
ffi::OsStr,
fmt::{Debug, Display, Formatter},
path::PathBuf,
process::Stdio,
};
use tokio::{
io::{AsyncBufReadExt as _, BufReader},
process::Command,
};
use tokio::io::{AsyncBufReadExt as _, BufReader};
use tracing::instrument;
/// 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
pub async fn find_script(
project: &Project,
engines: &Engines,
script_name: ScriptName,
) -> Result<Option<PathBuf>, errors::FindScriptError> {
) -> Result<Option<(Runtime, PathBuf)>, errors::FindScriptError> {
let script_name_str = script_name.to_string();
let script_path = match project
let (script, base) = match project
.deser_manifest()
.await?
.scripts
.remove(&script_name_str)
{
Some(script) => script.to_path(project.package_dir()),
Some(script) => (script, project.package_dir()),
None => match project
.deser_workspace_manifest()
.await?
.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 => {
return Ok(None);
}
},
};
Ok(Some(script_path))
parse_script(script, engines).map(|(rt, path)| Some((rt, path.to_path(base))))
}
#[allow(unused_variables)]
@ -74,20 +106,18 @@ pub(crate) async fn execute_script<
>(
script_name: ScriptName,
project: &Project,
engines: &Engines,
hooks: H,
args: A,
return_stdout: bool,
) -> 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);
return Ok(None);
};
match Command::new("lune")
.arg("run")
.arg(script_path.as_os_str())
.arg("--")
.args(args)
match runtime
.prepare_command(script_path.as_os_str(), args)
.current_dir(project.package_dir())
.stdin(Stdio::inherit())
.stdout(Stdio::piped())
@ -146,6 +176,8 @@ pub(crate) async fn execute_script<
pub mod errors {
use thiserror::Error;
use crate::engine::runtime::RuntimeKind;
/// Errors that can occur when finding a script
#[derive(Debug, Error)]
pub enum FindScriptError {
@ -156,6 +188,14 @@ pub mod errors {
/// An IO error occurred
#[error("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

View file

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

View file

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