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    pub(crate) 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(
68                lua_dependency::LuaDependencyType::Regular(git_dependencies.iter().collect()),
69                &config,
70            )
71            .await?;
72    }
73
74    let build_packages = data.build.unwrap_or_default();
75    if !build_packages.is_empty() {
76        let (dependencies, git_dependencies): (Vec<_>, Vec<_>) =
77            build_packages.iter().partition_map(|req| match req {
78                PackageReqOrGitShorthand::PackageReq(req) => Either::Left(req.clone()),
79                PackageReqOrGitShorthand::GitShorthand(url) => Either::Right(url.clone()),
80            });
81        project
82            .add(
83                lua_dependency::DependencyType::Build(dependencies.iter().collect()),
84                &db,
85            )
86            .await?;
87        project
88            .add_git(
89                lua_dependency::LuaDependencyType::Build(git_dependencies.iter().collect()),
90                &config,
91            )
92            .await?;
93    }
94
95    let test_packages = data.test.unwrap_or_default();
96    if !test_packages.is_empty() {
97        let (dependencies, git_dependencies): (Vec<_>, Vec<_>) =
98            test_packages.iter().partition_map(|req| match req {
99                PackageReqOrGitShorthand::PackageReq(req) => Either::Left(req.clone()),
100                PackageReqOrGitShorthand::GitShorthand(url) => Either::Right(url.clone()),
101            });
102        project
103            .add(
104                lua_dependency::DependencyType::Test(dependencies.iter().collect()),
105                &db,
106            )
107            .await?;
108        project
109            .add_git(
110                lua_dependency::LuaDependencyType::Test(git_dependencies.iter().collect()),
111                &config,
112            )
113            .await?;
114    }
115
116    if !data.package_req.is_empty() {
117        sync_dependencies_if_locked(&workspace, &config).await?;
118    }
119    if !build_packages.is_empty() {
120        sync_build_dependencies_if_locked(&workspace, &config).await?;
121    }
122    if !test_packages.is_empty() {
123        sync_test_dependencies_if_locked(&workspace, &config).await?;
124    }
125
126    Ok(())
127}
128
129#[cfg(test)]
130mod tests {
131    use assert_fs::{prelude::PathCopy, TempDir};
132    use lux_lib::config::ConfigBuilder;
133    use serial_test::serial;
134
135    use super::*;
136    use std::path::PathBuf;
137
138    #[serial]
139    #[tokio::test]
140    async fn test_add_regular_dependencies() {
141        if std::env::var("LUX_SKIP_IMPURE_TESTS").unwrap_or("0".into()) == "1" {
142            println!("Skipping impure test");
143            return;
144        }
145        let sample_project =
146            PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/sample-projects/init/");
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 =
192            PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/sample-projects/init/");
193        let project_root = TempDir::new().unwrap();
194        project_root.copy_from(&sample_project, &["**"]).unwrap();
195        let cwd = std::env::current_dir().unwrap();
196        std::env::set_current_dir(&project_root).unwrap();
197        let config = ConfigBuilder::new().unwrap().build().unwrap();
198        let args = Add {
199            package: None,
200            package_req: Vec::new(),
201            force: false,
202            build: Option::Some(vec!["penlight@1.5".parse().unwrap()]),
203            test: Option::None,
204        };
205        add(args, config.clone()).await.unwrap();
206        let lockfile_path = project_root.join("lux.lock");
207        let lockfile_content =
208            String::from_utf8(tokio::fs::read(&lockfile_path).await.unwrap()).unwrap();
209        assert!(lockfile_content.contains("penlight"));
210        assert!(lockfile_content.contains("luafilesystem")); // dependency
211
212        let args = Add {
213            package: None,
214            package_req: Vec::new(),
215            force: false,
216            build: Option::Some(vec!["md5".parse().unwrap()]),
217            test: Option::None,
218        };
219        add(args, config.clone()).await.unwrap();
220        let lockfile_path = project_root.join("lux.lock");
221        let lockfile_content =
222            String::from_utf8(tokio::fs::read(&lockfile_path).await.unwrap()).unwrap();
223        assert!(lockfile_content.contains("penlight"));
224        assert!(lockfile_content.contains("luafilesystem"));
225        assert!(lockfile_content.contains("md5"));
226
227        std::env::set_current_dir(&cwd).unwrap();
228    }
229
230    #[serial]
231    #[tokio::test]
232    async fn test_add_test_dependencies() {
233        if std::env::var("LUX_SKIP_IMPURE_TESTS").unwrap_or("0".into()) == "1" {
234            println!("Skipping impure test");
235            return;
236        }
237        let sample_project =
238            PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources/test/sample-projects/init/");
239        let project_root = TempDir::new().unwrap();
240        project_root.copy_from(&sample_project, &["**"]).unwrap();
241        let cwd = std::env::current_dir().unwrap();
242        std::env::set_current_dir(&project_root).unwrap();
243        let config = ConfigBuilder::new().unwrap().build().unwrap();
244        let args = Add {
245            package: None,
246            package_req: Vec::new(),
247            force: false,
248            build: Option::None,
249            test: Option::Some(vec!["penlight@1.5".parse().unwrap()]),
250        };
251        add(args, config.clone()).await.unwrap();
252        let lockfile_path = project_root.join("lux.lock");
253        let lockfile_content =
254            String::from_utf8(tokio::fs::read(&lockfile_path).await.unwrap()).unwrap();
255        assert!(lockfile_content.contains("penlight"));
256        assert!(lockfile_content.contains("luafilesystem")); // dependency
257
258        let args = Add {
259            package: None,
260            package_req: Vec::new(),
261            force: false,
262            build: Option::None,
263            test: Option::Some(vec!["md5".parse().unwrap()]),
264        };
265        add(args, config.clone()).await.unwrap();
266        let lockfile_path = project_root.join("lux.lock");
267        let lockfile_content =
268            String::from_utf8(tokio::fs::read(&lockfile_path).await.unwrap()).unwrap();
269        assert!(lockfile_content.contains("penlight"));
270        assert!(lockfile_content.contains("luafilesystem"));
271        assert!(lockfile_content.contains("md5"));
272
273        std::env::set_current_dir(&cwd).unwrap();
274    }
275}