midenup 0.3.2

The Miden toolchain manager
Documentation
//! This module contains some general purpose functions.

pub mod git {
    use std::path::Path;

    use anyhow::Context;

    pub fn find_latest_hash(repository_url: &str, branch_name: &str) -> anyhow::Result<String> {
        let check_revision_hash = std::process::Command::new("git")
            .arg("ls-remote")
            .arg(repository_url)
            .arg("--branch")
            .arg(branch_name)
            .stderr(std::process::Stdio::inherit())
            .stdout(std::process::Stdio::piped())
            .output()
            .context(format!(
                "failed to fetch latest git rev-hash from branch {branch_name}, is git installed?.",
            ))?;

        // This returns a string of the form:
        //
        // sym_ref\tref_name
        //
        // Source: https://github.com/git/git/blob/41905d60226a0346b22f0d0d99428c746a5a3b14/builtin/ls-remote.c#L169
        let revision_hash: String = String::from_utf8(check_revision_hash.stdout)
            .context(format!(
                "failed to format latest git rev-hash from branch {branch_name}, does the branch \
                 exist?.",
            ))?
            .chars()
            .take_while(|&c| c != '\t')
            .collect();

        Ok(revision_hash)
    }

    // Used in tests
    #[allow(dead_code)]
    pub fn clone_specific_revision(
        repository_url: &str,
        revision: &str,
        dir: &Path,
    ) -> anyhow::Result<()> {
        std::process::Command::new("git")
            .arg("clone")
            .args(["--revision", revision])
            .arg("--depth=1")
            .arg("--")
            .arg(repository_url)
            .arg(dir)
            .stderr(std::process::Stdio::inherit())
            .stdout(std::process::Stdio::inherit())
            .spawn()
            .context("Failed to spawn shell for git command")?
            .wait()
            .with_context(|| {
                format!("failed to clone {revision} of {repository_url} to {}", dir.display())
            })?;
        Ok(())
    }
}

pub mod fs {
    use std::{
        fs,
        path::{Path, PathBuf},
        time::SystemTime,
    };

    use anyhow::Context;

    #[cfg(unix)]
    pub fn symlink(from: &std::path::Path, to: &std::path::Path) -> anyhow::Result<()> {
        std::os::unix::fs::symlink(to, from).context("could not create symlink")
    }

    #[cfg(windows)]
    pub fn symlink(from: &std::path::Path, to: &std::path::Path) -> anyhow::Result<()> {
        std::os::windows::fs::symlink_file(to, from).context("could not create symlink")
    }

    const ENTRY_LIMIT: u32 = u32::MAX;

    /// Returns the latest registered modification time inside a directory, including its
    /// subdirectories.
    ///
    /// This is intended as a "best effort" approximation, if it encounters any errors while reading
    /// an entry, it simply skips it. Additionally, as a safety net, the `ENTRY_LIMIT` sets an upper
    /// bound on the number of entries the function can check before returning.
    pub fn latest_modification(dir: &Path) -> anyhow::Result<(SystemTime, PathBuf)> {
        fn traverse_directories(
            dir: &Path,
            latest: Option<(SystemTime, PathBuf)>,
            current_entry: u32,
        ) -> (Option<(SystemTime, PathBuf)>, u32) {
            let mut local_latest = latest;
            let mut current_entry_count = current_entry;

            let entries = fs::read_dir(dir);
            if let Ok(entries) = entries {
                for file in entries {
                    let Ok(file) = file else {
                        continue;
                    };
                    let Ok(metadata) = file.metadata() else {
                        continue;
                    };

                    if current_entry_count == ENTRY_LIMIT {
                        break;
                    }

                    let (current_entry_latest, visited_entries) =
                    // We avoid symlinks to directories to avoid infinite loops.
                    if metadata.is_dir() && !metadata.is_symlink() {
                        traverse_directories(&file.path(), local_latest.clone(), current_entry_count)
                    } else {
                        (metadata.modified().ok().map(|metadata| (metadata, file.path())), current_entry_count + 1)
                    };

                    current_entry_count = visited_entries;

                    local_latest = match (&local_latest, current_entry_latest) {
                        (
                            Some((local_latest_time, path_old)),
                            Some((current_entry_latest, path)),
                        ) => {
                            if current_entry_latest > *local_latest_time {
                                Some((current_entry_latest, path))
                            } else {
                                Some((*local_latest_time, path_old.to_path_buf()))
                            }
                        },
                        (Some(local_latest), None) => Some(local_latest.clone()),
                        (None, Some(current_entry_latest)) => Some(current_entry_latest),
                        (None, None) => None,
                    };
                }
            } else {
                println!("Failed to open {}, skipping it.", dir.display());
            }

            (local_latest, current_entry_count)
        }

        let directory_last_modification = dir
            .metadata()
            .and_then(|file| file.modified())
            .map(|metadata| (metadata, dir.to_path_buf()))
            .ok();

        let (latest_found_modification, _) =
            traverse_directories(dir, directory_last_modification, 0);

        // This should only be an error if every single metadata read failed, which should be
        // unlikely.
        latest_found_modification.context("Failed to read any file")
    }

    /// Recursively copy every entry from `src` into `dst`, preserving the directory layout and
    /// recreating symlinks. Entries whose file name appears in `skip` are not copied. `dst` is
    /// expected to already exist.
    pub fn copy_dir_recursive(src: &Path, dst: &Path, skip: &[&str]) -> anyhow::Result<()> {
        for entry in fs::read_dir(src)
            .with_context(|| format!("failed to read directory '{}'", src.display()))?
        {
            let entry =
                entry.with_context(|| format!("failed to read entry in '{}'", src.display()))?;
            let file_name = entry.file_name();
            if file_name.to_str().is_some_and(|name| skip.contains(&name)) {
                continue;
            }
            let file_type = entry
                .file_type()
                .with_context(|| format!("failed to stat entry '{}'", entry.path().display()))?;
            let target = dst.join(&file_name);
            if file_type.is_symlink() {
                let link_target = fs::read_link(entry.path()).with_context(|| {
                    format!("failed to read symlink '{}'", entry.path().display())
                })?;
                symlink(&target, &link_target).with_context(|| {
                    format!(
                        "failed to recreate symlink '{}' -> '{}'",
                        target.display(),
                        link_target.display()
                    )
                })?;
            } else if file_type.is_dir() {
                fs::create_dir_all(&target).with_context(|| {
                    format!("failed to create directory '{}'", target.display())
                })?;
                copy_dir_recursive(&entry.path(), &target, skip)?;
            } else {
                fs::copy(entry.path(), &target).with_context(|| {
                    format!("failed to copy '{}' to '{}'", entry.path().display(), target.display())
                })?;
            }
        }
        Ok(())
    }
}