1use std::io::Write;
7use std::path::{Path, PathBuf};
8use std::process::Command;
9
10use yog_data::Diagnostic;
11use yog_ids::script_problem;
12
13const SERVICES_DIR: &str = "META-INF/services";
15const DOT: char = '.';
17const SLASH: char = '/';
19
20pub struct JvmBuild<'a> {
24 pub javac: &'a Path,
25 pub release: u32,
26 pub classpath: &'a [PathBuf],
27 pub package: &'a str,
28 pub work: &'a Path,
29 pub to_server: bool,
31}
32
33pub fn build_jar(
35 sources: &[(String, String)],
36 b: &JvmBuild<'_>,
37 jar: &Path,
38) -> Result<(), Vec<Diagnostic>> {
39 let src = b.work.join("java");
40 let classes = b.work.join("classes");
41 let fail = |e: std::io::Error| {
42 vec![Diagnostic::error(
43 script_problem::NOT_COMPILED,
44 e.to_string(),
45 )]
46 };
47 let _ = std::fs::remove_dir_all(b.work);
48 std::fs::create_dir_all(&classes).map_err(fail)?;
49 let mut files = Vec::new();
50 let mut mods = Vec::new();
51 for (name, text) in sources {
52 let env = yog_script_analyze::Env {
53 to_server: b.to_server,
54 ..Default::default()
55 };
56 let checked = yog_script_analyze::check_in(name, text, env);
57 if checked.problems.iter().any(Diagnostic::is_error) {
58 return Err(checked.problems);
59 }
60 let class = yog_script_java::compile(name, text, &checked, b.package)?;
61 let dir = b
63 .package
64 .split(DOT)
65 .fold(src.clone(), |path, part| path.join(part));
66 std::fs::create_dir_all(&dir).map_err(fail)?;
67 let file = dir.join(format!("{}.java", class.name));
68 std::fs::write(&file, &class.source).map_err(fail)?;
69 files.push(file);
70 mods.extend(class.mods);
71 }
72 javac(b, &files, &classes)?;
73 write_jar(&classes, &mods, jar).map_err(fail)
74}
75
76fn javac(b: &JvmBuild<'_>, files: &[PathBuf], classes: &Path) -> Result<(), Vec<Diagnostic>> {
78 let classpath = std::env::join_paths(b.classpath).map_err(|e| {
79 vec![Diagnostic::error(
80 script_problem::NOT_COMPILED,
81 e.to_string(),
82 )]
83 })?;
84 let out = Command::new(b.javac)
85 .arg("--release")
86 .arg(b.release.to_string())
87 .arg("-encoding")
88 .arg("UTF-8")
89 .arg("-d")
90 .arg(classes)
91 .arg("-cp")
92 .arg(classpath)
93 .args(files)
94 .output()
95 .map_err(|e| {
96 vec![Diagnostic::error(
97 script_problem::NOT_COMPILED,
98 format!("javac: {e}"),
99 )]
100 })?;
101 if out.status.success() {
102 return Ok(());
103 }
104 let said = String::from_utf8_lossy(&out.stderr).to_string();
105 Err(vec![Diagnostic::error(
106 script_problem::NOT_COMPILED,
107 format!("javac rejected the compiled code (a compiler bug):\n{said}"),
108 )])
109}
110
111fn write_jar(classes: &Path, mods: &[String], jar: &Path) -> std::io::Result<()> {
113 let mut zip = zip::ZipWriter::new(std::fs::File::create(jar)?);
114 let options = zip::write::SimpleFileOptions::default();
115 for file in walk(classes)? {
116 let name = file
117 .strip_prefix(classes)
118 .map_err(std::io::Error::other)?
119 .components()
120 .map(|c| c.as_os_str().to_string_lossy().into_owned())
121 .collect::<Vec<_>>()
122 .join(&SLASH.to_string());
123 zip.start_file(name, options)?;
124 zip.write_all(&std::fs::read(&file)?)?;
125 }
126 if !mods.is_empty() {
127 zip.start_file(
128 format!("{SERVICES_DIR}{SLASH}{}", yog_script_java::MOD_INTERFACE),
129 options,
130 )?;
131 zip.write_all(mods.join("\n").as_bytes())?;
132 }
133 zip.finish()?;
134 Ok(())
135}
136
137fn walk(dir: &Path) -> std::io::Result<Vec<PathBuf>> {
139 let mut out = Vec::new();
140 for entry in std::fs::read_dir(dir)? {
141 let path = entry?.path();
142 if path.is_dir() {
143 out.extend(walk(&path)?);
144 } else {
145 out.push(path);
146 }
147 }
148 Ok(out)
149}