Fork of daniellemaywood.uk/gleam — Wasm codegen work
8.5 kB
274 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 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}