pyoxidizerlib/starlark/
python_executable.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        file::FileValue,
9        file_resource::file_manifest_add_python_executable,
10        python_embedded_resources::PythonEmbeddedResourcesValue,
11        python_extension_module::PythonExtensionModuleValue,
12        python_module_source::PythonModuleSourceValue,
13        python_package_distribution_resource::PythonPackageDistributionResourceValue,
14        python_package_resource::PythonPackageResourceValue,
15        python_packaging_policy::PythonPackagingPolicyValue,
16        python_resource::{is_resource_starlark_compatible, python_resource_to_value},
17        util::ToValue,
18    },
19    crate::{
20        licensing::licenses_from_cargo_manifest,
21        project_building::build_python_executable,
22        py_packaging::binary::PythonBinaryBuilder,
23        py_packaging::binary::{PackedResourcesLoadMode, WindowsRuntimeDllsMode},
24    },
25    anyhow::{anyhow, Context, Result},
26    linked_hash_map::LinkedHashMap,
27    log::{info, warn},
28    python_packaging::resource::PythonModuleSource,
29    simple_file_manifest::FileData,
30    starlark::{
31        environment::TypeValues,
32        eval::call_stack::CallStack,
33        values::{
34            error::{
35                RuntimeError, UnsupportedOperation, ValueError, INCORRECT_PARAMETER_TYPE_ERROR_CODE,
36            },
37            none::NoneType,
38            {Mutable, TypedValue, Value, ValueResult},
39        },
40        {
41            starlark_fun, starlark_module, starlark_parse_param_type, starlark_signature,
42            starlark_signature_extraction, starlark_signatures,
43        },
44    },
45    starlark_dialect_build_targets::{
46        optional_dict_arg, optional_list_arg, optional_str_arg, optional_type_arg,
47        required_list_arg, ResolvedTarget, ResolvedTargetValue, RunMode, ToOptional,
48    },
49    std::{
50        collections::HashMap,
51        io::Write,
52        ops::Deref,
53        path::{Path, PathBuf},
54        sync::{Arc, Mutex, MutexGuard},
55    },
56    tugger::starlark::{
57        code_signing::{handle_signable_event, SigningAction, SigningContext},
58        file_manifest::FileManifestValue,
59        wix_bundle_builder::WiXBundleBuilderValue,
60        wix_msi_builder::WiXMsiBuilderValue,
61    },
62    tugger_code_signing::SigningDestination,
63    tugger_wix::target_triple_to_wix_arch,
64};
65
66fn error_context<F, T>(label: &str, f: F) -> Result<T, ValueError>
67where
68    F: FnOnce() -> anyhow::Result<T>,
69{
70    f().map_err(|e| {
71        ValueError::Runtime(RuntimeError {
72            code: "PYOXIDIZER_PYTHON_EXECUTABLE",
73            message: format!("{:?}", e),
74            label: label.to_string(),
75        })
76    })
77}
78
79pub fn build_internal(
80    exe: MutexGuard<Box<dyn PythonBinaryBuilder>>,
81    type_values: &TypeValues,
82    target: &str,
83    context: &PyOxidizerEnvironmentContext,
84) -> Result<(ResolvedTarget, PathBuf)> {
85    // Build an executable by writing out a temporary Rust project
86    // and building it.
87    let build = build_python_executable(
88        context.env(),
89        &exe.name(),
90        &**exe,
91        &context.build_target_triple,
92        &context.build_opt_level,
93        context.build_release,
94    )
95    .context("building Python executable")?;
96
97    let output_path = context
98        .get_output_path(type_values, target)
99        .map_err(|_| anyhow!("unable to resolve output path"))?;
100    std::fs::create_dir_all(&output_path)
101        .with_context(|| format!("creating output directory {}", output_path.display()))?;
102
103    let dest_path = output_path.join(build.exe_name);
104    warn!("writing executable to {}", dest_path.display());
105    let mut fh =
106        std::fs::File::create(&dest_path).context(format!("creating {}", dest_path.display()))?;
107    fh.write_all(&build.exe_data)
108        .context(format!("writing {}", dest_path.display()))?;
109    simple_file_manifest::set_executable(&mut fh).context("making binary executable")?;
110
111    Ok((
112        ResolvedTarget {
113            run_mode: RunMode::Path {
114                path: dest_path.clone(),
115            },
116            output_path,
117        },
118        dest_path,
119    ))
120}
121
122/// Represents a builder for a Python executable.
123pub struct PythonExecutableValue {
124    // The non-Starlark API to construct the builder returns a Box<T> and it is
125    // easier to wrap a Box<T> then try to unpack it into an Arc<T>.
126    pub exe: Arc<Mutex<Box<dyn PythonBinaryBuilder>>>,
127
128    /// The Starlark Value for the Python packaging policy.
129    // This is stored as a Vec because I couldn't figure out how to implement
130    // values_for_descendant_check_and_freeze() without the borrow checker
131    // complaining due to a temporary vec/array.
132    policy: Vec<Value>,
133}
134
135impl PythonExecutableValue {
136    pub fn new(exe: Box<dyn PythonBinaryBuilder>, policy: PythonPackagingPolicyValue) -> Self {
137        Self {
138            exe: Arc::new(Mutex::new(exe)),
139            policy: vec![Value::new(policy)],
140        }
141    }
142
143    pub fn inner(
144        &self,
145        label: &str,
146    ) -> Result<MutexGuard<Box<dyn PythonBinaryBuilder>>, ValueError> {
147        self.exe.try_lock().map_err(|e| {
148            ValueError::Runtime(RuntimeError {
149                code: "PYTHON_EXECUTABLE",
150                message: format!("failed to acquire lock: {}", e),
151                label: label.to_string(),
152            })
153        })
154    }
155
156    /// Obtains a copy of the `PythonPackagingPolicyValue` stored internally.
157    pub fn python_packaging_policy(&self) -> PythonPackagingPolicyValue {
158        self.policy[0]
159            .downcast_ref::<PythonPackagingPolicyValue>()
160            .unwrap()
161            .clone()
162    }
163}
164
165impl TypedValue for PythonExecutableValue {
166    type Holder = Mutable<PythonExecutableValue>;
167    const TYPE: &'static str = "PythonExecutable";
168
169    fn values_for_descendant_check_and_freeze<'a>(
170        &'a self,
171    ) -> Box<dyn Iterator<Item = Value> + 'a> {
172        Box::new(self.policy.iter().cloned())
173    }
174
175    fn get_attr(&self, attribute: &str) -> ValueResult {
176        let exe = self.inner(&format!("PythonExecutable.{}", attribute))?;
177
178        match attribute {
179            "licenses_filename" => Ok(exe.licenses_filename().to_value()),
180            "packed_resources_load_mode" => {
181                Ok(Value::from(exe.packed_resources_load_mode().to_string()))
182            }
183            "tcl_files_path" => match exe.tcl_files_path() {
184                Some(value) => Ok(Value::from(value.to_string())),
185                None => Ok(Value::from(NoneType::None)),
186            },
187            "windows_runtime_dlls_mode" => {
188                Ok(Value::from(exe.windows_runtime_dlls_mode().to_string()))
189            }
190            "windows_subsystem" => Ok(Value::from(exe.windows_subsystem())),
191            _ => Err(ValueError::OperationNotSupported {
192                op: UnsupportedOperation::GetAttr(attribute.to_string()),
193                left: Self::TYPE.to_string(),
194                right: None,
195            }),
196        }
197    }
198
199    fn has_attr(&self, attribute: &str) -> Result<bool, ValueError> {
200        Ok(matches!(
201            attribute,
202            "licenses_filename"
203                | "packed_resources_load_mode"
204                | "tcl_files_path"
205                | "windows_runtime_dlls_mode"
206                | "windows_subsystem"
207        ))
208    }
209
210    fn set_attr(&mut self, attribute: &str, value: Value) -> Result<(), ValueError> {
211        let mut exe = self.inner(&format!("PythonExecutable.{}", attribute))?;
212
213        match attribute {
214            "licenses_filename" => {
215                let value = optional_str_arg("licenses_filename", &value)?;
216                exe.set_licenses_filename(value);
217
218                Ok(())
219            }
220            "packed_resources_load_mode" => {
221                exe.set_packed_resources_load_mode(
222                    PackedResourcesLoadMode::try_from(value.to_string().as_str()).map_err(|e| {
223                        ValueError::from(RuntimeError {
224                            code: INCORRECT_PARAMETER_TYPE_ERROR_CODE,
225                            message: e,
226                            label: format!("{}.{}", Self::TYPE, attribute),
227                        })
228                    })?,
229                );
230
231                Ok(())
232            }
233            "tcl_files_path" => {
234                exe.set_tcl_files_path(value.to_optional());
235
236                Ok(())
237            }
238            "windows_runtime_dlls_mode" => {
239                exe.set_windows_runtime_dlls_mode(
240                    WindowsRuntimeDllsMode::try_from(value.to_string().as_str()).map_err(|e| {
241                        ValueError::from(RuntimeError {
242                            code: INCORRECT_PARAMETER_TYPE_ERROR_CODE,
243                            message: e,
244                            label: format!("{}.{}", Self::TYPE, attribute),
245                        })
246                    })?,
247                );
248
249                Ok(())
250            }
251            "windows_subsystem" => {
252                exe.set_windows_subsystem(value.to_string().as_str())
253                    .map_err(|e| {
254                        ValueError::from(RuntimeError {
255                            code: INCORRECT_PARAMETER_TYPE_ERROR_CODE,
256                            message: format!("{:?}", e),
257                            label: format!("{}.{}", Self::TYPE, attribute),
258                        })
259                    })?;
260
261                Ok(())
262            }
263            _ => Err(ValueError::OperationNotSupported {
264                op: UnsupportedOperation::SetAttr(attribute.to_string()),
265                left: Self::TYPE.to_string(),
266                right: None,
267            }),
268        }
269    }
270}
271
272// Starlark functions.
273impl PythonExecutableValue {
274    fn build(
275        &self,
276        type_values: &TypeValues,
277        call_stack: &mut CallStack,
278        target: String,
279    ) -> ValueResult {
280        const LABEL: &str = "PythonExecutable.build()";
281
282        let pyoxidizer_context_value = get_context(type_values)?;
283        let pyoxidizer_context = pyoxidizer_context_value
284            .downcast_ref::<PyOxidizerEnvironmentContext>()
285            .ok_or(ValueError::IncorrectParameterType)?;
286
287        let exe = self.inner(LABEL)?;
288
289        let (inner, exe_path) = error_context(LABEL, || {
290            build_internal(exe, type_values, &target, &pyoxidizer_context)
291        })?;
292
293        let candidate = exe_path.clone().into();
294        let mut context = SigningContext::new(
295            "PythonExecutable.build()",
296            SigningAction::Other("python-executable-creation"),
297            exe_path.file_name().ok_or_else(|| {
298                ValueError::Runtime(RuntimeError {
299                    code: "PYTHON_EXECUTABLE",
300                    message: "could not determine executable filename (this should not happen)"
301                        .to_string(),
302                    label: LABEL.to_string(),
303                })
304            })?,
305            &candidate,
306        );
307        context.set_path(&exe_path);
308        context.set_signing_destination(SigningDestination::File(exe_path.clone()));
309
310        handle_signable_event(type_values, call_stack, context)?;
311
312        Ok(Value::new(ResolvedTargetValue { inner }))
313    }
314
315    /// PythonExecutable.make_python_module_source(name, source, is_package=false)
316    pub fn make_python_module_source(
317        &self,
318        type_values: &TypeValues,
319        call_stack: &mut CallStack,
320        name: String,
321        source: String,
322        is_package: bool,
323    ) -> ValueResult {
324        const LABEL: &str = "PythonExecutable.make_python_module_source()";
325
326        let module = PythonModuleSource {
327            name,
328            source: FileData::Memory(source.into_bytes()),
329            is_package,
330            cache_tag: self.inner(LABEL)?.cache_tag().to_string(),
331            is_stdlib: false,
332            is_test: false,
333        };
334
335        let mut value = PythonModuleSourceValue::new(module);
336        self.python_packaging_policy().apply_to_resource(
337            LABEL,
338            type_values,
339            call_stack,
340            &mut value,
341        )?;
342
343        Ok(Value::new(value))
344    }
345
346    /// PythonExecutable.pip_download(args)
347    pub fn pip_download(
348        &mut self,
349        type_values: &TypeValues,
350        call_stack: &mut CallStack,
351        args: &Value,
352    ) -> ValueResult {
353        const LABEL: &str = "PythonExecutable.pip_download()";
354
355        required_list_arg("args", "string", args)?;
356
357        let args: Vec<String> = args.iter()?.iter().map(|x| x.to_string()).collect();
358
359        let pyoxidizer_context_value = get_context(type_values)?;
360        let pyoxidizer_context = pyoxidizer_context_value
361            .downcast_ref::<PyOxidizerEnvironmentContext>()
362            .ok_or(ValueError::IncorrectParameterType)?;
363
364        let python_packaging_policy = self.python_packaging_policy();
365
366        let mut exe = self.inner(LABEL)?;
367
368        let resources = error_context("PythonExecutable.pip_download()", || {
369            exe.pip_download(pyoxidizer_context.env(), pyoxidizer_context.verbose, &args)
370        })?;
371
372        let resources = resources
373            .iter()
374            .filter(|r| is_resource_starlark_compatible(r))
375            .map(|r| {
376                python_resource_to_value(
377                    LABEL,
378                    type_values,
379                    call_stack,
380                    r,
381                    &python_packaging_policy,
382                )
383            })
384            .collect::<Result<Vec<Value>, ValueError>>()?;
385
386        Ok(Value::from(resources))
387    }
388
389    /// PythonExecutable.pip_install(args, extra_envs=None)
390    pub fn pip_install(
391        &mut self,
392        type_values: &TypeValues,
393        call_stack: &mut CallStack,
394        args: &Value,
395        extra_envs: &Value,
396    ) -> ValueResult {
397        const LABEL: &str = "PythonExecutable.pip_install()";
398
399        required_list_arg("args", "string", args)?;
400        optional_dict_arg("extra_envs", "string", "string", extra_envs)?;
401
402        let args: Vec<String> = args.iter()?.iter().map(|x| x.to_string()).collect();
403
404        let extra_envs = match extra_envs.get_type() {
405            "dict" => extra_envs
406                .iter()?
407                .iter()
408                .map(|key| {
409                    let k = key.to_string();
410                    let v = extra_envs.at(key).unwrap().to_string();
411                    (k, v)
412                })
413                .collect(),
414            "NoneType" => HashMap::new(),
415            _ => panic!("should have validated type above"),
416        };
417
418        let pyoxidizer_context_value = get_context(type_values)?;
419        let pyoxidizer_context = pyoxidizer_context_value
420            .downcast_ref::<PyOxidizerEnvironmentContext>()
421            .ok_or(ValueError::IncorrectParameterType)?;
422
423        let python_packaging_policy = self.python_packaging_policy();
424
425        let mut exe = self.inner(LABEL)?;
426
427        let resources = error_context(LABEL, || {
428            exe.pip_install(
429                pyoxidizer_context.env(),
430                pyoxidizer_context.verbose,
431                &args,
432                &extra_envs,
433            )
434        })?;
435
436        let resources = resources
437            .iter()
438            .filter(|r| is_resource_starlark_compatible(r))
439            .map(|r| {
440                python_resource_to_value(
441                    LABEL,
442                    type_values,
443                    call_stack,
444                    r,
445                    &python_packaging_policy,
446                )
447            })
448            .collect::<Result<Vec<Value>, ValueError>>()?;
449
450        Ok(Value::from(resources))
451    }
452
453    /// PythonExecutable.read_package_root(path, packages)
454    pub fn read_package_root(
455        &mut self,
456        type_values: &TypeValues,
457        call_stack: &mut CallStack,
458        path: String,
459        packages: &Value,
460    ) -> ValueResult {
461        const LABEL: &str = "PythonExecutable.read_package_root()";
462
463        required_list_arg("packages", "string", packages)?;
464
465        let packages = packages
466            .iter()?
467            .iter()
468            .map(|x| x.to_string())
469            .collect::<Vec<String>>();
470
471        let python_packaging_policy = self.python_packaging_policy();
472
473        let mut exe = self.inner(LABEL)?;
474
475        let resources =
476            error_context(LABEL, || exe.read_package_root(Path::new(&path), &packages))?;
477
478        let resources = resources
479            .iter()
480            .filter(|r| is_resource_starlark_compatible(r))
481            .map(|r| {
482                python_resource_to_value(
483                    LABEL,
484                    type_values,
485                    call_stack,
486                    r,
487                    &python_packaging_policy,
488                )
489            })
490            .collect::<Result<Vec<Value>, ValueError>>()?;
491
492        Ok(Value::from(resources))
493    }
494
495    /// PythonExecutable.read_virtualenv(path)
496    pub fn read_virtualenv(
497        &mut self,
498        type_values: &TypeValues,
499        call_stack: &mut CallStack,
500        path: String,
501    ) -> ValueResult {
502        const LABEL: &str = "PythonExecutable.read_virtualenv()";
503
504        let python_packaging_policy = self.python_packaging_policy();
505
506        let mut exe = self.inner(LABEL)?;
507
508        let resources = error_context(LABEL, || exe.read_virtualenv(Path::new(&path)))?;
509
510        let resources = resources
511            .iter()
512            .filter(|r| is_resource_starlark_compatible(r))
513            .map(|r| {
514                python_resource_to_value(
515                    LABEL,
516                    type_values,
517                    call_stack,
518                    r,
519                    &python_packaging_policy,
520                )
521            })
522            .collect::<Result<Vec<Value>, ValueError>>()?;
523
524        Ok(Value::from(resources))
525    }
526
527    /// PythonExecutable.setup_py_install(package_path, extra_envs=None, extra_global_arguments=None)
528    pub fn setup_py_install(
529        &mut self,
530        type_values: &TypeValues,
531        call_stack: &mut CallStack,
532        package_path: String,
533        extra_envs: &Value,
534        extra_global_arguments: &Value,
535    ) -> ValueResult {
536        const LABEL: &str = "PythonExecutable.setup_py_install()";
537
538        optional_dict_arg("extra_envs", "string", "string", extra_envs)?;
539        optional_list_arg("extra_global_arguments", "string", extra_global_arguments)?;
540
541        let extra_envs = match extra_envs.get_type() {
542            "dict" => extra_envs
543                .iter()?
544                .iter()
545                .map(|key| {
546                    let k = key.to_string();
547                    let v = extra_envs.at(key).unwrap().to_string();
548                    (k, v)
549                })
550                .collect(),
551            "NoneType" => HashMap::new(),
552            _ => panic!("should have validated type above"),
553        };
554        let extra_global_arguments = match extra_global_arguments.get_type() {
555            "list" => extra_global_arguments
556                .iter()?
557                .iter()
558                .map(|x| x.to_string())
559                .collect(),
560            "NoneType" => Vec::new(),
561            _ => panic!("should have validated type above"),
562        };
563
564        let package_path = PathBuf::from(package_path);
565
566        let pyoxidizer_context_value = get_context(type_values)?;
567        let pyoxidizer_context = pyoxidizer_context_value
568            .downcast_ref::<PyOxidizerEnvironmentContext>()
569            .ok_or(ValueError::IncorrectParameterType)?;
570
571        let package_path = if package_path.is_absolute() {
572            package_path
573        } else {
574            PathBuf::from(&pyoxidizer_context.cwd).join(package_path)
575        };
576
577        let python_packaging_policy = self.python_packaging_policy();
578
579        let mut exe = self.inner(LABEL)?;
580
581        let resources = error_context(LABEL, || {
582            exe.setup_py_install(
583                pyoxidizer_context.env(),
584                &package_path,
585                pyoxidizer_context.verbose,
586                &extra_envs,
587                &extra_global_arguments,
588            )
589        })?;
590
591        let resources = resources
592            .iter()
593            .filter(|r| is_resource_starlark_compatible(r))
594            .map(|r| {
595                python_resource_to_value(
596                    LABEL,
597                    type_values,
598                    call_stack,
599                    r,
600                    &python_packaging_policy,
601                )
602            })
603            .collect::<Result<Vec<Value>, ValueError>>()?;
604
605        warn!(
606            "collected {} resources from setup.py install",
607            resources.len()
608        );
609
610        Ok(Value::from(resources))
611    }
612
613    pub fn add_python_module_source(
614        &mut self,
615        label: &str,
616        module: &PythonModuleSourceValue,
617    ) -> ValueResult {
618        let inner = module.inner(label)?;
619
620        let mut exe = self.inner(label)?;
621
622        error_context(label, || {
623            for action in exe
624                .add_python_module_source(&inner.m, inner.add_context.clone())
625                .with_context(|| format!("adding {}", module.to_repr()))?
626            {
627                info!("{}", action.to_string());
628            }
629
630            Ok(())
631        })?;
632
633        Ok(Value::new(NoneType::None))
634    }
635
636    pub fn add_python_package_resource(
637        &mut self,
638        label: &str,
639        resource: &PythonPackageResourceValue,
640    ) -> ValueResult {
641        let inner = resource.inner(label)?;
642
643        let mut exe = self.inner(label)?;
644
645        error_context(label, || {
646            for action in exe
647                .add_python_package_resource(&inner.r, inner.add_context.clone())
648                .with_context(|| format!("adding {}", resource.to_repr()))?
649            {
650                info!("{}", action.to_string());
651            }
652
653            Ok(())
654        })?;
655
656        Ok(Value::new(NoneType::None))
657    }
658
659    pub fn add_python_package_distribution_resource(
660        &mut self,
661        label: &str,
662        resource: &PythonPackageDistributionResourceValue,
663    ) -> ValueResult {
664        let inner = resource.inner(label)?;
665
666        let mut exe = self.inner(label)?;
667
668        error_context(label, || {
669            for action in exe
670                .add_python_package_distribution_resource(&inner.r, inner.add_context.clone())
671                .with_context(|| format!("adding {}", resource.to_repr()))?
672            {
673                info!("{}", action.to_string());
674            }
675
676            Ok(())
677        })?;
678
679        Ok(Value::new(NoneType::None))
680    }
681
682    pub fn add_python_extension_module(
683        &mut self,
684        label: &str,
685        module: &PythonExtensionModuleValue,
686    ) -> ValueResult {
687        let inner = module.inner(label)?;
688
689        let mut exe = self.inner(label)?;
690
691        error_context(label, || {
692            for action in exe
693                .add_python_extension_module(&inner.em, inner.add_context.clone())
694                .with_context(|| format!("adding {}", module.to_repr()))?
695            {
696                info!("{}", action.to_string());
697            }
698
699            Ok(())
700        })?;
701
702        Ok(Value::new(NoneType::None))
703    }
704
705    pub fn add_file_data(&mut self, label: &str, file: &FileValue) -> ValueResult {
706        let inner = file.inner(label)?;
707
708        let mut exe = self.inner(label)?;
709
710        error_context(label, || {
711            for action in exe
712                .add_file_data(&inner.file, inner.add_context.clone())
713                .with_context(|| format!("adding {}", file.to_repr()))?
714            {
715                info!("{}", action.to_string());
716            }
717
718            Ok(())
719        })?;
720
721        Ok(Value::new(NoneType::None))
722    }
723
724    /// PythonExecutable.add_python_resource(resource)
725    pub fn add_python_resource(&mut self, resource: &Value, label: &str) -> ValueResult {
726        match resource.get_type() {
727            FileValue::TYPE => {
728                let file = resource.downcast_ref::<FileValue>().unwrap();
729                self.add_file_data(label, file.deref())
730            }
731            PythonModuleSourceValue::TYPE => {
732                let module = resource.downcast_ref::<PythonModuleSourceValue>().unwrap();
733                self.add_python_module_source(label, module.deref())
734            }
735            PythonPackageResourceValue::TYPE => {
736                let r = resource
737                    .downcast_ref::<PythonPackageResourceValue>()
738                    .unwrap();
739                self.add_python_package_resource(label, r.deref())
740            }
741            PythonPackageDistributionResourceValue::TYPE => {
742                let r = resource
743                    .downcast_ref::<PythonPackageDistributionResourceValue>()
744                    .unwrap();
745                self.add_python_package_distribution_resource(label, r.deref())
746            }
747            PythonExtensionModuleValue::TYPE => {
748                let module = resource
749                    .downcast_ref::<PythonExtensionModuleValue>()
750                    .unwrap();
751                self.add_python_extension_module(label, module.deref())
752            }
753            _ => Err(ValueError::from(RuntimeError {
754                code: INCORRECT_PARAMETER_TYPE_ERROR_CODE,
755                message: "resource argument must be a Python resource type".to_string(),
756                label: ".add_python_resource()".to_string(),
757            })),
758        }
759    }
760
761    /// PythonExecutable.add_python_resources(resources)
762    pub fn add_python_resources(&mut self, resources: &Value) -> ValueResult {
763        for resource in &resources.iter()? {
764            self.add_python_resource(&resource, "add_python_resources()")?;
765        }
766
767        Ok(Value::new(NoneType::None))
768    }
769
770    /// Add licensing information from a `Cargo.toml` manifest.
771    pub fn add_cargo_manifest_licensing(
772        &mut self,
773        type_values: &TypeValues,
774        manifest_path: &str,
775        all_features: bool,
776        features: &Value,
777    ) -> ValueResult {
778        const LABEL: &str = "PythonExecutable.add_cargo_manifest_licensing()";
779
780        optional_list_arg("features", "string", features)?;
781
782        let features = match features.get_type() {
783            "list" => features.iter()?.iter().map(|x| x.to_string()).collect(),
784            "NoneType" => Vec::new(),
785            _ => panic!("type should have been validated above"),
786        };
787
788        let pyoxidizer_context_value = get_context(type_values)?;
789        let pyoxidizer_context = pyoxidizer_context_value
790            .downcast_ref::<PyOxidizerEnvironmentContext>()
791            .ok_or(ValueError::IncorrectParameterType)?;
792
793        let mut exe = self.inner(LABEL)?;
794
795        error_context(LABEL, || {
796            let toolchain = pyoxidizer_context.env().ensure_rust_toolchain(None)?;
797
798            let components = licenses_from_cargo_manifest(
799                manifest_path,
800                all_features,
801                features.iter().map(|x| x.as_str()),
802                Some(exe.target_triple()),
803                &toolchain,
804                true,
805            )?;
806
807            for component in components.into_components() {
808                warn!("adding licensed component {}", component.flavor());
809                exe.add_licensed_component(component)?;
810            }
811
812            Ok(())
813        })?;
814
815        Ok(Value::new(NoneType::None))
816    }
817
818    /// PythonExecutable.to_embedded_resources()
819    pub fn to_embedded_resources(&self) -> ValueResult {
820        const LABEL: &str = "PythonExecutable.to_embedded_resources()";
821
822        Ok(Value::new(PythonEmbeddedResourcesValue {
823            exe: self.inner(LABEL)?.clone_trait(),
824        }))
825    }
826
827    /// PythonExecutable.to_file_manifest(prefix)
828    pub fn to_file_manifest(&self, type_values: &TypeValues, prefix: String) -> ValueResult {
829        const LABEL: &str = "PythonExecutable.to_file_manifest()";
830
831        let pyoxidizer_context_value = get_context(type_values)?;
832        let pyoxidizer_context = pyoxidizer_context_value
833            .downcast_ref::<PyOxidizerEnvironmentContext>()
834            .ok_or(ValueError::IncorrectParameterType)?;
835
836        let manifest_value = FileManifestValue::new_from_args()?;
837        let mut manifest = manifest_value
838            .downcast_mut::<FileManifestValue>()
839            .unwrap()
840            .unwrap();
841
842        let exe = self.inner(LABEL)?;
843
844        error_context(LABEL, || {
845            file_manifest_add_python_executable(
846                &mut manifest,
847                pyoxidizer_context.env(),
848                &prefix,
849                &**exe,
850                &pyoxidizer_context.build_target_triple,
851                pyoxidizer_context.build_release,
852                &pyoxidizer_context.build_opt_level,
853            )
854            .context("adding PythonExecutable to FileManifest")
855        })?;
856
857        Ok(manifest_value.clone())
858    }
859
860    /// PythonExecutable.to_wix_bundle_builder(id_prefix, name, version, manufacturer, msi_builder_callback)
861    #[allow(clippy::too_many_arguments)]
862    pub fn to_wix_bundle_builder(
863        &self,
864        type_values: &TypeValues,
865        call_stack: &mut CallStack,
866        id_prefix: String,
867        product_name: String,
868        product_version: String,
869        product_manufacturer: String,
870        msi_builder_callback: Value,
871    ) -> ValueResult {
872        const LABEL: &str = "PythonExecutable.to_wix_bundle_builder()";
873
874        optional_type_arg("msi_builder_callback", "function", &msi_builder_callback)?;
875
876        let msi_builder_value = self.to_wix_msi_builder(
877            type_values,
878            call_stack,
879            id_prefix.clone(),
880            product_name.clone(),
881            product_version.clone(),
882            product_manufacturer.clone(),
883        )?;
884
885        if msi_builder_callback.get_type() == "function" {
886            msi_builder_callback.call(
887                call_stack,
888                type_values,
889                vec![msi_builder_value.clone()],
890                LinkedHashMap::new(),
891                None,
892                None,
893            )?;
894        }
895
896        let msi_builder = msi_builder_value
897            .downcast_ref::<WiXMsiBuilderValue>()
898            .unwrap();
899
900        let arch = target_triple_to_wix_arch(self.inner(LABEL)?.target_triple()).unwrap_or("x64");
901
902        let bundle_builder_value = WiXBundleBuilderValue::new_from_args(
903            id_prefix,
904            product_name,
905            product_version,
906            product_manufacturer,
907            arch.to_string(),
908        )?;
909        let mut bundle_builder = bundle_builder_value
910            .downcast_mut::<WiXBundleBuilderValue>()
911            .unwrap()
912            .unwrap();
913
914        // Add the VC++ Redistributable for the target platform.
915        match self.inner(LABEL)?.target_triple() {
916            "i686-pc-windows-msvc" => {
917                bundle_builder.add_vc_redistributable(type_values, "x86".to_string())?;
918            }
919            "x86_64-pc-windows-msvc" => {
920                bundle_builder.add_vc_redistributable(type_values, "x64".to_string())?;
921            }
922            _ => {}
923        }
924
925        bundle_builder.add_wix_msi_builder(
926            msi_builder.deref().clone(),
927            false,
928            Value::new(NoneType::None),
929        )?;
930
931        Ok(bundle_builder_value.clone())
932    }
933
934    /// PythonExecutable.to_wix_msi_builder(id_prefix, product_name, product_version, product_manufacturer)
935    pub fn to_wix_msi_builder(
936        &self,
937        type_values: &TypeValues,
938        call_stack: &mut CallStack,
939        id_prefix: String,
940        product_name: String,
941        product_version: String,
942        product_manufacturer: String,
943    ) -> ValueResult {
944        const LABEL: &str = "PythonExecutable.to_wix_msi_builder()";
945
946        let manifest_value = self.to_file_manifest(type_values, ".".to_string())?;
947        let manifest = manifest_value.downcast_ref::<FileManifestValue>().unwrap();
948
949        let arch = target_triple_to_wix_arch(self.inner(LABEL)?.target_triple()).unwrap_or("x64");
950
951        let builder_value = WiXMsiBuilderValue::new_from_args(
952            id_prefix,
953            product_name,
954            product_version,
955            product_manufacturer,
956            arch.to_string(),
957        )?;
958        let mut builder = builder_value
959            .downcast_mut::<WiXMsiBuilderValue>()
960            .unwrap()
961            .unwrap();
962
963        builder.add_program_files_manifest(type_values, call_stack, manifest.deref().clone())?;
964
965        Ok(builder_value.clone())
966    }
967
968    /// PythonExecutable.filter_resources_from_files(files=None, glob_files=None)
969    pub fn filter_resources_from_files(
970        &mut self,
971        files: &Value,
972        glob_files: &Value,
973    ) -> ValueResult {
974        const LABEL: &str = "PythonExecutable.filter_resources_from_files()";
975
976        optional_list_arg("files", "string", files)?;
977        optional_list_arg("glob_files", "string", glob_files)?;
978
979        let files = match files.get_type() {
980            "list" => files
981                .iter()?
982                .iter()
983                .map(|x| PathBuf::from(x.to_string()))
984                .collect(),
985            "NoneType" => Vec::new(),
986            _ => panic!("type should have been validated above"),
987        };
988
989        let glob_files = match glob_files.get_type() {
990            "list" => glob_files.iter()?.iter().map(|x| x.to_string()).collect(),
991            "NoneType" => Vec::new(),
992            _ => panic!("type should have been validated above"),
993        };
994
995        let files_refs = files.iter().map(|x| x.as_ref()).collect::<Vec<&Path>>();
996        let glob_files_refs = glob_files.iter().map(|x| x.as_ref()).collect::<Vec<&str>>();
997
998        let mut exe = self.inner(LABEL)?;
999
1000        error_context(LABEL, || {
1001            exe.filter_resources_from_files(&files_refs, &glob_files_refs)
1002        })?;
1003
1004        Ok(Value::new(NoneType::None))
1005    }
1006}
1007
1008starlark_module! { python_executable_env =>
1009    PythonExecutable.build(env env, call_stack cs, this, target: String) {
1010        let this = this.downcast_ref::<PythonExecutableValue>().unwrap();
1011        this.build(env, cs, target)
1012    }
1013
1014    PythonExecutable.make_python_module_source(
1015        env env,
1016        call_stack cs,
1017        this,
1018        name: String,
1019        source: String,
1020        is_package: bool = false
1021    ) {
1022        let this = this.downcast_ref::<PythonExecutableValue>().unwrap();
1023        this.make_python_module_source(env, cs, name, source, is_package)
1024    }
1025
1026    PythonExecutable.pip_download(
1027        env env,
1028        call_stack cs,
1029        this,
1030        args
1031    ) {
1032        let mut this = this.downcast_mut::<PythonExecutableValue>().unwrap().unwrap();
1033        this.pip_download(env, cs, &args)
1034    }
1035
1036    PythonExecutable.pip_install(
1037        env env,
1038        call_stack cs,
1039        this,
1040        args,
1041        extra_envs=NoneType::None
1042    ) {
1043        let mut this = this.downcast_mut::<PythonExecutableValue>().unwrap().unwrap();
1044        this.pip_install(env, cs, &args, &extra_envs)
1045    }
1046
1047    PythonExecutable.read_package_root(
1048        env env,
1049        call_stack cs,
1050        this,
1051        path: String,
1052        packages
1053    ) {
1054        let mut this = this.downcast_mut::<PythonExecutableValue>().unwrap().unwrap();
1055        this.read_package_root(env, cs, path, &packages)
1056    }
1057
1058    PythonExecutable.read_virtualenv(
1059        env env,
1060        call_stack cs,
1061        this,
1062        path: String
1063    ) {
1064        let mut this = this.downcast_mut::<PythonExecutableValue>().unwrap().unwrap();
1065        this.read_virtualenv(env, cs, path)
1066    }
1067
1068    PythonExecutable.setup_py_install(
1069        env env,
1070        call_stack cs,
1071        this,
1072        package_path: String,
1073        extra_envs=NoneType::None,
1074        extra_global_arguments=NoneType::None
1075    ) {
1076        let mut this = this.downcast_mut::<PythonExecutableValue>().unwrap().unwrap();
1077        this.setup_py_install(env, cs, package_path, &extra_envs, &extra_global_arguments)
1078    }
1079
1080    PythonExecutable.add_python_resource(
1081        this,
1082        resource
1083    ) {
1084        let mut this = this.downcast_mut::<PythonExecutableValue>().unwrap().unwrap();
1085        this.add_python_resource(
1086            &resource,
1087            "add_python_resource",
1088        )
1089    }
1090
1091    PythonExecutable.add_python_resources(
1092        this,
1093        resources
1094    ) {
1095        let mut this = this.downcast_mut::<PythonExecutableValue>().unwrap().unwrap();
1096        this.add_python_resources(
1097            &resources,
1098        )
1099    }
1100
1101    PythonExecutable.add_cargo_manifest_licensing(
1102        env env,
1103        this,
1104        manifest_path: String,
1105        all_features: bool = false,
1106        features=NoneType::None
1107    ) {
1108        let mut this = this.downcast_mut::<PythonExecutableValue>().unwrap().unwrap();
1109        this.add_cargo_manifest_licensing(env, &manifest_path, all_features, &features)
1110    }
1111
1112    PythonExecutable.filter_resources_from_files(
1113        this,
1114        files=NoneType::None,
1115        glob_files=NoneType::None)
1116    {
1117        let mut this = this.downcast_mut::<PythonExecutableValue>().unwrap().unwrap();
1118        this.filter_resources_from_files(&files, &glob_files)
1119    }
1120
1121    PythonExecutable.to_embedded_resources(this) {
1122        let this = this.downcast_ref::<PythonExecutableValue>().unwrap();
1123        this.to_embedded_resources()
1124    }
1125
1126    PythonExecutable.to_file_manifest(env env, this, prefix: String) {
1127        let this = this.downcast_ref::<PythonExecutableValue>().unwrap();
1128        this.to_file_manifest(env, prefix)
1129    }
1130
1131    PythonExecutable.to_wix_bundle_builder(
1132        env env,
1133        call_stack cs,
1134        this,
1135        id_prefix: String,
1136        product_name: String,
1137        product_version: String,
1138        product_manufacturer: String,
1139        msi_builder_callback = NoneType::None
1140    ) {
1141        let this = this.downcast_ref::<PythonExecutableValue>().unwrap();
1142        this.to_wix_bundle_builder(
1143            env,
1144            cs,
1145            id_prefix,
1146            product_name,
1147            product_version,
1148            product_manufacturer,
1149            msi_builder_callback
1150        )
1151    }
1152
1153    PythonExecutable.to_wix_msi_builder(
1154        env env,
1155        call_stack cs,
1156        this,
1157        id_prefix: String,
1158        product_name: String,
1159        product_version: String,
1160        product_manufacturer: String
1161    ) {
1162        let this = this.downcast_ref::<PythonExecutableValue>().unwrap();
1163        this.to_wix_msi_builder(env, cs, id_prefix, product_name, product_version, product_manufacturer)
1164    }
1165}
1166
1167#[cfg(test)]
1168mod tests {
1169    use {
1170        super::super::testutil::*,
1171        super::*,
1172        crate::{python_distributions::PYTHON_DISTRIBUTIONS, testutil::*},
1173    };
1174
1175    #[test]
1176    fn test_default_values() -> Result<()> {
1177        let mut env = test_evaluation_context_builder()?.into_context()?;
1178        add_exe(&mut env)?;
1179        let exe = env.eval("exe")?;
1180
1181        assert_eq!(exe.get_type(), "PythonExecutable");
1182
1183        let exe = exe.downcast_ref::<PythonExecutableValue>().unwrap();
1184        let inner = exe.inner("ignored").unwrap();
1185        assert!(inner
1186            .iter_resources()
1187            .any(|(_, r)| r.in_memory_source.is_some()));
1188        assert!(inner
1189            .iter_resources()
1190            .all(|(_, r)| r.in_memory_resources.is_none()));
1191
1192        Ok(())
1193    }
1194
1195    #[test]
1196    fn test_no_sources() -> Result<()> {
1197        let mut env = test_evaluation_context_builder()?.into_context()?;
1198
1199        env.eval("dist = default_python_distribution()")?;
1200        env.eval("policy = dist.make_python_packaging_policy()")?;
1201        env.eval("policy.include_distribution_sources = False")?;
1202
1203        let exe = env.eval("dist.to_python_executable('testapp', packaging_policy=policy)")?;
1204
1205        assert_eq!(exe.get_type(), "PythonExecutable");
1206
1207        let exe = exe.downcast_ref::<PythonExecutableValue>().unwrap();
1208        let inner = exe.inner("ignored").unwrap();
1209        assert!(inner
1210            .iter_resources()
1211            .all(|(_, r)| r.in_memory_source.is_none()));
1212
1213        Ok(())
1214    }
1215
1216    #[test]
1217    fn test_make_python_module_source() -> Result<()> {
1218        let mut env = test_evaluation_context_builder()?.into_context()?;
1219        add_exe(&mut env)?;
1220        let m = env.eval("exe.make_python_module_source('foo', 'import bar')")?;
1221
1222        assert_eq!(m.get_type(), PythonModuleSourceValue::TYPE);
1223        assert_eq!(m.get_attr("name").unwrap().to_str(), "foo");
1224        assert_eq!(m.get_attr("source").unwrap().to_str(), "import bar");
1225        assert!(!m.get_attr("is_package").unwrap().to_bool());
1226
1227        Ok(())
1228    }
1229
1230    #[test]
1231    fn test_make_python_module_source_callback() -> Result<()> {
1232        let mut env = test_evaluation_context_builder()?.into_context()?;
1233        env.eval("dist = default_python_distribution()")?;
1234        env.eval("policy = dist.make_python_packaging_policy()")?;
1235        env.eval(
1236            "def my_func(policy, resource):\n    resource.add_source = True\n    resource.add_bytecode_optimization_level_two = True\n",
1237        )?;
1238        env.eval("policy.register_resource_callback(my_func)")?;
1239        env.eval("exe = dist.to_python_executable('testapp', packaging_policy = policy)")?;
1240
1241        let m = env.eval("exe.make_python_module_source('foo', 'import bar')")?;
1242
1243        assert_eq!(m.get_type(), PythonModuleSourceValue::TYPE);
1244        assert_eq!(m.get_attr("name").unwrap().to_str(), "foo");
1245        assert_eq!(m.get_attr("source").unwrap().to_str(), "import bar");
1246        assert!(!m.get_attr("is_package").unwrap().to_bool());
1247        assert!(m.get_attr("add_source").unwrap().to_bool());
1248        assert!(m
1249            .get_attr("add_bytecode_optimization_level_two")
1250            .unwrap()
1251            .to_bool(),);
1252
1253        Ok(())
1254    }
1255
1256    #[test]
1257    fn test_pip_download_pyflakes() -> Result<()> {
1258        for target_triple in PYTHON_DISTRIBUTIONS.all_target_triples() {
1259            let mut env = test_evaluation_context_builder()?
1260                .build_target_triple(target_triple)
1261                .into_context()?;
1262
1263            env.eval("dist = default_python_distribution()")?;
1264            env.eval("exe = dist.to_python_executable('testapp')")?;
1265
1266            let resources = env.eval("exe.pip_download(['pyflakes==2.2.0'])")?;
1267
1268            assert_eq!(resources.get_type(), "list");
1269
1270            let raw_it = resources.iter().unwrap();
1271            let mut it = raw_it.iter();
1272
1273            let v = it.next().unwrap();
1274            assert_eq!(v.get_type(), PythonModuleSourceValue::TYPE);
1275            let x = v.downcast_ref::<PythonModuleSourceValue>().unwrap();
1276            let inner = x.inner("ignored").unwrap();
1277            assert!(inner.m.package().starts_with("pyflakes"));
1278        }
1279
1280        Ok(())
1281    }
1282
1283    #[test]
1284    fn test_pip_install_simple() -> Result<()> {
1285        let mut env = test_evaluation_context_builder()?.into_context()?;
1286
1287        env.eval("dist = default_python_distribution()")?;
1288        env.eval("policy = dist.make_python_packaging_policy()")?;
1289        env.eval("policy.include_distribution_sources = False")?;
1290        env.eval("exe = dist.to_python_executable('testapp', packaging_policy = policy)")?;
1291
1292        let resources = env.eval("exe.pip_install(['pyflakes==2.1.1'])")?;
1293        assert_eq!(resources.get_type(), "list");
1294
1295        let raw_it = resources.iter().unwrap();
1296        let mut it = raw_it.iter();
1297
1298        let v = it.next().unwrap();
1299        assert_eq!(v.get_type(), PythonModuleSourceValue::TYPE);
1300        let x = v.downcast_ref::<PythonModuleSourceValue>().unwrap();
1301        let inner = x.inner("ignored").unwrap();
1302        assert_eq!(inner.m.name, "pyflakes");
1303        assert!(inner.m.is_package);
1304
1305        Ok(())
1306    }
1307
1308    #[test]
1309    fn test_read_package_root_simple() -> Result<()> {
1310        let temp_dir = get_env()?.temporary_directory("pyoxidizer-test")?;
1311
1312        let root = temp_dir.path();
1313        std::fs::create_dir(root.join("bar"))?;
1314        let bar_init = root.join("bar").join("__init__.py");
1315        std::fs::write(&bar_init, "# bar")?;
1316
1317        let foo_path = root.join("foo.py");
1318        std::fs::write(&foo_path, "# foo")?;
1319
1320        let baz_path = root.join("baz.py");
1321        std::fs::write(&baz_path, "# baz")?;
1322
1323        std::fs::create_dir(root.join("extra"))?;
1324        let extra_path = root.join("extra").join("__init__.py");
1325        std::fs::write(&extra_path, "# extra")?;
1326
1327        let mut env = test_evaluation_context_builder()?.into_context()?;
1328        env.eval("dist = default_python_distribution()")?;
1329        env.eval("policy = dist.make_python_packaging_policy()")?;
1330        env.eval("policy.include_distribution_sources = False")?;
1331        env.eval("exe = dist.to_python_executable('testapp', packaging_policy = policy)")?;
1332
1333        let resources = env.eval(&format!(
1334            "exe.read_package_root(\"{}\", packages=['foo', 'bar'])",
1335            root.display().to_string().replace('\\', "/")
1336        ))?;
1337
1338        assert_eq!(resources.get_type(), "list");
1339        assert_eq!(resources.length().unwrap(), 2);
1340
1341        let raw_it = resources.iter().unwrap();
1342        let mut it = raw_it.iter();
1343
1344        let v = it.next().unwrap();
1345        assert_eq!(v.get_type(), PythonModuleSourceValue::TYPE);
1346        let x = v.downcast_ref::<PythonModuleSourceValue>().unwrap();
1347        let inner = x.inner("ignored").unwrap();
1348        assert_eq!(inner.m.name, "bar");
1349        assert!(inner.m.is_package);
1350        assert_eq!(inner.m.source.resolve_content().unwrap(), b"# bar");
1351        drop(inner);
1352
1353        let v = it.next().unwrap();
1354        assert_eq!(v.get_type(), PythonModuleSourceValue::TYPE);
1355        let x = v.downcast_ref::<PythonModuleSourceValue>().unwrap();
1356        let inner = x.inner("ignored").unwrap();
1357        assert_eq!(inner.m.name, "foo");
1358        assert!(!inner.m.is_package);
1359        assert_eq!(inner.m.source.resolve_content().unwrap(), b"# foo");
1360        drop(inner);
1361
1362        temp_dir.close()?;
1363
1364        Ok(())
1365    }
1366
1367    #[test]
1368    fn licenses_filename() -> Result<()> {
1369        let mut env = test_evaluation_context_builder()?.into_context()?;
1370        add_exe(&mut env)?;
1371
1372        let v = env.eval("exe.licenses_filename")?;
1373        assert_eq!(v.get_type(), "string");
1374        assert_eq!(v.to_string(), "COPYING.txt");
1375
1376        env.eval("exe.licenses_filename = 'licenses'")?;
1377        let v = env.eval("exe.licenses_filename")?;
1378        assert_eq!(v.get_type(), "string");
1379        assert_eq!(v.to_string(), "licenses");
1380
1381        env.eval("exe.licenses_filename = None")?;
1382        let v = env.eval("exe.licenses_filename")?;
1383        assert_eq!(v.get_type(), "NoneType");
1384
1385        Ok(())
1386    }
1387
1388    #[test]
1389    fn test_windows_runtime_dlls_mode() -> Result<()> {
1390        let mut env = test_evaluation_context_builder()?.into_context()?;
1391        add_exe(&mut env)?;
1392
1393        let value = env.eval("exe.windows_runtime_dlls_mode")?;
1394        assert_eq!(value.get_type(), "string");
1395        assert_eq!(value.to_string(), "when-present");
1396
1397        let value =
1398            env.eval("exe.windows_runtime_dlls_mode = 'never'; exe.windows_runtime_dlls_mode")?;
1399        assert_eq!(value.to_string(), "never");
1400
1401        let value =
1402            env.eval("exe.windows_runtime_dlls_mode = 'always'; exe.windows_runtime_dlls_mode")?;
1403        assert_eq!(value.to_string(), "always");
1404
1405        assert!(env.eval("exe.windows_runtime_dlls_mode = 'bad'").is_err());
1406
1407        let value = env.eval(
1408            "exe.windows_runtime_dlls_mode = 'when-present'; exe.windows_runtime_dlls_mode",
1409        )?;
1410        assert_eq!(value.to_string(), "when-present");
1411
1412        Ok(())
1413    }
1414
1415    #[test]
1416    fn test_packed_resources_load_mode() -> Result<()> {
1417        let mut env = test_evaluation_context_builder()?.into_context()?;
1418        add_exe(&mut env)?;
1419
1420        let value = env.eval("exe.packed_resources_load_mode")?;
1421        assert_eq!(value.get_type(), "string");
1422        assert_eq!(value.to_string(), "embedded:packed-resources");
1423
1424        let value =
1425            env.eval("exe.packed_resources_load_mode = 'none'; exe.packed_resources_load_mode")?;
1426        assert_eq!(value.get_type(), "string");
1427        assert_eq!(value.to_string(), "none");
1428
1429        Ok(())
1430    }
1431
1432    #[test]
1433    fn test_windows_subsystem() -> Result<()> {
1434        let mut env = test_evaluation_context_builder()?.into_context()?;
1435        add_exe(&mut env)?;
1436
1437        let value = env.eval("exe.windows_subsystem")?;
1438        assert_eq!(value.get_type(), "string");
1439        assert_eq!(value.to_string(), "console");
1440
1441        let value = env.eval("exe.windows_subsystem = 'windows'; exe.windows_subsystem")?;
1442        assert_eq!(value.get_type(), "string");
1443        assert_eq!(value.to_string(), "windows");
1444
1445        Ok(())
1446    }
1447
1448    #[test]
1449    fn test_tcl_files_path() -> Result<()> {
1450        let mut env = test_evaluation_context_builder()?.into_context()?;
1451        add_exe(&mut env)?;
1452
1453        let value = env.eval("exe.tcl_files_path")?;
1454        assert_eq!(value.get_type(), "NoneType");
1455
1456        let value = env.eval("exe.tcl_files_path = 'lib'; exe.tcl_files_path")?;
1457        assert_eq!(value.get_type(), "string");
1458        assert_eq!(value.to_string(), "lib");
1459
1460        let value = env.eval("exe.tcl_files_path = None; exe.tcl_files_path")?;
1461        assert_eq!(value.get_type(), "NoneType");
1462
1463        Ok(())
1464    }
1465
1466    #[test]
1467    fn test_to_wix_bundle_builder_callback() -> Result<()> {
1468        let mut env = test_evaluation_context_builder()?.into_context()?;
1469        add_exe(&mut env)?;
1470        env.eval("def modify(msi):\n msi.package_description = 'description'\n")?;
1471        let builder_value = env.eval("exe.to_wix_bundle_builder('id_prefix', 'product_name', '0.1', 'manufacturer', msi_builder_callback = modify)")?;
1472        let builder = builder_value
1473            .downcast_ref::<WiXBundleBuilderValue>()
1474            .unwrap();
1475
1476        assert_eq!(builder.build_msis.len(), 1);
1477        let mut writer = xml::EventWriter::new(vec![]);
1478        builder.build_msis[0]
1479            .inner("ignored")
1480            .unwrap()
1481            .builder
1482            .write_xml(&mut writer)?;
1483
1484        let xml = String::from_utf8(writer.into_inner())?;
1485        assert!(xml.find("Description=\"description\"").is_some());
1486
1487        Ok(())
1488    }
1489
1490    #[cfg(windows)]
1491    #[test]
1492    fn test_to_wix_bundle_builder() -> Result<()> {
1493        let mut env = test_evaluation_context_builder()?.into_context()?;
1494        add_exe(&mut env)?;
1495        env.eval("bundle = exe.to_wix_bundle_builder('id_prefix', 'product_name', '0.1', 'product_manufacturer')")?;
1496        env.eval("bundle.build('test_to_wix_bundle_builder')")?;
1497
1498        let exe_path = env
1499            .target_build_path("test_to_wix_bundle_builder")
1500            .unwrap()
1501            .join("product_name-0.1.exe");
1502
1503        assert!(exe_path.exists());
1504
1505        Ok(())
1506    }
1507
1508    #[cfg(windows)]
1509    #[test]
1510    fn test_to_wix_msi_builder() -> Result<()> {
1511        let mut env = test_evaluation_context_builder()?.into_context()?;
1512        add_exe(&mut env)?;
1513        env.eval("msi = exe.to_wix_msi_builder('id_prefix', 'product_name', '0.1', 'product_manufacturer')")?;
1514        env.eval("msi.build('test_to_wix_msi_builder')")?;
1515
1516        let msi_path = env
1517            .target_build_path("test_to_wix_msi_builder")
1518            .unwrap()
1519            .join("product_name-0.1.msi");
1520
1521        assert!(msi_path.exists());
1522
1523        Ok(())
1524    }
1525}