pyoxidizer 0.24.0

Package self-contained Python applications
Documentation
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

/*!
Defining and manipulating binaries embedding Python.
*/

use {
    crate::{
        environment::Environment,
        py_packaging::{distribution::AppleSdkInfo, embedding::EmbeddedPythonContext},
    },
    anyhow::Result,
    python_packaging::{
        licensing::{LicensedComponent, LicensedComponents},
        policy::PythonPackagingPolicy,
        resource::{
            PythonExtensionModule, PythonModuleSource, PythonPackageDistributionResource,
            PythonPackageResource, PythonResource,
        },
        resource_collection::{
            AddResourceAction, PrePackagedResource, PythonResourceAddCollectionContext,
        },
    },
    simple_file_manifest::File,
    std::{collections::HashMap, path::Path, sync::Arc},
    tugger_windows::VcRedistributablePlatform,
};

/// How a binary should link against libpython.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LibpythonLinkMode {
    /// Libpython will be statically linked into the binary.
    Static,
    /// The binary will dynamically link against libpython.
    Dynamic,
}

/// Determines how packed resources are loaded by the generated binary.
///
/// This effectively controls how resources file are written to disk
/// and what `pyembed::PackedResourcesSource` will get serialized in the
/// configuration.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PackedResourcesLoadMode {
    /// Packed resources will not be loaded.
    None,

    /// Resources data will be embedded in the binary.
    ///
    /// The data will be referenced via an `include_bytes!()` and the
    /// stored path controls the name of the file that will be materialized
    /// in the artifacts directory.
    EmbeddedInBinary(String),

    /// Resources data will be serialized to a file relative to the built binary.
    ///
    /// The configuration will reference the file via a relative path using
    /// `$ORIGIN` expansion. Memory mapped I/O will be used to read the file.
    BinaryRelativePathMemoryMapped(String),
}

impl ToString for PackedResourcesLoadMode {
    fn to_string(&self) -> String {
        match self {
            Self::None => "none".to_string(),
            Self::EmbeddedInBinary(filename) => format!("embedded:{}", filename),
            Self::BinaryRelativePathMemoryMapped(path) => {
                format!("binary-relative-memory-mapped:{}", path)
            }
        }
    }
}

impl TryFrom<&str> for PackedResourcesLoadMode {
    type Error = String;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        if value == "none" {
            Ok(Self::None)
        } else {
            let parts = value.splitn(2, ':').collect::<Vec<_>>();
            if parts.len() != 2 {
                Err(
                    "resources load mode value not recognized; must have form `type:value`"
                        .to_string(),
                )
            } else {
                let prefix = parts[0];
                let value = parts[1];

                match prefix {
                    "embedded" => {
                        Ok(Self::EmbeddedInBinary(value.to_string()))
                    }
                    "binary-relative-memory-mapped" => {
                        Ok(Self::BinaryRelativePathMemoryMapped(value.to_string()))
                    }
                    _ => Err(format!("{} is not a valid prefix; must be 'embedded' or 'binary-relative-memory-mapped'", prefix))
                }
            }
        }
    }
}

/// Describes how Windows Runtime DLLs (e.g. vcruntime140.dll) should be handled during builds.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum WindowsRuntimeDllsMode {
    /// Never attempt to install Windows Runtime DLLs.
    ///
    /// A binary will be generated with no runtime DLLs next to it.
    Never,

    /// Install Windows Runtime DLLs if they can be located. Do nothing if not.
    WhenPresent,

    /// Always install Windows Runtime DLLs and fail if they can't be found.
    Always,
}

impl ToString for WindowsRuntimeDllsMode {
    fn to_string(&self) -> String {
        match self {
            Self::Never => "never",
            Self::WhenPresent => "when-present",
            Self::Always => "always",
        }
        .to_string()
    }
}

impl TryFrom<&str> for WindowsRuntimeDllsMode {
    type Error = String;

    fn try_from(value: &str) -> Result<Self, Self::Error> {
        match value {
            "never" => Ok(Self::Never),
            "when-present" => Ok(Self::WhenPresent),
            "always" => Ok(Self::Always),
            _ => Err(format!("{} is not a valid mode; must be 'never'", value)),
        }
    }
}

/// A callable that can influence PythonResourceAddCollectionContext.
pub type ResourceAddCollectionContextCallback<'a> = Box<
    dyn Fn(
            &PythonPackagingPolicy,
            &PythonResource,
            &mut PythonResourceAddCollectionContext,
        ) -> Result<()>
        + 'a,
>;

/// Describes a generic way to build a Python binary.
///
/// Binary here means an executable or library containing or linking to a
/// Python interpreter. It also includes embeddable resources within that
/// binary.
///
/// Concrete implementations can be turned into build artifacts or binaries
/// themselves.
pub trait PythonBinaryBuilder {
    /// Clone self into a Box'ed trait object.
    fn clone_trait(&self) -> Arc<dyn PythonBinaryBuilder>;

    /// The name of the binary.
    fn name(&self) -> String;

    /// How the binary will link against libpython.
    fn libpython_link_mode(&self) -> LibpythonLinkMode;

    /// Rust target triple the binary will run on.
    fn target_triple(&self) -> &str;

    /// Obtain run-time requirements for the Visual C++ Redistributable.
    ///
    /// If `None`, there is no dependency on `vcruntimeXXX.dll` files. If `Some`,
    /// the returned tuple declares the VC++ Redistributable major version string
    /// (e.g. `14`) and the VC++ Redistributable platform variant that is required.
    fn vc_runtime_requirements(&self) -> Option<(String, VcRedistributablePlatform)>;

    /// Obtain the cache tag to apply to Python bytecode modules.
    fn cache_tag(&self) -> &str;

    /// Obtain the `PythonPackagingPolicy` for the builder.
    fn python_packaging_policy(&self) -> &PythonPackagingPolicy;

    /// Path to Python executable that can be used to derive info at build time.
    ///
    /// The produced binary is effectively a clone of the Python distribution behind the
    /// returned executable.
    fn host_python_exe_path(&self) -> &Path;

    /// Path to Python executable that is native to the target architecture.
    // TODO this should not need to exist if we properly supported cross-compiling.
    fn target_python_exe_path(&self) -> &Path;

    /// Apple SDK build/targeting information.
    fn apple_sdk_info(&self) -> Option<&AppleSdkInfo>;

    /// Obtain how Windows runtime DLLs will be handled during builds.
    ///
    /// See the enum's documentation for behavior.
    ///
    /// This setting is ignored for binaries that don't need the Windows runtime
    /// DLLs.
    fn windows_runtime_dlls_mode(&self) -> &WindowsRuntimeDllsMode;

    /// Set the value for `windows_runtime_dlls_mode()`.
    fn set_windows_runtime_dlls_mode(&mut self, value: WindowsRuntimeDllsMode);

    /// The directory to install tcl/tk files into.
    fn tcl_files_path(&self) -> &Option<String>;

    /// Set the directory to install tcl/tk files into.
    fn set_tcl_files_path(&mut self, value: Option<String>);

    /// The value of the `windows_subsystem` Rust attribute for the generated Rust project.
    fn windows_subsystem(&self) -> &str;

    /// Set the value of the `windows_subsystem` Rust attribute for generated Rust projects.
    fn set_windows_subsystem(&mut self, value: &str) -> Result<()>;

    /// Obtain the path of a filename to write containing a licensing report.
    fn licenses_filename(&self) -> Option<&str>;

    /// Set the path of a filename to write containing a licensing report.
    fn set_licenses_filename(&mut self, value: Option<String>);

    /// How packed Python resources will be loaded by the binary.
    fn packed_resources_load_mode(&self) -> &PackedResourcesLoadMode;

    /// Set how packed Python resources will be loaded by the binary.
    fn set_packed_resources_load_mode(&mut self, load_mode: PackedResourcesLoadMode);

    /// Obtain an iterator over all resource entries that will be embedded in the binary.
    ///
    /// This likely does not return extension modules that are statically linked
    /// into the binary. For those, see `builtin_extension_module_names()`.
    fn iter_resources<'a>(
        &'a self,
    ) -> Box<dyn Iterator<Item = (&'a String, &'a PrePackagedResource)> + 'a>;

    /// Resolve license metadata from an iterable of `PythonResource` and store that data.
    ///
    /// The resolved license data can later be used to ensure packages conform
    /// to license restrictions. This method can safely be called on resources
    /// that aren't added to the instance / resource collector: it simply
    /// registers the license metadata so it can be consulted later.
    fn index_package_license_info_from_resources<'a>(
        &mut self,
        resources: &[PythonResource<'a>],
    ) -> Result<()>;

    /// Runs `pip download` using the binary builder's settings.
    ///
    /// Returns resources discovered from the Python packages downloaded.
    fn pip_download(
        &mut self,
        env: &Environment,
        verbose: bool,
        args: &[String],
    ) -> Result<Vec<PythonResource>>;

    /// Runs `pip install` using the binary builder's settings.
    ///
    /// Returns resources discovered as part of performing an install.
    fn pip_install(
        &mut self,
        env: &Environment,
        verbose: bool,
        install_args: &[String],
        extra_envs: &HashMap<String, String>,
    ) -> Result<Vec<PythonResource>>;

    /// Reads Python resources from the filesystem.
    fn read_package_root(
        &mut self,
        path: &Path,
        packages: &[String],
    ) -> Result<Vec<PythonResource>>;

    /// Read Python resources from a populated virtualenv directory.
    fn read_virtualenv(&mut self, path: &Path) -> Result<Vec<PythonResource>>;

    /// Runs `python setup.py install` using the binary builder's settings.
    ///
    /// Returns resources discovered as part of performing an install.
    fn setup_py_install(
        &mut self,
        env: &Environment,
        package_path: &Path,
        verbose: bool,
        extra_envs: &HashMap<String, String>,
        extra_global_arguments: &[String],
    ) -> Result<Vec<PythonResource>>;

    /// Add resources from the Python distribution to the builder.
    ///
    /// This method should likely be called soon after object construction
    /// in order to finish adding state from the Python distribution to the
    /// builder.
    ///
    /// The boundary between what distribution state should be initialized
    /// at binary construction time versus this method is not well-defined
    /// and is up to implementations. However, it is strongly recommended for
    /// the division to be handling of core/required interpreter state at
    /// construction time and all optional/standard library state in this
    /// method.
    ///
    /// `callback` defines an optional function which can be called between
    /// resource creation and adding that resource to the builder. This
    /// gives the caller an opportunity to influence how resources are added
    /// to the binary builder.
    fn add_distribution_resources(
        &mut self,
        callback: Option<ResourceAddCollectionContextCallback>,
    ) -> Result<Vec<AddResourceAction>>;

    /// Add a `PythonModuleSource` to the resources collection.
    ///
    /// The location to load the resource from is optional. If specified, it
    /// will be used. If not, an appropriate location based on the resources
    /// policy will be chosen.
    fn add_python_module_source(
        &mut self,
        module: &PythonModuleSource,
        add_context: Option<PythonResourceAddCollectionContext>,
    ) -> Result<Vec<AddResourceAction>>;

    /// Add a `PythonPackageResource` to the resources collection.
    ///
    /// The location to load the resource from is optional. If specified, it will
    /// be used. If not, an appropriate location based on the resources policy
    /// will be chosen.
    fn add_python_package_resource(
        &mut self,
        resource: &PythonPackageResource,
        add_context: Option<PythonResourceAddCollectionContext>,
    ) -> Result<Vec<AddResourceAction>>;

    /// Add a `PythonPackageDistributionResource` to the resources collection.
    ///
    /// The location to load the resource from is optional. If specified, it will
    /// be used. If not, an appropriate location based on the resources policy
    /// will be chosen.
    fn add_python_package_distribution_resource(
        &mut self,
        resource: &PythonPackageDistributionResource,
        add_context: Option<PythonResourceAddCollectionContext>,
    ) -> Result<Vec<AddResourceAction>>;

    /// Add a `PythonExtensionModule` to make available.
    ///
    /// The location to load the extension module from can be specified. However,
    /// different builders have different capabilities. And the location may be
    /// ignored in some cases. For example, when adding an extension module that
    /// is compiled into libpython itself, the location will always be inside
    /// libpython and it isn't possible to materialize the extension module as
    /// a standalone file.
    fn add_python_extension_module(
        &mut self,
        extension_module: &PythonExtensionModule,
        add_context: Option<PythonResourceAddCollectionContext>,
    ) -> Result<Vec<AddResourceAction>>;

    /// Add a `File` to the resource collection.
    fn add_file_data(
        &mut self,
        file: &File,
        add_context: Option<PythonResourceAddCollectionContext>,
    ) -> Result<Vec<AddResourceAction>>;

    /// Filter embedded resources against names in files.
    ///
    /// `files` is files to read names from.
    ///
    /// `glob_patterns` is file patterns of files to read names from.
    fn filter_resources_from_files(
        &mut self,
        files: &[&Path],
        glob_patterns: &[&str],
    ) -> Result<()>;

    /// Whether the binary requires the jemalloc library.
    fn requires_jemalloc(&self) -> bool;

    /// Whether the binary requires the Mimalloc library.
    fn requires_mimalloc(&self) -> bool;

    /// Whether the binary requires the Snmalloc library.
    fn requires_snmalloc(&self) -> bool;

    /// Obtain software licensing information.
    fn licensed_components(&self) -> Result<LicensedComponents>;

    /// Add a licensed software component to the instance.
    ///
    /// Calling this effectively conveys that the software will be built into
    /// the final binary and its licensing should be captured in order to
    /// generate a licensing report.
    fn add_licensed_component(&mut self, component: LicensedComponent) -> Result<()>;

    /// Obtain an `EmbeddedPythonContext` instance from this one.
    fn to_embedded_python_context(
        &self,
        env: &Environment,
        opt_level: &str,
    ) -> Result<EmbeddedPythonContext>;
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_resources_load_mode_serialization() {
        assert_eq!(
            PackedResourcesLoadMode::None.to_string(),
            "none".to_string()
        );
        assert_eq!(
            PackedResourcesLoadMode::EmbeddedInBinary("resources".into()).to_string(),
            "embedded:resources".to_string()
        );
        assert_eq!(
            PackedResourcesLoadMode::BinaryRelativePathMemoryMapped("relative-resources".into())
                .to_string(),
            "binary-relative-memory-mapped:relative-resources".to_string()
        );
    }

    #[test]
    fn test_resources_load_mode_parsing() -> Result<()> {
        assert_eq!(
            PackedResourcesLoadMode::try_from("none").unwrap(),
            PackedResourcesLoadMode::None
        );
        assert_eq!(
            PackedResourcesLoadMode::try_from("embedded:resources").unwrap(),
            PackedResourcesLoadMode::EmbeddedInBinary("resources".into())
        );
        assert_eq!(
            PackedResourcesLoadMode::try_from("binary-relative-memory-mapped:relative").unwrap(),
            PackedResourcesLoadMode::BinaryRelativePathMemoryMapped("relative".into())
        );

        Ok(())
    }
}