#![allow(
clippy::collapsible_if,
clippy::get_first,
clippy::lines_filter_map_ok,
clippy::needless_borrow,
clippy::needless_borrows_for_generic_args,
clippy::needless_doctest_main,
clippy::needless_return,
clippy::new_ret_no_self,
clippy::option_map_unit_fn,
clippy::too_many_arguments,
clippy::unnecessary_map_or,
clippy::useless_conversion,
clippy::zero_prefixed_literal
)]
use std::{
collections::BTreeSet,
env, fs,
io::BufRead,
path::{Path, PathBuf},
process::Command,
};
use version::Version;
mod builder;
#[cfg(feature = "metadata-hash")]
mod metadata_hash;
mod prerequisites;
mod version;
mod wasm_project;
pub use builder::{WasmBuilder, WasmBuilderSelectProject};
const SKIP_BUILD_ENV: &str = "SKIP_WASM_BUILD";
const OFFLINE: &str = "CARGO_NET_OFFLINE";
const WASM_BUILD_TYPE_ENV: &str = "WASM_BUILD_TYPE";
const WASM_BUILD_RUSTFLAGS_ENV: &str = "WASM_BUILD_RUSTFLAGS";
const WASM_TARGET_DIRECTORY: &str = "WASM_TARGET_DIRECTORY";
const WASM_BUILD_NO_COLOR: &str = "WASM_BUILD_NO_COLOR";
const WASM_BUILD_TOOLCHAIN: &str = "WASM_BUILD_TOOLCHAIN";
const FORCE_WASM_BUILD_ENV: &str = "FORCE_WASM_BUILD";
const WASM_BUILD_WORKSPACE_HINT: &str = "WASM_BUILD_WORKSPACE_HINT";
const WASM_BUILD_STD: &str = "WASM_BUILD_STD";
const RUNTIME_TARGET: &str = "SUBSTRATE_RUNTIME_TARGET";
fn write_file_if_changed(file: impl AsRef<Path>, content: impl AsRef<str>) {
if fs::read_to_string(file.as_ref()).ok().as_deref() != Some(content.as_ref()) {
fs::write(file.as_ref(), content.as_ref())
.unwrap_or_else(|_| panic!("Writing `{}` can not fail!", file.as_ref().display()));
}
}
fn copy_file_if_changed(src: PathBuf, dst: PathBuf) {
let src_file = fs::read_to_string(&src).ok();
let dst_file = fs::read_to_string(&dst).ok();
if src_file != dst_file {
fs::copy(&src, &dst).unwrap_or_else(|_| {
panic!(
"Copying `{}` to `{}` can not fail; qed",
src.display(),
dst.display()
)
});
}
}
fn get_cargo_command(target: RuntimeTarget) -> CargoCommand {
let env_cargo =
CargoCommand::new(&env::var("CARGO").expect("`CARGO` env variable is always set by cargo"));
let default_cargo = CargoCommand::new("cargo");
let wasm_toolchain = env::var(WASM_BUILD_TOOLCHAIN).ok();
if let Some(cmd) =
wasm_toolchain.map(|t| CargoCommand::new_with_args("rustup", &["run", &t, "cargo"]))
{
cmd
} else if env_cargo.supports_substrate_runtime_env(target) {
env_cargo
} else if default_cargo.supports_substrate_runtime_env(target) {
default_cargo
} else {
get_rustup_command(target).unwrap_or(default_cargo)
}
}
fn get_rustup_command(target: RuntimeTarget) -> Option<CargoCommand> {
let output = Command::new("rustup")
.args(&["toolchain", "list"])
.output()
.ok()?
.stdout;
let lines = output.as_slice().lines();
let mut versions = Vec::new();
for line in lines.filter_map(|l| l.ok()) {
let rustup_version = line.split(" ").next().unwrap();
let cmd = CargoCommand::new_with_args("rustup", &["run", &rustup_version, "cargo"]);
if !cmd.supports_substrate_runtime_env(target) {
continue;
}
let Some(cargo_version) = cmd.version() else {
continue;
};
versions.push((cargo_version, rustup_version.to_string()));
}
versions.sort_by_key(|v| v.0);
let version = &versions.last()?.1;
Some(CargoCommand::new_with_args(
"rustup",
&["run", &version, "cargo"],
))
}
#[derive(Debug, Clone)]
struct CargoCommand {
program: String,
args: Vec<String>,
version: Option<Version>,
target_list: Option<BTreeSet<String>>,
}
impl CargoCommand {
fn new(program: &str) -> Self {
let version = Self::extract_version(program, &[]);
let target_list = Self::extract_target_list(program, &[]);
CargoCommand {
program: program.into(),
args: Vec::new(),
version,
target_list,
}
}
fn new_with_args(program: &str, args: &[&str]) -> Self {
let version = Self::extract_version(program, args);
let target_list = Self::extract_target_list(program, args);
CargoCommand {
program: program.into(),
args: args.iter().map(ToString::to_string).collect(),
version,
target_list,
}
}
fn command(&self) -> Command {
let mut cmd = Command::new(&self.program);
cmd.args(&self.args);
cmd
}
fn extract_version(program: &str, args: &[&str]) -> Option<Version> {
let version = Command::new(program)
.args(args)
.arg("--version")
.output()
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())?;
Version::extract(&version)
}
fn extract_target_list(program: &str, args: &[&str]) -> Option<BTreeSet<String>> {
let list = Command::new(program)
.args(args)
.args(&["rustc", "-Z", "unstable-options", "--print", "target-list"])
.env_remove("RUSTC")
.output()
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())?;
Some(list.trim().split("\n").map(ToString::to_string).collect())
}
fn version(&self) -> Option<Version> {
self.version
}
fn supports_nightly_features(&self) -> bool {
self.version.map_or(false, |version| version.is_nightly)
|| env::var("RUSTC_BOOTSTRAP").is_ok()
}
fn supports_substrate_runtime_env(&self, target: RuntimeTarget) -> bool {
match target {
RuntimeTarget::Wasm => self.supports_substrate_runtime_env_wasm(),
RuntimeTarget::Riscv => self.supports_substrate_runtime_env_riscv(),
}
}
fn supports_substrate_runtime_env_riscv(&self) -> bool {
let Some(target_list) = self.target_list.as_ref() else {
return false;
};
target_list.contains("riscv32ema-unknown-none-elf")
}
fn supports_substrate_runtime_env_wasm(&self) -> bool {
if env::var("RUSTC_BOOTSTRAP").is_ok() {
return true;
}
let Some(version) = self.version() else {
return false;
};
version.major > 1 || (version.major == 1 && version.minor >= 68) || version.is_nightly
}
fn supports_wasm32v1_none_target(&self) -> bool {
self.version.map_or(false, |version| {
version.major > 1 || (version.major == 1 && version.minor >= 84)
})
}
}
#[derive(Clone)]
struct CargoCommandVersioned {
command: CargoCommand,
version: String,
}
impl CargoCommandVersioned {
fn new(command: CargoCommand, version: String) -> Self {
Self { command, version }
}
fn rustc_version(&self) -> &str {
&self.version
}
}
impl std::ops::Deref for CargoCommandVersioned {
type Target = CargoCommand;
fn deref(&self) -> &CargoCommand {
&self.command
}
}
fn color_output_enabled() -> bool {
env::var(crate::WASM_BUILD_NO_COLOR).is_err()
}
fn get_bool_environment_variable(name: &str) -> Option<bool> {
let value = env::var_os(name)?;
if value == "1" {
Some(true)
} else if value == "0" {
Some(false)
} else {
build_helper::warning!(
"the '{}' environment variable has an invalid value; it must be either '1' or '0'",
name
);
std::process::exit(1);
}
}
fn build_std_required(cargo_command: &CargoCommand) -> bool {
crate::get_bool_environment_variable(crate::WASM_BUILD_STD).unwrap_or_else(|| {
match runtime_target() {
RuntimeTarget::Wasm => !cargo_command.supports_wasm32v1_none_target(),
RuntimeTarget::Riscv => true,
}
})
}
#[derive(Copy, Clone, PartialEq, Eq)]
enum RuntimeTarget {
Wasm,
Riscv,
}
impl RuntimeTarget {
fn rustc_target(self, cargo_command: &CargoCommand) -> &'static str {
match self {
RuntimeTarget::Wasm => {
if cargo_command.supports_wasm32v1_none_target() {
"wasm32v1-none"
} else {
"wasm32-unknown-unknown"
}
}
RuntimeTarget::Riscv => "riscv32ema-unknown-none-elf",
}
}
fn build_subdirectory(self) -> &'static str {
match self {
RuntimeTarget::Wasm => "wbuild",
RuntimeTarget::Riscv => "rbuild",
}
}
}
fn runtime_target() -> RuntimeTarget {
let Some(value) = env::var_os(RUNTIME_TARGET) else {
return RuntimeTarget::Wasm;
};
if value == "wasm" {
RuntimeTarget::Wasm
} else if value == "riscv" {
RuntimeTarget::Riscv
} else {
build_helper::warning!(
"the '{RUNTIME_TARGET}' environment variable has an invalid value; it must be either 'wasm' or 'riscv'"
);
std::process::exit(1);
}
}