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-cli / src / run.rs
12 kB 427 lines
1// SPDX-License-Identifier: Apache-2.0 2// SPDX-FileCopyrightText: 2020 The Gleam contributors 3 4use std::sync::OnceLock; 5 6use camino::Utf8PathBuf; 7use ecow::EcoString; 8use gleam_core::{ 9 analyse::TargetSupport, 10 build::{Built, Codegen, Compile, Mode, NullTelemetry, Options, Runtime, Target, Telemetry}, 11 config::{DenoFlag, PackageConfig}, 12 error::Error, 13 io::{Command, CommandExecutor, Stdio}, 14 paths::ProjectPaths, 15 type_::ModuleFunction, 16 version::COMPILER_VERSION, 17}; 18use regex::Regex; 19 20use crate::{config::PackageKind, fs::ProjectIO}; 21 22#[derive(Debug, Clone, Copy)] 23pub enum Which { 24 Src, 25 Test, 26 Dev, 27} 28 29pub fn command( 30 paths: &ProjectPaths, 31 arguments: Vec<String>, 32 target: Option<Target>, 33 runtime: Option<Runtime>, 34 module: Option<String>, 35 which: Which, 36 no_print_progress: bool, 37) -> Result<(), Error> { 38 // Don't exit on ctrl+c as it is used by child erlang shell 39 ctrlc::set_handler(move || {}).expect("Error setting Ctrl-C handler"); 40 let command = setup( 41 paths, 42 arguments, 43 target, 44 runtime, 45 module, 46 which, 47 no_print_progress, 48 )?; 49 let status = ProjectIO::new().exec(command)?; 50 std::process::exit(status); 51} 52 53pub fn setup( 54 paths: &ProjectPaths, 55 arguments: Vec<String>, 56 target: Option<Target>, 57 runtime: Option<Runtime>, 58 module: Option<String>, 59 which: Which, 60 no_print_progress: bool, 61) -> Result<Command, Error> { 62 // Validate the module path 63 if let Some(mod_path) = &module 64 && !is_gleam_module(mod_path) 65 { 66 return Err(Error::InvalidModuleName { 67 module: mod_path.to_owned(), 68 }); 69 }; 70 71 let telemetry: &'static dyn Telemetry = if no_print_progress { 72 &NullTelemetry 73 } else { 74 &crate::cli::Reporter 75 }; 76 77 // Download dependencies 78 let manifest = if no_print_progress { 79 crate::build::download_dependencies(paths, NullTelemetry)? 80 } else { 81 crate::build::download_dependencies(paths, crate::cli::Reporter::new())? 82 }; 83 84 // Get the config for the module that is being run to check the target. 85 // Also get the kind of the package the module belongs to: wether the module 86 // belongs to a dependency or to the root package. 87 let (mod_config, package_kind) = match &module { 88 Some(mod_path) => { 89 crate::config::find_package_config_for_module(mod_path, &manifest, paths)? 90 } 91 _ => (crate::config::root_config(paths)?, PackageKind::Root), 92 }; 93 94 // The root config is required to run the project. 95 let root_config = crate::config::root_config(paths)?; 96 97 // Determine which module to run 98 let module = module.unwrap_or(match which { 99 Which::Src => root_config.name.to_string(), 100 Which::Test => format!("{}_test", root_config.name), 101 Which::Dev => format!("{}_dev", root_config.name), 102 }); 103 104 let target = target.unwrap_or(mod_config.target); 105 106 let options = Options { 107 warnings_as_errors: false, 108 compile: match package_kind { 109 // If we're trying to run a dependecy module we do not compile and 110 // check the root package. So we can run the main function from a 111 // dependency's module even if the root package doesn't compile. 112 PackageKind::Dependency => Compile::DepsOnly, 113 PackageKind::Root => Compile::All, 114 }, 115 codegen: Codegen::All, 116 mode: Mode::Dev, 117 target: Some(target), 118 root_target_support: match package_kind { 119 // The module we want to run is in the root package, so we make sure that the package 120 // can compile successfully for the current target. 121 PackageKind::Root => TargetSupport::Enforced, 122 // On the other hand, if we're trying to run a module that belongs to a dependency, we 123 // only care if the dependency can compile for the current target. 124 PackageKind::Dependency => TargetSupport::NotEnforced, 125 }, 126 no_print_progress, 127 }; 128 129 let built = crate::build::main(paths, options, manifest)?; 130 131 // A module can not be run if it does not exist or does not have a public main function. 132 let main_function = get_or_suggest_main_function(built, &module, target)?; 133 134 telemetry.running(&format!("{module}.main")); 135 136 // Get the command to run the project. 137 match target { 138 Target::Erlang => match runtime { 139 Some(r) => Err(Error::InvalidRuntime { 140 target: Target::Erlang, 141 invalid_runtime: r, 142 }), 143 _ => run_erlang_command(paths, &root_config.name, &module, arguments), 144 }, 145 Target::JavaScript => match runtime.unwrap_or(mod_config.javascript.runtime) { 146 Runtime::Deno => run_javascript_deno_command( 147 paths, 148 &root_config, 149 &main_function.package, 150 &module, 151 arguments, 152 ), 153 Runtime::NodeJs => { 154 run_javascript_node_command(paths, &main_function.package, &module, arguments) 155 } 156 Runtime::Bun => { 157 run_javascript_bun_command(paths, &main_function.package, &module, arguments) 158 } 159 }, 160 } 161} 162 163fn run_erlang_command( 164 paths: &ProjectPaths, 165 package: &str, 166 module: &str, 167 arguments: Vec<String>, 168) -> Result<Command, Error> { 169 let mut args = vec![]; 170 171 // Specify locations of Erlang applications 172 let packages = paths.build_directory_for_target(Mode::Dev, Target::Erlang); 173 174 for entry in crate::fs::read_dir(packages)?.filter_map(Result::ok) { 175 args.push("-pa".into()); 176 args.push(entry.path().join("ebin").into()); 177 } 178 179 // gleam modules are separated by `/`. Erlang modules are separated by `@`. 180 let module = module.replace('/', "@"); 181 182 args.push("-eval".into()); 183 args.push(format!("{package}@@main:run({module})")); 184 185 // Don't run the Erlang shell 186 args.push("-noshell".into()); 187 188 // Tell the BEAM that any following argument are for the program 189 args.push("-extra".into()); 190 for argument in arguments.into_iter() { 191 args.push(argument); 192 } 193 194 Ok(Command { 195 program: "erl".to_string(), 196 args, 197 env: vec![], 198 cwd: None, 199 stdio: Stdio::Inherit, 200 }) 201} 202 203fn run_javascript_bun_command( 204 paths: &ProjectPaths, 205 package: &str, 206 module: &str, 207 arguments: Vec<String>, 208) -> Result<Command, Error> { 209 let mut args = vec!["run".to_string()]; 210 let entry = write_javascript_entrypoint(paths, package, module)?; 211 212 args.push(entry.to_string()); 213 214 for arg in arguments.into_iter() { 215 args.push(arg); 216 } 217 218 Ok(Command { 219 program: "bun".to_string(), 220 args, 221 env: vec![], 222 cwd: None, 223 stdio: Stdio::Inherit, 224 }) 225} 226 227fn run_javascript_node_command( 228 paths: &ProjectPaths, 229 package: &str, 230 module: &str, 231 arguments: Vec<String>, 232) -> Result<Command, Error> { 233 let mut args = vec![]; 234 let entry = write_javascript_entrypoint(paths, package, module)?; 235 236 args.push(entry.to_string()); 237 238 for argument in arguments.into_iter() { 239 args.push(argument); 240 } 241 242 Ok(Command { 243 program: "node".to_string(), 244 args, 245 env: vec![], 246 cwd: None, 247 stdio: Stdio::Inherit, 248 }) 249} 250 251fn write_javascript_entrypoint( 252 paths: &ProjectPaths, 253 package: &str, 254 module: &str, 255) -> Result<Utf8PathBuf, Error> { 256 let path = paths 257 .build_directory_for_package(Mode::Dev, Target::JavaScript, package) 258 .to_path_buf() 259 .join(format!("gleam@@private_main_v{}.mjs", COMPILER_VERSION)); 260 let module = format!( 261 r#"import {{ main }} from "./{module}.mjs"; 262main(); 263"#, 264 ); 265 crate::fs::write(&path, &module)?; 266 Ok(path) 267} 268 269fn run_javascript_deno_command( 270 paths: &ProjectPaths, 271 config: &PackageConfig, 272 package: &str, 273 module: &str, 274 arguments: Vec<String>, 275) -> Result<Command, Error> { 276 let mut args = vec![]; 277 278 // Run the main function. 279 args.push("run".into()); 280 281 // Enable unstable features and APIs 282 if config.javascript.deno.unstable { 283 args.push("--unstable".into()) 284 } 285 286 // Enable location API 287 if let Some(location) = &config.javascript.deno.location { 288 args.push(format!("--location={location}")); 289 } 290 291 // Set deno permissions 292 if config.javascript.deno.allow_all { 293 // Allow all 294 args.push("--allow-all".into()) 295 } else { 296 // Allow env 297 add_deno_flag(&mut args, "--allow-env", &config.javascript.deno.allow_env); 298 299 // Allow sys 300 if config.javascript.deno.allow_sys { 301 args.push("--allow-sys".into()) 302 } 303 304 // Allow hrtime 305 if config.javascript.deno.allow_hrtime { 306 args.push("--allow-hrtime".into()) 307 } 308 309 // Allow net 310 add_deno_flag(&mut args, "--allow-net", &config.javascript.deno.allow_net); 311 312 // Allow ffi 313 if config.javascript.deno.allow_ffi { 314 args.push("--allow-ffi".into()) 315 } 316 317 // Allow read 318 add_deno_flag( 319 &mut args, 320 "--allow-read", 321 &config.javascript.deno.allow_read, 322 ); 323 324 // Allow run 325 add_deno_flag(&mut args, "--allow-run", &config.javascript.deno.allow_run); 326 327 // Allow write 328 add_deno_flag( 329 &mut args, 330 "--allow-write", 331 &config.javascript.deno.allow_write, 332 ); 333 } 334 335 let entrypoint = write_javascript_entrypoint(paths, package, module)?; 336 args.push(entrypoint.to_string()); 337 338 for argument in arguments.into_iter() { 339 args.push(argument); 340 } 341 342 Ok(Command { 343 program: "deno".to_string(), 344 args, 345 env: vec![], 346 cwd: None, 347 stdio: Stdio::Inherit, 348 }) 349} 350 351fn add_deno_flag(args: &mut Vec<String>, flag: &str, flags: &DenoFlag) { 352 match flags { 353 DenoFlag::AllowAll => args.push(flag.to_owned()), 354 DenoFlag::Allow(allow) => { 355 if !allow.is_empty() { 356 args.push(format!("{}={}", flag.to_owned(), allow.join(","))); 357 } 358 } 359 } 360} 361 362static IS_GLEAM_MODULE_PATTERN: OnceLock<Regex> = OnceLock::new(); 363 364/// Check if a module name is a valid gleam module name. 365fn is_gleam_module(module: &str) -> bool { 366 IS_GLEAM_MODULE_PATTERN 367 .get_or_init(|| { 368 Regex::new(&format!( 369 "^({module}{slash})*{module}$", 370 module = "[a-z][_a-z0-9]*", 371 slash = "/", 372 )) 373 .expect("is_gleam_module() RE regex") 374 }) 375 .is_match(module) 376} 377 378/// If provided module is not executable, suggest a possible valid module. 379fn get_or_suggest_main_function( 380 built: Built, 381 module: &str, 382 target: Target, 383) -> Result<ModuleFunction, Error> { 384 // Check if the module exists 385 let error = match built.get_main_function(&module.into(), target) { 386 Ok(main_fn) => return Ok(main_fn), 387 Err(error) => error, 388 }; 389 390 // Otherwise see if the module has been prefixed with "src/", "test/" or "dev/". 391 for prefix in ["src/", "test/", "dev/"] { 392 let other = match module.strip_prefix(prefix) { 393 Some(other) => other.into(), 394 None => continue, 395 }; 396 if built.get_main_function(&other, target).is_ok() { 397 return Err(Error::ModuleDoesNotExist { 398 module: EcoString::from(module), 399 suggestion: Some(other), 400 }); 401 } 402 } 403 404 Err(error) 405} 406 407#[test] 408fn invalid_module_names() { 409 for mod_name in [ 410 "", 411 "/mod/name", 412 "/mod/name/", 413 "mod/name/", 414 "/mod/", 415 "mod/", 416 "common-invalid-character", 417 ] { 418 assert!(!is_gleam_module(mod_name)); 419 } 420} 421 422#[test] 423fn valid_module_names() { 424 for mod_name in ["valid", "valid/name", "valid/mod/name"] { 425 assert!(is_gleam_module(mod_name)); 426 } 427}