Fork of daniellemaywood.uk/gleam — Wasm codegen work
2

Configure Feed

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

gleam / compiler-core / src / build / native_file_copier.rs
9.7 kB 270 lines
1// SPDX-License-Identifier: Apache-2.0 2// SPDX-FileCopyrightText: 2022 The Gleam contributors 3 4#[cfg(test)] 5mod tests; 6 7use std::collections::{HashMap, HashSet}; 8 9use camino::{Utf8Path, Utf8PathBuf}; 10use ecow::{EcoString, eco_format}; 11 12use crate::{ 13 Error, Result, 14 io::{DirWalker, FileSystemReader, FileSystemWriter}, 15 paths::ProjectPaths, 16}; 17 18use super::package_compiler::CheckModuleConflicts; 19 20#[derive(Debug, Clone, PartialEq, Eq)] 21pub(crate) struct CopiedNativeFiles { 22 pub any_elixir: bool, 23 pub to_compile: Vec<Utf8PathBuf>, 24} 25 26pub(crate) struct NativeFileCopier<'a, IO> { 27 io: IO, 28 paths: ProjectPaths, 29 destination_dir: &'a Utf8Path, 30 seen_native_files: HashSet<Utf8PathBuf>, 31 seen_modules: HashMap<EcoString, Utf8PathBuf>, 32 to_compile: Vec<Utf8PathBuf>, 33 elixir_files_copied: bool, 34 check_module_conflicts: CheckModuleConflicts, 35} 36 37impl<'a, IO> NativeFileCopier<'a, IO> 38where 39 IO: FileSystemReader + FileSystemWriter + Clone, 40{ 41 pub(crate) fn new( 42 io: IO, 43 root: &'a Utf8Path, 44 out: &'a Utf8Path, 45 check_module_conflicts: CheckModuleConflicts, 46 ) -> Self { 47 Self { 48 io, 49 paths: ProjectPaths::new(root.into()), 50 destination_dir: out, 51 to_compile: Vec::new(), 52 seen_native_files: HashSet::new(), 53 seen_modules: HashMap::new(), 54 elixir_files_copied: false, 55 check_module_conflicts, 56 } 57 } 58 59 /// Copy native files from the given directory to the build directory. 60 /// 61 /// Errors if any duplicate files are found. 62 /// 63 /// Returns a list of files that need to be compiled (Elixir and Erlang). 64 /// 65 pub fn run(mut self) -> Result<CopiedNativeFiles> { 66 self.io.mkdir(&self.destination_dir)?; 67 68 let src = self.paths.src_directory(); 69 self.copy_files(&src)?; 70 71 let test = self.paths.test_directory(); 72 if self.io.is_directory(&test) { 73 self.copy_files(&test)?; 74 } 75 76 let dev = self.paths.dev_directory(); 77 if self.io.is_directory(&dev) { 78 self.copy_files(&dev)?; 79 } 80 81 // Sort for deterministic output 82 self.to_compile.sort_unstable(); 83 84 Ok(CopiedNativeFiles { 85 to_compile: self.to_compile, 86 any_elixir: self.elixir_files_copied, 87 }) 88 } 89 90 fn copy_files(&mut self, src_root: &Utf8Path) -> Result<()> { 91 let mut dir_walker = DirWalker::new(src_root.to_path_buf()); 92 while let Some(path) = dir_walker.next_file(&self.io)? { 93 self.copy(path, &src_root)?; 94 } 95 Ok(()) 96 } 97 98 fn copy(&mut self, file: Utf8PathBuf, src_root: &Utf8Path) -> Result<()> { 99 let extension = file.extension().unwrap_or_default(); 100 101 let relative_path = file 102 .strip_prefix(src_root) 103 .expect("copy_native_files strip prefix") 104 .to_path_buf(); 105 106 // No need to run duplicate native file checks for .gleam files, but we 107 // still need to check for conflicting `.gleam` and `.mjs` files, so we 108 // add a special case for `.gleam`. 109 if extension == "gleam" { 110 self.check_for_conflicting_javascript_modules(&relative_path)?; 111 self.check_for_conflicting_erlang_modules(&relative_path)?; 112 113 return Ok(()); 114 } 115 116 // Skip unknown file formats that are not supported native files 117 if !crate::io::is_native_file_extension(extension) { 118 return Ok(()); 119 } 120 121 let destination = self.destination_dir.join(&relative_path); 122 123 // Check that this native file was not already copied 124 self.check_for_duplicate(&relative_path)?; 125 126 // Check for JavaScript modules conflicting between each other within 127 // the same relative path. We need to do this as '.gleam' files can 128 // also cause a conflict, despite not being native files, as they are 129 // compiled to `.mjs`. 130 self.check_for_conflicting_javascript_modules(&relative_path)?; 131 132 // Check for Erlang modules conflicting between each other anywhere in 133 // the tree. 134 self.check_for_conflicting_erlang_modules(&relative_path)?; 135 136 // If the source file's mtime is older than the destination file's mtime 137 // then it has not changed and as such does not need to be copied. 138 // 139 // This makes no practical difference for JavaScript etc files, but for 140 // Erlang and Elixir files it mean we can skip compiling them. 141 if self.io.is_file(&destination) 142 && self.io.modification_time(&file)? <= self.io.modification_time(&destination)? 143 { 144 tracing::debug!(?file, "skipping_unchanged_native_file_unchanged"); 145 return Ok(()); 146 } 147 148 tracing::debug!(?file, "copying_native_file"); 149 150 // Ensure destination exists (subdir might not exist yet in the output) 151 if let Some(parent) = destination.parent() { 152 self.io.mkdir(parent)?; 153 } 154 155 self.io.copy(&file, &destination)?; 156 self.elixir_files_copied = self.elixir_files_copied || extension == "ex"; 157 158 // BEAM native modules need to be compiled 159 if matches!(extension, "erl" | "ex") { 160 _ = self.to_compile.push(relative_path.clone()); 161 } 162 163 Ok(()) 164 } 165 166 fn check_for_duplicate(&mut self, relative_path: &Utf8PathBuf) -> Result<(), Error> { 167 if !self.seen_native_files.insert(relative_path.clone()) { 168 return Err(Error::DuplicateSourceFile { 169 file: relative_path.to_string(), 170 }); 171 } 172 Ok(()) 173 } 174 175 /// Gleam files are compiled to `.mjs` files, which must not conflict with 176 /// an FFI `.mjs` file with the same name, so we check for this case here. 177 fn check_for_conflicting_javascript_modules( 178 &mut self, 179 relative_path: &Utf8PathBuf, 180 ) -> Result<(), Error> { 181 let mjs_path = match relative_path.extension() { 182 Some("gleam") => eco_format!("{}", relative_path.with_extension("mjs")), 183 Some("mjs") => eco_format!("{}", relative_path), 184 _ => return Ok(()), 185 }; 186 187 // Insert the full relative `.mjs` path in `seen_modules` as there is 188 // no conflict if two `.mjs` files have the same name but are in 189 // different subpaths, unlike Erlang files. 190 let existing = self 191 .seen_modules 192 .insert(mjs_path.clone(), relative_path.clone()); 193 194 // If there was no already existing one then there's no problem. 195 let Some(existing) = existing else { 196 return Ok(()); 197 }; 198 199 let existing_is_gleam = existing.extension() == Some("gleam"); 200 if existing_is_gleam || relative_path.extension() == Some("gleam") { 201 let (gleam_file, native_file) = if existing_is_gleam { 202 (&existing, relative_path) 203 } else { 204 (relative_path, &existing) 205 }; 206 return Err(Error::ClashingGleamModuleAndNativeFileName { 207 module: eco_format!("{}", gleam_file.with_extension("")), 208 gleam_file: gleam_file.clone(), 209 native_file: native_file.clone(), 210 }); 211 } 212 213 // The only way for two `.mjs` files to clash is by having 214 // the exact same path. 215 assert_eq!(&existing, relative_path); 216 return Err(Error::DuplicateSourceFile { 217 file: existing.to_string(), 218 }); 219 } 220 221 /// Erlang module files cannot have the same name regardless of their 222 /// relative positions within the project. Ensure we raise an error if the 223 /// user attempts to create `.erl` files with the same name. 224 fn check_for_conflicting_erlang_modules( 225 &mut self, 226 relative_path: &Utf8PathBuf, 227 ) -> Result<(), Error> { 228 let erlang_module_name = match relative_path.extension() { 229 Some("erl") => { 230 eco_format!("{}", relative_path.file_name().expect("path has file name")) 231 } 232 Some("gleam") if self.check_module_conflicts.should_check() => relative_path 233 .with_extension("erl") 234 .as_str() 235 .replace("/", "@") 236 .into(), 237 _ => return Ok(()), 238 }; 239 240 // Insert just the `.erl` module filename in `seen_modules` instead of 241 // its full relative path, because `.erl` files with the same name 242 // cause a conflict when targetting Erlang regardless of subpath. 243 if let Some(existing) = self 244 .seen_modules 245 .insert(erlang_module_name, relative_path.clone()) 246 { 247 let existing_is_gleam = existing.extension() == Some("gleam"); 248 if existing_is_gleam || relative_path.extension() == Some("gleam") { 249 let (gleam_file, native_file) = if existing_is_gleam { 250 (&existing, relative_path) 251 } else { 252 (relative_path, &existing) 253 }; 254 return Err(Error::ClashingGleamModuleAndNativeFileName { 255 module: eco_format!("{}", gleam_file.with_extension("")), 256 gleam_file: gleam_file.clone(), 257 native_file: native_file.clone(), 258 }); 259 } 260 261 return Err(Error::DuplicateNativeErlangModule { 262 module: eco_format!("{}", relative_path.file_stem().expect("path has file stem")), 263 first: existing, 264 second: relative_path.clone(), 265 }); 266 } 267 268 Ok(()) 269 } 270}