From 56131a768da3720e4cdea9b13353a0a9b1cf95f8 Mon Sep 17 00:00:00 2001 From: Reto Trappitsch Date: Sun, 24 Mar 2024 23:34:38 +0100 Subject: [PATCH] Add `PYAPP_IS_GUI` option (#97) Co-authored-by: Ofek Lev --- Cargo.toml | 1 + build.rs | 10 ++++++++++ docs/changelog.md | 4 ++++ docs/config.md | 13 +++++++++++++ src/app.rs | 15 +++++++++++++++ src/distribution.rs | 7 +++++++ src/process.rs | 26 +++++++++++++++++++++----- 7 files changed, 71 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1dd21a0..5b770b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,6 +58,7 @@ passthrough = [ "PYAPP_EXPOSE_PYTHON", "PYAPP_EXPOSE_PYTHON_PATH", "PYAPP_FULL_ISOLATION", + "PYAPP_IS_GUI", "PYAPP_METADATA_TEMPLATE", "PYAPP_PASS_LOCATION", "PYAPP_PIP_ALLOW_CONFIG", diff --git a/build.rs b/build.rs index 47ea958..3ffb70c 100644 --- a/build.rs +++ b/build.rs @@ -798,6 +798,15 @@ fn set_execution_mode() { } } +fn set_is_gui() { + let variable = "PYAPP_IS_GUI"; + if is_enabled(variable) { + set_runtime_variable(variable, "1"); + } else { + set_runtime_variable(variable, "0"); + } +} + fn set_isolation_mode() { let variable = "PYAPP_FULL_ISOLATION"; if is_enabled(variable) { @@ -931,6 +940,7 @@ fn main() { set_project(); set_distribution(); set_execution_mode(); + set_is_gui(); set_isolation_mode(); set_upgrade_virtualenv(); set_pip_external(); diff --git a/docs/changelog.md b/docs/changelog.md index 800ca6c..766ee05 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -8,6 +8,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## Unreleased +***Added:*** + +- Add `PYAPP_IS_GUI` option to support graphical applications + ## 0.15.1 - 2024-03-03 ***Fixed:*** diff --git a/docs/config.md b/docs/config.md index 0fd9a0d..e5a884c 100644 --- a/docs/config.md +++ b/docs/config.md @@ -51,6 +51,19 @@ The following options are mutually exclusive: If none are set then the `PYAPP_EXEC_MODULE` option will default to the value of `PYAPP_PROJECT_NAME` with hyphens replaced by underscores. +### GUI + +If you are packaging a graphical user interface (GUI), you can set `PYAPP_IS_GUI` to `true` or `1`. + +On Windows, this will use `pythonw.exe` instead of `python.exe` to execute [the application](https://docs.python.org/3/using/windows.html#python-application), which avoids a console window from appearing. Running a GUI application with `pythonw.exe` means that all `stdout` and `stderr` output from your GUI will be discarded. + +Otherwise, the application will execute as usual. PyApp will run your GUI by spawning a new process, such that the console window that runs the application terminates after successful spawning. + +Even when `PYAPP_IS_GUI` is enabled you can still run the application from the command line. Furthermore, PyApp-specific logic (e.g. installation and setup) will still display a console window with status messages. + +!!! note + On macOS, the console by default does not automatically close when processes have terminated (however it can be closed manually without interferring with the GUI). The default console behavior [can be changed](https://stackoverflow.com/questions/5560167/osx-how-to-auto-close-terminal-window-after-the-exit-command-executed) in the user settings to close after the last process terminates successfully. + ## Python distribution ### Known diff --git a/src/app.rs b/src/app.rs index 0815ba4..d53e3be 100644 --- a/src/app.rs +++ b/src/app.rs @@ -74,6 +74,12 @@ fn installation_python_path() -> String { env!("PYAPP__INSTALLATION_PYTHON_PATH").into() } +#[cfg(windows)] +fn installation_pythonw_path() -> String { + let python_path: String = env!("PYAPP__INSTALLATION_PYTHON_PATH").into(); + python_path.replace("python.exe", "pythonw.exe") +} + fn installation_site_packages_path() -> String { env!("PYAPP__INSTALLATION_SITE_PACKAGES_PATH").into() } @@ -180,6 +186,10 @@ pub fn pip_external() -> bool { env!("PYAPP_PIP_EXTERNAL") == "1" } +pub fn is_gui() -> bool { + env!("PYAPP_IS_GUI") == "1" +} + pub fn full_isolation() -> bool { env!("PYAPP_FULL_ISOLATION") == "1" } @@ -204,6 +214,11 @@ pub fn python_path() -> PathBuf { install_dir().join(installation_python_path()) } +#[cfg(windows)] +pub fn pythonw_path() -> PathBuf { + install_dir().join(installation_pythonw_path()) +} + pub fn site_packages_path() -> PathBuf { install_dir().join(installation_site_packages_path()) } diff --git a/src/distribution.rs b/src/distribution.rs index f28b9f4..41437a0 100644 --- a/src/distribution.rs +++ b/src/distribution.rs @@ -19,6 +19,13 @@ pub fn python_command(python: &PathBuf) -> Command { pub fn run_project() -> Result<()> { let mut command = python_command(&app::python_path()); + #[cfg(windows)] + { + if app::is_gui() { + command = python_command(&app::pythonw_path()); + } + } + if !app::exec_code().is_empty() { command.args(["-c", app::exec_code().as_str()]); } else if !app::exec_module().is_empty() { diff --git a/src/process.rs b/src/process.rs index 2296b7f..6f7e49e 100644 --- a/src/process.rs +++ b/src/process.rs @@ -1,13 +1,12 @@ use std::io::Read; #[cfg(unix)] use std::os::unix::process::CommandExt; -#[cfg(windows)] use std::process::exit; use std::process::{Command, ExitStatus}; use anyhow::Result; -use crate::terminal; +use crate::{app, terminal}; pub fn wait_for(mut command: Command, message: String) -> Result<(ExitStatus, String)> { let (mut reader, writer_stdout) = os_pipe::pipe()?; @@ -32,11 +31,28 @@ pub fn wait_for(mut command: Command, message: String) -> Result<(ExitStatus, St #[cfg(unix)] pub fn exec(mut command: Command) -> Result<()> { - Err(command.exec().into()) + if app::is_gui() { + exec_gui(command) + } else { + Err(command.exec().into()) + } } #[cfg(windows)] pub fn exec(mut command: Command) -> Result<()> { - let status = command.status()?; - exit(status.code().unwrap_or(1)); + if app::is_gui() { + exec_gui(command) + } else { + let status = command.status()?; + exit(status.code().unwrap_or(1)); + } +} + +fn exec_gui(mut command: Command) -> Result<()> { + let mut child = command.spawn()?; + match child.try_wait() { + Ok(Some(status)) => exit(status.code().unwrap_or(1)), + Ok(None) => Ok(()), // The child is still running + Err(e) => Err(e.into()), + } }