dioxus-cli 0.7.9

CLI for building fullstack web, desktop, and mobile apps with a single codebase.
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;

fn main() {
    println!("cargo:rerun-if-env-changed=DIOXUS_CLI_GIT_SHA");
    println!("cargo:rerun-if-env-changed=DIOXUS_CLI_GIT_SHA_SHORT");

    let manifest_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
    let git_dir = find_git_dir(&manifest_dir);

    // Track the files that determine the current commit. Absolute paths are
    // required because the build script runs from `packages/cli/`, not the
    // workspace root where `.git` lives.
    if let Some(git_dir) = git_dir.as_deref() {
        emit_rerun(&git_dir.join("HEAD"));
        emit_rerun(&git_dir.join("packed-refs"));
        if let Some(target) = head_ref_target(git_dir) {
            emit_rerun(&git_dir.join(target));
        }
    }
    emit_rerun(&manifest_dir.join(".cargo_vcs_info.json"));

    let full_hash = env_nonempty("DIOXUS_CLI_GIT_SHA")
        .or_else(|| git_dir.as_deref().and_then(read_head_sha))
        .or_else(|| read_cargo_vcs_info(&manifest_dir))
        .or_else(|| git_cli_sha(&manifest_dir));

    if let Some(full_hash) = full_hash {
        let full_hash = full_hash.trim();
        println!("cargo:rustc-env=DIOXUS_CLI_GIT_SHA={full_hash}");

        let short_hash = env_nonempty("DIOXUS_CLI_GIT_SHA_SHORT")
            .unwrap_or_else(|| full_hash.chars().take(7).collect());
        println!("cargo:rustc-env=DIOXUS_CLI_GIT_SHA_SHORT={short_hash}");
    }
}

fn env_nonempty(key: &str) -> Option<String> {
    env::var(key).ok().filter(|s| !s.trim().is_empty())
}

fn emit_rerun(path: &Path) {
    println!("cargo:rerun-if-changed={}", path.display());
}

/// Walk up from `start` looking for `.git`. Handles the file form used by
/// git worktrees and submodules, where `.git` contains `gitdir: <path>`.
fn find_git_dir(start: &Path) -> Option<PathBuf> {
    for dir in start.ancestors() {
        let candidate = dir.join(".git");
        let meta = match fs::metadata(&candidate) {
            Ok(m) => m,
            Err(_) => continue,
        };
        if meta.is_dir() {
            return Some(candidate);
        }
        if meta.is_file() {
            let contents = fs::read_to_string(&candidate).ok()?;
            let pointer = contents
                .lines()
                .find_map(|l| l.strip_prefix("gitdir:"))?
                .trim();
            let resolved = if Path::new(pointer).is_absolute() {
                PathBuf::from(pointer)
            } else {
                dir.join(pointer)
            };
            return Some(resolved);
        }
    }
    None
}

/// If HEAD is a symbolic ref (`ref: refs/heads/foo`), return the target path.
fn head_ref_target(git_dir: &Path) -> Option<String> {
    let head = fs::read_to_string(git_dir.join("HEAD")).ok()?;
    head.strip_prefix("ref:").map(|s| s.trim().to_owned())
}

fn read_head_sha(git_dir: &Path) -> Option<String> {
    let head = fs::read_to_string(git_dir.join("HEAD")).ok()?;
    let head = head.trim();

    let Some(target) = head.strip_prefix("ref:").map(str::trim) else {
        // Detached HEAD: the file holds the SHA directly.
        return is_sha(head).then(|| head.to_owned());
    };

    // Loose ref file.
    if let Ok(sha) = fs::read_to_string(git_dir.join(target)) {
        let sha = sha.trim();
        if is_sha(sha) {
            return Some(sha.to_owned());
        }
    }

    // Packed refs (after `git gc` / shallow clones, refs only live here).
    let packed = fs::read_to_string(git_dir.join("packed-refs")).ok()?;
    for line in packed.lines() {
        if line.starts_with('#') || line.starts_with('^') || line.is_empty() {
            continue;
        }
        let (sha, name) = line.split_once(' ')?;
        if name == target && is_sha(sha) {
            return Some(sha.to_owned());
        }
    }
    None
}

/// Cargo writes `.cargo_vcs_info.json` next to `Cargo.toml` when publishing,
/// recording the source SHA at publish time. This is how `cargo install`
/// from crates.io can still report a real commit despite the `.crate`
/// tarball having no `.git/`.
fn read_cargo_vcs_info(manifest_dir: &Path) -> Option<String> {
    let contents = fs::read_to_string(manifest_dir.join(".cargo_vcs_info.json")).ok()?;
    // Cheap targeted parse so we don't pull serde into the build script.
    // The file is generated by cargo and the field name is stable.
    let after = contents.split("\"sha1\"").nth(1)?;
    let sha = after.split('"').nth(1)?.trim();
    is_sha(sha).then(|| sha.to_owned())
}

fn git_cli_sha(manifest_dir: &Path) -> Option<String> {
    // Scope `git` to the manifest dir so we never accidentally pick up a
    // SHA from a parent repo (e.g. someone's `~` is a git repo).
    let output = Command::new("git")
        .arg("-C")
        .arg(manifest_dir)
        .args(["rev-parse", "HEAD"])
        .output()
        .ok()?;
    if !output.status.success() {
        return None;
    }
    let sha = String::from_utf8(output.stdout).ok()?.trim().to_owned();
    is_sha(&sha).then_some(sha)
}

fn is_sha(s: &str) -> bool {
    matches!(s.len(), 40 | 64) && s.chars().all(|c| c.is_ascii_hexdigit())
}