main-spex 0.23.0

AI-first development framework that turns specifications into deterministic, verifiable code
use std::fs;
use std::path::{Component, Path, PathBuf};

use eros::{Context, IntoAnyUnion};
use include_dir::{Dir, DirEntry};

use crate::error::{Result, could_not_read_dir};

#[cfg(test)]
#[path = "fs_test.rs"]
mod fs_test;

pub fn file_url(path: &Path) -> String {
    anstyle_hyperlink::file_to_url(None, &canonical(path)).unwrap_or_default()
}

pub fn current_dir() -> Result<PathBuf> {
    std::env::current_dir()
        .context("could not determine the current directory")
        .any_union()
}

pub fn write_file(path: &Path, contents: impl AsRef<[u8]>) -> Result<()> {
    fs::write(path, contents)
        .with_context(|| format!("could not write `{}`", path.display()))
        .any_union()
}

pub fn create_dir_all(path: &Path) -> Result<()> {
    fs::create_dir_all(path)
        .with_context(|| format!("could not create directory `{}`", path.display()))
        .any_union()
}

pub fn dir_name(path: &Path) -> Option<&str> {
    path.file_name().and_then(|name| name.to_str())
}

pub fn canonical(path: &Path) -> PathBuf {
    path.canonicalize().unwrap_or_else(|_| path.to_path_buf())
}

pub fn normalize(path: &Path) -> PathBuf {
    let mut normalized = PathBuf::new();
    for component in path.components() {
        match component {
            Component::CurDir => {}
            Component::ParentDir if normalized.pop() => {}
            other => normalized.push(other),
        }
    }
    normalized
}

pub fn relative_to<'a>(path: &'a Path, base: &Path) -> &'a Path {
    path.strip_prefix(base).unwrap_or(path)
}

pub fn write_embedded_dir(dir: &Dir, dest_root: &Path) -> Result<()> {
    write_embedded_dir_at(dir, &dest_root.join(dir.path()))
}

pub fn write_embedded_dir_at(dir: &Dir, dest_dir: &Path) -> Result<()> {
    write_embedded_entries(dir, dir.path(), dest_dir)
}

fn write_embedded_entries(dir: &Dir, root: &Path, dest_dir: &Path) -> Result<()> {
    for entry in dir.entries() {
        match entry {
            DirEntry::Dir(sub) => write_embedded_entries(sub, root, dest_dir)?,
            DirEntry::File(file) => {
                let relative = file.path().strip_prefix(root).unwrap_or(file.path());
                let dest = dest_dir.join(relative);
                if let Some(parent) = dest.parent() {
                    create_dir_all(parent)?;
                }
                write_file(&dest, file.contents())?;
            }
        }
    }
    Ok(())
}

#[cfg(unix)]
pub fn symlink_if_absent(link: &Path, target: &str) -> Result<()> {
    use std::os::unix::fs::symlink;

    if link.symlink_metadata().is_ok() {
        return Ok(());
    }

    symlink(target, link)
        .with_context(|| format!("could not link `{}` to `{target}`", link.display()))
        .any_union()
}

#[cfg(not(unix))]
pub fn symlink_if_absent(_link: &Path, _target: &str) -> Result<()> {
    Ok(())
}

#[cfg(unix)]
pub fn make_files_executable(dir: &Path) -> Result<()> {
    use std::os::unix::fs::PermissionsExt;

    use walkdir::WalkDir;

    if !dir.is_dir() {
        return Ok(());
    }

    for entry in WalkDir::new(dir) {
        let entry = entry.with_context(|| could_not_read_dir(dir))?;
        if !entry.file_type().is_file() {
            continue;
        }
        let path = entry.path();
        let mut permissions = entry
            .metadata()
            .with_context(|| format!("could not read metadata of `{}`", path.display()))?
            .permissions();
        permissions.set_mode(permissions.mode() | 0o111);
        fs::set_permissions(path, permissions)
            .with_context(|| format!("could not set permissions on `{}`", path.display()))?;
    }
    Ok(())
}

#[cfg(not(unix))]
pub fn make_files_executable(_dir: &Path) -> Result<()> {
    Ok(())
}