fix(downloads): invoke system curl portably instead of curl.exe

Spawn bare `curl` (which Windows still resolves to curl.exe via
CreateProcess, and macOS/Linux ship natively) and write the size-probe
output to the platform null device instead of the Windows-only NUL,
removing the hard Windows dependency from the model download path.
Spawn quirks (console-window suppression) are centralized in a new
curl_command() helper.
This commit is contained in:
2026-07-05 20:21:23 +01:00
parent 7020a6b6cf
commit a791f112f5
+24 -11
View File
@@ -16,12 +16,29 @@ use std::sync::Mutex;
use std::time::Instant; use std::time::Instant;
use tokenizers::Tokenizer; use tokenizers::Tokenizer;
// Suppress the console window when spawning curl.exe from the GUI app. // Suppress the console window when spawning curl from the GUI app.
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt; use std::os::windows::process::CommandExt;
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
const CREATE_NO_WINDOW: u32 = 0x08000000; const CREATE_NO_WINDOW: u32 = 0x08000000;
/// Discard target for curl output we only need the headers of.
#[cfg(target_os = "windows")]
const NULL_DEVICE: &str = "NUL";
#[cfg(not(target_os = "windows"))]
const NULL_DEVICE: &str = "/dev/null";
/// Build a `curl` command with platform quirks applied. Windows resolves the
/// bare name to `curl.exe` (bundled since Windows 10 1803) and needs the
/// no-window flag; macOS ships curl; Linux is expected to have it installed.
fn curl_command() -> Command {
#[allow(unused_mut)]
let mut command = Command::new("curl");
#[cfg(target_os = "windows")]
command.creation_flags(CREATE_NO_WINDOW);
command
}
pub const FLORENCE_MODEL_ID: &str = "onnx-community/Florence-2-base-ft"; pub const FLORENCE_MODEL_ID: &str = "onnx-community/Florence-2-base-ft";
pub const FLORENCE_CAPTION_MODEL_NAME: &str = "florence-2-base-ft-onnx-q4"; pub const FLORENCE_CAPTION_MODEL_NAME: &str = "florence-2-base-ft-onnx-q4";
const ONNX_RUNTIME_NUGET_URL: &str = const ONNX_RUNTIME_NUGET_URL: &str =
@@ -727,7 +744,7 @@ fn download_onnx_runtime_files(local_dir: &Path) -> Result<()> {
// rather than locking the UI on "preparing" for many minutes. // rather than locking the UI on "preparing" for many minutes.
const MAX_STALL_RETRIES: usize = 3; const MAX_STALL_RETRIES: usize = 3;
/// Resiliently download `url` to `destination` using the system `curl.exe`. /// Resiliently download `url` to `destination` using the system `curl`.
/// ///
/// ureq's read timeout does not fire on a stalled large transfer on Windows /// ureq's read timeout does not fire on a stalled large transfer on Windows
/// (schannel doesn't honor the socket read timeout), so a stall there hangs /// (schannel doesn't honor the socket read timeout), so a stall there hangs
@@ -818,7 +835,7 @@ pub fn download_file_resilient(
/// ureq so no part of the download path depends on ureq (which hangs on this /// ureq so no part of the download path depends on ureq (which hangs on this
/// VM's TLS stack). Returns None if the server doesn't report a size. /// VM's TLS stack). Returns None if the server doesn't report a size.
fn remote_content_length(url: &str) -> Option<u64> { fn remote_content_length(url: &str) -> Option<u64> {
let mut command = Command::new("curl.exe"); let mut command = curl_command();
command.args([ command.args([
"-sL", "-sL",
"-r", "-r",
@@ -826,15 +843,13 @@ fn remote_content_length(url: &str) -> Option<u64> {
"-D", "-D",
"-", "-",
"-o", "-o",
"NUL", NULL_DEVICE,
"--connect-timeout", "--connect-timeout",
"30", "30",
"--max-time", "--max-time",
"30", "30",
url, url,
]); ]);
#[cfg(target_os = "windows")]
command.creation_flags(CREATE_NO_WINDOW);
let output = command.output().ok()?; let output = command.output().ok()?;
let headers = String::from_utf8_lossy(&output.stdout); let headers = String::from_utf8_lossy(&output.stdout);
for line in headers.lines() { for line in headers.lines() {
@@ -849,7 +864,7 @@ fn remote_content_length(url: &str) -> Option<u64> {
None None
} }
/// Run one `curl.exe` download to `dest`, resuming from any partial file, while /// Run one `curl` download to `dest`, resuming from any partial file, while
/// reporting progress from the growing file size. Returns an error (leaving the /// reporting progress from the growing file size. Returns an error (leaving the
/// partial in place) if curl exits non-zero. /// partial in place) if curl exits non-zero.
fn run_curl_download( fn run_curl_download(
@@ -858,7 +873,7 @@ fn run_curl_download(
total: Option<u64>, total: Option<u64>,
on_progress: &mut impl FnMut(u64, Option<u64>), on_progress: &mut impl FnMut(u64, Option<u64>),
) -> Result<()> { ) -> Result<()> {
let mut command = Command::new("curl.exe"); let mut command = curl_command();
command command
.arg("-fSL") // fail on HTTP errors, follow redirects, show errors .arg("-fSL") // fail on HTTP errors, follow redirects, show errors
.args(["-C", "-"]) // resume from the existing output file .args(["-C", "-"]) // resume from the existing output file
@@ -873,12 +888,10 @@ fn run_curl_download(
.arg(url) .arg(url)
.stdout(std::process::Stdio::null()) .stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::piped()); .stderr(std::process::Stdio::piped());
#[cfg(target_os = "windows")]
command.creation_flags(CREATE_NO_WINDOW);
let mut child = command let mut child = command
.spawn() .spawn()
.map_err(|e| anyhow::anyhow!("failed to launch curl.exe (required for downloads): {e}"))?; .map_err(|e| anyhow::anyhow!("failed to launch curl (required for downloads): {e}"))?;
loop { loop {
if let Some(status) = child.try_wait()? { if let Some(status) = child.try_wait()? {