// SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2018 The Gleam contributors #![warn( clippy::all, clippy::redundant_clone, clippy::dbg_macro, clippy::todo, clippy::mem_forget, clippy::use_self, clippy::filter_map_next, clippy::needless_continue, clippy::needless_borrow, clippy::match_wildcard_for_single_variants, clippy::imprecise_flops, clippy::suboptimal_flops, clippy::lossy_float_literal, clippy::rest_pat_in_fully_bound_structs, clippy::fn_params_excessive_bools, clippy::inefficient_to_string, clippy::linkedlist, clippy::macro_use_imports, clippy::option_option, clippy::verbose_file_reads, clippy::unnested_or_patterns, clippy::default_trait_access, clippy::format_push_string, rust_2018_idioms, missing_debug_implementations, missing_copy_implementations, trivial_casts, trivial_numeric_casts, nonstandard_style, unexpected_cfgs, unused_import_braces, unused_qualifications )] #![deny( clippy::await_holding_lock, clippy::if_let_mutex, clippy::indexing_slicing, clippy::mem_forget, clippy::ok_expect, clippy::unimplemented, clippy::unwrap_used, unsafe_code, unstable_features, unused_results )] #![allow( clippy::match_single_binding, clippy::inconsistent_struct_constructor, clippy::assign_op_pattern, clippy::len_without_is_empty, clippy::let_unit_value )] #[cfg(test)] #[macro_use] extern crate pretty_assertions; mod add; mod beam_compiler; mod build; mod build_lock; mod cli; mod compile_package; mod config; mod dependencies; mod docs; mod export; mod fix; mod format; pub mod fs; mod hex; mod http; mod lsp; mod new; mod owner; mod panic; mod publish; mod remove; pub mod run; mod shell; mod text_layout; use config::root_config; use fs::{get_current_directory, get_project_root}; pub use gleam_core::error::{Error, Result}; use camino::Utf8PathBuf; use clap::{ Args, Parser, Subcommand, builder::{Styles, styling}, }; use gleam_core::{ analyse::TargetSupport, build::{Codegen, Compile, Mode, NullTelemetry, Options, Runtime, Target}, hex::RetirementReason, paths::ProjectPaths, version::COMPILER_VERSION, }; #[derive(Args, Debug, Clone)] pub struct UpdateOptions { /// (optional) Names of the packages to update /// If omitted, all dependencies will be updated #[arg(verbatim_doc_comment)] packages: Vec, } #[derive(Args, Debug, Clone)] pub struct TreeOptions { /// Name of the package to get the dependency tree for #[arg( short, long, ignore_case = true, conflicts_with = "invert", help = "Package to be used as the root of the tree" )] package: Option, /// Name of the package to get the inverted dependency tree for #[arg( short, long, ignore_case = true, conflicts_with = "package", help = "Invert the tree direction and focus on the given package", value_name = "PACKAGE" )] invert: Option, } #[derive(Parser, Debug)] #[command( version, name = "gleam", next_display_order = None, help_template = "\ {before-help}{name} {version} {usage-heading} {usage} {all-args}{after-help}", styles = Styles::styled() .header(styling::AnsiColor::Yellow.on_default()) .usage(styling::AnsiColor::Yellow.on_default()) .literal(styling::AnsiColor::Green.on_default()) )] pub enum Command { /// Build the project /// /// This command optionally accepts the environment variable /// `HEXPM_READ_API_KEY`, which can hold a Hex API key to authenticate /// with Hex with a higher rate limit. /// #[command(verbatim_doc_comment)] Build { /// Consider the build failed if the package contains any warnings #[arg(long)] warnings_as_errors: bool, /// Which compilation target to use #[arg(short, long, ignore_case = true, help = target_doc())] target: Option, #[arg(long, help = no_print_progress_doc())] no_print_progress: bool, }, /// Type check the project Check { /// Which compilation target to use #[arg(short, long, ignore_case = true, help = target_doc())] target: Option, }, /// Publish the project to the Hex package repository /// /// Please ensure your package is suitable for production use before /// publishing. If you have a prototype package that you wish to use /// in another project then use git dependencies instead of publishing /// to the package repository. /// /// This command optionally accepts the environment variable /// `HEXPM_API_KEY`, which can hold a Hex API key to authenticate /// with Hex. /// #[command(verbatim_doc_comment)] Publish { /// Replace the latest release with this one #[arg(long)] replace: bool, /// Automatically accept confirmation prompts #[arg(short, long)] yes: bool, }, /// Render HTML documentation for the package /// /// There are several options in `gleam.toml` that can be used to /// configure the output. /// /// repository = { type = "github", user = "lpil", repo = "wibble" } /// /// Specify the location of the source code repository for the package. /// This will add a link to the side bar, and add "view code" links for /// the documented types and values. /// /// links = [ /// { title = "Home page", href = "https://example.com" }, /// { title = "Other site", href = "https://another.example.com" }, /// ] /// /// Specify some additional links to include in the sidebar. /// /// [documentation] /// pages = [ /// { title = "My Page", path = "my-page.html", source = "./path/to/my-page.md" }, /// ] /// /// Specify additional markdown pages to include in the documentation, /// with links for each added to the sidebar. /// #[command(subcommand, verbatim_doc_comment)] Docs(Docs), /// Work with dependency packages /// /// The packages and the acceptable version ranges are specified in /// `gleam.toml`. You can edit this file manually, or use the `gleam add` /// and `gleam remove` commands. /// /// Package versions follow semantic versioning: MAJOR.MINOR.PATCH. /// - Major updates include breaking changes, either type changes or /// semantic changes. /// - Minor updates include new functionality, but no breaking changes. /// - Patch updates include only bug fixes. /// /// Dependency resolution will be performed automatically by any command /// that builds your project. Once versions have been selected they are /// written to `manifest.toml`, which locks the package to those versions, /// making your build deterministic. You should not edit this file manually. /// /// To upgrade the dependencies you can use the `gleam update` command, /// which will select the newest versions compatible with the requirements /// in `gleam.toml`. /// /// The `[dependencies]` section of `gleam.toml` holds the production /// dependencies. These are included in your production application, or /// are used as the dependencies when published as a library. The /// `[dev_dependencies]` section holds dependencies that are only used /// during development, e.g. code used for testing the package. /// /// ## Syntax examples: /// /// [dependencies] /// wibble = ">= 1.2.0 and < 2.0.0" /// /// Require the package `wibble`, permitting versions greater than or /// equal to 1.2.0, but lower than 2.0.0. /// /// [dependencies] /// wibble = ">= 1.2.0 and < 2.0.0 and != 1.4.1" /// /// Permit a range, but deny a specific version within that range. This /// could be useful if there is a version known to have a bug. /// /// [dependencies] /// wibble = { git = "https://example.com/wibble.git", ref = "a8b3c5d82" } /// /// A dependency fetched from Git instead of from Hex. This is useful /// for using packages of yours that are not-yet production-ready, or /// for bug fixes that have not yet been published to Hex. /// /// [dependencies] /// wibble = { path = "../wibble" } /// /// A local dependency, on your computer. This is useful for testing and /// and for applications made of multiple packages in a single version /// control repository. /// /// This command optionally accepts the environment variable /// `HEXPM_READ_API_KEY`, which can hold a Hex API key to authenticate /// with Hex with a higher rate limit. /// #[command(subcommand, verbatim_doc_comment)] Deps(Dependencies), /// Update dependency packages to their latest versions /// /// This command optionally accepts the environment variable /// `HEXPM_READ_API_KEY`, which can hold a Hex API key to authenticate /// with Hex with a higher rate limit. /// #[command(verbatim_doc_comment)] Update(UpdateOptions), /// Work with the Hex package manager #[command(subcommand)] Hex(Hex), /// Create a new project New(NewOptions), /// Format source code Format { /// The files or directories to format #[arg(default_value = ".")] files: Vec, /// Read source from standard-input #[arg(long)] stdin: bool, /// Only check if inputs are formatted correctly, erroring if they are not #[arg(long)] check: bool, }, /// Rewrite deprecated Gleam code Fix, /// Start an Erlang REPL with the Gleam code loaded Shell, /// Run the project /// /// This command runs the `main` function from the `` module. #[command(trailing_var_arg = true)] Run { /// Which compilation target to use #[arg(short, long, ignore_case = true, help = target_doc())] target: Option, /// Which runtime to use #[arg(long, ignore_case = true, help = runtime_doc())] runtime: Option, /// The module to run #[arg(short, long)] module: Option, #[arg(long, help = no_print_progress_doc())] no_print_progress: bool, arguments: Vec, }, /// Run the project tests /// /// This command runs the `main` function from the `_test` module. #[command(trailing_var_arg = true)] Test { /// Which compilation target to use #[arg(short, long, ignore_case = true, help = target_doc())] target: Option, /// Which runtime to use #[arg(long, ignore_case = true, help = runtime_doc())] runtime: Option, arguments: Vec, }, /// Run the project development entrypoint /// /// This command runs the `main` function from the `_dev` module. #[command(trailing_var_arg = true)] Dev { /// Which compilation target to use #[arg(short, long, ignore_case = true, help = target_doc())] target: Option, /// Which runtime to use #[arg(long, ignore_case = true, help = runtime_doc())] runtime: Option, #[arg(long, help = no_print_progress_doc())] no_print_progress: bool, arguments: Vec, }, /// A low-level API for compiling a single Gleam package /// /// This is to be used by other build tools to implement support for Gleam /// code. It is not used directly by humans. /// #[command(verbatim_doc_comment)] CompilePackage(CompilePackage), /// Read and print gleam.toml for debugging #[command(hide = true)] PrintConfig, /// Add new dependencies /// /// The newest compatible version of the package is determined, and then /// `gleam.toml` is updated to require at least that version, with a range /// permitting future patch and minor updates. /// /// Add the package "wibble": /// gleam add wibble /// /// Add the package "wibble", requiring version >= v2.0.0 and < v3.0.0: /// gleam add wibble@2 /// /// Add the package "wibble", requiring version >= v2.5.1 and < v3.0.0: /// gleam add wibble@2.5.1 /// /// Add multiple packages: /// gleam add wibble@2 warble@1 /// /// Add a package as a non-production dependency: /// gleam add --dev wibble /// /// You can also edit `gleam.toml` directly, for further control over your /// package dependencies. Run `gleam help deps` for documentation on the /// format. /// #[command(verbatim_doc_comment)] Add { /// The names of Hex packages to add #[arg(required = true)] packages: Vec, /// Add the packages as dev-only dependencies #[arg(long)] dev: bool, }, /// Remove project dependencies Remove { /// The names of packages to remove #[arg(required = true)] packages: Vec, }, /// Delete any build artefacts for this project Clean, /// Run the language server, to be used by editors #[command(name = "lsp")] LanguageServer, /// Export something useful from the Gleam project #[command(subcommand)] Export(ExportTarget), } impl Command { pub fn run(self, directory: Utf8PathBuf) -> Result<(), Error> { match self { Self::Build { target, warnings_as_errors, no_print_progress, } => { let paths = find_project_paths(directory)?; command_build(&paths, target, warnings_as_errors, no_print_progress) } Self::Check { target } => { let paths = find_project_paths(directory)?; command_check(&paths, target) } Self::Docs(Docs::Build { open, target }) => { let paths = find_project_paths(directory)?; docs::build(&paths, docs::BuildOptions { open, target }) } Self::Docs(Docs::Publish) => { let paths = find_project_paths(directory)?; docs::publish(&paths) } Self::Docs(Docs::Remove { package, version }) => docs::remove(package, version), Self::Format { stdin, files, check, } => format::run(stdin, check, files), Self::Fix => { let paths = find_project_paths(directory)?; fix::run(&paths) } Self::Deps(Dependencies::List) => { let paths = find_project_paths(directory)?; dependencies::list(&paths) } Self::Deps(Dependencies::Download) => { let paths = find_project_paths(directory)?; download_dependencies(&paths) } Self::Deps(Dependencies::Outdated) => { let paths = find_project_paths(directory)?; dependencies::outdated(&paths) } Self::Deps(Dependencies::Update(options)) => { let paths = find_project_paths(directory)?; dependencies::update(&paths, options.packages) } Self::Deps(Dependencies::Tree(options)) => { let paths = find_project_paths(directory)?; dependencies::tree(&paths, options) } Self::Hex(Hex::Authenticate) => hex::authenticate(), Self::New(options) => new::create(options, COMPILER_VERSION), Self::Shell => { let paths = find_project_paths(directory)?; shell::command(&paths) } Self::Run { target, arguments, runtime, module, no_print_progress, } => { let paths = find_project_paths(directory)?; run::command( &paths, arguments, target, runtime, module, run::Which::Src, no_print_progress, ) } Self::Test { target, arguments, runtime, } => { let paths = find_project_paths(directory)?; run::command( &paths, arguments, target, runtime, None, run::Which::Test, false, ) } Self::Dev { target, arguments, runtime, no_print_progress, } => { let paths = find_project_paths(directory)?; run::command( &paths, arguments, target, runtime, None, run::Which::Dev, no_print_progress, ) } Self::CompilePackage(opts) => compile_package::command(opts), Self::Publish { replace, yes } => { let paths = find_project_paths(directory)?; publish::command(&paths, replace, yes) } Self::PrintConfig => { let paths = find_project_paths(directory)?; print_config(&paths) } Self::Hex(Hex::Retire { package, version, reason, message, }) => hex::retire(package, version, reason, message), Self::Hex(Hex::Unretire { package, version }) => hex::unretire(package, version), Self::Hex(Hex::Revert { package, version }) => { let paths = find_project_paths(directory)?; hex::revert(&paths, package, version) } Self::Hex(Hex::Owner(Owner::Add { package, username_or_email, level, })) => owner::add(package, username_or_email, level), Self::Hex(Hex::Owner(Owner::Transfer { package, username_or_email, })) => owner::transfer(package, username_or_email), Self::Add { packages, dev } => { let paths = find_project_paths(directory)?; add::command(&paths, packages, dev) } Self::Remove { packages } => { let paths = find_project_paths(directory)?; remove::command(&paths, packages) } Self::Update(options) => { let paths = find_project_paths(directory)?; dependencies::update(&paths, options.packages) } Self::Clean => { let paths = find_project_paths(directory)?; clean(&paths) } Self::LanguageServer => lsp::main(), Self::Export(ExportTarget::ErlangShipment) => { let paths = find_project_paths(directory)?; export::erlang_shipment(&paths) } Self::Export(ExportTarget::Escript) => { let paths = find_project_paths(directory)?; export::escript(&paths) } Self::Export(ExportTarget::HexTarball) => { let paths = find_project_paths(directory)?; export::hex_tarball(&paths) } Self::Export(ExportTarget::JavascriptPrelude) => export::javascript_prelude(), Self::Export(ExportTarget::TypescriptPrelude) => export::typescript_prelude(), Self::Export(ExportTarget::PackageInterface { output }) => { let paths = find_project_paths(directory)?; export::package_interface(&paths, output) } Self::Export(ExportTarget::PackageInformation { output }) => { let paths = find_project_paths(directory)?; export::package_information(&paths, output) } Self::Export(ExportTarget::ZedExtension) => { let paths = find_project_paths(directory)?; export::zed_extension(&paths) } } } } fn template_doc() -> &'static str { "The template to use" } fn target_doc() -> &'static str { "The platform to target" } fn no_print_progress_doc() -> &'static str { "Don't print progress information" } fn runtime_doc() -> &'static str { "The JavaScript runtime to target. This is only available on the \ JavaScript target" } #[derive(Subcommand, Debug, Clone)] pub enum ExportTarget { /// Precompiled Erlang in a single file, suitable for CLIs Escript, /// Precompiled Erlang, suitable for deployment ErlangShipment, /// The package bundled into a tarball, suitable for publishing to Hex HexTarball, /// The JavaScript prelude module JavascriptPrelude, /// The TypeScript prelude module TypescriptPrelude, /// Information on the modules, functions, and types in the project in JSON format PackageInterface { /// The path to write the JSON file to #[arg(long = "out", required = true)] output: Utf8PathBuf, }, /// Package information (gleam.toml) in JSON format PackageInformation { /// The path to write the JSON file to #[arg(long = "out", required = true)] output: Utf8PathBuf, }, /// Build a Zed editor extension component (`extension.wasm`) /// /// Compiles the package for the Wasm target and packages a full /// `zed:extension` world guest (API 0.7.0) with `zed:api-version`. /// Writes `extension.wasm` at the package root. #[command(name = "zed-extension")] ZedExtension, } #[derive(Args, Debug, Clone)] pub struct NewOptions { /// Location of the project root pub project_root: String, /// Name of the project #[arg(long)] pub name: Option, #[arg(long, ignore_case = true, default_value = "erlang", help = template_doc())] pub template: new::Template, /// Skip git initialization and creation of .gitignore, .git/* and .github/* files #[arg(long)] pub skip_git: bool, /// Skip creation of .github/* files #[arg(long)] pub skip_github: bool, } #[derive(Args, Debug)] pub struct CompilePackage { /// The compilation target for the generated project #[arg(long, ignore_case = true, help = target_doc())] target: Target, /// The directory of the Gleam package #[arg(long = "package")] package_directory: Utf8PathBuf, /// A directory to write compiled package to #[arg(long = "out")] output_directory: Utf8PathBuf, /// A directories of precompiled Gleam projects #[arg(long = "lib")] libraries_directory: Utf8PathBuf, /// The location of the JavaScript prelude module, relative to the `out` /// directory. /// /// Required when compiling to JavaScript. /// /// This likely wants to be a `.mjs` file as NodeJS does not permit /// importing of other JavaScript file extensions. /// #[arg(verbatim_doc_comment, long = "javascript-prelude")] javascript_prelude: Option, /// Skip Erlang to BEAM bytecode compilation #[arg(long = "no-beam")] skip_beam_compilation: bool, } #[derive(Subcommand, Debug)] pub enum Dependencies { /// List all dependency packages List, /// Download all dependency packages Download, /// List all outdated dependencies Outdated, /// Update dependency packages to their latest versions Update(UpdateOptions), /// Tree of all the dependency packages Tree(TreeOptions), } #[derive(Subcommand, Debug)] pub enum Hex { /// Retire a release from Hex /// /// This command uses the environment variable: /// /// - HEXPM_API_KEY: (optional) A Hex API key to authenticate with the Hex package manager. /// #[command(verbatim_doc_comment)] Retire { /// The name of the package to retire #[arg(long)] package: String, /// The version to retire #[arg(long)] version: String, /// The reason for the retirement #[arg(long)] reason: RetirementReason, message: Option, }, /// Un-retire a release from Hex /// /// This command uses this environment variable: /// /// - HEXPM_API_KEY: (optional) A Hex API key to authenticate with the Hex package manager. /// #[command(verbatim_doc_comment)] Unretire { /// The name of the package to unretire #[arg(long)] package: String, /// The version to unretire #[arg(long)] version: String, }, /// Revert a release, removing it from Hex. /// /// Releases can only be reverted within 24 hours since they were published. /// /// This command uses this environment variable: /// /// - HEXPM_API_KEY: (optional) A Hex API key to authenticate with the Hex package manager. /// #[command(verbatim_doc_comment)] Revert { /// The name of the package to revert #[arg(long)] package: Option, /// The version to revert #[arg(long)] version: Option, }, /// Deal with package ownership #[command(subcommand)] Owner(Owner), /// Log in to Hex. Replaces the credentials with new ones if already logged in. Authenticate, } #[derive(Subcommand, Debug)] pub enum Owner { /// Adds a new owner to the given package on Hex /// /// This command uses this environment variable: /// /// - HEXPM_API_KEY: (optional) A Hex API key to authenticate against the Hex package manager. /// #[command(verbatim_doc_comment)] Add { #[arg(long)] package: String, /// The username or email of the additional owner #[arg(long = "user")] username_or_email: String, /// The ownership level #[arg(long, default_value = "maintainer")] level: hexpm::OwnerLevel, }, /// Transfers ownership of the given package to a new Hex user /// /// This command uses this environment variable: /// /// - HEXPM_API_KEY: (optional) A Hex API key to authenticate against the Hex package manager. /// #[command(verbatim_doc_comment)] Transfer { /// The name of the package #[arg(long)] package: String, /// The username or email of the new owner #[arg(long = "user")] username_or_email: String, }, } #[derive(Subcommand, Debug)] pub enum Docs { /// Render HTML docs locally Build { /// Opens the docs in a browser after rendering #[arg(long)] open: bool, /// Which compilation target to use #[arg(short, long, ignore_case = true, help = target_doc())] target: Option, }, /// Publish HTML docs to HexDocs /// /// This command uses this environment variable: /// /// - HEXPM_API_KEY: (optional) A Hex API key to authenticate with the Hex package manager. /// #[command(verbatim_doc_comment)] Publish, /// Remove HTML docs from HexDocs /// /// This command uses this environment variable: /// /// - HEXPM_API_KEY: (optional) A Hex API key to authenticate with the Hex package manager. /// #[command(verbatim_doc_comment)] Remove { /// The name of the package #[arg(long)] package: String, /// The version of the docs to remove #[arg(long)] version: String, }, } pub fn main() { initialise_logger(); panic::add_handler(); let stderr = cli::stderr_buffer_writer(); let result = get_current_directory() .and_then(|working_directory| Command::parse().run(working_directory)); match result { Ok(_) => { tracing::info!("Successfully completed"); } Err(error) => { tracing::error!(error = ?error, "Failed"); let mut buffer = stderr.buffer(); error.pretty(&mut buffer); stderr.print(&buffer).expect("Final result error writing"); std::process::exit(1); } } } fn command_check(paths: &ProjectPaths, target: Option) -> Result<()> { let _ = build::main( paths, Options { root_target_support: TargetSupport::Enforced, warnings_as_errors: false, codegen: Codegen::DepsOnly, compile: Compile::All, mode: Mode::Dev, target, no_print_progress: false, }, build::download_dependencies(paths, cli::Reporter::new())?, )?; Ok(()) } fn command_build( paths: &ProjectPaths, target: Option, warnings_as_errors: bool, no_print_progress: bool, ) -> Result<()> { let manifest = if no_print_progress { build::download_dependencies(paths, NullTelemetry)? } else { build::download_dependencies(paths, cli::Reporter::new())? }; let _ = build::main( paths, Options { root_target_support: TargetSupport::Enforced, warnings_as_errors, codegen: Codegen::All, compile: Compile::All, mode: Mode::Dev, target, no_print_progress, }, manifest, )?; Ok(()) } fn print_config(paths: &ProjectPaths) -> Result<()> { let config = root_config(paths)?; println!("{config:#?}"); Ok(()) } fn clean(paths: &ProjectPaths) -> Result<()> { fs::delete_directory(&paths.build_directory()) } fn initialise_logger() { let enable_colours = std::env::var("GLEAM_LOG_NOCOLOUR").is_err(); tracing_subscriber::fmt() .with_writer(std::io::stderr) .with_env_filter(std::env::var("GLEAM_LOG").unwrap_or_else(|_| "off".into())) .with_target(false) .with_ansi(enable_colours) .without_time() .init(); } fn find_project_paths(current_dir: Utf8PathBuf) -> Result { get_project_root(current_dir).map(ProjectPaths::new) } #[cfg(test)] fn project_paths_at_current_directory_without_toml() -> ProjectPaths { let current_dir = get_current_directory().expect("Failed to get current directory"); ProjectPaths::new(current_dir) } fn download_dependencies(paths: &ProjectPaths) -> Result<()> { _ = dependencies::resolve_and_download( paths, cli::Reporter::new(), None, Vec::new(), dependencies::DependencyManagerConfig { use_manifest: dependencies::UseManifest::Yes, check_major_versions: dependencies::CheckMajorVersions::No, }, )?; Ok(()) }