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