use crate::compile::pkg_prefix_for_src_root;
use anyhow::{Context, Result};
use std::io::Write as _;
use std::path::{Path, PathBuf};
use walkdir::WalkDir;
use zip::write::SimpleFileOptions;
use zip::ZipWriter;
fn epoch() -> zip::DateTime {
zip::DateTime::from_date_and_time(2024, 1, 1, 0, 0, 0).expect("epoch constant is valid")
}
pub fn write_sources_jar(
jar_path: &Path,
src_roots: &[PathBuf],
resources_dir: Option<&Path>,
) -> Result<()> {
let file = std::fs::File::create(jar_path)
.with_context(|| format!("cannot create {}", jar_path.display()))?;
let mut zip = ZipWriter::new(file);
let file_opts = SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Deflated)
.last_modified_time(epoch())
.unix_permissions(0o644);
let dir_opts = SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Stored)
.last_modified_time(epoch())
.unix_permissions(0o755);
zip.start_file("META-INF/", dir_opts)
.context("failed to write META-INF/ entry")?;
zip.start_file("META-INF/MANIFEST.MF", file_opts)
.context("failed to start MANIFEST.MF entry")?;
zip.write_all(b"Manifest-Version: 1.0\r\n\r\n")
.context("failed to write MANIFEST.MF")?;
let mut entries: std::collections::BTreeMap<String, PathBuf> = std::collections::BTreeMap::new();
for root in src_roots {
let pkg_prefix_dotted = pkg_prefix_for_src_root(root);
let pkg_prefix_slashed = pkg_prefix_dotted.replace('.', "/");
for entry in WalkDir::new(root).into_iter().filter_map(|e| e.ok()) {
if !entry.file_type().is_file() {
continue;
}
let fs_path = entry.path().to_path_buf();
let is_source = matches!(
fs_path.extension().and_then(|s| s.to_str()),
Some("java") | Some("kt") | Some("groovy")
);
if !is_source {
continue;
}
let rel = match fs_path.strip_prefix(root) {
Ok(r) => r.to_string_lossy().replace('\\', "/"),
Err(_) => continue,
};
let entry_path = if pkg_prefix_slashed.is_empty() {
rel
} else {
format!("{}/{}", pkg_prefix_slashed, rel)
};
entries.insert(entry_path, fs_path);
}
}
if let Some(res_root) = resources_dir {
for entry in WalkDir::new(res_root).into_iter().filter_map(|e| e.ok()) {
if !entry.file_type().is_file() {
continue;
}
let fs_path = entry.path().to_path_buf();
if let Ok(rel) = fs_path.strip_prefix(res_root) {
let entry_path = rel.to_string_lossy().replace('\\', "/");
entries.entry(entry_path).or_insert(fs_path);
}
}
}
let mut dirs: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
for entry_path in entries.keys() {
let parts: Vec<&str> = entry_path.split('/').collect();
for i in 1..parts.len() {
dirs.insert(format!("{}/", parts[..i].join("/")));
}
}
for d in &dirs {
if d == "META-INF/" {
continue;
}
zip.start_file(d, dir_opts)
.with_context(|| format!("failed to write dir entry {d}"))?;
}
for (entry_path, fs_path) in &entries {
let bytes = std::fs::read(fs_path)
.with_context(|| format!("failed to read source {}", fs_path.display()))?;
zip.start_file(entry_path.as_str(), file_opts)
.with_context(|| format!("failed to start jar entry {entry_path}"))?;
zip.write_all(&bytes)
.with_context(|| format!("failed to write jar entry {entry_path}"))?;
}
zip.finish().context("failed to finalize sources jar")?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Read;
fn list_jar_entries(jar_path: &Path) -> Vec<String> {
let f = std::fs::File::open(jar_path).unwrap();
let mut zip = zip::ZipArchive::new(f).unwrap();
let mut names = Vec::new();
for i in 0..zip.len() {
let entry = zip.by_index(i).unwrap();
if !entry.is_dir() {
names.push(entry.name().to_string());
}
}
names.sort();
names
}
fn read_jar_entry(jar_path: &Path, name: &str) -> Option<Vec<u8>> {
let f = std::fs::File::open(jar_path).unwrap();
let mut zip = zip::ZipArchive::new(f).unwrap();
let mut e = zip.by_name(name).ok()?;
let mut buf = Vec::new();
e.read_to_end(&mut buf).unwrap();
Some(buf)
}
#[test]
fn sources_jar_includes_maven_layout_java() {
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("src").join("main").join("java");
std::fs::create_dir_all(src.join("com").join("foo")).unwrap();
std::fs::write(src.join("com").join("foo").join("Bar.java"), b"package com.foo; class Bar {}").unwrap();
let jar = dir.path().join("out.jar");
write_sources_jar(&jar, &[src], None).unwrap();
let entries = list_jar_entries(&jar);
assert!(entries.contains(&"com/foo/Bar.java".to_string()), "got: {entries:?}");
}
#[test]
fn sources_jar_includes_flat_package_files() {
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("src").join("com.example.foo");
std::fs::create_dir_all(&src).unwrap();
std::fs::write(src.join("Hello.java"), b"package com.example.foo; class Hello {}").unwrap();
let jar = dir.path().join("out.jar");
write_sources_jar(&jar, &[src], None).unwrap();
let entries = list_jar_entries(&jar);
assert!(
entries.contains(&"com/example/foo/Hello.java".to_string()),
"flat-package file must land at com/example/foo/Hello.java; got: {entries:?}",
);
}
#[test]
fn sources_jar_includes_kotlin_sources() {
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("src").join("main").join("kotlin");
std::fs::create_dir_all(src.join("com").join("foo")).unwrap();
std::fs::write(src.join("com").join("foo").join("Baz.kt"), b"package com.foo; fun main() {}").unwrap();
let jar = dir.path().join("out.jar");
write_sources_jar(&jar, &[src], None).unwrap();
let entries = list_jar_entries(&jar);
assert!(entries.contains(&"com/foo/Baz.kt".to_string()), "got: {entries:?}");
}
#[test]
fn sources_jar_includes_resources() {
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("src").join("main").join("java");
std::fs::create_dir_all(src.join("p")).unwrap();
std::fs::write(src.join("p").join("X.java"), b"package p; class X {}").unwrap();
let res = dir.path().join("src").join("main").join("resources");
std::fs::create_dir_all(&res).unwrap();
std::fs::write(res.join("config.properties"), b"k=v\n").unwrap();
let jar = dir.path().join("out.jar");
write_sources_jar(&jar, &[src], Some(&res)).unwrap();
let entries = list_jar_entries(&jar);
assert!(entries.contains(&"config.properties".to_string()), "got: {entries:?}");
}
#[test]
fn sources_jar_includes_groovy_sources() {
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("src").join("com.example");
std::fs::create_dir_all(&src).unwrap();
std::fs::write(src.join("Greeter.groovy"), b"package com.example; class Greeter {}").unwrap();
std::fs::write(src.join("Helper.java"), b"package com.example; class Helper {}").unwrap();
let jar = dir.path().join("out.jar");
write_sources_jar(&jar, &[src], None).unwrap();
let entries = list_jar_entries(&jar);
assert!(
entries.contains(&"com/example/Greeter.groovy".to_string()),
"Groovy source must land at com/example/Greeter.groovy; got: {entries:?}",
);
assert!(
entries.contains(&"com/example/Helper.java".to_string()),
"Java source must also be present; got: {entries:?}",
);
}
#[test]
fn sources_jar_is_deterministic() {
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("src").join("main").join("java");
std::fs::create_dir_all(src.join("p")).unwrap();
std::fs::write(src.join("p").join("X.java"), b"package p; class X {}").unwrap();
std::fs::write(src.join("p").join("Y.java"), b"package p; class Y {}").unwrap();
let a = dir.path().join("a.jar");
let b = dir.path().join("b.jar");
write_sources_jar(&a, &[src.clone()], None).unwrap();
write_sources_jar(&b, &[src], None).unwrap();
let bytes_a = std::fs::read(&a).unwrap();
let bytes_b = std::fs::read(&b).unwrap();
assert_eq!(bytes_a, bytes_b, "two runs must produce byte-identical jars");
}
#[test]
fn sources_jar_has_manifest() {
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("src").join("main").join("java");
std::fs::create_dir_all(&src).unwrap();
std::fs::write(src.join("Empty.java"), b"class Empty {}").unwrap();
let jar = dir.path().join("out.jar");
write_sources_jar(&jar, &[src], None).unwrap();
let manifest = read_jar_entry(&jar, "META-INF/MANIFEST.MF").unwrap();
assert!(String::from_utf8_lossy(&manifest).contains("Manifest-Version: 1.0"));
}
}