use std::path::Path; use std::process::Command; use std::fs::File; use std::io::Read; use std::io::Write; use std::time::Duration; // Constantes para download e validação do OllamaSetup.exe const OLLAMA_SETUP_URL: &str = "https://ollama.com/download/OllamaSetup.exe"; const MAX_RETRIES: u32 = 3; const DOWNLOAD_TIMEOUT_SECS: u64 = 300; // Hash SHA256 esperado do OllamaSetup.exe // Pode ser sobrescrito via variável de ambiente OLLAMA_SETUP_EXE_SHA256 // IMPORTANTE: Atualizar este valor com o hash SHA256 oficial do instalador const DEFAULT_EXPECTED_OLLAMA_SETUP_SHA256: &str = "AC9C74B41A8303E5566C5AD3ED6EF0E0123D58AEBC87D3109973F02A38D3FF61"; /// Calcula o hash SHA256 de um arquivo /// Usa chunks de 8KB para leitura eficiente (padrão do projeto) fn calculate_sha256(file_path: &Path) -> Result { use sha2::{Sha256, Digest}; let mut file = File::open(file_path) .map_err(|e| format!("Failed to open file for hash calculation: {}", e))?; let mut hasher = Sha256::new(); let mut buffer = vec![0u8; 8192]; // Buffer de 8KB para leitura em chunks loop { let bytes_read = file.read(&mut buffer) .map_err(|e| format!("Failed to read file for hash: {}", e))?; if bytes_read == 0 { break; } hasher.update(&buffer[..bytes_read]); } let hash = hasher.finalize(); Ok(format!("{:x}", hash)) } /// Faz download de um arquivo com retry logic e backoff exponencial fn download_with_retry(url: &str, dest_path: &Path) -> Result<(), String> { use reqwest::blocking::Client; // Limpar arquivo parcial se existir antes de iniciar if dest_path.exists() { std::fs::remove_file(dest_path) .map_err(|e| format!("Failed to remove existing file before download: {}", e))?; } let client = Client::builder() .timeout(Duration::from_secs(DOWNLOAD_TIMEOUT_SECS)) .build() .map_err(|e| format!("Failed to create HTTP client: {}", e))?; let mut last_error = None; for attempt in 1..=MAX_RETRIES { println!("cargo:warning=Download attempt {}/{}: {}", attempt, MAX_RETRIES, url); // Limpar arquivo parcial antes de cada tentativa (exceto na primeira, já limpo acima) if attempt > 1 && dest_path.exists() { std::fs::remove_file(dest_path) .map_err(|e| format!("Failed to remove partial file before retry: {}", e))?; } // Garantir que o diretório pai existe if let Some(parent) = dest_path.parent() { std::fs::create_dir_all(parent) .map_err(|e| format!("Failed to create parent directory: {}", e))?; } match client.get(url).send() { Ok(response) => { if !response.status().is_success() { let err_msg = format!("HTTP error: {}", response.status()); println!("cargo:warning={}", err_msg); last_error = Some(err_msg); if attempt < MAX_RETRIES { let backoff_secs = 2_u64.pow(attempt - 1); // 1s, 2s, 4s println!("cargo:warning=Waiting {} seconds before retry...", backoff_secs); std::thread::sleep(Duration::from_secs(backoff_secs)); } continue; } // Obter bytes do response match response.bytes() { Ok(bytes) => { // Criar arquivo e escrever conteúdo match File::create(dest_path) { Ok(mut dest_file) => { match dest_file.write_all(&bytes) { Ok(_) => { drop(dest_file); // Fechar arquivo antes de validar println!("cargo:warning=Download completed successfully"); return Ok(()); } Err(e) => { let err_msg = format!("Failed to write file: {}", e); println!("cargo:warning={}", err_msg); last_error = Some(err_msg); // Limpar arquivo parcial let _ = std::fs::remove_file(dest_path); if attempt < MAX_RETRIES { let backoff_secs = 2_u64.pow(attempt - 1); println!("cargo:warning=Waiting {} seconds before retry...", backoff_secs); std::thread::sleep(Duration::from_secs(backoff_secs)); } continue; } } } Err(e) => { let err_msg = format!("Failed to create destination file: {}", e); println!("cargo:warning={}", err_msg); last_error = Some(err_msg); if attempt < MAX_RETRIES { let backoff_secs = 2_u64.pow(attempt - 1); println!("cargo:warning=Waiting {} seconds before retry...", backoff_secs); std::thread::sleep(Duration::from_secs(backoff_secs)); } continue; } } } Err(e) => { let err_msg = format!("Failed to read response bytes: {}", e); println!("cargo:warning={}", err_msg); last_error = Some(err_msg); // Limpar arquivo parcial let _ = std::fs::remove_file(dest_path); if attempt < MAX_RETRIES { let backoff_secs = 2_u64.pow(attempt - 1); println!("cargo:warning=Waiting {} seconds before retry...", backoff_secs); std::thread::sleep(Duration::from_secs(backoff_secs)); } continue; } } } Err(e) => { let err_msg = format!("Download failed: {}", e); println!("cargo:warning={}", err_msg); last_error = Some(err_msg); if attempt < MAX_RETRIES { let backoff_secs = 2_u64.pow(attempt - 1); println!("cargo:warning=Waiting {} seconds before retry...", backoff_secs); std::thread::sleep(Duration::from_secs(backoff_secs)); } } } } Err(format!( "Failed to download {} after {} attempts. Last error: {}", url, MAX_RETRIES, last_error.unwrap_or_else(|| "Unknown error".to_string()) )) } /// Valida o hash SHA256 de um arquivo contra o hash esperado fn validate_sha256(file_path: &Path, expected_hash: &str) -> Result<(), String> { println!("cargo:warning=Validating SHA256 hash..."); let calculated_hash = calculate_sha256(file_path)?; let calculated_lower = calculated_hash.to_lowercase(); let expected_lower = expected_hash.to_lowercase(); if calculated_lower != expected_lower { // Remover arquivo corrompido let _ = std::fs::remove_file(file_path); return Err(format!( "SHA256 hash mismatch! Expected: {}, Got: {}. Corrupted file removed.", expected_hash, calculated_hash )); } println!("cargo:warning=SHA256 hash validated successfully"); Ok(()) } /// Obtém o hash SHA256 esperado (variável de ambiente ou constante padrão) fn get_expected_sha256() -> String { std::env::var("OLLAMA_SETUP_EXE_SHA256") .unwrap_or_else(|_| DEFAULT_EXPECTED_OLLAMA_SETUP_SHA256.to_string()) } /// Baixa e valida o instalador do Ollama fn download_and_validate_ollama_setup(ollama_path: &Path) -> Result<(), String> { let expected_hash = get_expected_sha256(); // Se o hash esperado está vazio, falhar imediatamente if expected_hash.is_empty() { panic!( "OLLAMA_SETUP_EXE_SHA256 não configurado! \ Configure a variável de ambiente OLLAMA_SETUP_EXE_SHA256 ou \ atualize DEFAULT_EXPECTED_OLLAMA_SETUP_SHA256 no build.rs" ); } // Verificar se arquivo já existe if ollama_path.exists() { println!("cargo:warning=OllamaSetup.exe already exists, validating hash..."); // Validar hash do arquivo existente match validate_sha256(ollama_path, &expected_hash) { Ok(_) => { println!("cargo:warning=Existing OllamaSetup.exe is valid, skipping download"); return Ok(()); } Err(e) => { println!("cargo:warning=Existing file failed validation: {}", e); println!("cargo:warning=Re-downloading..."); // Continuar para baixar novamente } } } // Fazer download com retry println!("cargo:warning=Downloading Ollama installer..."); download_with_retry(OLLAMA_SETUP_URL, ollama_path)?; // Validar hash após download validate_sha256(ollama_path, &expected_hash)?; println!("cargo:warning=Ollama installer downloaded and validated successfully"); Ok(()) } fn main() { // Criar estrutura de diretórios para bundle let bundle_dir = Path::new("bundle"); let ollama_dir = bundle_dir.join("ollama"); let embeddings_dir = bundle_dir.join("models/embeddings"); std::fs::create_dir_all(&ollama_dir).ok(); std::fs::create_dir_all(&embeddings_dir).ok(); #[cfg(target_os = "windows")] { println!("cargo:warning=Preparing bundle resources for Windows..."); // 1. Baixar e validar instalador do Ollama com SHA256 let ollama_path = ollama_dir.join("OllamaSetup.exe"); match download_and_validate_ollama_setup(&ollama_path) { Ok(_) => {} Err(e) => { panic!("cargo:error=Failed to download and validate OllamaSetup.exe: {}", e); } } // 2. Baixar modelo de embeddings ONNX (apenas se não existir) let model_url = "https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/resolve/main/onnx/model.onnx"; let model_path = embeddings_dir.join("all-MiniLM-L6-v2.onnx"); if !model_path.exists() { println!("cargo:warning=Downloading ONNX embedding model (this may take a while, ~90MB)..."); let status = Command::new("powershell") .args(&[ "-Command", &format!( "if (!(Test-Path '{}')) {{ Invoke-WebRequest -Uri '{}' -OutFile '{}' -UseBasicParsing }}", model_path.display(), model_url, model_path.display() ) ]) .status(); if let Ok(exit_status) = status { if exit_status.success() { println!("cargo:warning=ONNX model downloaded successfully"); } else { println!("cargo:warning=Failed to download ONNX model (non-zero exit code)"); } } else { println!("cargo:warning=Failed to download ONNX model (command failed)"); } } else { println!("cargo:warning=ONNX model already exists, skipping download"); } // 3. Baixar tokenizer let tokenizer_url = "https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/resolve/main/tokenizer.json"; let tokenizer_path = embeddings_dir.join("tokenizer.json"); if !tokenizer_path.exists() { println!("cargo:warning=Downloading tokenizer..."); let status = Command::new("powershell") .args(&[ "-Command", &format!( "if (!(Test-Path '{}')) {{ Invoke-WebRequest -Uri '{}' -OutFile '{}' -UseBasicParsing }}", tokenizer_path.display(), tokenizer_url, tokenizer_path.display() ) ]) .status(); if let Ok(exit_status) = status { if exit_status.success() { println!("cargo:warning=Tokenizer downloaded successfully"); } else { println!("cargo:warning=Failed to download tokenizer (non-zero exit code)"); } } else { println!("cargo:warning=Failed to download tokenizer (command failed)"); } } else { println!("cargo:warning=Tokenizer already exists, skipping download"); } // 4. Baixar e extrair ONNX Runtime DLL let ort_url = "https://github.com/microsoft/onnxruntime/releases/download/v1.20.1/onnxruntime-win-x64-1.20.1.zip"; let ort_zip_path = embeddings_dir.join("onnxruntime.zip"); let ort_dll_path = embeddings_dir.join("onnxruntime.dll"); if !ort_dll_path.exists() { if !ort_zip_path.exists() { println!("cargo:warning=Downloading ONNX Runtime library (~50MB)..."); let status = Command::new("powershell") .args(&[ "-Command", &format!( "if (!(Test-Path '{}')) {{ Invoke-WebRequest -Uri '{}' -OutFile '{}' -UseBasicParsing }}", ort_zip_path.display(), ort_url, ort_zip_path.display() ) ]) .status(); if let Ok(exit_status) = status { if !exit_status.success() { println!("cargo:warning=Failed to download ONNX Runtime (non-zero exit code)"); } else { // Aguardar um pouco para garantir que o download foi concluído std::thread::sleep(std::time::Duration::from_secs(1)); } } else { println!("cargo:warning=Failed to download ONNX Runtime (command failed)"); } } // Extrair DLL do zip se o zip existe e está completo if ort_zip_path.exists() && !ort_dll_path.exists() { // Verificar se o arquivo zip não está vazio e tem tamanho mínimo razoável (~40MB) let min_zip_size = 40 * 1024 * 1024; // 40MB let zip_valid = if let Ok(metadata) = std::fs::metadata(&ort_zip_path) { let size = metadata.len(); if size < 1000 { println!("cargo:warning=ONNX Runtime zip file appears to be incomplete (too small: {} bytes), will be downloaded at runtime", size); false } else if size < min_zip_size { println!("cargo:warning=ONNX Runtime zip file appears to be incomplete ({} bytes, expected at least {} bytes), will be re-downloaded", size, min_zip_size); // Tentar remover ZIP corrompido para forçar re-download let _ = std::fs::remove_file(&ort_zip_path); false } else { true } } else { println!("cargo:warning=Failed to get metadata for ONNX Runtime zip file"); false }; if zip_valid { println!("cargo:warning=Extracting ONNX Runtime DLL from zip..."); // Usar zip crate para extrair (mais confiável que PowerShell) match extract_onnx_dll_from_zip(&ort_zip_path, &ort_dll_path) { Ok(_) => { println!("cargo:warning=ONNX Runtime DLL extracted successfully"); // Remover zip após extração bem-sucedida let _ = std::fs::remove_file(&ort_zip_path); } Err(e) => { println!("cargo:warning=Failed to extract ONNX Runtime DLL: {}. ZIP may be corrupted, will be downloaded at runtime", e); // Remover ZIP corrompido para forçar re-download em runtime let _ = std::fs::remove_file(&ort_zip_path); // Não falhar o build se a extração falhar - será baixado em runtime se necessário } } } } } else { println!("cargo:warning=ONNX Runtime DLL already exists, skipping download"); } } tauri_build::build() } /// Extrai onnxruntime.dll do arquivo zip fn extract_onnx_dll_from_zip(zip_path: &Path, dll_path: &Path) -> Result<(), String> { use zip::ZipArchive; let file = File::open(zip_path) .map_err(|e| format!("Failed to open zip file: {}", e))?; let mut archive = ZipArchive::new(file) .map_err(|e| format!("Failed to read zip archive: {}", e))?; // Procurar pela DLL no zip for i in 0..archive.len() { let mut file = archive.by_index(i) .map_err(|e| format!("Failed to read zip entry {}: {}", i, e))?; let name = file.name().to_string(); // Procurar por onnxruntime.dll (pode estar em subdiretórios) if name.ends_with("onnxruntime.dll") { // Criar diretório de destino se não existir if let Some(parent) = dll_path.parent() { std::fs::create_dir_all(parent) .map_err(|e| format!("Failed to create destination directory: {}", e))?; } // Extrair arquivo let mut outfile = File::create(dll_path) .map_err(|e| format!("Failed to create output file: {}", e))?; std::io::copy(&mut file, &mut outfile) .map_err(|e| format!("Failed to copy file: {}", e))?; return Ok(()); } } Err("onnxruntime.dll not found in zip archive".to_string()) }