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