pub mod schema;
use anyhow::{Context, Result};
use schema::{Lockfile, YmConfig};
use std::path::{Path, PathBuf};
pub const CONFIG_FILE: &str = "ym.json";
pub const LOCKFILE_NAME: &str = "ym-lock.json";
pub const CACHE_DIR: &str = ".ym";
pub const OUTPUT_DIR: &str = "out";
pub const CLASSES_DIR: &str = "classes";
pub const TEST_CLASSES_DIR: &str = "test-classes";
pub const SOURCE_DIR: &str = "src";
pub const MAVEN_CACHE_DIR: &str = "maven";
pub const BUILD_CACHE_DIR: &str = "build-cache";
pub const POM_CACHE_DIR: &str = "pom-cache";
pub fn find_config(start: &Path) -> Option<PathBuf> {
let mut dir = start.to_path_buf();
loop {
let config = dir.join(CONFIG_FILE);
if config.exists() {
return Some(config);
}
if !dir.pop() {
return None;
}
}
}
pub fn find_workspace_root(start: &Path) -> Option<PathBuf> {
let mut dir = start.to_path_buf();
let mut last_with_workspaces = None;
loop {
let config_path = dir.join(CONFIG_FILE);
if config_path.exists() {
if let Ok(config) = load_config(&config_path) {
if config.workspaces.is_some() {
last_with_workspaces = Some(dir.clone());
}
}
}
if !dir.pop() {
break;
}
}
last_with_workspaces
}
pub fn load_config(path: &Path) -> Result<YmConfig> {
let content =
std::fs::read_to_string(path).with_context(|| format!("Failed to read {}", path.display()))?;
let config: YmConfig =
serde_json::from_str(&content).with_context(|| format!("Failed to parse {}", path.display()))?;
Ok(config)
}
pub fn save_config(path: &Path, config: &YmConfig) -> Result<()> {
let content = serde_json::to_string_pretty(config)? + "\n";
std::fs::write(path, content)?;
Ok(())
}
pub fn load_lockfile(project: &Path) -> Result<Lockfile> {
let path = lockfile_path(project);
if !path.exists() {
return Ok(Lockfile::default());
}
let content = std::fs::read_to_string(&path)?;
let lock: Lockfile = serde_json::from_str(&content)?;
Ok(lock)
}
pub fn load_lockfile_checked(project: &Path, cfg: &YmConfig) -> Result<Lockfile> {
let mut lock = load_lockfile(project)?;
if is_workspace_child(project) {
return Ok(lock);
}
let current_hash = cfg.dependency_fingerprint();
if lock.config_hash != current_hash {
lock.dependencies.clear();
lock.config_hash = current_hash;
}
Ok(lock)
}
fn is_workspace_child(project: &Path) -> bool {
match find_workspace_root(project) {
Some(ws_root) => ws_root != project,
None => false,
}
}
pub fn save_lockfile(project: &Path, lock: &Lockfile) -> Result<()> {
if is_workspace_child(project) {
return Ok(());
}
let mut lock = lock.clone();
lock.ymc_version = env!("CARGO_PKG_VERSION").to_string();
lock.generated_at = chrono::Utc::now().to_rfc3339();
if lock.version_winner_strategy.is_empty() {
lock.version_winner_strategy = "latest-wins".to_string();
}
if lock.lockfile_version == 0 {
lock.lockfile_version = 1;
}
let path = lockfile_path(project);
if let Some(dir) = path.parent() {
std::fs::create_dir_all(dir)?;
}
let content = serde_json::to_string_pretty(&lock)? + "\n";
if let Ok(existing) = std::fs::read_to_string(&path) {
if let Ok(existing_lock) = serde_json::from_str::<Lockfile>(&existing) {
if existing_lock.config_hash == lock.config_hash
&& existing_lock.dependencies == lock.dependencies
&& existing_lock.lockfile_version == lock.lockfile_version
&& existing_lock.version_winner_strategy == lock.version_winner_strategy
{
return Ok(());
}
}
}
let tmp_path = path.with_extension("json.tmp");
std::fs::write(&tmp_path, &content)?;
std::fs::rename(&tmp_path, &path)?;
Ok(())
}
pub fn load_or_find_config() -> Result<(PathBuf, YmConfig)> {
let cwd = std::env::current_dir()?;
let config_path = find_config(&cwd).context("No ym.json found. Run 'ym init' to create one.")?;
let config = load_config(&config_path)?;
Ok((config_path, config))
}
pub fn project_dir(config_path: &Path) -> PathBuf {
config_path.parent().unwrap().to_path_buf()
}
pub fn output_classes_dir(project: &Path) -> PathBuf {
project.join(OUTPUT_DIR).join(CLASSES_DIR)
}
pub fn output_test_classes_dir(project: &Path) -> PathBuf {
project.join(OUTPUT_DIR).join(TEST_CLASSES_DIR)
}
pub fn source_dir(project: &Path) -> PathBuf {
let maven_src = project.join("src").join("main").join("java");
if maven_src.exists() {
maven_src
} else {
project.join(SOURCE_DIR)
}
}
pub fn source_dir_for(project: &Path, cfg: &YmConfig) -> PathBuf {
if let Some(ref custom) = cfg.source_dir {
project.join(custom)
} else {
source_dir(project)
}
}
pub fn test_dir(project: &Path) -> PathBuf {
let maven_test = project.join("src").join("test").join("java");
if maven_test.exists() {
maven_test
} else {
project.join("test")
}
}
pub fn test_dir_for(project: &Path, cfg: &YmConfig) -> PathBuf {
if let Some(ref custom) = cfg.test_dir {
project.join(custom)
} else {
test_dir(project)
}
}
pub fn cache_dir(project: &Path) -> PathBuf {
let root = find_workspace_root(project).unwrap_or_else(|| project.to_path_buf());
root.join(CACHE_DIR)
}
pub fn maven_cache_dir() -> PathBuf {
dirs::home_dir()
.expect("Cannot determine home directory")
.join(CACHE_DIR)
.join(MAVEN_CACHE_DIR)
}
pub fn pom_cache_dir() -> PathBuf {
dirs::home_dir()
.expect("Cannot determine home directory")
.join(CACHE_DIR)
.join(POM_CACHE_DIR)
}
pub fn lockfile_path(project: &Path) -> PathBuf {
let root = find_workspace_root(project).unwrap_or_else(|| project.to_path_buf());
root.join(LOCKFILE_NAME)
}
pub fn dir_size(path: &Path) -> u64 {
walkdir::WalkDir::new(path)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.file_type().is_file())
.filter_map(|e| e.metadata().ok())
.map(|m| m.len())
.sum()
}
pub fn format_size(bytes: u64) -> String {
if bytes >= 1_073_741_824 {
format!("{:.1} GB", bytes as f64 / 1_073_741_824.0)
} else if bytes >= 1_048_576 {
format!("{:.1} MB", bytes as f64 / 1_048_576.0)
} else if bytes >= 1024 {
format!("{:.1} KB", bytes as f64 / 1024.0)
} else {
format!("{} B", bytes)
}
}
#[cfg(test)]
mod tests {
use super::*;
use schema::ResolvedDependency;
use std::collections::BTreeMap;
use std::fs;
fn write_workspace(root: &Path) {
fs::write(
root.join(CONFIG_FILE),
r#"{"name":"root","groupId":"com.example","workspaces":["child"],"dependencies":{"@google/guava":"33.4.0"},"scopeMapping":{"@google/guava":"com.google.guava:guava"}}"#,
).unwrap();
let child = root.join("child");
fs::create_dir_all(&child).unwrap();
fs::write(
child.join(CONFIG_FILE),
r#"{"name":"child","groupId":"com.example","dependencies":{"@google/guava":{"workspace":true}}}"#,
).unwrap();
}
fn make_lock(hash: &str, deps: &[&str]) -> Lockfile {
let mut lock = Lockfile::default();
lock.config_hash = hash.to_string();
lock.lockfile_version = 1;
lock.version_winner_strategy = "latest-wins".to_string();
let mut map = BTreeMap::new();
for gav in deps {
map.insert(gav.to_string(), ResolvedDependency::default());
}
lock.dependencies = map;
lock
}
#[test]
fn save_lockfile_is_noop_for_workspace_child() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
write_workspace(root);
let good = make_lock("root_hash_aaaa", &["com.google.guava:guava:33.4.0"]);
save_lockfile(root, &good).unwrap();
let before = fs::read_to_string(lockfile_path(root)).unwrap();
let child = root.join("child");
let bad = make_lock("child_hash_xxxx", &[]);
save_lockfile(&child, &bad).unwrap();
let after = fs::read_to_string(lockfile_path(&child)).unwrap();
assert_eq!(
before, after,
"workspace-child save_lockfile must be a no-op; lockfile changed"
);
}
#[test]
fn load_lockfile_checked_keeps_deps_for_workspace_child() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
write_workspace(root);
let good = make_lock("root_hash_aaaa", &["com.google.guava:guava:33.4.0"]);
save_lockfile(root, &good).unwrap();
let child_path = root.join("child");
let child_cfg = load_config(&child_path.join(CONFIG_FILE)).unwrap();
let loaded = load_lockfile_checked(&child_path, &child_cfg).unwrap();
assert_eq!(loaded.config_hash, "root_hash_aaaa");
assert_eq!(loaded.dependencies.len(), 1);
}
#[test]
fn save_lockfile_still_writes_for_workspace_root() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
write_workspace(root);
let lock = make_lock("root_hash_bbbb", &["com.google.guava:guava:33.4.0"]);
save_lockfile(root, &lock).unwrap();
let written: Lockfile = serde_json::from_str(
&fs::read_to_string(lockfile_path(root)).unwrap(),
).unwrap();
assert_eq!(written.config_hash, "root_hash_bbbb");
assert_eq!(written.dependencies.len(), 1);
}
}