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};
use tracing::instrument;
#[allow(clippy::enum_variant_names)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum ScriptName {
RobloxSyncConfigGenerator,
#[cfg(feature = "wally-compat")]
SourcemapGenerator,
}
impl Display for ScriptName {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ScriptName::RobloxSyncConfigGenerator => write!(f, "roblox_sync_config_generator"),
#[cfg(feature = "wally-compat")]
ScriptName::SourcemapGenerator => write!(f, "sourcemap_generator"),
}
}
}
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)
}
})
}
pub async fn find_script(
project: &Project,
engines: &Engines,
script_name: ScriptName,
) -> Result<Option<(Runtime, PathBuf)>, errors::FindScriptError> {
let script_name_str = script_name.to_string();
let (script, base) = match project
.deser_manifest()
.await?
.scripts
.remove(&script_name_str)
{
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, project.workspace_dir().unwrap()),
None => {
return Ok(None);
}
},
};
parse_script(script, engines).map(|(rt, path)| Some((rt, path.to_path(base))))
}
#[allow(unused_variables)]
pub(crate) trait ExecuteScriptHooks {
fn not_found(&self, script: ScriptName) {}
}
impl ExecuteScriptHooks for () {
#[allow(unused_variables)]
fn not_found(&self, script: ScriptName) {}
}
#[instrument(skip(project, hooks), ret(level = "trace"), level = "debug")]
pub(crate) async fn execute_script<
A: IntoIterator<Item = S> + Debug,
S: AsRef<OsStr> + Debug,
H: ExecuteScriptHooks,
>(
script_name: ScriptName,
project: &Project,
engines: &Engines,
hooks: H,
args: A,
return_stdout: bool,
) -> Result<Option<String>, errors::ExecuteScriptError> {
let Some((runtime, script_path)) = find_script(project, engines, script_name).await? else {
hooks.not_found(script_name);
return Ok(None);
};
match runtime
.prepare_command(script_path.as_os_str(), args)
.current_dir(project.package_dir())
.stdin(Stdio::inherit())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
{
Ok(mut child) => {
let mut stdout = BufReader::new(child.stdout.take().unwrap()).lines();
let mut stderr = BufReader::new(child.stderr.take().unwrap()).lines();
let mut stdout_str = String::new();
loop {
tokio::select! {
Some(line) = stdout.next_line().map(Result::transpose) => match line {
Ok(line) => {
if return_stdout {
stdout_str.push_str(&line);
stdout_str.push('\n');
} else {
tracing::info!("[{script_name}]: {line}");
}
}
Err(e) => {
tracing::error!("ERROR IN READING STDOUT OF {script_name}: {e}");
}
},
Some(line) = stderr.next_line().map(Result::transpose) => match line {
Ok(line) => {
tracing::error!("[{script_name}]: {line}");
}
Err(e) => {
tracing::error!("ERROR IN READING STDERR OF {script_name}: {e}");
}
},
else => break,
}
}
Ok(return_stdout.then_some(stdout_str))
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
tracing::warn!("`{}` could not be found in PATH: {e}", runtime.kind());
Ok(None)
}
Err(e) => Err(e.into()),
}
}
pub mod errors {
use thiserror::Error;
use crate::engine::runtime::RuntimeKind;
#[derive(Debug, Error)]
pub enum FindScriptError {
#[error("error reading manifest")]
ManifestRead(#[from] crate::errors::ManifestReadError),
#[error("IO error")]
Io(#[from] std::io::Error),
#[error("don't know which runtime to use. use specific form and specify the runtime")]
AmbiguousRuntime,
#[error("runtime `{0}` was specified in the script, but it is not present in engines")]
SpecifiedRuntimeUnknown(RuntimeKind),
}
#[derive(Debug, Error)]
pub enum ExecuteScriptError {
#[error("finding the script failed")]
FindScript(#[from] FindScriptError),
#[error("IO error")]
Io(#[from] std::io::Error),
}
}