forked from
gleam.run/gleam
⭐️ A friendly language for building type-safe, scalable systems!
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}