pyoxidizerlib/starlark/
python_distribution.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        env::{get_context, PyOxidizerEnvironmentContext},
8        python_executable::PythonExecutableValue,
9        python_interpreter_config::PythonInterpreterConfigValue,
10        python_packaging_policy::PythonPackagingPolicyValue,
11        python_resource::{add_context_for_value, python_resource_to_value},
12    },
13    crate::py_packaging::{
14        distribution::BinaryLibpythonLinkMode,
15        distribution::{
16            default_distribution_location, DistributionFlavor, PythonDistribution,
17            PythonDistributionLocation,
18        },
19    },
20    anyhow::{anyhow, Result},
21    log::{info, warn},
22    python_packaging::{
23        policy::PythonPackagingPolicy, resource::PythonResource,
24        resource_collection::PythonResourceAddCollectionContext,
25    },
26    starlark::{
27        environment::TypeValues,
28        eval::call_stack::CallStack,
29        values::{
30            error::{RuntimeError, ValueError, INCORRECT_PARAMETER_TYPE_ERROR_CODE},
31            none::NoneType,
32            {Mutable, TypedValue, Value, ValueResult},
33        },
34        {
35            starlark_fun, starlark_module, starlark_parse_param_type, starlark_signature,
36            starlark_signature_extraction, starlark_signatures,
37        },
38    },
39    starlark_dialect_build_targets::{optional_str_arg, optional_type_arg},
40    std::{ops::Deref, sync::Arc},
41};
42
43/// A Starlark Value wrapper for `PythonDistribution` traits.
44pub struct PythonDistributionValue {
45    /// Where the distribution should be obtained from.
46    pub source: PythonDistributionLocation,
47
48    /// The actual distribution.
49    ///
50    /// Populated on first read.
51    pub distribution: Option<Arc<dyn PythonDistribution>>,
52}
53
54impl PythonDistributionValue {
55    fn from_location(location: PythonDistributionLocation) -> PythonDistributionValue {
56        PythonDistributionValue {
57            source: location,
58            distribution: None,
59        }
60    }
61
62    pub fn resolve_distribution(
63        &mut self,
64        type_values: &TypeValues,
65        label: &str,
66    ) -> Result<Arc<dyn PythonDistribution>, ValueError> {
67        if self.distribution.is_none() {
68            let pyoxidizer_context_value = get_context(type_values)?;
69            let pyoxidizer_context = pyoxidizer_context_value
70                .downcast_mut::<PyOxidizerEnvironmentContext>()?
71                .ok_or(ValueError::IncorrectParameterType)?;
72
73            let dest_dir = pyoxidizer_context.python_distributions_path()?;
74
75            self.distribution = Some(
76                pyoxidizer_context
77                    .distribution_cache
78                    .resolve_distribution(&self.source, Some(&dest_dir))
79                    .map_err(|e| {
80                        ValueError::from(RuntimeError {
81                            code: "PYOXIDIZER_BUILD",
82                            message: format!("{:?}", e),
83                            label: label.to_string(),
84                        })
85                    })?
86                    .clone_trait(),
87            );
88        }
89
90        Ok(self.distribution.as_ref().unwrap().clone())
91    }
92}
93
94impl TypedValue for PythonDistributionValue {
95    type Holder = Mutable<PythonDistributionValue>;
96    const TYPE: &'static str = "PythonDistribution";
97
98    fn values_for_descendant_check_and_freeze(&self) -> Box<dyn Iterator<Item = Value>> {
99        Box::new(std::iter::empty())
100    }
101
102    fn to_str(&self) -> String {
103        format!("PythonDistribution<{:#?}>", self.source)
104    }
105}
106
107// Starlark functions.
108impl PythonDistributionValue {
109    /// default_python_distribution(flavor, build_target=None, python_version=None)
110    fn default_python_distribution(
111        type_values: &TypeValues,
112        flavor: String,
113        build_target: &Value,
114        python_version: &Value,
115    ) -> ValueResult {
116        let build_target = optional_str_arg("build_target", build_target)?;
117        let python_version = optional_str_arg("python_version", python_version)?;
118
119        let pyoxidizer_context_value = get_context(type_values)?;
120        let pyoxidizer_context = pyoxidizer_context_value
121            .downcast_ref::<PyOxidizerEnvironmentContext>()
122            .ok_or(ValueError::IncorrectParameterType)?;
123
124        let build_target = match build_target {
125            Some(t) => t,
126            None => pyoxidizer_context.build_target_triple.clone(),
127        };
128
129        let flavor = DistributionFlavor::try_from(flavor.as_str()).map_err(|e| {
130            ValueError::from(RuntimeError {
131                code: "PYOXIDIZER_BUILD",
132                message: e,
133                label: "default_python_distribution()".to_string(),
134            })
135        })?;
136
137        let python_version_str = python_version.as_deref();
138
139        let location = default_distribution_location(&flavor, &build_target, python_version_str)
140            .map_err(|e| {
141                ValueError::from(RuntimeError {
142                    code: "PYOXIDIZER_BUILD",
143                    message: format!("{:?}", e),
144                    label: "default_python_distribution()".to_string(),
145                })
146            })?;
147
148        warn!(
149            "target Python distribution for {} resolves to: {}",
150            build_target, location
151        );
152
153        Ok(Value::new(PythonDistributionValue::from_location(location)))
154    }
155
156    /// PythonDistribution()
157    fn from_args(sha256: String, local_path: &Value, url: &Value, flavor: String) -> ValueResult {
158        optional_str_arg("local_path", local_path)?;
159        optional_str_arg("url", url)?;
160
161        if local_path.get_type() != "NoneType" && url.get_type() != "NoneType" {
162            return Err(ValueError::from(RuntimeError {
163                code: INCORRECT_PARAMETER_TYPE_ERROR_CODE,
164                message: "cannot define both local_path and url".to_string(),
165                label: "cannot define both local_path and url".to_string(),
166            }));
167        }
168
169        let distribution = if local_path.get_type() != "NoneType" {
170            PythonDistributionLocation::Local {
171                local_path: local_path.to_string(),
172                sha256,
173            }
174        } else {
175            PythonDistributionLocation::Url {
176                url: url.to_string(),
177                sha256,
178            }
179        };
180
181        match flavor.as_ref() {
182            "standalone" => (),
183            v => {
184                return Err(ValueError::from(RuntimeError {
185                    code: "PYOXIDIZER_BUILD",
186                    message: format!("invalid distribution flavor {}", v),
187                    label: "PythonDistribution()".to_string(),
188                }))
189            }
190        }
191
192        Ok(Value::new(PythonDistributionValue::from_location(
193            distribution,
194        )))
195    }
196
197    /// PythonDistribution.make_python_packaging_policy()
198    fn make_python_packaging_policy_starlark(&mut self, type_values: &TypeValues) -> ValueResult {
199        let dist = self.resolve_distribution(type_values, "resolve_distribution")?;
200
201        let policy = dist.create_packaging_policy().map_err(|e| {
202            ValueError::from(RuntimeError {
203                code: "PYOXIDIZER_BUILD",
204                message: format!("{:?}", e),
205                label: "make_python_packaging_policy()".to_string(),
206            })
207        })?;
208
209        Ok(Value::new(PythonPackagingPolicyValue::new(policy)))
210    }
211
212    /// PythonDistribution.make_python_interpreter_config()
213    fn make_python_interpreter_config_starlark(&mut self, type_values: &TypeValues) -> ValueResult {
214        let dist = self.resolve_distribution(type_values, "resolve_distribution()")?;
215
216        let config = dist.create_python_interpreter_config().map_err(|e| {
217            ValueError::from(RuntimeError {
218                code: "PYOXIDIZER_BUILD",
219                message: format!("{:?}", e),
220                label: "make_python_packaging_policy()".to_string(),
221            })
222        })?;
223
224        Ok(Value::new(PythonInterpreterConfigValue::new(config)))
225    }
226
227    /// PythonDistribution.to_python_executable(
228    ///     name,
229    ///     packaging_policy=None,
230    ///     config=None,
231    /// )
232    #[allow(clippy::too_many_arguments, clippy::wrong_self_convention)]
233    fn to_python_executable_starlark(
234        &mut self,
235        type_values: &TypeValues,
236        call_stack: &mut CallStack,
237        name: String,
238        packaging_policy: &Value,
239        config: &Value,
240    ) -> ValueResult {
241        const LABEL: &str = "PythonDistribution.to_python_executable()";
242
243        optional_type_arg(
244            "packaging_policy",
245            "PythonPackagingPolicy",
246            packaging_policy,
247        )?;
248        optional_type_arg("config", "PythonInterpreterConfig", config)?;
249
250        let dist = self.resolve_distribution(type_values, "resolve_distribution()")?;
251
252        let policy = if packaging_policy.get_type() == "NoneType" {
253            Ok(PythonPackagingPolicyValue::new(
254                dist.create_packaging_policy().map_err(|e| {
255                    ValueError::from(RuntimeError {
256                        code: "PYOXIDIZER_BUILD",
257                        message: format!("{:?}", e),
258                        label: "to_python_executable_starlark()".to_string(),
259                    })
260                })?,
261            ))
262        } else {
263            match packaging_policy.downcast_ref::<PythonPackagingPolicyValue>() {
264                Some(policy) => Ok(policy.clone()),
265                None => Err(ValueError::IncorrectParameterType),
266            }
267        }?;
268
269        let config = if config.get_type() == "NoneType" {
270            Ok(PythonInterpreterConfigValue::new(
271                dist.create_python_interpreter_config().map_err(|e| {
272                    ValueError::from(RuntimeError {
273                        code: "PYOXIDIZER_BUILD",
274                        message: format!("{:?}", e),
275                        label: "to_python_executable_starlark()".to_string(),
276                    })
277                })?,
278            ))
279        } else {
280            match config.downcast_ref::<PythonInterpreterConfigValue>() {
281                Some(c) => Ok(c.clone()),
282                None => Err(ValueError::IncorrectParameterType),
283            }
284        }?;
285
286        let pyoxidizer_context_value = get_context(type_values)?;
287        let pyoxidizer_context = pyoxidizer_context_value
288            .downcast_ref::<PyOxidizerEnvironmentContext>()
289            .ok_or(ValueError::IncorrectParameterType)?;
290
291        let python_distributions_path = pyoxidizer_context.python_distributions_path()?;
292
293        let host_distribution = if dist
294            .compatible_host_triples()
295            .contains(&pyoxidizer_context.build_host_triple)
296        {
297            warn!("reusing target Python distribution for host execution");
298            Some(dist.clone())
299        } else {
300            info!(
301                "searching for host Python {} distribution",
302                dist.python_major_minor_version()
303            );
304            let host_dist = pyoxidizer_context
305                .distribution_cache
306                .host_distribution(
307                    Some(dist.python_major_minor_version().as_str()),
308                    Some(&python_distributions_path),
309                )
310                .map_err(|e| {
311                    ValueError::from(RuntimeError {
312                        code: "PYOXIDIZER_BUILD",
313                        message: format!("{:?}", e),
314                        label: "to_python_executable()".to_string(),
315                    })
316                })?;
317
318            Some(host_dist.clone_trait())
319        };
320
321        let mut builder = dist
322            .as_python_executable_builder(
323                &pyoxidizer_context.build_host_triple,
324                &pyoxidizer_context.build_target_triple,
325                &name,
326                // TODO make configurable
327                BinaryLibpythonLinkMode::Default,
328                policy.inner(LABEL)?.deref(),
329                config.inner(LABEL)?.deref(),
330                host_distribution,
331            )
332            .map_err(|e| {
333                ValueError::from(RuntimeError {
334                    code: "PYOXIDIZER_BUILD",
335                    message: format!("{:?}", e),
336                    label: "to_python_executable()".to_string(),
337                })
338            })?;
339
340        let callback = Box::new(
341            |_policy: &PythonPackagingPolicy,
342             resource: &PythonResource,
343             add_context: &mut PythonResourceAddCollectionContext|
344             -> Result<()> {
345                // Callback is declared Fn, so we can't take a mutable reference.
346                // A copy should be fine.
347                let mut cs = call_stack.clone();
348
349                // There is a PythonPackagingPolicy passed into this callback
350                // and one passed into the outer function as a &Value. The
351                // former is derived from the latter. And the latter has Starlark
352                // callbacks registered on it.
353                //
354                // When we call python_resource_to_value(), the Starlark
355                // callbacks are automatically called.
356
357                let value =
358                    python_resource_to_value(LABEL, type_values, &mut cs, resource, &policy)
359                        .map_err(|e| {
360                            anyhow!("error converting PythonResource to Value: {:?}", e)
361                        })?;
362
363                let new_add_context = add_context_for_value(&value, "to_python_executable")
364                    .map_err(|e| anyhow!("error obtaining add context from Value: {:?}", e))?
365                    .expect("add context should have been populated as part of Value conversion");
366
367                add_context.replace(&new_add_context);
368
369                Ok(())
370            },
371        );
372
373        for action in builder
374            .add_distribution_resources(Some(callback))
375            .map_err(|e| {
376                ValueError::from(RuntimeError {
377                    code: "PYOXIDIZER_BUILD",
378                    message: format!("{:?}", e),
379                    label: "to_python_executable()".to_string(),
380                })
381            })?
382        {
383            info!("{}", action.to_string());
384        }
385
386        Ok(Value::new(PythonExecutableValue::new(builder, policy)))
387    }
388
389    pub fn python_resources_starlark(
390        &mut self,
391        type_values: &TypeValues,
392        call_stack: &mut CallStack,
393    ) -> ValueResult {
394        const LABEL: &str = "PythonDistribution.python_resources()";
395
396        let dist = self.resolve_distribution(type_values, "resolve_distribution")?;
397        let policy =
398            PythonPackagingPolicyValue::new(dist.create_packaging_policy().map_err(|e| {
399                ValueError::from(RuntimeError {
400                    code: "PYTHON_DISTRIBUTION",
401                    message: format!("{:?}", e),
402                    label: LABEL.to_string(),
403                })
404            })?);
405
406        let values = dist
407            .python_resources()
408            .iter()
409            .map(|resource| {
410                python_resource_to_value(LABEL, type_values, call_stack, resource, &policy)
411            })
412            .collect::<Result<Vec<Value>, ValueError>>()?;
413
414        Ok(Value::from(values))
415    }
416}
417
418starlark_module! { python_distribution_module =>
419    #[allow(non_snake_case)]
420    PythonDistribution(sha256: String, local_path=NoneType::None, url=NoneType::None, flavor: String = "standalone".to_string()) {
421        PythonDistributionValue::from_args(sha256, &local_path, &url, flavor)
422    }
423
424    PythonDistribution.make_python_packaging_policy(env env, this) {
425        let mut this = this.downcast_mut::<PythonDistributionValue>().unwrap().unwrap();
426        this.make_python_packaging_policy_starlark(env)
427    }
428
429    PythonDistribution.make_python_interpreter_config(env env, this) {
430        let mut this = this.downcast_mut::<PythonDistributionValue>().unwrap().unwrap();
431        this.make_python_interpreter_config_starlark(env)
432    }
433
434    PythonDistribution.python_resources(env env, call_stack cs, this) {
435        let mut this = this.downcast_mut::<PythonDistributionValue>().unwrap().unwrap();
436        this.python_resources_starlark(env, cs)
437    }
438
439    PythonDistribution.to_python_executable(
440        env env,
441        call_stack cs,
442        this,
443        name: String,
444        packaging_policy=NoneType::None,
445        config=NoneType::None
446    ) {
447        let mut this = this.downcast_mut::<PythonDistributionValue>().unwrap().unwrap();
448        this.to_python_executable_starlark(
449            env,
450            cs,
451            name,
452            &packaging_policy,
453            &config,
454        )
455    }
456
457    default_python_distribution(
458        env env,
459        flavor: String = "standalone".to_string(),
460        build_target=NoneType::None,
461        python_version=NoneType::None
462    ) {
463        PythonDistributionValue::default_python_distribution(env, flavor, &build_target, &python_version)
464    }
465}
466
467#[cfg(test)]
468mod tests {
469    use {
470        super::*,
471        crate::{
472            environment::default_target_triple,
473            py_packaging::distribution::DistributionFlavor,
474            python_distributions::PYTHON_DISTRIBUTIONS,
475            starlark::{
476                python_extension_module::PythonExtensionModuleValue,
477                python_module_source::PythonModuleSourceValue,
478                python_package_resource::PythonPackageResourceValue, testutil::*,
479            },
480        },
481    };
482
483    #[test]
484    fn test_default_python_distribution() {
485        let dist = starlark_ok("default_python_distribution()");
486        assert_eq!(dist.get_type(), "PythonDistribution");
487
488        let host_distribution = PYTHON_DISTRIBUTIONS
489            .find_distribution(
490                default_target_triple(),
491                &DistributionFlavor::Standalone,
492                None,
493            )
494            .unwrap();
495
496        let x = dist.downcast_ref::<PythonDistributionValue>().unwrap();
497        assert_eq!(x.source, host_distribution.location)
498    }
499
500    #[test]
501    // Python 3.8 not supported on aarch64.
502    #[cfg(not(target_arch = "aarch64"))]
503    fn test_default_python_distribution_python_38() -> Result<()> {
504        let mut env = test_evaluation_context_builder()?.into_context()?;
505
506        let dist = env.eval("default_python_distribution(python_version='3.8')")?;
507        assert_eq!(dist.get_type(), "PythonDistribution");
508
509        let wanted = PYTHON_DISTRIBUTIONS
510            .find_distribution(
511                default_target_triple(),
512                &DistributionFlavor::Standalone,
513                Some("3.8"),
514            )
515            .unwrap();
516
517        let x = dist.downcast_ref::<PythonDistributionValue>().unwrap();
518        assert_eq!(x.source, wanted.location);
519
520        Ok(())
521    }
522
523    #[test]
524    fn test_default_python_distribution_python_39() -> Result<()> {
525        let mut env = test_evaluation_context_builder()?.into_context()?;
526
527        let dist = env.eval("default_python_distribution(python_version='3.9')")?;
528        assert_eq!(dist.get_type(), "PythonDistribution");
529
530        let wanted = PYTHON_DISTRIBUTIONS
531            .find_distribution(
532                default_target_triple(),
533                &DistributionFlavor::Standalone,
534                Some("3.9"),
535            )
536            .unwrap();
537
538        let x = dist.downcast_ref::<PythonDistributionValue>().unwrap();
539        assert_eq!(x.source, wanted.location);
540
541        Ok(())
542    }
543
544    #[test]
545    fn test_default_python_distribution_python_310() -> Result<()> {
546        let mut env = test_evaluation_context_builder()?.into_context()?;
547
548        let dist = env.eval("default_python_distribution(python_version='3.10')")?;
549        assert_eq!(dist.get_type(), "PythonDistribution");
550
551        let wanted = PYTHON_DISTRIBUTIONS
552            .find_distribution(
553                default_target_triple(),
554                &DistributionFlavor::Standalone,
555                Some("3.10"),
556            )
557            .unwrap();
558
559        let x = dist.downcast_ref::<PythonDistributionValue>().unwrap();
560        assert_eq!(x.source, wanted.location);
561
562        Ok(())
563    }
564
565    #[test]
566    #[cfg(windows)]
567    fn test_default_python_distribution_dynamic_windows() {
568        let dist = starlark_ok("default_python_distribution(flavor='standalone_dynamic')");
569        assert_eq!(dist.get_type(), "PythonDistribution");
570
571        let host_distribution = PYTHON_DISTRIBUTIONS
572            .find_distribution(
573                default_target_triple(),
574                &DistributionFlavor::StandaloneDynamic,
575                None,
576            )
577            .unwrap();
578
579        let x = dist.downcast_ref::<PythonDistributionValue>().unwrap();
580        assert_eq!(x.source, host_distribution.location)
581    }
582
583    #[test]
584    fn test_python_distribution_no_args() {
585        let err = starlark_nok("PythonDistribution()");
586        assert!(err.message.starts_with("Missing parameter sha256"));
587    }
588
589    #[test]
590    fn test_python_distribution_multiple_args() {
591        let err = starlark_nok(
592            "PythonDistribution('sha256', url='url_value', local_path='local_path_value')",
593        );
594        assert_eq!(err.message, "cannot define both local_path and url");
595    }
596
597    #[test]
598    fn test_python_distribution_url() {
599        let dist = starlark_ok("PythonDistribution('sha256', url='some_url')");
600        let wanted = PythonDistributionLocation::Url {
601            url: "some_url".to_string(),
602            sha256: "sha256".to_string(),
603        };
604
605        let x = dist.downcast_ref::<PythonDistributionValue>().unwrap();
606        assert_eq!(x.source, wanted);
607    }
608
609    #[test]
610    fn test_python_distribution_local_path() {
611        let dist = starlark_ok("PythonDistribution('sha256', local_path='some_path')");
612        let wanted = PythonDistributionLocation::Local {
613            local_path: "some_path".to_string(),
614            sha256: "sha256".to_string(),
615        };
616
617        let x = dist.downcast_ref::<PythonDistributionValue>().unwrap();
618        assert_eq!(x.source, wanted);
619    }
620
621    #[test]
622    fn test_make_python_packaging_policy() {
623        let policy = starlark_ok("default_python_distribution().make_python_packaging_policy()");
624        assert_eq!(policy.get_type(), "PythonPackagingPolicy");
625    }
626
627    #[test]
628    fn test_make_python_interpreter_config() {
629        let config = starlark_ok("default_python_distribution().make_python_interpreter_config()");
630        assert_eq!(config.get_type(), "PythonInterpreterConfig");
631    }
632
633    #[test]
634    fn test_python_resources() {
635        let resources = starlark_ok("default_python_distribution().python_resources()");
636        assert_eq!(resources.get_type(), "list");
637
638        let values = resources.iter().unwrap().to_vec();
639
640        assert!(values.len() > 100);
641
642        assert!(values
643            .iter()
644            .any(|v| v.get_type() == PythonModuleSourceValue::TYPE));
645        assert!(values
646            .iter()
647            .any(|v| v.get_type() == PythonExtensionModuleValue::TYPE));
648        assert!(values
649            .iter()
650            .any(|v| v.get_type() == PythonPackageResourceValue::TYPE));
651
652        assert!(values
653            .iter()
654            .filter(|v| v.get_type() == PythonModuleSourceValue::TYPE)
655            .all(|v| v.get_attr("is_stdlib").unwrap().to_bool()));
656        assert!(values
657            .iter()
658            .filter(|v| v.get_type() == PythonExtensionModuleValue::TYPE)
659            .all(|v| v.get_attr("is_stdlib").unwrap().to_bool()));
660        assert!(values
661            .iter()
662            .filter(|v| v.get_type() == PythonPackageResourceValue::TYPE)
663            .all(|v| v.get_attr("is_stdlib").unwrap().to_bool()));
664    }
665}