embuild/
fs.rs

1//! Filesystem utilities.
2
3use std::fs::{self, File};
4use std::io::{self, Read};
5use std::path::Path;
6
7use anyhow::Result;
8
9/// Copy `src_file` to `dest_file_or_dir` if `src_file` is different or the destination
10/// file doesn't exist.
11///
12/// ### Panics
13/// If `src_file` is not a file this function will panic.
14pub fn copy_file_if_different(
15    src_file: impl AsRef<Path>,
16    dest_file_or_dir: impl AsRef<Path>,
17) -> Result<()> {
18    let src_file: &Path = src_file.as_ref();
19    let dest_file_or_dir: &Path = dest_file_or_dir.as_ref();
20
21    assert!(src_file.is_file());
22
23    let src_fd = fs::File::open(src_file)?;
24
25    let (dest_fd, dest_file) = if dest_file_or_dir.exists() {
26        if dest_file_or_dir.is_dir() {
27            let dest_file = dest_file_or_dir.join(src_file.file_name().unwrap());
28            if dest_file.exists() {
29                (Some(fs::File::open(&dest_file)?), dest_file)
30            } else {
31                (None, dest_file)
32            }
33        } else {
34            (
35                Some(fs::File::open(dest_file_or_dir)?),
36                dest_file_or_dir.to_owned(),
37            )
38        }
39    } else {
40        (None, dest_file_or_dir.to_owned())
41    };
42
43    if let Some(dest_fd) = dest_fd {
44        if !is_file_eq(&src_fd, &dest_fd)? {
45            drop(dest_fd);
46            drop(src_fd);
47            copy_with_metadata(src_file, dest_file)?;
48        }
49    } else {
50        copy_with_metadata(src_file, dest_file)?;
51    }
52    Ok(())
53}
54
55/// Whether the file type and contents of `file` are equal to `other`.
56pub fn is_file_eq(file: &File, other: &File) -> Result<bool> {
57    let file_meta = file.metadata()?;
58    let other_meta = other.metadata()?;
59
60    if file_meta.file_type() == other_meta.file_type()
61        && file_meta.len() == other_meta.len()
62        && file_meta.modified()? == other_meta.modified()?
63    {
64        let mut file_bytes = io::BufReader::new(file).bytes();
65        let mut other_bytes = io::BufReader::new(other).bytes();
66
67        // TODO: check performance
68        loop {
69            match (file_bytes.next(), other_bytes.next()) {
70                (Some(Ok(b0)), Some(Ok(b1))) => {
71                    if b0 != b1 {
72                        break Ok(false);
73                    }
74                }
75                (None, None) => break Ok(true),
76                (None, Some(_)) | (Some(_), None) => break Ok(false),
77                (Some(Err(e)), _) | (_, Some(Err(e))) => return Err(e.into()),
78            }
79        }
80    } else {
81        Ok(false)
82    }
83}
84
85/// Wrap [`fs::copy`] to also copy metadata such as permissions and file times.
86///
87/// This function is required because Cargo's fingerprinting uses mtime which is not perpetuated by
88/// [`fs::copy`]. If Cargo detects a fingerprint mismatch, it will rebuild the crate with the
89/// mismatch. This could lead Cargo to keep rebuilding the same crate over and over because the
90/// comparison will always yield a new fingerprint since [`fs::copy`] doesn't take mtime into
91/// account.
92pub fn copy_with_metadata(src_file: impl AsRef<Path>, dest_file: impl AsRef<Path>) -> Result<()> {
93    fs::copy(&src_file, &dest_file)?;
94    let src_file_meta = fs::File::open(&src_file)?.metadata()?;
95
96    let src_atime = filetime::FileTime::from_last_access_time(&src_file_meta);
97    let src_mtime = filetime::FileTime::from_last_modification_time(&src_file_meta);
98
99    fs::set_permissions(dest_file.as_ref(), src_file_meta.permissions())?;
100    filetime::set_file_times(dest_file, src_atime, src_mtime)?;
101
102    Ok(())
103}