// SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2024 The Gleam contributors use gleam_core::{ Result, error::{Error, ShellCommandFailureReason}, io::{FileSystemWriter, Stdio}, paths, }; use crate::fs::get_os; use std::{ collections::HashSet, io::{self, BufRead, BufReader, Write}, process::{Child, ChildStdin, ChildStdout}, }; use camino::{Utf8Path, Utf8PathBuf}; use itertools::Itertools; #[derive(Debug)] pub struct BeamCompilerInstance { process: Child, stdin: Option, stdout: BufReader, // A guard held for cleaning up the temporary file used to start the BEAM instance. _source: tempfile::NamedTempFile, } impl BeamCompilerInstance { pub fn compile( &mut self, out: &Utf8Path, lib: &Utf8Path, modules: &HashSet, stdio: Stdio, ) -> Result, Error> { // Check that the BEAM instance is still alive before attempting to use it. let exit_status = self .process .try_wait() .expect("access BEAM instance exit state"); if let Some(status) = exit_status { panic!("BEAM compiler instance exited: {status}"); } // Prepare work to send to the BEAM instance. let args = format!( "{{\"{}\", \"{}\", [\"{}\"]}}", escape_path(lib), escape_path(out.join("ebin")), modules .iter() .map(|module| escape_path(out.join(paths::ARTEFACT_DIRECTORY_NAME).join(module))) .join("\", \"") ); tracing::debug!(args=?args, "call_beam_compiler"); writeln!(self.stdin.as_ref().expect("stdin present"), "{args}.").map_err(|e| { Error::ShellCommand { program: "escript".into(), reason: ShellCommandFailureReason::IoError(e.kind()), } })?; let mut buf = String::new(); let mut accumulated_modules: Vec = Vec::new(); while let (Ok(_), Ok(None)) = (self.stdout.read_line(&mut buf), self.process.try_wait()) { match buf.trim() { "gleam-compile-result-ok" => { // Return Ok with the accumulated modules return Ok(accumulated_modules); } "gleam-compile-result-error" => { return Err(Error::ShellCommand { program: "escript".into(), reason: ShellCommandFailureReason::Unknown, }); } s if s.starts_with("gleam-compile-module:") => { if let Some(module_content) = s.strip_prefix("gleam-compile-module:") { accumulated_modules.push(module_content.to_string()); } } _ => match stdio { Stdio::Inherit => print!("{buf}"), Stdio::Null => {} }, } buf.clear(); } // if we get here, stdout got closed before we got an "ok" or "err". Err(Error::ShellCommand { program: "escript".into(), reason: ShellCommandFailureReason::Unknown, }) } pub fn new(io: &IO) -> Result { let escript_source = std::include_str!("../templates/gleam@@compile.erl"); let escript_file = tempfile::NamedTempFile::new().map_err(|e| Error::CouldNotCreateTempFile { error: e.to_string(), })?; let escript_path = Utf8PathBuf::from_path_buf(escript_file.path().to_path_buf()).expect("UTF8 temp"); io.write(&escript_path, escript_source)?; tracing::trace!(escript_path=?escript_path, "spawn_beam_compiler"); let mut process = std::process::Command::new("escript") .arg(escript_path) .stdin(std::process::Stdio::piped()) .stdout(std::process::Stdio::piped()) .spawn() .map_err(|e| match e.kind() { io::ErrorKind::NotFound => Error::ShellProgramNotFound { program: "escript".into(), os: get_os(), }, other => Error::ShellCommand { program: "escript".into(), reason: ShellCommandFailureReason::IoError(other), }, })?; let stdin = process.stdin.take().expect("could not get child stdin"); let stdout = process.stdout.take().expect("could not get child stdout"); Ok(Self { process, stdin: Some(stdin), stdout: BufReader::new(stdout), _source: escript_file, }) } } impl Drop for BeamCompilerInstance { fn drop(&mut self) { // closing stdin will cause the BEAM instance to exit. drop(self.stdin.take()); let _ = self.process.wait(); } } fn escape_path>(path: T) -> String { path.as_ref().replace('\\', "\\\\").replace('"', "\\\"") } #[cfg(test)] mod tests { use super::escape_path; #[test] fn escape_path_plain() { assert_eq!( escape_path("/build/packages/foo/src/bar.erl"), "/build/packages/foo/src/bar.erl" ); } #[test] fn escape_path_backslash() { assert_eq!( escape_path("C:\\Users\\foo\\bar.erl"), "C:\\\\Users\\\\foo\\\\bar.erl" ); } #[test] fn escape_path_double_quote() { assert_eq!( escape_path(r#"a", "/tmp/other.erl"#), r#"a\", \"/tmp/other.erl"# ); } #[test] fn escape_path_double_quote_does_not_break_erlang_string() { // A double-quote in a path must not close the surrounding Erlang string // literal and produce extra elements in the module list. let path = r#"a", "/tmp/other"#; let escaped = escape_path(path); assert!( !escaped.contains("\", \""), "unescaped quote still present: {escaped}" ); } }