pyoxidizerlib/starlark/
python_resource.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5use {
6    super::{
7        file::FileValue, python_extension_module::PythonExtensionModuleValue,
8        python_module_source::PythonModuleSourceValue,
9        python_package_distribution_resource::PythonPackageDistributionResourceValue,
10        python_package_resource::PythonPackageResourceValue,
11        python_packaging_policy::PythonPackagingPolicyValue,
12    },
13    python_packaging::{
14        location::ConcreteResourceLocation, resource::PythonResource,
15        resource_collection::PythonResourceAddCollectionContext,
16    },
17    starlark::{
18        environment::TypeValues,
19        eval::call_stack::CallStack,
20        values::{
21            error::{
22                RuntimeError, UnsupportedOperation, ValueError, INCORRECT_PARAMETER_TYPE_ERROR_CODE,
23            },
24            none::NoneType,
25            {Value, ValueResult},
26        },
27    },
28};
29
30#[derive(Clone, Debug)]
31pub struct OptionalResourceLocation {
32    inner: Option<ConcreteResourceLocation>,
33}
34
35impl From<&OptionalResourceLocation> for Value {
36    fn from(location: &OptionalResourceLocation) -> Self {
37        match &location.inner {
38            Some(ConcreteResourceLocation::InMemory) => Value::from("in-memory"),
39            Some(ConcreteResourceLocation::RelativePath(prefix)) => {
40                Value::from(format!("filesystem-relative:{}", prefix))
41            }
42            None => Value::from(NoneType::None),
43        }
44    }
45}
46
47impl TryFrom<&str> for OptionalResourceLocation {
48    type Error = ValueError;
49
50    fn try_from(s: &str) -> Result<Self, Self::Error> {
51        if s == "default" {
52            Ok(OptionalResourceLocation { inner: None })
53        } else if s == "in-memory" {
54            Ok(OptionalResourceLocation {
55                inner: Some(ConcreteResourceLocation::InMemory),
56            })
57        } else if s.starts_with("filesystem-relative:") {
58            let prefix = s.split_at("filesystem-relative:".len()).1;
59            Ok(OptionalResourceLocation {
60                inner: Some(ConcreteResourceLocation::RelativePath(prefix.to_string())),
61            })
62        } else {
63            Err(ValueError::from(RuntimeError {
64                code: INCORRECT_PARAMETER_TYPE_ERROR_CODE,
65                message: format!("unable to convert value {} to a resource location", s),
66                label: format!(
67                    "expected `default`, `in-memory`, or `filesystem-relative:*`; got {}",
68                    s
69                ),
70            }))
71        }
72    }
73}
74
75impl TryFrom<&Value> for OptionalResourceLocation {
76    type Error = ValueError;
77
78    fn try_from(value: &Value) -> Result<Self, Self::Error> {
79        match value.get_type() {
80            "NoneType" => Ok(OptionalResourceLocation { inner: None }),
81            "string" => {
82                let s = value.to_str();
83                Ok(OptionalResourceLocation::try_from(s.as_str())?)
84            }
85            t => Err(ValueError::from(RuntimeError {
86                code: INCORRECT_PARAMETER_TYPE_ERROR_CODE,
87                message: format!("unable to convert value {} to resource location", t),
88                label: "resource location conversion".to_string(),
89            })),
90        }
91    }
92}
93
94impl From<OptionalResourceLocation> for Option<ConcreteResourceLocation> {
95    fn from(location: OptionalResourceLocation) -> Self {
96        location.inner
97    }
98}
99
100/// Defines functionality for exposing `PythonResourceAddCollectionContext` from a type.
101pub trait ResourceCollectionContext {
102    /// Obtain the `PythonResourceAddCollectionContext` associated with this instance, if available.
103    fn add_collection_context(
104        &self,
105    ) -> Result<Option<PythonResourceAddCollectionContext>, ValueError>;
106
107    /// Set the [PythonResourceAddCollectionContext] value on this instance.
108    ///
109    /// Returns the old value.
110    fn replace_add_collection_context(
111        &mut self,
112        add_context: PythonResourceAddCollectionContext,
113    ) -> Result<Option<PythonResourceAddCollectionContext>, ValueError>;
114
115    /// Cast this instance to a `PythonResource`.
116    fn as_python_resource(&self) -> Result<PythonResource, ValueError>;
117
118    /// Obtains the Starlark object attributes that are defined by the add collection context.
119    fn add_collection_context_attrs(&self) -> Vec<&'static str> {
120        vec![
121            "add_include",
122            "add_location",
123            "add_location_fallback",
124            "add_source",
125            "add_bytecode_optimization_level_zero",
126            "add_bytecode_optimization_level_one",
127            "add_bytecode_optimization_level_two",
128        ]
129    }
130
131    /// Obtain the attribute value for an add collection context.
132    ///
133    /// The caller should verify the attribute should be serviced by us
134    /// before calling.
135    fn get_attr_add_collection_context(&self, attribute: &str) -> ValueResult {
136        if !self.add_collection_context_attrs().contains(&attribute) {
137            panic!(
138                "get_attr_add_collection_context({}) called when it shouldn't have been",
139                attribute
140            );
141        }
142
143        let context = self.add_collection_context()?;
144
145        Ok(match context {
146            Some(context) => match attribute {
147                "add_bytecode_optimization_level_zero" => Value::new(context.optimize_level_zero),
148                "add_bytecode_optimization_level_one" => Value::new(context.optimize_level_one),
149                "add_bytecode_optimization_level_two" => Value::new(context.optimize_level_two),
150                "add_include" => Value::new(context.include),
151                "add_location" => Value::new::<String>(context.location.into()),
152                "add_location_fallback" => match context.location_fallback.as_ref() {
153                    Some(location) => Value::new::<String>(location.clone().into()),
154                    None => Value::from(NoneType::None),
155                },
156                "add_source" => Value::new(context.store_source),
157                _ => panic!("this should not happen"),
158            },
159            None => Value::from(NoneType::None),
160        })
161    }
162
163    // The caller should validate that the attribute is an add collection attribute
164    // before calling. Otherwise a panic can occur.
165    fn set_attr_add_collection_context(
166        &mut self,
167        attribute: &str,
168        value: Value,
169    ) -> Result<(), ValueError> {
170        let mut context = self.add_collection_context()?;
171
172        match context {
173            Some(ref mut context) => {
174                match attribute {
175                    "add_bytecode_optimization_level_zero" => {
176                        context.optimize_level_zero = value.to_bool();
177                        Ok(())
178                    }
179                    "add_bytecode_optimization_level_one" => {
180                        context.optimize_level_one = value.to_bool();
181                        Ok(())
182                    }
183                    "add_bytecode_optimization_level_two" => {
184                        context.optimize_level_two = value.to_bool();
185                        Ok(())
186                    }
187                    "add_include" => {
188                        context.include = value.to_bool();
189                        Ok(())
190                    }
191                    "add_location" => {
192                        let location: OptionalResourceLocation = (&value).try_into()?;
193
194                        match location.inner {
195                            Some(location) => {
196                                context.location = location;
197
198                                Ok(())
199                            }
200                            None => {
201                                Err(ValueError::OperationNotSupported {
202                                    op: UnsupportedOperation::SetAttr(attribute.to_string()),
203                                    left: "set_attr".to_string(),
204                                    right: None,
205                                })
206                            }
207                        }
208                    }
209                    "add_location_fallback" => {
210                        let location: OptionalResourceLocation = (&value).try_into()?;
211
212                        match location.inner {
213                            Some(location) => {
214                                context.location_fallback = Some(location);
215                                Ok(())
216                            }
217                            None => {
218                                context.location_fallback = None;
219                                Ok(())
220                            }
221                        }
222                    }
223                    "add_source" => {
224                        context.store_source = value.to_bool();
225                        Ok(())
226                    }
227                    attr => panic!("set_attr_add_collection_context({}) called when it shouldn't have been", attr)
228                }
229            },
230            None => Err(ValueError::from(RuntimeError {
231                code: "PYOXIDIZER",
232                message: "attempting to set a collection context attribute on an object without a context".to_string(),
233                label: "setattr()".to_string()
234            }))
235        }?;
236
237        self.replace_add_collection_context(context.unwrap())?;
238
239        Ok(())
240    }
241}
242
243/// Whether a `PythonResource` can be converted to a Starlark value.
244pub fn is_resource_starlark_compatible(resource: &PythonResource) -> bool {
245    match resource {
246        PythonResource::ModuleSource(_) => true,
247        PythonResource::PackageResource(_) => true,
248        PythonResource::PackageDistributionResource(_) => true,
249        PythonResource::ExtensionModule(_) => true,
250        PythonResource::ModuleBytecode(_) => false,
251        PythonResource::ModuleBytecodeRequest(_) => false,
252        PythonResource::EggFile(_) => false,
253        PythonResource::PathExtension(_) => false,
254        PythonResource::File(_) => true,
255    }
256}
257
258pub fn python_resource_to_value(
259    label: &str,
260    type_values: &TypeValues,
261    call_stack: &mut CallStack,
262    resource: &PythonResource,
263    policy: &PythonPackagingPolicyValue,
264) -> ValueResult {
265    match resource {
266        PythonResource::ModuleSource(sm) => {
267            let mut m = PythonModuleSourceValue::new(sm.clone().into_owned());
268            policy.apply_to_resource(label, type_values, call_stack, &mut m)?;
269
270            Ok(Value::new(m))
271        }
272
273        PythonResource::PackageResource(data) => {
274            let mut r = PythonPackageResourceValue::new(data.clone().into_owned());
275            policy.apply_to_resource(label, type_values, call_stack, &mut r)?;
276
277            Ok(Value::new(r))
278        }
279
280        PythonResource::PackageDistributionResource(resource) => {
281            let mut r = PythonPackageDistributionResourceValue::new(resource.clone().into_owned());
282            policy.apply_to_resource(label, type_values, call_stack, &mut r)?;
283
284            Ok(Value::new(r))
285        }
286
287        PythonResource::ExtensionModule(em) => {
288            let mut em = PythonExtensionModuleValue::new(em.clone().into_owned());
289            policy.apply_to_resource(label, type_values, call_stack, &mut em)?;
290
291            Ok(Value::new(em))
292        }
293
294        PythonResource::File(f) => {
295            let mut value = FileValue::new(f.clone().into_owned());
296            policy.apply_to_resource(label, type_values, call_stack, &mut value)?;
297
298            Ok(Value::new(value))
299        }
300
301        _ => {
302            panic!("incompatible PythonResource variant passed; did you forget to filter through is_resource_starlark_compatible()?")
303        }
304    }
305}
306
307/// Attempt to resolve the `PythonResourceAddCollectionContext` for a Value.
308pub fn add_context_for_value(
309    value: &Value,
310    label: &str,
311) -> Result<Option<PythonResourceAddCollectionContext>, ValueError> {
312    match value.get_type() {
313        "PythonModuleSource" => Ok(value
314            .downcast_ref::<PythonModuleSourceValue>()
315            .unwrap()
316            .add_collection_context()?),
317        "PythonPackageResource" => Ok(value
318            .downcast_ref::<PythonPackageResourceValue>()
319            .unwrap()
320            .add_collection_context()?),
321        "PythonPackageDistributionResource" => Ok(value
322            .downcast_ref::<PythonPackageDistributionResourceValue>()
323            .unwrap()
324            .add_collection_context()?),
325        "PythonExtensionModule" => Ok(value
326            .downcast_ref::<PythonExtensionModuleValue>()
327            .unwrap()
328            .add_collection_context()?),
329        "File" => Ok(value
330            .downcast_ref::<FileValue>()
331            .unwrap()
332            .add_collection_context()?),
333        t => Err(ValueError::from(RuntimeError {
334            code: INCORRECT_PARAMETER_TYPE_ERROR_CODE,
335            message: format!("unable to obtain add collection context from {}", t),
336            label: label.to_string(),
337        })),
338    }
339}