diff --git a/CHANGES.md b/CHANGES.md index c69796d..d5c0ff6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,9 @@ # Release Notes +## 0.13.0 + +This release brings support for `gui-scripts` and `pythonw.exe` on Windows. + ## 0.12.6 This release fixes generation of `--sh-boot` headers on Windows. diff --git a/Cargo.lock b/Cargo.lock index b5a49f6..8bc4746 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2040,7 +2040,7 @@ dependencies = [ [[package]] name = "pexrc" -version = "0.12.6" +version = "0.13.0" dependencies = [ "anstream", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index a0a5fc2..e8f2bd6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ cargo-features = ["profile-rustflags"] [package] name = "pexrc" -version = "0.12.6" +version = "0.13.0" edition = { workspace = true } publish = false diff --git a/build.rs b/build.rs index ebecdae..b78249f 100644 --- a/build.rs +++ b/build.rs @@ -178,6 +178,8 @@ fn main() -> anyhow::Result<()> { &[] }; + let python_proxyw_options = [Cow::Borrowed("--features"), Cow::Borrowed("windows")]; + println!("cargo::rerun-if-env-changed=PEXRC_TARGETS"); if all_targets { println!("cargo::rerun-if-changed=rust-toolchain"); @@ -202,6 +204,8 @@ fn main() -> anyhow::Result<()> { tgt_arg, "--package", "python-proxy", + "--bin", + "python-proxy", ], &[], embeds_configuration.profile, @@ -211,6 +215,33 @@ fn main() -> anyhow::Result<()> { .map(BuildTarget::zigbuild_target), python_proxy_optimizations, )?; + custom_cargo_build( + &cargo, + &[ + "zigbuild", + "--target-dir", + tgt_arg, + "--package", + "python-proxy", + "--bin", + "python-proxyw", + ], + &python_proxyw_options, + embeds_configuration.profile, + &found_tools, + targets + .iter_zigbuild_targets() + .filter_map(|target| { + if target.is_windows() { + Some(target.zigbuild_target()) + } else { + None + } + }) + .collect::>() + .into_iter(), + python_proxy_optimizations, + )?; custom_cargo_build( &cargo, @@ -237,6 +268,8 @@ fn main() -> anyhow::Result<()> { tgt_arg, "--package", "python-proxy", + "--bin", + "python-proxy", ], &[], embeds_configuration.profile, @@ -244,17 +277,36 @@ fn main() -> anyhow::Result<()> { targets.iter_xwin_targets().map(BuildTarget::as_str), python_proxy_optimizations, )?; + custom_cargo_build( + &cargo, + &[ + "xwin", + "build", + "--target-dir", + tgt_arg, + "--package", + "python-proxy", + "--bin", + "python-proxyw", + ], + &python_proxyw_options, + embeds_configuration.profile, + &found_tools, + targets.iter_xwin_targets().map(BuildTarget::as_str), + python_proxy_optimizations, + )?; collect_embeds(&targets, &tgt_path, embeds_configuration, &embeds_dir, true) } else { let target = env::var("TARGET")?; let targets = ClassifiedTargets::parse([target.as_str()].into_iter(), &glibc); + let current_target = BuildTarget::current(&glibc); custom_cargo_build( &cargo, &["build", "--target-dir", tgt_arg, "--package", "clib"], optional_clib_args, embeds_configuration.profile, &found_tools, - [BuildTarget::current(&glibc).as_str()].into_iter(), + [current_target.as_str()].into_iter(), clib_optimizations, )?; custom_cargo_build( @@ -265,13 +317,34 @@ fn main() -> anyhow::Result<()> { tgt_arg, "--package", "python-proxy", + "--bin", + "python-proxy", ], &[], embeds_configuration.profile, &found_tools, - [BuildTarget::current(&glibc).as_str()].into_iter(), + [current_target.as_str()].into_iter(), python_proxy_optimizations, )?; + if current_target.is_windows() { + custom_cargo_build( + &cargo, + &[ + "build", + "--target-dir", + tgt_arg, + "--package", + "python-proxy", + "--bin", + "python-proxyw", + ], + &python_proxyw_options, + embeds_configuration.profile, + &found_tools, + [current_target.as_str()].into_iter(), + python_proxy_optimizations, + )?; + } collect_embeds(&targets, &tgt_path, embeds_configuration, &embeds_dir, true) } } @@ -368,6 +441,19 @@ fn collect_embeds<'a>( &target_name, compress, )?; + if target.is_windows() { + let python_proxy_name = target.binary_name("python-proxyw", None); + let target_name = target.fully_qualified_binary_name("python-proxyw", None)?; + collect_embed( + &python_proxy_name, + &proxies_dir, + embeds_configuration, + target, + target_dir, + &target_name, + compress, + )?; + } } Ok(()) } diff --git a/crates/build-system/src/rust_toolchain.rs b/crates/build-system/src/rust_toolchain.rs index 708061b..4127be3 100644 --- a/crates/build-system/src/rust_toolchain.rs +++ b/crates/build-system/src/rust_toolchain.rs @@ -43,6 +43,10 @@ impl<'a> BuildTarget<'a> { } } + pub fn is_windows(&self) -> bool { + matches!(self.target, Target::Windows(_)) + } + pub fn as_str(&self) -> &str { self.target.as_str() } diff --git a/crates/pexrs/src/lib.rs b/crates/pexrs/src/lib.rs index 470827a..ab9193a 100644 --- a/crates/pexrs/src/lib.rs +++ b/crates/pexrs/src/lib.rs @@ -26,7 +26,7 @@ struct PythonProxyLinker<'a>(&'a Pex<'a>); impl<'a> Linker for PythonProxyLinker<'a> { #[cfg(unix)] - fn link(&self, dest: &Path, interpreter: Option<&Path>) -> anyhow::Result<()> { + fn link(&self, dest: &Path, interpreter: Option<&Path>, is_gui: bool) -> anyhow::Result<()> { let file_name = dest.file_name().ok_or_else(|| { anyhow!( "The destination for the python-proxy doesn't have a file name: {path}", @@ -54,6 +54,7 @@ impl<'a> Linker for PythonProxyLinker<'a> { venv_python_file_name.as_ref(), file.into_file(), None, + is_gui, ) })?; @@ -72,13 +73,14 @@ impl<'a> Linker for PythonProxyLinker<'a> { } #[cfg(windows)] - fn link(&self, dest: &Path, interpreter: Option<&Path>) -> anyhow::Result<()> { + fn link(&self, dest: &Path, interpreter: Option<&Path>, is_gui: bool) -> anyhow::Result<()> { python_proxy::create( ProxySource::Pex(self.0), interpreter .ok_or_else(|| anyhow!("Windows venvs require an interpreter to link to."))?, fs::File::create(dest)?.into_file(), None, + is_gui, ) } } diff --git a/crates/platform/src/lib.rs b/crates/platform/src/lib.rs index e3d3341..3436be7 100644 --- a/crates/platform/src/lib.rs +++ b/crates/platform/src/lib.rs @@ -123,7 +123,7 @@ impl<'a> Display for PosixPath<'a> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { for (idx, component) in self.0.components().enumerate() { if idx > 0 { - f.write_str("/")?; + f.write_char('/')?; } match component { Component::CurDir => f.write_char('.')?, diff --git a/crates/python-proxy/Cargo.toml b/crates/python-proxy/Cargo.toml index 2846c8e..8015dd4 100644 --- a/crates/python-proxy/Cargo.toml +++ b/crates/python-proxy/Cargo.toml @@ -6,6 +6,15 @@ name = "python-proxy" edition = { workspace = true } publish = false +[[bin]] +name = "python-proxy" +path = "src/bin/main.rs" + +[[bin]] +name = "python-proxyw" +path = "src/bin/mainw.rs" +required-features = ["windows"] + [dependencies] anyhow = { workspace = true } cache = { path = "../cache" } @@ -13,3 +22,6 @@ pex = { path = "../pex" } platform = { path = "../platform" } target = { path = "../target" } zip = { workspace = true } + +[features] +windows = [] \ No newline at end of file diff --git a/crates/python-proxy/src/bin/main.rs b/crates/python-proxy/src/bin/main.rs new file mode 100644 index 0000000..120cf37 --- /dev/null +++ b/crates/python-proxy/src/bin/main.rs @@ -0,0 +1,56 @@ +// Copyright 2026 Pex project contributors. +// SPDX-License-Identifier: Apache-2.0 + +#![deny(clippy::all)] + +use std::io::ErrorKind; +use std::path::PathBuf; +use std::process::exit; +use std::{env, io}; + +use python_proxy::read_proxy; + +#[cfg(unix)] +fn proxy_path() -> io::Result { + env::args() + .next() + .ok_or_else(|| { + io::Error::new( + ErrorKind::NotFound, + "No argv0 was present; python-proxy cannot run.", + ) + }) + .map(PathBuf::from) +} + +#[cfg(windows)] +fn proxy_path() -> io::Result { + env::current_exe() +} + +fn main() { + let python_proxy = match proxy_path().and_then(read_proxy) { + Ok(python) => python, + Err(err) => { + eprintln!("Failed to determine python executable path: {err}"); + exit(1); + } + }; + let (mut command, pexrc_cache_read_lock) = match python_proxy.prepare_command() { + Ok(proxy) => proxy, + Err(err) => { + eprintln!("Failed to prepare python proxy command: {err}"); + exit(1); + } + }; + match platform::exec(&mut command, &[pexrc_cache_read_lock]) { + Ok(status) => exit(status), + Err(err) => { + eprintln!( + "Failed to spawn {python}: {err}", + python = python_proxy.target.display() + ); + exit(1); + } + } +} diff --git a/crates/python-proxy/src/bin/mainw.rs b/crates/python-proxy/src/bin/mainw.rs new file mode 100644 index 0000000..de66ca3 --- /dev/null +++ b/crates/python-proxy/src/bin/mainw.rs @@ -0,0 +1,55 @@ +// Copyright 2026 Pex project contributors. +// SPDX-License-Identifier: Apache-2.0 + +#![cfg(windows)] +#![deny(clippy::all)] +#![windows_subsystem = "windows"] + +use std::env; +use std::os::windows::process::CommandExt; +use std::process::{Child, exit}; + +use python_proxy::{PythonProxy, read_proxy}; + +// See: https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags +const DETACHED_PROCESS: u32 = 0x00000008; + +fn main() { + let python_proxy = match env::current_exe().and_then(read_proxy) { + Ok(python) => python, + Err(err) => { + eprintln!("Failed to determine python executable path: {err}"); + exit(1); + } + }; + + let (mut command, _pexrc_cache_read_lock) = match python_proxy.prepare_command() { + Ok(proxy) => proxy, + Err(err) => { + eprintln!("Failed to prepare python proxy command: {err}"); + exit(1); + } + }; + command.creation_flags(DETACHED_PROCESS); + + match command.spawn() { + Ok(mut child) => match child.wait() { + Ok(exit_status) => { + if !exit_status.success() { + exit(exit_status.code().unwrap_or(1)) + } + } + Err(err) => { + eprintln!( + "Failed to wait for python proxy child process {id} to complete: {err}", + id = child.id() + ); + exit(1) + } + }, + Err(err) => { + eprintln!("{err}"); + exit(1) + } + } +} diff --git a/crates/python-proxy/src/lib.rs b/crates/python-proxy/src/lib.rs index 6d07846..173d652 100644 --- a/crates/python-proxy/src/lib.rs +++ b/crates/python-proxy/src/lib.rs @@ -2,12 +2,14 @@ // SPDX-License-Identifier: Apache-2.0 #![deny(clippy::all)] +#![feature(string_from_utf8_lossy_owned)] use std::fs::File; -use std::io; -use std::io::{Read, Seek, Write}; -use std::path::Path; +use std::io::{BufReader, Read, Seek, SeekFrom, Write}; +use std::path::{Path, PathBuf}; +use std::process::Command; use std::sync::LazyLock; +use std::{env, io}; use anyhow::anyhow; use pex::{Layout, Pex}; @@ -18,26 +20,137 @@ use zip::{CompressionMethod, ZipArchive, ZipWriter}; pub const SHEBANG_PREFIX: &str = "\n#!"; const SHEBANG_SUFFIX: &str = "\n"; +const PATH_MAX: usize = 4096; + +pub struct PythonProxy { + pub proxy: PathBuf, + pub target: PathBuf, + pub has_script: bool, +} + +impl PythonProxy { + pub fn prepare_command(&self) -> io::Result<(Command, File)> { + let mut command = if self.target.is_absolute() { + Command::new(&self.target) + } else if let Some(proxy_dir) = self.proxy.parent() { + Command::new(proxy_dir.join(&self.target)) + } else { + return Err(io::Error::other(format!( + "The proxy target {target} is relative but the python-proxy at {proxy} has no \ + parent directory to base that relative path in", + target = self.target.display(), + proxy = self.proxy.display() + ))); + }; + if self.has_script { + command.arg(self.proxy.as_os_str()); + } + command.args(env::args_os().skip(1)); + command.env("__PYVENV_LAUNCHER__", &self.proxy); + + // N.B.: For Mac Python Framework builds (and Windows Python builds) __PYVENV_LAUNCHER__ is + // deleted from the env on launch. We need to know about the launcher in the venv `pex` + // script; so we duplicate that knowledge in our own env var. + command.env("__PEXRC_PYVENV_LAUNCHER__", &self.proxy); + + let lock = match cache::read_lock() { + Ok(lock) => lock, + Err(err) => { + return Err(io::Error::other(format!( + "Failed to obtain PEXRC cache read lock: {err}" + ))); + } + }; + Ok((command, lock)) + } +} + +pub fn read_proxy(proxy: PathBuf) -> io::Result { + let mut buf = vec![0u8; PATH_MAX]; + let mut exe_fp = BufReader::new(File::open(&proxy)?); + exe_fp.seek(SeekFrom::End(-(buf.len() as i64)))?; + exe_fp.read_to_end(&mut buf)?; + match buf + .windows(SHEBANG_PREFIX.len()) + .rposition(|chunk| SHEBANG_PREFIX.as_bytes() == chunk) + { + Some(index) => { + const EOCD_MAGIC: &[u8] = b"PK\x05\x06"; + let eocd_start = index - 22; + let has_script = &buf[eocd_start..(eocd_start + EOCD_MAGIC.len())] == EOCD_MAGIC; + buf.drain(..index + SHEBANG_PREFIX.len()); + buf.truncate(buf.trim_ascii_end().len()); + let target = String::from_utf8(buf).map(PathBuf::from).map_err(|err| { + io::Error::other(format!( + "Python shebang footer contained a non-UTF-8 path: {buf}", + buf = err.into_utf8_lossy() + )) + })?; + Ok(PythonProxy { + proxy, + target, + has_script, + }) + } + None => Err(io::Error::other("Failed to find Python shebang footer.")), + } +} + pub enum ProxySource<'a> { Pex(&'a Pex<'a>), Read(Box), } +#[cfg(windows)] +pub fn create( + proxy_source: ProxySource, + interpreter: &Path, + target_python: File, + script: Option, + is_gui: bool, +) -> anyhow::Result<()> { + use std::borrow::Cow; + let interpreter = if is_gui { + Cow::Owned(interpreter.with_file_name("pythonw.exe")) + } else { + Cow::Borrowed(interpreter) + }; + create_proxy( + proxy_source, + interpreter.as_ref(), + target_python, + script, + is_gui, + ) +} + +#[cfg(unix)] pub fn create( + proxy_source: ProxySource, + interpreter: &Path, + target_python: File, + script: Option, + is_gui: bool, +) -> anyhow::Result<()> { + create_proxy(proxy_source, interpreter, target_python, script, is_gui) +} + +fn create_proxy( proxy_source: ProxySource, interpreter: &Path, mut target_python: File, script: Option, + is_gui: bool, ) -> anyhow::Result<()> { match proxy_source { ProxySource::Pex(pex) => match pex.layout { Layout::Loose | Layout::Packed => { - let mut python_proxy = read_python_proxy_from_dir(pex.path)?; + let mut python_proxy = read_python_proxy_from_dir(pex.path, is_gui)?; io::copy(&mut python_proxy, &mut target_python)?; } Layout::ZipApp => { let mut pex_zip = ZipArchive::new(File::open(pex.path)?)?; - let mut python_proxy = read_python_proxy_from_zip(&mut pex_zip)?; + let mut python_proxy = read_python_proxy_from_zip(&mut pex_zip, is_gui)?; io::copy(&mut python_proxy, &mut target_python)?; } }, @@ -45,7 +158,6 @@ pub fn create( io::copy(&mut bytes, &mut target_python)?; } } - let shebang_python = interpreter.as_os_str(); if let Some(script) = script { let mut script_zip = ZipWriter::new(&target_python); @@ -77,26 +189,38 @@ static PYTHON_PROXY_FILE_NAME: LazyLock> = LazyLock::new( current_target.fully_qualified_binary_name("python-proxy", None) }); -fn python_proxy_file_name<'a>() -> anyhow::Result<&'a str> { - PYTHON_PROXY_FILE_NAME - .as_deref() - .map_err(|err| anyhow!("{err}")) +static PYTHON_PROXY_GUI_FILE_NAME: LazyLock> = LazyLock::new(|| { + let current_target = Target::current()?; + current_target.fully_qualified_binary_name("python-proxyw", None) +}); + +fn python_proxy_file_name<'a>(is_gui: bool) -> anyhow::Result<&'a str> { + if is_gui { + PYTHON_PROXY_GUI_FILE_NAME + .as_deref() + .map_err(|err| anyhow!("{err}")) + } else { + PYTHON_PROXY_FILE_NAME + .as_deref() + .map_err(|err| anyhow!("{err}")) + } } -fn read_python_proxy_from_dir(pex_dir: &Path) -> anyhow::Result { +fn read_python_proxy_from_dir(pex_dir: &Path, is_gui: bool) -> anyhow::Result { Ok(File::open( pex_dir .join("__pex__") .join(".proxies") - .join(python_proxy_file_name()?), + .join(python_proxy_file_name(is_gui)?), )?) } fn read_python_proxy_from_zip( pex_zip: &mut ZipArchive, + is_gui: bool, ) -> anyhow::Result { Ok(pex_zip.by_name(&format!( "__pex__/.proxies/{python_proxy_name}", - python_proxy_name = python_proxy_file_name()? + python_proxy_name = python_proxy_file_name(is_gui)? ))?) } diff --git a/crates/python-proxy/src/main.rs b/crates/python-proxy/src/main.rs deleted file mode 100644 index c661af9..0000000 --- a/crates/python-proxy/src/main.rs +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright 2026 Pex project contributors. -// SPDX-License-Identifier: Apache-2.0 - -#![deny(clippy::all)] -#![feature(string_from_utf8_lossy_owned)] - -use std::fs::File; -use std::io::{BufReader, ErrorKind, Read, Seek, SeekFrom}; -use std::path::PathBuf; -use std::process::{Command, exit}; -use std::{env, io}; - -use python_proxy::SHEBANG_PREFIX; - -const PATH_MAX: usize = 4096; - -struct PythonProxy { - proxy: PathBuf, - target: PathBuf, - has_script: bool, -} - -fn read_proxy() -> io::Result { - let mut buf = vec![0u8; PATH_MAX]; - let proxy = proxy_path()?; - let mut exe_fp = BufReader::new(File::open(&proxy)?); - exe_fp.seek(SeekFrom::End(-(buf.len() as i64)))?; - exe_fp.read_to_end(&mut buf)?; - match buf - .windows(SHEBANG_PREFIX.len()) - .rposition(|chunk| SHEBANG_PREFIX.as_bytes() == chunk) - { - Some(index) => { - const EOCD_MAGIC: &[u8] = b"PK\x05\x06"; - let eocd_start = index - 22; - let has_script = &buf[eocd_start..(eocd_start + EOCD_MAGIC.len())] == EOCD_MAGIC; - buf.drain(..index + SHEBANG_PREFIX.len()); - buf.truncate(buf.trim_ascii_end().len()); - let target = String::from_utf8(buf).map(PathBuf::from).map_err(|err| { - io::Error::new( - ErrorKind::InvalidFilename, - format!( - "Python shebang footer contained a non-UTF-8 path: {buf}", - buf = err.into_utf8_lossy() - ), - ) - })?; - Ok(PythonProxy { - proxy, - target, - has_script, - }) - } - None => Err(io::Error::new( - ErrorKind::NotFound, - "Failed to find Python shebang footer.", - )), - } -} - -#[cfg(unix)] -fn proxy_path() -> io::Result { - env::args() - .next() - .ok_or_else(|| { - io::Error::new( - ErrorKind::NotFound, - "No argv0 was present; python-proxy cannot run.", - ) - }) - .map(PathBuf::from) -} - -#[cfg(windows)] -fn proxy_path() -> io::Result { - env::current_exe() -} - -fn main() { - let python_proxy = match read_proxy() { - Ok(python) => python, - Err(err) => { - eprintln!("Failed to determine python executable path: {err}"); - exit(1); - } - }; - let mut command = if python_proxy.target.is_absolute() { - Command::new(&python_proxy.target) - } else if let Some(proxy_dir) = python_proxy.proxy.parent() { - Command::new(proxy_dir.join(&python_proxy.target)) - } else { - eprintln!( - "The proxy target {target} is relative but the python-proxy at {proxy} has no parent \ - directory to base that relative path in", - target = python_proxy.target.display(), - proxy = python_proxy.proxy.display() - ); - exit(1); - }; - if python_proxy.has_script { - command.arg(python_proxy.proxy.as_os_str()); - } - command.args(env::args_os().skip(1)); - command.env("__PYVENV_LAUNCHER__", &python_proxy.proxy); - - // N.B.: For Mac Python Framework builds (and Windows Python builds) __PYVENV_LAUNCHER__ is - // deleted from the env on launch. We need to know about the launcher in the venv `pex` script; - // so we duplicate that knowledge in our own env var. - command.env("__PEXRC_PYVENV_LAUNCHER__", &python_proxy.proxy); - - let lock = match cache::read_lock() { - Ok(lock) => lock, - Err(err) => { - eprintln!("Failed to obtain PEXRC cache read lock: {err}"); - exit(1); - } - }; - match platform::exec(&mut command, &[lock]) { - Ok(status) => exit(status), - Err(err) => { - eprintln!( - "Failed to spawn {python}: {err}", - python = python_proxy.target.display() - ); - exit(1); - } - } -} diff --git a/crates/venv/src/venv_pex.rs b/crates/venv/src/venv_pex.rs index 0037228..9f729e9 100644 --- a/crates/venv/src/venv_pex.rs +++ b/crates/venv/src/venv_pex.rs @@ -13,7 +13,6 @@ use cache::{Fingerprint, default_digest, fingerprint_file}; use fs_err as fs; use fs_err::File; use indexmap::IndexMap; -use log::warn; use logging_timer::time; use pex::{ BinPath, @@ -157,7 +156,15 @@ fn install_scripts( return Ok(()); } - for (name, entry_point) in entry_points.console_scripts() { + for (name, entry_point, is_gui) in entry_points + .console_scripts() + .map(|(name, entry_point)| (name, entry_point, false)) + .chain( + entry_points + .gui_scripts() + .map(|(name, entry_point)| (name, entry_point, true)), + ) + { let script_path = virtualenv .script_path(name) .with_extension(env::consts::EXE_EXTENSION); @@ -181,32 +188,13 @@ fn install_scripts( } Err(err) => bail!("{err}"), }; - write_script(pex, shebang_interpreter, script_file, script_contents)?; - } - - let gui_scripts = entry_points - .gui_scripts() - .map(|(name, _)| name) - .collect::>(); - if !gui_scripts.is_empty() { - struct GuiScriptsList<'a>(Vec<&'a str>); - impl<'a> Display for GuiScriptsList<'a> { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - for name in &self.0 { - writeln!(f, "{name}")?; - } - Ok(()) - } - } - warn!( - "There is currently no support for gui scripts, skipping install of {count}.\n\ - Found these installing {entry_points_txt} in venv at {venv}:\n\ - {gui_scripts_list}", - count = gui_scripts.len(), - entry_points_txt = entry_points_txt.display(), - venv = virtualenv.prefix().display(), - gui_scripts_list = GuiScriptsList(gui_scripts) - ); + write_script( + pex, + shebang_interpreter, + script_file, + script_contents, + is_gui, + )?; } Ok(()) } @@ -217,6 +205,7 @@ fn write_script( _shebang_interpreter: &Path, mut script_file: File, script_contents: String, + _is_gui: bool, ) -> anyhow::Result<()> { script_file.write_all(script_contents.as_bytes())?; mark_executable(script_file.file_mut())?; @@ -229,12 +218,14 @@ fn write_script( shebang_interpreter: &Path, script_file: File, script_contents: String, + is_gui: bool, ) -> anyhow::Result<()> { python_proxy::create( python_proxy::ProxySource::Pex(pex), shebang_interpreter, script_file.into_file(), Some(script_contents), + is_gui, ) } diff --git a/crates/venv/src/virtualenv.rs b/crates/venv/src/virtualenv.rs index 1c379f6..adf3a56 100644 --- a/crates/venv/src/virtualenv.rs +++ b/crates/venv/src/virtualenv.rs @@ -53,13 +53,13 @@ fn executable_rel_path(interpreter: &Interpreter) -> Cow<'static, str> { } pub trait Linker { - fn link(&self, dest: &Path, interpreter: Option<&Path>) -> anyhow::Result<()>; + fn link(&self, dest: &Path, interpreter: Option<&Path>, is_gui: bool) -> anyhow::Result<()>; } pub struct FileSystemLinker(); impl Linker for FileSystemLinker { - fn link(&self, dest: &Path, interpreter: Option<&Path>) -> anyhow::Result<()> { + fn link(&self, dest: &Path, interpreter: Option<&Path>, _is_gui: bool) -> anyhow::Result<()> { if let Some(interpreter) = interpreter { symlink_or_link_or_copy(interpreter, dest, false)?; } @@ -358,7 +358,13 @@ fn create_pep_405_venv<'a>( if let Some(parent) = venv_python.parent() { fs::create_dir_all(parent)?; } - linker.link(&venv_python, Some(&raw_base_interpreter.realpath))?; + linker.link(&venv_python, Some(&raw_base_interpreter.realpath), false)?; + #[cfg(windows)] + linker.link( + &venv_python.with_file_name("pythonw.exe"), + Some(&raw_base_interpreter.realpath.with_file_name("pythonw.exe")), + true, + )?; let site_packages_relpath = site_packages_relpath(&base_interpreter); fs::create_dir_all(path.join(site_packages_relpath.as_ref()))?; if pip { @@ -431,7 +437,9 @@ fn create_virtualenv_venv<'a>( }; pyvenv_cfg.write(path.as_ref())?; - linker.link(&venv_python, None)?; + linker.link(&venv_python, None, false)?; + #[cfg(windows)] + linker.link(&venv_python.with_file_name("pythonw.exe"), None, true)?; if pip { ensure_pip(raw_interpreter, &venv_python)?; } diff --git a/pexrc.rs b/pexrc.rs index 33b10b0..a151155 100644 --- a/pexrc.rs +++ b/pexrc.rs @@ -12,7 +12,7 @@ use clap::{ArgAction, Parser, Subcommand, ValueEnum}; use indexmap::{Equivalent, IndexSet}; use pex::{Pex, WheelOptions}; use pexrc::commands::{extract, info, inject, script}; -use pexrc::embeds::{CLIB_BY_TARGET, PROXY_BY_TARGET}; +use pexrc::embeds::{CLIB_BY_TARGET, PROXY_BY_TARGET, PROXYW_BY_TARGET}; use pexrc::source; use target::Target; @@ -138,12 +138,15 @@ enum Commands { #[arg(long)] target: Option, - #[arg(short = 'p', long, required = true)] + #[arg(short = 'p', long)] python: PathBuf, #[arg(short = 'o', long)] output_file: PathBuf, + #[arg(long, default_value_t = false)] + gui: bool, + #[arg(value_name = "SCRIPT")] script: PathBuf, }, @@ -205,12 +208,20 @@ fn main() -> anyhow::Result<()> { "The allowed --target values are all keys in PROXY_BY_TARGET.", ) }) + .chain( + targets + .iter() + .filter_map(|target| PROXYW_BY_TARGET.get(target)), + ) .collect::>(), ) } else { ( CLIB_BY_TARGET.values().collect::>(), - PROXY_BY_TARGET.values().collect::>(), + PROXY_BY_TARGET + .values() + .chain(PROXYW_BY_TARGET.values()) + .collect::>(), ) }; let pexes = pexes @@ -231,9 +242,10 @@ fn main() -> anyhow::Result<()> { python, script, output_file, + gui: is_gui, } => { if let Some(target) = target { - script::create(target.into(), &python, &script, &output_file) + script::create(target.into(), &python, &script, &output_file, is_gui) } else { let current_target = Target::current()?; script::create( @@ -241,6 +253,7 @@ fn main() -> anyhow::Result<()> { &python, &script, &output_file, + is_gui, ) } } diff --git a/pyproject.toml b/pyproject.toml index 5437cea..d5389a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,9 +13,11 @@ dev = [ "dev-cmd[old-pythons]; python_version >= '3.8'", "mypy", "pex", + "psutil", "pytest", "pytest-xdist", "ruff", + "types-psutil", "types-setuptools", ] diff --git a/python/tests/test_gui_scripts.py b/python/tests/test_gui_scripts.py new file mode 100644 index 0000000..81b06ee --- /dev/null +++ b/python/tests/test_gui_scripts.py @@ -0,0 +1,65 @@ +# Copyright 2026 Pex project contributors. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import absolute_import + +import os.path +import subprocess +import sys +import time + +import psutil +import pytest +from testing import IS_CI, IS_WINDOWS, pexrc_inject + +TYPE_CHECKING = False +if TYPE_CHECKING: + # Ruff doesn't understand Python 2 and thus the type comment usages. + from typing import Any # noqa: F401 + + +def find_and_kill( + process_name, # type: str + timeout=5.0, # type: float +): + # type: (...) -> None + + found = False + start = time.time() + while True: + wait_time = time.time() - start + assert wait_time <= timeout, ( + "Waited past {timeout} seconds (wait_time) for gui to appear.".format( + timeout=timeout, + ) + ) + + for ps in psutil.process_iter(attrs=["name", "cmdline"]): + attrs = ps.as_dict(attrs=["name", "cmdline"]) + if process_name in (attrs.get("name") or "") or any( + process_name in arg for arg in (attrs.get("cmdline") or "") + ): + ps.kill() + found = True + if found: + return + + +@pytest.mark.skipif(IS_CI, reason="This test pops up windows and expects a display is present.") +def test_gui_scripts(tmpdir): + # type: (Any) -> None + + pex_root = os.path.join(str(tmpdir), "pex-root") + pex = os.path.join(str(tmpdir), "psgdemos.pex") + subprocess.check_call( + args=["pex", "--runtime-pex-root", pex_root, "psgdemos", "-c", "psgdemos", "-o", pex] + ) + injected_pex = pexrc_inject(pex) + process = subprocess.Popen(args=[sys.executable, injected_pex]) + time.sleep( + # Windows is slow, at least in my vm. This allows time for the UI to come up before psutil + # kills it. + 5 if IS_WINDOWS else 1 + ) + find_and_kill("psgdemos") + process.wait(timeout=1.0) diff --git a/src/commands/script.rs b/src/commands/script.rs index 6f802ed..55c7a4e 100644 --- a/src/commands/script.rs +++ b/src/commands/script.rs @@ -14,8 +14,9 @@ pub fn create( python: &Path, script: &Path, output_file: &Path, + is_gui: bool, ) -> anyhow::Result<()> { - let proxy_bytes = Box::new(read_proxy_content(target)?); + let proxy_bytes = Box::new(read_proxy_content(target, is_gui)?); let script = fs::read_to_string(script)?; let target_script = fs::File::create(output_file)?; python_proxy::create( @@ -23,5 +24,6 @@ pub fn create( python, target_script.into_file(), Some(script), + is_gui, ) } diff --git a/src/embeds.rs b/src/embeds.rs index 188f742..c719692 100644 --- a/src/embeds.rs +++ b/src/embeds.rs @@ -54,39 +54,60 @@ pub(crate) static PROXIES_DIR: LazyLock<&'static Dir> = LazyLock::new(|| { .expect("Embeds include proxies/.") }); -pub static PROXY_BY_TARGET: LazyLock>> = - LazyLock::new(|| { - PROXIES_DIR - .files() - .map(|file| { - let path = file.path(); - let target = path +fn identify_proxy_files(name: &str) -> IndexMap> { + PROXIES_DIR + .files() + .filter_map(|file| { + let path = file.path(); + let mut components = path .file_stem() .expect("The Python proxies all have a file name.") .to_str() .expect("The Python proxy file names are utf-8 strings.") - .splitn(3, "-") - .nth(2) - .expect( - "The Python proxy file names are all of the form `python-proxy-(.exe)?", - ); - let target = SimplifiedTarget::try_from(target) - .expect("The Python proxy file names are all derived from simplified targets."); - ( + .splitn(3, "-"); + assert_eq!(Some("python"), components.next()); + if components.next().expect( + "The Python proxy file names are all of the form `python-proxyw?-(.exe)?", + ) != name + { + return None; + } + let target = components.next().expect( + "The Python proxy file names are all of the form `python-proxyw?-(.exe)?", + ); + let target = SimplifiedTarget::try_from(target) + .expect("The Python proxy file names are all derived from simplified targets."); + Some(( + target, + Binary { target, - Binary { - target, - path, - contents: file.contents(), - }, - ) - }) - .collect() - }); + path, + contents: file.contents(), + }, + )) + }) + .collect() +} + +pub static PROXY_BY_TARGET: LazyLock>> = + LazyLock::new(|| identify_proxy_files("proxy")); + +pub static PROXYW_BY_TARGET: LazyLock>> = + LazyLock::new(|| identify_proxy_files("proxyw")); -pub fn read_proxy_content(target: SimplifiedTarget) -> anyhow::Result { - let proxy = PROXY_BY_TARGET - .get(&target) - .ok_or_else(|| anyhow!("There is no python-proxy for {target}"))?; +pub fn read_proxy_content(target: SimplifiedTarget, is_gui: bool) -> anyhow::Result { + let proxy = if is_gui + && matches!( + target, + SimplifiedTarget::Arm64Windows | SimplifiedTarget::X64Windows + ) { + PROXYW_BY_TARGET + .get(&target) + .ok_or_else(|| anyhow!("There is no python-proxyw for {target}")) + } else { + PROXY_BY_TARGET + .get(&target) + .ok_or_else(|| anyhow!("There is no python-proxy for {target}")) + }?; Ok(zstd::Decoder::new(proxy.contents)?) } diff --git a/uv.lock b/uv.lock index eeb0630..d08501f 100644 --- a/uv.lock +++ b/uv.lock @@ -1236,6 +1236,8 @@ dev = [ { name = "mypy", version = "1.19.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, { name = "mypy", version = "1.20.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "pex" }, + { name = "psutil", version = "6.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.6'" }, + { name = "psutil", version = "7.2.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.6'" }, { name = "pytest", version = "4.6.11", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.5'" }, { name = "pytest", version = "6.1.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.5.*'" }, { name = "pytest", version = "7.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.6.*'" }, @@ -1251,6 +1253,11 @@ dev = [ { name = "pytest-xdist", version = "3.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, { name = "ruff", version = "0.0.17", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.7'" }, { name = "ruff", version = "0.15.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.7'" }, + { name = "types-psutil", version = "5.9.5.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.7'" }, + { name = "types-psutil", version = "5.9.5.17", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.7.*'" }, + { name = "types-psutil", version = "6.1.0.20241221", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, + { name = "types-psutil", version = "7.2.2.20260130", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "types-psutil", version = "7.2.2.20260518", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "types-setuptools", version = "68.2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.7'" }, { name = "types-setuptools", version = "69.0.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.7.*'" }, { name = "types-setuptools", version = "75.8.0.20250110", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, @@ -1267,9 +1274,11 @@ dev = [ { name = "dev-cmd", extras = ["old-pythons"], marker = "python_full_version >= '3.8'" }, { name = "mypy" }, { name = "pex" }, + { name = "psutil" }, { name = "pytest" }, { name = "pytest-xdist" }, { name = "ruff" }, + { name = "types-psutil" }, { name = "types-setuptools" }, ] @@ -1359,6 +1368,68 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "psutil" +version = "6.1.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.5.*'", + "python_full_version == '3.4.*' and sys_platform == 'win32'", + "python_full_version >= '2.8' and python_full_version < '3.4' and sys_platform == 'win32'", + "python_full_version == '3.4.*' and sys_platform != 'win32'", + "python_full_version >= '2.8' and python_full_version < '3.4' and sys_platform != 'win32'", + "python_full_version < '2.8' and sys_platform == 'win32'", + "python_full_version < '2.8' and sys_platform != 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/5a/07871137bb752428aa4b659f910b399ba6f291156bdea939be3e96cae7cb/psutil-6.1.1.tar.gz", hash = "sha256:cf8496728c18f2d0b45198f06895be52f36611711746b7f30c464b422b50e2f5", size = 508502, upload-time = "2024-12-19T18:21:20.568Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/99/ca79d302be46f7bdd8321089762dd4476ee725fce16fc2b2e1dbba8cac17/psutil-6.1.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:fc0ed7fe2231a444fc219b9c42d0376e0a9a1a72f16c5cfa0f68d19f1a0663e8", size = 247511, upload-time = "2024-12-19T18:21:45.163Z" }, + { url = "https://files.pythonhosted.org/packages/0b/6b/73dbde0dd38f3782905d4587049b9be64d76671042fdcaf60e2430c6796d/psutil-6.1.1-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0bdd4eab935276290ad3cb718e9809412895ca6b5b334f5a9111ee6d9aff9377", size = 248985, upload-time = "2024-12-19T18:21:49.254Z" }, + { url = "https://files.pythonhosted.org/packages/17/38/c319d31a1d3f88c5b79c68b3116c129e5133f1822157dd6da34043e32ed6/psutil-6.1.1-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6e06c20c05fe95a3d7302d74e7097756d4ba1247975ad6905441ae1b5b66003", size = 284488, upload-time = "2024-12-19T18:21:51.638Z" }, + { url = "https://files.pythonhosted.org/packages/9c/39/0f88a830a1c8a3aba27fededc642da37613c57cbff143412e3536f89784f/psutil-6.1.1-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97f7cb9921fbec4904f522d972f0c0e1f4fabbdd4e0287813b21215074a0f160", size = 287477, upload-time = "2024-12-19T18:21:55.306Z" }, + { url = "https://files.pythonhosted.org/packages/47/da/99f4345d4ddf2845cb5b5bd0d93d554e84542d116934fde07a0c50bd4e9f/psutil-6.1.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33431e84fee02bc84ea36d9e2c4a6d395d479c9dd9bba2376c1f6ee8f3a4e0b3", size = 289017, upload-time = "2024-12-19T18:21:57.875Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1f/1aebe4dd5914ccba6f7d6cc6d11fb79f6f23f95b858a7f631446bdc5d67f/psutil-6.1.1-cp36-cp36m-win32.whl", hash = "sha256:384636b1a64b47814437d1173be1427a7c83681b17a450bfc309a1953e329603", size = 252576, upload-time = "2024-12-19T18:22:01.852Z" }, + { url = "https://files.pythonhosted.org/packages/f4/de/fb4561e59611c19a2d7377c2b2534d11274b8a7df9bb7b7e7f1de5be3641/psutil-6.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8be07491f6ebe1a693f17d4f11e69d0dc1811fa082736500f649f79df7735303", size = 258012, upload-time = "2024-12-19T18:22:04.204Z" }, + { url = "https://files.pythonhosted.org/packages/38/53/bd755c2896f4461fd4f36fa6a6dcb66a88a9e4b9fd4e5b66a77cf9d4a584/psutil-6.1.1-cp37-abi3-win32.whl", hash = "sha256:eaa912e0b11848c4d9279a93d7e2783df352b082f40111e078388701fd479e53", size = 250602, upload-time = "2024-12-19T18:22:08.808Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d7/7831438e6c3ebbfa6e01a927127a6cb42ad3ab844247f3c5b96bea25d73d/psutil-6.1.1-cp37-abi3-win_amd64.whl", hash = "sha256:f35cfccb065fff93529d2afb4a2e89e363fe63ca1e4a5da22b603a85833c2649", size = 254444, upload-time = "2024-12-19T18:22:11.335Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", + "python_full_version == '3.8.*'", + "python_full_version == '3.7.*'", + "python_full_version >= '3.6.8' and python_full_version < '3.7'", + "python_full_version >= '3.6' and python_full_version < '3.6.8'", +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + [[package]] name = "py" version = "1.11.0" @@ -2039,6 +2110,74 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/30/bd/c815051404c4293265634d9d3e292f04fcf681d0502a9484c38b8f224d04/typed_ast-1.5.5-cp39-cp39-win_amd64.whl", hash = "sha256:335f22ccb244da2b5c296e6f96b06ee9bed46526db0de38d2f0e5a6597b81155", size = 139486, upload-time = "2023-07-04T18:38:07.249Z" }, ] +[[package]] +name = "types-psutil" +version = "5.9.5.16" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.6.8' and python_full_version < '3.7'", + "python_full_version >= '3.6' and python_full_version < '3.6.8'", + "python_full_version == '3.5.*'", + "python_full_version == '3.4.*' and sys_platform == 'win32'", + "python_full_version >= '2.8' and python_full_version < '3.4' and sys_platform == 'win32'", + "python_full_version == '3.4.*' and sys_platform != 'win32'", + "python_full_version >= '2.8' and python_full_version < '3.4' and sys_platform != 'win32'", + "python_full_version < '2.8' and sys_platform == 'win32'", + "python_full_version < '2.8' and sys_platform != 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f0/82/faffe83ad8bf945797ed7c1176f7539d2ae5e7b16a4fd6ce634282158746/types-psutil-5.9.5.16.tar.gz", hash = "sha256:4e9b219efb625d3d04f6bf106934f87cab49aa41a94b0a3b3089403f47a79228", size = 14060, upload-time = "2023-07-20T15:19:03.065Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/0a/fd3b20a2ad38da85fde049ec013930d546b2a77300a9d5cf407161fcacf1/types_psutil-5.9.5.16-py3-none-any.whl", hash = "sha256:fec713104d5d143afea7b976cfa691ca1840f5d19e8714a5d02a96ebd061363e", size = 17737, upload-time = "2023-07-20T15:19:02.083Z" }, +] + +[[package]] +name = "types-psutil" +version = "5.9.5.17" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.7.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/58/c53d349f954ade9c85841900971ff2989fe2f0e98e0d935bba968665e03a/types-psutil-5.9.5.17.tar.gz", hash = "sha256:f7d8769812d72a4b513d7ec9eb5580fe2f6013fc270394a603cb6534811f3e4d", size = 14260, upload-time = "2023-10-16T02:17:16.285Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/47/aa7a8314e298a76df914d8f9966c71774b28f283f2faf21e13b5a342395e/types_psutil-5.9.5.17-py3-none-any.whl", hash = "sha256:2161d166256084acf629d30aaf6bda8bee726ae1fea530559650281056b491fc", size = 17891, upload-time = "2023-10-16T02:17:14.643Z" }, +] + +[[package]] +name = "types-psutil" +version = "6.1.0.20241221" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.8.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/44/8c/5f82cd554cc5bb79d137f082e4c9f8d22e85c8c08dabee4971d422a9abdd/types_psutil-6.1.0.20241221.tar.gz", hash = "sha256:600f5a36bd5e0eb8887f0e3f3ff2cf154d90690ad8123c8a707bba4ab94d3185", size = 20035, upload-time = "2024-12-21T02:40:18.665Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/8f/eda6f25fe59e683b875108f520dfaf683dd45aed59051ec55b9470d869b7/types_psutil-6.1.0.20241221-py3-none-any.whl", hash = "sha256:8498dbe13285a9ba7d4b2fa934c569cc380efc74e3dacdb34ae16d2cdf389ec3", size = 23361, upload-time = "2024-12-21T02:40:17.393Z" }, +] + +[[package]] +name = "types-psutil" +version = "7.2.2.20260130" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/69/14/fc5fb0a6ddfadf68c27e254a02ececd4d5c7fdb0efcb7e7e917a183497fb/types_psutil-7.2.2.20260130.tar.gz", hash = "sha256:15b0ab69c52841cf9ce3c383e8480c620a4d13d6a8e22b16978ebddac5590950", size = 26535, upload-time = "2026-01-30T03:58:14.116Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/d7/60974b7e31545d3768d1770c5fe6e093182c3bfd819429b33133ba6b3e89/types_psutil-7.2.2.20260130-py3-none-any.whl", hash = "sha256:15523a3caa7b3ff03ac7f9b78a6470a59f88f48df1d74a39e70e06d2a99107da", size = 32876, upload-time = "2026-01-30T03:58:13.172Z" }, +] + +[[package]] +name = "types-psutil" +version = "7.2.2.20260518" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/f1/6901857281d4e8d792492e1495eef6f4f01318a3b6a066486d81000a4511/types_psutil-7.2.2.20260518.tar.gz", hash = "sha256:9f825f631463a5b4d26f19f63aebc9ec25f01140d655026f3ad8a67841f9b331", size = 26660, upload-time = "2026-05-18T06:05:09.389Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/eb/f726339668879819599c74c2e1f0cab760912a4159046942bdae2ad37bd6/types_psutil-7.2.2.20260518-py3-none-any.whl", hash = "sha256:6a3d697665754a60d7b5a41d5a2cff12b53f5e0676d77810cd28ba5e14cb4049", size = 32820, upload-time = "2026-05-18T06:05:08.321Z" }, +] + [[package]] name = "types-setuptools" version = "68.2.0.0"