godoru 0.1.0

UI Framework for Rust using Godot
#![allow(non_snake_case)]

use serde::Deserialize;
use sha2::{Digest, Sha256};
use std::env;
use std::fs;
use std::io::Read;
use std::path::{Path, PathBuf};

#[derive(Deserialize)]
struct ArtifactConfig {
    godot_version: String,
    artifacts: Vec<Artifact>,
}

#[derive(Clone, Deserialize)]
struct Artifact {
    target: String,
    path: String,
    url: String,
    sha256: String,
}

fn main() {
    println!("cargo:rerun-if-changed=godot-artifacts.toml");
    println!("cargo:rerun-if-env-changed=GODORU_GODOT_DIR");
    println!("cargo:rerun-if-env-changed=GODORU_NO_DOWNLOAD");

    let manifestDir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
    let metadataPath = manifestDir.join("godot-artifacts.toml");
    let metadata = fs::read_to_string(&metadataPath).unwrap_or_else(|err| {
        panic!(
            "failed to read Godoru artifact metadata at {}: {err}",
            metadataPath.display()
        )
    });
    let config: ArtifactConfig = toml::from_str(&metadata).unwrap_or_else(|err| {
        panic!(
            "failed to parse Godoru artifact metadata at {}: {err}",
            metadataPath.display()
        )
    });
    let target = env::var("TARGET").unwrap();
    let artifacts: Vec<Artifact> = config
        .artifacts
        .iter()
        .filter(|artifact| artifact.target == target)
        .cloned()
        .collect();

    if artifacts.is_empty() {
        println!("cargo:rustc-env=GODORU_GODOT_DIR_RESOLVED=");
        return;
    }

    if let Ok(path) = env::var("GODORU_GODOT_DIR") {
        let root = PathBuf::from(path);
        verifyRoot(&root, &artifacts);
        println!(
            "cargo:rustc-env=GODORU_GODOT_DIR_RESOLVED={}",
            root.display()
        );
        return;
    }

    let localVendorRoot = manifestDir
        .join("vendor")
        .join("godot")
        .join(&config.godot_version);

    if hasArtifacts(&localVendorRoot, &artifacts) {
        verifyRoot(&localVendorRoot, &artifacts);
        println!(
            "cargo:rustc-env=GODORU_GODOT_DIR_RESOLVED={}",
            localVendorRoot.display()
        );
        return;
    }

    let cacheRoot = targetRoot()
        .join("godoru-godot")
        .join(&config.godot_version);

    if hasArtifacts(&cacheRoot, &artifacts) {
        verifyRoot(&cacheRoot, &artifacts);
        println!(
            "cargo:rustc-env=GODORU_GODOT_DIR_RESOLVED={}",
            cacheRoot.display()
        );
        return;
    }

    if env::var("GODORU_NO_DOWNLOAD").ok().as_deref() == Some("1") {
        panic!(
            "Godoru Godot artifacts are missing and GODORU_NO_DOWNLOAD=1 is set. Set GODORU_GODOT_DIR or restore {}",
            localVendorRoot.display()
        );
    }

    fs::create_dir_all(&cacheRoot).unwrap_or_else(|err| {
        panic!(
            "failed to create Godoru artifact cache at {}: {err}",
            cacheRoot.display()
        )
    });

    for artifact in &artifacts {
        downloadArtifact(&cacheRoot, artifact);
    }

    verifyRoot(&cacheRoot, &artifacts);
    println!(
        "cargo:rustc-env=GODORU_GODOT_DIR_RESOLVED={}",
        cacheRoot.display()
    );
}

fn targetRoot() -> PathBuf {
    let outDir = PathBuf::from(env::var("OUT_DIR").unwrap());
    for ancestor in outDir.ancestors() {
        if ancestor.file_name().and_then(|name| name.to_str()) == Some("target") {
            return ancestor.to_path_buf();
        }
    }
    outDir.join("godoru-target")
}

fn hasArtifacts(root: &Path, artifacts: &[Artifact]) -> bool {
    artifacts
        .iter()
        .all(|artifact| root.join(&artifact.path).is_file())
}

fn verifyRoot(root: &Path, artifacts: &[Artifact]) {
    for artifact in artifacts {
        let path = root.join(&artifact.path);
        if !path.is_file() {
            panic!("missing Godoru Godot artifact: {}", path.display());
        }

        let digest = fileSha256(&path);
        if digest != artifact.sha256 {
            panic!(
                "checksum mismatch for {}. expected {}, got {}",
                path.display(),
                artifact.sha256,
                digest
            );
        }
    }
}

fn fileSha256(path: &Path) -> String {
    let mut file = fs::File::open(path)
        .unwrap_or_else(|err| panic!("failed to open {}: {err}", path.display()));
    let mut hasher = Sha256::new();
    let mut buffer = [0u8; 64 * 1024];

    loop {
        let read = file
            .read(&mut buffer)
            .unwrap_or_else(|err| panic!("failed to read {}: {err}", path.display()));
        if read == 0 {
            break;
        }
        hasher.update(&buffer[..read]);
    }

    format!("{:x}", hasher.finalize())
}

fn downloadArtifact(root: &Path, artifact: &Artifact) {
    let path = root.join(&artifact.path);
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent).unwrap_or_else(|err| {
            panic!(
                "failed to create Godoru artifact directory {}: {err}",
                parent.display()
            )
        });
    }

    let response = ureq::get(&artifact.url)
        .call()
        .unwrap_or_else(|err| panic!("failed to download {}: {err}", artifact.url));
    let mut reader = response.into_reader();
    let mut file = fs::File::create(&path)
        .unwrap_or_else(|err| panic!("failed to create {}: {err}", path.display()));
    std::io::copy(&mut reader, &mut file)
        .unwrap_or_else(|err| panic!("failed to write {}: {err}", path.display()));
}