Skip to main content

lux_lib/lua_rockspec/
partial.rs

1use std::collections::HashMap;
2
3use crate::{
4    lua_rockspec::RockspecFormat, package::PackageName,
5    rockspec::lua_dependency::LuaDependencySpec, ROCKSPEC_FUEL_LIMIT,
6};
7use miette::Diagnostic;
8use ottavino::{Closure, Executor, Fuel};
9use ottavino_util::serde::from_value;
10use thiserror::Error;
11
12use super::{
13    parse_lua_tbl_or_default, BuildSpecInternal, DeploySpec, ExternalDependencySpec,
14    PlatformSupport, RockDescription, TestSpecInternal,
15};
16
17/// Deserialized from a Lua `.rockspec`, not yet validated
18#[derive(Debug)]
19pub struct PartialLuaRockspec {
20    pub(crate) rockspec_format: Option<RockspecFormat>,
21    pub(crate) package: Option<PackageName>,
22    pub(crate) build: Option<BuildSpecInternal>,
23    pub(crate) deploy: Option<DeploySpec>,
24    pub(crate) description: Option<RockDescription>,
25    pub(crate) supported_platforms: Option<PlatformSupport>,
26    pub(crate) dependencies: Option<Vec<LuaDependencySpec>>,
27    pub(crate) build_dependencies: Option<Vec<LuaDependencySpec>>,
28    pub(crate) external_dependencies: Option<HashMap<String, ExternalDependencySpec>>,
29    pub(crate) test_dependencies: Option<Vec<LuaDependencySpec>>,
30    pub(crate) test: Option<TestSpecInternal>,
31}
32
33#[derive(Debug, Error, Diagnostic)]
34pub enum PartialRockspecError {
35    #[error("rockspec execution exceeded fuel limit of {ROCKSPEC_FUEL_LIMIT} steps")]
36    #[diagnostic(help(
37        r#"the rockspec may be too complex or contain too many dependencies.
38        Try reducing the number of dependencies or simplifying build instructions."#
39    ))]
40    FuelLimitExceeded,
41    #[error("field '{0}' should not be declared in extra.rockspec")]
42    ExtraneousField(String),
43    #[error("error while parsing rockspec: {0}")]
44    Lua(#[from] ottavino::ExternError),
45    #[error(transparent)]
46    Io(#[from] std::io::Error),
47}
48
49impl PartialLuaRockspec {
50    pub fn new(rockspec_content: &str) -> Result<Self, PartialRockspecError> {
51        let mut lua = ottavino::Lua::core();
52
53        let rockspec = lua.try_enter(|ctx| {
54            let closure = Closure::load(ctx, None, rockspec_content.as_bytes())?;
55
56            let executor = Executor::start(ctx, closure.into(), ());
57
58            let output = executor.step(ctx, &mut Fuel::with(ROCKSPEC_FUEL_LIMIT))?;
59
60            if !output {
61                return Ok(Err(PartialRockspecError::FuelLimitExceeded));
62            }
63
64            let globals = ctx.globals();
65
66            if !matches!(globals.get_value(ctx, "version"), ottavino::Value::Nil) {
67                return Ok(Err(PartialRockspecError::ExtraneousField(
68                    "version".to_string(),
69                )));
70            }
71            if !matches!(globals.get_value(ctx, "source"), ottavino::Value::Nil) {
72                return Ok(Err(PartialRockspecError::ExtraneousField(
73                    "source".to_string(),
74                )));
75            }
76
77            let rockspec = PartialLuaRockspec {
78                rockspec_format: from_value(globals.get_value(ctx, "rockspec_format"))
79                    .unwrap_or_default(),
80                package: from_value(globals.get_value(ctx, "package")).unwrap_or_default(),
81                description: parse_lua_tbl_or_default(ctx, "description").unwrap_or_default(),
82                supported_platforms: parse_lua_tbl_or_default(ctx, "supported_platforms")
83                    .unwrap_or_default(),
84                dependencies: from_value(globals.get_value(ctx, "dependencies"))
85                    .unwrap_or_default(),
86                build_dependencies: from_value(globals.get_value(ctx, "build_dependencies"))
87                    .unwrap_or_default(),
88                test_dependencies: from_value(globals.get_value(ctx, "test_dependencies"))
89                    .unwrap_or_default(),
90                external_dependencies: from_value(globals.get_value(ctx, "external_dependencies"))
91                    .unwrap_or_default(),
92                build: from_value(globals.get_value(ctx, "build")).unwrap_or_default(),
93                test: from_value(globals.get_value(ctx, "test")).unwrap_or_default(),
94                deploy: from_value(globals.get_value(ctx, "deploy")).unwrap_or_default(),
95            };
96
97            Ok(Ok(rockspec))
98        })??;
99
100        Ok(rockspec)
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn parse_partial_rockspec() {
110        let partial_rockspec = r#"
111            package = "my-package"
112        "#;
113
114        PartialLuaRockspec::new(partial_rockspec).unwrap();
115
116        // Whether the partial rockspec format can still support entire rockspecs
117        let full_rockspec = r#"
118            rockspec_format = "3.0"
119            package = "my-package"
120
121            description = {
122                summary = "A summary",
123                detailed = "A detailed description",
124                license = "MIT",
125                homepage = "https://example.com",
126                issues_url = "https://example.com/issues",
127                maintainer = "John Doe",
128                labels = {"label1", "label2"},
129            }
130
131            supported_platforms = {"linux", "!windows"}
132
133            dependencies = {
134                "lua 5.1",
135                "foo 1.0",
136                "bar >=2.0",
137            }
138
139            build_dependencies = {
140                "baz 1.0",
141            }
142
143            external_dependencies = {
144                foo = { header = "foo.h" },
145                bar = { library = "libbar.so" },
146            }
147
148            test_dependencies = {
149                "busted 1.0",
150            }
151
152            test = {
153                type = "command",
154                script = "test.lua",
155                flags = {"foo", "bar"},
156            }
157
158            build = {
159                type = "builtin",
160            }
161        "#;
162
163        let rockspec = PartialLuaRockspec::new(full_rockspec).unwrap();
164
165        // No need to verify if the fields were parsed correctly, but worth checking if they were
166        // parsed at all.
167
168        assert!(rockspec.rockspec_format.is_some());
169        assert!(rockspec.package.is_some());
170        assert!(rockspec.description.is_some());
171        assert!(rockspec.supported_platforms.is_some());
172        assert!(rockspec.dependencies.is_some());
173        assert!(rockspec.build_dependencies.is_some());
174        assert!(rockspec.external_dependencies.is_some());
175        assert!(rockspec.test_dependencies.is_some());
176        assert!(rockspec.build.is_some());
177        assert!(rockspec.test.is_some());
178
179        // We don't allow version and source in extra.rockspec
180        let partial_rockspec = r#"
181            version = "2.0.0"
182        "#;
183
184        PartialLuaRockspec::new(partial_rockspec).unwrap_err();
185
186        let partial_rockspec = r#"
187            source = {
188                url = "https://example.com",
189            }
190        "#;
191
192        PartialLuaRockspec::new(partial_rockspec).unwrap_err();
193    }
194}