Skip to main content

lux_cli/
add.rs

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