use crate::config::Dependency;
use anyhow::{Context, Result};
use colored::*;
use git2::Repository;
use indicatif::{ProgressBar, ProgressStyle};
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::fs;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::process::Command;
#[allow(dead_code)]
pub fn verify_sha256(path: &Path, expected_hash: Option<&str>) -> Result<bool> {
let expected = match expected_hash {
Some(h) => h,
None => return Ok(true), };
let mut file = fs::File::open(path).with_context(|| {
format!(
"Failed to open file for hash verification: {}",
path.display()
)
})?;
let mut hasher = Sha256::new();
let mut buffer = [0u8; 8192];
loop {
let n = file.read(&mut buffer)?;
if n == 0 {
break;
}
hasher.update(&buffer[..n]);
}
let result = hasher.finalize();
let actual_hash = format!("{:x}", result);
if actual_hash.eq_ignore_ascii_case(expected) {
Ok(true)
} else {
Err(anyhow::anyhow!(
"SHA256 hash mismatch for {}:\n Expected: {}\n Actual: {}",
path.display(),
expected,
actual_hash
))
}
}
struct PrebuiltConfig {
asset_pattern: &'static str,
lib_path: &'static str,
include_path: &'static str,
}
fn get_prebuilt_config(name: &str) -> Option<PrebuiltConfig> {
match name.to_lowercase().as_str() {
"glfw" => Some(PrebuiltConfig {
asset_pattern: "glfw-{version}.bin.WIN64.zip",
lib_path: "glfw-{version}.bin.WIN64/lib-static-ucrt/glfw3.lib",
include_path: "glfw-{version}.bin.WIN64/include",
}),
"sdl2" | "sdl" => Some(PrebuiltConfig {
asset_pattern: "SDL2-devel-{version}-VC.zip",
lib_path: "SDL2-{version}/lib/x64/SDL2.lib",
include_path: "SDL2-{version}/include",
}),
_ => None,
}
}
fn detect_msvc_lib_folder() -> Option<&'static str> {
#[cfg(windows)]
{
if let Ok(output) = Command::new("cl.exe").output() {
let stderr = String::from_utf8_lossy(&output.stderr);
if stderr.contains("Version 19.5") || stderr.contains("Version 19.4") {
return None;
} else if stderr.contains("Version 19.3") {
return Some("lib-vc2022");
} else if stderr.contains("Version 19.2") {
return Some("lib-vc2019");
} else if stderr.contains("Version 19.1") {
return Some("lib-vc2017");
} else if stderr.contains("Version 19.0") {
return Some("lib-vc2015");
}
}
if std::path::Path::new("C:\\Program Files\\Microsoft Visual Studio\\2022").exists()
|| std::path::Path::new("C:\\Program Files (x86)\\Microsoft Visual Studio\\2022")
.exists()
{
return None;
} else if std::path::Path::new("C:\\Program Files (x86)\\Microsoft Visual Studio\\2019")
.exists()
{
return Some("lib-vc2019");
} else if std::path::Path::new("C:\\Program Files (x86)\\Microsoft Visual Studio\\2017")
.exists()
{
return Some("lib-vc2017");
}
}
None
}
fn try_download_prebuilt(
name: &str,
url: &str,
tag: Option<&str>,
lib_path: &Path,
output_file: &str,
) -> Result<bool> {
#[cfg(not(windows))]
{
return Ok(false);
}
let version = match tag {
Some(t) => t.trim_start_matches('v').trim_start_matches("release-"),
None => return Ok(false),
};
let config = match get_prebuilt_config(name) {
Some(c) => c,
None => return Ok(false),
};
let (owner, repo) = match parse_github_url(url) {
Some(pair) => pair,
None => return Ok(false),
};
let asset_name = config.asset_pattern.replace("{version}", version);
let download_url = format!(
"https://github.com/{}/{}/releases/download/{}/{}",
owner,
repo,
tag.unwrap_or(version),
asset_name
);
let expected_output = lib_path.join(output_file);
if expected_output.exists() {
return Ok(true);
}
println!(" {} Checking for prebuilt {}...", "âš¡".cyan(), name);
let agent = ureq::agent();
let response = match agent.get(&download_url).call() {
Ok(r) => r,
Err(_) => {
return Ok(false);
}
};
if response.status() != 200 {
return Ok(false);
}
println!(
" {} Downloading prebuilt {} (faster!)...",
"📦".blue(),
name
);
let temp_zip = lib_path.join("_prebuilt.zip");
let mut file = fs::File::create(&temp_zip)?;
let body = response.into_body();
let mut reader = body.into_reader();
let mut buffer = Vec::new();
reader.read_to_end(&mut buffer)?;
file.write_all(&buffer)?;
drop(file);
let zip_file = fs::File::open(&temp_zip)?;
let mut archive = zip::ZipArchive::new(zip_file)?;
let lib_suffix = config
.lib_path
.replace("{version}", version)
.split('/')
.next_back()
.unwrap_or("glfw3.lib")
.to_string();
let msvc_lib_folder = detect_msvc_lib_folder();
let mut lib_found = false;
for i in 0..archive.len() {
if let Ok(mut entry) = archive.by_index(i) {
let entry_name = entry.name().to_string();
if !entry_name.ends_with(&lib_suffix) || entry_name.contains("_mt.") {
continue;
}
let is_preferred = if let Some(lib_folder) = msvc_lib_folder {
entry_name.contains(lib_folder)
} else {
false
};
if is_preferred {
let out_path = lib_path.join(output_file);
if let Some(parent) = out_path.parent() {
fs::create_dir_all(parent)?;
}
let mut out_file = fs::File::create(&out_path)?;
std::io::copy(&mut entry, &mut out_file)?;
lib_found = true;
break;
}
}
}
if !lib_found {
let _ = fs::remove_file(&temp_zip);
return Ok(false);
}
let include_prefix = config.include_path.replace("{version}", version);
for i in 0..archive.len() {
if let Ok(mut entry) = archive.by_index(i) {
let entry_name = entry.name().to_string();
if entry_name.starts_with(&include_prefix) && !entry.is_dir() {
let relative = entry_name
.strip_prefix(&include_prefix)
.unwrap_or(&entry_name);
let out_path = lib_path
.join("include")
.join(relative.trim_start_matches('/'));
if let Some(parent) = out_path.parent() {
fs::create_dir_all(parent)?;
}
let mut out_file = fs::File::create(&out_path)?;
std::io::copy(&mut entry, &mut out_file)?;
}
}
}
let _ = fs::remove_file(&temp_zip);
println!(" {} Prebuilt {} ready!", "✓".green(), name);
Ok(true)
}
fn parse_github_url(url: &str) -> Option<(String, String)> {
let url = url.trim_end_matches(".git");
if url.contains("github.com") {
let parts: Vec<&str> = url.split('/').collect();
if parts.len() >= 2 {
let repo = parts.last()?;
let owner = parts.get(parts.len() - 2)?;
return Some((owner.to_string(), repo.to_string()));
}
}
None
}
pub fn fetch_dependencies(
deps: &HashMap<String, Dependency>,
) -> Result<(Vec<PathBuf>, Vec<String>, Vec<String>)> {
let home_dir = dirs::home_dir().context("Could not find home directory")?;
let cache_dir = home_dir.join(".cx").join("cache");
fs::create_dir_all(&cache_dir)?;
let mut lockfile = crate::lock::LockFile::load().unwrap_or_default();
let mut include_paths = Vec::new(); let mut extra_cflags = Vec::new(); let mut link_flags = Vec::new();
if !deps.is_empty() {
println!("{} Checking {} dependencies...", "📦".blue(), deps.len());
}
for (name, dep_data) in deps {
if let Dependency::Complex {
pkg: Some(pkg_name),
..
} = dep_data
{
println!(" {} Resolving system pkg: {}", "🔎".cyan(), pkg_name);
match Command::new("pkg-config")
.args(["--cflags", pkg_name])
.output()
{
Ok(out) => {
let out_str = String::from_utf8_lossy(&out.stdout).trim().to_string();
if !out_str.is_empty() {
for flag in out_str.split_whitespace() {
extra_cflags.push(flag.to_string());
}
}
}
Err(_) => println!("{} Warning: pkg-config tool not found", "!".yellow()),
}
if let Ok(out) = Command::new("pkg-config")
.args(["--libs", pkg_name])
.output()
{
if !out.status.success() {
println!(
"{} Package '{}' not found via pkg-config",
"x".red(),
pkg_name
);
}
let out_str = String::from_utf8_lossy(&out.stdout).trim().to_string();
if !out_str.is_empty() {
for flag in out_str.split_whitespace() {
link_flags.push(flag.to_string());
}
}
}
continue;
}
let (url, build_script, output_file, tag, branch, rev) = match dep_data {
Dependency::Simple(u) => (u.clone(), None, None, None, None, None),
Dependency::Complex {
git: Some(u),
build,
output,
tag,
branch,
rev,
..
} => (
u.clone(),
build.clone(),
output.clone(),
tag.clone(),
branch.clone(),
rev.clone(),
),
_ => continue,
};
let vendor_path = std::env::current_dir()?.join("vendor").join(name);
let (lib_path, is_vendor) = if vendor_path.exists() {
(vendor_path, true)
} else {
(cache_dir.join(name), false)
};
let repo = if !lib_path.exists() {
let pb = ProgressBar::new_spinner();
pb.set_style(
ProgressStyle::default_spinner()
.template("{spinner:.blue} {msg}")
.unwrap_or_else(|_| ProgressStyle::default_spinner())
.tick_chars("⣾⣽⣻⢿⡿⣟⣯⣷"),
);
pb.set_message(format!("Downloading {}...", name));
pb.enable_steady_tick(std::time::Duration::from_millis(100));
match Repository::clone(&url, &lib_path) {
Ok(r) => {
pb.finish_with_message(format!("{} Downloaded {}", "✓".green(), name));
r
}
Err(e) => {
pb.finish_with_message(format!("{} Failed {}", "x".red(), name));
println!("Error: {}", e);
continue;
}
}
} else {
if is_vendor {
println!(" {} Using vendor: {}", "📦".blue(), name);
} else {
println!(" {} Using cached: {}", "âš¡".green(), name);
}
match Repository::open(&lib_path) {
Ok(r) => r,
Err(_) => continue,
}
};
let mut obj_to_checkout = None;
let mut checkout_msg = String::new();
let mut locked_commit = None;
if let Some(lock_entry) = lockfile.get(name)
&& lock_entry.git == url
{
locked_commit = Some(lock_entry.rev.clone());
}
if let Some(r) = rev {
if let Ok(oid) = git2::Oid::from_str(&r)
&& let Ok(obj) = repo.find_object(oid, None)
{
obj_to_checkout = Some(obj);
checkout_msg = format!("commit {}", &r[..7]);
}
} else if let Some(ref t) = tag {
let refname = format!("refs/tags/{}", t);
if let Ok(r_ref) = repo.find_reference(&refname)
&& let Ok(obj) = r_ref.peel_to_commit()
{
obj_to_checkout = Some(obj.into_object());
checkout_msg = format!("tag {}", t);
}
} else if let Some(b) = branch {
if let Ok(r_ref) = repo.find_branch(&b, git2::BranchType::Local) {
if let Ok(obj) = r_ref.get().peel_to_commit() {
obj_to_checkout = Some(obj.into_object());
checkout_msg = format!("branch {}", b);
}
} else {
let remote_ref = format!("origin/{}", b);
if let Ok(r_ref) = repo.find_branch(&remote_ref, git2::BranchType::Remote)
&& let Ok(obj) = r_ref.get().peel_to_commit()
{
obj_to_checkout = Some(obj.into_object());
checkout_msg = format!("branch {}", b);
}
}
} else if let Some(rev) = locked_commit {
if let Ok(oid) = git2::Oid::from_str(&rev)
&& let Ok(obj) = repo.find_object(oid, None)
{
obj_to_checkout = Some(obj);
checkout_msg = format!("locked {}", &rev[..7]);
}
}
if let Some(obj) = obj_to_checkout {
repo.set_head_detached(obj.id())?;
let mut checkout_opts = git2::build::CheckoutBuilder::new();
checkout_opts.force();
repo.checkout_tree(&obj, Some(&mut checkout_opts))
.context(format!("Failed to checkout {}", checkout_msg))?;
println!(" {} Locked to {}", "📌".blue(), checkout_msg);
}
if let Ok(head) = repo.head()
&& let Ok(target) = head.peel_to_commit()
{
let current_hash = target.id().to_string();
lockfile.insert(name.clone(), url.clone(), current_hash);
}
let tag_ref = tag.as_deref();
let out_filename = output_file.as_deref().unwrap_or("");
let prebuilt_success = if !out_filename.is_empty() {
try_download_prebuilt(name, &url, tag_ref, &lib_path, out_filename).unwrap_or(false)
} else {
false
};
if !prebuilt_success && let Some(cmd_str) = build_script {
let should_build = if !out_filename.is_empty() {
!lib_path.join(out_filename).exists()
} else {
true
};
if should_build {
println!(" {} Building {}...", "🔨".yellow(), name);
let status = if cfg!(target_os = "windows") {
Command::new("cmd")
.args(["/C", &cmd_str])
.current_dir(&lib_path)
.status()
} else {
Command::new("sh")
.args(["-c", &cmd_str])
.current_dir(&lib_path)
.status()
};
match status {
Ok(s) if s.success() => {}
_ => {
println!("{} Build script failed for {}", "x".red(), name);
continue;
}
}
}
}
include_paths.push(lib_path.clone());
include_paths.push(lib_path.join("include"));
include_paths.push(lib_path.join("src"));
include_paths.push(lib_path.join("build").join("include"));
include_paths.push(lib_path.join("build").join("include").join("SDL2"));
include_paths.push(lib_path.join("dist"));
include_paths.push(lib_path.join("dist").join("include"));
if let Some(out_file) = output_file {
for single_output in out_file.split(',').map(|s| s.trim()) {
let full_lib_path = lib_path.join(single_output);
if full_lib_path.exists() {
link_flags.push(full_lib_path.to_string_lossy().to_string());
} else {
println!(
"{} Warning: Output file not found: {}",
"!".yellow(),
full_lib_path.display()
);
}
}
}
}
lockfile.save()?;
Ok((include_paths, extra_cflags, link_flags))
}