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