Skip to main content

yog_script_build/
jvm.rs

1//! Yog Script built for the JVM: each source compiled to Java
2//! (`yog-script-java`), `javac` run on them, and a jar made of the classes
3//! with its mods listed for `ServiceLoader` — what a `.yog` carries under
4//! `scripts/` and the hosts load.
5
6use std::io::Write;
7use std::path::{Path, PathBuf};
8use std::process::Command;
9
10use yog_data::Diagnostic;
11use yog_ids::script_problem;
12
13/// Where a jar lists a service's providers.
14const SERVICES_DIR: &str = "META-INF/services";
15/// What separates a package's names.
16const DOT: char = '.';
17/// What separates a jar's folders: `/` whatever the platform (zip's).
18const SLASH: char = '/';
19
20/// What a build for one game version needs: `javac`, the Java the version
21/// runs (its `java_version`), what the code is compiled against (that
22/// version's loader jar), the package its classes go in, a folder to work in.
23pub 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    /// Its mod says it sends to the server (`yog.toml`'s `[network] to_server`).
30    pub to_server: bool,
31}
32
33/// `sources` (name, text) built into the jar `jar`, or what's wrong.
34pub 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        // The package's folders, joined as the platform joins them.
62        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
76/// `javac` on `files`, its classes into `classes`.
77fn 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
111/// The jar: every class under `classes`, and the mods listed.
112fn 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
137/// Every file under `dir`.
138fn 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}