diff --git a/pyoxidizer/docs/pyoxidizer_config_type_python_distribution.rst b/pyoxidizer/docs/pyoxidizer_config_type_python_distribution.rst index 79f1fca77..4d5b4b7cb 100644 --- a/pyoxidizer/docs/pyoxidizer_config_type_python_distribution.rst +++ b/pyoxidizer/docs/pyoxidizer_config_type_python_distribution.rst @@ -84,7 +84,7 @@ The policy automatically uses settings globally appropriate for the distribution. - .. py:method:: to_python_executable(name: str, packaging_policy: PythonPackagingPolicy, config: PythonInterpreterConfig) -> PythonExecutable + .. py:method:: to_python_executable(name: str, packaging_policy: PythonPackagingPolicy, config: PythonInterpreterConfig, runtime: str) -> PythonExecutable This method constructs a :py:class:`PythonExecutable` instance. It essentially says *build an executable embedding Python from this @@ -107,6 +107,12 @@ Default is what :py:meth:`make_python_interpreter_config` returns. + ``runtime`` + The path to a pyoxidizer Rust project that will invoke pyembed. + See :ref:`rust_projects` for more on the expected composition of Rust projects. + + If not supplied, a temporary Rust project will be created. + .. important:: Libraries that extension modules link against have various software diff --git a/pyoxidizer/docs/pyoxidizer_rust_projects.rst b/pyoxidizer/docs/pyoxidizer_rust_projects.rst index 7101a23c1..ee2eae7a4 100644 --- a/pyoxidizer/docs/pyoxidizer_rust_projects.rst +++ b/pyoxidizer/docs/pyoxidizer_rust_projects.rst @@ -67,13 +67,14 @@ to ``exit()``. Succinctly, we instantiate and run an embedded Python interpreter. That's our executable. The ``pyoxidizer.bzl`` is our auto-generated -:ref:`PyOxidizer configuration file `. +:ref:`PyOxidizer configuration file ` with an additional `runtime` option set to the current directory. +This specifies that we want this project to be used as the pyembed runner instead of a temporary one. Crate Features ============== The auto-generated Rust project defines a number of features to control -behavior. These are documented in the sections below. +behavior when building via cargo. These are documented in the sections below. ``build-mode-standalone`` ------------------------- @@ -127,7 +128,7 @@ name via the ``PYOXIDIZER_BUILD_TARGET`` environment variable. e.g.:: This mode tells the build script to reuse artifacts that were already built. (Perhaps you called ``pyoxidizer build`` or ``pyoxidizer run-build-script`` -outside the context of a normal ``cargo build``.) +outside the context of a normal ``cargo build``.) This is what is used when pyoxidizer invokes cargo as part of ``pyoxidizer build``. In this mode, the build script will look for artifacts in the directory specified by ``PYOXIDIZER_ARTIFACT_DIR`` if set, falling back to ``OUT_DIR``. diff --git a/pyoxidizer/src/project_building.rs b/pyoxidizer/src/project_building.rs index 57a04fb6c..c8e797dd0 100644 --- a/pyoxidizer/src/project_building.rs +++ b/pyoxidizer/src/project_building.rs @@ -427,46 +427,73 @@ pub fn build_python_executable<'a>( .context("resolving Rust toolchain")? .cargo_exe; - let temp_dir = env.temporary_directory("pyoxidizer")?; - - // Directory needs to have name of project. - let project_path = temp_dir.path().join(bin_name); - let build_path = temp_dir.path().join("build"); - let artifacts_path = temp_dir.path().join("artifacts"); - - initialize_project( - &env.pyoxidizer_source, - &project_path, - &cargo_exe, - None, - &[], - exe.windows_subsystem(), - ) - .context("initializing project")?; - - let mut build = build_executable_with_rust_project( - env, - &project_path, - bin_name, - exe, - &build_path, - &artifacts_path, - target_triple, - opt_level, - release, - // Always build with locked because we crated a Cargo.lock with the - // Rust project we just created. - true, - // Don't include license for self because the Rust project is temporary and its - // licensing isn't material. - false, - ) - .context("building executable with Rust project")?; - - // Blank out the path since it is in the temporary directory. - build.exe_path = None; - - temp_dir.close().context("closing temporary directory")?; + let build = match exe.runtime_path() { + Some(path) => { + let project_path = + canonicalize_path(&Path::new(path)).context("getting runtime path")?; + let build_path = project_path.join("build"); + let artifacts_path = build_path.join("artifacts"); + build_executable_with_rust_project( + env, + &project_path, + bin_name, + exe, + &build_path, + &artifacts_path, + target_triple, + opt_level, + release, + false, + true, + ) + .context("building executable with persistent Rust project")? + } + None => { + let temp_dir = env.temporary_directory("pyoxidizer")?; + + // Directory needs to have name of project. + let project_path = temp_dir.path().join(bin_name); + let build_path = temp_dir.path().join("build"); + let artifacts_path = temp_dir.path().join("artifacts"); + + initialize_project( + &env.pyoxidizer_source, + &project_path, + &cargo_exe, + None, + &[], + exe.windows_subsystem(), + false, + ) + .context("initializing project")?; + + let mut build = build_executable_with_rust_project( + env, + &project_path, + bin_name, + exe, + &build_path, + &artifacts_path, + target_triple, + opt_level, + release, + // Always build with locked because we crated a Cargo.lock with the + // Rust project we just created. + true, + // Don't include license for self because the Rust project is temporary and its + // licensing isn't material. + false, + ) + .context("building executable with generated Rust project")?; + + // Blank out the path since it is in the temporary directory. + build.exe_path = None; + + temp_dir.close().context("closing temporary directory")?; + + build + } + }; Ok(build) } diff --git a/pyoxidizer/src/project_layout.rs b/pyoxidizer/src/project_layout.rs index 8411d2f50..2b7f81bbd 100644 --- a/pyoxidizer/src/project_layout.rs +++ b/pyoxidizer/src/project_layout.rs @@ -90,6 +90,7 @@ struct TemplateData { program_name: Option, code: Option, pip_install_simple: Vec, + rust_init: bool, } impl TemplateData { @@ -105,6 +106,7 @@ impl TemplateData { program_name: None, code: None, pip_install_simple: Vec::new(), + rust_init: false, } } } @@ -285,12 +287,14 @@ pub fn write_new_pyoxidizer_config_file( name: &str, code: Option<&str>, pip_install: &[&str], + rust_init: bool, ) -> Result<()> { let path = project_dir.join("pyoxidizer.bzl"); let mut data = TemplateData::new(); populate_template_data(source, &mut data); data.program_name = Some(name.to_string()); + data.rust_init = rust_init; if let Some(code) = code { // Replace " with \" to work around @@ -421,6 +425,7 @@ pub fn initialize_project( code: Option<&str>, pip_install: &[&str], windows_subsystem: &str, + rust_init: bool, ) -> Result<()> { let status = std::process::Command::new(cargo_exe) .arg("init") @@ -443,7 +448,7 @@ pub fn initialize_project( write_new_build_rs(&path.join("build.rs"), name).context("writing build.rs")?; write_new_main_rs(&path.join("src").join("main.rs"), windows_subsystem) .context("writing main.rs")?; - write_new_pyoxidizer_config_file(source, &path, name, code, pip_install) + write_new_pyoxidizer_config_file(source, &path, name, code, pip_install, rust_init) .context("writing PyOxidizer config file")?; write_application_manifest(&path, name).context("writing application manifest")?; diff --git a/pyoxidizer/src/projectmgmt.rs b/pyoxidizer/src/projectmgmt.rs index b66d9e347..c08eed6e9 100644 --- a/pyoxidizer/src/projectmgmt.rs +++ b/pyoxidizer/src/projectmgmt.rs @@ -315,7 +315,7 @@ pub fn init_config_file( let name = project_dir.iter().last().unwrap().to_str().unwrap(); - write_new_pyoxidizer_config_file(source, project_dir, name, code, pip_install)?; + write_new_pyoxidizer_config_file(source, project_dir, name, code, pip_install, false)?; println!(); println!("A new PyOxidizer configuration file has been created."); @@ -347,6 +347,7 @@ pub fn init_rust_project(env: &Environment, project_path: &Path) -> Result<()> { None, &[], "console", + true, )?; println!(); println!( @@ -617,6 +618,7 @@ pub fn generate_python_embedding_artifacts( BinaryLibpythonLinkMode::Default, &policy, &interpreter_config, + None, Some(host_dist.clone_trait()), )?; diff --git a/pyoxidizer/src/py_packaging/binary.rs b/pyoxidizer/src/py_packaging/binary.rs index fdf2a8513..1d637f199 100644 --- a/pyoxidizer/src/py_packaging/binary.rs +++ b/pyoxidizer/src/py_packaging/binary.rs @@ -197,6 +197,9 @@ pub trait PythonBinaryBuilder { // TODO this should not need to exist if we properly supported cross-compiling. fn target_python_exe_path(&self) -> &Path; + /// The directory to install tcl/tk files into. + fn runtime_path(&self) -> &Option; + /// Apple SDK build/targeting information. fn apple_sdk_info(&self) -> Option<&AppleSdkInfo>; diff --git a/pyoxidizer/src/py_packaging/config.rs b/pyoxidizer/src/py_packaging/config.rs index 1501fd591..900b94000 100644 --- a/pyoxidizer/src/py_packaging/config.rs +++ b/pyoxidizer/src/py_packaging/config.rs @@ -615,6 +615,7 @@ mod tests { &policy, &config, None, + None, )?; crate::project_building::build_python_executable( diff --git a/pyoxidizer/src/py_packaging/distribution.rs b/pyoxidizer/src/py_packaging/distribution.rs index 96ad6612e..4b33ea438 100644 --- a/pyoxidizer/src/py_packaging/distribution.rs +++ b/pyoxidizer/src/py_packaging/distribution.rs @@ -185,6 +185,7 @@ pub trait PythonDistribution { libpython_link_mode: BinaryLibpythonLinkMode, policy: &PythonPackagingPolicy, config: &PyembedPythonInterpreterConfig, + runtime_path: Option, host_distribution: Option>, ) -> Result>; diff --git a/pyoxidizer/src/py_packaging/standalone_builder.rs b/pyoxidizer/src/py_packaging/standalone_builder.rs index 4b937494c..07ef16d7b 100644 --- a/pyoxidizer/src/py_packaging/standalone_builder.rs +++ b/pyoxidizer/src/py_packaging/standalone_builder.rs @@ -130,6 +130,9 @@ pub struct StandalonePythonExecutableBuilder { /// Path to install tcl/tk files into. tcl_files_path: Option, + /// Path to the Rust project that will invoke our embedded python interpreter. + runtime_path: Option, + /// Describes how Windows runtime DLLs should be handled during builds. windows_runtime_dlls_mode: WindowsRuntimeDllsMode, } @@ -145,6 +148,7 @@ impl StandalonePythonExecutableBuilder { link_mode: BinaryLibpythonLinkMode, packaging_policy: PythonPackagingPolicy, config: PyembedPythonInterpreterConfig, + runtime_path: Option, ) -> Result> { let host_python_exe = host_distribution.python_exe_path().to_path_buf(); @@ -230,6 +234,7 @@ impl StandalonePythonExecutableBuilder { licenses_filename: Some("COPYING.txt".into()), windows_subsystem: "console".to_string(), tcl_files_path: None, + runtime_path, windows_runtime_dlls_mode: WindowsRuntimeDllsMode::WhenPresent, }); @@ -464,6 +469,10 @@ impl PythonBinaryBuilder for StandalonePythonExecutableBuilder { self.target_distribution.python_exe_path() } + fn runtime_path(&self) -> &Option { + &self.runtime_path + } + fn apple_sdk_info(&self) -> Option<&AppleSdkInfo> { self.target_distribution.apple_sdk_info() } @@ -1211,6 +1220,7 @@ pub mod tests { self.libpython_link_mode.clone(), policy, self.config.clone(), + None, )?; builder.add_distribution_resources(None)?; @@ -3034,6 +3044,7 @@ pub mod tests { BinaryLibpythonLinkMode::Default, dist.create_packaging_policy()?, dist.create_python_interpreter_config()?, + None, )?; let reqs = builder.vc_runtime_requirements(); @@ -3070,6 +3081,7 @@ pub mod tests { BinaryLibpythonLinkMode::Default, dist.create_packaging_policy()?, dist.create_python_interpreter_config()?, + None, )?; // In Never mode, the set of extra files should always be empty. @@ -3154,6 +3166,7 @@ pub mod tests { BinaryLibpythonLinkMode::Default, policy, dist.create_python_interpreter_config()?, + None, )?; builder.add_distribution_resources(None)?; diff --git a/pyoxidizer/src/py_packaging/standalone_distribution.rs b/pyoxidizer/src/py_packaging/standalone_distribution.rs index de0425f8b..830fb5b0d 100644 --- a/pyoxidizer/src/py_packaging/standalone_distribution.rs +++ b/pyoxidizer/src/py_packaging/standalone_distribution.rs @@ -1247,6 +1247,7 @@ impl PythonDistribution for StandaloneDistribution { libpython_link_mode: BinaryLibpythonLinkMode, policy: &PythonPackagingPolicy, config: &PyembedPythonInterpreterConfig, + runtime_path: Option, host_distribution: Option>, ) -> Result> { // TODO can we avoid these clones? @@ -1263,6 +1264,7 @@ impl PythonDistribution for StandaloneDistribution { libpython_link_mode, policy.clone(), config.clone(), + runtime_path, )?; Ok(builder as Box) diff --git a/pyoxidizer/src/starlark/python_distribution.rs b/pyoxidizer/src/starlark/python_distribution.rs index db34613f9..0dd7af003 100644 --- a/pyoxidizer/src/starlark/python_distribution.rs +++ b/pyoxidizer/src/starlark/python_distribution.rs @@ -228,6 +228,7 @@ impl PythonDistributionValue { /// name, /// packaging_policy=None, /// config=None, + /// runtime=None, /// ) #[allow(clippy::too_many_arguments, clippy::wrong_self_convention)] fn to_python_executable_starlark( @@ -237,6 +238,7 @@ impl PythonDistributionValue { name: String, packaging_policy: &Value, config: &Value, + runtime: &Value, ) -> ValueResult { const LABEL: &str = "PythonDistribution.to_python_executable()"; @@ -246,6 +248,7 @@ impl PythonDistributionValue { packaging_policy, )?; optional_type_arg("config", "PythonInterpreterConfig", config)?; + optional_type_arg("runtime", "string", runtime)?; let dist = self.resolve_distribution(type_values, "resolve_distribution()")?; @@ -283,6 +286,15 @@ impl PythonDistributionValue { } }?; + let runtime_path = if runtime.get_type() == "NoneType" { + None + } else { + match runtime.downcast_ref::() { + Some(p) => Ok(Some(p.clone())), + None => Err(ValueError::IncorrectParameterType), + }? + }; + let pyoxidizer_context_value = get_context(type_values)?; let pyoxidizer_context = pyoxidizer_context_value .downcast_ref::() @@ -327,6 +339,7 @@ impl PythonDistributionValue { BinaryLibpythonLinkMode::Default, policy.inner(LABEL)?.deref(), config.inner(LABEL)?.deref(), + runtime_path, host_distribution, ) .map_err(|e| { @@ -442,7 +455,8 @@ starlark_module! { python_distribution_module => this, name: String, packaging_policy=NoneType::None, - config=NoneType::None + config=NoneType::None, + runtime=NoneType::None ) { let mut this = this.downcast_mut::().unwrap().unwrap(); this.to_python_executable_starlark( @@ -451,6 +465,7 @@ starlark_module! { python_distribution_module => name, &packaging_policy, &config, + &runtime, ) } diff --git a/pyoxidizer/src/templates/new-pyoxidizer.bzl.hbs b/pyoxidizer/src/templates/new-pyoxidizer.bzl.hbs index f372d7f2a..fc3fa133c 100644 --- a/pyoxidizer/src/templates/new-pyoxidizer.bzl.hbs +++ b/pyoxidizer/src/templates/new-pyoxidizer.bzl.hbs @@ -210,6 +210,13 @@ def make_exe(): # If no argument passed, the default `PythonInterpreterConfig` is used. config=python_config, + + # If no argument passed, an appropriate Rust project will be auto-generated. + {{#if rust_init}} + runtime="." + {{else}} + runtime=None + {{/if}} ) # Install tcl/tk support files to a specified directory so the `tkinter` Python