use std::{
cell::RefCell,
collections::{hash_map::Entry, HashMap},
ffi::OsStr,
os::unix::ffi::OsStrExt,
string::FromUtf8Error,
time::SystemTime,
};
use ninja_metrics::scoped_metric;
use thiserror::Error;
use crate::{
build_task::{CommandTask, CommandTaskResult, NinjaTask, NoopTask},
disk_interface::DiskInterface,
interface::Rebuilder,
task::{Key, Task},
};
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum Dirtiness {
Clean,
Dirty,
DoesNotExist,
Modified(SystemTime),
}
pub trait DirtyCache {
fn dirtiness(&self, key: Key) -> std::io::Result<Dirtiness>;
fn mark_dirty(&self, key: Key, is_dirty: bool);
}
#[derive(Debug)]
pub struct DiskDirtyCache<Disk>
where
Disk: DiskInterface,
{
dirty: RefCell<HashMap<Key, Dirtiness>>,
disk: Disk,
}
impl<Disk> DiskDirtyCache<Disk>
where
Disk: DiskInterface,
{
pub fn new(disk: Disk) -> Self {
DiskDirtyCache {
disk,
dirty: Default::default(),
}
}
}
impl<Disk> DirtyCache for DiskDirtyCache<Disk>
where
Disk: DiskInterface,
{
fn dirtiness(&self, key: Key) -> std::io::Result<Dirtiness> {
match self.dirty.borrow_mut().entry(key.clone()) {
Entry::Occupied(e) => Ok(*e.get()),
Entry::Vacant(entry) => match key {
Key::Path(key) => {
scoped_metric!("mtime_state_insert");
let inserted = entry.insert(
self.disk
.modified(OsStr::from_bytes(key.as_bytes()))
.map(Dirtiness::Modified)
.or_else(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
Ok(Dirtiness::DoesNotExist)
} else {
Err(e)
}
})?,
);
Ok(*inserted)
}
Key::Multi(_) => {
panic!("Cannot mtime a multi-key. Did you forget to mark it as dirty to ensure it is in the cache?");
}
},
}
}
fn mark_dirty(&self, key: Key, is_dirty: bool) {
if is_dirty || key.is_multi() {
self.dirty.borrow_mut().insert(
key,
if is_dirty {
Dirtiness::Dirty
} else {
Dirtiness::Clean
},
);
}
}
}
#[derive(Debug)]
pub struct CachingMTimeRebuilder<Cache>
where
Cache: DirtyCache,
{
mtime_state: Cache,
}
impl<Cache> CachingMTimeRebuilder<Cache>
where
Cache: DirtyCache,
{
pub fn new(mtime_state: Cache) -> Self {
Self { mtime_state }
}
}
#[derive(Error, Debug)]
pub enum RebuilderError {
#[error("utf-8 error")]
Utf8Error(#[from] FromUtf8Error),
#[error("'{input}', needed by '{output}', missing and no known rule to make it")]
MissingInput { output: String, input: String },
#[error("error looking up mtime")]
IOError(#[from] std::io::Error),
}
impl<Cache> Rebuilder<Key, CommandTaskResult> for CachingMTimeRebuilder<Cache>
where
Cache: DirtyCache,
{
type Error = RebuilderError;
type Task = dyn NinjaTask;
fn build(
&self,
key: Key,
_unused: Option<CommandTaskResult>,
task: &Task,
) -> Result<Box<Self::Task>, Self::Error> {
let outputs_dirty: Dirtiness = match key.clone() {
Key::Path(_) => self.mtime_state.dirtiness(key.clone())?,
Key::Multi(keys) => {
debug_assert!(keys.len() > 1);
keys.iter()
.try_fold(
None,
|so_far, current_key| -> Result<Option<Dirtiness>, RebuilderError> {
let dirty =
self.mtime_state.dirtiness(Key::Path(current_key.clone()))?;
match so_far {
None => Ok(Some(dirty)),
Some(so_far) => Ok(Some(match (so_far, dirty) {
(
Dirtiness::Modified(so_far),
Dirtiness::Modified(this_one),
) => Dirtiness::Modified(std::cmp::min(so_far, this_one)),
_ => Dirtiness::Dirty,
})),
}
},
)?
.expect("non-None because multi-key has at least two elements")
}
};
let dependencies = task.dependencies();
let inputs_dirty = if dependencies.len() == 1 && matches!(dependencies[0], Key::Multi(_)) {
assert!(task.is_retrieve());
Some(self.mtime_state.dirtiness(dependencies[0].clone())?)
} else {
for dep in dependencies {
assert!(dep.is_path());
}
dependencies.iter().try_fold(
None,
|so_far, current_dep| -> Result<Option<Dirtiness>, RebuilderError> {
match current_dep {
Key::Path(key_path) => {
let dep_mtime = self.mtime_state.dirtiness(current_dep.clone())?;
if dep_mtime == Dirtiness::DoesNotExist {
let output = match key.clone() {
Key::Path(key) => String::from_utf8(key.as_bytes().to_vec())?,
Key::Multi(keys) => {
String::from_utf8(keys[0].as_bytes().to_vec())?
}
};
Err(RebuilderError::MissingInput {
input: String::from_utf8(key_path.as_bytes().to_vec())?,
output,
})
} else {
Ok(match so_far {
None => Some(dep_mtime),
Some(so_far) => {
assert_ne!(so_far, Dirtiness::DoesNotExist);
assert_ne!(dep_mtime, Dirtiness::DoesNotExist);
Some(match (so_far, dep_mtime) {
(
Dirtiness::Modified(so_far),
Dirtiness::Modified(dep_mtime),
) => Dirtiness::Modified(std::cmp::max(
so_far, dep_mtime,
)),
_ => Dirtiness::Dirty,
})
}
})
}
}
_ => unreachable!(),
}
},
)?
};
let dirty = if let Dirtiness::Modified(output_mtime) = outputs_dirty {
if let Some(inputs_dirty) = inputs_dirty {
match inputs_dirty {
Dirtiness::Clean => false,
Dirtiness::Dirty => true,
Dirtiness::DoesNotExist => unreachable!(),
Dirtiness::Modified(input_mtime) => input_mtime > output_mtime,
}
} else {
false
}
} else {
true
};
self.mtime_state.mark_dirty(key.clone(), dirty);
if dirty && task.is_command() {
Ok(Box::new(CommandTask::new(
key,
task.command().unwrap().clone(),
)))
} else {
Ok(Box::new(NoopTask::default()))
}
}
}
#[cfg(test)]
mod test {
use insta::assert_display_snapshot;
use std::{
io::{Error, ErrorKind, Result},
path::Path,
time::{Duration, SystemTime, UNIX_EPOCH},
};
use super::*;
use crate::task::*;
macro_rules! mocked_rebuilder {
($path:ident, $body:expr) => {{
struct MockDiskInterface {}
impl DiskInterface for MockDiskInterface {
fn modified<P: AsRef<Path>>(&self, $path: P) -> Result<SystemTime> {
$body
}
}
let mock_disk = MockDiskInterface {};
let state = $crate::DiskDirtyCache::new(mock_disk);
$crate::CachingMTimeRebuilder::new(state)
}};
($body:expr) => {
mocked_rebuilder! {_unused, $body}
};
}
#[test]
fn test_basic() {
let rebuilder = mocked_rebuilder! {p,
if p.as_ref() == Path::new("foo.c") {
Ok(UNIX_EPOCH.checked_add(Duration::from_secs(100)).unwrap())
} else {
Err(Error::new(ErrorKind::NotFound, "mock not found"))
}
};
let task = Task {
dependencies: vec![Key::Path(b"foo.c".to_vec().into())],
order_dependencies: vec![],
variant: TaskVariant::Command("cc -c foo.c".to_owned()),
};
let task = rebuilder
.build(Key::Path(b"foo.o".to_vec().into()), None, &task)
.expect("valid task");
assert!(task.is_command());
}
#[test]
fn test_input_does_not_exist() {
let rebuilder = mocked_rebuilder! {
Err(Error::new(ErrorKind::NotFound, "mock not found"))
};
let task = rebuilder.build(
Key::Path(b"phony_user".to_vec().into()),
None,
&Task {
dependencies: vec![Key::Path(
b"phony_target_that_does_not_exist".to_vec().into(),
)],
order_dependencies: vec![],
variant: TaskVariant::Retrieve,
},
);
assert!(task.is_err());
match task {
Err(e) => {
assert_display_snapshot!(e);
}
_ => assert!(false, "Expected error"),
}
let task = rebuilder.build(
Key::Path(b"phony_user".to_vec().into()),
None,
&Task {
dependencies: vec![Key::Path(
b"phony_target_that_does_not_exist".to_vec().into(),
)],
order_dependencies: vec![],
variant: TaskVariant::Command("whatever".to_string()),
},
);
assert!(task.is_err());
match task {
Err(e) => {
assert_display_snapshot!(e);
}
_ => assert!(false, "Expected error"),
}
}
#[test]
fn test_input_does_not_exist_multiple_out_error_message() {
let rebuilder = mocked_rebuilder! {
Err(Error::new(ErrorKind::NotFound, "mock not found"))
};
let task = Task {
dependencies: vec![Key::Path(
b"phony_target_that_does_not_exist".to_vec().into(),
)],
order_dependencies: vec![],
variant: TaskVariant::Retrieve,
};
let task = rebuilder.build(
Key::Multi(
vec![
b"phony_user".to_vec().into(),
b"phony_user2".to_vec().into(),
]
.into(),
),
None,
&task,
);
assert!(task.is_err());
match task {
Err(e) => {
assert_display_snapshot!(e);
}
_ => assert!(false, "Expected error"),
}
}
#[test]
fn test_phony_input() {
let rebuilder = mocked_rebuilder! {
Err(Error::new(ErrorKind::NotFound, "mock not found"))
};
let task = rebuilder.build(
Key::Path(b"phony_target_that_does_not_exist".to_vec().into()),
None,
&Task {
dependencies: vec![],
order_dependencies: vec![],
variant: TaskVariant::Retrieve,
},
);
assert!(task.is_ok());
let task = rebuilder.build(
Key::Path(b"phony_user".to_vec().into()),
None,
&Task {
dependencies: vec![Key::Path(
b"phony_target_that_does_not_exist".to_vec().into(),
)],
order_dependencies: vec![],
variant: TaskVariant::Retrieve,
},
);
assert!(task.is_ok());
}
#[test]
fn test_older_input() {
let _rebuilder = mocked_rebuilder! {
Err(Error::new(ErrorKind::NotFound, "mock not found"))
};
}
#[test]
fn test_clean_chain() {
let rebuilder = mocked_rebuilder! {p,
if p.as_ref() == Path::new("foo.c") {
Ok(UNIX_EPOCH.checked_add(Duration::from_secs(100)).unwrap())
} else if p.as_ref() == Path::new("foo.o") {
Ok(UNIX_EPOCH.checked_add(Duration::from_secs(1000)).unwrap())
} else if p.as_ref() == Path::new("foo") {
Ok(UNIX_EPOCH.checked_add(Duration::from_secs(500)).unwrap())
} else {
Err(Error::new(ErrorKind::NotFound, "mock not found"))
}
};
let cc_task = Task {
dependencies: vec![Key::Path(b"foo.c".to_vec().into())],
order_dependencies: vec![],
variant: TaskVariant::Command("cc -c foo.c".to_owned()),
};
let link_task = Task {
dependencies: vec![Key::Path(b"foo.o".to_vec().into())],
order_dependencies: vec![],
variant: TaskVariant::Command("cc -o foo foo.o".to_owned()),
};
let task = rebuilder
.build(Key::Path(b"foo.o".to_vec().into()), None, &cc_task)
.expect("valid task");
assert!(!task.is_command(), "foo.o newer than foo.c");
let task = rebuilder
.build(Key::Path(b"foo".to_vec().into()), None, &link_task)
.expect("valid task");
assert!(task.is_command());
}
#[test]
fn test_order_dependencies_newer() {
}
}