Skip to main content

lux_cli/
install_rockspec.rs

1use miette::miette;
2use std::path::PathBuf;
3
4use clap::Args;
5use lux_lib::{
6    build::{self, BuildBehaviour},
7    config::Config,
8    lockfile::{OptState, PinnedState},
9    lua_installation::LuaInstallation,
10    lua_rockspec::{BuildBackendSpec, RemoteLuaRockspec},
11    luarocks::luarocks_installation::LuaRocksInstallation,
12    operations::{Install, PackageInstallSpec},
13    rockspec::{LuaVersionCompatibility, Rockspec},
14    tree::{self, InstallTree},
15};
16
17use miette::{IntoDiagnostic, Result};
18
19#[derive(Args, Default)]
20pub struct InstallRockspec {
21    /// The path to the RockSpec file to install
22    rockspec_path: PathBuf,
23
24    /// Whether to pin the installed package and dependencies.
25    #[arg(long)]
26    pin: bool,
27}
28
29/// Install a rockspec into the user tree.
30pub async fn install_rockspec(data: InstallRockspec, config: Config) -> Result<()> {
31    let pin = PinnedState::from(data.pin);
32    let path = data.rockspec_path;
33
34    if path
35        .extension()
36        .map(|ext| ext != "rockspec")
37        .unwrap_or(true)
38    {
39        return Err(miette!("Provided path is not a valid rockspec!"));
40    }
41
42    let content = std::fs::read_to_string(path).into_diagnostic()?;
43    let rockspec = RemoteLuaRockspec::new(&content)?;
44    let lua_version = rockspec.lua_version_matches(&config)?;
45    let lua = LuaInstallation::new(&lua_version, &config).await?;
46    let tree = config.user_tree(lua_version)?;
47
48    // Ensure all dependencies and build dependencies are installed first
49
50    let build_dependencies = rockspec.build_dependencies().current_platform();
51
52    let build_dependencies_to_install = build_dependencies
53        .iter()
54        .filter(|dep| {
55            // Exclude luarocks build backends that we have implemented in lux
56            !matches!(
57                dep.name().to_string().as_str(),
58                "luarocks-build-rust-mlua" | "luarocks-build-treesitter-parser"
59            )
60        })
61        .filter(|dep| {
62            tree.match_rocks(dep.package_req())
63                .is_ok_and(|rock_match| rock_match.is_found())
64        })
65        .map(|dep| {
66            PackageInstallSpec::new(dep.package_req().clone(), tree::EntryType::Entrypoint)
67                .build_behaviour(BuildBehaviour::NoForce)
68                .pin(pin)
69                .opt(OptState::Required)
70                .maybe_source(dep.source().clone())
71                .build()
72        })
73        .collect();
74
75    Install::new(&config)
76        .packages(build_dependencies_to_install)
77        .tree(tree.build_tree(&config)?)
78        .install()
79        .await?;
80
81    let dependencies = rockspec.dependencies().current_platform();
82
83    let mut dependencies_to_install = Vec::new();
84    for dep in dependencies {
85        let rock_match = tree.match_rocks(dep.package_req())?;
86        if !rock_match.is_found() {
87            let dep =
88                PackageInstallSpec::new(dep.package_req().clone(), tree::EntryType::DependencyOnly)
89                    .build_behaviour(BuildBehaviour::NoForce)
90                    .pin(pin)
91                    .opt(OptState::Required)
92                    .maybe_source(dep.source().clone())
93                    .build();
94            dependencies_to_install.push(dep);
95        }
96    }
97
98    Install::new(&config)
99        .packages(dependencies_to_install)
100        .tree(tree.clone())
101        .install()
102        .await?;
103
104    if let Some(BuildBackendSpec::LuaRock(_)) = &rockspec.build().current_platform().build_backend {
105        let build_tree = tree.build_tree(&config)?;
106        let luarocks = LuaRocksInstallation::new(&config, build_tree)?;
107        luarocks.ensure_installed(&lua).await?;
108    }
109
110    build::Build::new()
111        .rockspec(&rockspec)
112        .tree(&tree)
113        .lua(&lua)
114        .entry_type(tree::EntryType::Entrypoint)
115        .config(&config)
116        .pin(pin)
117        .behaviour(BuildBehaviour::Force)
118        .build()
119        .await?;
120
121    Ok(())
122}
123
124#[cfg(test)]
125mod tests {
126
127    use super::*;
128
129    use assert_fs::{
130        prelude::{FileWriteStr, PathChild, PathCreateDir},
131        TempDir,
132    };
133
134    use lux_lib::{
135        config::ConfigBuilder, lua_installation::detect_installed_lua_version,
136        lua_version::LuaVersion,
137    };
138
139    #[tokio::test]
140    async fn test_install_rockspec_from_vendored() {
141        // This test runs without a network connection when run with Nix
142        let vendor_dir = TempDir::new().unwrap();
143        let foo_dir = vendor_dir.child("foo@1.0.0-1");
144        foo_dir.create_dir_all().unwrap();
145        let foo_rockspec = vendor_dir.child("foo-1.0.0-1.rockspec");
146        foo_rockspec
147            .write_str(
148                r#"
149                package = 'foo'
150                version = '1.0.0-1'
151                source = {
152                    url = 'https://github.com/lumen-oss/luarocks-stub',
153                }
154            "#,
155            )
156            .unwrap();
157        let bar_dir = vendor_dir.child("bar@2.0.0-2");
158        bar_dir.create_dir_all().unwrap();
159        let bar_rockspec = vendor_dir.child("bar-2.0.0-2.rockspec");
160        bar_rockspec
161            .write_str(
162                r#"
163                package = 'bar'
164                version = '2.0.0-2'
165                source = {
166                    url = 'https://github.com/lumen-oss/luarocks-stub',
167                }
168            "#,
169            )
170            .unwrap();
171        let baz_dir = vendor_dir.child("baz@2.0.0-1");
172        baz_dir.create_dir_all().unwrap();
173        let baz_rockspec = vendor_dir.child("baz-2.0.0-1.rockspec");
174        baz_rockspec
175            .write_str(
176                r#"
177                package = 'baz'
178                version = '2.0.0-1'
179                source = {
180                    url = 'https://github.com/lumen-oss/luarocks-stub',
181                }
182            "#,
183            )
184            .unwrap();
185        let test_rock_dir = vendor_dir.child("test_rock@scm-1");
186        test_rock_dir.create_dir_all().unwrap();
187        let rockspec_content = r#"
188        package = 'test_rock'
189        version = 'scm-1'
190        source = {
191            url = 'https://github.com/lumen-oss/luarocks-stub',
192        }
193        dependencies = {
194            'foo >= 1.0.0',
195            'bar',
196            'baz == 2.0.0',
197        }
198        "#;
199        let temp_dir = TempDir::new().unwrap();
200        let rockspec = temp_dir.child("test_rock-scm-1.rockspec");
201        rockspec.write_str(rockspec_content).unwrap();
202        let lua_version = detect_installed_lua_version().or(Some(LuaVersion::Lua51));
203        let config = ConfigBuilder::new()
204            .unwrap()
205            .vendor_dir(Some(vendor_dir.to_path_buf()))
206            .lua_version(lua_version)
207            .user_tree(Some(temp_dir.to_path_buf()))
208            .build()
209            .unwrap();
210        install_rockspec(
211            InstallRockspec {
212                rockspec_path: rockspec.to_path_buf(),
213                pin: false,
214            },
215            config,
216        )
217        .await
218        .unwrap()
219    }
220}