lux_cli/
add.rs

1use eyre::{OptionExt, Result};
2use itertools::{Either, Itertools};
3use lux_lib::{
4    config::Config,
5    progress::{MultiProgress, Progress, ProgressBar},
6    project::Project,
7    remote_package_db::RemotePackageDB,
8    rockspec::lua_dependency::{self},
9};
10
11use crate::utils::project::{
12    sync_build_dependencies_if_locked, sync_dependencies_if_locked,
13    sync_test_dependencies_if_locked, PackageReqOrGitShorthand,
14};
15
16#[derive(clap::Args)]
17pub struct Add {
18    /// Package or list of packages to install and add to the project's dependencies. {n}
19    /// Examples: "pkg", "pkg@1.0.0", "pkg>=1.0.0" {n}
20    /// If you do not specify a version requirement, lux will fetch the latest version. {n}
21    /// {n}
22    /// You can also specify git packages by providing a git URL shorthand. {n}
23    /// Example: "github:owner/repo" {n}
24    /// Supported git host prefixes are: "github:", "gitlab:", "sourcehut:" and "codeberg:". {n}
25    /// Lux will automatically fetch the latest SemVer tag or commit SHA if no SemVer tag is found. {n}
26    /// Note that projects with git dependencies cannot be published to luarocks.org.
27    package_req: Vec<PackageReqOrGitShorthand>,
28
29    /// Reinstall without prompt if a package is already installed.
30    #[arg(long, visible_short_alias = 'f')]
31    force: bool,
32
33    /// Install the package as a development dependency. {n}
34    /// Also called `dev`.
35    #[arg(short, long, alias = "dev", visible_short_aliases = ['d', 'b'])]
36    build: Option<Vec<PackageReqOrGitShorthand>>,
37
38    /// Install the package as a test dependency.
39    #[arg(short, long, visible_short_alias = 't')]
40    test: Option<Vec<PackageReqOrGitShorthand>>,
41}
42
43pub async fn add(data: Add, config: Config) -> Result<()> {
44    let mut project = Project::current()?.ok_or_eyre("No project found")?;
45
46    let db = RemotePackageDB::from_config(&config, &Progress::Progress(ProgressBar::new())).await?;
47
48    let progress = MultiProgress::new_arc();
49
50    let (dependencies, git_dependencies): (Vec<_>, Vec<_>) =
51        data.package_req.iter().partition_map(|req| match req {
52            PackageReqOrGitShorthand::PackageReq(req) => Either::Left(req.clone()),
53            PackageReqOrGitShorthand::GitShorthand(url) => Either::Right(url.clone()),
54        });
55
56    if !data.package_req.is_empty() {
57        project
58            .add(lua_dependency::DependencyType::Regular(dependencies), &db)
59            .await?;
60        project
61            .add_git(lua_dependency::LuaDependencyType::Regular(git_dependencies))
62            .await?;
63        sync_dependencies_if_locked(&project, progress.clone(), &config).await?;
64    }
65
66    let build_packages = data.build.unwrap_or_default();
67    if !build_packages.is_empty() {
68        let (dependencies, git_dependencies): (Vec<_>, Vec<_>) =
69            build_packages.iter().partition_map(|req| match req {
70                PackageReqOrGitShorthand::PackageReq(req) => Either::Left(req.clone()),
71                PackageReqOrGitShorthand::GitShorthand(url) => Either::Right(url.clone()),
72            });
73        project
74            .add(lua_dependency::DependencyType::Build(dependencies), &db)
75            .await?;
76        project
77            .add_git(lua_dependency::LuaDependencyType::Build(git_dependencies))
78            .await?;
79        sync_build_dependencies_if_locked(&project, progress.clone(), &config).await?;
80    }
81
82    let test_packages = data.test.unwrap_or_default();
83    if !test_packages.is_empty() {
84        let (dependencies, git_dependencies): (Vec<_>, Vec<_>) =
85            test_packages.iter().partition_map(|req| match req {
86                PackageReqOrGitShorthand::PackageReq(req) => Either::Left(req.clone()),
87                PackageReqOrGitShorthand::GitShorthand(url) => Either::Right(url.clone()),
88            });
89        project
90            .add(lua_dependency::DependencyType::Test(dependencies), &db)
91            .await?;
92        project
93            .add_git(lua_dependency::LuaDependencyType::Test(git_dependencies))
94            .await?;
95        sync_test_dependencies_if_locked(&project, progress.clone(), &config).await?;
96    }
97
98    Ok(())
99}
100
101#[cfg(test)]
102mod test {
103    use assert_fs::{prelude::PathCopy, TempDir};
104    use lux_lib::config::ConfigBuilder;
105    use serial_test::serial;
106
107    use super::*;
108    use std::path::PathBuf;
109
110    #[serial]
111    #[tokio::test]
112    async fn test_add_regular_dependencies() {
113        if std::env::var("LUX_SKIP_IMPURE_TESTS").unwrap_or("0".into()) == "1" {
114            println!("Skipping impure test");
115            return;
116        }
117        let sample_project: PathBuf = "resources/test/sample-project-init/".into();
118        let project_root = TempDir::new().unwrap();
119        project_root.copy_from(&sample_project, &["**"]).unwrap();
120        let cwd = std::env::current_dir().unwrap();
121        std::env::set_current_dir(&project_root).unwrap();
122        let config = ConfigBuilder::new().unwrap().build().unwrap();
123        let args = Add {
124            package_req: vec!["penlight@1.5".parse().unwrap()],
125            force: false,
126            build: Option::None,
127            test: Option::None,
128        };
129        add(args, config.clone()).await.unwrap();
130        let lockfile_path = project_root.join("lux.lock");
131        let lockfile_content =
132            String::from_utf8(tokio::fs::read(&lockfile_path).await.unwrap()).unwrap();
133        assert!(lockfile_content.contains("penlight"));
134        assert!(lockfile_content.contains("luafilesystem")); // dependency
135
136        let args = Add {
137            package_req: vec!["md5".parse().unwrap()],
138            force: false,
139            build: Option::None,
140            test: Option::None,
141        };
142        add(args, config.clone()).await.unwrap();
143        let lockfile_path = project_root.join("lux.lock");
144        let lockfile_content =
145            String::from_utf8(tokio::fs::read(&lockfile_path).await.unwrap()).unwrap();
146        assert!(lockfile_content.contains("penlight"));
147        assert!(lockfile_content.contains("luafilesystem"));
148        assert!(lockfile_content.contains("md5"));
149
150        std::env::set_current_dir(&cwd).unwrap();
151    }
152
153    #[serial]
154    #[tokio::test]
155    async fn test_add_build_dependencies() {
156        if std::env::var("LUX_SKIP_IMPURE_TESTS").unwrap_or("0".into()) == "1" {
157            println!("Skipping impure test");
158            return;
159        }
160        let sample_project: PathBuf = "resources/test/sample-project-init/".into();
161        let project_root = TempDir::new().unwrap();
162        project_root.copy_from(&sample_project, &["**"]).unwrap();
163        let cwd = std::env::current_dir().unwrap();
164        std::env::set_current_dir(&project_root).unwrap();
165        let config = ConfigBuilder::new().unwrap().build().unwrap();
166        let args = Add {
167            package_req: Vec::new(),
168            force: false,
169            build: Option::Some(vec!["penlight@1.5".parse().unwrap()]),
170            test: Option::None,
171        };
172        add(args, config.clone()).await.unwrap();
173        let lockfile_path = project_root.join("lux.lock");
174        let lockfile_content =
175            String::from_utf8(tokio::fs::read(&lockfile_path).await.unwrap()).unwrap();
176        assert!(lockfile_content.contains("penlight"));
177        assert!(lockfile_content.contains("luafilesystem")); // dependency
178
179        let args = Add {
180            package_req: Vec::new(),
181            force: false,
182            build: Option::Some(vec!["md5".parse().unwrap()]),
183            test: Option::None,
184        };
185        add(args, config.clone()).await.unwrap();
186        let lockfile_path = project_root.join("lux.lock");
187        let lockfile_content =
188            String::from_utf8(tokio::fs::read(&lockfile_path).await.unwrap()).unwrap();
189        assert!(lockfile_content.contains("penlight"));
190        assert!(lockfile_content.contains("luafilesystem"));
191        assert!(lockfile_content.contains("md5"));
192
193        std::env::set_current_dir(&cwd).unwrap();
194    }
195
196    #[serial]
197    #[tokio::test]
198    async fn test_add_test_dependencies() {
199        if std::env::var("LUX_SKIP_IMPURE_TESTS").unwrap_or("0".into()) == "1" {
200            println!("Skipping impure test");
201            return;
202        }
203        let sample_project: PathBuf = "resources/test/sample-project-init/".into();
204        let project_root = TempDir::new().unwrap();
205        project_root.copy_from(&sample_project, &["**"]).unwrap();
206        let cwd = std::env::current_dir().unwrap();
207        std::env::set_current_dir(&project_root).unwrap();
208        let config = ConfigBuilder::new().unwrap().build().unwrap();
209        let args = Add {
210            package_req: Vec::new(),
211            force: false,
212            build: Option::None,
213            test: Option::Some(vec!["penlight@1.5".parse().unwrap()]),
214        };
215        add(args, config.clone()).await.unwrap();
216        let lockfile_path = project_root.join("lux.lock");
217        let lockfile_content =
218            String::from_utf8(tokio::fs::read(&lockfile_path).await.unwrap()).unwrap();
219        assert!(lockfile_content.contains("penlight"));
220        assert!(lockfile_content.contains("luafilesystem")); // dependency
221
222        let args = Add {
223            package_req: Vec::new(),
224            force: false,
225            build: Option::None,
226            test: Option::Some(vec!["md5".parse().unwrap()]),
227        };
228        add(args, config.clone()).await.unwrap();
229        let lockfile_path = project_root.join("lux.lock");
230        let lockfile_content =
231            String::from_utf8(tokio::fs::read(&lockfile_path).await.unwrap()).unwrap();
232        assert!(lockfile_content.contains("penlight"));
233        assert!(lockfile_content.contains("luafilesystem"));
234        assert!(lockfile_content.contains("md5"));
235
236        std::env::set_current_dir(&cwd).unwrap();
237    }
238}