turtle-build 0.3.9

Ninja-compatible build system for high-level programming languages
use super::{build_hash::BuildHash, context::Context};
use crate::{
    error::InfrastructureError,
    ir::{Build, Rule},
};
use std::{
    collections::hash_map::DefaultHasher,
    hash::{Hash, Hasher},
    path::Path,
    time::SystemTime,
};
use tokio::{
    fs::{metadata, File},
    io::AsyncReadExt,
};

const BUFFER_CAPACITY: usize = 2 << 10;

pub async fn calculate_timestamp_hash(
    context: &Context,
    build: &Build,
    file_inputs: &[&str],
    phony_inputs: &[&str],
) -> Result<u64, InfrastructureError> {
    if let Some(hash) = calculate_fallback_hash(build, file_inputs, phony_inputs) {
        return Ok(hash);
    }

    let mut hasher = DefaultHasher::new();

    hash_command(build, &mut hasher);

    for input in file_inputs {
        read_timestamp(input).await?.hash(&mut hasher);
    }

    for &input in phony_inputs {
        get_build_hash(context, input)?
            .timestamp()
            .hash(&mut hasher);
    }

    Ok(hasher.finish())
}

pub async fn calculate_content_hash(
    context: &Context,
    build: &Build,
    file_inputs: &[&str],
    phony_inputs: &[&str],
) -> Result<u64, InfrastructureError> {
    if let Some(hash) = calculate_fallback_hash(build, file_inputs, phony_inputs) {
        return Ok(hash);
    }

    let mut hasher = DefaultHasher::new();

    hash_command(build, &mut hasher);

    let mut buffer = Vec::with_capacity(BUFFER_CAPACITY);

    for input in file_inputs {
        File::open(input).await?.read_to_end(&mut buffer).await?;
        buffer.hash(&mut hasher);
        buffer.clear();
    }

    for &input in phony_inputs {
        get_build_hash(context, input)?.content().hash(&mut hasher);
    }

    Ok(hasher.finish())
}

fn get_build_hash(context: &Context, input: &str) -> Result<BuildHash, InfrastructureError> {
    context
        .database()
        .get(
            context
                .configuration()
                .outputs()
                .get(input)
                .ok_or_else(|| InfrastructureError::InputNotFound(input.into()))?
                .id(),
        )?
        .ok_or_else(|| InfrastructureError::InputNotBuilt(input.into()))
}

fn calculate_fallback_hash(
    build: &Build,
    file_inputs: &[&str],
    phony_inputs: &[&str],
) -> Option<u64> {
    if build.rule().is_none() && file_inputs.is_empty() && phony_inputs.is_empty() {
        Some(rand::random())
    } else {
        None
    }
}

fn hash_command(build: &Build, hasher: &mut impl Hasher) {
    build.rule().map(Rule::command).hash(hasher);
}

async fn read_timestamp(path: impl AsRef<Path>) -> Result<SystemTime, InfrastructureError> {
    let path = path.as_ref();

    metadata(path)
        .await
        .map_err(|error| InfrastructureError::with_path(error, path))?
        .modified()
        .map_err(|error| InfrastructureError::with_path(error, path))
}