pyembed/
config.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
5//! Data structures for configuring a Python interpreter.
6
7use {
8    crate::NewInterpreterError,
9    oxidized_importer::{PackedResourcesSource, PythonResourcesState},
10    pyo3::ffi as pyffi,
11    python_packaging::interpreter::{
12        MemoryAllocatorBackend, MultiprocessingStartMethod, PythonInterpreterConfig,
13        PythonInterpreterProfile, TerminfoResolution,
14    },
15    std::{
16        ffi::{CString, OsString},
17        ops::Deref,
18        path::PathBuf,
19    },
20};
21
22#[cfg(feature = "serialization")]
23use serde::{Deserialize, Serialize};
24
25/// Defines a Python extension module and its initialization function.
26///
27/// Essentially represents a module name and pointer to its initialization
28/// function.
29#[derive(Clone, Debug)]
30pub struct ExtensionModule {
31    /// Name of the extension module.
32    pub name: CString,
33
34    /// Extension module initialization function.
35    pub init_func: unsafe extern "C" fn() -> *mut pyffi::PyObject,
36}
37
38/// Configuration for a Python interpreter.
39///
40/// This type is used to create a [crate::MainPythonInterpreter], which manages
41/// a Python interpreter running in the current process.
42///
43/// This type wraps a [PythonInterpreterConfig], which is an abstraction over
44/// the low-level C structs (`PyPreConfig` and `PyConfig`) used as part of
45/// Python's C initialization API. In addition to this data structure, the
46/// fields on this type facilitate control of additional features provided by
47/// this crate.
48///
49/// The [PythonInterpreterConfig] has a single non-optional field:
50/// [PythonInterpreterConfig::profile]. This defines the defaults for various
51/// fields of the `PyPreConfig` and `PyConfig` C structs. See
52/// <https://docs.python.org/3/c-api/init_config.html#isolated-configuration> for
53/// more.
54///
55/// When this type is converted to `PyPreConfig` and `PyConfig`, instances
56/// of these C structs are created from the specified profile. e.g. by calling
57/// `PyPreConfig_InitPythonConfig()`, `PyPreConfig_InitIsolatedConfig`,
58/// `PyConfig_InitPythonConfig`, and `PyConfig_InitIsolatedConfig`. Then
59/// for each field in `PyPreConfig` and `PyConfig`, if a corresponding field
60/// on [PythonInterpreterConfig] is [Some], then the `PyPreConfig` or
61/// `PyConfig` field will be updated accordingly.
62///
63/// During interpreter initialization, [Self::resolve()] is called to
64/// resolve/finalize any missing values and convert the instance into a
65/// [ResolvedOxidizedPythonInterpreterConfig]. It is this type that is
66/// used to produce a `PyPreConfig` and `PyConfig`, which are used to
67/// initialize the Python interpreter.
68///
69/// Some fields on this type are redundant or conflict with those on
70/// [PythonInterpreterConfig]. Read the documentation of each field to
71/// understand how they interact. Since [PythonInterpreterConfig] is defined
72/// in a different crate, its docs are not aware of the existence of
73/// this crate/type.
74///
75/// This struct implements `Deserialize` and `Serialize` and therefore can be
76/// serialized to any format supported by the `serde` crate. This feature is
77/// used by `pyoxy` to allow YAML-based configuration of Python interpreters.
78#[derive(Clone, Debug)]
79#[cfg_attr(feature = "serialization", derive(Deserialize, Serialize))]
80#[cfg_attr(feature = "serialization", serde(default))]
81pub struct OxidizedPythonInterpreterConfig<'a> {
82    /// The path of the currently executing executable.
83    ///
84    /// This value will always be [Some] on [ResolvedOxidizedPythonInterpreterConfig]
85    /// instances.
86    ///
87    /// Default value: [None].
88    ///
89    /// [Self::resolve()] behavior: sets to [std::env::current_exe()] if not set.
90    /// Will canonicalize the final path, which may entail filesystem I/O.
91    pub exe: Option<PathBuf>,
92
93    /// The filesystem path from which relative paths will be interpreted.
94    ///
95    /// This value will always be [Some] on [ResolvedOxidizedPythonInterpreterConfig]
96    /// instances.
97    ///
98    /// Default value: [None].
99    ///
100    /// [Self::resolve()] behavior: sets to [Self::exe.parent()] if not set.
101    pub origin: Option<PathBuf>,
102
103    /// Low-level configuration of Python interpreter.
104    ///
105    /// Default value: [PythonInterpreterConfig::default()] with
106    /// [PythonInterpreterConfig::profile] always set to [PythonInterpreterProfile::Python].
107    ///
108    /// [Self::resolve()] behavior: most fields are copied verbatim.
109    /// [PythonInterpreterConfig::module_search_paths] entries have the special token
110    /// `$ORIGIN` expanded to the resolved value of [Self::origin].
111    pub interpreter_config: PythonInterpreterConfig,
112
113    /// Memory allocator backend to use.
114    ///
115    /// Default value: [MemoryAllocatorBackend::Default].
116    ///
117    /// Interpreter initialization behavior: after `Py_PreInitialize()` is called,
118    /// [crate::pyalloc::PythonMemoryAllocator::from_backend()] is called. If this
119    /// resolves to a [crate::pyalloc::PythonMemoryAllocator], that allocator will
120    /// be installed as per [Self::allocator_raw], [Self::allocator_mem],
121    /// [Self::allocator_obj], and [Self::allocator_pymalloc_arena]. If a custom
122    /// allocator backend is defined but all the `allocator_*` flags are [false],
123    /// the allocator won't be used.
124    pub allocator_backend: MemoryAllocatorBackend,
125
126    /// Whether to install the custom allocator for the `raw` memory domain.
127    ///
128    /// See <https://docs.python.org/3/c-api/memory.html> for documentation on how Python
129    /// memory allocator domains work.
130    ///
131    /// Default value: [true]
132    ///
133    /// Interpreter initialization behavior: controls whether [Self::allocator_backend]
134    /// is used for the `raw` memory domain.
135    ///
136    /// Has no effect if [Self::allocator_backend] is [MemoryAllocatorBackend::Default].
137    pub allocator_raw: bool,
138
139    /// Whether to install the custom allocator for the `mem` memory domain.
140    ///
141    /// See <https://docs.python.org/3/c-api/memory.html> for documentation on how Python
142    /// memory allocator domains work.
143    ///
144    /// Default value: [false]
145    ///
146    /// Interpreter initialization behavior: controls whether [Self::allocator_backend]
147    /// is used for the `mem` memory domain.
148    ///
149    /// Has no effect if [Self::allocator_backend] is [MemoryAllocatorBackend::Default].
150    pub allocator_mem: bool,
151
152    /// Whether to install the custom allocator for the `obj` memory domain.
153    ///
154    /// See <https://docs.python.org/3/c-api/memory.html> for documentation on how Python
155    /// memory allocator domains work.
156    ///
157    /// Default value: [false]
158    ///
159    /// Interpreter initialization behavior: controls whether [Self::allocator_backend]
160    /// is used for the `obj` memory domain.
161    ///
162    /// Has no effect if [Self::allocator_backend] is [MemoryAllocatorBackend::Default].
163    pub allocator_obj: bool,
164
165    /// Whether to install the custom allocator for the `pymalloc` arena allocator.
166    ///
167    /// See <https://docs.python.org/3/c-api/memory.html> for documentation on how Python
168    /// memory allocation works.
169    ///
170    /// Default value: [false]
171    ///
172    /// Interpreter initialization behavior: controls whether [Self::allocator_backend]
173    /// is used for the `pymalloc` arena allocator.
174    ///
175    /// This setting requires the `pymalloc` allocator to be used for the `mem`
176    /// or `obj` domains (`allocator_mem = false` and `allocator_obj = false` - this is
177    /// the default behavior) and for [Self::allocator_backend] to not be
178    /// [MemoryAllocatorBackend::Default].
179    pub allocator_pymalloc_arena: bool,
180
181    /// Whether to set up Python allocator debug hooks to detect memory bugs.
182    ///
183    /// Default value: [false]
184    ///
185    /// Interpreter initialization behavior: triggers the calling of
186    /// `PyMem_SetupDebugHooks()` after custom allocators are installed.
187    ///
188    /// This setting can be used with or without custom memory allocators
189    /// (see other `allocator_*` fields).
190    pub allocator_debug: bool,
191
192    /// Whether to automatically set missing "path configuration" fields.
193    ///
194    /// If `true`, various path configuration
195    /// (<https://docs.python.org/3/c-api/init_config.html#path-configuration>) fields
196    /// will be set automatically if their corresponding `.interpreter_config`
197    /// fields are `None`. For example, `program_name` will be set to the current
198    /// executable and `home` will be set to the executable's directory.
199    ///
200    /// If this is `false`, the default path configuration built into libpython
201    /// is used.
202    ///
203    /// Setting this to `false` likely enables isolated interpreters to be used
204    /// with "external" Python installs. If this is `true`, the default isolated
205    /// configuration expects files like the Python standard library to be installed
206    /// relative to the current executable. You will need to either ensure these
207    /// files are present, define `packed_resources`, and/or set
208    /// `.interpreter_config.module_search_paths` to ensure the interpreter can find
209    /// the Python standard library, otherwise the interpreter will fail to start.
210    ///
211    /// Without this set or corresponding `.interpreter_config` fields set, you
212    /// may also get run-time errors like
213    /// `Could not find platform independent libraries <prefix>` or
214    /// `Consider setting $PYTHONHOME to <prefix>[:<exec_prefix>]`. If you see
215    /// these errors, it means the automatic path config resolutions built into
216    /// libpython didn't work because the run-time layout didn't match the
217    /// build-time configuration.
218    ///
219    /// Default value: [true]
220    pub set_missing_path_configuration: bool,
221
222    /// Whether to install `oxidized_importer` during interpreter initialization.
223    ///
224    /// If [true], `oxidized_importer` will be imported during interpreter
225    /// initialization and an instance of `oxidized_importer.OxidizedFinder`
226    /// will be installed on `sys.meta_path` as the first element.
227    ///
228    /// If [Self::packed_resources] are defined, they will be loaded into the
229    /// `OxidizedFinder`.
230    ///
231    /// If [Self::filesystem_importer] is [true], its *path hook* will be
232    /// registered on [`sys.path_hooks`] so `PathFinder` (the standard filesystem
233    /// based importer) and [`pkgutil`] can use it.
234    ///
235    /// Default value: [false]
236    ///
237    /// Interpreter initialization behavior: See above.
238    ///
239    /// [`sys.path_hooks`]: https://docs.python.org/3/library/sys.html#sys.path_hooks
240    /// [`pkgutil`]: https://docs.python.org/3/library/pkgutil.html
241    pub oxidized_importer: bool,
242
243    /// Whether to install the path-based finder.
244    ///
245    /// Controls whether to install the Python standard library `PathFinder` meta
246    /// path finder (this is the meta path finder that loads Python modules and
247    /// resources from the filesystem).
248    ///
249    /// Also controls whether to add `OxidizedFinder`'s path hook to
250    /// [`sys.path_hooks`].
251    ///
252    /// Due to lack of control over low-level Python interpreter initialization,
253    /// the standard library `PathFinder` will be registered on `sys.meta_path`
254    /// and `sys.path_hooks` for a brief moment when the interpreter is initialized.
255    /// If `sys.path` contains valid entries that would be serviced by this finder
256    /// and `oxidized_importer` isn't able to service imports, it is possible for the
257    /// path-based finder to be used to import some Python modules needed to initialize
258    /// the Python interpreter. In many cases, this behavior is harmless. In all cases,
259    /// the path-based importer is removed after Python interpreter initialization, so
260    /// future imports won't be serviced by this path-based importer if it is disabled
261    /// by this flag.
262    ///
263    /// Default value: [true]
264    ///
265    /// Interpreter initialization behavior: If false, path-based finders are removed
266    /// from `sys.meta_path` and `sys.path_hooks` is cleared.
267    ///
268    /// [`sys.path_hooks`]: https://docs.python.org/3/library/sys.html#sys.path_hooks
269    pub filesystem_importer: bool,
270
271    /// References to packed resources data.
272    ///
273    /// The format of the data is defined by the ``python-packed-resources``
274    /// crate. The data will be parsed as part of initializing the custom
275    /// meta path importer during interpreter initialization when
276    /// `oxidized_importer=true`. If `oxidized_importer=false`, this field
277    /// is ignored.
278    ///
279    /// If paths are relative, that will be evaluated relative to the process's
280    /// current working directory following the operating system's standard
281    /// path expansion behavior.
282    ///
283    /// Default value: `vec![]`
284    ///
285    /// [Self::resolve()] behavior: [PackedResourcesSource::MemoryMappedPath] members
286    /// have the special string `$ORIGIN` expanded to the string value that
287    /// [Self::origin] resolves to.
288    ///
289    /// This field is ignored during serialization.
290    #[cfg_attr(feature = "serialization", serde(skip))]
291    pub packed_resources: Vec<PackedResourcesSource<'a>>,
292
293    /// Extra extension modules to make available to the interpreter.
294    ///
295    /// The values will effectively be passed to ``PyImport_ExtendInitTab()``.
296    ///
297    /// Default value: [None]
298    ///
299    /// Interpreter initialization behavior: `PyImport_Inittab` will be extended
300    /// with entries from this list. This makes the extensions available as
301    /// built-in extension modules.
302    ///
303    /// This field is ignored during serialization.
304    #[cfg_attr(feature = "serialization", serde(skip))]
305    pub extra_extension_modules: Option<Vec<ExtensionModule>>,
306
307    /// Command line arguments to initialize `sys.argv` with.
308    ///
309    /// Default value: [None]
310    ///
311    /// [Self::resolve()] behavior: [Some] value is used if set. Otherwise
312    /// [PythonInterpreterConfig::argv] is used if set. Otherwise
313    /// [std::env::args_os()] is called.
314    ///
315    /// Interpreter initialization behavior: the resolved [Some] value is used
316    /// to populate `PyConfig.argv`.
317    pub argv: Option<Vec<OsString>>,
318
319    /// Whether to set `sys.argvb` with bytes versions of process arguments.
320    ///
321    /// On Windows, bytes will be UTF-16. On POSIX, bytes will be raw char*
322    /// values passed to `int main()`.
323    ///
324    /// Enabling this feature will give Python applications access to the raw
325    /// `bytes` values of raw argument data passed into the executable. The single
326    /// or double width bytes nature of the data is preserved.
327    ///
328    /// Unlike `sys.argv` which may chomp off leading argument depending on the
329    /// Python execution mode, `sys.argvb` has all the arguments used to initialize
330    /// the process. i.e. the first argument is always the executable.
331    ///
332    /// Default value: [false]
333    ///
334    /// Interpreter initialization behavior: `sys.argvb` will be set to a
335    /// `list[bytes]`. `sys.argv` and `sys.argvb` should have the same number
336    /// of elements.
337    pub argvb: bool,
338
339    /// Automatically detect and run in `multiprocessing` mode.
340    ///
341    /// If set, [crate::MainPythonInterpreter::run()] will detect when the invoked
342    /// interpreter looks like it is supposed to be a `multiprocessing` worker and
343    /// will automatically call into the `multiprocessing` module instead of running
344    /// the configured code.
345    ///
346    /// Enabling this has the same effect as calling `multiprocessing.freeze_support()`
347    /// in your application code's `__main__` and replaces the need to do so.
348    ///
349    /// Default value: [true]
350    pub multiprocessing_auto_dispatch: bool,
351
352    /// Controls how to call `multiprocessing.set_start_method()`.
353    ///
354    /// Default value: [MultiprocessingStartMethod::Auto]
355    ///
356    /// Interpreter initialization behavior: if [Self::oxidized_importer] is [true],
357    /// the `OxidizedImporter` will be taught to call `multiprocessing.set_start_method()`
358    /// when `multiprocessing` is imported. If [false], this value has no effect.
359    pub multiprocessing_start_method: MultiprocessingStartMethod,
360
361    /// Whether to set sys.frozen=True.
362    ///
363    /// Setting this will enable Python to emulate "frozen" binaries, such as
364    /// those used by PyInstaller.
365    ///
366    /// Default value: [false]
367    ///
368    /// Interpreter initialization behavior: If [true], `sys.frozen = True`.
369    /// If [false], `sys.frozen` is not defined.
370    pub sys_frozen: bool,
371
372    /// Whether to set sys._MEIPASS to the directory of the executable.
373    ///
374    /// Setting this will enable Python to emulate PyInstaller's behavior
375    /// of setting this attribute. This could potentially help with self-contained
376    /// application compatibility by masquerading as PyInstaller and causing code
377    /// to activate *PyInstaller mode*.
378    ///
379    /// Default value: [false]
380    ///
381    /// Interpreter initialization behavior: If [true], `sys._MEIPASS` will
382    /// be set to a `str` holding the value of [Self::origin]. If [false],
383    /// `sys._MEIPASS` will not be defined.
384    pub sys_meipass: bool,
385
386    /// How to resolve the `terminfo` database.
387    ///
388    /// Default value: [TerminfoResolution::Dynamic]
389    ///
390    /// Interpreter initialization behavior: the `TERMINFO_DIRS` environment
391    /// variable may be set for this process depending on what [TerminfoResolution]
392    /// instructs to do.
393    ///
394    /// `terminfo` is not used on Windows and this setting is ignored on that
395    /// platform.
396    pub terminfo_resolution: TerminfoResolution,
397
398    /// Path to use to define the `TCL_LIBRARY` environment variable.
399    ///
400    /// This directory should contain an `init.tcl` file. It is commonly
401    /// a directory named `tclX.Y`. e.g. `tcl8.6`.
402    ///
403    /// Default value: [None]
404    ///
405    /// [Self::resolve()] behavior: the token `$ORIGIN` is expanded to the
406    /// resolved value of [Self::origin].
407    ///
408    /// Interpreter initialization behavior: if set, the `TCL_LIBRARY` environment
409    /// variable will be set for the current process.
410    pub tcl_library: Option<PathBuf>,
411
412    /// Environment variable holding the directory to write a loaded modules file.
413    ///
414    /// If this value is set and the environment it refers to is set,
415    /// on interpreter shutdown, we will write a `modules-<random>` file to
416    /// the directory specified containing a `\n` delimited list of modules
417    /// loaded in `sys.modules`.
418    ///
419    /// This setting is useful to record which modules are loaded during the execution
420    /// of a Python interpreter.
421    ///
422    /// Default value: [None]
423    pub write_modules_directory_env: Option<String>,
424}
425
426impl<'a> Default for OxidizedPythonInterpreterConfig<'a> {
427    fn default() -> Self {
428        Self {
429            exe: None,
430            origin: None,
431            interpreter_config: PythonInterpreterConfig {
432                profile: PythonInterpreterProfile::Python,
433                ..PythonInterpreterConfig::default()
434            },
435            allocator_backend: MemoryAllocatorBackend::Default,
436            // We set to true by default so any installed custom backend
437            // takes effect.
438            allocator_raw: true,
439            allocator_mem: false,
440            allocator_obj: false,
441            allocator_pymalloc_arena: false,
442            allocator_debug: false,
443            set_missing_path_configuration: true,
444            oxidized_importer: false,
445            filesystem_importer: true,
446            packed_resources: vec![],
447            extra_extension_modules: None,
448            argv: None,
449            argvb: false,
450            multiprocessing_auto_dispatch: true,
451            multiprocessing_start_method: MultiprocessingStartMethod::Auto,
452            sys_frozen: false,
453            sys_meipass: false,
454            terminfo_resolution: TerminfoResolution::Dynamic,
455            tcl_library: None,
456            write_modules_directory_env: None,
457        }
458    }
459}
460
461impl<'a> OxidizedPythonInterpreterConfig<'a> {
462    /// Create a new type with all values resolved.
463    pub fn resolve(
464        self,
465    ) -> Result<ResolvedOxidizedPythonInterpreterConfig<'a>, NewInterpreterError> {
466        let argv = if let Some(args) = self.argv {
467            Some(args)
468        } else if self.interpreter_config.argv.is_some() {
469            None
470        } else {
471            Some(std::env::args_os().collect::<Vec<_>>())
472        };
473
474        let exe = if let Some(exe) = self.exe {
475            exe
476        } else {
477            std::env::current_exe()
478                .map_err(|_| NewInterpreterError::Simple("could not obtain current executable"))?
479        };
480
481        // We always canonicalize the current executable because we use path
482        // comparisons in the path hooks importer to assess whether a given sys.path
483        // entry is this executable.
484        let exe = dunce::canonicalize(exe)
485            .map_err(|_| NewInterpreterError::Simple("could not obtain current executable path"))?;
486
487        let origin = if let Some(origin) = self.origin {
488            origin
489        } else {
490            exe.parent()
491                .ok_or(NewInterpreterError::Simple(
492                    "unable to obtain current executable parent directory",
493                ))?
494                .to_path_buf()
495        };
496
497        let origin_string = origin.display().to_string();
498
499        let packed_resources = self
500            .packed_resources
501            .into_iter()
502            .map(|entry| match entry {
503                PackedResourcesSource::Memory(_) => entry,
504                PackedResourcesSource::MemoryMappedPath(p) => {
505                    PackedResourcesSource::MemoryMappedPath(PathBuf::from(
506                        p.display().to_string().replace("$ORIGIN", &origin_string),
507                    ))
508                }
509            })
510            .collect::<Vec<_>>();
511
512        let module_search_paths = self
513            .interpreter_config
514            .module_search_paths
515            .as_ref()
516            .map(|x| {
517                x.iter()
518                    .map(|p| {
519                        PathBuf::from(p.display().to_string().replace("$ORIGIN", &origin_string))
520                    })
521                    .collect::<Vec<_>>()
522            });
523
524        let tcl_library = self
525            .tcl_library
526            .as_ref()
527            .map(|x| PathBuf::from(x.display().to_string().replace("$ORIGIN", &origin_string)));
528
529        Ok(ResolvedOxidizedPythonInterpreterConfig {
530            inner: Self {
531                exe: Some(exe),
532                origin: Some(origin),
533                interpreter_config: PythonInterpreterConfig {
534                    module_search_paths,
535                    ..self.interpreter_config
536                },
537                argv,
538                packed_resources,
539                tcl_library,
540                ..self
541            },
542        })
543    }
544}
545
546/// An `OxidizedPythonInterpreterConfig` that has fields resolved.
547pub struct ResolvedOxidizedPythonInterpreterConfig<'a> {
548    inner: OxidizedPythonInterpreterConfig<'a>,
549}
550
551impl<'a> Deref for ResolvedOxidizedPythonInterpreterConfig<'a> {
552    type Target = OxidizedPythonInterpreterConfig<'a>;
553
554    fn deref(&self) -> &Self::Target {
555        &self.inner
556    }
557}
558
559impl<'a> TryFrom<OxidizedPythonInterpreterConfig<'a>>
560    for ResolvedOxidizedPythonInterpreterConfig<'a>
561{
562    type Error = NewInterpreterError;
563
564    fn try_from(value: OxidizedPythonInterpreterConfig<'a>) -> Result<Self, Self::Error> {
565        value.resolve()
566    }
567}
568
569impl<'a> ResolvedOxidizedPythonInterpreterConfig<'a> {
570    /// Obtain the value for the current executable.
571    pub fn exe(&self) -> &PathBuf {
572        self.inner.exe.as_ref().expect("exe should have a value")
573    }
574
575    /// Obtain the path for $ORIGIN.
576    pub fn origin(&self) -> &PathBuf {
577        self.inner
578            .origin
579            .as_ref()
580            .expect("origin should have a value")
581    }
582
583    /// Resolve the effective value of `sys.argv`.
584    pub fn resolve_sys_argv(&self) -> &[OsString] {
585        if let Some(args) = &self.inner.argv {
586            args
587        } else if let Some(args) = &self.inner.interpreter_config.argv {
588            args
589        } else {
590            panic!("1 of .argv or .interpreter_config.argv should be set")
591        }
592    }
593
594    /// Resolve the value to use for `sys.argvb`.
595    pub fn resolve_sys_argvb(&self) -> Vec<OsString> {
596        if let Some(args) = &self.inner.interpreter_config.argv {
597            args.clone()
598        } else if let Some(args) = &self.inner.argv {
599            args.clone()
600        } else {
601            std::env::args_os().collect::<Vec<_>>()
602        }
603    }
604}
605
606impl<'a, 'config: 'a> TryFrom<&ResolvedOxidizedPythonInterpreterConfig<'config>>
607    for PythonResourcesState<'a, u8>
608{
609    type Error = NewInterpreterError;
610
611    fn try_from(
612        config: &ResolvedOxidizedPythonInterpreterConfig<'config>,
613    ) -> Result<Self, Self::Error> {
614        let mut state = Self::default();
615        state.set_current_exe(config.exe().to_path_buf());
616        state.set_origin(config.origin().to_path_buf());
617
618        for source in &config.packed_resources {
619            match source {
620                PackedResourcesSource::Memory(data) => {
621                    state
622                        .index_data(data)
623                        .map_err(NewInterpreterError::Simple)?;
624                }
625                PackedResourcesSource::MemoryMappedPath(path) => {
626                    state
627                        .index_path_memory_mapped(path)
628                        .map_err(NewInterpreterError::Dynamic)?;
629                }
630            }
631        }
632
633        state
634            .index_interpreter_builtins()
635            .map_err(NewInterpreterError::Simple)?;
636
637        Ok(state)
638    }
639}
640
641#[cfg(test)]
642mod tests {
643    use {super::*, anyhow::Result};
644
645    #[test]
646    fn test_packed_resources_implicit_origin() -> Result<()> {
647        let mut config = OxidizedPythonInterpreterConfig::default();
648        config
649            .packed_resources
650            .push(PackedResourcesSource::MemoryMappedPath(PathBuf::from(
651                "$ORIGIN/lib/packed-resources",
652            )));
653
654        let resolved = config.resolve()?;
655
656        assert_eq!(
657            resolved.packed_resources,
658            vec![PackedResourcesSource::MemoryMappedPath(
659                resolved.origin().join("lib/packed-resources")
660            )]
661        );
662
663        Ok(())
664    }
665
666    #[test]
667    fn test_packed_resources_explicit_origin() -> Result<()> {
668        let mut config = OxidizedPythonInterpreterConfig {
669            origin: Some(PathBuf::from("/other/origin")),
670            ..Default::default()
671        };
672
673        config
674            .packed_resources
675            .push(PackedResourcesSource::MemoryMappedPath(PathBuf::from(
676                "$ORIGIN/lib/packed-resources",
677            )));
678
679        let resolved = config.resolve()?;
680
681        assert_eq!(
682            resolved.packed_resources,
683            vec![PackedResourcesSource::MemoryMappedPath(PathBuf::from(
684                "/other/origin/lib/packed-resources"
685            ))]
686        );
687
688        Ok(())
689    }
690}