From 8181ecda3f91e1579fc513e32fac8bdbde6c86b9 Mon Sep 17 00:00:00 2001 From: razorblade23 Date: Tue, 13 Jan 2026 21:23:49 +0100 Subject: [PATCH 1/5] Now we stream only uv binary from the archive from their GitHub Releases --- .../workflows/better-uv-download-logic.yml | 81 +++++ pycrucible/src/payload.rs | 9 +- runner/src/run.rs | 2 +- shared/src/lib.rs | 4 +- shared/src/uv_handler/download.rs | 35 ++ shared/src/uv_handler/extract.rs | 67 ++++ shared/src/uv_handler/install.rs | 126 +++++++ shared/src/uv_handler/mod.rs | 12 +- shared/src/uv_handler/platform.rs | 13 + shared/src/uv_handler/uv_handler_core.rs | 131 ------- shared/src/uv_handler/uv_handler_unix.rs | 26 -- shared/src/uv_handler/uv_handler_windows.rs | 142 -------- shared/src/uv_handler_v2.rs | 328 ------------------ 13 files changed, 337 insertions(+), 639 deletions(-) create mode 100644 .github/workflows/better-uv-download-logic.yml create mode 100644 shared/src/uv_handler/download.rs create mode 100644 shared/src/uv_handler/extract.rs create mode 100644 shared/src/uv_handler/install.rs create mode 100644 shared/src/uv_handler/platform.rs delete mode 100644 shared/src/uv_handler/uv_handler_core.rs delete mode 100644 shared/src/uv_handler/uv_handler_unix.rs delete mode 100644 shared/src/uv_handler/uv_handler_windows.rs delete mode 100644 shared/src/uv_handler_v2.rs diff --git a/.github/workflows/better-uv-download-logic.yml b/.github/workflows/better-uv-download-logic.yml new file mode 100644 index 0000000..a09e9ae --- /dev/null +++ b/.github/workflows/better-uv-download-logic.yml @@ -0,0 +1,81 @@ +name: Test without uv embedding + +on: + push: + branches: [better-uv-download-logic] + +env: + CARGO_TERM_COLOR: always + +jobs: + test: + name: Run Tests (Linux) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + + - name: Build runner + run: cargo build -p pycrucible_runner --target x86_64-unknown-linux-gnu + + - name: Run tests + run: | + cargo test -p pycrucible --target x86_64-unknown-linux-gnu + cargo test -p shared --target x86_64-unknown-linux-gnu + + smoke-test: + name: Smoke Test PyCrucible without uv embedding + needs: test + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + # Linux + - runner: ubuntu-latest + target: x86_64-unknown-linux-gnu + bin_ext: "" + simple: linux + # Windows + - runner: windows-latest + target: x86_64-pc-windows-msvc + bin_ext: ".exe" + simple: windows + # macOS + - runner: macos-latest + target: aarch64-apple-darwin + bin_ext: "" + simple: macos + + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + + - name: Build runner + run: cargo build -p pycrucible_runner --release --target ${{ matrix.target }} + + - name: Build PyCrucible + run: cargo build -p pycrucible --release --target ${{ matrix.target }} + + - name: Clone example project + run: git clone https://github.com/razorblade23/test-project-for-PyCrucible + + - name: Extract and copy binary + shell: bash + run: | + ls ./target/${{ matrix.target }}/release + cp ./target/${{ matrix.target }}/release/pycrucible${{ matrix.bin_ext }} test-project-for-PyCrucible/pycrucible${{ matrix.bin_ext }} + + - name: Make binary executable (non-Windows) + if: runner.os != 'Windows' + run: chmod +x test-project-for-PyCrucible/pycrucible${{ matrix.bin_ext }} + + - name: Run PyCrucible + working-directory: test-project-for-PyCrucible + run: ./pycrucible${{ matrix.bin_ext }} -e . -o cowsay${{ matrix.bin_ext }} --no-uv-embed --debug + + - name: Smoke-test generated artifact + working-directory: test-project-for-PyCrucible + run: | + ls + ./cowsay${{ matrix.bin_ext }} \ No newline at end of file diff --git a/pycrucible/src/payload.rs b/pycrucible/src/payload.rs index a4114b8..59b4399 100644 --- a/pycrucible/src/payload.rs +++ b/pycrucible/src/payload.rs @@ -2,7 +2,7 @@ use crate::{config, runner}; use crate::{debug_println, project}; -use shared::uv_handler_v2::{download_and_install_uv_v2, find_or_download_uv}; +use shared::uv_handler::find_or_download_uv; use std::fs::{self, OpenOptions}; use std::io::{self, Cursor, Seek, SeekFrom, Write}; use std::path::{Path, PathBuf}; @@ -178,7 +178,12 @@ pub fn embed_payload( debug_println!( "[payload.embed_payload] - Force uv download flag is set, re-downloading uv" ); - download_and_install_uv_v2(&cli_options.uv_path); + let uv_path = if cli_options.uv_path.exists() { + Some(cli_options.uv_path.clone()) + } else { + None + }; + find_or_download_uv(uv_path); } debug_println!("[payload.embed_payload] - Looking for uv binary to embed"); if let Some(_path) = embed_uv(cli_options.uv_path, &mut zip, options)? { diff --git a/runner/src/run.rs b/runner/src/run.rs index 7fa3325..df3bc0e 100644 --- a/runner/src/run.rs +++ b/runner/src/run.rs @@ -3,7 +3,7 @@ use std::path::Path; use std::path::PathBuf; use std::process::Command; use std::{self, io}; -use shared::uv_handler_v2::find_or_download_uv; +use shared::uv_handler::find_or_download_uv; use shared::{debug_println, debuging}; use shared::config::{ProjectConfig, load_project_config}; diff --git a/shared/src/lib.rs b/shared/src/lib.rs index 6df8da1..cfc810a 100644 --- a/shared/src/lib.rs +++ b/shared/src/lib.rs @@ -2,14 +2,14 @@ pub mod config; pub mod debuging; pub mod footer; pub mod spinner; -pub mod uv_handler_v2; +pub mod uv_handler; // pub mod uv_handler; pub use config::*; pub use debuging::*; pub use footer::{FOOTER_SIZE, PayloadInfo}; pub use spinner::*; -pub use uv_handler_v2::{find_or_download_uv, download_and_install_uv_v2}; +pub use uv_handler::install_uv; // pub use uv_handler::uv_handler_core::find_or_download_uv; pub static PYCRUCIBLE_RUNNER_NAME: &str = if cfg!(target_os = "windows") { diff --git a/shared/src/uv_handler/download.rs b/shared/src/uv_handler/download.rs new file mode 100644 index 0000000..2e22272 --- /dev/null +++ b/shared/src/uv_handler/download.rs @@ -0,0 +1,35 @@ +use reqwest::blocking::get; +use std::io::Cursor; + +pub enum Archive<'a> { + Zip(Cursor>), + TarGz(&'a mut reqwest::blocking::Response), +} + +pub fn build_release_url(version: &str, target: &str) -> String { + let ext = if target.contains("windows") { + "zip" + } else { + "tar.gz" + }; + + format!( + "https://github.com/astral-sh/uv/releases/download/{v}/uv-{target}.{ext}", + v = version + ) +} + +pub fn download(url: &str) -> Result> { + let response = get(url)?.error_for_status()?; + if url.ends_with(".zip") { + let bytes = response.bytes()?.to_vec(); + Ok(DownloadResult::Zip(Cursor::new(bytes))) + } else { + Ok(DownloadResult::TarGz(response)) + } +} + +pub enum DownloadResult { + Zip(Cursor>), + TarGz(reqwest::blocking::Response), +} diff --git a/shared/src/uv_handler/extract.rs b/shared/src/uv_handler/extract.rs new file mode 100644 index 0000000..05f63da --- /dev/null +++ b/shared/src/uv_handler/extract.rs @@ -0,0 +1,67 @@ +use crate::uv_handler::download::Archive; +use std::fs; +use std::path::Path; +use zip::ZipArchive; +use flate2::read::GzDecoder; +#[cfg(unix)] +use std::os::unix::fs::PermissionsExt; + +pub fn extract_uv<'a>( + archive: &mut Archive<'a>, + install_dir: &Path, +) -> Result<(), Box> { + fs::create_dir_all(install_dir)?; + + match archive { + Archive::Zip(reader) => extract_zip(reader, install_dir), + Archive::TarGz(response) => extract_targz(*response, install_dir), + } +} + +fn extract_zip( + reader: &std::io::Cursor>, + install_dir: &std::path::Path, +) -> Result<(), Box> { + // NOTE: The zip crate requires the full archive in memory for random access. + // True streaming extraction is not possible with this crate. + // This function extracts only "uv.exe" and ignores the rest. + let mut zip = ZipArchive::new(reader.clone())?; + if let Ok(mut file) = zip.by_name("uv.exe") { + let out_path = install_dir.join("uv.exe"); + let mut out = std::fs::File::create(out_path)?; + std::io::copy(&mut file, &mut out)?; + } else { + return Err("uv.exe not found in zip archive".into()); + } + Ok(()) +} + +fn extract_targz( + response: &mut reqwest::blocking::Response, + install_dir: &std::path::Path, +) -> Result<(), Box> { + // Streaming extraction: only extract the "uv" binary, skip all other entries. + let decoder = GzDecoder::new(response); + let mut archive = tar::Archive::new(decoder); + let mut found = false; + for entry in archive.entries()? { + let mut entry = entry?; + if let Ok(path) = entry.path() { + if let Some(file_name) = path.file_name() { + if file_name == "uv" { + let out_path = install_dir.join("uv"); + let mut out = std::fs::File::create(&out_path)?; + std::io::copy(&mut entry, &mut out)?; + #[cfg(unix)] + std::fs::set_permissions(&out_path, std::fs::Permissions::from_mode(0o755))?; + found = true; + break; + } + } + } + } + if !found { + return Err("uv not found in tar.gz archive".into()); + } + Ok(()) +} diff --git a/shared/src/uv_handler/install.rs b/shared/src/uv_handler/install.rs new file mode 100644 index 0000000..95d1b05 --- /dev/null +++ b/shared/src/uv_handler/install.rs @@ -0,0 +1,126 @@ +use std::path::Path; +use crate::uv_handler::platform; +use crate::uv_handler::download; +use crate::uv_handler::extract; +use crate::debug_println; +use std::path::PathBuf; +use crate::{create_spinner_with_message, stop_and_persist_spinner_with_message}; + +pub fn install_uv(version: &str, install_dir: &Path) -> Result<(), Box> { + let target = platform::target_triple(); + let url = download::build_release_url(version, &target); + + let mut download_result = download::download(&url)?; + match download_result { + download::DownloadResult::Zip(ref reader) => { + let mut archive = download::Archive::Zip(reader.clone()); + extract::extract_uv(&mut archive, install_dir)?; + } + download::DownloadResult::TarGz(ref mut response) => { + let mut archive = download::Archive::TarGz(response); + extract::extract_uv(&mut archive, install_dir)?; + } + } + Ok(()) +} + +pub fn uv_exists(path: &PathBuf) -> Option { + let candidates = [ + path.join("uv"), + path.join("uv.exe"), + path.join("bin").join("uv"), + path.join("bin").join("uv.exe"), + ]; + + let uv_bin = match candidates.iter().find(|p| p.exists()).cloned() { + Some(p) => p, + None => { + eprintln!("uv binary not found."); + return None; + } + }; + Some(uv_bin) +} + +pub fn find_or_download_uv(cli_uv_path: Option) -> Option { + debug_println!("[uv_handler.find_or_download_uv] - Looking for uv"); + + let exe_dir = std::env::current_exe().expect("Could not find current working directory. Exiting ....").parent().unwrap().to_path_buf(); + debug_println!("[uv_handler.find_or_download_uv] - Current working directory: {:?}", exe_dir); + let local_uv = if cli_uv_path.is_some() { + debug_println!("CLI supplied uv path detected, using it"); + let lc_uv = Some(cli_uv_path.unwrap()); + if lc_uv.as_ref().unwrap().exists() { + debug_println!("CLI supplied uv path exists"); + lc_uv + } else { + debug_println!("CLI supplied uv path does not exist"); + None + } + } else if let Ok(path) = which::which("uv") { + debug_println!("`which` returned uv path, using it"); + Some(path) + } else { + let local_uv_path = exe_dir.join("uv"); + if local_uv_path.exists() { + debug_println!("Found uv next to binary, using it"); + Some(local_uv_path) + } else { + None + } + }; + let uv_path = if local_uv.is_some() { + debug_println!("[uv_handler.find_or_download_uv] - uv found locally [{:?}], using it", local_uv.as_ref().unwrap().canonicalize()); + local_uv + } else { + debug_println!("[uv_handler.find_or_download_uv] - uv not found locally, lets see if we have it cached ..."); + let home = dirs::home_dir().unwrap(); + + let uv_install_root = home.join(".pycrucible").join("cache").join("uv"); + + let uv_bin = uv_exists(&uv_install_root); + if uv_bin.is_some() { + debug_println!("[uv_handler.find_or_download_uv] - uv found cached at {:?}, using it", uv_bin.as_ref().unwrap()); + return uv_bin; + } + + debug_println!("[uv_handler.find_or_download_uv] - uv binary not found locally, proceeding to download."); + let sp = create_spinner_with_message("Downloading `uv` ..."); + install_uv("0.9.21", &uv_install_root).expect("uv installation failed"); + stop_and_persist_spinner_with_message(sp, "Downloaded `uv` successfully"); + + let uv_bin = uv_exists(&uv_install_root); + if uv_bin.is_some() { + debug_println!("[uv_handler.find_or_download_uv] - uv downloaded and found at {:?}, using it", uv_bin.as_ref().unwrap()); + return uv_bin; + } + + return Some(uv_bin.expect("uv binary should exist after download")); + }; + + #[cfg(unix)] + { + use std::{fs, os::unix::fs::PermissionsExt}; + + if let Some(ref path) = uv_path { + if path.exists() { + let mut perms = fs::metadata(path) + .expect("Could not stat uv binary") + .permissions(); + let current_mode = perms.mode() & 0o777; + if current_mode == 0o755 { + debug_println!("[uv_handler.find_or_download_uv] - uv permissions already 0o755, skipping chmod for {:?}", path); + return uv_path.clone(); + } + + perms.set_mode(0o755); + fs::set_permissions(path, perms) + .expect("Could not chmod uv binary"); + debug_println!("[uv_handler.find_or_download_uv] - Set executable permissions for uv at {:?}", path); + } else { + eprintln!("uv binary not found at {:?}", path); + } + } + } + uv_path +} \ No newline at end of file diff --git a/shared/src/uv_handler/mod.rs b/shared/src/uv_handler/mod.rs index 2d6bc0b..1cfa252 100644 --- a/shared/src/uv_handler/mod.rs +++ b/shared/src/uv_handler/mod.rs @@ -1,8 +1,6 @@ -pub mod uv_handler_core; +mod install; +mod platform; +mod download; +mod extract; -#[cfg(unix)] -pub mod uv_handler_unix; -#[cfg(target_os = "windows")] -pub mod uv_handler_windows; - -pub use uv_handler_core::{download_and_install_uv, find_or_download_uv, uv_exists}; +pub use install::{install_uv, find_or_download_uv}; \ No newline at end of file diff --git a/shared/src/uv_handler/platform.rs b/shared/src/uv_handler/platform.rs new file mode 100644 index 0000000..237ab3d --- /dev/null +++ b/shared/src/uv_handler/platform.rs @@ -0,0 +1,13 @@ +pub fn target_triple() -> String { + let os = std::env::consts::OS; + let arch = std::env::consts::ARCH; + + match (os, arch) { + ("windows", "x86_64") => "x86_64-pc-windows-msvc", + ("linux", "x86_64") => "x86_64-unknown-linux-gnu", + ("macos", "x86_64") => "x86_64-apple-darwin", + ("macos", "aarch64") => "aarch64-apple-darwin", + _ => panic!("Unsupported platform: {os}/{arch}"), + } + .to_string() +} diff --git a/shared/src/uv_handler/uv_handler_core.rs b/shared/src/uv_handler/uv_handler_core.rs deleted file mode 100644 index adc90ad..0000000 --- a/shared/src/uv_handler/uv_handler_core.rs +++ /dev/null @@ -1,131 +0,0 @@ -use crate::debug_println; -use crate::spinner::{create_spinner_with_message, stop_and_persist_spinner_with_message}; -use std::path::PathBuf; - -pub fn uv_exists(path: &PathBuf) -> Option { - let candidates = [ - path.join("uv"), - path.join("uv.exe"), - path.join("bin").join("uv"), - path.join("bin").join("uv.exe"), - ]; - - let uv_bin = match candidates.iter().find(|p| p.exists()).cloned() { - Some(p) => p, - None => { - eprintln!("uv binary not found."); - return None; - } - }; - Some(uv_bin) -} - -pub fn download_and_install_uv(install_path: &PathBuf) { - #[cfg(unix)] - { - use crate::uv_handler::uv_handler_unix::install_uv_unix; - let installation_status = install_uv_unix(install_path); - if installation_status.is_err() { - eprintln!( - "uv installation via script failed: {}", - installation_status.err().unwrap() - ); - } - }; - #[cfg(target_os = "windows")] - { - use crate::uv_handler::uv_handler_windows::install_uv_windows; - let installation_status = install_uv_windows(install_path); - if installation_status.is_err() { - eprintln!( - "uv installation via script or direct download failed: {}", - installation_status.err().unwrap() - ); - } - }; -} - -pub fn find_or_download_uv(cli_uv_path: Option) -> Option { - debug_println!("[uv_handler.find_or_download_uv] - Looking for uv"); - - let exe_dir = std::env::current_exe() - .expect("Could not find current working directory. Exiting ....") - .parent() - .unwrap() - .to_path_buf(); - let local_uv = if let Some(p) = cli_uv_path { - Some(p) - } else if let Ok(path) = which::which("uv") { - Some(path) - } else { - let local_uv_path = exe_dir.join("uv"); - if local_uv_path.exists() { - Some(local_uv_path) - } else { - None - } - }; - let uv_path = if local_uv.is_some() { - debug_println!("[uv_handler.find_or_download_uv] - uv found locally, using it"); - local_uv - } else { - debug_println!( - "[uv_handler.find_or_download_uv] - uv not found locally, lets see if we have it cached ..." - ); - let home = dirs::home_dir().unwrap(); - - let uv_install_root = home.join(".pycrucible").join("cache").join("uv"); - - let uv_bin = uv_exists(&uv_install_root); - if uv_bin.is_some() { - debug_println!( - "[uv_handler.find_or_download_uv] - uv found cached at {:?}, using it", - uv_bin.as_ref().unwrap() - ); - return uv_bin; - } - - debug_println!( - "[uv_handler.find_or_download_uv] - uv binary not found locally, proceeding to download." - ); - let sp = create_spinner_with_message("Downloading `uv` ..."); - download_and_install_uv(&uv_install_root); - stop_and_persist_spinner_with_message(sp, "Downloaded `uv` successfully"); - - let uv_bin = uv_exists(&uv_install_root); - if uv_bin.is_some() { - debug_println!( - "[uv_handler.find_or_download_uv] - uv downloaded and found at {:?}, using it", - uv_bin.as_ref().unwrap() - ); - return uv_bin; - } - - return Some(uv_bin.expect("uv binary should exist after download")); - }; - - #[cfg(unix)] - { - use std::{fs, os::unix::fs::PermissionsExt}; - - if let Some(ref path) = uv_path { - if path.exists() { - let mut perms = fs::metadata(path) - .expect("Could not stat uv binary") - .permissions(); - let current_mode = perms.mode() & 0o777; - // Skip chmoding if permission is already set - if current_mode == 0o755 { - return uv_path.clone(); - } - - // Otherwise set permissions to execute the file - perms.set_mode(0o755); - fs::set_permissions(path, perms).expect("Could not chmod uv binary"); - } else { - eprintln!("uv binary not found at {:?}", path); - } - } - } - uv_path -} diff --git a/shared/src/uv_handler/uv_handler_unix.rs b/shared/src/uv_handler/uv_handler_unix.rs deleted file mode 100644 index 8bdbdd0..0000000 --- a/shared/src/uv_handler/uv_handler_unix.rs +++ /dev/null @@ -1,26 +0,0 @@ -use std::{path::PathBuf, process::Command, process::Stdio}; - -#[cfg(unix)] -pub fn install_uv_unix(install_path: &PathBuf) -> Result<(), String> { - let mut curl = Command::new("curl") - .args(["-sL", "https://astral.sh/uv/install.sh"]) - .stdout(Stdio::piped()) - .spawn() - .expect("Failed to start curl"); - - let mut sh = Command::new("sh") - .env("UV_UNMANAGED_INSTALL", install_path) - .stdin(Stdio::from(curl.stdout.take().unwrap())) - .stdout(Stdio::null()) - .spawn() - .expect("Failed to start shell"); - - let curl_status = curl.wait().expect("Failed to wait for curl"); - let sh_status = sh.wait().expect("Failed to wait for sh"); - - if !curl_status.success() || !sh_status.success() { - eprintln!("Installation failed."); - return Err("Installation of uv failed.".to_string()); - } - Ok(()) -} diff --git a/shared/src/uv_handler/uv_handler_windows.rs b/shared/src/uv_handler/uv_handler_windows.rs deleted file mode 100644 index 0c09aa0..0000000 --- a/shared/src/uv_handler/uv_handler_windows.rs +++ /dev/null @@ -1,142 +0,0 @@ -use crate::debug_println; -use crate::uv_handler::uv_exists; -use std::{fs::File, io, path::Path, path::PathBuf, process::Command}; -use {tempfile::tempdir, zip::ZipArchive}; - -fn is_ci() -> bool { - std::env::var("CI").is_ok() || std::env::var("GITHUB_ACTIONS").is_ok() -} - -pub fn install_uv_windows(install_path: &PathBuf) -> Result<(), String> { - if is_ci() { - debug_println!("CI detected — using fallback UV binary download"); - download_uv_binary_for_windows(install_path); - return Ok(()); - } - - debug_println!("Attempting UV install using PowerShell script..."); - - let script_result = install_uv_via_powershell_script(install_path); - - match script_result { - Ok(_) => { - debug_println!("UV installer script completed, checking if binary was created..."); - if uv_exists(install_path).is_some() { - debug_println!("UV installed successfully via script."); - return Ok(()); - } else { - debug_println!( - "UV script exited OK but no binary found — falling back to direct download.", - ); - } - } - Err(err) => { - debug_println!("UV installer script failed: {err}"); - debug_println!("Falling back to direct binary download..."); - } - } - - download_uv_binary_for_windows(install_path); - - if !uv_exists(install_path).is_some() { - eprintln!("Failed both installer script AND fallback binary download. Cannot continue."); - return Err("Failed to install uv via both script and direct download.".to_string()); - } - Ok(()) -} - -fn install_uv_via_powershell_script(install_path: &PathBuf) -> Result<(), String> { - let status = Command::new("powershell") - .args([ - "-NoProfile", - "-NonInteractive", - "-ExecutionPolicy", - "Bypass", - "-Command", - "irm https://astral.sh/uv/install.ps1 | iex", - ]) - .env("UV_UNMANAGED_INSTALL", install_path) - .status() - .map_err(|e| format!("Failed to spawn PowerShell: {e}"))?; - - if !status.success() { - return Err(format!( - "PowerShell installer returned exit code {}", - status - )); - } - - Ok(()) -} - -fn download_uv_binary_for_windows(install_path: &Path) { - let dir = tempdir(); - match dir { - Err(e) => panic!( - "Failed to create temporary directory for uv download: {}", - e - ), - Ok(d) => { - let uv_temp = d.path().join("uv-windows.zip"); - let url = "https://github.com/astral-sh/uv/releases/download/0.8.5/uv-x86_64-pc-windows-msvc.zip"; - - let status = Command::new("powershell") - .args([ - "-NoProfile", - "-NonInteractive", - "-Command", - &format!( - "Invoke-WebRequest '{}' -OutFile '{}'", - url, - uv_temp.display() - ), - ]) - .status() - .expect("Failed to run PowerShell for binary download"); - - if !status.success() { - panic!("Direct download of uv.exe failed"); - } - debug_println!("Downloaded uv archive to {:?}", uv_temp); - - extract_uv_from_zip_archive(&uv_temp, install_path) - .expect("Failed to extract uv from zip archive"); - } - }; -} - -fn extract_uv_from_zip_archive( - archive_path: &Path, - install_path: &Path, -) -> Result<(), Box> { - debug_println!("Extracting uv from archive {:?}", archive_path); - debug_println!("Extracting uv to {:?}", install_path); - if !install_path.exists() { - debug_println!("Filepath to cache do not exists, creating ..."); - std::fs::create_dir_all(install_path)?; - debug_println!("Created install directory {:?}", install_path); - } - - // Open the archive file - let file = File::open(archive_path)?; - let mut archive = ZipArchive::new(file)?; - debug_println!("Opened zip archive, contains {} files", archive.len()); - - let uv_binary_path = install_path.join("uv.exe"); - - // Iterate through files inside the ZIP - for i in 0..archive.len() { - let mut file = archive.by_index(i)?; - - if file.name().contains("uv.exe") { - debug_println!("Found uv.exe in archive at {}, extracting...", file.name()); - let mut outfile = File::create(&uv_binary_path)?; - debug_println!("Created output file at {:?}", uv_binary_path); - io::copy(&mut file, &mut outfile)?; - debug_println!("Extracted uv.exe to {:?}", uv_binary_path); - return Ok(()); - } - } - - Err("uv.exe not found in archive".into()) -} diff --git a/shared/src/uv_handler_v2.rs b/shared/src/uv_handler_v2.rs deleted file mode 100644 index a36319e..0000000 --- a/shared/src/uv_handler_v2.rs +++ /dev/null @@ -1,328 +0,0 @@ -use std::{path::PathBuf, process::Command, process::Stdio}; -use crate::debug_println; -use crate::spinner::{create_spinner_with_message, stop_and_persist_spinner_with_message}; - -#[cfg(target_os = "windows")] -use { - tempfile::tempdir, - zip::ZipArchive, - std::fs::File, - std::io::{self, Write}, - std::path::Path, -}; - -fn uv_exists(path: &PathBuf) -> Option { - let candidates = vec![ - path.join("uv"), - path.join("uv.exe"), - path.join("bin").join("uv"), - path.join("bin").join("uv.exe"), - ]; - - let uv_bin = match candidates.iter().find(|p| p.exists()).cloned() { - Some(p) => p, - None => { - eprintln!("uv binary not found."); - return None; - } - }; - Some(uv_bin) -} - -#[cfg(target_os = "windows")] -fn is_ci() -> bool { - std::env::var("CI").is_ok() || std::env::var("GITHUB_ACTIONS").is_ok() -} - -#[cfg(target_os = "windows")] -fn install_uv_windows(install_path: &PathBuf) -> Result<(), String> { - if is_ci() { - println!("CI detected — using fallback UV binary download"); - download_uv_binary_for_windows(install_path); - return Ok(()); - } - - println!("Attempting UV install using PowerShell script..."); - - let script_result = install_uv_via_powershell_script(install_path); - - match script_result { - Ok(_) => { - println!("UV installer script completed, checking if binary was created..."); - if uv_exists(install_path).is_some() { - println!("UV installed successfully via script."); - return Ok(()); - } else { - println!("UV script exited OK but no binary found — falling back to direct download."); - } - } - Err(err) => { - println!("UV installer script failed: {err}"); - println!("Falling back to direct binary download..."); - } - } - - download_uv_binary_for_windows(install_path); - - if !uv_exists(install_path).is_some() { - eprintln!("Failed both installer script AND fallback binary download. Cannot continue."); - return Err("Failed to install uv via both script and direct download.".to_string()); - } - Ok(()) -} - -#[cfg(target_os = "windows")] -fn install_uv_via_powershell_script(install_path: &PathBuf) -> Result<(), String> { - let status = Command::new("powershell") - .args([ - "-NoProfile", - "-NonInteractive", - "-ExecutionPolicy", "Bypass", - "-Command", - "irm https://astral.sh/uv/install.ps1 | iex" - ]) - .env("UV_UNMANAGED_INSTALL", install_path) - .status() - .map_err(|e| format!("Failed to spawn PowerShell: {e}"))?; - - if !status.success() { - return Err(format!("PowerShell installer returned exit code {}", status)); - } - - Ok(()) -} - -#[cfg(target_os = "windows")] -fn download_uv_binary_for_windows(install_path: &Path) { - let dir = tempdir(); - match dir { - Err(e) => panic!("Failed to create temporary directory for uv download: {}", e), - Ok(d) => { - let uv_temp = d.path().join("uv-windows.zip"); - let url = "https://github.com/astral-sh/uv/releases/download/0.9.11/uv-aarch64-pc-windows-msvc.zip"; - - let status = Command::new("powershell") - .args([ - "-NoProfile", - "-NonInteractive", - "-Command", - &format!("Invoke-WebRequest '{}' -OutFile '{}'", url, uv_temp.display()), - ]) - .status() - .expect("Failed to run PowerShell for binary download"); - - if !status.success() { - panic!("Direct download of uv.exe failed"); - } - println!("Downloaded uv archive to {:?}", uv_temp); - - extract_uv_from_zip_archive(&uv_temp, install_path).expect("Failed to extract uv from zip archive"); - - }, - }; -} - -#[cfg(target_os = "windows")] -fn extract_uv_from_zip_archive( - archive_path: &Path, - install_path: &Path, -) -> Result<(), Box> { - println!("Extracting uv from archive {:?}", archive_path); - println!("Extracting uv to {:?}", install_path); - - // Open the archive file - let file = File::open(archive_path)?; - let mut archive = ZipArchive::new(file)?; - println!("Opened zip archive, contains {} files", archive.len()); - - let uv_binary_path = install_path.join("uv.exe"); - - // Iterate through files inside the ZIP - for i in 0..archive.len() { - let mut file = archive.by_index(i)?; - - if file.name().contains("uv.exe") { - println!("Found uv.exe in archive at {}, extracting...", file.name()); - let mut outfile = File::create(&uv_binary_path)?; - println!("Created output file at {:?}", uv_binary_path); - io::copy(&mut file, &mut outfile)?; - println!("Extracted uv.exe to {:?}", uv_binary_path); - return Ok(()); - } - } - - Err("uv.exe not found in archive".into()) -} - - -#[cfg(unix)] -fn install_uv_unix(install_path: &PathBuf) -> Result<(), String> { - let mut curl = Command::new("curl") - .args(["-sL", "https://astral.sh/uv/install.sh"]) - .stdout(Stdio::piped()) - .spawn() - .expect("Failed to start curl"); - - let mut sh = Command::new("sh") - .env("UV_UNMANAGED_INSTALL", install_path) - .stdin(Stdio::from(curl.stdout.take().unwrap())) - .stdout(Stdio::null()) - .spawn() - .expect("Failed to start shell"); - - let curl_status = curl.wait().expect("Failed to wait for curl"); - let sh_status = sh.wait().expect("Failed to wait for sh"); - - if !curl_status.success() || !sh_status.success() { - eprintln!("Installation failed."); - return Err("Installation of uv failed.".to_string()); - } - Ok(()) -} - -pub fn download_and_install_uv_v2(install_path: &PathBuf) { - #[cfg(unix)] - { - let installation_status = install_uv_unix(install_path); - if installation_status.is_err() { - eprintln!("uv installation via script failed: {}", installation_status.err().unwrap()); - } - }; - #[cfg(target_os = "windows")] - { - let installation_status = install_uv_windows(install_path); - if installation_status.is_err() { - eprintln!("uv installation via script or direct download failed: {}", installation_status.err().unwrap()); - } - }; -} - -// pub fn download_and_install_uv(install_path: &PathBuf) { -// let _status = if cfg!(target_os = "linux") || cfg!(target_os = "macos") { -// // Download and run the install script via sh if unix-based OS -// let mut wget = Command::new("wget") -// .args(["-qO-", "https://astral.sh/uv/install.sh"]) -// .stdout(Stdio::piped()) -// .spawn() -// .expect("Failed to start wget"); - -// let mut sh = Command::new("sh") -// .env("UV_UNMANAGED_INSTALL", install_path) -// .stdin(Stdio::from(wget.stdout.take().unwrap())) -// .stdout(Stdio::null()) -// .spawn() -// .expect("Failed to start shell"); - -// let wget_status = wget.wait().expect("Failed to wait for wget"); -// let sh_status = sh.wait().expect("Failed to wait for sh"); - -// if !wget_status.success() || !sh_status.success() { -// eprintln!("Installation failed."); -// } -// } else if cfg!(target_os = "windows") { -// // Download and run the install script via powershell if windows -// println!("Downloading and installing uv via PowerShell..."); -// Command::new("powershell") -// .args([ -// "-NoProfile", -// "-NonInteractive", -// "-ExecutionPolicy", "Bypass", -// "-Command", -// "irm https://astral.sh/uv/install.ps1 | iex" -// ]) -// .env("UV_UNMANAGED_INSTALL", install_path) -// .status() -// .expect("Failed to install uv"); - -// // let execute_status = ps_execute.wait().expect("Failed to wait for PowerShell execution"); - -// // if !download_status.success() || !execute_status.success() { -// // eprintln!("UV installation failed."); -// // } -// } else { -// eprintln!("Unsupported OS for uv installation."); -// }; -// } - -pub fn find_or_download_uv(cli_uv_path: Option) -> Option { - debug_println!("[uv_handler.find_or_download_uv] - Looking for uv"); - - let exe_dir = std::env::current_exe().expect("Could not find current working directory. Exiting ....").parent().unwrap().to_path_buf(); - debug_println!("[uv_handler.find_or_download_uv] - Current working directory: {:?}", exe_dir); - let local_uv = if cli_uv_path.is_some() { - debug_println!("CLI supplied uv path detected, using it"); - let lc_uv = Some(cli_uv_path.unwrap()); - if lc_uv.as_ref().unwrap().exists() { - debug_println!("CLI supplied uv path exists"); - lc_uv - } else { - debug_println!("CLI supplied uv path does not exist"); - None - } - } else if let Ok(path) = which::which("uv") { - debug_println!("`which` returned uv path, using it"); - Some(path) - } else { - let local_uv_path = exe_dir.join("uv"); - if local_uv_path.exists() { - debug_println!("Found uv next to binary, using it"); - Some(local_uv_path) - } else { - None - } - }; - let uv_path = if local_uv.is_some() { - debug_println!("[uv_handler.find_or_download_uv] - uv found locally [{:?}], using it", local_uv.as_ref().unwrap().canonicalize()); - local_uv - } else { - debug_println!("[uv_handler.find_or_download_uv] - uv not found locally, lets see if we have it cached ..."); - let home = dirs::home_dir().unwrap(); - - let uv_install_root = home.join(".pycrucible").join("cache").join("uv"); - - let uv_bin = uv_exists(&uv_install_root); - if uv_bin.is_some() { - debug_println!("[uv_handler.find_or_download_uv] - uv found cached at {:?}, using it", uv_bin.as_ref().unwrap()); - return uv_bin; - } - - debug_println!("[uv_handler.find_or_download_uv] - uv binary not found locally, proceeding to download."); - let sp = create_spinner_with_message("Downloading `uv` ..."); - download_and_install_uv_v2(&uv_install_root); - stop_and_persist_spinner_with_message(sp, "Downloaded `uv` successfully"); - - let uv_bin = uv_exists(&uv_install_root); if uv_bin.is_some() { - debug_println!("[uv_handler.find_or_download_uv] - uv downloaded and found at {:?}, using it", uv_bin.as_ref().unwrap()); - return uv_bin; - } - - return Some(uv_bin.expect("uv binary should exist after download")); - }; - - #[cfg(unix)] - { - use std::{fs, os::unix::fs::PermissionsExt}; - - if let Some(ref path) = uv_path { - if path.exists() { - let mut perms = fs::metadata(path) - .expect("Could not stat uv binary") - .permissions(); - let current_mode = perms.mode() & 0o777; - if current_mode == 0o755 { - debug_println!("[uv_handler.find_or_download_uv] - uv permissions already 0o755, skipping chmod for {:?}", path); - return uv_path.clone(); - } - - perms.set_mode(0o755); - fs::set_permissions(path, perms) - .expect("Could not chmod uv binary"); - debug_println!("[uv_handler.find_or_download_uv] - Set executable permissions for uv at {:?}", path); - } else { - eprintln!("uv binary not found at {:?}", path); - } - } - } - uv_path -} - \ No newline at end of file From e4499b97c50ba0c5ce26cf854ea53739066b674c Mon Sep 17 00:00:00 2001 From: razorblade23 Date: Tue, 13 Jan 2026 21:33:09 +0100 Subject: [PATCH 2/5] Better formatted code --- pycrucible/src/payload.rs | 27 +++++-------- runner/src/run.rs | 65 ++++++++++---------------------- shared/src/config.rs | 48 +++++++++++------------ shared/src/spinner.rs | 1 - shared/src/uv_handler/extract.rs | 6 +-- shared/src/uv_handler/install.rs | 59 ++++++++++++++++++++--------- shared/src/uv_handler/mod.rs | 6 +-- 7 files changed, 100 insertions(+), 112 deletions(-) diff --git a/pycrucible/src/payload.rs b/pycrucible/src/payload.rs index 59b4399..864e2ef 100644 --- a/pycrucible/src/payload.rs +++ b/pycrucible/src/payload.rs @@ -3,13 +3,13 @@ use crate::{config, runner}; use crate::{debug_println, project}; use shared::uv_handler::find_or_download_uv; +use std::fs::File; use std::fs::{self, OpenOptions}; +use std::io::Read; use std::io::{self, Cursor, Seek, SeekFrom, Write}; use std::path::{Path, PathBuf}; -use zip::{ZipWriter, write::FileOptions}; -use std::fs::File; -use std::io::Read; use zip::ZipArchive; +use zip::{ZipWriter, write::FileOptions}; pub fn find_manifest_file(source_dir: &Path) -> Option { if source_dir.join("pyproject.toml").exists() { @@ -73,20 +73,6 @@ fn write_to_zip( Ok(()) } -#[cfg(unix)] -fn set_permissions_on_unix(file: PathBuf) -> Result<(), io::Error> { - { - use std::os::unix::fs::PermissionsExt; - let mut perms = fs::metadata(&file)?.permissions(); - perms.set_mode(0o755); - fs::set_permissions(&file, perms)?; - debug_println!("[payload.embed_payload] - Set permissions for uv on linux"); - Ok(()) - } -} - - - fn read_wheel_name(path: &str) -> Result> { let file = File::open(path)?; let mut zip = ZipArchive::new(file)?; @@ -370,7 +356,12 @@ mod tests { debug: false, }; - let result = embed_payload(&source_files, &manifest_option, &mut project_config, cli_options); + let result = embed_payload( + &source_files, + &manifest_option, + &mut project_config, + cli_options, + ); assert!(result.is_ok(), "embed_payload should succeed"); assert!(output_path.exists()); diff --git a/runner/src/run.rs b/runner/src/run.rs index df3bc0e..9f1b444 100644 --- a/runner/src/run.rs +++ b/runner/src/run.rs @@ -1,42 +1,21 @@ // use shared::uv_handler::find_or_download_uv; +use shared::uv_handler::find_or_download_uv; +use shared::{debug_println, debuging}; use std::path::Path; use std::path::PathBuf; use std::process::Command; use std::{self, io}; -use shared::uv_handler::find_or_download_uv; -use shared::{debug_println, debuging}; use shared::config::{ProjectConfig, load_project_config}; -fn find_manifest_file(project_dir: &Path) -> io::Result { - let manifest_files = [ - "pyproject.toml", - "requirements.txt", - "pylock.toml", - "setup.py", - "setup.cfg", - ]; - - for file in &manifest_files { - let path = project_dir.join(file); - if path.exists() { - return Ok(path); - } - } - - Err(io::Error::new( - io::ErrorKind::NotFound, - "No manifest file found in the source directory. \nManifest files can be pyproject.toml, requirements.txt, pylock.toml, setup.py or setup.cfg", - )) -} - fn apply_env_from_config(config: &ProjectConfig) { if let Some(env_config) = &config.env - && let Some(vars) = &env_config.variables { - for (k, v) in vars { - unsafe { std::env::set_var(k, v) }; // Set env variables - not thread safe - } + && let Some(vars) = &env_config.variables + { + for (k, v) in vars { + unsafe { std::env::set_var(k, v) }; // Set env variables - not thread safe } + } } fn prepare_hooks(config: &ProjectConfig) -> (String, String) { @@ -56,12 +35,7 @@ fn prepare_hooks(config: &ProjectConfig) -> (String, String) { (pre_hook, post_hook) } -fn run_uv( - uv_path: &Path, - project_dir: &Path, - with: &[&str], - args: &[&str], -) -> io::Result<()> { +fn run_uv(uv_path: &Path, project_dir: &Path, with: &[&str], args: &[&str]) -> io::Result<()> { let mut cmd = Command::new(uv_path); cmd.arg("run").arg("-q"); @@ -69,8 +43,7 @@ fn run_uv( cmd.arg("--with").arg(w); } - cmd.args(args) - .current_dir(project_dir); + cmd.args(args).current_dir(project_dir); let status = cmd.status()?; @@ -104,7 +77,6 @@ fn find_single_wheel(project_dir: &Path) -> io::Result> { Ok(wheel) } - pub fn run_extracted_project(project_dir: &Path, runtime_args: Vec) -> io::Result<()> { // Load project configuration and determine entrypoint let config = load_project_config(&project_dir.to_path_buf()); @@ -178,19 +150,23 @@ pub fn run_extracted_project(project_dir: &Path, runtime_args: Vec) -> i let args_refs: Vec<&str> = args_vec.iter().map(|s| s.as_str()).collect(); run_uv(&uv_path, project_dir, &[], &args_refs)?; - }, + } "wheel" => { debug_println!("[main.run_extracted_project] - Running in wheel mode"); let wheel_file = wheel.ok_or(io::Error::new( io::ErrorKind::NotFound, "No .whl file found in the project directory", ))?; - run_uv(&uv_path, project_dir, &[wheel_file.to_str().unwrap()], &[config.package.entrypoint.as_str()])?; - }, + run_uv( + &uv_path, + project_dir, + &[wheel_file.to_str().unwrap()], + &[config.package.entrypoint.as_str()], + )?; + } _ => unreachable!(), } - // Run post-hook if specified debug_println!("[main.run_extracted_project] - Running post-hook if specified"); if !post_hook.is_empty() { @@ -201,10 +177,9 @@ pub fn run_extracted_project(project_dir: &Path, runtime_args: Vec) -> i debug_println!( "[main.run_extracted_project] - Cleaning up extracted project if configured to do so" ); - if (config.options.delete_after_run || config.options.extract_to_temp) - && project_dir.exists() { - std::fs::remove_dir_all(project_dir)?; - } + if (config.options.delete_after_run || config.options.extract_to_temp) && project_dir.exists() { + std::fs::remove_dir_all(project_dir)?; + } Ok(()) } diff --git a/shared/src/config.rs b/shared/src/config.rs index e4b0087..8325881 100644 --- a/shared/src/config.rs +++ b/shared/src/config.rs @@ -48,21 +48,18 @@ impl Default for PackageConfig { } } -#[derive(serde::Serialize, Debug, Deserialize)] -#[derive(Default)] +#[derive(serde::Serialize, Debug, Deserialize, Default)] pub struct UVConfig { pub args: Option>, } -#[derive(serde::Serialize, Debug, Deserialize)] -#[derive(Default)] +#[derive(serde::Serialize, Debug, Deserialize, Default)] pub struct EnvConfig { #[serde(flatten)] pub variables: Option>, } -#[derive(serde::Serialize, Debug, Deserialize)] -#[derive(Default)] +#[derive(serde::Serialize, Debug, Deserialize, Default)] pub struct Hooks { pub pre_run: Option, pub post_run: Option, @@ -88,8 +85,7 @@ impl Default for SourceConfig { } } -#[derive(serde::Serialize, Debug, Deserialize, Clone)] -#[derive(Default)] +#[derive(serde::Serialize, Debug, Deserialize, Clone, Default)] pub struct ToolOptions { #[serde(default)] pub debug: bool, @@ -135,7 +131,7 @@ impl ProjectConfig { // Re-serialize just that sub-table so we can leverage // the existing `ProjectConfig` derive. let slice = toml::to_string(tbl).map_err(|e| e.to_string())?; - + toml::from_str(&slice).map_err(|e| e.to_string()) } } @@ -182,27 +178,29 @@ fn load_config(config_path: &PathBuf) -> ProjectConfig { pub fn load_project_config(source_dir: &PathBuf) -> ProjectConfig { // Is there pycrucible.toml in the source directory? if let Ok(path) = source_dir.join("pycrucible.toml").canonicalize() - && path.exists() { - debug_println!("[config] using pycrucible.toml"); - return load_config(&path); - } + && path.exists() + { + debug_println!("[config] using pycrucible.toml"); + return load_config(&path); + } let pyproject = source_dir.join("pyproject.toml").canonicalize(); if let Ok(pyproject) = pyproject - && pyproject.exists() { - match ProjectConfig::from_pyproject(&pyproject) { - Ok(cfg) => { - debug_println!("[config] using [tool.pycrucible] in pyproject.toml"); - return cfg; - } - Err(e) => { - debug_println!( - "[config] pyproject.toml found but no usable [tool.pycrucible] - {}", - e - ); - } + && pyproject.exists() + { + match ProjectConfig::from_pyproject(&pyproject) { + Ok(cfg) => { + debug_println!("[config] using [tool.pycrucible] in pyproject.toml"); + return cfg; + } + Err(e) => { + debug_println!( + "[config] pyproject.toml found but no usable [tool.pycrucible] - {}", + e + ); } } + } // No config file found, use built-in defaults debug_println!("[config] using built-in defaults"); diff --git a/shared/src/spinner.rs b/shared/src/spinner.rs index fe9fd84..03dbbf2 100644 --- a/shared/src/spinner.rs +++ b/shared/src/spinner.rs @@ -1,7 +1,6 @@ use spinners::{Spinner, Spinners}; pub fn create_spinner_with_message(msg: &str) -> Spinner { - Spinner::new(Spinners::Dots9, msg.into()) } diff --git a/shared/src/uv_handler/extract.rs b/shared/src/uv_handler/extract.rs index 05f63da..eefe399 100644 --- a/shared/src/uv_handler/extract.rs +++ b/shared/src/uv_handler/extract.rs @@ -1,10 +1,10 @@ use crate::uv_handler::download::Archive; -use std::fs; -use std::path::Path; -use zip::ZipArchive; use flate2::read::GzDecoder; +use std::fs; #[cfg(unix)] use std::os::unix::fs::PermissionsExt; +use std::path::Path; +use zip::ZipArchive; pub fn extract_uv<'a>( archive: &mut Archive<'a>, diff --git a/shared/src/uv_handler/install.rs b/shared/src/uv_handler/install.rs index 95d1b05..8b49ced 100644 --- a/shared/src/uv_handler/install.rs +++ b/shared/src/uv_handler/install.rs @@ -1,10 +1,10 @@ -use std::path::Path; -use crate::uv_handler::platform; +use crate::debug_println; use crate::uv_handler::download; use crate::uv_handler::extract; -use crate::debug_println; -use std::path::PathBuf; +use crate::uv_handler::platform; use crate::{create_spinner_with_message, stop_and_persist_spinner_with_message}; +use std::path::Path; +use std::path::PathBuf; pub fn install_uv(version: &str, install_dir: &Path) -> Result<(), Box> { let target = platform::target_triple(); @@ -45,8 +45,15 @@ pub fn uv_exists(path: &PathBuf) -> Option { pub fn find_or_download_uv(cli_uv_path: Option) -> Option { debug_println!("[uv_handler.find_or_download_uv] - Looking for uv"); - let exe_dir = std::env::current_exe().expect("Could not find current working directory. Exiting ....").parent().unwrap().to_path_buf(); - debug_println!("[uv_handler.find_or_download_uv] - Current working directory: {:?}", exe_dir); + let exe_dir = std::env::current_exe() + .expect("Could not find current working directory. Exiting ....") + .parent() + .unwrap() + .to_path_buf(); + debug_println!( + "[uv_handler.find_or_download_uv] - Current working directory: {:?}", + exe_dir + ); let local_uv = if cli_uv_path.is_some() { debug_println!("CLI supplied uv path detected, using it"); let lc_uv = Some(cli_uv_path.unwrap()); @@ -70,28 +77,41 @@ pub fn find_or_download_uv(cli_uv_path: Option) -> Option { } }; let uv_path = if local_uv.is_some() { - debug_println!("[uv_handler.find_or_download_uv] - uv found locally [{:?}], using it", local_uv.as_ref().unwrap().canonicalize()); + debug_println!( + "[uv_handler.find_or_download_uv] - uv found locally [{:?}], using it", + local_uv.as_ref().unwrap().canonicalize() + ); local_uv } else { - debug_println!("[uv_handler.find_or_download_uv] - uv not found locally, lets see if we have it cached ..."); + debug_println!( + "[uv_handler.find_or_download_uv] - uv not found locally, lets see if we have it cached ..." + ); let home = dirs::home_dir().unwrap(); let uv_install_root = home.join(".pycrucible").join("cache").join("uv"); - + let uv_bin = uv_exists(&uv_install_root); if uv_bin.is_some() { - debug_println!("[uv_handler.find_or_download_uv] - uv found cached at {:?}, using it", uv_bin.as_ref().unwrap()); + debug_println!( + "[uv_handler.find_or_download_uv] - uv found cached at {:?}, using it", + uv_bin.as_ref().unwrap() + ); return uv_bin; } - debug_println!("[uv_handler.find_or_download_uv] - uv binary not found locally, proceeding to download."); + debug_println!( + "[uv_handler.find_or_download_uv] - uv binary not found locally, proceeding to download." + ); let sp = create_spinner_with_message("Downloading `uv` ..."); install_uv("0.9.21", &uv_install_root).expect("uv installation failed"); stop_and_persist_spinner_with_message(sp, "Downloaded `uv` successfully"); let uv_bin = uv_exists(&uv_install_root); if uv_bin.is_some() { - debug_println!("[uv_handler.find_or_download_uv] - uv downloaded and found at {:?}, using it", uv_bin.as_ref().unwrap()); + debug_println!( + "[uv_handler.find_or_download_uv] - uv downloaded and found at {:?}, using it", + uv_bin.as_ref().unwrap() + ); return uv_bin; } @@ -109,18 +129,23 @@ pub fn find_or_download_uv(cli_uv_path: Option) -> Option { .permissions(); let current_mode = perms.mode() & 0o777; if current_mode == 0o755 { - debug_println!("[uv_handler.find_or_download_uv] - uv permissions already 0o755, skipping chmod for {:?}", path); + debug_println!( + "[uv_handler.find_or_download_uv] - uv permissions already 0o755, skipping chmod for {:?}", + path + ); return uv_path.clone(); } perms.set_mode(0o755); - fs::set_permissions(path, perms) - .expect("Could not chmod uv binary"); - debug_println!("[uv_handler.find_or_download_uv] - Set executable permissions for uv at {:?}", path); + fs::set_permissions(path, perms).expect("Could not chmod uv binary"); + debug_println!( + "[uv_handler.find_or_download_uv] - Set executable permissions for uv at {:?}", + path + ); } else { eprintln!("uv binary not found at {:?}", path); } } } uv_path -} \ No newline at end of file +} diff --git a/shared/src/uv_handler/mod.rs b/shared/src/uv_handler/mod.rs index 1cfa252..e0a0c57 100644 --- a/shared/src/uv_handler/mod.rs +++ b/shared/src/uv_handler/mod.rs @@ -1,6 +1,6 @@ -mod install; -mod platform; mod download; mod extract; +mod install; +mod platform; -pub use install::{install_uv, find_or_download_uv}; \ No newline at end of file +pub use install::{find_or_download_uv, install_uv}; From afe82cd240bde9341b5ccfef3bc4f8612c63a9b1 Mon Sep 17 00:00:00 2001 From: razorblade23 Date: Tue, 13 Jan 2026 22:04:31 +0100 Subject: [PATCH 3/5] Added CLI and configuration option to change uv version --- CHANGELOG.md | 2 ++ pycrucible.example.toml | 1 + pycrucible/src/cli.rs | 7 +++++++ pycrucible/src/main.rs | 2 ++ pycrucible/src/payload.rs | 14 +++++++++----- runner/src/run.rs | 2 +- shared/src/config.rs | 2 ++ shared/src/uv_handler/install.rs | 8 ++++++-- 8 files changed, 30 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a2b9fe..bf7079e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # v0.4.x ## v0.4.0 +- Downloading only `uv` binary, disregarding the rest of the archive. + - `--uv-version` - Select the version of uv to download. [default: `0.9.21`] - Added support for .whl embedding - new CLI options are provided for this mode only - `--extract-to-temp` - ["wheel" mode only] - sets configuration option with the same name diff --git a/pycrucible.example.toml b/pycrucible.example.toml index 5646fe2..481770b 100644 --- a/pycrucible.example.toml +++ b/pycrucible.example.toml @@ -6,6 +6,7 @@ entrypoint = "src/main.py" # debug = false # extract_to_temp = false # delete_after_run = false +# uv_version = "0.9.21" # # Optional - uncomment if you need it # [source] diff --git a/pycrucible/src/cli.rs b/pycrucible/src/cli.rs index 196bea1..7bad1a8 100644 --- a/pycrucible/src/cli.rs +++ b/pycrucible/src/cli.rs @@ -40,6 +40,13 @@ pub struct Cli { #[arg(default_value_os_t = get_output_dir().join(UV_BINARY))] pub uv_path: PathBuf, + #[arg( + long, + help = "Path to `uv` executable. If not found, it will be downloaded automatically" + )] + #[arg(default_value_os_t = "0.9.21")] + pub uv_version: &str, + #[arg( long, help = "Disable embedding `uv` binary into the output executable. This will require `uv` to be present alongside (or downloaded) the output binary at runtime." diff --git a/pycrucible/src/main.rs b/pycrucible/src/main.rs index c3c268c..2129282 100644 --- a/pycrucible/src/main.rs +++ b/pycrucible/src/main.rs @@ -16,6 +16,7 @@ pub struct CLIOptions { source_dir: PathBuf, output_path: PathBuf, uv_path: PathBuf, + uv_version: &'static str, no_uv_embed: bool, extract_to_temp: bool, delete_after_run: bool, @@ -88,6 +89,7 @@ fn main() -> io::Result<()> { source_dir: payload_path.clone(), output_path: output_path.clone(), uv_path: cli.uv_path, + uv_version: cli.uv_version, no_uv_embed: cli.no_uv_embed, extract_to_temp: cli.extract_to_temp, delete_after_run: cli.delete_after_run, diff --git a/pycrucible/src/payload.rs b/pycrucible/src/payload.rs index 864e2ef..dc15861 100644 --- a/pycrucible/src/payload.rs +++ b/pycrucible/src/payload.rs @@ -31,12 +31,13 @@ pub fn find_manifest_file(source_dir: &Path) -> Option { } fn embed_uv( + cli_options: &crate::CLIOptions, cli_uv_path: PathBuf, zip: &mut ZipWriter<&mut Cursor>>, options: FileOptions<'_, ()>, ) -> io::Result> { debug_println!("[payload.embed_uv] - Embedding uv binary into payload"); - let uv_path = find_or_download_uv(Some(cli_uv_path)); + let uv_path = find_or_download_uv(Some(cli_uv_path), cli_options.uv_version); match uv_path { None => { eprintln!("Could not find or download uv binary. uv will be required at runtime."); @@ -112,12 +113,14 @@ pub fn embed_payload( // Update project config with CLI options as we do not use any other file to store these in wheel mode project_config.options.debug = cli_options.debug; - project_config.options.extract_to_temp = cli_options.extract_to_temp; - project_config.options.delete_after_run = cli_options.delete_after_run; + project_config.options.uv_version = cli_options.uv_version.to_string(); // Check to see if we have a wheel or source files and handle accordingly match source_files { project::CollectedSources::Wheel(wheel) => { + project_config.options.extract_to_temp = cli_options.extract_to_temp; + project_config.options.delete_after_run = cli_options.delete_after_run; + let wheel_path = &wheel.absolute_path; let wheel_file_name = wheel_path @@ -169,10 +172,10 @@ pub fn embed_payload( } else { None }; - find_or_download_uv(uv_path); + find_or_download_uv(uv_path, cli_options.uv_version); } debug_println!("[payload.embed_payload] - Looking for uv binary to embed"); - if let Some(_path) = embed_uv(cli_options.uv_path, &mut zip, options)? { + if let Some(_path) = embed_uv(&cli_options, cli_options.uv_path, &mut zip, options)? { debug_println!("[payload.embed_payload] - uv binary embedded successfully"); } else { eprintln!("Could not find or download uv binary. uv will be required at runtime."); @@ -349,6 +352,7 @@ mod tests { source_dir: src_dir.clone(), output_path: output_path.clone(), uv_path: uv_path.clone(), + uv_version: "0.9.21", no_uv_embed: false, extract_to_temp: true, delete_after_run: false, diff --git a/runner/src/run.rs b/runner/src/run.rs index 9f1b444..e81a285 100644 --- a/runner/src/run.rs +++ b/runner/src/run.rs @@ -89,7 +89,7 @@ pub fn run_extracted_project(project_dir: &Path, runtime_args: Vec) -> i // Ensure UV is available debug_println!("[main.run_extracted_project] - Ensuring UV is available"); - let uv_path = find_or_download_uv(None).ok_or(io::Error::new( + let uv_path = find_or_download_uv(None, "0.9.21").ok_or(io::Error::new( io::ErrorKind::NotFound, "Could not find or download uv binary", ))?; diff --git a/shared/src/config.rs b/shared/src/config.rs index 8325881..63f5b4f 100644 --- a/shared/src/config.rs +++ b/shared/src/config.rs @@ -95,6 +95,8 @@ pub struct ToolOptions { pub delete_after_run: bool, #[serde(default)] pub offline_mode: bool, + #[serde(default)] + pub uv_version: String, } #[derive(serde::Serialize, Debug, Deserialize)] diff --git a/shared/src/uv_handler/install.rs b/shared/src/uv_handler/install.rs index 8b49ced..7de82de 100644 --- a/shared/src/uv_handler/install.rs +++ b/shared/src/uv_handler/install.rs @@ -42,7 +42,7 @@ pub fn uv_exists(path: &PathBuf) -> Option { Some(uv_bin) } -pub fn find_or_download_uv(cli_uv_path: Option) -> Option { +pub fn find_or_download_uv(cli_uv_path: Option, uv_version: &str) -> Option { debug_println!("[uv_handler.find_or_download_uv] - Looking for uv"); let exe_dir = std::env::current_exe() @@ -54,6 +54,7 @@ pub fn find_or_download_uv(cli_uv_path: Option) -> Option { "[uv_handler.find_or_download_uv] - Current working directory: {:?}", exe_dir ); + // Check CLI supplied path first let local_uv = if cli_uv_path.is_some() { debug_println!("CLI supplied uv path detected, using it"); let lc_uv = Some(cli_uv_path.unwrap()); @@ -64,9 +65,11 @@ pub fn find_or_download_uv(cli_uv_path: Option) -> Option { debug_println!("CLI supplied uv path does not exist"); None } + // Check system path next } else if let Ok(path) = which::which("uv") { debug_println!("`which` returned uv path, using it"); Some(path) + // Check local uv next to binary } else { let local_uv_path = exe_dir.join("uv"); if local_uv_path.exists() { @@ -76,6 +79,7 @@ pub fn find_or_download_uv(cli_uv_path: Option) -> Option { None } }; + // If not found locally, check cache or download let uv_path = if local_uv.is_some() { debug_println!( "[uv_handler.find_or_download_uv] - uv found locally [{:?}], using it", @@ -103,7 +107,7 @@ pub fn find_or_download_uv(cli_uv_path: Option) -> Option { "[uv_handler.find_or_download_uv] - uv binary not found locally, proceeding to download." ); let sp = create_spinner_with_message("Downloading `uv` ..."); - install_uv("0.9.21", &uv_install_root).expect("uv installation failed"); + install_uv(uv_version, &uv_install_root).expect("uv installation failed"); stop_and_persist_spinner_with_message(sp, "Downloaded `uv` successfully"); let uv_bin = uv_exists(&uv_install_root); From 046ed4f5215e81e5a5687841ee365e8b55df9b6e Mon Sep 17 00:00:00 2001 From: razorblade23 Date: Tue, 13 Jan 2026 22:18:08 +0100 Subject: [PATCH 4/5] Fixed a few errors --- pycrucible/src/cli.rs | 4 ++-- pycrucible/src/main.rs | 2 +- pycrucible/src/payload.rs | 9 ++++----- runner/src/run.rs | 2 +- shared/src/uv_handler/install.rs | 2 +- 5 files changed, 9 insertions(+), 10 deletions(-) diff --git a/pycrucible/src/cli.rs b/pycrucible/src/cli.rs index 7bad1a8..c773721 100644 --- a/pycrucible/src/cli.rs +++ b/pycrucible/src/cli.rs @@ -44,8 +44,8 @@ pub struct Cli { long, help = "Path to `uv` executable. If not found, it will be downloaded automatically" )] - #[arg(default_value_os_t = "0.9.21")] - pub uv_version: &str, + #[arg(default_value_t = String::from("0.9.21"))] + pub uv_version: String, #[arg( long, diff --git a/pycrucible/src/main.rs b/pycrucible/src/main.rs index 2129282..bdecddf 100644 --- a/pycrucible/src/main.rs +++ b/pycrucible/src/main.rs @@ -16,7 +16,7 @@ pub struct CLIOptions { source_dir: PathBuf, output_path: PathBuf, uv_path: PathBuf, - uv_version: &'static str, + uv_version: String, no_uv_embed: bool, extract_to_temp: bool, delete_after_run: bool, diff --git a/pycrucible/src/payload.rs b/pycrucible/src/payload.rs index dc15861..dec4eed 100644 --- a/pycrucible/src/payload.rs +++ b/pycrucible/src/payload.rs @@ -32,12 +32,11 @@ pub fn find_manifest_file(source_dir: &Path) -> Option { fn embed_uv( cli_options: &crate::CLIOptions, - cli_uv_path: PathBuf, zip: &mut ZipWriter<&mut Cursor>>, options: FileOptions<'_, ()>, ) -> io::Result> { debug_println!("[payload.embed_uv] - Embedding uv binary into payload"); - let uv_path = find_or_download_uv(Some(cli_uv_path), cli_options.uv_version); + let uv_path = find_or_download_uv(Some(cli_options.uv_path.clone()), &cli_options.uv_version); match uv_path { None => { eprintln!("Could not find or download uv binary. uv will be required at runtime."); @@ -172,10 +171,10 @@ pub fn embed_payload( } else { None }; - find_or_download_uv(uv_path, cli_options.uv_version); + find_or_download_uv(uv_path, cli_options.uv_version.as_str()); } debug_println!("[payload.embed_payload] - Looking for uv binary to embed"); - if let Some(_path) = embed_uv(&cli_options, cli_options.uv_path, &mut zip, options)? { + if let Some(_path) = embed_uv(&cli_options, &mut zip, options)? { debug_println!("[payload.embed_payload] - uv binary embedded successfully"); } else { eprintln!("Could not find or download uv binary. uv will be required at runtime."); @@ -352,7 +351,7 @@ mod tests { source_dir: src_dir.clone(), output_path: output_path.clone(), uv_path: uv_path.clone(), - uv_version: "0.9.21", + uv_version: "0.9.21".to_string(), no_uv_embed: false, extract_to_temp: true, delete_after_run: false, diff --git a/runner/src/run.rs b/runner/src/run.rs index e81a285..b67b8e6 100644 --- a/runner/src/run.rs +++ b/runner/src/run.rs @@ -89,7 +89,7 @@ pub fn run_extracted_project(project_dir: &Path, runtime_args: Vec) -> i // Ensure UV is available debug_println!("[main.run_extracted_project] - Ensuring UV is available"); - let uv_path = find_or_download_uv(None, "0.9.21").ok_or(io::Error::new( + let uv_path = find_or_download_uv(None, config.options.uv_version.as_str()).ok_or(io::Error::new( io::ErrorKind::NotFound, "Could not find or download uv binary", ))?; diff --git a/shared/src/uv_handler/install.rs b/shared/src/uv_handler/install.rs index 7de82de..2446e6e 100644 --- a/shared/src/uv_handler/install.rs +++ b/shared/src/uv_handler/install.rs @@ -57,7 +57,7 @@ pub fn find_or_download_uv(cli_uv_path: Option, uv_version: &str) -> Op // Check CLI supplied path first let local_uv = if cli_uv_path.is_some() { debug_println!("CLI supplied uv path detected, using it"); - let lc_uv = Some(cli_uv_path.unwrap()); + let lc_uv = Some(cli_uv_path.as_ref().unwrap().clone()); if lc_uv.as_ref().unwrap().exists() { debug_println!("CLI supplied uv path exists"); lc_uv From 76c96c587fa333aa78ba0fe96fac1a9dbaf4958e Mon Sep 17 00:00:00 2001 From: razorblade23 Date: Tue, 13 Jan 2026 22:32:03 +0100 Subject: [PATCH 5/5] Better debug messages --- runner/src/run.rs | 20 ++++++++++---------- shared/src/uv_handler/install.rs | 3 ++- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/runner/src/run.rs b/runner/src/run.rs index b67b8e6..b2e7c83 100644 --- a/runner/src/run.rs +++ b/runner/src/run.rs @@ -80,6 +80,7 @@ fn find_single_wheel(project_dir: &Path) -> io::Result> { pub fn run_extracted_project(project_dir: &Path, runtime_args: Vec) -> io::Result<()> { // Load project configuration and determine entrypoint let config = load_project_config(&project_dir.to_path_buf()); + debug_println!("[main.run_extracted_project] - Loaded project configuration"); // Enable debug mode if specified in config if config.options.debug { @@ -89,10 +90,11 @@ pub fn run_extracted_project(project_dir: &Path, runtime_args: Vec) -> i // Ensure UV is available debug_println!("[main.run_extracted_project] - Ensuring UV is available"); - let uv_path = find_or_download_uv(None, config.options.uv_version.as_str()).ok_or(io::Error::new( - io::ErrorKind::NotFound, - "Could not find or download uv binary", - ))?; + let uv_path = + find_or_download_uv(None, config.options.uv_version.as_str()).ok_or(io::Error::new( + io::ErrorKind::NotFound, + "Could not find or download uv binary", + ))?; // Apply environment variables from config (unsafe but we are single-threaded so it should be fine) apply_env_from_config(&config); @@ -101,7 +103,6 @@ pub fn run_extracted_project(project_dir: &Path, runtime_args: Vec) -> i ); // Determine entrypoint - debug_println!("[main.run_extracted_project] - Loaded project configuration"); let entrypoint = &config.package.entrypoint; let entry_point_path = project_dir.join(entrypoint); if entrypoint.ends_with(".py") { @@ -136,11 +137,12 @@ pub fn run_extracted_project(project_dir: &Path, runtime_args: Vec) -> i ); // Run pre-hook if specified - debug_println!("[main.run_extracted_project] - Running pre-hook if specified"); if !pre_hook.is_empty() { + debug_println!("[main.run_extracted_project] - Running pre-hook"); run_uv(&uv_path, project_dir, &[], &[pre_hook.as_str()])?; } + debug_println!("[main.run_extracted_project] - Running main project"); match run_mode { "source" => { debug_println!("[main.run_extracted_project] - Running in source mode"); @@ -168,16 +170,14 @@ pub fn run_extracted_project(project_dir: &Path, runtime_args: Vec) -> i } // Run post-hook if specified - debug_println!("[main.run_extracted_project] - Running post-hook if specified"); if !post_hook.is_empty() { + debug_println!("[main.run_extracted_project] - Running post-hook"); run_uv(&uv_path, project_dir, &[], &[post_hook.as_str()])?; } // Clean up if delete_after_run is set or extract_to_temp is set - debug_println!( - "[main.run_extracted_project] - Cleaning up extracted project if configured to do so" - ); if (config.options.delete_after_run || config.options.extract_to_temp) && project_dir.exists() { + debug_println!("[main.run_extracted_project] - Cleaning up extracted project"); std::fs::remove_dir_all(project_dir)?; } diff --git a/shared/src/uv_handler/install.rs b/shared/src/uv_handler/install.rs index 2446e6e..d56a5ce 100644 --- a/shared/src/uv_handler/install.rs +++ b/shared/src/uv_handler/install.rs @@ -104,7 +104,8 @@ pub fn find_or_download_uv(cli_uv_path: Option, uv_version: &str) -> Op } debug_println!( - "[uv_handler.find_or_download_uv] - uv binary not found locally, proceeding to download." + "[uv_handler.find_or_download_uv] - uv binary not found locally, proceeding to download. uv version: `{}`", + uv_version ); let sp = create_spinner_with_message("Downloading `uv` ..."); install_uv(uv_version, &uv_install_root).expect("uv installation failed");