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 itertools::Itertools;
6use lux_lib::{
7    build::{Build, BuildBehaviour},
8    config::Config,
9    lua_installation::LuaInstallation,
10    lua_rockspec::RemoteLuaRockspec,
11    lua_version::LuaVersion,
12    operations::{self, Install, PackageInstallSpec},
13    package::PackageName,
14    progress::MultiProgress,
15    rockspec::Rockspec as _,
16    tree::{self, InstallTree},
17    workspace::Workspace,
18};
19use miette::{miette, IntoDiagnostic, Result};
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(miette!("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().into_diagnostic()?;
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().into_diagnostic()?;
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
123                        .first()
124                        .ok_or_else(|| miette!("no packages installed"))?;
125                    let rock_path = operations::Pack::new(dest_dir, tree, package.clone())
126                        .pack()
127                        .await?;
128                    Ok(vec![rock_path])
129                }
130                lux_lib::tree::RockMatches::Single(local_package_id) => {
131                    let lockfile = user_tree.lockfile()?;
132                    let package = lockfile.get(&local_package_id).ok_or_else(|| {
133                        miette!("package is installed, but was not found in the lockfile")
134                    })?;
135                    let rock_path = operations::Pack::new(dest_dir, user_tree, package.clone())
136                        .pack()
137                        .await?;
138                    Ok(vec![rock_path])
139                }
140                lux_lib::tree::RockMatches::Many(vec) => {
141                    let local_package_id = vec.first();
142                    let lockfile = user_tree.lockfile()?;
143                    let package = lockfile.get(local_package_id).ok_or_else(|| {
144                        miette!(
145                            "multiple package installations found, but not found in the lockfile"
146                        )
147                    })?;
148                    let rock_path = operations::Pack::new(dest_dir, user_tree, package.clone())
149                        .pack()
150                        .await?;
151                    Ok(vec![rock_path])
152                }
153            }
154        }
155        Some(PackageOrRockspec::RockSpec(rockspec_path)) => {
156            let content = tokio::fs::read_to_string(&rockspec_path)
157                .await
158                .into_diagnostic()?;
159            let rockspec = match rockspec_path
160                .extension()
161                .map(|ext| ext.to_string_lossy().to_string())
162                .unwrap_or("".into())
163                .as_str()
164            {
165                "rockspec" => Ok(RemoteLuaRockspec::new(&content)?),
166                _ => Err(miette!(
167                    "expected a path to a .rockspec or a package requirement."
168                )),
169            }?;
170            let temp_dir = tempdir().into_diagnostic()?;
171            let bar = progress.map(|p| p.new_bar());
172            let config = config.with_tree(temp_dir.path().to_path_buf());
173            let lua = LuaInstallation::new(
174                &lua_version,
175                &config,
176                &progress.map(|progress| progress.new_bar()),
177            )
178            .await?;
179            let tree = config.user_tree(lua_version)?;
180            let package = Build::new()
181                .rockspec(&rockspec)
182                .lua(&lua)
183                .tree(&tree)
184                .entry_type(tree::EntryType::Entrypoint)
185                .config(&config)
186                .progress(&bar)
187                .build()
188                .await?;
189            let rock_path = operations::Pack::new(dest_dir, tree, package)
190                .pack()
191                .await?;
192            Ok(vec![rock_path])
193        }
194        None => pack_workspace(None, &dest_dir, &config).await,
195    }?;
196
197    if rock_paths.len() > 1 {
198        let rock_paths = rock_paths
199            .iter()
200            .map(|path| path.to_slash_lossy().to_string())
201            .join("\n");
202        print!("packed rocks created at\n{}", rock_paths)
203    } else {
204        rock_paths
205            .first()
206            .iter()
207            .for_each(|path| print!("packed rock created at {}", path.display()));
208    }
209    Ok(())
210}