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 pub files: Option<&'a dyn yog_script_analyze::macros::ModFiles>,
33 pub mod_id: &'a str,
35 pub content: &'a [(String, String)],
38}
39
40pub fn build_jar(
42 sources: &[(String, String)],
43 b: &JvmBuild<'_>,
44 jar: &Path,
45) -> Result<(), Vec<Diagnostic>> {
46 let src = b.work.join("java");
47 let classes = b.work.join("classes");
48 let fail = |e: std::io::Error| {
49 vec![Diagnostic::error(
50 script_problem::NOT_COMPILED,
51 e.to_string(),
52 )]
53 };
54 let _ = std::fs::remove_dir_all(b.work);
55 std::fs::create_dir_all(&classes).map_err(fail)?;
56 let mut files = Vec::new();
57 let mut mods = Vec::new();
58 let env = yog_script_analyze::Env {
59 to_server: b.to_server,
60 files: b.files,
61 content: Some(b.content),
62 ..Default::default()
63 };
64 let decls: Vec<Vec<yog_script_analyze::ast::Decl>> = sources
67 .iter()
68 .map(|(name, text)| yog_script_analyze::declarations(name, text, env))
69 .collect();
70 for (i, (name, text)) in sources.iter().enumerate() {
71 let others: Vec<&[yog_script_analyze::ast::Decl]> =
72 project_but(&decls, i).map(|(_, d)| d).collect();
73 let env = yog_script_analyze::Env {
74 others: &others,
75 ..env
76 };
77 let checked = yog_script_analyze::check_in(name, text, env);
78 if checked.problems.iter().any(Diagnostic::is_error) {
79 return Err(checked.problems);
80 }
81 let named: Vec<(&str, &[yog_script_analyze::ast::Decl])> = project_but(&decls, i)
82 .map(|(j, d)| (sources[j].0.as_str(), d))
83 .collect();
84 let target = yog_script_java::Target {
85 package: b.package,
86 mod_id: Some(b.mod_id),
87 others: &named,
88 };
89 let class = yog_script_java::compile_in((name, text), &checked, &target)?;
90 let dir = b
92 .package
93 .split(DOT)
94 .fold(src.clone(), |path, part| path.join(part));
95 std::fs::create_dir_all(&dir).map_err(fail)?;
96 let file = dir.join(format!("{}.java", class.name));
97 std::fs::write(&file, &class.source).map_err(fail)?;
98 files.push(file);
99 mods.extend(class.mods);
100 }
101 javac(b, &files, &classes)?;
102 write_jar(&classes, &mods, jar).map_err(fail)
103}
104
105fn project_but(
107 decls: &[Vec<yog_script_analyze::ast::Decl>],
108 skip: usize,
109) -> impl Iterator<Item = (usize, &[yog_script_analyze::ast::Decl])> {
110 decls
111 .iter()
112 .enumerate()
113 .filter(move |(j, _)| *j != skip)
114 .map(|(j, d)| (j, d.as_slice()))
115}
116
117fn javac(b: &JvmBuild<'_>, files: &[PathBuf], classes: &Path) -> Result<(), Vec<Diagnostic>> {
119 let classpath = std::env::join_paths(b.classpath).map_err(|e| {
120 vec![Diagnostic::error(
121 script_problem::NOT_COMPILED,
122 e.to_string(),
123 )]
124 })?;
125 let out = Command::new(b.javac)
126 .arg("--release")
127 .arg(b.release.to_string())
128 .arg("-encoding")
129 .arg("UTF-8")
130 .arg("-d")
131 .arg(classes)
132 .arg("-cp")
133 .arg(classpath)
134 .args(files)
135 .output()
136 .map_err(|e| {
137 vec![Diagnostic::error(
138 script_problem::NOT_COMPILED,
139 format!("javac: {e}"),
140 )]
141 })?;
142 if out.status.success() {
143 return Ok(());
144 }
145 let said = String::from_utf8_lossy(&out.stderr).to_string();
146 Err(vec![Diagnostic::error(
147 script_problem::NOT_COMPILED,
148 format!("javac rejected the compiled code (a compiler bug):\n{said}"),
149 )])
150}
151
152fn write_jar(classes: &Path, mods: &[String], jar: &Path) -> std::io::Result<()> {
154 let mut zip = zip::ZipWriter::new(std::fs::File::create(jar)?);
155 let options = zip::write::SimpleFileOptions::default();
156 for file in walk(classes)? {
157 let name = file
158 .strip_prefix(classes)
159 .map_err(std::io::Error::other)?
160 .components()
161 .map(|c| c.as_os_str().to_string_lossy().into_owned())
162 .collect::<Vec<_>>()
163 .join(&SLASH.to_string());
164 zip.start_file(name, options)?;
165 zip.write_all(&std::fs::read(&file)?)?;
166 }
167 if !mods.is_empty() {
168 zip.start_file(
169 format!(
170 "{SERVICES_DIR}{SLASH}{}",
171 yog_script_ids::jvm::MOD_INTERFACE
172 ),
173 options,
174 )?;
175 zip.write_all(mods.join("\n").as_bytes())?;
176 }
177 zip.finish()?;
178 Ok(())
179}
180
181fn walk(dir: &Path) -> std::io::Result<Vec<PathBuf>> {
183 let mut out = Vec::new();
184 for entry in std::fs::read_dir(dir)? {
185 let path = entry?.path();
186 if path.is_dir() {
187 out.extend(walk(&path)?);
188 } else {
189 out.push(path);
190 }
191 }
192 Ok(out)
193}