use crate::cli::{
compatible_runtime, get_project_engines, style::WARN_STYLE, up_to_date_lockfile,
ExecReplace as _,
};
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, get_bin_require_path},
manifest::{Alias, Manifest},
names::{PackageName, PackageNames},
scripts::parse_script,
source::traits::{GetTargetOptions, PackageRef as _, PackageSource as _, RefreshOptions},
Project, MANIFEST_FILE_NAME,
};
use relative_path::{RelativePath, RelativePathBuf};
use std::{
collections::HashSet, env::current_dir, ffi::OsString, io::Write as _, path::Path, sync::Arc,
};
#[derive(Debug, Args)]
pub struct RunCommand {
#[arg(index = 1)]
package_or_script: Option<String>,
#[arg(index = 2, last = true)]
args: Vec<OsString>,
}
impl RunCommand {
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, project.auth_config()).await?);
let run = async |runtime: Runtime, root: &Path, file_path: &Path| -> ! {
let tempdir = project.cas_dir().join(".tmp");
fs::create_dir_all(&tempdir)
.await
.expect("failed to create temporary directory");
let mut caller = tempfile::Builder::new()
.suffix(".luau")
.tempfile_in(&tempdir)
.expect("failed to create tempfile");
caller
.write_all(
generate_bin_linking_module(
root,
&get_bin_require_path(
&tempdir,
RelativePath::from_path(
file_path
.file_name()
.unwrap()
.to_str()
.expect("path contains invalid characters"),
)
.unwrap(),
file_path.parent().unwrap(),
),
)
.as_bytes(),
)
.expect("failed to write to tempfile");
let mut command = runtime.prepare_command(caller.path().as_os_str(), self.args);
command.current_dir(current_dir().expect("failed to get current directory"));
command.exec_replace()
};
let Some(package_or_script) = self.package_or_script else {
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")
};
let mut package_info = None;
if let Ok(pkg_name) = package_or_script.parse::<PackageName>() {
let graph = if let Some(lockfile) = up_to_date_lockfile(&project).await? {
lockfile.graph
} else {
anyhow::bail!("outdated lockfile, please run the install command first")
};
let pkg_name = PackageNames::Pesde(pkg_name);
let mut versions = graph
.into_iter()
.filter(|(id, node)| *id.name() == pkg_name && node.direct.is_some())
.collect::<Vec<_>>();
package_info = Some(match versions.len() {
0 => anyhow::bail!("package not found"),
1 => versions.pop().unwrap(),
_ => anyhow::bail!("multiple versions found. use the package's alias instead."),
});
} else if let Ok(alias) = package_or_script.parse::<Alias>() {
if let Some(lockfile) = up_to_date_lockfile(&project).await? {
package_info = lockfile
.graph
.into_iter()
.find(|(_, node)| node.direct.as_ref().is_some_and(|(a, _, _)| alias == *a));
} else {
eprintln!(
"{}",
WARN_STYLE.apply_to(
"outdated lockfile, please run the install command first to use an alias"
)
);
};
}
if let Some((id, node)) = package_info {
let container_folder = node.container_folder_from_project(
&id,
&project,
project
.deser_manifest()
.await
.context("failed to deserialize manifest")?
.target
.kind(),
);
let source = node.pkg_ref.source();
source
.refresh(&RefreshOptions {
project: project.clone(),
})
.await
.context("failed to refresh source")?;
let target = source
.get_target(
&node.pkg_ref,
&GetTargetOptions {
project: project.clone(),
path: container_folder.as_path().into(),
id: id.into(),
engines: engines.clone(),
},
)
.await?;
let Some(bin_path) = target.bin_path() else {
anyhow::bail!("package has no bin path");
};
let path = bin_path.to_path(&container_folder);
run(compatible_runtime(target.kind(), &engines)?, &path, &path).await;
}
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;
}
}
let relative_path = RelativePathBuf::from(package_or_script);
let path = relative_path.to_path(project.package_dir());
if fs::metadata(&path).await.is_err() {
anyhow::bail!("path `{}` does not exist", path.display());
}
let workspace_dir = project
.workspace_dir()
.unwrap_or_else(|| project.package_dir());
let members = match project.workspace_members(false).await {
Ok(members) => members.boxed(),
Err(WorkspaceMembersError::ManifestParse(ManifestReadError::Io(e)))
if e.kind() == std::io::ErrorKind::NotFound =>
{
futures::stream::empty().boxed()
}
Err(e) => Err(e).context("failed to get workspace members")?,
};
let members = members
.then(|res| async {
fs::canonicalize(res.map_err(anyhow::Error::from)?.0)
.await
.map_err(anyhow::Error::from)
})
.chain(futures::stream::once(async {
fs::canonicalize(workspace_dir).await.map_err(Into::into)
}))
.try_collect::<HashSet<_>>()
.await
.context("failed to collect workspace members")?;
let root = 'finder: {
let mut current_path = path.clone();
loop {
let canonical_path = fs::canonicalize(¤t_path)
.await
.context("failed to canonicalize parent")?;
if members.contains(&canonical_path)
&& fs::metadata(canonical_path.join(MANIFEST_FILE_NAME))
.await
.is_ok()
{
break 'finder canonical_path;
}
if let Some(parent) = current_path.parent() {
current_path = parent.to_path_buf();
} else {
break;
}
}
project.package_dir().to_path_buf()
};
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;
}
}