referrerpolicy=no-referrer-when-downgrade

substrate_wasm_builder/
wasm_project.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18#[cfg(feature = "metadata-hash")]
19use crate::builder::MetadataExtraInfo;
20use crate::{write_file_if_changed, CargoCommandVersioned, RuntimeTarget, OFFLINE};
21
22use build_helper::rerun_if_changed;
23use cargo_metadata::{DependencyKind, Metadata, MetadataCommand};
24use console::style;
25use parity_wasm::elements::{deserialize_buffer, Module};
26use std::{
27	borrow::ToOwned,
28	collections::HashSet,
29	env, fs,
30	hash::{Hash, Hasher},
31	ops::Deref,
32	path::{Path, PathBuf},
33	process,
34	sync::OnceLock,
35};
36use strum::{EnumIter, IntoEnumIterator};
37use toml::value::Table;
38use walkdir::WalkDir;
39
40/// Colorize an info message.
41///
42/// Returns the colorized message.
43fn colorize_info_message(message: &str) -> String {
44	if super::color_output_enabled() {
45		style(message).yellow().bold().to_string()
46	} else {
47		message.into()
48	}
49}
50
51/// Holds the path to the bloaty WASM binary.
52pub struct WasmBinaryBloaty(PathBuf);
53
54impl WasmBinaryBloaty {
55	/// Returns the escaped path to the bloaty binary.
56	pub fn bloaty_path_escaped(&self) -> String {
57		self.0.display().to_string().escape_default().to_string()
58	}
59
60	/// Returns the path to the binary.
61	pub fn bloaty_path(&self) -> &Path {
62		&self.0
63	}
64}
65
66/// Holds the path to the WASM binary.
67pub struct WasmBinary(PathBuf);
68
69impl WasmBinary {
70	/// Returns the path to the wasm binary.
71	pub fn wasm_binary_path(&self) -> &Path {
72		&self.0
73	}
74
75	/// Returns the escaped path to the wasm binary.
76	pub fn wasm_binary_path_escaped(&self) -> String {
77		self.0.display().to_string().escape_default().to_string()
78	}
79}
80
81fn crate_metadata(cargo_manifest: &Path) -> Metadata {
82	let mut cargo_lock = cargo_manifest.to_path_buf();
83	cargo_lock.set_file_name("Cargo.lock");
84
85	let cargo_lock_existed = cargo_lock.exists();
86
87	// If we can find a `Cargo.lock`, we assume that this is the workspace root and there exists a
88	// `Cargo.toml` that we can use for getting the metadata.
89	let cargo_manifest = if let Some(mut cargo_lock) = find_cargo_lock(cargo_manifest) {
90		cargo_lock.set_file_name("Cargo.toml");
91		cargo_lock
92	} else {
93		cargo_manifest.to_path_buf()
94	};
95
96	let crate_metadata_command = create_metadata_command(cargo_manifest);
97
98	let crate_metadata = crate_metadata_command
99		.exec()
100		.expect("`cargo metadata` can not fail on project `Cargo.toml`; qed");
101	// If the `Cargo.lock` didn't exist, we need to remove it after
102	// calling `cargo metadata`. This is required to ensure that we don't change
103	// the build directory outside of the `target` folder. Commands like
104	// `cargo publish` require this.
105	if !cargo_lock_existed {
106		let _ = fs::remove_file(&cargo_lock);
107	}
108
109	crate_metadata
110}
111
112/// Keep the build directories separate so that when switching between the
113/// targets we won't trigger unnecessary rebuilds.
114fn build_subdirectory(target: RuntimeTarget) -> &'static str {
115	match target {
116		RuntimeTarget::Wasm => "wbuild",
117		RuntimeTarget::Riscv => "rbuild",
118	}
119}
120
121/// Creates the WASM project, compiles the WASM binary and compacts the WASM binary.
122///
123/// # Returns
124///
125/// The path to the compact runtime binary and the bloaty runtime binary.
126pub(crate) fn create_and_compile(
127	target: RuntimeTarget,
128	orig_project_cargo_toml: &Path,
129	default_rustflags: &str,
130	cargo_cmd: CargoCommandVersioned,
131	features_to_enable: Vec<String>,
132	blob_out_name_override: Option<String>,
133	check_for_runtime_version_section: bool,
134	#[cfg(feature = "metadata-hash")] enable_metadata_hash: Option<MetadataExtraInfo>,
135) -> (Option<WasmBinary>, WasmBinaryBloaty) {
136	let runtime_workspace_root = get_wasm_workspace_root();
137	let runtime_workspace = runtime_workspace_root.join(build_subdirectory(target));
138
139	let crate_metadata = crate_metadata(orig_project_cargo_toml);
140
141	let project = create_project(
142		target,
143		orig_project_cargo_toml,
144		&runtime_workspace,
145		&crate_metadata,
146		crate_metadata.workspace_root.as_ref(),
147		features_to_enable,
148	);
149	let wasm_project_cargo_toml = project.join("Cargo.toml");
150
151	let build_config = BuildConfiguration::detect(target, &project);
152
153	#[cfg(feature = "metadata-hash")]
154	let raw_blob_path = match enable_metadata_hash {
155		Some(extra_info) => {
156			// When the metadata hash is enabled we need to build the runtime twice.
157			let raw_blob_path = build_bloaty_blob(
158				target,
159				&build_config.blob_build_profile,
160				&project,
161				default_rustflags,
162				cargo_cmd.clone(),
163				None,
164			);
165
166			let hash = crate::metadata_hash::generate_metadata_hash(&raw_blob_path, extra_info);
167
168			build_bloaty_blob(
169				target,
170				&build_config.blob_build_profile,
171				&project,
172				default_rustflags,
173				cargo_cmd,
174				Some(hash),
175			)
176		},
177		None => build_bloaty_blob(
178			target,
179			&build_config.blob_build_profile,
180			&project,
181			default_rustflags,
182			cargo_cmd,
183			None,
184		),
185	};
186
187	// If the feature is not enabled, we only need to do it once.
188	#[cfg(not(feature = "metadata-hash"))]
189	let raw_blob_path = {
190		build_bloaty_blob(
191			target,
192			&build_config.blob_build_profile,
193			&project,
194			default_rustflags,
195			cargo_cmd,
196		)
197	};
198
199	let blob_name =
200		blob_out_name_override.unwrap_or_else(|| get_blob_name(target, &wasm_project_cargo_toml));
201
202	let (final_blob_binary, bloaty_blob_binary) = match target {
203		RuntimeTarget::Wasm => {
204			let out_path = project.join(format!("{blob_name}.wasm"));
205			fs::copy(raw_blob_path, &out_path).expect("copying the runtime blob should never fail");
206
207			maybe_compact_and_compress_wasm(
208				&wasm_project_cargo_toml,
209				&project,
210				WasmBinaryBloaty(out_path),
211				&blob_name,
212				check_for_runtime_version_section,
213				&build_config,
214			)
215		},
216		RuntimeTarget::Riscv => {
217			let out_path = project.join(format!("{blob_name}.polkavm"));
218			fs::copy(raw_blob_path, &out_path).expect("copying the runtime blob should never fail");
219			(None, WasmBinaryBloaty(out_path))
220		},
221	};
222
223	generate_rerun_if_changed_instructions(
224		orig_project_cargo_toml,
225		&project,
226		&runtime_workspace,
227		final_blob_binary.as_ref(),
228		&bloaty_blob_binary,
229	);
230
231	if let Err(err) = adjust_mtime(&bloaty_blob_binary, final_blob_binary.as_ref()) {
232		build_helper::warning!("Error while adjusting the mtime of the blob binaries: {}", err)
233	}
234
235	(final_blob_binary, bloaty_blob_binary)
236}
237
238fn maybe_compact_and_compress_wasm(
239	wasm_project_cargo_toml: &Path,
240	project: &Path,
241	bloaty_blob_binary: WasmBinaryBloaty,
242	blob_name: &str,
243	check_for_runtime_version_section: bool,
244	build_config: &BuildConfiguration,
245) -> (Option<WasmBinary>, WasmBinaryBloaty) {
246	// Try to compact and compress the bloaty blob, if the *outer* profile wants it.
247	//
248	// This is because, by default the inner profile will be set to `Release` even when the outer
249	// profile is `Debug`, because the blob built in `Debug` profile is too slow for normal
250	// development activities.
251	let (compact_blob_path, compact_compressed_blob_path) =
252		if build_config.outer_build_profile.wants_compact() {
253			let compact_blob_path = compact_wasm(&project, blob_name, &bloaty_blob_binary);
254			let compact_compressed_blob_path =
255				compact_blob_path.as_ref().and_then(|p| try_compress_blob(&p.0, blob_name));
256			(compact_blob_path, compact_compressed_blob_path)
257		} else {
258			// We at least want to lower the `sign-ext` code to `mvp`.
259			wasm_opt::OptimizationOptions::new_opt_level_0()
260				.add_pass(wasm_opt::Pass::SignextLowering)
261				.debug_info(true)
262				.run(bloaty_blob_binary.bloaty_path(), bloaty_blob_binary.bloaty_path())
263				.expect("Failed to lower sign-ext in WASM binary.");
264
265			(None, None)
266		};
267
268	if check_for_runtime_version_section {
269		ensure_runtime_version_wasm_section_exists(bloaty_blob_binary.bloaty_path());
270	}
271
272	let final_blob_binary = compact_compressed_blob_path.or(compact_blob_path);
273
274	final_blob_binary
275		.as_ref()
276		.map(|binary| copy_blob_to_target_directory(wasm_project_cargo_toml, binary));
277
278	(final_blob_binary, bloaty_blob_binary)
279}
280
281/// Ensures that the `runtime_version` section exists in the given blob.
282///
283/// If the section can not be found, it will print an error and exit the builder.
284fn ensure_runtime_version_wasm_section_exists(blob_path: &Path) {
285	let blob = fs::read(blob_path).expect("`{blob_path}` was just written and should exist; qed");
286
287	let module: Module = match deserialize_buffer(&blob) {
288		Ok(m) => m,
289		Err(e) => {
290			println!("Failed to deserialize `{}`: {e:?}", blob_path.display());
291			process::exit(1);
292		},
293	};
294
295	if !module.custom_sections().any(|cs| cs.name() == "runtime_version") {
296		println!(
297			"Couldn't find the `runtime_version` section. \
298				  Please ensure that you are using the `sp_version::runtime_version` attribute macro!"
299		);
300		process::exit(1);
301	}
302}
303
304/// Adjust the mtime of the bloaty and compressed/compact wasm files.
305///
306/// We add the bloaty and the compressed/compact wasm file to the `rerun-if-changed` files.
307/// Cargo/Rustc determines based on the timestamp of the `invoked.timestamp` file that can be found
308/// in the `OUT_DIR/..`, if it needs to rerun a `build.rs` script. The problem is that this
309/// `invoked.timestamp` is created when the `build.rs` is executed and the wasm binaries are created
310/// later. This leads to them having a later mtime than the `invoked.timestamp` file and thus,
311/// cargo/rustc always re-executes the `build.rs` script. To hack around this, we copy the mtime of
312/// the `invoked.timestamp` to the wasm binaries.
313fn adjust_mtime(
314	bloaty_wasm: &WasmBinaryBloaty,
315	compressed_or_compact_wasm: Option<&WasmBinary>,
316) -> std::io::Result<()> {
317	let out_dir = build_helper::out_dir();
318	let invoked_timestamp = out_dir.join("../invoked.timestamp");
319
320	// Get the mtime of the `invoked.timestamp`
321	let metadata = fs::metadata(invoked_timestamp)?;
322	let mtime = filetime::FileTime::from_last_modification_time(&metadata);
323
324	filetime::set_file_mtime(bloaty_wasm.bloaty_path(), mtime)?;
325	if let Some(binary) = compressed_or_compact_wasm.as_ref() {
326		filetime::set_file_mtime(binary.wasm_binary_path(), mtime)?;
327	}
328
329	Ok(())
330}
331
332/// Find the `Cargo.lock` relative to the `OUT_DIR` environment variable.
333///
334/// If the `Cargo.lock` cannot be found, we emit a warning and return `None`.
335fn find_cargo_lock(cargo_manifest: &Path) -> Option<PathBuf> {
336	fn find_impl(mut path: PathBuf) -> Option<PathBuf> {
337		loop {
338			if path.join("Cargo.lock").exists() {
339				return Some(path.join("Cargo.lock"))
340			}
341
342			if !path.pop() {
343				return None
344			}
345		}
346	}
347
348	if let Ok(workspace) = env::var(crate::WASM_BUILD_WORKSPACE_HINT) {
349		let path = PathBuf::from(workspace);
350
351		if path.join("Cargo.lock").exists() {
352			return Some(path.join("Cargo.lock"))
353		} else {
354			build_helper::warning!(
355				"`{}` env variable doesn't point to a directory that contains a `Cargo.lock`.",
356				crate::WASM_BUILD_WORKSPACE_HINT,
357			);
358		}
359	}
360
361	if let Some(path) = find_impl(build_helper::out_dir()) {
362		return Some(path)
363	}
364
365	build_helper::warning!(
366		"Could not find `Cargo.lock` for `{}`, while searching from `{}`. \
367		 To fix this, point the `{}` env variable to the directory of the workspace being compiled.",
368		cargo_manifest.display(),
369		build_helper::out_dir().display(),
370		crate::WASM_BUILD_WORKSPACE_HINT,
371	);
372
373	None
374}
375
376/// Extract the crate name from the given `Cargo.toml`.
377fn get_crate_name(cargo_manifest: &Path) -> String {
378	let cargo_toml: Table = toml::from_str(
379		&fs::read_to_string(cargo_manifest).expect("File exists as checked before; qed"),
380	)
381	.expect("Cargo manifest is a valid toml file; qed");
382
383	let package = cargo_toml
384		.get("package")
385		.and_then(|t| t.as_table())
386		.expect("`package` key exists in valid `Cargo.toml`; qed");
387
388	package
389		.get("name")
390		.and_then(|p| p.as_str())
391		.map(ToOwned::to_owned)
392		.expect("Package name exists; qed")
393}
394
395/// Extract the `lib.name` from the given `Cargo.toml`.
396fn get_lib_name(cargo_manifest: &Path) -> Option<String> {
397	let cargo_toml: Table = toml::from_str(
398		&fs::read_to_string(cargo_manifest).expect("File exists as checked before; qed"),
399	)
400	.expect("Cargo manifest is a valid toml file; qed");
401
402	let lib = cargo_toml.get("lib").and_then(|t| t.as_table())?;
403
404	lib.get("name").and_then(|p| p.as_str()).map(ToOwned::to_owned)
405}
406
407/// Returns the name for the blob binary.
408fn get_blob_name(target: RuntimeTarget, cargo_manifest: &Path) -> String {
409	match target {
410		RuntimeTarget::Wasm => get_lib_name(cargo_manifest)
411			.expect("The wasm project should have a `lib.name`; qed")
412			.replace('-', "_"),
413		RuntimeTarget::Riscv => get_crate_name(cargo_manifest),
414	}
415}
416
417/// Returns the root path of the wasm workspace.
418fn get_wasm_workspace_root() -> PathBuf {
419	let mut out_dir = build_helper::out_dir();
420
421	loop {
422		match out_dir.parent() {
423			Some(parent) if out_dir.ends_with("build") => return parent.to_path_buf(),
424			_ =>
425				if !out_dir.pop() {
426					break
427				},
428		}
429	}
430
431	panic!("Could not find target dir in: {}", build_helper::out_dir().display())
432}
433
434fn create_project_cargo_toml(
435	target: RuntimeTarget,
436	wasm_workspace: &Path,
437	workspace_root_path: &Path,
438	crate_name: &str,
439	crate_path: &Path,
440	enabled_features: impl Iterator<Item = String>,
441) {
442	let mut workspace_toml: Table = toml::from_str(
443		&fs::read_to_string(workspace_root_path.join("Cargo.toml"))
444			.expect("Workspace root `Cargo.toml` exists; qed"),
445	)
446	.expect("Workspace root `Cargo.toml` is a valid toml file; qed");
447
448	let mut wasm_workspace_toml = Table::new();
449
450	// Add different profiles which are selected by setting `WASM_BUILD_TYPE`.
451	let mut release_profile = Table::new();
452	release_profile.insert("panic".into(), "abort".into());
453	release_profile.insert("lto".into(), "thin".into());
454
455	let mut production_profile = Table::new();
456	production_profile.insert("inherits".into(), "release".into());
457	production_profile.insert("lto".into(), "fat".into());
458	production_profile.insert("codegen-units".into(), 1.into());
459
460	let mut dev_profile = Table::new();
461	dev_profile.insert("panic".into(), "abort".into());
462
463	let mut profile = Table::new();
464	profile.insert("release".into(), release_profile.into());
465	profile.insert("production".into(), production_profile.into());
466	profile.insert("dev".into(), dev_profile.into());
467
468	wasm_workspace_toml.insert("profile".into(), profile.into());
469
470	// Add patch section from the project root `Cargo.toml`
471	while let Some(mut patch) =
472		workspace_toml.remove("patch").and_then(|p| p.try_into::<Table>().ok())
473	{
474		// Iterate over all patches and make the patch path absolute from the workspace root path.
475		patch
476			.iter_mut()
477			.filter_map(|p| {
478				p.1.as_table_mut().map(|t| t.iter_mut().filter_map(|t| t.1.as_table_mut()))
479			})
480			.flatten()
481			.for_each(|p| {
482				p.iter_mut().filter(|(k, _)| k == &"path").for_each(|(_, v)| {
483					if let Some(path) = v.as_str().map(PathBuf::from) {
484						if path.is_relative() {
485							*v = workspace_root_path.join(path).display().to_string().into();
486						}
487					}
488				})
489			});
490
491		wasm_workspace_toml.insert("patch".into(), patch.into());
492	}
493
494	let mut package = Table::new();
495	package.insert("name".into(), format!("{}-blob", crate_name).into());
496	package.insert("version".into(), "1.0.0".into());
497	package.insert("edition".into(), "2021".into());
498
499	wasm_workspace_toml.insert("package".into(), package.into());
500
501	if target == RuntimeTarget::Wasm {
502		let mut lib = Table::new();
503		lib.insert("name".into(), crate_name.replace("-", "_").into());
504		lib.insert("crate-type".into(), vec!["cdylib".to_string()].into());
505		wasm_workspace_toml.insert("lib".into(), lib.into());
506	}
507
508	let mut dependencies = Table::new();
509
510	let mut wasm_project = Table::new();
511	wasm_project.insert("package".into(), crate_name.into());
512	wasm_project.insert("path".into(), crate_path.display().to_string().into());
513	wasm_project.insert("default-features".into(), false.into());
514	wasm_project.insert("features".into(), enabled_features.collect::<Vec<_>>().into());
515
516	dependencies.insert("wasm-project".into(), wasm_project.into());
517
518	wasm_workspace_toml.insert("dependencies".into(), dependencies.into());
519
520	let mut workspace = Table::new();
521	workspace.insert("resolver".into(), "2".into());
522
523	wasm_workspace_toml.insert("workspace".into(), workspace.into());
524
525	if target == RuntimeTarget::Riscv {
526		// This dependency currently doesn't compile under RISC-V, so patch it with our own fork.
527		//
528		// TODO: Remove this once a new version of `bitvec` (which uses a new version of `radium`
529		//       which doesn't have this problem) is released on crates.io.
530		let radium_patch = toml::toml! {
531			radium = { git = "https://github.com/paritytech/radium-0.7-fork.git", rev = "a5da15a15c90fd169d661d206cf0db592487f52b" }
532		};
533
534		let mut patch = wasm_workspace_toml
535			.get("patch")
536			.and_then(|p| p.as_table().cloned())
537			.unwrap_or_default();
538
539		if let Some(existing_crates_io) = patch.get_mut("crates-io").and_then(|t| t.as_table_mut())
540		{
541			existing_crates_io.extend(radium_patch);
542		} else {
543			patch.insert("crates-io".into(), radium_patch.into());
544		}
545
546		wasm_workspace_toml.insert("patch".into(), patch.into());
547	}
548
549	write_file_if_changed(
550		wasm_workspace.join("Cargo.toml"),
551		toml::to_string_pretty(&wasm_workspace_toml).expect("Wasm workspace toml is valid; qed"),
552	);
553}
554
555/// Find a package by the given `manifest_path` in the metadata. In case it can't be found by its
556/// manifest_path, fallback to finding it by name; this is necessary during publish because the
557/// package's manifest path will be *generated* within a specific packaging directory, thus it won't
558/// be found by its original path anymore.
559///
560/// Panics if the package could not be found.
561fn find_package_by_manifest_path<'a>(
562	pkg_name: &str,
563	manifest_path: &Path,
564	crate_metadata: &'a cargo_metadata::Metadata,
565) -> &'a cargo_metadata::Package {
566	if let Some(pkg) = crate_metadata.packages.iter().find(|p| p.manifest_path == manifest_path) {
567		return pkg
568	}
569
570	let pkgs_by_name = crate_metadata
571		.packages
572		.iter()
573		.filter(|p| p.name == pkg_name)
574		.collect::<Vec<_>>();
575
576	if let Some(pkg) = pkgs_by_name.first() {
577		if pkgs_by_name.len() > 1 {
578			panic!(
579				"Found multiple packages matching the name {pkg_name} ({manifest_path:?}): {:?}",
580				pkgs_by_name
581			);
582		} else {
583			return pkg
584		}
585	} else {
586		panic!("Failed to find entry for package {pkg_name} ({manifest_path:?}).");
587	}
588}
589
590/// Get a list of enabled features for the project.
591fn project_enabled_features(
592	pkg_name: &str,
593	cargo_manifest: &Path,
594	crate_metadata: &cargo_metadata::Metadata,
595) -> Vec<String> {
596	let package = find_package_by_manifest_path(pkg_name, cargo_manifest, crate_metadata);
597
598	let std_enabled = package.features.get("std");
599
600	let mut enabled_features = package
601		.features
602		.iter()
603		.filter(|(f, v)| {
604			let mut feature_env = f.replace("-", "_");
605			feature_env.make_ascii_uppercase();
606
607			// If this is a feature that corresponds only to an optional dependency
608			// and this feature is enabled by the `std` feature, we assume that this
609			// is only done through the `std` feature. This is a bad heuristic and should
610			// be removed after namespaced features are landed:
611			// https://doc.rust-lang.org/cargo/reference/unstable.html#namespaced-features
612			// Then we can just express this directly in the `Cargo.toml` and do not require
613			// this heuristic anymore. However, for the transition phase between now and namespaced
614			// features already being present in nightly, we need this code to make
615			// runtimes compile with all the possible rustc versions.
616			if v.len() == 1 &&
617				v.get(0).map_or(false, |v| *v == format!("dep:{}", f)) &&
618				std_enabled.as_ref().map(|e| e.iter().any(|ef| ef == *f)).unwrap_or(false)
619			{
620				return false
621			}
622
623			// We don't want to enable the `std`/`default` feature for the wasm build and
624			// we need to check if the feature is enabled by checking the env variable.
625			*f != "std" &&
626				*f != "default" &&
627				env::var(format!("CARGO_FEATURE_{feature_env}"))
628					.map(|v| v == "1")
629					.unwrap_or_default()
630		})
631		.map(|d| d.0.clone())
632		.collect::<Vec<_>>();
633
634	enabled_features.sort();
635	enabled_features
636}
637
638/// Returns if the project has the `runtime-wasm` feature
639fn has_runtime_wasm_feature_declared(
640	pkg_name: &str,
641	cargo_manifest: &Path,
642	crate_metadata: &cargo_metadata::Metadata,
643) -> bool {
644	let package = find_package_by_manifest_path(pkg_name, cargo_manifest, crate_metadata);
645
646	package.features.keys().any(|k| k == "runtime-wasm")
647}
648
649/// Create the project used to build the wasm binary.
650///
651/// # Returns
652///
653/// The path to the created wasm project.
654fn create_project(
655	target: RuntimeTarget,
656	project_cargo_toml: &Path,
657	wasm_workspace: &Path,
658	crate_metadata: &Metadata,
659	workspace_root_path: &Path,
660	features_to_enable: Vec<String>,
661) -> PathBuf {
662	let crate_name = get_crate_name(project_cargo_toml);
663	let crate_path = project_cargo_toml.parent().expect("Parent path exists; qed");
664	let wasm_project_folder = wasm_workspace.join(&crate_name);
665
666	fs::create_dir_all(wasm_project_folder.join("src"))
667		.expect("Wasm project dir create can not fail; qed");
668
669	let mut enabled_features =
670		project_enabled_features(&crate_name, project_cargo_toml, crate_metadata);
671
672	if has_runtime_wasm_feature_declared(&crate_name, project_cargo_toml, crate_metadata) {
673		enabled_features.push("runtime-wasm".into());
674	}
675
676	let mut enabled_features = enabled_features.into_iter().collect::<HashSet<_>>();
677	enabled_features.extend(features_to_enable.into_iter());
678
679	create_project_cargo_toml(
680		target,
681		&wasm_project_folder,
682		workspace_root_path,
683		&crate_name,
684		crate_path,
685		enabled_features.into_iter(),
686	);
687
688	match target {
689		RuntimeTarget::Wasm => {
690			write_file_if_changed(
691				wasm_project_folder.join("src/lib.rs"),
692				"#![no_std] #![allow(unused_imports)] pub use wasm_project::*;",
693			);
694		},
695		RuntimeTarget::Riscv => {
696			write_file_if_changed(
697				wasm_project_folder.join("src/main.rs"),
698				"#![no_std] #![no_main] #![allow(unused_imports)] pub use wasm_project::*;",
699			);
700		},
701	}
702
703	if let Some(crate_lock_file) = find_cargo_lock(project_cargo_toml) {
704		// Use the `Cargo.lock` of the main project.
705		crate::copy_file_if_changed(crate_lock_file, wasm_project_folder.join("Cargo.lock"));
706	}
707
708	wasm_project_folder
709}
710
711/// A rustc profile.
712#[derive(Clone, Debug, EnumIter)]
713enum Profile {
714	/// The `--profile dev` profile.
715	Debug,
716	/// The `--profile release` profile.
717	Release,
718	/// The `--profile production` profile.
719	Production,
720}
721
722impl Profile {
723	/// The name of the profile as supplied to the cargo `--profile` cli option.
724	fn name(&self) -> &'static str {
725		match self {
726			Self::Debug => "dev",
727			Self::Release => "release",
728			Self::Production => "production",
729		}
730	}
731
732	/// The sub directory within `target` where cargo places the build output.
733	///
734	/// # Note
735	///
736	/// Usually this is the same as [`Self::name`] with the exception of the debug
737	/// profile which is called `dev`.
738	fn directory(&self) -> &'static str {
739		match self {
740			Self::Debug => "debug",
741			_ => self.name(),
742		}
743	}
744
745	/// Whether the resulting binary should be compacted and compressed.
746	fn wants_compact(&self) -> bool {
747		!matches!(self, Self::Debug)
748	}
749}
750
751/// The build configuration for this build.
752#[derive(Debug)]
753struct BuildConfiguration {
754	/// The profile that is used to build the outer project.
755	pub outer_build_profile: Profile,
756	/// The profile to use to build the runtime blob.
757	pub blob_build_profile: Profile,
758}
759
760impl BuildConfiguration {
761	/// Create a [`BuildConfiguration`] by detecting which profile is used for the main build and
762	/// checking any env var overrides.
763	///
764	/// We cannot easily determine the profile that is used by the main cargo invocation
765	/// because the `PROFILE` environment variable won't contain any custom profiles like
766	/// "production". It would only contain the builtin profile where the custom profile
767	/// inherits from. This is why we inspect the build path to learn which profile is used.
768	///
769	/// When not overridden by a env variable we always default to building wasm with the `Release`
770	/// profile even when the main build uses the debug build. This is because wasm built with the
771	/// `Debug` profile is too slow for normal development activities and almost never intended.
772	///
773	/// When cargo is building in `--profile dev`, user likely intends to compile fast, so we don't
774	/// bother producing compact or compressed blobs.
775	///
776	/// # Note
777	///
778	/// Can be overridden by setting [`crate::WASM_BUILD_TYPE_ENV`].
779	fn detect(target: RuntimeTarget, wasm_project: &Path) -> Self {
780		let (name, overridden) = if let Ok(name) = env::var(crate::WASM_BUILD_TYPE_ENV) {
781			(name, true)
782		} else {
783			// First go backwards to the beginning of the target directory.
784			// Then go forwards to find the build subdirectory.
785			// We need to go backwards first because when starting from the root there
786			// might be a chance that someone has a directory somewhere in the path with the same
787			// name.
788			let name = wasm_project
789				.components()
790				.rev()
791				.take_while(|c| c.as_os_str() != "target")
792				.collect::<Vec<_>>()
793				.iter()
794				.rev()
795				.take_while(|c| c.as_os_str() != build_subdirectory(target))
796				.last()
797				.expect("We put the runtime project within a `target/.../[rw]build` path; qed")
798				.as_os_str()
799				.to_str()
800				.expect("All our profile directory names are ascii; qed")
801				.to_string();
802			(name, false)
803		};
804		let outer_build_profile = Profile::iter().find(|p| p.directory() == name);
805		let blob_build_profile = match (outer_build_profile.clone(), overridden) {
806			// When not overridden by a env variable we default to using the `Release` profile
807			// for the wasm build even when the main build uses the debug build. This
808			// is because the `Debug` profile is too slow for normal development activities.
809			(Some(Profile::Debug), false) => Profile::Release,
810			// For any other profile or when overridden we take it at face value.
811			(Some(profile), _) => profile,
812			// For non overridden unknown profiles we fall back to `Release`.
813			// This allows us to continue building when a custom profile is used for the
814			// main builds cargo. When explicitly passing a profile via env variable we are
815			// not doing a fallback.
816			(None, false) => {
817				let profile = Profile::Release;
818				build_helper::warning!(
819					"Unknown cargo profile `{name}`. Defaulted to `{profile:?}` for the runtime build.",
820				);
821				profile
822			},
823			// Invalid profile specified.
824			(None, true) => {
825				// We use println! + exit instead of a panic in order to have a cleaner output.
826				println!(
827					"Unexpected profile name: `{name}`. One of the following is expected: {:?}",
828					Profile::iter().map(|p| p.directory()).collect::<Vec<_>>(),
829				);
830				process::exit(1);
831			},
832		};
833		BuildConfiguration {
834			outer_build_profile: outer_build_profile.unwrap_or(Profile::Release),
835			blob_build_profile,
836		}
837	}
838}
839
840/// Check environment whether we should build without network
841fn offline_build() -> bool {
842	env::var(OFFLINE).map_or(false, |v| v == "true")
843}
844
845/// Build the project and create the bloaty runtime blob.
846///
847/// Returns the path to the generated bloaty runtime blob.
848fn build_bloaty_blob(
849	target: RuntimeTarget,
850	blob_build_profile: &Profile,
851	project: &Path,
852	default_rustflags: &str,
853	cargo_cmd: CargoCommandVersioned,
854	#[cfg(feature = "metadata-hash")] metadata_hash: Option<[u8; 32]>,
855) -> PathBuf {
856	let manifest_path = project.join("Cargo.toml");
857	let mut build_cmd = cargo_cmd.command();
858
859	let mut rustflags = String::new();
860	match target {
861		RuntimeTarget::Wasm => {
862			// For Rust >= 1.70 and Rust < 1.84 with `wasm32-unknown-unknown` target,
863			// it's required to disable default WASM features:
864			// - `sign-ext` (since Rust 1.70)
865			// - `multivalue` and `reference-types` (since Rust 1.82)
866			//
867			// For Rust >= 1.84, we use `wasm32v1-none` target
868			// (disables all "post-MVP" WASM features except `mutable-globals`):
869			// - https://doc.rust-lang.org/beta/rustc/platform-support/wasm32v1-none.html
870			//
871			// Also see:
872			// https://blog.rust-lang.org/2024/09/24/webassembly-targets-change-in-default-target-features.html#disabling-on-by-default-webassembly-proposals
873
874			if !cargo_cmd.is_wasm32v1_none_target_available() {
875				rustflags.push_str("-C target-cpu=mvp ");
876			}
877
878			rustflags.push_str("-C link-arg=--export-table ");
879		},
880		RuntimeTarget::Riscv => (),
881	}
882
883	rustflags.push_str(default_rustflags);
884	rustflags.push_str(" --cfg substrate_runtime ");
885	rustflags.push_str(&env::var(crate::WASM_BUILD_RUSTFLAGS_ENV).unwrap_or_default());
886
887	build_cmd
888		.arg("rustc")
889		.arg(format!("--target={}", target.rustc_target(&cargo_cmd)))
890		.arg(format!("--manifest-path={}", manifest_path.display()))
891		.env("RUSTFLAGS", rustflags)
892		// Manually set the `CARGO_TARGET_DIR` to prevent a cargo deadlock (cargo locks a target dir
893		// exclusive). The runner project is created in `CARGO_TARGET_DIR` and executing it will
894		// create a sub target directory inside of `CARGO_TARGET_DIR`.
895		.env("CARGO_TARGET_DIR", &project.join("target").display().to_string())
896		// As we are being called inside a build-script, this env variable is set. However, we set
897		// our own `RUSTFLAGS` and thus, we need to remove this. Otherwise cargo favors this
898		// env variable.
899		.env_remove("CARGO_ENCODED_RUSTFLAGS")
900		// Make sure if we're called from within a `build.rs` the host toolchain won't override a
901		// rustup toolchain we've picked.
902		.env_remove("RUSTC")
903		// We don't want to call ourselves recursively
904		.env(crate::SKIP_BUILD_ENV, "");
905
906	let cargo_args = env::var(crate::WASM_BUILD_CARGO_ARGS).unwrap_or_default();
907	if !cargo_args.is_empty() {
908		let Some(args) = shlex::split(&cargo_args) else {
909			build_helper::warning(format!(
910				"the {} environment variable is not a valid shell string",
911				crate::WASM_BUILD_CARGO_ARGS
912			));
913			std::process::exit(1);
914		};
915		build_cmd.args(args);
916	}
917
918	#[cfg(feature = "metadata-hash")]
919	if let Some(hash) = metadata_hash {
920		build_cmd.env("RUNTIME_METADATA_HASH", array_bytes::bytes2hex("0x", &hash));
921	}
922
923	if super::color_output_enabled() {
924		build_cmd.arg("--color=always");
925	}
926
927	build_cmd.arg("--profile");
928	build_cmd.arg(blob_build_profile.name());
929
930	if offline_build() {
931		build_cmd.arg("--offline");
932	}
933
934	// For Rust >= 1.70 and Rust < 1.84 with `wasm32-unknown-unknown` target,
935	// it's required to disable default WASM features:
936	// - `sign-ext` (since Rust 1.70)
937	// - `multivalue` and `reference-types` (since Rust 1.82)
938	//
939	// For Rust >= 1.84, we use `wasm32v1-none` target
940	// (disables all "post-MVP" WASM features except `mutable-globals`):
941	// - https://doc.rust-lang.org/beta/rustc/platform-support/wasm32v1-none.html
942	//
943	// Our executor currently only supports the WASM MVP feature set, however nowadays
944	// when compiling WASM the Rust compiler has more features enabled by default.
945	//
946	// We do set the `-C target-cpu=mvp` flag to make sure that *our* code gets compiled
947	// in a way that is compatible with our executor, however this doesn't affect Rust's
948	// standard library crates (`std`, `core` and `alloc`) which are by default precompiled
949	// and still can make use of these extra features.
950	//
951	// So here we force the compiler to also compile the standard library crates for us
952	// to make sure that they also only use the MVP features.
953	//
954	// So the `-Zbuild-std` and `RUSTC_BOOTSTRAP=1` hacks are only used for Rust < 1.84.
955	//
956	// Also see:
957	// https://blog.rust-lang.org/2024/09/24/webassembly-targets-change-in-default-target-features.html#disabling-on-by-default-webassembly-proposals
958	if let Some(arg) = target.rustc_target_build_std(&cargo_cmd) {
959		build_cmd.arg("-Z").arg(arg);
960
961		if !cargo_cmd.supports_nightly_features() {
962			build_cmd.env("RUSTC_BOOTSTRAP", "1");
963		}
964	}
965
966	// Inherit jobserver in child cargo command to ensure we don't try to use more concurrency than
967	// available
968	if let Some(c) = get_jobserver() {
969		c.configure(&mut build_cmd);
970	}
971
972	println!("{}", colorize_info_message("Information that should be included in a bug report."));
973	println!("{} {:?}", colorize_info_message("Executing build command:"), build_cmd);
974	println!("{} {}", colorize_info_message("Using rustc version:"), cargo_cmd.rustc_version());
975
976	// Use `process::exit(1)` to have a clean error output.
977	if !build_cmd.status().map_or(false, |s| s.success()) {
978		process::exit(1);
979	}
980
981	let blob_name = get_blob_name(target, &manifest_path);
982	let target_directory = project
983		.join("target")
984		.join(target.rustc_target_dir(&cargo_cmd))
985		.join(blob_build_profile.directory());
986	match target {
987		RuntimeTarget::Riscv => {
988			let elf_path = target_directory.join(&blob_name);
989			let elf_metadata = match elf_path.metadata() {
990				Ok(path) => path,
991				Err(error) =>
992					panic!("internal error: couldn't read the metadata of {elf_path:?}: {error}"),
993			};
994
995			let polkavm_path = target_directory.join(format!("{}.polkavm", blob_name));
996			if polkavm_path
997				.metadata()
998				.map(|polkavm_metadata| {
999					polkavm_metadata.modified().unwrap() < elf_metadata.modified().unwrap()
1000				})
1001				.unwrap_or(true)
1002			{
1003				let blob_bytes =
1004					std::fs::read(elf_path).expect("binary always exists after its built");
1005
1006				let mut config = polkavm_linker::Config::default();
1007				config.set_strip(true); // TODO: This shouldn't always be done.
1008
1009				let program = match polkavm_linker::program_from_elf(config, &blob_bytes) {
1010					Ok(program) => program,
1011					Err(error) => {
1012						println!("Failed to link the runtime blob; this is probably a bug!");
1013						println!("Linking error: {error}");
1014						process::exit(1);
1015					},
1016				};
1017
1018				std::fs::write(&polkavm_path, program)
1019					.expect("writing the blob to a file always works");
1020			}
1021
1022			polkavm_path
1023		},
1024		RuntimeTarget::Wasm => target_directory.join(format!("{}.wasm", blob_name)),
1025	}
1026}
1027
1028fn compact_wasm(
1029	project: &Path,
1030	blob_name: &str,
1031	bloaty_binary: &WasmBinaryBloaty,
1032) -> Option<WasmBinary> {
1033	let wasm_compact_path = project.join(format!("{blob_name}.compact.wasm"));
1034	let start = std::time::Instant::now();
1035	wasm_opt::OptimizationOptions::new_opt_level_0()
1036		.mvp_features_only()
1037		.debug_info(true)
1038		.add_pass(wasm_opt::Pass::StripDwarf)
1039		.add_pass(wasm_opt::Pass::SignextLowering)
1040		.run(bloaty_binary.bloaty_path(), &wasm_compact_path)
1041		.expect("Failed to compact generated WASM binary.");
1042
1043	println!(
1044		"{} {}",
1045		colorize_info_message("Compacted wasm in"),
1046		colorize_info_message(format!("{:?}", start.elapsed()).as_str())
1047	);
1048
1049	Some(WasmBinary(wasm_compact_path))
1050}
1051
1052fn try_compress_blob(compact_blob_path: &Path, out_name: &str) -> Option<WasmBinary> {
1053	use sp_maybe_compressed_blob::CODE_BLOB_BOMB_LIMIT;
1054
1055	let project = compact_blob_path.parent().expect("blob path should have a parent directory");
1056	let compact_compressed_blob_path =
1057		project.join(format!("{}.compact.compressed.wasm", out_name));
1058
1059	let start = std::time::Instant::now();
1060	let data = fs::read(compact_blob_path).expect("Failed to read WASM binary");
1061	if let Some(compressed) = sp_maybe_compressed_blob::compress(&data, CODE_BLOB_BOMB_LIMIT) {
1062		fs::write(&compact_compressed_blob_path, &compressed[..])
1063			.expect("Failed to write WASM binary");
1064
1065		println!(
1066			"{} {}",
1067			colorize_info_message("Compressed blob in"),
1068			colorize_info_message(format!("{:?}", start.elapsed()).as_str())
1069		);
1070		Some(WasmBinary(compact_compressed_blob_path))
1071	} else {
1072		build_helper::warning!(
1073			"Writing uncompressed blob. Exceeded maximum size {}",
1074			CODE_BLOB_BOMB_LIMIT,
1075		);
1076		println!("{}", colorize_info_message("Skipping blob compression"));
1077		None
1078	}
1079}
1080
1081/// Custom wrapper for a [`cargo_metadata::Package`] to store it in
1082/// a `HashSet`.
1083#[derive(Debug)]
1084struct DeduplicatePackage<'a> {
1085	package: &'a cargo_metadata::Package,
1086	identifier: String,
1087}
1088
1089impl<'a> From<&'a cargo_metadata::Package> for DeduplicatePackage<'a> {
1090	fn from(package: &'a cargo_metadata::Package) -> Self {
1091		Self {
1092			package,
1093			identifier: format!("{}{}{:?}", package.name, package.version, package.source),
1094		}
1095	}
1096}
1097
1098impl<'a> Hash for DeduplicatePackage<'a> {
1099	fn hash<H: Hasher>(&self, state: &mut H) {
1100		self.identifier.hash(state);
1101	}
1102}
1103
1104impl<'a> PartialEq for DeduplicatePackage<'a> {
1105	fn eq(&self, other: &Self) -> bool {
1106		self.identifier == other.identifier
1107	}
1108}
1109
1110impl<'a> Eq for DeduplicatePackage<'a> {}
1111
1112impl<'a> Deref for DeduplicatePackage<'a> {
1113	type Target = cargo_metadata::Package;
1114
1115	fn deref(&self) -> &Self::Target {
1116		self.package
1117	}
1118}
1119
1120fn create_metadata_command(path: impl Into<PathBuf>) -> MetadataCommand {
1121	let mut metadata_command = MetadataCommand::new();
1122	metadata_command.manifest_path(path);
1123
1124	if offline_build() {
1125		metadata_command.other_options(vec!["--offline".to_owned()]);
1126	}
1127	metadata_command
1128}
1129
1130/// Generate the `rerun-if-changed` instructions for cargo to make sure that the WASM binary is
1131/// rebuilt when needed.
1132fn generate_rerun_if_changed_instructions(
1133	cargo_manifest: &Path,
1134	project_folder: &Path,
1135	wasm_workspace: &Path,
1136	compressed_or_compact_wasm: Option<&WasmBinary>,
1137	bloaty_wasm: &WasmBinaryBloaty,
1138) {
1139	// Rerun `build.rs` if the `Cargo.lock` changes
1140	if let Some(cargo_lock) = find_cargo_lock(cargo_manifest) {
1141		rerun_if_changed(cargo_lock);
1142	}
1143
1144	let metadata = create_metadata_command(project_folder.join("Cargo.toml"))
1145		.exec()
1146		.expect("`cargo metadata` can not fail!");
1147
1148	let package = metadata
1149		.packages
1150		.iter()
1151		.find(|p| p.manifest_path == cargo_manifest)
1152		.expect("The crate package is contained in its own metadata; qed");
1153
1154	// Start with the dependencies of the crate we want to compile for wasm.
1155	let mut dependencies = package.dependencies.iter().collect::<Vec<_>>();
1156
1157	// Collect all packages by follow the dependencies of all packages we find.
1158	let mut packages = HashSet::new();
1159	packages.insert(DeduplicatePackage::from(package));
1160
1161	while let Some(dependency) = dependencies.pop() {
1162		// Ignore all dev dependencies
1163		if dependency.kind == DependencyKind::Development {
1164			continue
1165		}
1166
1167		let path_or_git_dep =
1168			dependency.source.as_ref().map(|s| s.starts_with("git+")).unwrap_or(true);
1169
1170		let package = metadata
1171			.packages
1172			.iter()
1173			.filter(|p| !p.manifest_path.starts_with(wasm_workspace))
1174			.find(|p| {
1175				// Check that the name matches and that the version matches or this is
1176				// a git or path dep. A git or path dependency can only occur once, so we don't
1177				// need to check the version.
1178				(path_or_git_dep || dependency.req.matches(&p.version)) && dependency.name == p.name
1179			});
1180
1181		if let Some(package) = package {
1182			if packages.insert(DeduplicatePackage::from(package)) {
1183				dependencies.extend(package.dependencies.iter());
1184			}
1185		}
1186	}
1187
1188	// Make sure that if any file/folder of a dependency change, we need to rerun the `build.rs`
1189	packages.iter().for_each(package_rerun_if_changed);
1190
1191	compressed_or_compact_wasm.map(|w| rerun_if_changed(w.wasm_binary_path()));
1192	rerun_if_changed(bloaty_wasm.bloaty_path());
1193
1194	// Register our env variables
1195	println!("cargo:rerun-if-env-changed={}", crate::SKIP_BUILD_ENV);
1196	println!("cargo:rerun-if-env-changed={}", crate::WASM_BUILD_TYPE_ENV);
1197	println!("cargo:rerun-if-env-changed={}", crate::WASM_BUILD_RUSTFLAGS_ENV);
1198	println!("cargo:rerun-if-env-changed={}", crate::WASM_TARGET_DIRECTORY);
1199	println!("cargo:rerun-if-env-changed={}", crate::WASM_BUILD_TOOLCHAIN);
1200	println!("cargo:rerun-if-env-changed={}", crate::WASM_BUILD_STD);
1201	println!("cargo:rerun-if-env-changed={}", crate::RUNTIME_TARGET);
1202	println!("cargo:rerun-if-env-changed={}", crate::WASM_BUILD_CARGO_ARGS);
1203}
1204
1205/// Track files and paths related to the given package to rerun `build.rs` on any relevant change.
1206fn package_rerun_if_changed(package: &DeduplicatePackage) {
1207	let mut manifest_path = package.manifest_path.clone();
1208	if manifest_path.ends_with("Cargo.toml") {
1209		manifest_path.pop();
1210	}
1211
1212	WalkDir::new(&manifest_path)
1213		.into_iter()
1214		.filter_entry(|p| {
1215			// Ignore this entry if it is a directory that contains a `Cargo.toml` that is not the
1216			// `Cargo.toml` related to the current package. This is done to ignore sub-crates of a
1217			// crate. If such a sub-crate is a dependency, it will be processed independently
1218			// anyway.
1219			p.path() == manifest_path || !p.path().is_dir() || !p.path().join("Cargo.toml").exists()
1220		})
1221		.filter_map(|p| p.ok().map(|p| p.into_path()))
1222		.filter(|p| p.extension().map(|e| e == "rs" || e == "toml").unwrap_or_default())
1223		.for_each(rerun_if_changed);
1224}
1225
1226/// Copy the blob binary to the target directory set in `WASM_TARGET_DIRECTORY` environment
1227/// variable. If the variable is not set, this is a no-op.
1228fn copy_blob_to_target_directory(cargo_manifest: &Path, blob_binary: &WasmBinary) {
1229	let target_dir = match env::var(crate::WASM_TARGET_DIRECTORY) {
1230		Ok(path) => PathBuf::from(path),
1231		Err(_) => return,
1232	};
1233
1234	if !target_dir.is_absolute() {
1235		// We use println! + exit instead of a panic in order to have a cleaner output.
1236		println!(
1237			"Environment variable `{}` with `{}` is not an absolute path!",
1238			crate::WASM_TARGET_DIRECTORY,
1239			target_dir.display(),
1240		);
1241		process::exit(1);
1242	}
1243
1244	fs::create_dir_all(&target_dir).expect("Creates `WASM_TARGET_DIRECTORY`.");
1245
1246	fs::copy(
1247		blob_binary.wasm_binary_path(),
1248		target_dir.join(format!("{}.wasm", get_blob_name(RuntimeTarget::Wasm, cargo_manifest))),
1249	)
1250	.expect("Copies blob binary to `WASM_TARGET_DIRECTORY`.");
1251}
1252
1253// Get jobserver from parent cargo command
1254pub fn get_jobserver() -> &'static Option<jobserver::Client> {
1255	static JOBSERVER: OnceLock<Option<jobserver::Client>> = OnceLock::new();
1256
1257	JOBSERVER.get_or_init(|| {
1258		// Unsafe because it deals with raw fds
1259		unsafe { jobserver::Client::from_env() }
1260	})
1261}