Fork of daniellemaywood.uk/gleam — Wasm codegen work
4.5 kB
134 lines
1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2023 The Gleam contributors
3
4use crate::{
5 Error,
6 error::ShellCommandFailureReason,
7 io::{Command, CommandExecutor, FileSystemReader, FileSystemWriter, Stdio},
8};
9use camino::Utf8PathBuf;
10
11#[cfg(not(target_os = "windows"))]
12const ELIXIR_EXECUTABLE: &str = "elixir";
13#[cfg(target_os = "windows")]
14const ELIXIR_EXECUTABLE: &str = "elixir.bat";
15
16// These Elixir core libs will be loaded with the current project
17const ELIXIR_LIBS: [&str; 4] = ["eex", "elixir", "logger", "mix"];
18
19pub struct ElixirLibraries<'a, IO> {
20 io: &'a IO,
21 build_dir: &'a Utf8PathBuf,
22 subprocess_stdio: Stdio,
23}
24
25impl<'a, IO> ElixirLibraries<'a, IO> {
26 fn new(io: &'a IO, build_dir: &'a Utf8PathBuf, subprocess_stdio: Stdio) -> Self {
27 Self {
28 io,
29 build_dir,
30 subprocess_stdio,
31 }
32 }
33}
34
35impl<'a, IO> ElixirLibraries<'a, IO>
36where
37 IO: CommandExecutor + FileSystemReader + FileSystemWriter + Clone,
38{
39 pub fn make_available(
40 io: &'a IO,
41 build_dir: &'a Utf8PathBuf,
42 subprocess_stdio: Stdio,
43 ) -> Result<(), Error> {
44 let it = Self::new(io, build_dir, subprocess_stdio);
45 let result = it.run();
46
47 if result.is_err() {
48 it.cleanup();
49 }
50
51 result
52 }
53
54 fn cleanup(&self) {
55 self.io
56 .delete_file(&self.paths_cache_path())
57 .expect("deleting paths cache in cleanup");
58 }
59
60 fn paths_cache_filename(&self) -> &'static str {
61 "gleam_elixir_paths"
62 }
63
64 fn paths_cache_path(&self) -> Utf8PathBuf {
65 self.build_dir.join(self.paths_cache_filename())
66 }
67
68 fn run(&self) -> Result<(), Error> {
69 // The pathfinder is a file in build/{target}/erlang
70 // It contains the full path for each Elixir core lib we need, new-line delimited
71 // The pathfinder saves us from repeatedly loading Elixir to get this info
72 let mut update_links = false;
73 let cache = self.paths_cache_path();
74 if !self.io.is_file(&cache) {
75 // The pathfinder must be written
76 // Any existing core lib links will get updated
77 update_links = true;
78 // TODO: test
79 let env = vec![("TERM".to_string(), "dumb".to_string())];
80 // Prepare the libs for Erlang's code:lib_dir function
81 let elixir_atoms: Vec<String> =
82 ELIXIR_LIBS.iter().map(|lib| format!(":{}", lib)).collect();
83 // Use Elixir to find its core lib paths and write the pathfinder file
84 let args = vec![
85 "--eval".to_string(),
86 format!(
87 ":ok = File.write(~s({}), [{}] |> Stream.map(fn(lib) -> lib |> :code.lib_dir |> Path.expand end) |> Enum.join(~s(\\n)))",
88 self.paths_cache_filename(),
89 elixir_atoms.join(", "),
90 ),
91 ];
92 tracing::debug!("writing_elixir_paths_to_build");
93 let status = self.io.exec(Command {
94 program: ELIXIR_EXECUTABLE.into(),
95 args,
96 env,
97 cwd: Some(self.build_dir.clone()),
98 stdio: self.subprocess_stdio,
99 })?;
100 if status != 0 {
101 return Err(Error::ShellCommand {
102 program: "elixir".into(),
103 reason: ShellCommandFailureReason::Unknown,
104 });
105 }
106 }
107
108 // Each pathfinder line is a system path for an Elixir core library
109 let read_pathfinder = self.io.read(&cache)?;
110 for lib_path in read_pathfinder.split('\n') {
111 let source = Utf8PathBuf::from(lib_path);
112 let name = source.as_path().file_name().expect(&format!(
113 "Unexpanded path in {}",
114 self.paths_cache_filename()
115 ));
116 let dest = self.build_dir.join(name);
117 let ebin = dest.join("ebin");
118 if !update_links || self.io.is_directory(&ebin) {
119 // Either links don't need updating
120 // Or this library is already linked
121 continue;
122 }
123 // TODO: unit test
124 if self.io.is_directory(&dest) {
125 // Delete the existing link
126 self.io.delete_directory(&dest)?;
127 }
128 tracing::debug!("linking_{}_to_build", name,);
129 self.io.symlink_dir(&source, &dest)?;
130 }
131
132 Ok(())
133 }
134}