Skip to main content

lux_cli/
install_rockspec.rs

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