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