1use std::{
2 path::{Path, PathBuf},
3 str::FromStr,
4};
5
6use crate::build;
7use clap::Args;
8use eyre::{eyre, OptionExt, Result};
9use itertools::Itertools;
10use lux_lib::{
11 build::{Build, BuildBehaviour},
12 config::Config,
13 lua_installation::LuaInstallation,
14 lua_rockspec::RemoteLuaRockspec,
15 lua_version::LuaVersion,
16 operations::{self, Install, PackageInstallSpec},
17 package::{PackageName, PackageReq},
18 progress::MultiProgress,
19 rockspec::Rockspec as _,
20 tree,
21 workspace::Workspace,
22};
23use path_slash::PathBufExt;
24use tempfile::tempdir;
25
26#[derive(Debug, Clone)]
27pub enum PackageOrRockspec {
28 Package(PackageReq),
29 RockSpec(PathBuf),
30}
31
32impl FromStr for PackageOrRockspec {
33 type Err = eyre::Error;
34
35 fn from_str(s: &str) -> Result<Self, Self::Err> {
36 let path = PathBuf::from(s);
37 if path.is_file() {
38 Ok(Self::RockSpec(path))
39 } else {
40 let pkg = PackageReq::from_str(s).map_err(|err| {
41 eyre!(
42 "No file {0} found and cannot parse package query: {1}",
43 s,
44 err
45 )
46 })?;
47 Ok(Self::Package(pkg))
48 }
49 }
50}
51
52#[derive(Args)]
53pub struct Pack {
54 #[clap(value_parser)]
70 package_or_rockspec: Option<PackageOrRockspec>,
71}
72
73fn has_matching_workspace_member(package_req: &PackageReq) -> Result<bool> {
74 let workspace = Workspace::current()?;
75 let has_match = workspace.is_some_and(|ws| {
76 ws.select_member(package_req.name()).is_ok_and(|project| {
77 project
78 .toml()
79 .version()
80 .is_ok_and(|version| package_req.version_req().matches(&version))
81 })
82 });
83 Ok(has_match)
84}
85
86async fn pack_workspace(
87 member: Option<&PackageName>,
88 dest_dir: &Path,
89 config: &Config,
90) -> Result<Vec<PathBuf>> {
91 let workspace = Workspace::current_or_err()?;
92
93 let packages = match member {
96 Some(package_name) => {
98 let project = workspace.select_member(package_name)?;
99 project
100 .toml()
101 .into_remote(None)?
102 .to_lua_remote_rockspec_string()?;
103
104 let mut build = build::Build::default();
105 build.package = Some(package_name.clone());
106 build::build(build, config.clone())
107 }
108 None => {
110 for project in workspace.members() {
111 project
112 .toml()
113 .into_remote(None)?
114 .to_lua_remote_rockspec_string()?;
115 }
116 build::build(build::Build::default(), config.clone())
117 }
118 }
119 .await?;
120
121 if packages.is_empty() {
122 return Err(eyre!("build did not produce a package"));
123 }
124
125 let mut rock_paths = Vec::new();
126 for package in packages {
127 let tree = workspace.tree(config)?;
128 let rock_path = operations::Pack::new(dest_dir.to_path_buf(), tree, package)
129 .pack()
130 .await?;
131 rock_paths.push(rock_path);
132 }
133
134 Ok(rock_paths)
135}
136
137pub async fn pack(args: Pack, config: Config) -> Result<()> {
138 let lua_version = LuaVersion::from(&config)?.clone();
139 let dest_dir = std::env::current_dir()?;
140 let progress = MultiProgress::new_arc(&config);
141 let rock_paths: Vec<PathBuf> = match args.package_or_rockspec {
142 Some(PackageOrRockspec::Package(package_req))
143 if has_matching_workspace_member(&package_req)? =>
144 {
145 pack_workspace(Some(package_req.name()), &dest_dir, &config).await
146 }
147 Some(PackageOrRockspec::Package(package_req)) => {
148 let user_tree = config.user_tree(lua_version.clone())?;
149 match user_tree.match_rocks(&package_req)? {
150 lux_lib::tree::RockMatches::NotFound(_) => {
151 let temp_dir = tempdir()?;
152 let temp_config = config.with_tree(temp_dir.path().to_path_buf());
153 let tree = temp_config.user_tree(lua_version.clone())?;
154 let packages = Install::new(&temp_config)
155 .package(
156 PackageInstallSpec::new(package_req, tree::EntryType::Entrypoint)
157 .build_behaviour(BuildBehaviour::Force)
158 .build(),
159 )
160 .tree(tree.clone())
161 .progress(progress)
162 .install()
163 .await?;
164 let package = packages.first().ok_or_eyre("no packages installed")?;
165 let rock_path = operations::Pack::new(dest_dir, tree, package.clone())
166 .pack()
167 .await?;
168 Ok(vec![rock_path])
169 }
170 lux_lib::tree::RockMatches::Single(local_package_id) => {
171 let lockfile = user_tree.lockfile()?;
172 let package = lockfile
173 .get(&local_package_id)
174 .ok_or_eyre("package is installed, but was not found in the lockfile")?;
175 let rock_path = operations::Pack::new(dest_dir, user_tree, package.clone())
176 .pack()
177 .await?;
178 Ok(vec![rock_path])
179 }
180 lux_lib::tree::RockMatches::Many(vec) => {
181 let local_package_id = vec.first();
182 let lockfile = user_tree.lockfile()?;
183 let package = lockfile.get(local_package_id).ok_or_eyre(
184 "multiple package installations found, but not found in the lockfile",
185 )?;
186 let rock_path = operations::Pack::new(dest_dir, user_tree, package.clone())
187 .pack()
188 .await?;
189 Ok(vec![rock_path])
190 }
191 }
192 }
193 Some(PackageOrRockspec::RockSpec(rockspec_path)) => {
194 let content = tokio::fs::read_to_string(&rockspec_path).await?;
195 let rockspec = match rockspec_path
196 .extension()
197 .map(|ext| ext.to_string_lossy().to_string())
198 .unwrap_or("".into())
199 .as_str()
200 {
201 "rockspec" => Ok(RemoteLuaRockspec::new(&content)?),
202 _ => Err(eyre!(
203 "expected a path to a .rockspec or a package requirement."
204 )),
205 }?;
206 let temp_dir = tempdir()?;
207 let bar = progress.map(|p| p.new_bar());
208 let config = config.with_tree(temp_dir.path().to_path_buf());
209 let lua = LuaInstallation::new(
210 &lua_version,
211 &config,
212 &progress.map(|progress| progress.new_bar()),
213 )
214 .await?;
215 let tree = config.user_tree(lua_version)?;
216 let package = Build::new()
217 .rockspec(&rockspec)
218 .lua(&lua)
219 .tree(&tree)
220 .entry_type(tree::EntryType::Entrypoint)
221 .config(&config)
222 .progress(&bar)
223 .build()
224 .await?;
225 let rock_path = operations::Pack::new(dest_dir, tree, package)
226 .pack()
227 .await?;
228 Ok(vec![rock_path])
229 }
230 None => pack_workspace(None, &dest_dir, &config).await,
231 }?;
232
233 if rock_paths.len() > 1 {
234 let rock_paths = rock_paths
235 .iter()
236 .map(|path| path.to_slash_lossy().to_string())
237 .join("\n");
238 print!("packed rocks created at\n{}", rock_paths)
239 } else {
240 rock_paths
241 .first()
242 .iter()
243 .for_each(|path| print!("packed rock created at {}", path.display()));
244 }
245 Ok(())
246}