1use std::{
29 env, process::{Command, self}, fs, path::{PathBuf, Path}, hash::{Hash, Hasher},
30 collections::hash_map::DefaultHasher,
31};
32
33const SKIP_BUILD_ENV: &str = "SKIP_WASM_BUILD";
35
36const DUMMY_WASM_BINARY_ENV: &str = "BUILD_DUMMY_WASM_BINARY";
44
45const TRIGGER_WASM_BUILD_ENV: &str = "TRIGGER_WASM_BUILD";
47
48fn replace_back_slashes<T: ToString>(path: T) -> String {
50 path.to_string().replace("\\", "/")
51}
52
53fn get_manifest_dir() -> PathBuf {
55 env::var("CARGO_MANIFEST_DIR")
56 .expect("`CARGO_MANIFEST_DIR` is always set for `build.rs` files; qed")
57 .into()
58}
59
60pub struct WasmBuilderSelectProject {
62 _ignore: (),
65}
66
67impl WasmBuilderSelectProject {
68 pub fn with_current_project(self) -> WasmBuilderSelectSource {
75 WasmBuilderSelectSource(get_manifest_dir().join("Cargo.toml"))
76 }
77
78 pub fn with_project(
82 self,
83 path: impl Into<PathBuf>,
84 ) -> Result<WasmBuilderSelectSource, &'static str> {
85 let path = path.into();
86
87 if path.ends_with("Cargo.toml") {
88 Ok(WasmBuilderSelectSource(path))
89 } else {
90 Err("Project path must point to the `Cargo.toml` of the project")
91 }
92 }
93}
94
95pub struct WasmBuilderSelectSource(PathBuf);
97
98impl WasmBuilderSelectSource {
99 pub fn with_wasm_builder_from_path(self, path: &'static str) -> WasmBuilder {
104 WasmBuilder {
105 source: WasmBuilderSource::Path(path),
106 rust_flags: Vec::new(),
107 file_name: None,
108 project_cargo_toml: self.0,
109 }
110 }
111
112 pub fn with_wasm_builder_from_git(self, repo: &'static str, rev: &'static str) -> WasmBuilder {
114 WasmBuilder {
115 source: WasmBuilderSource::Git { repo, rev },
116 rust_flags: Vec::new(),
117 file_name: None,
118 project_cargo_toml: self.0,
119 }
120 }
121
122 pub fn with_wasm_builder_from_crates(self, version: &'static str) -> WasmBuilder {
124 WasmBuilder {
125 source: WasmBuilderSource::Crates(version),
126 rust_flags: Vec::new(),
127 file_name: None,
128 project_cargo_toml: self.0,
129 }
130 }
131
132 pub fn with_wasm_builder_from_crates_or_path(
138 self,
139 version: &'static str,
140 path: &'static str,
141 ) -> WasmBuilder {
142 WasmBuilder {
143 source: WasmBuilderSource::CratesOrPath { version, path },
144 rust_flags: Vec::new(),
145 file_name: None,
146 project_cargo_toml: self.0,
147 }
148 }
149
150 pub fn with_wasm_builder_source(self, source: WasmBuilderSource) -> WasmBuilder {
152 WasmBuilder {
153 source,
154 rust_flags: Vec::new(),
155 file_name: None,
156 project_cargo_toml: self.0,
157 }
158 }
159}
160
161pub struct WasmBuilder {
175 source: WasmBuilderSource,
177 rust_flags: Vec<String>,
179 file_name: Option<String>,
183 project_cargo_toml: PathBuf,
186}
187
188impl WasmBuilder {
189 pub fn new() -> WasmBuilderSelectProject {
191 WasmBuilderSelectProject {
192 _ignore: (),
193 }
194 }
195
196 pub fn export_heap_base(mut self) -> Self {
200 self.rust_flags.push("-Clink-arg=--export=__heap_base".into());
201 self
202 }
203
204 pub fn set_file_name(mut self, file_name: impl Into<String>) -> Self {
210 self.file_name = Some(file_name.into());
211 self
212 }
213
214 pub fn import_memory(mut self) -> Self {
218 self.rust_flags.push("-C link-arg=--import-memory".into());
219 self
220 }
221
222 pub fn append_to_rust_flags(mut self, flag: impl Into<String>) -> Self {
226 self.rust_flags.push(flag.into());
227 self
228 }
229
230 pub fn build(self) {
232 if check_skip_build() {
233 generate_rerun_if_changed_instructions();
236 return;
237 }
238
239 let out_dir = PathBuf::from(env::var("OUT_DIR").expect("`OUT_DIR` is set by cargo!"));
240 let file_path = out_dir.join(self.file_name.unwrap_or_else(|| "wasm_binary.rs".into()));
241
242 let mut hasher = DefaultHasher::new();
244 self.project_cargo_toml.hash(&mut hasher);
245
246 let project_name = env::var("CARGO_PKG_NAME").expect("`CARGO_PKG_NAME` is set by cargo!");
247 let project_folder = get_workspace_root()
251 .join(format!("{}{}", project_name, hasher.finish()));
252
253 if check_provide_dummy_wasm_binary() {
254 provide_dummy_wasm_binary(&file_path);
255 } else {
256 create_project(
257 &project_folder,
258 &file_path,
259 self.source,
260 &self.project_cargo_toml,
261 &self.rust_flags.into_iter().map(|f| format!("{} ", f)).collect::<String>(),
262 );
263 run_project(&project_folder);
264 }
265
266 generate_rerun_if_changed_instructions();
269 }
270}
271
272pub enum WasmBuilderSource {
274 Path(&'static str),
276 Git {
278 repo: &'static str,
279 rev: &'static str,
280 },
281 Crates(&'static str),
283 CratesOrPath {
285 version: &'static str,
286 path: &'static str,
287 }
288}
289
290impl WasmBuilderSource {
291 fn to_cargo_source(&self, manifest_dir: &Path) -> String {
295 match self {
296 WasmBuilderSource::Path(path) => {
297 replace_back_slashes(format!("path = \"{}\"", manifest_dir.join(path).display()))
298 }
299 WasmBuilderSource::Git { repo, rev } => {
300 format!("git = \"{}\", rev=\"{}\"", repo, rev)
301 }
302 WasmBuilderSource::Crates(version) => {
303 format!("version = \"{}\"", version)
304 }
305 WasmBuilderSource::CratesOrPath { version, path } => {
306 replace_back_slashes(
307 format!(
308 "path = \"{}\", version = \"{}\"",
309 manifest_dir.join(path).display(),
310 version
311 )
312 )
313 }
314 }
315 }
316}
317
318#[deprecated(
322 since = "1.0.5",
323 note = "Please switch to [`WasmBuilder`]",
324)]
325pub fn build_current_project_with_rustflags(
326 file_name: &str,
327 wasm_builder_source: WasmBuilderSource,
328 default_rust_flags: &str,
329) {
330 WasmBuilder::new()
331 .with_current_project()
332 .with_wasm_builder_source(wasm_builder_source)
333 .append_to_rust_flags(default_rust_flags)
334 .set_file_name(file_name)
335 .build()
336}
337
338#[deprecated(
346 since = "1.0.5",
347 note = "Please switch to [`WasmBuilder`]",
348)]
349pub fn build_current_project(file_name: &str, wasm_builder_source: WasmBuilderSource) {
350 #[allow(deprecated)]
351 build_current_project_with_rustflags(file_name, wasm_builder_source, "");
352}
353
354fn get_workspace_root() -> PathBuf {
358 let out_dir_env = env::var("OUT_DIR").expect("`OUT_DIR` is set by cargo!");
359 let mut out_dir = PathBuf::from(&out_dir_env);
360
361 loop {
362 match out_dir.parent() {
363 Some(parent) if out_dir.ends_with("build") => return parent.join("wbuild-runner"),
364 _ => if !out_dir.pop() {
365 break;
366 }
367 }
368 }
369
370 panic!("Could not find target dir in: {}", out_dir_env)
371}
372
373fn create_project(
374 project_folder: &Path,
375 file_path: &Path,
376 wasm_builder_source: WasmBuilderSource,
377 cargo_toml_path: &Path,
378 default_rustflags: &str,
379) {
380 fs::create_dir_all(project_folder.join("src"))
381 .expect("WASM build runner dir create can not fail; qed");
382
383 fs::write(
384 project_folder.join("Cargo.toml"),
385 format!(
386 r#"
387 [package]
388 name = "wasm-build-runner-impl"
389 version = "1.0.0"
390 edition = "2018"
391
392 [dependencies]
393
394 wasm-builder = {{ {wasm_builder_source} }}
395
396 [workspace]
397 "#,
398 wasm_builder_source = wasm_builder_source.to_cargo_source(&get_manifest_dir()),
399 )
400 ).expect("WASM build runner `Cargo.toml` writing can not fail; qed");
401
402 fs::write(
403 project_folder.join("src/main.rs"),
404 format!(
405 r#"
406 use wasm_builder::build_project_with_default_rustflags;
407
408 fn main() {{
409 build_project_with_default_rustflags(
410 "{file_path}",
411 "{cargo_toml_path}",
412 "{default_rustflags}",
413 )
414 }}
415 "#,
416 file_path = replace_back_slashes(file_path.display()),
417 cargo_toml_path = replace_back_slashes(cargo_toml_path.display()),
418 default_rustflags = default_rustflags,
419 )
420 ).expect("WASM build runner `main.rs` writing can not fail; qed");
421}
422
423fn run_project(project_folder: &Path) {
424 let cargo = env::var("CARGO").expect("`CARGO` env variable is always set when executing `build.rs`.");
425 let mut cmd = Command::new(cargo);
426 cmd.arg("run").arg(format!("--manifest-path={}", project_folder.join("Cargo.toml").display()));
427
428 if env::var("DEBUG") != Ok(String::from("true")) {
429 cmd.arg("--release");
430 }
431
432 cmd.env_remove("CARGO_TARGET_DIR");
436
437 if !cmd.status().map(|s| s.success()).unwrap_or(false) {
438 process::exit(1);
440 }
441}
442
443fn generate_crate_skip_build_env_name() -> String {
445 format!(
446 "SKIP_{}_WASM_BUILD",
447 env::var("CARGO_PKG_NAME").expect("Package name is set").to_uppercase().replace('-', "_"),
448 )
449}
450
451fn check_skip_build() -> bool {
453 env::var(SKIP_BUILD_ENV).is_ok() || env::var(generate_crate_skip_build_env_name()).is_ok()
454}
455
456fn check_provide_dummy_wasm_binary() -> bool {
458 env::var(DUMMY_WASM_BINARY_ENV).is_ok()
459}
460
461fn provide_dummy_wasm_binary(file_path: &Path) {
463 fs::write(
464 file_path,
465 "pub const WASM_BINARY: &[u8] = &[]; pub const WASM_BINARY_BLOATY: &[u8] = &[];",
466 ).expect("Writing dummy WASM binary should not fail");
467}
468
469fn generate_rerun_if_changed_instructions() {
472 println!("cargo:rerun-if-env-changed={}", SKIP_BUILD_ENV);
474 println!("cargo:rerun-if-env-changed={}", DUMMY_WASM_BINARY_ENV);
475 println!("cargo:rerun-if-env-changed={}", TRIGGER_WASM_BUILD_ENV);
476 println!("cargo:rerun-if-env-changed={}", generate_crate_skip_build_env_name());
477}