Skip to main content

lux_cli/
pack.rs

1use std::path::{Path, PathBuf};
2
3use crate::{args::PackageOrRockspec, build, workspace::exists_matching_workspace_member};
4use clap::Args;
5use eyre::{eyre, OptionExt, Result};
6use itertools::Itertools;
7use lux_lib::{
8    build::{Build, BuildBehaviour},
9    config::Config,
10    lua_installation::LuaInstallation,
11    lua_rockspec::RemoteLuaRockspec,
12    lua_version::LuaVersion,
13    operations::{self, Install, PackageInstallSpec},
14    package::PackageName,
15    progress::MultiProgress,
16    rockspec::Rockspec as _,
17    tree::{self, InstallTree},
18    workspace::Workspace,
19};
20use path_slash::PathBufExt;
21use tempfile::tempdir;
22
23#[derive(Args)]
24pub struct Pack {
25    /// Path to a RockSpec or a package query for a package to pack.{n}
26    /// Prioritises local projects if in a workspace, then installed rocks.{n}
27    /// If there is no matching workspace member or installed rock,{n}
28    /// a rock will be downloaded and installed to a temporary directory.{n}
29    /// In case of multiple matches, the latest version will be packed.{n}
30    ///{n}
31    /// Examples:{n}
32    ///     - "pkg"{n}
33    ///     - "pkg@1.0.0"{n}
34    ///     - "pkg>=1.0.0"{n}
35    ///     - "/path/to/foo-1.0.0-1.rockspec"{n}
36    ///{n}
37    /// If not set, lux will attempt to pack either all workspace members{n}
38    /// or the current project.{n}
39    /// To pack a project, lux must be able to generate a release or dev RockSpec.{n}
40    #[clap(value_parser)]
41    package_or_rockspec: Option<PackageOrRockspec>,
42}
43
44async fn pack_workspace(
45    member: Option<&PackageName>,
46    dest_dir: &Path,
47    config: &Config,
48) -> Result<Vec<PathBuf>> {
49    let workspace = Workspace::current_or_err()?;
50
51    // luarocks expects a `<package>-<version>.rockspec` in the package root,
52    // so we add a guard that it can be created here.
53    let packages = match member {
54        // Pack only the provided workspace member
55        Some(package_name) => {
56            let project = workspace.select_member(package_name)?;
57            project
58                .toml()
59                .into_remote(None)?
60                .to_lua_remote_rockspec_string()?;
61
62            let mut build = build::Build::default();
63            build.package = Some(package_name.clone());
64            build::build(build, config.clone())
65        }
66        // Pack all workspace members
67        None => {
68            for project in workspace.members() {
69                project
70                    .toml()
71                    .into_remote(None)?
72                    .to_lua_remote_rockspec_string()?;
73            }
74            build::build(build::Build::default(), config.clone())
75        }
76    }
77    .await?;
78
79    if packages.is_empty() {
80        return Err(eyre!("build did not produce a package"));
81    }
82
83    let mut rock_paths = Vec::new();
84    for package in packages {
85        let tree = workspace.tree(config)?;
86        let rock_path = operations::Pack::new(dest_dir.to_path_buf(), tree, package)
87            .pack()
88            .await?;
89        rock_paths.push(rock_path);
90    }
91
92    Ok(rock_paths)
93}
94
95pub async fn pack(args: Pack, config: Config) -> Result<()> {
96    let lua_version = LuaVersion::from(&config)?.clone();
97    let dest_dir = std::env::current_dir()?;
98    let progress = MultiProgress::new_arc(&config);
99    let rock_paths: Vec<PathBuf> = match args.package_or_rockspec {
100        Some(PackageOrRockspec::Package(package_req))
101            if exists_matching_workspace_member(&package_req)? =>
102        {
103            pack_workspace(Some(package_req.name()), &dest_dir, &config).await
104        }
105        Some(PackageOrRockspec::Package(package_req)) => {
106            let user_tree = config.user_tree(lua_version.clone())?;
107            match user_tree.match_rocks(&package_req)? {
108                lux_lib::tree::RockMatches::NotFound(_) => {
109                    let temp_dir = tempdir()?;
110                    let temp_config = config.with_tree(temp_dir.path().to_path_buf());
111                    let tree = temp_config.user_tree(lua_version.clone())?;
112                    let packages = Install::new(&temp_config)
113                        .package(
114                            PackageInstallSpec::new(package_req, tree::EntryType::Entrypoint)
115                                .build_behaviour(BuildBehaviour::Force)
116                                .build(),
117                        )
118                        .tree(tree.clone())
119                        .progress(progress)
120                        .install()
121                        .await?;
122                    let package = packages.first().ok_or_eyre("no packages installed")?;
123                    let rock_path = operations::Pack::new(dest_dir, tree, package.clone())
124                        .pack()
125                        .await?;
126                    Ok(vec![rock_path])
127                }
128                lux_lib::tree::RockMatches::Single(local_package_id) => {
129                    let lockfile = user_tree.lockfile()?;
130                    let package = lockfile
131                        .get(&local_package_id)
132                        .ok_or_eyre("package is installed, but was not found in the lockfile")?;
133                    let rock_path = operations::Pack::new(dest_dir, user_tree, package.clone())
134                        .pack()
135                        .await?;
136                    Ok(vec![rock_path])
137                }
138                lux_lib::tree::RockMatches::Many(vec) => {
139                    let local_package_id = vec.first();
140                    let lockfile = user_tree.lockfile()?;
141                    let package = lockfile.get(local_package_id).ok_or_eyre(
142                        "multiple package installations found, but not found in the lockfile",
143                    )?;
144                    let rock_path = operations::Pack::new(dest_dir, user_tree, package.clone())
145                        .pack()
146                        .await?;
147                    Ok(vec![rock_path])
148                }
149            }
150        }
151        Some(PackageOrRockspec::RockSpec(rockspec_path)) => {
152            let content = tokio::fs::read_to_string(&rockspec_path).await?;
153            let rockspec = match rockspec_path
154                .extension()
155                .map(|ext| ext.to_string_lossy().to_string())
156                .unwrap_or("".into())
157                .as_str()
158            {
159                "rockspec" => Ok(RemoteLuaRockspec::new(&content)?),
160                _ => Err(eyre!(
161                    "expected a path to a .rockspec or a package requirement."
162                )),
163            }?;
164            let temp_dir = tempdir()?;
165            let bar = progress.map(|p| p.new_bar());
166            let config = config.with_tree(temp_dir.path().to_path_buf());
167            let lua = LuaInstallation::new(
168                &lua_version,
169                &config,
170                &progress.map(|progress| progress.new_bar()),
171            )
172            .await?;
173            let tree = config.user_tree(lua_version)?;
174            let package = Build::new()
175                .rockspec(&rockspec)
176                .lua(&lua)
177                .tree(&tree)
178                .entry_type(tree::EntryType::Entrypoint)
179                .config(&config)
180                .progress(&bar)
181                .build()
182                .await?;
183            let rock_path = operations::Pack::new(dest_dir, tree, package)
184                .pack()
185                .await?;
186            Ok(vec![rock_path])
187        }
188        None => pack_workspace(None, &dest_dir, &config).await,
189    }?;
190
191    if rock_paths.len() > 1 {
192        let rock_paths = rock_paths
193            .iter()
194            .map(|path| path.to_slash_lossy().to_string())
195            .join("\n");
196        print!("packed rocks created at\n{}", rock_paths)
197    } else {
198        rock_paths
199            .first()
200            .iter()
201            .for_each(|path| print!("packed rock created at {}", path.display()));
202    }
203    Ok(())
204}