diff --git a/src/cli/commands/execute.rs b/src/cli/commands/execute.rs index fdade01..0a55b99 100644 --- a/src/cli/commands/execute.rs +++ b/src/cli/commands/execute.rs @@ -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::, ()>::new(reqwest) + DownloadAndLinkOptions::, ()>::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); diff --git a/src/cli/commands/init.rs b/src/cli/commands/init.rs index 48cdb0d..668009a 100644 --- a/src/cli/commands/init.rs +++ b/src/cli/commands/init.rs @@ -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" diff --git a/src/cli/commands/mod.rs b/src/cli/commands/mod.rs index 9da2a5c..1007c82 100644 --- a/src/cli/commands/mod.rs +++ b/src/cli/commands/mod.rs @@ -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, diff --git a/src/cli/commands/publish.rs b/src/cli/commands/publish.rs index 96159f6..24d5f2c 100644 --- a/src/cli/commands/publish.rs +++ b/src/cli/commands/publish.rs @@ -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?; diff --git a/src/cli/commands/run.rs b/src/cli/commands/run.rs index 329b95a..a46388f 100644 --- a/src/cli/commands/run.rs +++ b/src/cli/commands/run.rs @@ -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) + .context("failed to deserialize manifest at root")?; + + run( + compatible_runtime(manifest.target.kind(), &engines)?, + &root, + &path, + ) + .await; } } diff --git a/src/cli/commands/self_install.rs b/src/cli/commands/self_install.rs index 24b5fa7..8405921 100644 --- a/src/cli/commands/self_install.rs +++ b/src/cli/commands/self_install.rs @@ -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(), ); }; diff --git a/src/cli/install.rs b/src/cli/install.rs index a5bda1a..a05954b 100644 --- a/src/cli/install.rs +++ b/src/cli/install.rs @@ -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::>(); - - 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")?; diff --git a/src/cli/mod.rs b/src/cli/mod.rs index c3fe53d..8eeb15b 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -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> { + 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::>(); + + 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> { + Ok(Default::default()) +} + +pub fn compatible_runtime( + target: TargetKind, + engines: &HashMap, +) -> anyhow::Result { + 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(), + )) +} diff --git a/src/download_and_link.rs b/src/download_and_link.rs index c701b4c..92d8888 100644 --- a/src/download_and_link.rs +++ b/src/download_and_link.rs @@ -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 { 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, } impl DownloadAndLinkOptions @@ -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>) -> 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, + engines: &Arc, ) -> Result<(), errors::DownloadAndLinkError> { 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?; diff --git a/src/engine/mod.rs b/src/engine/mod.rs index eae4a03..cf1f3c6 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -1,3 +1,5 @@ +/// Engines as runtimes +pub mod runtime; /// Sources of engines pub mod source; diff --git a/src/engine/runtime.rs b/src/engine/runtime.rs new file mode 100644 index 0000000..3abafe0 --- /dev/null +++ b/src/engine/runtime.rs @@ -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; + +/// 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 + Debug, S: AsRef + 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 { + Some(match self { + EngineKind::Pesde => return None, + EngineKind::Lune => RuntimeKind::Lune, + }) + } +} + +impl From for EngineKind { + fn from(value: RuntimeKind) -> Self { + match value { + RuntimeKind::Lune => EngineKind::Lune, + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 318bcc0..2c78750 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -377,7 +377,7 @@ impl RefreshedSources { async fn deser_manifest(path: &Path) -> Result { 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 { /// 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, #[source] toml::de::Error), } /// Errors that can occur when reading the lockfile diff --git a/src/linking/mod.rs b/src/linking/mod.rs index 65b001d..78f1c92 100644 --- a/src/linking/mod.rs +++ b/src/linking/mod.rs @@ -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, 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)), diff --git a/src/main.rs b/src/main.rs index 8110c6e..9f8e330 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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::>(); + .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::>(); 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 { diff --git a/src/manifest/mod.rs b/src/manifest/mod.rs index b6a6da5..9105de8 100644 --- a/src/manifest/mod.rs +++ b/src/manifest/mod.rs @@ -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"))] - pub scripts: BTreeMap, + pub scripts: BTreeMap, /// 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")] diff --git a/src/manifest/target.rs b/src/manifest/target.rs index 100ddc6..7617bfe 100644 --- a/src/manifest/target.rs +++ b/src/manifest/target.rs @@ -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 diff --git a/src/scripts.rs b/src/scripts.rs index 29337cb..42909ad 100644 --- a/src/scripts.rs +++ b/src/scripts.rs @@ -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::>(); + 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, errors::FindScriptError> { +) -> Result, 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, 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 diff --git a/src/source/traits.rs b/src/source/traits.rs index 5a09957..f5b85ac 100644 --- a/src/source/traits.rs +++ b/src/source/traits.rs @@ -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, /// The package ID of the package to be downloaded pub id: Arc, + /// The engines this project is using + pub engines: Arc, } /// A source of packages diff --git a/src/source/wally/compat_util.rs b/src/source/wally/compat_util.rs index 0de18d7..2a13bd1 100644 --- a/src/source/wally/compat_util.rs +++ b/src/source/wally/compat_util.rs @@ -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, 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 { - 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();