use std::{
path::{Path, PathBuf},
str::FromStr,
};
use crate::build;
use clap::Args;
use eyre::{eyre, OptionExt, Result};
use itertools::Itertools;
use lux_lib::{
build::{Build, BuildBehaviour},
config::Config,
lua_installation::LuaInstallation,
lua_rockspec::RemoteLuaRockspec,
lua_version::LuaVersion,
operations::{self, Install, PackageInstallSpec},
package::{PackageName, PackageReq},
progress::MultiProgress,
rockspec::Rockspec as _,
tree,
workspace::Workspace,
};
use path_slash::PathBufExt;
use tempfile::tempdir;
#[derive(Debug, Clone)]
pub enum PackageOrRockspec {
Package(PackageReq),
RockSpec(PathBuf),
}
impl FromStr for PackageOrRockspec {
type Err = eyre::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let path = PathBuf::from(s);
if path.is_file() {
Ok(Self::RockSpec(path))
} else {
let pkg = PackageReq::from_str(s).map_err(|err| {
eyre!(
"No file {0} found and cannot parse package query: {1}",
s,
err
)
})?;
Ok(Self::Package(pkg))
}
}
}
#[derive(Args)]
pub struct Pack {
#[clap(value_parser)]
package_or_rockspec: Option<PackageOrRockspec>,
}
fn has_matching_workspace_member(package_req: &PackageReq) -> Result<bool> {
let workspace = Workspace::current()?;
let has_match = workspace.is_some_and(|ws| {
ws.select_member(package_req.name()).is_ok_and(|project| {
project
.toml()
.version()
.is_ok_and(|version| package_req.version_req().matches(&version))
})
});
Ok(has_match)
}
async fn pack_workspace(
member: Option<&PackageName>,
dest_dir: &Path,
config: &Config,
) -> Result<Vec<PathBuf>> {
let workspace = Workspace::current_or_err()?;
let packages = match member {
Some(package_name) => {
let project = workspace.select_member(package_name)?;
project
.toml()
.into_remote(None)?
.to_lua_remote_rockspec_string()?;
let mut build = build::Build::default();
build.package = Some(package_name.clone());
build::build(build, config.clone())
}
None => {
for project in workspace.members() {
project
.toml()
.into_remote(None)?
.to_lua_remote_rockspec_string()?;
}
build::build(build::Build::default(), config.clone())
}
}
.await?;
if packages.is_empty() {
return Err(eyre!("build did not produce a package"));
}
let mut rock_paths = Vec::new();
for package in packages {
let tree = workspace.tree(config)?;
let rock_path = operations::Pack::new(dest_dir.to_path_buf(), tree, package)
.pack()
.await?;
rock_paths.push(rock_path);
}
Ok(rock_paths)
}
pub async fn pack(args: Pack, config: Config) -> Result<()> {
let lua_version = LuaVersion::from(&config)?.clone();
let dest_dir = std::env::current_dir()?;
let progress = MultiProgress::new_arc(&config);
let rock_paths: Vec<PathBuf> = match args.package_or_rockspec {
Some(PackageOrRockspec::Package(package_req))
if has_matching_workspace_member(&package_req)? =>
{
pack_workspace(Some(package_req.name()), &dest_dir, &config).await
}
Some(PackageOrRockspec::Package(package_req)) => {
let user_tree = config.user_tree(lua_version.clone())?;
match user_tree.match_rocks(&package_req)? {
lux_lib::tree::RockMatches::NotFound(_) => {
let temp_dir = tempdir()?;
let temp_config = config.with_tree(temp_dir.path().to_path_buf());
let tree = temp_config.user_tree(lua_version.clone())?;
let packages = Install::new(&temp_config)
.package(
PackageInstallSpec::new(package_req, tree::EntryType::Entrypoint)
.build_behaviour(BuildBehaviour::Force)
.build(),
)
.tree(tree.clone())
.progress(progress)
.install()
.await?;
let package = packages.first().ok_or_eyre("no packages installed")?;
let rock_path = operations::Pack::new(dest_dir, tree, package.clone())
.pack()
.await?;
Ok(vec![rock_path])
}
lux_lib::tree::RockMatches::Single(local_package_id) => {
let lockfile = user_tree.lockfile()?;
let package = lockfile
.get(&local_package_id)
.ok_or_eyre("package is installed, but was not found in the lockfile")?;
let rock_path = operations::Pack::new(dest_dir, user_tree, package.clone())
.pack()
.await?;
Ok(vec![rock_path])
}
lux_lib::tree::RockMatches::Many(vec) => {
let local_package_id = vec.first();
let lockfile = user_tree.lockfile()?;
let package = lockfile.get(local_package_id).ok_or_eyre(
"multiple package installations found, but not found in the lockfile",
)?;
let rock_path = operations::Pack::new(dest_dir, user_tree, package.clone())
.pack()
.await?;
Ok(vec![rock_path])
}
}
}
Some(PackageOrRockspec::RockSpec(rockspec_path)) => {
let content = tokio::fs::read_to_string(&rockspec_path).await?;
let rockspec = match rockspec_path
.extension()
.map(|ext| ext.to_string_lossy().to_string())
.unwrap_or("".into())
.as_str()
{
"rockspec" => Ok(RemoteLuaRockspec::new(&content)?),
_ => Err(eyre!(
"expected a path to a .rockspec or a package requirement."
)),
}?;
let temp_dir = tempdir()?;
let bar = progress.map(|p| p.new_bar());
let config = config.with_tree(temp_dir.path().to_path_buf());
let lua = LuaInstallation::new(
&lua_version,
&config,
&progress.map(|progress| progress.new_bar()),
)
.await?;
let tree = config.user_tree(lua_version)?;
let package = Build::new()
.rockspec(&rockspec)
.lua(&lua)
.tree(&tree)
.entry_type(tree::EntryType::Entrypoint)
.config(&config)
.progress(&bar)
.build()
.await?;
let rock_path = operations::Pack::new(dest_dir, tree, package)
.pack()
.await?;
Ok(vec![rock_path])
}
None => pack_workspace(None, &dest_dir, &config).await,
}?;
if rock_paths.len() > 1 {
let rock_paths = rock_paths
.iter()
.map(|path| path.to_slash_lossy().to_string())
.join("\n");
print!("packed rocks created at\n{}", rock_paths)
} else {
rock_paths
.first()
.iter()
.for_each(|path| print!("packed rock created at {}", path.display()));
}
Ok(())
}