Skip to main content

lux_cli/
add.rs

1use itertools::{Either, Itertools};
2use lux_lib::{
3    config::Config,
4    package::PackageName,
5    progress::MultiProgress,
6    remote_package_db::RemotePackageDB,
7    rockspec::lua_dependency::{self},
8    workspace::Workspace,
9};
10use miette::Result;
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 tests {
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 =
147            PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/sample-projects/init/");
148        let project_root = TempDir::new().unwrap();
149        project_root.copy_from(&sample_project, &["**"]).unwrap();
150        let cwd = std::env::current_dir().unwrap();
151        std::env::set_current_dir(&project_root).unwrap();
152        let config = ConfigBuilder::new().unwrap().build().unwrap();
153        let args = Add {
154            package: None,
155            package_req: vec!["penlight@1.5".parse().unwrap()],
156            force: false,
157            build: Option::None,
158            test: Option::None,
159        };
160        add(args, config.clone()).await.unwrap();
161        let lockfile_path = project_root.join("lux.lock");
162        let lockfile_content =
163            String::from_utf8(tokio::fs::read(&lockfile_path).await.unwrap()).unwrap();
164        assert!(lockfile_content.contains("penlight"));
165        assert!(lockfile_content.contains("luafilesystem")); // dependency
166
167        let args = Add {
168            package: None,
169            package_req: vec!["md5".parse().unwrap()],
170            force: false,
171            build: Option::None,
172            test: Option::None,
173        };
174        add(args, config.clone()).await.unwrap();
175        let lockfile_path = project_root.join("lux.lock");
176        let lockfile_content =
177            String::from_utf8(tokio::fs::read(&lockfile_path).await.unwrap()).unwrap();
178        assert!(lockfile_content.contains("penlight"));
179        assert!(lockfile_content.contains("luafilesystem"));
180        assert!(lockfile_content.contains("md5"));
181
182        std::env::set_current_dir(&cwd).unwrap();
183    }
184
185    #[serial]
186    #[tokio::test]
187    async fn test_add_build_dependencies() {
188        if std::env::var("LUX_SKIP_IMPURE_TESTS").unwrap_or("0".into()) == "1" {
189            println!("Skipping impure test");
190            return;
191        }
192        let sample_project =
193            PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/sample-projects/init/");
194        let project_root = TempDir::new().unwrap();
195        project_root.copy_from(&sample_project, &["**"]).unwrap();
196        let cwd = std::env::current_dir().unwrap();
197        std::env::set_current_dir(&project_root).unwrap();
198        let config = ConfigBuilder::new().unwrap().build().unwrap();
199        let args = Add {
200            package: None,
201            package_req: Vec::new(),
202            force: false,
203            build: Option::Some(vec!["penlight@1.5".parse().unwrap()]),
204            test: Option::None,
205        };
206        add(args, config.clone()).await.unwrap();
207        let lockfile_path = project_root.join("lux.lock");
208        let lockfile_content =
209            String::from_utf8(tokio::fs::read(&lockfile_path).await.unwrap()).unwrap();
210        assert!(lockfile_content.contains("penlight"));
211        assert!(lockfile_content.contains("luafilesystem")); // dependency
212
213        let args = Add {
214            package: None,
215            package_req: Vec::new(),
216            force: false,
217            build: Option::Some(vec!["md5".parse().unwrap()]),
218            test: Option::None,
219        };
220        add(args, config.clone()).await.unwrap();
221        let lockfile_path = project_root.join("lux.lock");
222        let lockfile_content =
223            String::from_utf8(tokio::fs::read(&lockfile_path).await.unwrap()).unwrap();
224        assert!(lockfile_content.contains("penlight"));
225        assert!(lockfile_content.contains("luafilesystem"));
226        assert!(lockfile_content.contains("md5"));
227
228        std::env::set_current_dir(&cwd).unwrap();
229    }
230
231    #[serial]
232    #[tokio::test]
233    async fn test_add_test_dependencies() {
234        if std::env::var("LUX_SKIP_IMPURE_TESTS").unwrap_or("0".into()) == "1" {
235            println!("Skipping impure test");
236            return;
237        }
238        let sample_project =
239            PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/sample-projects/init/");
240        let project_root = TempDir::new().unwrap();
241        project_root.copy_from(&sample_project, &["**"]).unwrap();
242        let cwd = std::env::current_dir().unwrap();
243        std::env::set_current_dir(&project_root).unwrap();
244        let config = ConfigBuilder::new().unwrap().build().unwrap();
245        let args = Add {
246            package: None,
247            package_req: Vec::new(),
248            force: false,
249            build: Option::None,
250            test: Option::Some(vec!["penlight@1.5".parse().unwrap()]),
251        };
252        add(args, config.clone()).await.unwrap();
253        let lockfile_path = project_root.join("lux.lock");
254        let lockfile_content =
255            String::from_utf8(tokio::fs::read(&lockfile_path).await.unwrap()).unwrap();
256        assert!(lockfile_content.contains("penlight"));
257        assert!(lockfile_content.contains("luafilesystem")); // dependency
258
259        let args = Add {
260            package: None,
261            package_req: Vec::new(),
262            force: false,
263            build: Option::None,
264            test: Option::Some(vec!["md5".parse().unwrap()]),
265        };
266        add(args, config.clone()).await.unwrap();
267        let lockfile_path = project_root.join("lux.lock");
268        let lockfile_content =
269            String::from_utf8(tokio::fs::read(&lockfile_path).await.unwrap()).unwrap();
270        assert!(lockfile_content.contains("penlight"));
271        assert!(lockfile_content.contains("luafilesystem"));
272        assert!(lockfile_content.contains("md5"));
273
274        std::env::set_current_dir(&cwd).unwrap();
275    }
276}