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