Skip to main content

lux_cli/
pack.rs

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    /// Path to a RockSpec or a package query for a package to pack.{n}
55    /// Prioritises local projects if in a workspace, then installed rocks.{n}
56    /// If there is no matching workspace member or installed rock,{n}
57    /// a rock will be downloaded and installed to a temporary directory.{n}
58    /// In case of multiple matches, the latest version will be packed.{n}
59    ///{n}
60    /// Examples:{n}
61    ///     - "pkg"{n}
62    ///     - "pkg@1.0.0"{n}
63    ///     - "pkg>=1.0.0"{n}
64    ///     - "/path/to/foo-1.0.0-1.rockspec"{n}
65    ///{n}
66    /// If not set, lux will attempt to pack either all workspace members{n}
67    /// or the current project.{n}
68    /// To pack a project, lux must be able to generate a release or dev RockSpec.{n}
69    #[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    // luarocks expects a `<package>-<version>.rockspec` in the package root,
94    // so we add a guard that it can be created here.
95    let packages = match member {
96        // Pack only the provided workspace member
97        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        // Pack all workspace members
109        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}