// SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2023 The Gleam contributors use crate::{ Error, error::ShellCommandFailureReason, io::{Command, CommandExecutor, FileSystemReader, FileSystemWriter, Stdio}, }; use camino::Utf8PathBuf; #[cfg(not(target_os = "windows"))] const ELIXIR_EXECUTABLE: &str = "elixir"; #[cfg(target_os = "windows")] const ELIXIR_EXECUTABLE: &str = "elixir.bat"; // These Elixir core libs will be loaded with the current project const ELIXIR_LIBS: [&str; 4] = ["eex", "elixir", "logger", "mix"]; pub struct ElixirLibraries<'a, IO> { io: &'a IO, build_dir: &'a Utf8PathBuf, subprocess_stdio: Stdio, } impl<'a, IO> ElixirLibraries<'a, IO> { fn new(io: &'a IO, build_dir: &'a Utf8PathBuf, subprocess_stdio: Stdio) -> Self { Self { io, build_dir, subprocess_stdio, } } } impl<'a, IO> ElixirLibraries<'a, IO> where IO: CommandExecutor + FileSystemReader + FileSystemWriter + Clone, { pub fn make_available( io: &'a IO, build_dir: &'a Utf8PathBuf, subprocess_stdio: Stdio, ) -> Result<(), Error> { let it = Self::new(io, build_dir, subprocess_stdio); let result = it.run(); if result.is_err() { it.cleanup(); } result } fn cleanup(&self) { self.io .delete_file(&self.paths_cache_path()) .expect("deleting paths cache in cleanup"); } fn paths_cache_filename(&self) -> &'static str { "gleam_elixir_paths" } fn paths_cache_path(&self) -> Utf8PathBuf { self.build_dir.join(self.paths_cache_filename()) } fn run(&self) -> Result<(), Error> { // The pathfinder is a file in build/{target}/erlang // It contains the full path for each Elixir core lib we need, new-line delimited // The pathfinder saves us from repeatedly loading Elixir to get this info let mut update_links = false; let cache = self.paths_cache_path(); if !self.io.is_file(&cache) { // The pathfinder must be written // Any existing core lib links will get updated update_links = true; // TODO: test let env = vec![("TERM".to_string(), "dumb".to_string())]; // Prepare the libs for Erlang's code:lib_dir function let elixir_atoms: Vec = ELIXIR_LIBS.iter().map(|lib| format!(":{}", lib)).collect(); // Use Elixir to find its core lib paths and write the pathfinder file let args = vec![ "--eval".to_string(), format!( ":ok = File.write(~s({}), [{}] |> Stream.map(fn(lib) -> lib |> :code.lib_dir |> Path.expand end) |> Enum.join(~s(\\n)))", self.paths_cache_filename(), elixir_atoms.join(", "), ), ]; tracing::debug!("writing_elixir_paths_to_build"); let status = self.io.exec(Command { program: ELIXIR_EXECUTABLE.into(), args, env, cwd: Some(self.build_dir.clone()), stdio: self.subprocess_stdio, })?; if status != 0 { return Err(Error::ShellCommand { program: "elixir".into(), reason: ShellCommandFailureReason::Unknown, }); } } // Each pathfinder line is a system path for an Elixir core library let read_pathfinder = self.io.read(&cache)?; for lib_path in read_pathfinder.split('\n') { let source = Utf8PathBuf::from(lib_path); let name = source.as_path().file_name().expect(&format!( "Unexpanded path in {}", self.paths_cache_filename() )); let dest = self.build_dir.join(name); let ebin = dest.join("ebin"); if !update_links || self.io.is_directory(&ebin) { // Either links don't need updating // Or this library is already linked continue; } // TODO: unit test if self.io.is_directory(&dest) { // Delete the existing link self.io.delete_directory(&dest)?; } tracing::debug!("linking_{}_to_build", name,); self.io.symlink_dir(&source, &dest)?; } Ok(()) } }