⭐️ A friendly language for building type-safe, scalable systems!
0

Configure Feed

Select the types of activity you want to include in your feed.

gleam / compiler-cli / src / beam_compiler.rs
6.2 kB 195 lines
1// SPDX-License-Identifier: Apache-2.0 2// SPDX-FileCopyrightText: 2024 The Gleam contributors 3 4use gleam_core::{ 5 Result, 6 error::{Error, ShellCommandFailureReason}, 7 io::{FileSystemWriter, Stdio}, 8 paths, 9}; 10 11use crate::fs::get_os; 12 13use std::{ 14 collections::HashSet, 15 io::{self, BufRead, BufReader, Write}, 16 process::{Child, ChildStdin, ChildStdout}, 17}; 18 19use camino::{Utf8Path, Utf8PathBuf}; 20use itertools::Itertools; 21 22#[derive(Debug)] 23pub struct BeamCompilerInstance { 24 process: Child, 25 stdin: Option<ChildStdin>, 26 stdout: BufReader<ChildStdout>, 27 // A guard held for cleaning up the temporary file used to start the BEAM instance. 28 _source: tempfile::NamedTempFile, 29} 30 31impl BeamCompilerInstance { 32 pub fn compile( 33 &mut self, 34 out: &Utf8Path, 35 lib: &Utf8Path, 36 modules: &HashSet<Utf8PathBuf>, 37 stdio: Stdio, 38 ) -> Result<Vec<String>, Error> { 39 // Check that the BEAM instance is still alive before attempting to use it. 40 let exit_status = self 41 .process 42 .try_wait() 43 .expect("access BEAM instance exit state"); 44 if let Some(status) = exit_status { 45 panic!("BEAM compiler instance exited: {status}"); 46 } 47 48 // Prepare work to send to the BEAM instance. 49 let args = format!( 50 "{{\"{}\", \"{}\", [\"{}\"]}}", 51 escape_path(lib), 52 escape_path(out.join("ebin")), 53 modules 54 .iter() 55 .map(|module| escape_path(out.join(paths::ARTEFACT_DIRECTORY_NAME).join(module))) 56 .join("\", \"") 57 ); 58 59 tracing::debug!(args=?args, "call_beam_compiler"); 60 61 writeln!(self.stdin.as_ref().expect("stdin present"), "{args}.").map_err(|e| { 62 Error::ShellCommand { 63 program: "escript".into(), 64 reason: ShellCommandFailureReason::IoError(e.kind()), 65 } 66 })?; 67 68 let mut buf = String::new(); 69 let mut accumulated_modules: Vec<String> = Vec::new(); 70 while let (Ok(_), Ok(None)) = (self.stdout.read_line(&mut buf), self.process.try_wait()) { 71 match buf.trim() { 72 "gleam-compile-result-ok" => { 73 // Return Ok with the accumulated modules 74 return Ok(accumulated_modules); 75 } 76 "gleam-compile-result-error" => { 77 return Err(Error::ShellCommand { 78 program: "escript".into(), 79 reason: ShellCommandFailureReason::Unknown, 80 }); 81 } 82 s if s.starts_with("gleam-compile-module:") => { 83 if let Some(module_content) = s.strip_prefix("gleam-compile-module:") { 84 accumulated_modules.push(module_content.to_string()); 85 } 86 } 87 _ => match stdio { 88 Stdio::Inherit => print!("{buf}"), 89 Stdio::Null => {} 90 }, 91 } 92 93 buf.clear(); 94 } 95 96 // if we get here, stdout got closed before we got an "ok" or "err". 97 Err(Error::ShellCommand { 98 program: "escript".into(), 99 reason: ShellCommandFailureReason::Unknown, 100 }) 101 } 102 103 pub fn new<IO: FileSystemWriter>(io: &IO) -> Result<Self, Error> { 104 let escript_source = std::include_str!("../templates/gleam@@compile.erl"); 105 let escript_file = 106 tempfile::NamedTempFile::new().map_err(|e| Error::CouldNotCreateTempFile { 107 error: e.to_string(), 108 })?; 109 let escript_path = 110 Utf8PathBuf::from_path_buf(escript_file.path().to_path_buf()).expect("UTF8 temp"); 111 112 io.write(&escript_path, escript_source)?; 113 114 tracing::trace!(escript_path=?escript_path, "spawn_beam_compiler"); 115 116 let mut process = std::process::Command::new("escript") 117 .arg(escript_path) 118 .stdin(std::process::Stdio::piped()) 119 .stdout(std::process::Stdio::piped()) 120 .spawn() 121 .map_err(|e| match e.kind() { 122 io::ErrorKind::NotFound => Error::ShellProgramNotFound { 123 program: "escript".into(), 124 os: get_os(), 125 }, 126 other => Error::ShellCommand { 127 program: "escript".into(), 128 reason: ShellCommandFailureReason::IoError(other), 129 }, 130 })?; 131 132 let stdin = process.stdin.take().expect("could not get child stdin"); 133 let stdout = process.stdout.take().expect("could not get child stdout"); 134 135 Ok(Self { 136 process, 137 stdin: Some(stdin), 138 stdout: BufReader::new(stdout), 139 _source: escript_file, 140 }) 141 } 142} 143 144impl Drop for BeamCompilerInstance { 145 fn drop(&mut self) { 146 // closing stdin will cause the BEAM instance to exit. 147 drop(self.stdin.take()); 148 let _ = self.process.wait(); 149 } 150} 151 152fn escape_path<T: AsRef<str>>(path: T) -> String { 153 path.as_ref().replace('\\', "\\\\").replace('"', "\\\"") 154} 155 156#[cfg(test)] 157mod tests { 158 use super::escape_path; 159 160 #[test] 161 fn escape_path_plain() { 162 assert_eq!( 163 escape_path("/build/packages/foo/src/bar.erl"), 164 "/build/packages/foo/src/bar.erl" 165 ); 166 } 167 168 #[test] 169 fn escape_path_backslash() { 170 assert_eq!( 171 escape_path("C:\\Users\\foo\\bar.erl"), 172 "C:\\\\Users\\\\foo\\\\bar.erl" 173 ); 174 } 175 176 #[test] 177 fn escape_path_double_quote() { 178 assert_eq!( 179 escape_path(r#"a", "/tmp/other.erl"#), 180 r#"a\", \"/tmp/other.erl"# 181 ); 182 } 183 184 #[test] 185 fn escape_path_double_quote_does_not_break_erlang_string() { 186 // A double-quote in a path must not close the surrounding Erlang string 187 // literal and produce extra elements in the module list. 188 let path = r#"a", "/tmp/other"#; 189 let escaped = escape_path(path); 190 assert!( 191 !escaped.contains("\", \""), 192 "unescaped quote still present: {escaped}" 193 ); 194 } 195}