⭐️ A friendly language for building type-safe, scalable systems!
at main 21 kB view raw
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}