Fork of daniellemaywood.uk/gleam — Wasm codegen work
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}