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}