Fork of daniellemaywood.uk/gleam — Wasm codegen work
2

Configure Feed

Select the types of activity you want to include in your feed.

gleam / compiler-cli / src / export.rs
10 kB 328 lines
1// SPDX-License-Identifier: Apache-2.0 2// SPDX-FileCopyrightText: 2022 The Gleam contributors 3 4use crate::fs::{self, ZipArchive}; 5use camino::Utf8PathBuf; 6use gleam_core::{ 7 Error, Result, 8 analyse::TargetSupport, 9 build::{Codegen, Compile, Mode, Options, Target}, 10 paths::ProjectPaths, 11 type_::ModuleFunction, 12}; 13use std::io::Cursor; 14 15static ENTRYPOINT_FILENAME_POWERSHELL: &str = "entrypoint.ps1"; 16static ENTRYPOINT_FILENAME_POSIX_SHELL: &str = "entrypoint.sh"; 17 18static ENTRYPOINT_TEMPLATE_POWERSHELL: &str = 19 include_str!("../templates/erlang-shipment-entrypoint.ps1"); 20static ENTRYPOINT_TEMPLATE_POSIX_SHELL: &str = 21 include_str!("../templates/erlang-shipment-entrypoint.sh"); 22 23/// Generate a single file of precompiled Erlang, suitable for CLIs. 24/// 25pub fn escript(paths: &ProjectPaths) -> Result<()> { 26 let target = Target::Erlang; 27 let mode = Mode::Prod; 28 let build = paths.build_directory_for_target(mode, target); 29 30 // Reset the directories to ensure we have a clean slate and no old code 31 fs::delete_directory(&build)?; 32 33 let manifest = crate::build::download_dependencies(paths, crate::cli::Reporter::new())?; 34 35 // Build project in production mode 36 let build_options = Options { 37 root_target_support: TargetSupport::Enforced, 38 warnings_as_errors: false, 39 codegen: Codegen::All, 40 compile: Compile::All, 41 mode, 42 target: Some(target), 43 no_print_progress: false, 44 }; 45 let built = crate::build::main(paths, build_options, manifest)?; 46 let package_name = &built.root_package.config.name; 47 48 // The main function must exist for the escript to call. This will return an 49 // error if it could not be found. 50 let _: ModuleFunction = built.get_main_function(package_name, target)?; 51 52 // Create the zip archive for the code 53 let mut zip = ZipArchive::new(Cursor::new(Vec::new())); 54 55 for entry in fs::read_dir(&build)?.filter_map(Result::ok) { 56 let ebin = entry.path().join("ebin"); 57 58 // We want the ebin code directories for each package 59 if !ebin.is_dir() { 60 continue; 61 } 62 63 for entry in fs::read_dir(&ebin)?.filter_map(Result::ok) { 64 let path = entry.path(); 65 let extension = path.extension().unwrap_or_default(); 66 67 let Some(name) = path.file_name() else { 68 continue; 69 }; 70 71 if !path.is_file() { 72 continue; 73 } 74 75 // We want to copy compiled BEAM bytecode and app configuration files 76 if extension != "beam" && extension != "app" { 77 continue; 78 } 79 80 zip.add_file_from_disc(path, name)?; 81 } 82 } 83 84 let zip = zip.finish()?.into_inner(); 85 86 let escript_path = paths.root().join(package_name.as_str()); 87 let mut file = fs::open_file(&escript_path)?; 88 89 // The -escript flag in the header instructs the BEAM `escript` program 90 // to run the regular Gleam entrypoint module when running this escript. 91 let header = format!( 92 "#!/usr/bin/env escript 93%% 94%%!-escript main {package_name}@@main 95" 96 ); 97 98 fs::write_to_open_file(&mut file, &escript_path, header)?; 99 fs::write_to_open_file(&mut file, &escript_path, zip)?; 100 fs::make_executable(&escript_path)?; 101 102 // Windows shells largely do not use shebangs, so for the escript to be 103 // directly executable a .cmd wrapper script is provided. 104 if cfg!(windows) { 105 let cmd_path = escript_path.with_extension("cmd"); 106 fs::write(&cmd_path, "@echo off\r\nescript.exe \"%~dpn0\" %*\r\n")?; 107 } 108 109 println!( 110 " 111Your escript has been generated to {escript_path}. 112", 113 ); 114 115 Ok(()) 116} 117 118/// Generate a directory of precompiled Erlang along with a start script. 119/// Suitable for deployment to a server. 120/// 121/// For each Erlang application (aka package) directory these directories are 122/// copied across: 123/// - ebin 124/// - include 125/// - priv 126pub(crate) fn erlang_shipment(paths: &ProjectPaths) -> Result<()> { 127 let target = Target::Erlang; 128 let mode = Mode::Prod; 129 let build = paths.build_directory_for_target(mode, target); 130 let out = paths.erlang_shipment_directory(); 131 132 fs::mkdir(&out)?; 133 134 // Reset the directories to ensure we have a clean slate and no old code 135 fs::delete_directory(&build)?; 136 fs::delete_directory(&out)?; 137 138 // Build project in production mode 139 let built = crate::build::main( 140 paths, 141 Options { 142 root_target_support: TargetSupport::Enforced, 143 warnings_as_errors: false, 144 codegen: Codegen::All, 145 compile: Compile::All, 146 mode, 147 target: Some(target), 148 no_print_progress: false, 149 }, 150 crate::build::download_dependencies(paths, crate::cli::Reporter::new())?, 151 )?; 152 153 for entry in fs::read_dir(&build)?.filter_map(Result::ok) { 154 let path = entry.path(); 155 156 // We are only interested in package directories 157 if !path.is_dir() { 158 continue; 159 } 160 161 let name = path.file_name().expect("Directory name"); 162 let build = build.join(name); 163 let out = out.join(name); 164 fs::mkdir(&out)?; 165 166 // Copy desired package subdirectories 167 for subdirectory in ["ebin", "priv", "include"] { 168 let source = build.join(subdirectory); 169 if source.is_dir() { 170 let source = fs::canonicalise(&source)?; 171 let out = out.join(subdirectory); 172 fs::copy_dir(source, &out)?; 173 } 174 } 175 } 176 177 // PowerShell entry point script. 178 write_entrypoint_script( 179 &out.join(ENTRYPOINT_FILENAME_POWERSHELL), 180 ENTRYPOINT_TEMPLATE_POWERSHELL, 181 &built.root_package.config.name, 182 )?; 183 184 // POSIX Shell entry point script. 185 write_entrypoint_script( 186 &out.join(ENTRYPOINT_FILENAME_POSIX_SHELL), 187 ENTRYPOINT_TEMPLATE_POSIX_SHELL, 188 &built.root_package.config.name, 189 )?; 190 191 crate::cli::print_exported(&built.root_package.config.name); 192 193 println!( 194 " 195Your Erlang shipment has been generated to {out}. 196 197It can be copied to a compatible server with Erlang installed and run with 198one of the following scripts: 199 - {ENTRYPOINT_FILENAME_POWERSHELL} (PowerShell script) 200 - {ENTRYPOINT_FILENAME_POSIX_SHELL} (POSIX Shell script) 201", 202 ); 203 204 Ok(()) 205} 206 207fn write_entrypoint_script( 208 entrypoint_output_path: &Utf8PathBuf, 209 entrypoint_template_path: &str, 210 package_name: &str, 211) -> Result<()> { 212 let text = entrypoint_template_path.replace("$PACKAGE_NAME_FROM_GLEAM", package_name); 213 fs::write(entrypoint_output_path, &text)?; 214 fs::make_executable(entrypoint_output_path)?; 215 Ok(()) 216} 217 218pub fn hex_tarball(paths: &ProjectPaths) -> Result<()> { 219 let mut config = crate::config::root_config(paths)?; 220 let data: Vec<u8> = crate::publish::build_hex_tarball(paths, &mut config)?; 221 222 let path = paths.build_export_hex_tarball(&config.name, &config.version.to_string()); 223 fs::write_bytes(&path, &data)?; 224 println!( 225 " 226Your hex tarball has been generated in {}. 227", 228 path 229 ); 230 Ok(()) 231} 232 233pub fn javascript_prelude() -> Result<()> { 234 print!("{}", gleam_core::javascript::PRELUDE); 235 Ok(()) 236} 237 238pub fn typescript_prelude() -> Result<()> { 239 print!("{}", gleam_core::javascript::PRELUDE_TS_DEF); 240 Ok(()) 241} 242 243pub fn package_interface(paths: &ProjectPaths, out: Utf8PathBuf) -> Result<()> { 244 // Build the project 245 let mut built = crate::build::main( 246 paths, 247 Options { 248 mode: Mode::Prod, 249 target: None, 250 codegen: Codegen::None, 251 compile: Compile::All, 252 warnings_as_errors: false, 253 root_target_support: TargetSupport::Enforced, 254 no_print_progress: false, 255 }, 256 crate::build::download_dependencies(paths, crate::cli::Reporter::new())?, 257 )?; 258 built.root_package.attach_doc_and_module_comments(); 259 260 let out = gleam_core::docs::generate_json_package_interface( 261 out, 262 &built.root_package, 263 &built.module_interfaces, 264 ); 265 fs::write_outputs_under(&[out], paths.root())?; 266 Ok(()) 267} 268 269pub fn package_information(paths: &ProjectPaths, out: Utf8PathBuf) -> Result<()> { 270 let config = crate::config::root_config(paths)?; 271 let out = gleam_core::docs::generate_json_package_information(out, config); 272 fs::write_outputs_under(&[out], paths.root())?; 273 Ok(()) 274} 275 276/// Build a Zed extension component (`extension.wasm`) for the current package. 277/// 278/// Typechecks with `target = "wasm"`, then packages a full `zed:extension` 279/// world guest (API 0.7.0) including `language-server-command` and defaults 280/// for every other world export. 281pub fn zed_extension(paths: &ProjectPaths) -> Result<()> { 282 use gleam_core::error::ShellCommandFailureReason; 283 use gleam_core::wasm; 284 285 let built = crate::build::main( 286 paths, 287 Options { 288 mode: Mode::Prod, 289 target: Some(Target::Wasm), 290 codegen: Codegen::All, 291 compile: Compile::All, 292 warnings_as_errors: false, 293 root_target_support: TargetSupport::Enforced, 294 no_print_progress: false, 295 }, 296 crate::build::download_dependencies(paths, crate::cli::Reporter::new())?, 297 )?; 298 299 let package_name = built.root_package.config.name.to_string(); 300 301 // Translate typed modules → monomorphised MIR → component. 302 let mir_modules: Vec<_> = built 303 .root_package 304 .modules 305 .iter() 306 .map(|module| wasm::mir::Translator::new(&module.ast).translate()) 307 .collect(); 308 309 let component = wasm::zed::build_from_mir_modules(mir_modules, &package_name).map_err( 310 |error| Error::ShellCommand { 311 program: "zed-extension".into(), 312 reason: ShellCommandFailureReason::ShellCommandError(error.to_string()), 313 }, 314 )?; 315 316 let out = paths.root().join("extension.wasm"); 317 fs::write_bytes(&out, &component)?; 318 crate::cli::print_exported(&package_name); 319 println!( 320 " 321Zed extension component written to {out} 322 323Install as a Zed Dev Extension (no Cargo.toml in the package so Zed will not 324try a Rust rebuild). Requires zed:api-version 0.7.0. 325" 326 ); 327 Ok(()) 328}