rusty-javac 0.2.2

A Java compiler written in Rust.
Documentation
use crate::compiler::config::CompilerConfig;
use std::fs;
use std::path::{Path, PathBuf};
use std::time::SystemTime;

pub(crate) struct IncrementalBuild {
    newest_input: Option<SystemTime>,
}

impl IncrementalBuild {
    pub(crate) fn from_config(config: &CompilerConfig) -> Result<Option<Self>, Vec<String>> {
        if !config.incremental {
            return Ok(None);
        }

        let mut timestamps = InputTimestamps::default();
        for source_file in &config.source_files {
            timestamps.visit_required_file(Path::new(source_file));
        }
        for entry in classpath_entries(&config.classpath) {
            timestamps.visit_classpath_entry(&entry);
        }

        if timestamps.errors.is_empty() {
            Ok(Some(Self {
                newest_input: timestamps.newest,
            }))
        } else {
            Err(timestamps.errors)
        }
    }

    pub(crate) fn class_is_fresh(&self, class_file: &Path) -> bool {
        let Some(newest_input) = self.newest_input else {
            return false;
        };
        let Ok(metadata) = fs::metadata(class_file) else {
            return false;
        };
        if !metadata.is_file() {
            return false;
        }

        metadata
            .modified()
            .is_ok_and(|class_modified| class_modified >= newest_input)
    }
}

#[derive(Default)]
struct InputTimestamps {
    newest: Option<SystemTime>,
    errors: Vec<String>,
}

impl InputTimestamps {
    fn visit_required_file(&mut self, path: &Path) {
        match fs::metadata(path) {
            Ok(metadata) if metadata.is_file() => self.record_modified(path, &metadata),
            Ok(_) => self
                .errors
                .push(format!("source is not a file: {}", path.display())),
            Err(error) => self.errors.push(format!(
                "failed to read source metadata {}: {}",
                path.display(),
                error
            )),
        }
    }

    fn visit_classpath_entry(&mut self, path: &Path) {
        match fs::metadata(path) {
            Ok(metadata) if metadata.is_dir() => {
                self.record_modified(path, &metadata);
                self.visit_classpath_directory(path);
            }
            Ok(metadata) if metadata.is_file() => self.record_modified(path, &metadata),
            Ok(_) => self.errors.push(format!(
                "classpath entry is not a file or directory: {}",
                path.display()
            )),
            Err(error) => self.errors.push(format!(
                "failed to read classpath metadata {}: {}",
                path.display(),
                error
            )),
        }
    }

    fn visit_classpath_directory(&mut self, directory: &Path) {
        let entries = match fs::read_dir(directory) {
            Ok(entries) => entries,
            Err(error) => {
                self.errors.push(format!(
                    "failed to read classpath directory {}: {}",
                    directory.display(),
                    error
                ));
                return;
            }
        };

        for entry in entries {
            match entry {
                Ok(entry) => self.visit_classpath_entry(&entry.path()),
                Err(error) => self.errors.push(format!(
                    "failed to read entry in {}: {}",
                    directory.display(),
                    error
                )),
            }
        }
    }

    fn record_modified(&mut self, path: &Path, metadata: &fs::Metadata) {
        match metadata.modified() {
            Ok(modified) => {
                self.newest = Some(self.newest.map_or(modified, |newest| newest.max(modified)));
            }
            Err(error) => self.errors.push(format!(
                "failed to read modification time for {}: {}",
                path.display(),
                error
            )),
        }
    }
}

fn classpath_entries(classpath: &[String]) -> Vec<PathBuf> {
    classpath
        .iter()
        .flat_map(|entry| std::env::split_paths(entry))
        .filter(|path| !path.as_os_str().is_empty())
        .collect()
}