Skip to main content

lux_cli/
pack.rs

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