Skip to main content

lux_cli/
add.rs

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