Skip to main content

lux_cli/
add.rs

1use eyre::{OptionExt, Result};
2use itertools::{Either, Itertools};
3use lux_lib::{
4    config::Config,
5    progress::MultiProgress,
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 progress = MultiProgress::new(&config);
47    let bar = progress.map(MultiProgress::new_bar);
48    let db = RemotePackageDB::from_config(&config, &bar).await?;
49
50    let progress = MultiProgress::new_arc(&config);
51
52    let (dependencies, git_dependencies): (Vec<_>, Vec<_>) =
53        data.package_req.iter().partition_map(|req| match req {
54            PackageReqOrGitShorthand::PackageReq(req) => Either::Left(req.clone()),
55            PackageReqOrGitShorthand::GitShorthand(url) => Either::Right(url.clone()),
56        });
57
58    if !data.package_req.is_empty() {
59        project
60            .add(lua_dependency::DependencyType::Regular(dependencies), &db)
61            .await?;
62        project
63            .add_git(lua_dependency::LuaDependencyType::Regular(git_dependencies))
64            .await?;
65        sync_dependencies_if_locked(&project, progress.clone(), &config).await?;
66    }
67
68    let build_packages = data.build.unwrap_or_default();
69    if !build_packages.is_empty() {
70        let (dependencies, git_dependencies): (Vec<_>, Vec<_>) =
71            build_packages.iter().partition_map(|req| match req {
72                PackageReqOrGitShorthand::PackageReq(req) => Either::Left(req.clone()),
73                PackageReqOrGitShorthand::GitShorthand(url) => Either::Right(url.clone()),
74            });
75        project
76            .add(lua_dependency::DependencyType::Build(dependencies), &db)
77            .await?;
78        project
79            .add_git(lua_dependency::LuaDependencyType::Build(git_dependencies))
80            .await?;
81        sync_build_dependencies_if_locked(&project, progress.clone(), &config).await?;
82    }
83
84    let test_packages = data.test.unwrap_or_default();
85    if !test_packages.is_empty() {
86        let (dependencies, git_dependencies): (Vec<_>, Vec<_>) =
87            test_packages.iter().partition_map(|req| match req {
88                PackageReqOrGitShorthand::PackageReq(req) => Either::Left(req.clone()),
89                PackageReqOrGitShorthand::GitShorthand(url) => Either::Right(url.clone()),
90            });
91        project
92            .add(lua_dependency::DependencyType::Test(dependencies), &db)
93            .await?;
94        project
95            .add_git(lua_dependency::LuaDependencyType::Test(git_dependencies))
96            .await?;
97        sync_test_dependencies_if_locked(&project, progress.clone(), &config).await?;
98    }
99
100    Ok(())
101}
102
103#[cfg(test)]
104mod test {
105    use assert_fs::{prelude::PathCopy, TempDir};
106    use lux_lib::config::ConfigBuilder;
107    use serial_test::serial;
108
109    use super::*;
110    use std::path::PathBuf;
111
112    #[serial]
113    #[tokio::test]
114    async fn test_add_regular_dependencies() {
115        if std::env::var("LUX_SKIP_IMPURE_TESTS").unwrap_or("0".into()) == "1" {
116            println!("Skipping impure test");
117            return;
118        }
119        let sample_project: PathBuf = "resources/test/sample-projects/init/".into();
120        let project_root = TempDir::new().unwrap();
121        project_root.copy_from(&sample_project, &["**"]).unwrap();
122        let cwd = std::env::current_dir().unwrap();
123        std::env::set_current_dir(&project_root).unwrap();
124        let config = ConfigBuilder::new().unwrap().build().unwrap();
125        let args = Add {
126            package_req: vec!["penlight@1.5".parse().unwrap()],
127            force: false,
128            build: Option::None,
129            test: Option::None,
130        };
131        add(args, config.clone()).await.unwrap();
132        let lockfile_path = project_root.join("lux.lock");
133        let lockfile_content =
134            String::from_utf8(tokio::fs::read(&lockfile_path).await.unwrap()).unwrap();
135        assert!(lockfile_content.contains("penlight"));
136        assert!(lockfile_content.contains("luafilesystem")); // dependency
137
138        let args = Add {
139            package_req: vec!["md5".parse().unwrap()],
140            force: false,
141            build: Option::None,
142            test: Option::None,
143        };
144        add(args, config.clone()).await.unwrap();
145        let lockfile_path = project_root.join("lux.lock");
146        let lockfile_content =
147            String::from_utf8(tokio::fs::read(&lockfile_path).await.unwrap()).unwrap();
148        assert!(lockfile_content.contains("penlight"));
149        assert!(lockfile_content.contains("luafilesystem"));
150        assert!(lockfile_content.contains("md5"));
151
152        std::env::set_current_dir(&cwd).unwrap();
153    }
154
155    #[serial]
156    #[tokio::test]
157    async fn test_add_build_dependencies() {
158        if std::env::var("LUX_SKIP_IMPURE_TESTS").unwrap_or("0".into()) == "1" {
159            println!("Skipping impure test");
160            return;
161        }
162        let sample_project: PathBuf = "resources/test/sample-projects/init/".into();
163        let project_root = TempDir::new().unwrap();
164        project_root.copy_from(&sample_project, &["**"]).unwrap();
165        let cwd = std::env::current_dir().unwrap();
166        std::env::set_current_dir(&project_root).unwrap();
167        let config = ConfigBuilder::new().unwrap().build().unwrap();
168        let args = Add {
169            package_req: Vec::new(),
170            force: false,
171            build: Option::Some(vec!["penlight@1.5".parse().unwrap()]),
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")); // dependency
180
181        let args = Add {
182            package_req: Vec::new(),
183            force: false,
184            build: Option::Some(vec!["md5".parse().unwrap()]),
185            test: Option::None,
186        };
187        add(args, config.clone()).await.unwrap();
188        let lockfile_path = project_root.join("lux.lock");
189        let lockfile_content =
190            String::from_utf8(tokio::fs::read(&lockfile_path).await.unwrap()).unwrap();
191        assert!(lockfile_content.contains("penlight"));
192        assert!(lockfile_content.contains("luafilesystem"));
193        assert!(lockfile_content.contains("md5"));
194
195        std::env::set_current_dir(&cwd).unwrap();
196    }
197
198    #[serial]
199    #[tokio::test]
200    async fn test_add_test_dependencies() {
201        if std::env::var("LUX_SKIP_IMPURE_TESTS").unwrap_or("0".into()) == "1" {
202            println!("Skipping impure test");
203            return;
204        }
205        let sample_project: PathBuf = "resources/test/sample-projects/init/".into();
206        let project_root = TempDir::new().unwrap();
207        project_root.copy_from(&sample_project, &["**"]).unwrap();
208        let cwd = std::env::current_dir().unwrap();
209        std::env::set_current_dir(&project_root).unwrap();
210        let config = ConfigBuilder::new().unwrap().build().unwrap();
211        let args = Add {
212            package_req: Vec::new(),
213            force: false,
214            build: Option::None,
215            test: Option::Some(vec!["penlight@1.5".parse().unwrap()]),
216        };
217        add(args, config.clone()).await.unwrap();
218        let lockfile_path = project_root.join("lux.lock");
219        let lockfile_content =
220            String::from_utf8(tokio::fs::read(&lockfile_path).await.unwrap()).unwrap();
221        assert!(lockfile_content.contains("penlight"));
222        assert!(lockfile_content.contains("luafilesystem")); // dependency
223
224        let args = Add {
225            package_req: Vec::new(),
226            force: false,
227            build: Option::None,
228            test: Option::Some(vec!["md5".parse().unwrap()]),
229        };
230        add(args, config.clone()).await.unwrap();
231        let lockfile_path = project_root.join("lux.lock");
232        let lockfile_content =
233            String::from_utf8(tokio::fs::read(&lockfile_path).await.unwrap()).unwrap();
234        assert!(lockfile_content.contains("penlight"));
235        assert!(lockfile_content.contains("luafilesystem"));
236        assert!(lockfile_content.contains("md5"));
237
238        std::env::set_current_dir(&cwd).unwrap();
239    }
240}