Avoid running sphinx-build if possible

Despite the caching in `sphinx-build`, it takes several seconds to run
`sphinx-build` when no rebuilding is necessary, which slows down build times
significantly.

Add custom logic to `build.rs` to avoid calling `sphinx-build` if deemed
unnecessary based on the mtime of the source files.
This is done by writing the most recent timestamp of the source files into a
dedicated file, and only calling `sphinx-build` (and updating the timestamp)
when a cached timestamp is older than the most recent source file mtime.
This commit is contained in:
Daniel Rainer
2025-06-27 19:02:26 +02:00
parent eae633c4af
commit 2c11bfa532

110
build.rs
View File

@@ -406,6 +406,106 @@ fn get_git_hash() -> Result<String, Box<dyn std::error::Error>> {
get_git_hash().expect("Could not get a version. Either set $FISH_BUILD_VERSION or install git.")
}
#[cfg(feature = "embed-data")]
#[cfg(not(clippy))]
/// Given a path, returns an iterator containing this path, and possibly others depending on what
/// the input path refers to.
/// If the path points to a file, only this file will be included.
/// If it is a symlink, the path of the link will be included, as well as the target,
/// provided the target is a file. Panics on non-file targets, including non-existing targets.
/// If it is a directory, the function recurses on each entry and combines the results into the
/// returned iterator.
fn read_dir_all<P: AsRef<Path>>(path: P) -> impl Iterator<Item = PathBuf> {
fn walk(path: PathBuf) -> Box<dyn Iterator<Item = PathBuf>> {
if path.is_file() {
return Box::new([path].into_iter());
}
if path.is_symlink() {
let target = path.read_link().unwrap();
if target.is_file() {
return Box::new([path, target].into_iter());
}
panic!("Symlink {path:?} does not point to a file. Such symlinks are unsupported.");
}
if path.is_dir() {
return Box::new(
[path.clone()].into_iter().chain(
std::fs::read_dir(path)
.unwrap()
.flat_map(|p| walk(p.unwrap().path())),
),
);
}
panic!("Unexpected type for path {path:?}");
}
walk(path.as_ref().to_owned())
}
#[cfg(feature = "embed-data")]
#[cfg(not(clippy))]
/// Helper function for avoiding unnecessary rebuilds.
/// Specify all files/directories which should trigger a rebuild if modified via `source_paths`.
/// `cache_file` is used to store the time of the latest build.
/// If this file does not exist, it will be created, if possible.
/// The modification time of all files in `source_paths` are checked.
/// For symlinks, both the link and the target are considered, but only links to files are
/// supported.
/// If the most recent mtime found this way is not more recent than the one stored in `cache_file`,
/// then the build directory is considered up-to-date and `true` is returned.
/// Otherwise, the timestamp in `cache_file` is updated and false is returned.
/// The mtime is stored as milliseconds since the Unix epoch.
/// There are some edge cases around the time, which could theoretically result in unnecessary
/// builds or builds not being updated when they should.
/// <https://doc.rust-lang.org/std/time/struct.SystemTime.html>
/// Deleting the `cache_file` before invoking this function prevents the latter case (but makes
/// this function useless).
fn is_build_dir_up_to_date<'a, P: AsRef<Path> + 'a, I: IntoIterator<Item = &'a P>>(
source_paths: I,
cache_file: &Path,
) -> bool {
use std::{
io::{Read, Seek, SeekFrom, Write},
str::FromStr,
time::UNIX_EPOCH,
};
let source_timestamp = source_paths
.into_iter()
.flat_map(read_dir_all)
.map(|p| p.metadata().unwrap().modified().unwrap())
.max()
.unwrap()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis();
let cache_file_dir = cache_file.parent().unwrap();
std::fs::create_dir_all(cache_file_dir).unwrap();
let mut timestamp_file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(cache_file)
.unwrap();
let mut timestamp_file_content = vec![];
timestamp_file
.read_to_end(&mut timestamp_file_content)
.unwrap();
if !timestamp_file_content.is_empty() {
let timestamp_file_str = std::str::from_utf8(&timestamp_file_content).unwrap().trim();
let build_timestamp = u128::from_str(timestamp_file_str).unwrap();
if source_timestamp <= build_timestamp {
return true;
}
timestamp_file.seek(SeekFrom::Start(0)).unwrap();
timestamp_file.set_len(0).unwrap();
}
timestamp_file
.write_all(format!("{source_timestamp}\n").as_bytes())
.unwrap();
false
}
#[cfg(feature = "embed-data")]
// disable clippy because otherwise it would panic without sphinx
#[cfg(not(clippy))]
@@ -415,6 +515,16 @@ fn build_man(build_dir: &Path) {
let sec1dir = mandir.join("man1");
let docsrc_path = canonicalize(MANIFEST_DIR).join("doc_src");
let docsrc = docsrc_path.to_str().unwrap();
// `sphinx-build` runs for several seconds even if none of the sources changed.
// This can result in the completely useless `sphinx-build` invocation almost doubling the
// duration of a `cargo b` (for a non-clean build).
// https://github.com/sphinx-doc/sphinx/issues/13727
let timestamp_file_path = mandir.join("build_timestamp");
if is_build_dir_up_to_date(&SPHINX_DOC_SOURCES, &timestamp_file_path) {
return;
}
let args = &[
"-j",
"auto",