pyoxidizerlib/py_packaging/
standalone_distribution.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5/*! Functionality for standalone Python distributions. */
6
7use {
8    super::{
9        binary::{LibpythonLinkMode, PythonBinaryBuilder},
10        config::{default_memory_allocator, PyembedPythonInterpreterConfig},
11        distribution::{
12            resolve_python_distribution_from_location, AppleSdkInfo, BinaryLibpythonLinkMode,
13            DistributionExtractLock, PythonDistribution, PythonDistributionLocation,
14        },
15        distutils::prepare_hacked_distutils,
16        standalone_builder::StandalonePythonExecutableBuilder,
17    },
18    crate::environment::{Environment, LINUX_TARGET_TRIPLES, MACOS_TARGET_TRIPLES},
19    anyhow::{anyhow, Context, Result},
20    duct::cmd,
21    log::{info, warn},
22    once_cell::sync::Lazy,
23    path_dedot::ParseDot,
24    python_packaging::{
25        bytecode::{BytecodeCompiler, PythonBytecodeCompiler},
26        filesystem_scanning::{find_python_resources, walk_tree_files},
27        interpreter::{PythonInterpreterConfig, PythonInterpreterProfile, TerminfoResolution},
28        licensing::{ComponentFlavor, LicenseFlavor, LicensedComponent},
29        location::ConcreteResourceLocation,
30        module_util::{is_package_from_path, PythonModuleSuffixes},
31        policy::PythonPackagingPolicy,
32        resource::{
33            LibraryDependency, PythonExtensionModule, PythonExtensionModuleVariants,
34            PythonModuleSource, PythonPackageResource, PythonResource,
35        },
36    },
37    serde::Deserialize,
38    simple_file_manifest::{FileData, FileEntry},
39    std::{
40        collections::{hash_map::RandomState, BTreeMap, HashMap},
41        io::{BufRead, BufReader, Read},
42        path::{Path, PathBuf},
43        sync::Arc,
44    },
45};
46
47// This needs to be kept in sync with *compiler.py
48const PYOXIDIZER_STATE_DIR: &str = "state/pyoxidizer";
49
50#[cfg(windows)]
51const PYTHON_EXE_BASENAME: &str = "python.exe";
52
53#[cfg(unix)]
54const PYTHON_EXE_BASENAME: &str = "python3";
55
56#[cfg(windows)]
57const PIP_EXE_BASENAME: &str = "pip3.exe";
58
59#[cfg(unix)]
60const PIP_EXE_BASENAME: &str = "pip3";
61
62/// Distribution extensions with known problems on Linux.
63///
64/// These will never be packaged.
65pub static BROKEN_EXTENSIONS_LINUX: Lazy<Vec<String>> = Lazy::new(|| {
66    vec![
67        // Linking issues.
68        "_crypt".to_string(),
69        // Linking issues.
70        "nis".to_string(),
71    ]
72});
73
74/// Distribution extensions with known problems on macOS.
75///
76/// These will never be packaged.
77pub static BROKEN_EXTENSIONS_MACOS: Lazy<Vec<String>> = Lazy::new(|| {
78    vec![
79        // curses and readline have linking issues.
80        "curses".to_string(),
81        "_curses_panel".to_string(),
82        "readline".to_string(),
83    ]
84});
85
86/// Python modules that we shouldn't generate bytecode for by default.
87///
88/// These are Python modules in the standard library that don't have valid bytecode.
89pub static NO_BYTECODE_MODULES: Lazy<Vec<&'static str>> = Lazy::new(|| {
90    vec![
91        "lib2to3.tests.data.bom",
92        "lib2to3.tests.data.crlf",
93        "lib2to3.tests.data.different_encoding",
94        "lib2to3.tests.data.false_encoding",
95        "lib2to3.tests.data.py2_test_grammar",
96        "lib2to3.tests.data.py3_test_grammar",
97        "test.bad_coding",
98        "test.badsyntax_3131",
99        "test.badsyntax_future3",
100        "test.badsyntax_future4",
101        "test.badsyntax_future5",
102        "test.badsyntax_future6",
103        "test.badsyntax_future7",
104        "test.badsyntax_future8",
105        "test.badsyntax_future9",
106        "test.badsyntax_future10",
107        "test.badsyntax_pep3120",
108    ]
109});
110
111#[derive(Debug, Deserialize)]
112struct LinkEntry {
113    name: String,
114    path_static: Option<String>,
115    path_dynamic: Option<String>,
116    framework: Option<bool>,
117    system: Option<bool>,
118}
119
120impl LinkEntry {
121    /// Convert the instance to a `LibraryDependency`.
122    fn to_library_dependency(&self, python_path: &Path) -> LibraryDependency {
123        LibraryDependency {
124            name: self.name.clone(),
125            static_library: self
126                .path_static
127                .clone()
128                .map(|p| FileData::Path(python_path.join(p))),
129            static_filename: self
130                .path_static
131                .as_ref()
132                .map(|f| PathBuf::from(PathBuf::from(f).file_name().unwrap())),
133            dynamic_library: self
134                .path_dynamic
135                .clone()
136                .map(|p| FileData::Path(python_path.join(p))),
137            dynamic_filename: self
138                .path_dynamic
139                .as_ref()
140                .map(|f| PathBuf::from(PathBuf::from(f).file_name().unwrap())),
141            framework: self.framework.unwrap_or(false),
142            system: self.system.unwrap_or(false),
143        }
144    }
145}
146
147#[allow(unused)]
148#[derive(Debug, Deserialize)]
149struct PythonBuildExtensionInfo {
150    in_core: bool,
151    init_fn: String,
152    licenses: Option<Vec<String>>,
153    license_paths: Option<Vec<String>>,
154    license_public_domain: Option<bool>,
155    links: Vec<LinkEntry>,
156    objs: Vec<String>,
157    required: bool,
158    static_lib: Option<String>,
159    shared_lib: Option<String>,
160    variant: String,
161}
162
163#[allow(unused)]
164#[derive(Debug, Deserialize)]
165struct PythonBuildCoreInfo {
166    objs: Vec<String>,
167    links: Vec<LinkEntry>,
168    shared_lib: Option<String>,
169    static_lib: Option<String>,
170}
171
172#[allow(unused)]
173#[derive(Debug, Deserialize)]
174struct PythonBuildInfo {
175    core: PythonBuildCoreInfo,
176    extensions: BTreeMap<String, Vec<PythonBuildExtensionInfo>>,
177    inittab_object: String,
178    inittab_source: String,
179    inittab_cflags: Vec<String>,
180    object_file_format: String,
181}
182
183#[allow(unused)]
184#[derive(Debug, Deserialize)]
185struct PythonJsonMain {
186    version: String,
187    target_triple: String,
188    optimizations: String,
189    python_tag: String,
190    python_abi_tag: Option<String>,
191    python_config_vars: HashMap<String, String>,
192    python_platform_tag: String,
193    python_implementation_cache_tag: String,
194    python_implementation_hex_version: u64,
195    python_implementation_name: String,
196    python_implementation_version: Vec<String>,
197    python_version: String,
198    python_major_minor_version: String,
199    python_paths: HashMap<String, String>,
200    python_paths_abstract: HashMap<String, String>,
201    python_exe: String,
202    python_stdlib_test_packages: Vec<String>,
203    python_suffixes: HashMap<String, Vec<String>>,
204    python_bytecode_magic_number: String,
205    python_symbol_visibility: String,
206    python_extension_module_loading: Vec<String>,
207    apple_sdk_canonical_name: Option<String>,
208    apple_sdk_platform: Option<String>,
209    apple_sdk_version: Option<String>,
210    apple_sdk_deployment_target: Option<String>,
211    libpython_link_mode: String,
212    crt_features: Vec<String>,
213    run_tests: String,
214    build_info: PythonBuildInfo,
215    licenses: Option<Vec<String>>,
216    license_path: Option<String>,
217    tcl_library_path: Option<String>,
218    tcl_library_paths: Option<Vec<String>>,
219}
220
221fn parse_python_json(path: &Path) -> Result<PythonJsonMain> {
222    if !path.exists() {
223        return Err(anyhow!("PYTHON.json does not exist; are you using an up-to-date Python distribution that conforms with our requirements?"));
224    }
225
226    let buf = std::fs::read(path)?;
227
228    let value: serde_json::Value = serde_json::from_slice(&buf)?;
229    let o = value
230        .as_object()
231        .ok_or_else(|| anyhow!("PYTHON.json does not parse to an object"))?;
232
233    match o.get("version") {
234        Some(version) => {
235            let version = version
236                .as_str()
237                .ok_or_else(|| anyhow!("unable to parse version as a string"))?;
238
239            if version != "7" {
240                return Err(anyhow!(
241                    "expected version 7 standalone distribution; found version {}",
242                    version
243                ));
244            }
245        }
246        None => return Err(anyhow!("version key not present in PYTHON.json")),
247    }
248
249    let v: PythonJsonMain = serde_json::from_slice(&buf)?;
250
251    Ok(v)
252}
253
254fn parse_python_json_from_distribution(dist_dir: &Path) -> Result<PythonJsonMain> {
255    let python_json_path = dist_dir.join("python").join("PYTHON.json");
256    parse_python_json(&python_json_path)
257}
258
259fn parse_python_major_minor_version(version: &str) -> String {
260    let mut at_least_minor_version = String::from(version);
261    if !version.contains('.') {
262        at_least_minor_version.push_str(".0");
263    }
264    at_least_minor_version
265        .split('.')
266        .take(2)
267        .collect::<Vec<_>>()
268        .join(".")
269}
270
271/// Resolve the path to a `python` executable in a Python distribution.
272pub fn python_exe_path(dist_dir: &Path) -> Result<PathBuf> {
273    let pi = parse_python_json_from_distribution(dist_dir)?;
274
275    Ok(dist_dir.join("python").join(&pi.python_exe))
276}
277
278#[derive(Debug)]
279pub struct PythonPaths {
280    pub prefix: PathBuf,
281    pub bin_dir: PathBuf,
282    pub python_exe: PathBuf,
283    pub stdlib: PathBuf,
284    pub site_packages: PathBuf,
285    pub pyoxidizer_state_dir: PathBuf,
286}
287
288/// Resolve the location of Python modules given a base install path.
289pub fn resolve_python_paths(base: &Path, python_version: &str) -> PythonPaths {
290    let prefix = base.to_path_buf();
291
292    let p = prefix.clone();
293
294    let windows_layout = p.join("Scripts").exists();
295
296    let bin_dir = if windows_layout {
297        p.join("Scripts")
298    } else {
299        p.join("bin")
300    };
301
302    let python_exe = if bin_dir.join(PYTHON_EXE_BASENAME).exists() {
303        bin_dir.join(PYTHON_EXE_BASENAME)
304    } else {
305        p.join(PYTHON_EXE_BASENAME)
306    };
307
308    let mut pyoxidizer_state_dir = p.clone();
309    pyoxidizer_state_dir.extend(PYOXIDIZER_STATE_DIR.split('/'));
310
311    let unix_lib_dir = p.join("lib").join(format!(
312        "python{}",
313        parse_python_major_minor_version(python_version)
314    ));
315
316    let stdlib = if unix_lib_dir.exists() {
317        unix_lib_dir
318    } else if windows_layout {
319        p.join("Lib")
320    } else {
321        unix_lib_dir
322    };
323
324    let site_packages = stdlib.join("site-packages");
325
326    PythonPaths {
327        prefix,
328        bin_dir,
329        python_exe,
330        stdlib,
331        site_packages,
332        pyoxidizer_state_dir,
333    }
334}
335
336pub fn invoke_python(python_paths: &PythonPaths, args: &[&str]) {
337    let site_packages_s = python_paths.site_packages.display().to_string();
338
339    if site_packages_s.starts_with("\\\\?\\") {
340        panic!("Unexpected Windows UNC path in site-packages path");
341    }
342
343    info!("setting PYTHONPATH {}", site_packages_s);
344
345    let mut envs: HashMap<String, String, RandomState> = std::env::vars().collect();
346    envs.insert("PYTHONPATH".to_string(), site_packages_s);
347
348    info!(
349        "running {} {}",
350        python_paths.python_exe.display(),
351        args.join(" ")
352    );
353
354    let command = cmd(&python_paths.python_exe, args)
355        .full_env(&envs)
356        .stderr_to_stdout()
357        .reader()
358        .unwrap_or_else(|_| {
359            panic!(
360                "failed to run {} {}",
361                python_paths.python_exe.display(),
362                args.join(" ")
363            )
364        });
365    {
366        let reader = BufReader::new(&command);
367        for line in reader.lines() {
368            match line {
369                Ok(line) => {
370                    warn!("{}", line);
371                }
372                Err(err) => {
373                    warn!("Error when reading output: {:?}", err);
374                }
375            }
376        }
377    }
378}
379
380/// Describes how libpython is linked in a standalone distribution.
381#[derive(Clone, Debug, PartialEq, Eq)]
382pub enum StandaloneDistributionLinkMode {
383    Static,
384    Dynamic,
385}
386
387/// Represents a standalone Python distribution.
388///
389/// This is a Python distributed produced by the `python-build-standalone`
390/// project. It is derived from a tarball containing a `PYTHON.json` file
391/// describing the distribution.
392#[allow(unused)]
393#[derive(Clone, Debug)]
394pub struct StandaloneDistribution {
395    /// Directory where distribution lives in the filesystem.
396    pub base_dir: PathBuf,
397
398    /// Rust target triple that this distribution runs on.
399    pub target_triple: String,
400
401    /// Python implementation name.
402    pub python_implementation: String,
403
404    /// PEP 425 Python tag value.
405    pub python_tag: String,
406
407    /// PEP 425 Python ABI tag.
408    pub python_abi_tag: Option<String>,
409
410    /// PEP 425 Python platform tag.
411    pub python_platform_tag: String,
412
413    /// Python version string.
414    pub version: String,
415
416    /// Path to Python interpreter executable.
417    pub python_exe: PathBuf,
418
419    /// Path to Python standard library.
420    pub stdlib_path: PathBuf,
421
422    /// Python packages in the standard library providing tests.
423    stdlib_test_packages: Vec<String>,
424
425    /// How libpython is linked in this distribution.
426    link_mode: StandaloneDistributionLinkMode,
427
428    /// Symbol visibility for Python symbols.
429    pub python_symbol_visibility: String,
430
431    /// Capabilities of distribution to load extension modules.
432    extension_module_loading: Vec<String>,
433
434    /// Apple SDK build/targeting settings.
435    apple_sdk_info: Option<AppleSdkInfo>,
436
437    /// Holds license information for the core distribution.
438    pub core_license: Option<LicensedComponent>,
439
440    /// SPDX license shortnames that apply to this distribution.
441    ///
442    /// Licenses only cover the core distribution. Licenses for libraries
443    /// required by extensions are stored next to the extension's linking
444    /// info.
445    pub licenses: Option<Vec<String>>,
446
447    /// Path to file holding license text for this distribution.
448    pub license_path: Option<PathBuf>,
449
450    /// Path to Tcl library files.
451    tcl_library_path: Option<PathBuf>,
452
453    /// Directories under `tcl_library_path` containing tcl files.
454    tcl_library_paths: Option<Vec<String>>,
455
456    /// Object files providing the core Python implementation.
457    ///
458    /// Keys are relative paths. Values are filesystem paths.
459    pub objs_core: BTreeMap<PathBuf, PathBuf>,
460
461    /// Linking information for the core Python implementation.
462    pub links_core: Vec<LibraryDependency>,
463
464    /// Filesystem location of pythonXY shared library for this distribution.
465    ///
466    /// Only set if `link_mode` is `StandaloneDistributionLinkMode::Dynamic`.
467    pub libpython_shared_library: Option<PathBuf>,
468
469    /// Extension modules available to this distribution.
470    pub extension_modules: BTreeMap<String, PythonExtensionModuleVariants>,
471
472    pub frozen_c: Vec<u8>,
473
474    /// Include files for Python.
475    ///
476    /// Keys are relative paths. Values are filesystem paths.
477    pub includes: BTreeMap<String, PathBuf>,
478
479    /// Static libraries available for linking.
480    ///
481    /// Keys are library names, without the "lib" prefix or file extension.
482    /// Values are filesystem paths where library is located.
483    pub libraries: BTreeMap<String, PathBuf>,
484
485    pub py_modules: BTreeMap<String, PathBuf>,
486
487    /// Non-module Python resource files.
488    ///
489    /// Keys are package names. Values are maps of resource name to data for the resource
490    /// within that package.
491    pub resources: BTreeMap<String, BTreeMap<String, PathBuf>>,
492
493    /// Path to copy of hacked dist to use for packaging rules venvs
494    pub venv_base: PathBuf,
495
496    /// Path to object file defining _PyImport_Inittab.
497    pub inittab_object: PathBuf,
498
499    /// Compiler flags to use to build object containing _PyImport_Inittab.
500    pub inittab_cflags: Vec<String>,
501
502    /// Tag to apply to bytecode files.
503    ///
504    /// e.g. `cpython-39`.
505    pub cache_tag: String,
506
507    /// Suffixes for Python module types.
508    module_suffixes: PythonModuleSuffixes,
509
510    /// List of strings denoting C Runtime requirements.
511    pub crt_features: Vec<String>,
512
513    /// Configuration variables used by Python.
514    config_vars: HashMap<String, String>,
515}
516
517impl StandaloneDistribution {
518    pub fn from_location(
519        location: &PythonDistributionLocation,
520        distributions_dir: &Path,
521    ) -> Result<Self> {
522        let (archive_path, extract_path) =
523            resolve_python_distribution_from_location(location, distributions_dir)?;
524
525        Self::from_tar_zst_file(&archive_path, &extract_path)
526    }
527
528    /// Create an instance from a .tar.zst file.
529    ///
530    /// The distribution will be extracted to ``extract_dir`` if necessary.
531    pub fn from_tar_zst_file(path: &Path, extract_dir: &Path) -> Result<Self> {
532        let basename = path
533            .file_name()
534            .ok_or_else(|| anyhow!("unable to determine filename"))?
535            .to_string_lossy();
536
537        if !basename.ends_with(".tar.zst") {
538            return Err(anyhow!("unhandled distribution format: {}", path.display()));
539        }
540
541        let fh = std::fs::File::open(path)
542            .with_context(|| format!("unable to open {}", path.display()))?;
543
544        let reader = BufReader::new(fh);
545
546        Self::from_tar_zst(reader, extract_dir).context("reading tar.zst distribution data")
547    }
548
549    /// Extract and analyze a standalone distribution from a zstd compressed tar stream.
550    pub fn from_tar_zst<R: Read>(source: R, extract_dir: &Path) -> Result<Self> {
551        let dctx = zstd::stream::Decoder::new(source)?;
552
553        Self::from_tar(dctx, extract_dir).context("reading tar distribution data")
554    }
555
556    /// Extract and analyze a standalone distribution from a tar stream.
557    #[allow(clippy::unnecessary_unwrap)]
558    pub fn from_tar<R: Read>(source: R, extract_dir: &Path) -> Result<Self> {
559        let mut tf = tar::Archive::new(source);
560
561        {
562            let _lock = DistributionExtractLock::new(extract_dir)?;
563
564            // The content of the distribution could change between runs. But caching
565            // the extraction does keep things fast.
566            let test_path = extract_dir.join("python").join("PYTHON.json");
567            if !test_path.exists() {
568                std::fs::create_dir_all(extract_dir)?;
569                let absolute_path = std::fs::canonicalize(extract_dir)?;
570
571                let mut symlinks = vec![];
572
573                for entry in tf.entries()? {
574                    let mut entry =
575                        entry.map_err(|e| anyhow!("failed to iterate over archive: {}", e))?;
576
577                    // The mtimes in the archive may be 0 / UNIX epoch. This shouldn't
578                    // matter. However, pip will sometimes attempt to produce a zip file of
579                    // its own content and Python's zip code won't handle times before 1980,
580                    // which is later than UNIX epoch. This can lead to pip blowing up at
581                    // run-time. We work around this by not adjusting the mtime when
582                    // extracting the archive. This effectively makes the mtime "now."
583                    entry.set_preserve_mtime(false);
584
585                    // Windows doesn't support symlinks without special permissions.
586                    // So we track symlinks explicitly and copy files post extract if
587                    // running on that platform.
588                    let link_name = entry.link_name().unwrap_or(None);
589
590                    if link_name.is_some() && cfg!(target_family = "windows") {
591                        // The entry's path is the file to write, relative to the archive's
592                        // root. We need to expand to an absolute path to facilitate copying.
593
594                        // The link name is the file to symlink to, or the file we're copying.
595                        // This path is relative to the entry path. So we need join with the
596                        // entry's directory and canonicalize. There is also a security issue
597                        // at play: archives could contain bogus symlinks pointing outside the
598                        // archive. So we detect this, just in case.
599
600                        let mut dest = absolute_path.clone();
601                        dest.extend(entry.path()?.components());
602                        let dest = dest
603                            .parse_dot()
604                            .with_context(|| "dedotting symlinked source")?
605                            .to_path_buf();
606
607                        let mut source = dest
608                            .parent()
609                            .ok_or_else(|| anyhow!("unable to resolve parent"))?
610                            .to_path_buf();
611                        source.extend(link_name.unwrap().components());
612                        let source = source
613                            .parse_dot()
614                            .with_context(|| "dedotting symlink destination")?
615                            .to_path_buf();
616
617                        if !source.starts_with(&absolute_path) {
618                            return Err(anyhow!("malicious symlink detected in archive"));
619                        }
620
621                        symlinks.push((source, dest));
622                    } else {
623                        entry
624                            .unpack_in(&absolute_path)
625                            .with_context(|| "unable to extract tar member")?;
626                    }
627                }
628
629                for (source, dest) in symlinks {
630                    std::fs::copy(&source, &dest).with_context(|| {
631                        format!(
632                            "copying symlinked file {} -> {}",
633                            source.display(),
634                            dest.display(),
635                        )
636                    })?;
637                }
638
639                // Ensure unpacked files are writable. We've had issues where we
640                // consume archives with read-only file permissions. When we later
641                // copy these files, we can run into trouble overwriting a read-only
642                // file.
643                let walk = walkdir::WalkDir::new(&absolute_path);
644                for entry in walk.into_iter() {
645                    let entry = entry?;
646
647                    let metadata = entry.metadata()?;
648                    let mut permissions = metadata.permissions();
649
650                    if permissions.readonly() {
651                        permissions.set_readonly(false);
652                        std::fs::set_permissions(entry.path(), permissions).with_context(|| {
653                            format!("unable to mark {} as writable", entry.path().display())
654                        })?;
655                    }
656                }
657            }
658        }
659
660        Self::from_directory(extract_dir)
661    }
662
663    /// Obtain an instance by scanning a directory containing an extracted distribution.
664    #[allow(clippy::cognitive_complexity)]
665    pub fn from_directory(dist_dir: &Path) -> Result<Self> {
666        let mut objs_core: BTreeMap<PathBuf, PathBuf> = BTreeMap::new();
667        let mut links_core: Vec<LibraryDependency> = Vec::new();
668        let mut extension_modules: BTreeMap<String, PythonExtensionModuleVariants> =
669            BTreeMap::new();
670        let mut includes: BTreeMap<String, PathBuf> = BTreeMap::new();
671        let mut libraries = BTreeMap::new();
672        let frozen_c: Vec<u8> = Vec::new();
673        let mut py_modules: BTreeMap<String, PathBuf> = BTreeMap::new();
674        let mut resources: BTreeMap<String, BTreeMap<String, PathBuf>> = BTreeMap::new();
675
676        for entry in std::fs::read_dir(dist_dir)? {
677            let entry = entry?;
678
679            match entry.file_name().to_str() {
680                Some("python") => continue,
681                Some(value) => {
682                    return Err(anyhow!(
683                        "unexpected entry in distribution root directory: {}",
684                        value
685                    ))
686                }
687                _ => {
688                    return Err(anyhow!(
689                        "error listing root directory of Python distribution"
690                    ))
691                }
692            };
693        }
694
695        let python_path = dist_dir.join("python");
696
697        for entry in std::fs::read_dir(&python_path)? {
698            let entry = entry?;
699
700            match entry.file_name().to_str() {
701                Some("build") => continue,
702                Some("install") => continue,
703                Some("lib") => continue,
704                Some("licenses") => continue,
705                Some("LICENSE.rst") => continue,
706                Some("PYTHON.json") => continue,
707                Some(value) => {
708                    return Err(anyhow!("unexpected entry in python/ directory: {}", value))
709                }
710                _ => return Err(anyhow!("error listing python/ directory")),
711            };
712        }
713
714        let pi = parse_python_json_from_distribution(dist_dir)?;
715
716        // Derive the distribution's license from a license file, if present.
717        let core_license = if let Some(ref python_license_path) = pi.license_path {
718            let license_path = python_path.join(python_license_path);
719            let license_text = std::fs::read_to_string(&license_path).with_context(|| {
720                format!("unable to read Python license {}", license_path.display())
721            })?;
722
723            let expression = pi.licenses.clone().unwrap().join(" OR ");
724
725            let mut component = LicensedComponent::new_spdx(
726                ComponentFlavor::PythonDistribution(pi.python_implementation_name.clone()),
727                &expression,
728            )?;
729            component.add_license_text(license_text);
730
731            Some(component)
732        } else {
733            None
734        };
735
736        // Collect object files for libpython.
737        for obj in &pi.build_info.core.objs {
738            let rel_path = PathBuf::from(obj);
739            let full_path = python_path.join(obj);
740
741            objs_core.insert(rel_path, full_path);
742        }
743
744        for entry in &pi.build_info.core.links {
745            let depends = entry.to_library_dependency(&python_path);
746
747            if let Some(p) = &depends.static_library {
748                if let Some(p) = p.backing_path() {
749                    libraries.insert(depends.name.clone(), p.to_path_buf());
750                }
751            }
752
753            links_core.push(depends);
754        }
755
756        let module_suffixes = PythonModuleSuffixes {
757            source: pi
758                .python_suffixes
759                .get("source")
760                .ok_or_else(|| anyhow!("distribution does not define source suffixes"))?
761                .clone(),
762            bytecode: pi
763                .python_suffixes
764                .get("bytecode")
765                .ok_or_else(|| anyhow!("distribution does not define bytecode suffixes"))?
766                .clone(),
767            debug_bytecode: pi
768                .python_suffixes
769                .get("debug_bytecode")
770                .ok_or_else(|| anyhow!("distribution does not define debug bytecode suffixes"))?
771                .clone(),
772            optimized_bytecode: pi
773                .python_suffixes
774                .get("optimized_bytecode")
775                .ok_or_else(|| anyhow!("distribution does not define optimized bytecode suffixes"))?
776                .clone(),
777            extension: pi
778                .python_suffixes
779                .get("extension")
780                .ok_or_else(|| anyhow!("distribution does not define extension suffixes"))?
781                .clone(),
782        };
783
784        // Collect extension modules.
785        for (module, variants) in &pi.build_info.extensions {
786            let mut ems = PythonExtensionModuleVariants::default();
787
788            for entry in variants.iter() {
789                let extension_file_suffix = if let Some(p) = &entry.shared_lib {
790                    if let Some(idx) = p.rfind('.') {
791                        p[idx..].to_string()
792                    } else {
793                        "".to_string()
794                    }
795                } else {
796                    "".to_string()
797                };
798
799                let object_file_data = entry
800                    .objs
801                    .iter()
802                    .map(|p| FileData::Path(python_path.join(p)))
803                    .collect();
804                let mut links = Vec::new();
805
806                for link in &entry.links {
807                    let depends = link.to_library_dependency(&python_path);
808
809                    if let Some(p) = &depends.static_library {
810                        if let Some(p) = p.backing_path() {
811                            libraries.insert(depends.name.clone(), p.to_path_buf());
812                        }
813                    }
814
815                    links.push(depends);
816                }
817
818                let component_flavor =
819                    ComponentFlavor::PythonStandardLibraryExtensionModule(module.clone());
820
821                let mut license = if entry.license_public_domain.unwrap_or(false) {
822                    LicensedComponent::new(component_flavor, LicenseFlavor::PublicDomain)
823                } else if let Some(licenses) = &entry.licenses {
824                    let expression = licenses.join(" OR ");
825                    LicensedComponent::new_spdx(component_flavor, &expression)?
826                } else if let Some(core) = &core_license {
827                    LicensedComponent::new_spdx(
828                        component_flavor,
829                        core.spdx_expression()
830                            .ok_or_else(|| anyhow!("could not resolve SPDX license for core"))?
831                            .as_ref(),
832                    )?
833                } else {
834                    LicensedComponent::new(component_flavor, LicenseFlavor::None)
835                };
836
837                if let Some(license_paths) = &entry.license_paths {
838                    for path in license_paths {
839                        let path = python_path.join(path);
840                        let text = std::fs::read_to_string(&path)
841                            .with_context(|| format!("reading {}", path.display()))?;
842
843                        license.add_license_text(text);
844                    }
845                }
846
847                ems.push(PythonExtensionModule {
848                    name: module.clone(),
849                    init_fn: Some(entry.init_fn.clone()),
850                    extension_file_suffix,
851                    shared_library: entry
852                        .shared_lib
853                        .as_ref()
854                        .map(|path| FileData::Path(python_path.join(path))),
855                    object_file_data,
856                    is_package: false,
857                    link_libraries: links,
858                    is_stdlib: true,
859                    builtin_default: entry.in_core,
860                    required: entry.required,
861                    variant: Some(entry.variant.clone()),
862                    license: Some(license),
863                });
864            }
865
866            extension_modules.insert(module.clone(), ems);
867        }
868
869        let include_path = if let Some(p) = pi.python_paths.get("include") {
870            python_path.join(p)
871        } else {
872            return Err(anyhow!("include path not defined in distribution"));
873        };
874
875        for entry in walk_tree_files(&include_path) {
876            let full_path = entry.path();
877            let rel_path = full_path
878                .strip_prefix(&include_path)
879                .expect("unable to strip prefix");
880            includes.insert(
881                String::from(rel_path.to_str().expect("path to string")),
882                full_path.to_path_buf(),
883            );
884        }
885
886        let stdlib_path = if let Some(p) = pi.python_paths.get("stdlib") {
887            python_path.join(p)
888        } else {
889            return Err(anyhow!("stdlib path not defined in distribution"));
890        };
891
892        for entry in find_python_resources(
893            &stdlib_path,
894            &pi.python_implementation_cache_tag,
895            &module_suffixes,
896            false,
897            true,
898        )? {
899            match entry? {
900                PythonResource::PackageResource(resource) => {
901                    if !resources.contains_key(&resource.leaf_package) {
902                        resources.insert(resource.leaf_package.clone(), BTreeMap::new());
903                    }
904
905                    resources.get_mut(&resource.leaf_package).unwrap().insert(
906                        resource.relative_name.clone(),
907                        match &resource.data {
908                            FileData::Path(path) => path.to_path_buf(),
909                            FileData::Memory(_) => {
910                                return Err(anyhow!(
911                                    "should not have received in-memory resource data"
912                                ))
913                            }
914                        },
915                    );
916                }
917                PythonResource::ModuleSource(source) => match &source.source {
918                    FileData::Path(path) => {
919                        py_modules.insert(source.name.clone(), path.to_path_buf());
920                    }
921                    FileData::Memory(_) => {
922                        return Err(anyhow!("should not have received in-memory source data"))
923                    }
924                },
925
926                PythonResource::ModuleBytecodeRequest(_) => {}
927                PythonResource::ModuleBytecode(_) => {}
928                PythonResource::PackageDistributionResource(_) => {}
929                PythonResource::ExtensionModule(_) => {}
930                PythonResource::EggFile(_) => {}
931                PythonResource::PathExtension(_) => {}
932                PythonResource::File(_) => {}
933            };
934        }
935
936        let venv_base = dist_dir.parent().unwrap().join("hacked_base");
937
938        let (link_mode, libpython_shared_library) = if pi.libpython_link_mode == "static" {
939            (StandaloneDistributionLinkMode::Static, None)
940        } else if pi.libpython_link_mode == "shared" {
941            (
942                StandaloneDistributionLinkMode::Dynamic,
943                Some(python_path.join(pi.build_info.core.shared_lib.unwrap())),
944            )
945        } else {
946            return Err(anyhow!("unhandled link mode: {}", pi.libpython_link_mode));
947        };
948
949        let apple_sdk_info = if let Some(canonical_name) = pi.apple_sdk_canonical_name {
950            let platform = pi
951                .apple_sdk_platform
952                .ok_or_else(|| anyhow!("apple_sdk_platform not defined"))?;
953            let version = pi
954                .apple_sdk_version
955                .ok_or_else(|| anyhow!("apple_sdk_version not defined"))?;
956            let deployment_target = pi
957                .apple_sdk_deployment_target
958                .ok_or_else(|| anyhow!("apple_sdk_deployment_target not defined"))?;
959
960            Some(AppleSdkInfo {
961                canonical_name,
962                platform,
963                version,
964                deployment_target,
965            })
966        } else {
967            None
968        };
969
970        let inittab_object = python_path.join(pi.build_info.inittab_object);
971
972        Ok(Self {
973            base_dir: dist_dir.to_path_buf(),
974            target_triple: pi.target_triple,
975            python_implementation: pi.python_implementation_name,
976            python_tag: pi.python_tag,
977            python_abi_tag: pi.python_abi_tag,
978            python_platform_tag: pi.python_platform_tag,
979            version: pi.python_version.clone(),
980            python_exe: python_exe_path(dist_dir)?,
981            stdlib_path,
982            stdlib_test_packages: pi.python_stdlib_test_packages,
983            link_mode,
984            python_symbol_visibility: pi.python_symbol_visibility,
985            extension_module_loading: pi.python_extension_module_loading,
986            apple_sdk_info,
987            core_license,
988            licenses: pi.licenses.clone(),
989            license_path: pi.license_path.as_ref().map(PathBuf::from),
990            tcl_library_path: pi
991                .tcl_library_path
992                .as_ref()
993                .map(|path| dist_dir.join("python").join(path)),
994            tcl_library_paths: pi.tcl_library_paths.clone(),
995            extension_modules,
996            frozen_c,
997            includes,
998            links_core,
999            libraries,
1000            objs_core,
1001            libpython_shared_library,
1002            py_modules,
1003            resources,
1004            venv_base,
1005            inittab_object,
1006            inittab_cflags: pi.build_info.inittab_cflags,
1007            cache_tag: pi.python_implementation_cache_tag,
1008            module_suffixes,
1009            crt_features: pi.crt_features,
1010            config_vars: pi.python_config_vars,
1011        })
1012    }
1013
1014    /// Determines support for building a libpython from this distribution.
1015    ///
1016    /// Returns a tuple of bools indicating whether this distribution can
1017    /// build a static libpython and a dynamically linked libpython.
1018    pub fn libpython_link_support(&self) -> (bool, bool) {
1019        if self.target_triple.contains("pc-windows") {
1020            // On Windows, support for libpython linkage is determined
1021            // by presence of a shared library in the distribution. This
1022            // isn't entirely semantically correct. Since we use `dllexport`
1023            // for all symbols in standalone distributions, it may
1024            // theoretically be possible to produce both a static and dynamic
1025            // libpython from the same object files. But since the
1026            // static and dynamic distributions are built so differently, we
1027            // don't want to take any chances and we force each distribution
1028            // to its own domain.
1029            (
1030                self.libpython_shared_library.is_none(),
1031                self.libpython_shared_library.is_some(),
1032            )
1033        } else if self.target_triple.contains("linux-musl") {
1034            // Musl binaries don't support dynamic linking.
1035            (true, false)
1036        } else {
1037            // Elsewhere we can choose which link mode to use.
1038            (true, true)
1039        }
1040    }
1041
1042    /// Whether the distribution is capable of loading filed-based Python extension modules.
1043    pub fn is_extension_module_file_loadable(&self) -> bool {
1044        self.extension_module_loading
1045            .contains(&"shared-library".to_string())
1046    }
1047}
1048
1049impl PythonDistribution for StandaloneDistribution {
1050    fn clone_trait(&self) -> Arc<dyn PythonDistribution> {
1051        Arc::new(self.clone())
1052    }
1053
1054    fn target_triple(&self) -> &str {
1055        &self.target_triple
1056    }
1057
1058    fn compatible_host_triples(&self) -> Vec<String> {
1059        let mut res = vec![self.target_triple.clone()];
1060
1061        res.extend(
1062            match self.target_triple() {
1063                "aarch64-unknown-linux-gnu" => vec![],
1064                // musl libc linked distributions run on GNU Linux.
1065                "aarch64-unknown-linux-musl" => vec!["aarch64-unknown-linux-gnu"],
1066                "x86_64-unknown-linux-gnu" => vec![],
1067                // musl libc linked distributions run on GNU Linux.
1068                "x86_64-unknown-linux-musl" => vec!["x86_64-unknown-linux-gnu"],
1069                "aarch64-apple-darwin" => vec![],
1070                "x86_64-apple-darwin" => vec![],
1071                // 32-bit Windows GNU on 32-bit Windows MSVC and 64-bit Windows.
1072                "i686-pc-windows-gnu" => vec![
1073                    "i686-pc-windows-msvc",
1074                    "x86_64-pc-windows-gnu",
1075                    "x86_64-pc-windows-msvc",
1076                ],
1077                // 32-bit Windows MSVC runs on 32-bit Windows MSVC and 64-bit Windows.
1078                "i686-pc-windows-msvc" => vec![
1079                    "i686-pc-windows-gnu",
1080                    "x86_64-pc-windows-gnu",
1081                    "x86_64-pc-windows-msvc",
1082                ],
1083                // 64-bit Windows GNU/MSVC runs on the other.
1084                "x86_64-pc-windows-gnu" => vec!["x86_64-pc-windows-msvc"],
1085                "x86_64-pc-windows-msvc" => vec!["x86_64-pc-windows-gnu"],
1086                _ => vec![],
1087            }
1088            .iter()
1089            .map(|x| x.to_string()),
1090        );
1091
1092        res
1093    }
1094
1095    fn python_exe_path(&self) -> &Path {
1096        &self.python_exe
1097    }
1098
1099    fn python_version(&self) -> &str {
1100        &self.version
1101    }
1102
1103    fn python_major_minor_version(&self) -> String {
1104        parse_python_major_minor_version(&self.version)
1105    }
1106
1107    fn python_implementation(&self) -> &str {
1108        &self.python_implementation
1109    }
1110
1111    fn python_implementation_short(&self) -> &str {
1112        // TODO capture this in distribution metadata
1113        match self.python_implementation.as_str() {
1114            "cpython" => "cp",
1115            "python" => "py",
1116            "pypy" => "pp",
1117            "ironpython" => "ip",
1118            "jython" => "jy",
1119            s => panic!("unsupported Python implementation: {}", s),
1120        }
1121    }
1122
1123    fn python_tag(&self) -> &str {
1124        &self.python_tag
1125    }
1126
1127    fn python_abi_tag(&self) -> Option<&str> {
1128        match &self.python_abi_tag {
1129            Some(tag) => {
1130                if tag.is_empty() {
1131                    None
1132                } else {
1133                    Some(tag)
1134                }
1135            }
1136            None => None,
1137        }
1138    }
1139
1140    fn python_platform_tag(&self) -> &str {
1141        &self.python_platform_tag
1142    }
1143
1144    fn python_platform_compatibility_tag(&self) -> &str {
1145        // TODO capture this in distribution metadata.
1146        if !self.is_extension_module_file_loadable() {
1147            return "none";
1148        }
1149
1150        match self.python_platform_tag.as_str() {
1151            "linux-aarch64" => "manylinux2014_aarch64",
1152            "linux-x86_64" => "manylinux2014_x86_64",
1153            "linux-i686" => "manylinux2014_i686",
1154            "macosx-10.9-x86_64" => "macosx_10_9_x86_64",
1155            "macosx-11.0-arm64" => "macosx_11_0_arm64",
1156            "win-amd64" => "win_amd64",
1157            "win32" => "win32",
1158            p => panic!("unsupported Python platform: {}", p),
1159        }
1160    }
1161
1162    fn cache_tag(&self) -> &str {
1163        &self.cache_tag
1164    }
1165
1166    fn python_module_suffixes(&self) -> Result<PythonModuleSuffixes> {
1167        Ok(self.module_suffixes.clone())
1168    }
1169
1170    fn python_config_vars(&self) -> &HashMap<String, String> {
1171        &self.config_vars
1172    }
1173
1174    fn stdlib_test_packages(&self) -> Vec<String> {
1175        self.stdlib_test_packages.clone()
1176    }
1177
1178    fn apple_sdk_info(&self) -> Option<&AppleSdkInfo> {
1179        self.apple_sdk_info.as_ref()
1180    }
1181
1182    fn create_bytecode_compiler(
1183        &self,
1184        env: &Environment,
1185    ) -> Result<Box<dyn PythonBytecodeCompiler>> {
1186        let temp_dir = env.temporary_directory("pyoxidizer-bytecode-compiler")?;
1187
1188        Ok(Box::new(BytecodeCompiler::new(
1189            &self.python_exe,
1190            temp_dir.path(),
1191        )?))
1192    }
1193
1194    fn create_packaging_policy(&self) -> Result<PythonPackagingPolicy> {
1195        let mut policy = PythonPackagingPolicy::default();
1196
1197        // In-memory shared library loading is brittle. Disable this configuration
1198        // even if supported because it leads to pain.
1199        if self.supports_in_memory_shared_library_loading() {
1200            policy.set_resources_location(ConcreteResourceLocation::InMemory);
1201            policy.set_resources_location_fallback(Some(ConcreteResourceLocation::RelativePath(
1202                "lib".to_string(),
1203            )));
1204        }
1205
1206        for triple in LINUX_TARGET_TRIPLES.iter() {
1207            for ext in BROKEN_EXTENSIONS_LINUX.iter() {
1208                policy.register_broken_extension(triple, ext);
1209            }
1210        }
1211
1212        for triple in MACOS_TARGET_TRIPLES.iter() {
1213            for ext in BROKEN_EXTENSIONS_MACOS.iter() {
1214                policy.register_broken_extension(triple, ext);
1215            }
1216        }
1217
1218        for name in NO_BYTECODE_MODULES.iter() {
1219            policy.register_no_bytecode_module(name);
1220        }
1221
1222        Ok(policy)
1223    }
1224
1225    fn create_python_interpreter_config(&self) -> Result<PyembedPythonInterpreterConfig> {
1226        let embedded_default = PyembedPythonInterpreterConfig::default();
1227
1228        Ok(PyembedPythonInterpreterConfig {
1229            config: PythonInterpreterConfig {
1230                profile: PythonInterpreterProfile::Isolated,
1231                ..embedded_default.config
1232            },
1233            allocator_backend: default_memory_allocator(self.target_triple()),
1234            allocator_raw: true,
1235            oxidized_importer: true,
1236            filesystem_importer: false,
1237            terminfo_resolution: TerminfoResolution::Dynamic,
1238            ..embedded_default
1239        })
1240    }
1241
1242    fn as_python_executable_builder(
1243        &self,
1244        host_triple: &str,
1245        target_triple: &str,
1246        name: &str,
1247        libpython_link_mode: BinaryLibpythonLinkMode,
1248        policy: &PythonPackagingPolicy,
1249        config: &PyembedPythonInterpreterConfig,
1250        host_distribution: Option<Arc<dyn PythonDistribution>>,
1251    ) -> Result<Box<dyn PythonBinaryBuilder>> {
1252        // TODO can we avoid these clones?
1253        let target_distribution = Arc::new(self.clone());
1254        let host_distribution: Arc<dyn PythonDistribution> =
1255            host_distribution.unwrap_or_else(|| Arc::new(self.clone()));
1256
1257        let builder = StandalonePythonExecutableBuilder::from_distribution(
1258            host_distribution,
1259            target_distribution,
1260            host_triple.to_string(),
1261            target_triple.to_string(),
1262            name.to_string(),
1263            libpython_link_mode,
1264            policy.clone(),
1265            config.clone(),
1266        )?;
1267
1268        Ok(builder as Box<dyn PythonBinaryBuilder>)
1269    }
1270
1271    fn python_resources<'a>(&self) -> Vec<PythonResource<'a>> {
1272        let extension_modules = self
1273            .extension_modules
1274            .iter()
1275            .flat_map(|(_, exts)| exts.iter().map(|e| PythonResource::from(e.to_owned())));
1276
1277        let module_sources = self.py_modules.iter().map(|(name, path)| {
1278            PythonResource::from(PythonModuleSource {
1279                name: name.clone(),
1280                source: FileData::Path(path.clone()),
1281                is_package: is_package_from_path(path),
1282                cache_tag: self.cache_tag.clone(),
1283                is_stdlib: true,
1284                is_test: self.is_stdlib_test_package(name),
1285            })
1286        });
1287
1288        let resource_datas = self.resources.iter().flat_map(|(package, inner)| {
1289            inner.iter().map(move |(name, path)| {
1290                PythonResource::from(PythonPackageResource {
1291                    leaf_package: package.clone(),
1292                    relative_name: name.clone(),
1293                    data: FileData::Path(path.clone()),
1294                    is_stdlib: true,
1295                    is_test: self.is_stdlib_test_package(package),
1296                })
1297            })
1298        });
1299
1300        extension_modules
1301            .chain(module_sources)
1302            .chain(resource_datas)
1303            .collect::<Vec<PythonResource<'a>>>()
1304    }
1305
1306    /// Ensure pip is available to run in the distribution.
1307    fn ensure_pip(&self) -> Result<PathBuf> {
1308        let dist_prefix = self.base_dir.join("python").join("install");
1309        let python_paths = resolve_python_paths(&dist_prefix, &self.version);
1310
1311        let pip_path = python_paths.bin_dir.join(PIP_EXE_BASENAME);
1312
1313        if !pip_path.exists() {
1314            warn!("{} doesnt exist", pip_path.display().to_string());
1315            invoke_python(&python_paths, &["-m", "ensurepip"]);
1316        }
1317
1318        Ok(pip_path)
1319    }
1320
1321    fn resolve_distutils(
1322        &self,
1323        libpython_link_mode: LibpythonLinkMode,
1324        dest_dir: &Path,
1325        extra_python_paths: &[&Path],
1326    ) -> Result<HashMap<String, String>> {
1327        let mut res = match libpython_link_mode {
1328            // We need to patch distutils if the distribution is statically linked.
1329            LibpythonLinkMode::Static => prepare_hacked_distutils(
1330                &self.stdlib_path.join("distutils"),
1331                dest_dir,
1332                extra_python_paths,
1333            ),
1334            LibpythonLinkMode::Dynamic => Ok(HashMap::new()),
1335        }?;
1336
1337        // Modern versions of setuptools vendor their own copy of distutils
1338        // and use it by default. If we hacked distutils above, we need to ensure
1339        // that hacked copy is used. Even if we don't hack distutils, there is an
1340        // unknown change in behavior in a release after setuptools 63.2.0 causing
1341        // extension module building to fail due to missing Python.h. In older
1342        // versions the CFLAGS has an -I with the path to our standalone
1343        // distribution. But in modern versions it uses the `/install/include/pythonX.Y`
1344        // path from sysconfig with the proper prefixing. This bug was exposed when
1345        // we attempted to upgrade PBS distributions from 20220802 to 20221002.
1346        // We'll need to fix this before Python 3.12, which drops distutils from the
1347        // stdlib.
1348        //
1349        // The actual value of the environment variable doesn't matter as long as it
1350        // isn't "local". However, the setuptools docs suggest using "stdlib."
1351        res.insert("SETUPTOOLS_USE_DISTUTILS".to_string(), "stdlib".to_string());
1352
1353        Ok(res)
1354    }
1355
1356    /// Determines whether dynamically linked extension modules can be loaded from memory.
1357    fn supports_in_memory_shared_library_loading(&self) -> bool {
1358        // Loading from memory is only supported on Windows where symbols are
1359        // declspec(dllexport) and the distribution is capable of loading
1360        // shared library extensions.
1361        self.target_triple.contains("pc-windows")
1362            && self.python_symbol_visibility == "dllexport"
1363            && self
1364                .extension_module_loading
1365                .contains(&"shared-library".to_string())
1366    }
1367
1368    fn tcl_files(&self) -> Result<Vec<(PathBuf, FileEntry)>> {
1369        let mut res = vec![];
1370
1371        if let Some(root) = &self.tcl_library_path {
1372            if let Some(paths) = &self.tcl_library_paths {
1373                for subdir in paths {
1374                    for entry in walkdir::WalkDir::new(root.join(subdir))
1375                        .sort_by(|a, b| a.file_name().cmp(b.file_name()))
1376                        .into_iter()
1377                    {
1378                        let entry = entry?;
1379
1380                        let path = entry.path();
1381
1382                        if path.is_dir() {
1383                            continue;
1384                        }
1385
1386                        let rel_path = path.strip_prefix(root)?;
1387
1388                        res.push((rel_path.to_path_buf(), FileEntry::try_from(path)?));
1389                    }
1390                }
1391            }
1392        }
1393
1394        Ok(res)
1395    }
1396
1397    fn tcl_library_path_directory(&self) -> Option<String> {
1398        // TODO this should probably be exposed from the JSON metadata.
1399        Some("tcl8.6".to_string())
1400    }
1401}
1402
1403#[cfg(test)]
1404pub mod tests {
1405    use {
1406        super::*,
1407        crate::testutil::*,
1408        python_packaging::{
1409            bytecode::CompileMode, policy::ExtensionModuleFilter,
1410            resource::BytecodeOptimizationLevel,
1411        },
1412        std::collections::BTreeSet,
1413    };
1414
1415    #[test]
1416    fn test_stdlib_annotations() -> Result<()> {
1417        let distribution = get_default_distribution(None)?;
1418
1419        for resource in distribution.python_resources() {
1420            match resource {
1421                PythonResource::ModuleSource(module) => {
1422                    assert!(module.is_stdlib);
1423
1424                    if module.name.starts_with("test") {
1425                        assert!(module.is_test);
1426                    }
1427                }
1428                PythonResource::PackageResource(r) => {
1429                    assert!(r.is_stdlib);
1430                    if r.leaf_package.starts_with("test") {
1431                        assert!(r.is_test);
1432                    }
1433                }
1434                _ => (),
1435            }
1436        }
1437
1438        Ok(())
1439    }
1440
1441    #[test]
1442    fn test_tcl_files() -> Result<()> {
1443        for dist in get_all_standalone_distributions()? {
1444            let tcl_files = dist.tcl_files()?;
1445
1446            if dist.target_triple().contains("pc-windows")
1447                && !dist.is_extension_module_file_loadable()
1448            {
1449                assert!(tcl_files.is_empty());
1450            } else {
1451                assert!(!tcl_files.is_empty());
1452            }
1453        }
1454
1455        Ok(())
1456    }
1457
1458    #[test]
1459    fn test_extension_module_copyleft_filtering() -> Result<()> {
1460        for dist in get_all_standalone_distributions()? {
1461            let mut policy = dist.create_packaging_policy()?;
1462            policy.set_extension_module_filter(ExtensionModuleFilter::All);
1463
1464            let all_extensions = policy
1465                .resolve_python_extension_modules(
1466                    dist.extension_modules.values(),
1467                    &dist.target_triple,
1468                )?
1469                .into_iter()
1470                .map(|e| (e.name, e.variant))
1471                .collect::<BTreeSet<_>>();
1472
1473            policy.set_extension_module_filter(ExtensionModuleFilter::NoCopyleft);
1474
1475            let no_copyleft_extensions = policy
1476                .resolve_python_extension_modules(
1477                    dist.extension_modules.values(),
1478                    &dist.target_triple,
1479                )?
1480                .into_iter()
1481                .map(|e| (e.name, e.variant))
1482                .collect::<BTreeSet<_>>();
1483
1484            let dropped = all_extensions
1485                .difference(&no_copyleft_extensions)
1486                .cloned()
1487                .collect::<Vec<_>>();
1488
1489            let added = no_copyleft_extensions
1490                .difference(&all_extensions)
1491                .cloned()
1492                .collect::<Vec<_>>();
1493
1494            // 3.10 distributions stopped shipping GPL licensed extensions.
1495            let (linux_dropped, linux_added) =
1496                if ["3.8", "3.9"].contains(&dist.python_major_minor_version().as_str()) {
1497                    (
1498                        vec![
1499                            ("_gdbm".to_string(), Some("default".to_string())),
1500                            ("readline".to_string(), Some("default".to_string())),
1501                        ],
1502                        vec![("readline".to_string(), Some("libedit".to_string()))],
1503                    )
1504                } else {
1505                    (vec![], vec![])
1506                };
1507
1508            let (wanted_dropped, wanted_added) = match (
1509                dist.python_major_minor_version().as_str(),
1510                dist.target_triple(),
1511            ) {
1512                (_, "aarch64-unknown-linux-gnu") => (linux_dropped.clone(), linux_added.clone()),
1513                (_, "x86_64-unknown-linux-gnu") => (linux_dropped.clone(), linux_added.clone()),
1514                (_, "x86_64_v2-unknown-linux-gnu") => (linux_dropped.clone(), linux_added.clone()),
1515                (_, "x86_64_v3-unknown-linux-gnu") => (linux_dropped.clone(), linux_added.clone()),
1516                (_, "x86_64-unknown-linux-musl") => (linux_dropped.clone(), linux_added.clone()),
1517                (_, "x86_64_v2-unknown-linux-musl") => (linux_dropped.clone(), linux_added.clone()),
1518                (_, "x86_64_v3-unknown-linux-musl") => (linux_dropped.clone(), linux_added.clone()),
1519                (_, "i686-pc-windows-msvc") => (vec![], vec![]),
1520                (_, "x86_64-pc-windows-msvc") => (vec![], vec![]),
1521                (_, "aarch64-apple-darwin") => (vec![], vec![]),
1522                (_, "x86_64-apple-darwin") => (vec![], vec![]),
1523                _ => (vec![], vec![]),
1524            };
1525
1526            assert_eq!(
1527                dropped,
1528                wanted_dropped,
1529                "dropped matches for {} {}",
1530                dist.python_major_minor_version(),
1531                dist.target_triple(),
1532            );
1533            assert_eq!(
1534                added,
1535                wanted_added,
1536                "added matches for {} {}",
1537                dist.python_major_minor_version(),
1538                dist.target_triple()
1539            );
1540        }
1541
1542        Ok(())
1543    }
1544
1545    #[test]
1546    fn compile_syntax_error() -> Result<()> {
1547        let env = get_env()?;
1548        let dist = get_default_distribution(None)?;
1549
1550        let temp_dir = env.temporary_directory("pyoxidizer-test")?;
1551
1552        let mut compiler = BytecodeCompiler::new(dist.python_exe_path(), temp_dir.path())?;
1553        let res = compiler.compile(
1554            b"invalid syntax",
1555            "foo.py",
1556            BytecodeOptimizationLevel::Zero,
1557            CompileMode::Bytecode,
1558        );
1559        assert!(res.is_err());
1560        let err = res.err().unwrap();
1561        assert!(err
1562            .to_string()
1563            .starts_with("compiling error: invalid syntax"));
1564
1565        temp_dir.close()?;
1566
1567        Ok(())
1568    }
1569
1570    #[test]
1571    fn apple_sdk_info() -> Result<()> {
1572        for dist in get_all_standalone_distributions()? {
1573            if dist.target_triple().contains("-apple-") {
1574                assert!(dist.apple_sdk_info().is_some());
1575            } else {
1576                assert!(dist.apple_sdk_info().is_none());
1577            }
1578        }
1579
1580        Ok(())
1581    }
1582
1583    #[test]
1584    fn test_parse_python_major_minor_version() {
1585        let version_expectations = [
1586            ("3.7.1", "3.7"),
1587            ("3.10.1", "3.10"),
1588            ("1.2.3.4.5", "1.2"),
1589            ("1", "1.0"),
1590        ];
1591        for (version, expected) in version_expectations {
1592            assert_eq!(parse_python_major_minor_version(version), expected);
1593        }
1594    }
1595
1596    #[test]
1597    fn test_resolve_python_paths_site_packages() -> Result<()> {
1598        let python_paths = resolve_python_paths(Path::new("/test/dir"), "3.10.4");
1599        assert_eq!(
1600            python_paths
1601                .site_packages
1602                .to_str()
1603                .unwrap()
1604                .replace('\\', "/"),
1605            "/test/dir/lib/python3.10/site-packages"
1606        );
1607        let python_paths = resolve_python_paths(Path::new("/test/dir"), "3.9.1");
1608        assert_eq!(
1609            python_paths
1610                .site_packages
1611                .to_str()
1612                .unwrap()
1613                .replace('\\', "/"),
1614            "/test/dir/lib/python3.9/site-packages"
1615        );
1616        Ok(())
1617    }
1618}