Fork of daniellemaywood.uk/gleam — Wasm codegen work
21 kB
788 lines
1#![warn(
2 clippy::all,
3 clippy::dbg_macro,
4 clippy::todo,
5 clippy::mem_forget,
6 clippy::use_self,
7 clippy::filter_map_next,
8 clippy::needless_continue,
9 clippy::needless_borrow,
10 clippy::match_wildcard_for_single_variants,
11 clippy::imprecise_flops,
12 clippy::suboptimal_flops,
13 clippy::lossy_float_literal,
14 clippy::rest_pat_in_fully_bound_structs,
15 clippy::fn_params_excessive_bools,
16 clippy::inefficient_to_string,
17 clippy::linkedlist,
18 clippy::macro_use_imports,
19 clippy::option_option,
20 clippy::verbose_file_reads,
21 clippy::unnested_or_patterns,
22 rust_2018_idioms,
23 missing_debug_implementations,
24 missing_copy_implementations,
25 trivial_casts,
26 trivial_numeric_casts,
27 nonstandard_style,
28 unexpected_cfgs,
29 unused_import_braces,
30 unused_qualifications
31)]
32#![deny(
33 clippy::await_holding_lock,
34 clippy::if_let_mutex,
35 clippy::indexing_slicing,
36 clippy::mem_forget,
37 clippy::ok_expect,
38 clippy::unimplemented,
39 clippy::unwrap_used,
40 unsafe_code,
41 unstable_features,
42 unused_results
43)]
44#![allow(
45 clippy::match_single_binding,
46 clippy::inconsistent_struct_constructor,
47 clippy::assign_op_pattern
48)]
49
50#[cfg(test)]
51#[macro_use]
52extern crate pretty_assertions;
53
54mod add;
55mod beam_compiler;
56mod build;
57mod build_lock;
58mod cli;
59mod compile_package;
60mod config;
61mod dependencies;
62mod docs;
63mod export;
64mod fix;
65mod format;
66pub mod fs;
67mod hex;
68mod http;
69mod lsp;
70mod new;
71mod panic;
72mod publish;
73mod remove;
74pub mod run;
75mod shell;
76
77use config::root_config;
78use fs::{get_current_directory, get_project_root};
79pub use gleam_core::error::{Error, Result};
80
81use gleam_core::{
82 analyse::TargetSupport,
83 build::{Codegen, Compile, Mode, NullTelemetry, Options, Runtime, Target},
84 hex::RetirementReason,
85 paths::ProjectPaths,
86 version::COMPILER_VERSION,
87};
88use std::str::FromStr;
89
90use camino::Utf8PathBuf;
91
92use clap::{
93 Args, Parser, Subcommand,
94 builder::{PossibleValuesParser, Styles, TypedValueParser, styling},
95};
96use strum::VariantNames;
97
98#[derive(Args, Debug, Clone)]
99struct UpdateOptions {
100 /// (optional) Names of the packages to update
101 /// If omitted, all dependencies will be updated
102 #[arg(verbatim_doc_comment)]
103 packages: Vec<String>,
104}
105
106#[derive(Args, Debug, Clone)]
107struct TreeOptions {
108 /// Name of the package to get the dependency tree for
109 #[arg(
110 short,
111 long,
112 ignore_case = true,
113 help = "Package to be used as the root of the tree"
114 )]
115 package: Option<String>,
116 /// Name of the package to get the inverted dependency tree for
117 #[arg(
118 short,
119 long,
120 ignore_case = true,
121 help = "Invert the tree direction and focus on the given package",
122 value_name = "PACKAGE"
123 )]
124 invert: Option<String>,
125}
126
127#[derive(Parser, Debug)]
128#[command(
129 version,
130 name = "gleam",
131 next_display_order = None,
132 help_template = "\
133{before-help}{name} {version}
134
135{usage-heading} {usage}
136
137{all-args}{after-help}",
138 styles = Styles::styled()
139 .header(styling::AnsiColor::Yellow.on_default())
140 .usage(styling::AnsiColor::Yellow.on_default())
141 .literal(styling::AnsiColor::Green.on_default())
142)]
143enum Command {
144 /// Build the project
145 Build {
146 /// Emit compile time warnings as errors
147 #[arg(long)]
148 warnings_as_errors: bool,
149
150 #[arg(short, long, ignore_case = true, help = target_doc())]
151 target: Option<Target>,
152
153 /// Don't print progress information
154 #[clap(long)]
155 no_print_progress: bool,
156 },
157
158 /// Type check the project
159 Check {
160 #[arg(short, long, ignore_case = true, help = target_doc())]
161 target: Option<Target>,
162 },
163
164 /// Publish the project to the Hex package manager
165 ///
166 /// This command uses the environment variable:
167 ///
168 /// - HEXPM_API_KEY: (optional) A Hex API key to use instead of authenticating.
169 ///
170 #[command(verbatim_doc_comment)]
171 Publish {
172 #[arg(long)]
173 replace: bool,
174 #[arg(short, long)]
175 yes: bool,
176 },
177
178 /// Render HTML documentation
179 #[command(subcommand)]
180 Docs(Docs),
181
182 /// Work with dependency packages
183 #[command(subcommand)]
184 Deps(Dependencies),
185
186 /// Update dependency packages to their latest versions
187 Update(UpdateOptions),
188
189 /// Work with the Hex package manager
190 #[command(subcommand)]
191 Hex(Hex),
192
193 /// Create a new project
194 New(NewOptions),
195
196 /// Format source code
197 Format {
198 /// Files to format
199 #[arg(default_value = ".")]
200 files: Vec<String>,
201
202 /// Read source from STDIN
203 #[arg(long)]
204 stdin: bool,
205
206 /// Check if inputs are formatted without changing them
207 #[arg(long)]
208 check: bool,
209 },
210 /// Rewrite deprecated Gleam code
211 Fix,
212
213 /// Start an Erlang shell
214 Shell,
215
216 /// Run the project
217 #[command(trailing_var_arg = true)]
218 Run {
219 #[arg(short, long, ignore_case = true, help = target_doc())]
220 target: Option<Target>,
221
222 #[arg(long, ignore_case = true, help = runtime_doc())]
223 runtime: Option<Runtime>,
224
225 /// The module to run
226 #[arg(short, long)]
227 module: Option<String>,
228
229 /// Don't print progress information
230 #[clap(long)]
231 no_print_progress: bool,
232
233 arguments: Vec<String>,
234 },
235
236 /// Run the project tests
237 #[command(trailing_var_arg = true)]
238 Test {
239 #[arg(short, long, ignore_case = true, help = target_doc())]
240 target: Option<Target>,
241
242 #[arg(long, ignore_case = true, help = runtime_doc())]
243 runtime: Option<Runtime>,
244
245 arguments: Vec<String>,
246 },
247
248 /// Run the project development entrypoint
249 #[command(trailing_var_arg = true)]
250 Dev {
251 #[arg(short, long, ignore_case = true, help = target_doc())]
252 target: Option<Target>,
253
254 #[arg(long, ignore_case = true, help = runtime_doc())]
255 runtime: Option<Runtime>,
256
257 arguments: Vec<String>,
258 },
259
260 /// Compile a single Gleam package
261 #[command(hide = true)]
262 CompilePackage(CompilePackage),
263
264 /// Read and print gleam.toml for debugging
265 #[command(hide = true)]
266 PrintConfig,
267
268 /// Add new project dependencies
269 Add {
270 /// The names of Hex packages to add
271 #[arg(required = true)]
272 packages: Vec<String>,
273
274 /// Add the packages as dev-only dependencies
275 #[arg(long)]
276 dev: bool,
277 },
278
279 /// Remove project dependencies
280 Remove {
281 /// The names of packages to remove
282 #[arg(required = true)]
283 packages: Vec<String>,
284 },
285
286 /// Clean build artifacts
287 Clean,
288
289 /// Run the language server, to be used by editors
290 #[command(name = "lsp")]
291 LanguageServer,
292
293 /// Export something useful from the Gleam project
294 #[command(subcommand)]
295 Export(ExportTarget),
296}
297
298fn template_doc() -> &'static str {
299 "The template to use"
300}
301
302fn target_doc() -> String {
303 format!("The platform to target ({})", Target::VARIANTS.join("|"))
304}
305
306fn runtime_doc() -> String {
307 format!("The runtime to target ({})", Runtime::VARIANTS.join("|"))
308}
309
310#[derive(Subcommand, Debug, Clone)]
311pub enum ExportTarget {
312 /// Precompiled Erlang, suitable for deployment
313 ErlangShipment,
314 /// The package bundled into a tarball, suitable for publishing to Hex
315 HexTarball,
316 /// The JavaScript prelude module
317 JavascriptPrelude,
318 /// The TypeScript prelude module
319 TypescriptPrelude,
320 /// Information on the modules, functions, and types in the project in JSON format
321 PackageInterface {
322 #[arg(long = "out", required = true)]
323 /// The path to write the JSON file to
324 output: Utf8PathBuf,
325 },
326 /// Package information (gleam.toml) in JSON format
327 PackageInformation {
328 #[arg(long = "out", required = true)]
329 /// The path to write the JSON file to
330 output: Utf8PathBuf,
331 },
332}
333
334#[derive(Args, Debug, Clone)]
335pub struct NewOptions {
336 /// Location of the project root
337 pub project_root: String,
338
339 /// Name of the project
340 #[arg(long)]
341 pub name: Option<String>,
342
343 #[arg(long, ignore_case = true, default_value = "erlang", help = template_doc())]
344 pub template: new::Template,
345
346 /// Skip git initialization and creation of .gitignore, .git/* and .github/* files
347 #[arg(long)]
348 pub skip_git: bool,
349
350 /// Skip creation of .github/* files
351 #[arg(long)]
352 pub skip_github: bool,
353}
354
355#[derive(Args, Debug)]
356pub struct CompilePackage {
357 /// The compilation target for the generated project
358 #[arg(long, ignore_case = true)]
359 target: Target,
360
361 /// The directory of the Gleam package
362 #[arg(long = "package")]
363 package_directory: Utf8PathBuf,
364
365 /// A directory to write compiled package to
366 #[arg(long = "out")]
367 output_directory: Utf8PathBuf,
368
369 /// A directories of precompiled Gleam projects
370 #[arg(long = "lib")]
371 libraries_directory: Utf8PathBuf,
372
373 /// The location of the JavaScript prelude module, relative to the `out`
374 /// directory.
375 ///
376 /// Required when compiling to JavaScript.
377 ///
378 /// This likely wants to be a `.mjs` file as NodeJS does not permit
379 /// importing of other JavaScript file extensions.
380 ///
381 #[arg(verbatim_doc_comment, long = "javascript-prelude")]
382 javascript_prelude: Option<Utf8PathBuf>,
383
384 /// Skip Erlang to BEAM bytecode compilation if given
385 #[arg(long = "no-beam")]
386 skip_beam_compilation: bool,
387}
388
389#[derive(Subcommand, Debug)]
390enum Dependencies {
391 /// List all dependency packages
392 List,
393
394 /// Download all dependency packages
395 Download,
396
397 /// Update dependency packages to their latest versions
398 Update(UpdateOptions),
399
400 /// Tree of all the dependency packages
401 Tree(TreeOptions),
402}
403
404#[derive(Subcommand, Debug)]
405enum Hex {
406 /// Retire a release from Hex
407 ///
408 /// This command uses the environment variable:
409 ///
410 /// - HEXPM_API_KEY: (optional) A Hex API key to authenticate against the Hex package manager.
411 ///
412 #[command(verbatim_doc_comment)]
413 Retire {
414 package: String,
415
416 version: String,
417
418 #[arg(value_parser = PossibleValuesParser::new(RetirementReason::VARIANTS).map(|s| RetirementReason::from_str(&s).unwrap()))]
419 reason: RetirementReason,
420
421 message: Option<String>,
422 },
423
424 /// Un-retire a release from Hex
425 ///
426 /// This command uses this environment variable:
427 ///
428 /// - HEXPM_API_KEY: (optional) A Hex API key to authenticate against the Hex package manager.
429 ///
430 #[command(verbatim_doc_comment)]
431 Unretire { package: String, version: String },
432
433 /// Revert a release from Hex
434 ///
435 /// This command uses this environment variable:
436 ///
437 /// - HEXPM_API_KEY: (optional) A Hex API key to authenticate against the Hex package manager.
438 ///
439 #[command(verbatim_doc_comment)]
440 Revert {
441 #[arg(long)]
442 package: Option<String>,
443
444 #[arg(long)]
445 version: Option<String>,
446 },
447
448 /// Authenticate with Hex
449 Authenticate,
450}
451
452#[derive(Subcommand, Debug)]
453enum Docs {
454 /// Render HTML docs locally
455 Build {
456 /// Opens the docs in a browser after rendering
457 #[arg(long)]
458 open: bool,
459
460 #[arg(short, long, ignore_case = true, help = target_doc())]
461 target: Option<Target>,
462 },
463
464 /// Publish HTML docs to HexDocs
465 ///
466 /// This command uses this environment variable:
467 ///
468 /// - HEXPM_API_KEY: (optional) A Hex API key to authenticate against the Hex package manager.
469 ///
470 #[command(verbatim_doc_comment)]
471 Publish,
472
473 /// Remove HTML docs from HexDocs
474 ///
475 /// This command uses this environment variable:
476 ///
477 /// - HEXPM_API_KEY: (optional) A Hex API key to authenticate against the Hex package manager.
478 ///
479 #[command(verbatim_doc_comment)]
480 Remove {
481 /// The name of the package
482 #[arg(long)]
483 package: String,
484
485 /// The version of the docs to remove
486 #[arg(long)]
487 version: String,
488 },
489}
490
491pub fn main() {
492 initialise_logger();
493 panic::add_handler();
494 let stderr = cli::stderr_buffer_writer();
495 let result = parse_and_run_command();
496 match result {
497 Ok(_) => {
498 tracing::info!("Successfully completed");
499 }
500 Err(error) => {
501 tracing::error!(error = ?error, "Failed");
502 let mut buffer = stderr.buffer();
503 error.pretty(&mut buffer);
504 stderr.print(&buffer).expect("Final result error writing");
505 std::process::exit(1);
506 }
507 }
508}
509
510fn parse_and_run_command() -> Result<(), Error> {
511 match Command::parse() {
512 Command::Build {
513 target,
514 warnings_as_errors,
515 no_print_progress,
516 } => {
517 let paths = find_project_paths()?;
518 command_build(&paths, target, warnings_as_errors, no_print_progress)
519 }
520
521 Command::Check { target } => {
522 let paths = find_project_paths()?;
523 command_check(&paths, target)
524 }
525
526 Command::Docs(Docs::Build { open, target }) => {
527 let paths = find_project_paths()?;
528 docs::build(&paths, docs::BuildOptions { open, target })
529 }
530
531 Command::Docs(Docs::Publish) => {
532 let paths = find_project_paths()?;
533 docs::publish(&paths)
534 }
535
536 Command::Docs(Docs::Remove { package, version }) => docs::remove(package, version),
537
538 Command::Format {
539 stdin,
540 files,
541 check,
542 } => format::run(stdin, check, files),
543
544 Command::Fix => {
545 let paths = find_project_paths()?;
546 fix::run(&paths)
547 }
548
549 Command::Deps(Dependencies::List) => {
550 let paths = find_project_paths()?;
551 dependencies::list(&paths)
552 }
553
554 Command::Deps(Dependencies::Download) => {
555 let paths = find_project_paths()?;
556 download_dependencies(&paths)
557 }
558
559 Command::Deps(Dependencies::Update(options)) => {
560 let paths = find_project_paths()?;
561 dependencies::update(&paths, options.packages)
562 }
563
564 Command::Deps(Dependencies::Tree(options)) => {
565 let paths = find_project_paths()?;
566 dependencies::tree(&paths, options)
567 }
568
569 Command::Hex(Hex::Authenticate) => hex::authenticate(),
570
571 Command::New(options) => new::create(options, COMPILER_VERSION),
572
573 Command::Shell => {
574 let paths = find_project_paths()?;
575 shell::command(&paths)
576 }
577
578 Command::Run {
579 target,
580 arguments,
581 runtime,
582 module,
583 no_print_progress,
584 } => {
585 let paths = find_project_paths()?;
586 run::command(
587 &paths,
588 arguments,
589 target,
590 runtime,
591 module,
592 run::Which::Src,
593 no_print_progress,
594 )
595 }
596
597 Command::Test {
598 target,
599 arguments,
600 runtime,
601 } => {
602 let paths = find_project_paths()?;
603 run::command(
604 &paths,
605 arguments,
606 target,
607 runtime,
608 None,
609 run::Which::Test,
610 false,
611 )
612 }
613
614 Command::Dev {
615 target,
616 arguments,
617 runtime,
618 } => {
619 let paths = find_project_paths()?;
620 run::command(
621 &paths,
622 arguments,
623 target,
624 runtime,
625 None,
626 run::Which::Dev,
627 false,
628 )
629 }
630
631 Command::CompilePackage(opts) => compile_package::command(opts),
632
633 Command::Publish { replace, yes } => {
634 let paths = find_project_paths()?;
635 publish::command(&paths, replace, yes)
636 }
637
638 Command::PrintConfig => {
639 let paths = find_project_paths()?;
640 print_config(&paths)
641 }
642
643 Command::Hex(Hex::Retire {
644 package,
645 version,
646 reason,
647 message,
648 }) => hex::retire(package, version, reason, message),
649
650 Command::Hex(Hex::Unretire { package, version }) => hex::unretire(package, version),
651
652 Command::Hex(Hex::Revert { package, version }) => {
653 let paths = find_project_paths()?;
654 hex::revert(&paths, package, version)
655 }
656
657 Command::Add { packages, dev } => {
658 let paths = find_project_paths()?;
659 add::command(&paths, packages, dev)
660 }
661
662 Command::Remove { packages } => {
663 let paths = find_project_paths()?;
664 remove::command(&paths, packages)
665 }
666
667 Command::Update(options) => {
668 let paths = find_project_paths()?;
669 dependencies::update(&paths, options.packages)
670 }
671
672 Command::Clean => {
673 let paths = find_project_paths()?;
674 clean(&paths)
675 }
676
677 Command::LanguageServer => lsp::main(),
678
679 Command::Export(ExportTarget::ErlangShipment) => {
680 let paths = find_project_paths()?;
681 export::erlang_shipment(&paths)
682 }
683 Command::Export(ExportTarget::HexTarball) => {
684 let paths = find_project_paths()?;
685 export::hex_tarball(&paths)
686 }
687 Command::Export(ExportTarget::JavascriptPrelude) => export::javascript_prelude(),
688 Command::Export(ExportTarget::TypescriptPrelude) => export::typescript_prelude(),
689 Command::Export(ExportTarget::PackageInterface { output }) => {
690 let paths = find_project_paths()?;
691 export::package_interface(&paths, output)
692 }
693 Command::Export(ExportTarget::PackageInformation { output }) => {
694 let paths = find_project_paths()?;
695 export::package_information(&paths, output)
696 }
697 }
698}
699
700fn command_check(paths: &ProjectPaths, target: Option<Target>) -> Result<()> {
701 let _ = build::main(
702 paths,
703 Options {
704 root_target_support: TargetSupport::Enforced,
705 warnings_as_errors: false,
706 codegen: Codegen::DepsOnly,
707 compile: Compile::All,
708 mode: Mode::Dev,
709 target,
710 no_print_progress: false,
711 },
712 build::download_dependencies(paths, cli::Reporter::new())?,
713 )?;
714 Ok(())
715}
716
717fn command_build(
718 paths: &ProjectPaths,
719 target: Option<Target>,
720 warnings_as_errors: bool,
721 no_print_progress: bool,
722) -> Result<()> {
723 let manifest = if no_print_progress {
724 build::download_dependencies(paths, NullTelemetry)?
725 } else {
726 build::download_dependencies(paths, cli::Reporter::new())?
727 };
728 let _ = build::main(
729 paths,
730 Options {
731 root_target_support: TargetSupport::Enforced,
732 warnings_as_errors,
733 codegen: Codegen::All,
734 compile: Compile::All,
735 mode: Mode::Dev,
736 target,
737 no_print_progress,
738 },
739 manifest,
740 )?;
741 Ok(())
742}
743
744fn print_config(paths: &ProjectPaths) -> Result<()> {
745 let config = root_config(paths)?;
746 println!("{config:#?}");
747 Ok(())
748}
749
750fn clean(paths: &ProjectPaths) -> Result<()> {
751 fs::delete_directory(&paths.build_directory())
752}
753
754fn initialise_logger() {
755 let enable_colours = std::env::var("GLEAM_LOG_NOCOLOUR").is_err();
756 tracing_subscriber::fmt()
757 .with_writer(std::io::stderr)
758 .with_env_filter(std::env::var("GLEAM_LOG").unwrap_or_else(|_| "off".into()))
759 .with_target(false)
760 .with_ansi(enable_colours)
761 .without_time()
762 .init();
763}
764
765fn find_project_paths() -> Result<ProjectPaths> {
766 let current_dir = get_current_directory()?;
767 get_project_root(current_dir).map(ProjectPaths::new)
768}
769
770#[cfg(test)]
771fn project_paths_at_current_directory_without_toml() -> ProjectPaths {
772 let current_dir = get_current_directory().expect("Failed to get current directory");
773 ProjectPaths::new(current_dir)
774}
775
776fn download_dependencies(paths: &ProjectPaths) -> Result<()> {
777 _ = dependencies::download(
778 paths,
779 cli::Reporter::new(),
780 None,
781 Vec::new(),
782 dependencies::DependencyManagerConfig {
783 use_manifest: dependencies::UseManifest::Yes,
784 check_major_versions: dependencies::CheckMajorVersions::No,
785 },
786 )?;
787 Ok(())
788}