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    /// The mod's files its sources include.
32    pub files: Option<&'a dyn yog_script_analyze::macros::ModFiles>,
33    /// The mod's id: its content's namespace.
34    pub mod_id: &'a str,
35    /// The mod's content files, each (kind, id): what its content
36    /// declarations bind to.
37    pub content: &'a [(String, String)],
38}
39
40/// `sources` (name, text) built into the jar `jar`, or what's wrong.
41pub 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    // Every source's declarations first: each source reaches the others'
65    // types, as a C# project's files do.
66    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        // The package's folders, joined as the platform joins them.
91        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
105/// Every source's declarations but the `skip`th's, each with its index.
106fn 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
117/// `javac` on `files`, its classes into `classes`.
118fn 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
152/// The jar: every class under `classes`, and the mods listed.
153fn 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
181/// Every file under `dir`.
182fn 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}