1use {
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
43pub struct PythonDistributionValue {
45 pub source: PythonDistributionLocation,
47
48 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
107impl PythonDistributionValue {
109 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 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 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 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 #[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 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 let mut cs = call_stack.clone();
348
349 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 #[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}