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 428 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 Target::Cranelift => todo!("run"), 161 } 162} 163 164fn run_erlang_command( 165 paths: &ProjectPaths, 166 package: &str, 167 module: &str, 168 arguments: Vec<String>, 169) -> Result<Command, Error> { 170 let mut args = vec![]; 171 172 // Specify locations of Erlang applications 173 let packages = paths.build_directory_for_target(Mode::Dev, Target::Erlang); 174 175 for entry in crate::fs::read_dir(packages)?.filter_map(Result::ok) { 176 args.push("-pa".into()); 177 args.push(entry.path().join("ebin").into()); 178 } 179 180 // gleam modules are separated by `/`. Erlang modules are separated by `@`. 181 let module = module.replace('/', "@"); 182 183 args.push("-eval".into()); 184 args.push(format!("{package}@@main:run({module})")); 185 186 // Don't run the Erlang shell 187 args.push("-noshell".into()); 188 189 // Tell the BEAM that any following argument are for the program 190 args.push("-extra".into()); 191 for argument in arguments.into_iter() { 192 args.push(argument); 193 } 194 195 Ok(Command { 196 program: "erl".to_string(), 197 args, 198 env: vec![], 199 cwd: None, 200 stdio: Stdio::Inherit, 201 }) 202} 203 204fn run_javascript_bun_command( 205 paths: &ProjectPaths, 206 package: &str, 207 module: &str, 208 arguments: Vec<String>, 209) -> Result<Command, Error> { 210 let mut args = vec!["run".to_string()]; 211 let entry = write_javascript_entrypoint(paths, package, module)?; 212 213 args.push(entry.to_string()); 214 215 for arg in arguments.into_iter() { 216 args.push(arg); 217 } 218 219 Ok(Command { 220 program: "bun".to_string(), 221 args, 222 env: vec![], 223 cwd: None, 224 stdio: Stdio::Inherit, 225 }) 226} 227 228fn run_javascript_node_command( 229 paths: &ProjectPaths, 230 package: &str, 231 module: &str, 232 arguments: Vec<String>, 233) -> Result<Command, Error> { 234 let mut args = vec![]; 235 let entry = write_javascript_entrypoint(paths, package, module)?; 236 237 args.push(entry.to_string()); 238 239 for argument in arguments.into_iter() { 240 args.push(argument); 241 } 242 243 Ok(Command { 244 program: "node".to_string(), 245 args, 246 env: vec![], 247 cwd: None, 248 stdio: Stdio::Inherit, 249 }) 250} 251 252fn write_javascript_entrypoint( 253 paths: &ProjectPaths, 254 package: &str, 255 module: &str, 256) -> Result<Utf8PathBuf, Error> { 257 let path = paths 258 .build_directory_for_package(Mode::Dev, Target::JavaScript, package) 259 .to_path_buf() 260 .join(format!("gleam@@private_main_v{}.mjs", COMPILER_VERSION)); 261 let module = format!( 262 r#"import {{ main }} from "./{module}.mjs"; 263main(); 264"#, 265 ); 266 crate::fs::write(&path, &module)?; 267 Ok(path) 268} 269 270fn run_javascript_deno_command( 271 paths: &ProjectPaths, 272 config: &PackageConfig, 273 package: &str, 274 module: &str, 275 arguments: Vec<String>, 276) -> Result<Command, Error> { 277 let mut args = vec![]; 278 279 // Run the main function. 280 args.push("run".into()); 281 282 // Enable unstable features and APIs 283 if config.javascript.deno.unstable { 284 args.push("--unstable".into()); 285 } 286 287 // Enable location API 288 if let Some(location) = &config.javascript.deno.location { 289 args.push(format!("--location={location}")); 290 } 291 292 // Set deno permissions 293 if config.javascript.deno.allow_all { 294 // Allow all 295 args.push("--allow-all".into()); 296 } else { 297 // Allow env 298 add_deno_flag(&mut args, "--allow-env", &config.javascript.deno.allow_env); 299 300 // Allow sys 301 if config.javascript.deno.allow_sys { 302 args.push("--allow-sys".into()); 303 } 304 305 // Allow hrtime 306 if config.javascript.deno.allow_hrtime { 307 args.push("--allow-hrtime".into()); 308 } 309 310 // Allow net 311 add_deno_flag(&mut args, "--allow-net", &config.javascript.deno.allow_net); 312 313 // Allow ffi 314 if config.javascript.deno.allow_ffi { 315 args.push("--allow-ffi".into()); 316 } 317 318 // Allow read 319 add_deno_flag( 320 &mut args, 321 "--allow-read", 322 &config.javascript.deno.allow_read, 323 ); 324 325 // Allow run 326 add_deno_flag(&mut args, "--allow-run", &config.javascript.deno.allow_run); 327 328 // Allow write 329 add_deno_flag( 330 &mut args, 331 "--allow-write", 332 &config.javascript.deno.allow_write, 333 ); 334 } 335 336 let entrypoint = write_javascript_entrypoint(paths, package, module)?; 337 args.push(entrypoint.to_string()); 338 339 for argument in arguments.into_iter() { 340 args.push(argument); 341 } 342 343 Ok(Command { 344 program: "deno".to_string(), 345 args, 346 env: vec![], 347 cwd: None, 348 stdio: Stdio::Inherit, 349 }) 350} 351 352fn add_deno_flag(args: &mut Vec<String>, flag: &str, flags: &DenoFlag) { 353 match flags { 354 DenoFlag::AllowAll => args.push(flag.to_owned()), 355 DenoFlag::Allow(allow) => { 356 if !allow.is_empty() { 357 args.push(format!("{}={}", flag.to_owned(), allow.join(","))); 358 } 359 } 360 } 361} 362 363static IS_GLEAM_MODULE_PATTERN: OnceLock<Regex> = OnceLock::new(); 364 365/// Check if a module name is a valid gleam module name. 366fn is_gleam_module(module: &str) -> bool { 367 IS_GLEAM_MODULE_PATTERN 368 .get_or_init(|| { 369 Regex::new(&format!( 370 "^({module}{slash})*{module}$", 371 module = "[a-z][_a-z0-9]*", 372 slash = "/", 373 )) 374 .expect("is_gleam_module() RE regex") 375 }) 376 .is_match(module) 377} 378 379/// If provided module is not executable, suggest a possible valid module. 380fn get_or_suggest_main_function( 381 built: Built, 382 module: &str, 383 target: Target, 384) -> Result<ModuleFunction, Error> { 385 // Check if the module exists 386 let error = match built.get_main_function(&module.into(), target) { 387 Ok(main_fn) => return Ok(main_fn), 388 Err(error) => error, 389 }; 390 391 // Otherwise see if the module has been prefixed with "src/", "test/" or "dev/". 392 for prefix in ["src/", "test/", "dev/"] { 393 let other = match module.strip_prefix(prefix) { 394 Some(other) => other.into(), 395 None => continue, 396 }; 397 if built.get_main_function(&other, target).is_ok() { 398 return Err(Error::ModuleDoesNotExist { 399 module: EcoString::from(module), 400 suggestion: Some(other), 401 }); 402 } 403 } 404 405 Err(error) 406} 407 408#[test] 409fn invalid_module_names() { 410 for mod_name in [ 411 "", 412 "/mod/name", 413 "/mod/name/", 414 "mod/name/", 415 "/mod/", 416 "mod/", 417 "common-invalid-character", 418 ] { 419 assert!(!is_gleam_module(mod_name)); 420 } 421} 422 423#[test] 424fn valid_module_names() { 425 for mod_name in ["valid", "valid/name", "valid/mod/name"] { 426 assert!(is_gleam_module(mod_name)); 427 } 428}