diff --git a/substrate/primitives/api/Cargo.toml b/substrate/primitives/api/Cargo.toml index a1438316289dd..cd882c7a050fe 100644 --- a/substrate/primitives/api/Cargo.toml +++ b/substrate/primitives/api/Cargo.toml @@ -21,13 +21,16 @@ sp-api-proc-macro = { path = "proc-macro", default-features = false } sp-core = { path = "../core", default-features = false } sp-std = { path = "../std", default-features = false } sp-runtime = { path = "../runtime", default-features = false } +sp-runtime-interface = { path = "../runtime-interface", default-features = false } sp-externalities = { path = "../externalities", default-features = false, optional = true } sp-version = { path = "../version", default-features = false } sp-state-machine = { path = "../state-machine", default-features = false, optional = true } sp-trie = { path = "../trie", default-features = false, optional = true } hash-db = { version = "0.16.0", optional = true } thiserror = { version = "1.0.48", optional = true } -scale-info = { version = "2.10.0", default-features = false, features = ["derive"] } +scale-info = { version = "2.10.0", default-features = false, features = [ + "derive", +] } sp-metadata-ir = { path = "../metadata-ir", default-features = false, optional = true } log = { version = "0.4.17", default-features = false } @@ -46,6 +49,7 @@ std = [ "sp-externalities", "sp-externalities?/std", "sp-metadata-ir?/std", + "sp-runtime-interface/std", "sp-runtime/std", "sp-state-machine/std", "sp-std/std", diff --git a/substrate/primitives/api/proc-macro/src/impl_runtime_apis.rs b/substrate/primitives/api/proc-macro/src/impl_runtime_apis.rs index fd81fdb624c1f..79378968437d9 100644 --- a/substrate/primitives/api/proc-macro/src/impl_runtime_apis.rs +++ b/substrate/primitives/api/proc-macro/src/impl_runtime_apis.rs @@ -237,7 +237,8 @@ fn generate_wasm_interface(impls: &[ItemImpl]) -> Result { #c::std_disabled! { #( #attrs )* #[no_mangle] - pub unsafe fn #fn_name(input_data: *mut u8, input_len: usize) -> u64 { + #[cfg_attr(any(target_arch = "riscv32", target_arch = "riscv64"), #c::__private::polkavm_export(abi = #c::__private::polkavm_abi))] + pub unsafe extern fn #fn_name(input_data: *mut u8, input_len: usize) -> u64 { let mut #input = if input_len == 0 { &[0u8; 0] } else { diff --git a/substrate/primitives/api/src/lib.rs b/substrate/primitives/api/src/lib.rs index 0a9b334a96f8e..190de1ab3fdee 100644 --- a/substrate/primitives/api/src/lib.rs +++ b/substrate/primitives/api/src/lib.rs @@ -105,6 +105,9 @@ pub mod __private { }; pub use sp_std::{mem, slice, vec}; pub use sp_version::{create_apis_vec, ApiId, ApisVec, RuntimeVersion}; + + #[cfg(all(any(target_arch = "riscv32", target_arch = "riscv64"), substrate_runtime))] + pub use sp_runtime_interface::polkavm::{polkavm_abi, polkavm_export}; } #[cfg(feature = "std")] diff --git a/substrate/primitives/io/src/lib.rs b/substrate/primitives/io/src/lib.rs index 20ea56bc26c98..6d34199416a36 100644 --- a/substrate/primitives/io/src/lib.rs +++ b/substrate/primitives/io/src/lib.rs @@ -1737,20 +1737,20 @@ mod tracing_setup { pub use tracing_setup::init_tracing; -/// Allocator used by Substrate when executing the Wasm runtime. -#[cfg(all(target_arch = "wasm32", not(feature = "std")))] -struct WasmAllocator; +/// Allocator used by Substrate from within the runtime. +#[cfg(substrate_runtime)] +struct RuntimeAllocator; -#[cfg(all(target_arch = "wasm32", not(feature = "disable_allocator"), not(feature = "std")))] +#[cfg(all(not(feature = "disable_allocator"), substrate_runtime))] #[global_allocator] -static ALLOCATOR: WasmAllocator = WasmAllocator; +static ALLOCATOR: RuntimeAllocator = RuntimeAllocator; -#[cfg(all(target_arch = "wasm32", not(feature = "std")))] +#[cfg(substrate_runtime)] mod allocator_impl { use super::*; use core::alloc::{GlobalAlloc, Layout}; - unsafe impl GlobalAlloc for WasmAllocator { + unsafe impl GlobalAlloc for RuntimeAllocator { unsafe fn alloc(&self, layout: Layout) -> *mut u8 { allocator::malloc(layout.size() as u32) } @@ -1761,8 +1761,27 @@ mod allocator_impl { } } -/// A default panic handler for WASM environment. -#[cfg(all(not(feature = "disable_panic_handler"), not(feature = "std")))] +/// Crashes the execution of the program. +/// +/// Equivalent to the WASM `unreachable` instruction, RISC-V `unimp` instruction, +/// or just the `unreachable!()` macro everywhere else. +pub fn unreachable() -> ! { + #[cfg(target_family = "wasm")] + { + core::arch::wasm32::unreachable(); + } + + #[cfg(any(target_arch = "riscv32", target_arch = "riscv64"))] + unsafe { + core::arch::asm!("unimp", options(noreturn)); + } + + #[cfg(not(any(target_arch = "riscv32", target_arch = "riscv64", target_family = "wasm")))] + unreachable!(); +} + +/// A default panic handler for the runtime environment. +#[cfg(all(not(feature = "disable_panic_handler"), substrate_runtime))] #[panic_handler] #[no_mangle] pub fn panic(info: &core::panic::PanicInfo) -> ! { @@ -1774,11 +1793,11 @@ pub fn panic(info: &core::panic::PanicInfo) -> ! { #[cfg(not(feature = "improved_panic_error_reporting"))] { logging::log(LogLevel::Error, "runtime", message.as_bytes()); - core::arch::wasm32::unreachable(); + unreachable(); } } -/// A default OOM handler for WASM environment. +/// A default OOM handler for the runtime environment. #[cfg(all(not(feature = "disable_oom"), enable_alloc_error_handler))] #[alloc_error_handler] pub fn oom(_: core::alloc::Layout) -> ! { @@ -1789,7 +1808,7 @@ pub fn oom(_: core::alloc::Layout) -> ! { #[cfg(not(feature = "improved_panic_error_reporting"))] { logging::log(LogLevel::Error, "runtime", b"Runtime memory exhausted. Aborting"); - core::arch::wasm32::unreachable(); + unreachable(); } } diff --git a/substrate/primitives/runtime-interface/Cargo.toml b/substrate/primitives/runtime-interface/Cargo.toml index 6e046567d1451..b4fab17eeb7c1 100644 --- a/substrate/primitives/runtime-interface/Cargo.toml +++ b/substrate/primitives/runtime-interface/Cargo.toml @@ -29,6 +29,9 @@ primitive-types = { version = "0.12.0", default-features = false } sp-storage = { path = "../storage", default-features = false } impl-trait-for-tuples = "0.2.2" +[target.'cfg(all(any(target_arch = "riscv32", target_arch = "riscv64"), substrate_runtime))'.dependencies] +polkavm-derive = { workspace = true } + [dev-dependencies] sp-runtime-interface-test-wasm = { path = "test-wasm" } sp-state-machine = { path = "../state-machine" } diff --git a/substrate/primitives/runtime-interface/proc-macro/src/runtime_interface/bare_function_interface.rs b/substrate/primitives/runtime-interface/proc-macro/src/runtime_interface/bare_function_interface.rs index 77a29bec3807f..32455b39eed6f 100644 --- a/substrate/primitives/runtime-interface/proc-macro/src/runtime_interface/bare_function_interface.rs +++ b/substrate/primitives/runtime-interface/proc-macro/src/runtime_interface/bare_function_interface.rs @@ -109,7 +109,12 @@ fn function_no_std_impl( }; let maybe_unreachable = if method.should_trap_on_return() { quote! { - ; core::arch::wasm32::unreachable(); + ; + #[cfg(target_family = "wasm")] + { core::arch::wasm32::unreachable(); } + + #[cfg(any(target_arch = "riscv32", target_arch = "riscv64"))] + unsafe { core::arch::asm!("unimp", options(noreturn)); } } } else { quote! {} @@ -118,7 +123,7 @@ fn function_no_std_impl( let attrs = method.attrs.iter().filter(|a| !a.path().is_ident("version")); let cfg_wasm_only = if is_wasm_only { - quote! { #[cfg(target_arch = "wasm32")] } + quote! { #[cfg(substrate_runtime)] } } else { quote! {} }; diff --git a/substrate/primitives/runtime-interface/proc-macro/src/runtime_interface/host_function_interface.rs b/substrate/primitives/runtime-interface/proc-macro/src/runtime_interface/host_function_interface.rs index 77a9e56eecba5..fc985157cdb7f 100644 --- a/substrate/primitives/runtime-interface/proc-macro/src/runtime_interface/host_function_interface.rs +++ b/substrate/primitives/runtime-interface/proc-macro/src/runtime_interface/host_function_interface.rs @@ -116,8 +116,8 @@ fn generate_extern_host_function( #(#cfg_attrs)* #[doc = #doc_string] pub fn #function ( #( #args ),* ) #return_value { + #[cfg_attr(any(target_arch = "riscv32", target_arch = "riscv64"), #crate_::polkavm::polkavm_import(abi = #crate_::polkavm::polkavm_abi))] extern "C" { - /// The extern function. pub fn #ext_function ( #( #arg_names: <#arg_types as #crate_::RIType>::FFIType ),* ) #ffi_return_value; diff --git a/substrate/primitives/runtime-interface/src/lib.rs b/substrate/primitives/runtime-interface/src/lib.rs index 1f1638880bb6c..8b0edf1ec818e 100644 --- a/substrate/primitives/runtime-interface/src/lib.rs +++ b/substrate/primitives/runtime-interface/src/lib.rs @@ -376,6 +376,9 @@ pub use sp_externalities::{ #[doc(hidden)] pub use codec; +#[cfg(all(any(target_arch = "riscv32", target_arch = "riscv64"), substrate_runtime))] +pub mod polkavm; + #[cfg(feature = "std")] pub mod host; pub(crate) mod impls; diff --git a/substrate/primitives/runtime-interface/src/polkavm.rs b/substrate/primitives/runtime-interface/src/polkavm.rs new file mode 100644 index 0000000000000..484a269fd14b7 --- /dev/null +++ b/substrate/primitives/runtime-interface/src/polkavm.rs @@ -0,0 +1,30 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub use polkavm_derive::{polkavm_export, polkavm_import}; + +#[polkavm_derive::polkavm_define_abi(allow_extra_input_registers)] +pub mod polkavm_abi {} + +impl self::polkavm_abi::FromHost for *mut u8 { + type Regs = (u32,); + + #[inline] + fn from_host((value,): Self::Regs) -> Self { + value as *mut u8 + } +} diff --git a/substrate/utils/wasm-builder/Cargo.toml b/substrate/utils/wasm-builder/Cargo.toml index f01adbc03d6cc..7abd1a202848f 100644 --- a/substrate/utils/wasm-builder/Cargo.toml +++ b/substrate/utils/wasm-builder/Cargo.toml @@ -26,3 +26,4 @@ sp-maybe-compressed-blob = { path = "../../primitives/maybe-compressed-blob" } filetime = "0.2.16" wasm-opt = "0.116" parity-wasm = "0.45" +polkavm-linker = { workspace = true } diff --git a/substrate/utils/wasm-builder/src/builder.rs b/substrate/utils/wasm-builder/src/builder.rs index 9c1655d85623b..d2aaff448bc5f 100644 --- a/substrate/utils/wasm-builder/src/builder.rs +++ b/substrate/utils/wasm-builder/src/builder.rs @@ -21,6 +21,8 @@ use std::{ process, }; +use crate::RuntimeTarget; + /// Returns the manifest dir from the `CARGO_MANIFEST_DIR` env. fn get_manifest_dir() -> PathBuf { env::var("CARGO_MANIFEST_DIR") @@ -49,6 +51,8 @@ impl WasmBuilderSelectProject { project_cargo_toml: get_manifest_dir().join("Cargo.toml"), features_to_enable: Vec::new(), disable_runtime_version_section_check: false, + export_heap_base: false, + import_memory: false, } } @@ -65,6 +69,8 @@ impl WasmBuilderSelectProject { project_cargo_toml: path, features_to_enable: Vec::new(), disable_runtime_version_section_check: false, + export_heap_base: false, + import_memory: false, }) } else { Err("Project path must point to the `Cargo.toml` of the project") @@ -97,6 +103,11 @@ pub struct WasmBuilder { features_to_enable: Vec, /// Should the builder not check that the `runtime_version` section exists in the wasm binary? disable_runtime_version_section_check: bool, + + /// Whether `__heap_base` should be exported (WASM-only). + export_heap_base: bool, + /// Whether `--import-memory` should be added to the link args (WASM-only). + import_memory: bool, } impl WasmBuilder { @@ -109,7 +120,7 @@ impl WasmBuilder { /// /// This adds `-Clink-arg=--export=__heap_base` to `RUST_FLAGS`. pub fn export_heap_base(mut self) -> Self { - self.rust_flags.push("-Clink-arg=--export=__heap_base".into()); + self.export_heap_base = true; self } @@ -127,7 +138,7 @@ impl WasmBuilder { /// /// This adds `-C link-arg=--import-memory` to `RUST_FLAGS`. pub fn import_memory(mut self) -> Self { - self.rust_flags.push("-C link-arg=--import-memory".into()); + self.import_memory = true; self } @@ -159,7 +170,18 @@ impl WasmBuilder { } /// Build the WASM binary. - pub fn build(self) { + pub fn build(mut self) { + let target = crate::runtime_target(); + if target == RuntimeTarget::Wasm { + if self.export_heap_base { + self.rust_flags.push("-Clink-arg=--export=__heap_base".into()); + } + + if self.import_memory { + self.rust_flags.push("-C link-arg=--import-memory".into()); + } + } + let out_dir = PathBuf::from(env::var("OUT_DIR").expect("`OUT_DIR` is set by cargo!")); let file_path = out_dir.join(self.file_name.clone().unwrap_or_else(|| "wasm_binary.rs".into())); @@ -175,6 +197,7 @@ impl WasmBuilder { } build_project( + target, file_path, self.project_cargo_toml, self.rust_flags.into_iter().map(|f| format!("{} ", f)).collect(), @@ -248,6 +271,7 @@ fn generate_rerun_if_changed_instructions() { /// `check_for_runtime_version_section` - Should the wasm binary be checked for the /// `runtime_version` section? fn build_project( + target: RuntimeTarget, file_name: PathBuf, project_cargo_toml: PathBuf, default_rustflags: String, @@ -255,7 +279,7 @@ fn build_project( wasm_binary_name: Option, check_for_runtime_version_section: bool, ) { - let cargo_cmd = match crate::prerequisites::check() { + let cargo_cmd = match crate::prerequisites::check(target) { Ok(cmd) => cmd, Err(err_msg) => { eprintln!("{}", err_msg); @@ -264,6 +288,7 @@ fn build_project( }; let (wasm_binary, bloaty) = crate::wasm_project::create_and_compile( + target, &project_cargo_toml, &default_rustflags, cargo_cmd, diff --git a/substrate/utils/wasm-builder/src/lib.rs b/substrate/utils/wasm-builder/src/lib.rs index ec85fd1ffddbf..5cde48c0950b3 100644 --- a/substrate/utils/wasm-builder/src/lib.rs +++ b/substrate/utils/wasm-builder/src/lib.rs @@ -113,6 +113,7 @@ //! wasm32-unknown-unknown --toolchain nightly-2020-02-20`. use std::{ + collections::BTreeSet, env, fs, io::BufRead, path::{Path, PathBuf}, @@ -164,6 +165,9 @@ const WASM_BUILD_WORKSPACE_HINT: &str = "WASM_BUILD_WORKSPACE_HINT"; /// Environment variable to set whether we'll build `core`/`std`. const WASM_BUILD_STD: &str = "WASM_BUILD_STD"; +/// The target to use for the runtime. Valid values are `wasm` (default) or `riscv`. +const RUNTIME_TARGET: &str = "SUBSTRATE_RUNTIME_TARGET"; + /// Write to the given `file` if the `content` is different. fn write_file_if_changed(file: impl AsRef, content: impl AsRef) { if fs::read_to_string(file.as_ref()).ok().as_deref() != Some(content.as_ref()) { @@ -185,7 +189,7 @@ fn copy_file_if_changed(src: PathBuf, dst: PathBuf) { } /// Get a cargo command that should be used to invoke the compilation. -fn get_cargo_command() -> CargoCommand { +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"); @@ -196,35 +200,33 @@ fn get_cargo_command() -> CargoCommand { wasm_toolchain.map(|t| CargoCommand::new_with_args("rustup", &["run", &t, "cargo"])) { cmd - } else if env_cargo.supports_substrate_wasm_env() { + } else if env_cargo.supports_substrate_runtime_env(target) { env_cargo - } else if default_cargo.supports_substrate_wasm_env() { + } else if default_cargo.supports_substrate_runtime_env(target) { default_cargo } else { // If no command before provided us with a cargo that supports our Substrate wasm env, we // try to search one with rustup. If that fails as well, we return the default cargo and let // the prequisities check fail. - get_rustup_command().unwrap_or(default_cargo) + get_rustup_command(target).unwrap_or(default_cargo) } } -/// Get the newest rustup command that supports our Substrate wasm env. +/// Get the newest rustup command that supports compiling a runtime. /// /// Stable versions are always favored over nightly versions even if the nightly versions are /// newer. -fn get_rustup_command() -> Option { - let host = format!("-{}", env::var("HOST").expect("`HOST` is always set by cargo")); - +fn get_rustup_command(target: RuntimeTarget) -> Option { 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.trim_end_matches(&host); - + // Split by a space to get rid of e.g. " (default)" at the end. + let rustup_version = line.split(" ").next().unwrap(); let cmd = CargoCommand::new_with_args("rustup", &["run", &rustup_version, "cargo"]); - if !cmd.supports_substrate_wasm_env() { + if !cmd.supports_substrate_runtime_env(target) { continue } @@ -247,22 +249,26 @@ struct CargoCommand { program: String, args: Vec, version: Option, + target_list: Option>, } 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 } + 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, } } @@ -283,6 +289,23 @@ impl CargoCommand { Version::extract(&version) } + fn extract_target_list(program: &str, args: &[&str]) -> Option> { + // This is technically an unstable option, but we don't care because we only need this + // to build RISC-V runtimes, and those currently require a specific nightly toolchain + // anyway, so it's totally fine for this to fail in other cases. + let list = Command::new(program) + .args(args) + .args(&["rustc", "-Z", "unstable-options", "--print", "target-list"]) + // Make sure if we're called from within a `build.rs` the host toolchain won't override + // a rustup toolchain we've picked. + .env_remove("RUSTC") + .output() + .ok() + .and_then(|o| String::from_utf8(o.stdout).ok())?; + + Some(list.trim().split("\n").map(ToString::to_string).collect()) + } + /// Returns the version of this cargo command or `None` if it failed to extract the version. fn version(&self) -> Option { self.version @@ -294,12 +317,29 @@ impl CargoCommand { env::var("RUSTC_BOOTSTRAP").is_ok() } + /// Check if the supplied cargo command supports our runtime environment. + 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(), + } + } + + /// Check if the supplied cargo command supports our RISC-V runtime environment. + fn supports_substrate_runtime_env_riscv(&self) -> bool { + let Some(target_list) = self.target_list.as_ref() else { return false }; + // This is our custom target which currently doesn't exist on any upstream toolchain, + // so if it exists it's guaranteed to be our custom toolchain and have have everything + // we need, so any further version checks are unnecessary at this point. + target_list.contains("riscv32ema-unknown-none-elf") + } + /// Check if the supplied cargo command supports our Substrate wasm environment. /// /// This means that either the cargo version is at minimum 1.68.0 or this is a nightly cargo. /// /// Assumes that cargo version matches the rustc version. - fn supports_substrate_wasm_env(&self) -> bool { + fn supports_substrate_runtime_env_wasm(&self) -> bool { // `RUSTC_BOOTSTRAP` tells a stable compiler to behave like a nightly. So, when this env // variable is set, we can assume that whatever rust compiler we have, it is a nightly // compiler. For "more" information, see: @@ -365,5 +405,48 @@ fn get_bool_environment_variable(name: &str) -> Option { /// Returns whether we need to also compile the standard library when compiling the runtime. fn build_std_required() -> bool { - crate::get_bool_environment_variable(crate::WASM_BUILD_STD).unwrap_or(true) + let default = runtime_target() == RuntimeTarget::Wasm; + + crate::get_bool_environment_variable(crate::WASM_BUILD_STD).unwrap_or(default) +} + +#[derive(Copy, Clone, PartialEq, Eq)] +enum RuntimeTarget { + Wasm, + Riscv, +} + +impl RuntimeTarget { + fn rustc_target(self) -> &'static str { + match self { + RuntimeTarget::Wasm => "wasm32-unknown-unknown", + RuntimeTarget::Riscv => "riscv32ema-unknown-none-elf", + } + } + + fn build_subdirectory(self) -> &'static str { + // Keep the build directories separate so that when switching between + // the targets we won't trigger unnecessary rebuilds. + 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); + } } diff --git a/substrate/utils/wasm-builder/src/prerequisites.rs b/substrate/utils/wasm-builder/src/prerequisites.rs index 99eb6ee1f18fc..a601e3210dd0c 100644 --- a/substrate/utils/wasm-builder/src/prerequisites.rs +++ b/substrate/utils/wasm-builder/src/prerequisites.rs @@ -15,14 +15,19 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::{write_file_if_changed, CargoCommand, CargoCommandVersioned}; +use crate::{write_file_if_changed, CargoCommand, CargoCommandVersioned, RuntimeTarget}; use console::style; -use std::{fs, path::Path}; +use std::{ + fs, + path::{Path, PathBuf}, + process::Command, +}; + use tempfile::tempdir; -/// Print an error message. -fn print_error_message(message: &str) -> String { +/// Colorizes an error message, if color output is enabled. +fn colorize_error_message(message: &str) -> String { if super::color_output_enabled() { style(message).red().bold().to_string() } else { @@ -30,121 +35,165 @@ fn print_error_message(message: &str) -> String { } } +/// Colorizes an auxiliary message, if color output is enabled. +fn colorize_aux_message(message: &str) -> String { + if super::color_output_enabled() { + style(message).yellow().bold().to_string() + } else { + message.into() + } +} + /// Checks that all prerequisites are installed. /// /// Returns the versioned cargo command on success. -pub(crate) fn check() -> Result { - let cargo_command = crate::get_cargo_command(); - - if !cargo_command.supports_substrate_wasm_env() { - return Err(print_error_message( - "Cannot compile the WASM runtime: no compatible Rust compiler found!\n\ - Install at least Rust 1.68.0 or a recent nightly version.", - )) - } +pub(crate) fn check(target: RuntimeTarget) -> Result { + let cargo_command = crate::get_cargo_command(target); + match target { + RuntimeTarget::Wasm => { + if !cargo_command.supports_substrate_runtime_env(target) { + return Err(colorize_error_message( + "Cannot compile a WASM runtime: no compatible Rust compiler found!\n\ + Install at least Rust 1.68.0 or a recent nightly version.", + )); + } - check_wasm_toolchain_installed(cargo_command) + check_wasm_toolchain_installed(cargo_command) + }, + RuntimeTarget::Riscv => { + if !cargo_command.supports_substrate_runtime_env(target) { + return Err(colorize_error_message( + "Cannot compile a RISC-V runtime: no compatible Rust compiler found!\n\ + Install a toolchain from here and try again: https://github.com/paritytech/rustc-rv32e-toolchain/", + )); + } + + let dummy_crate = DummyCrate::new(&cargo_command, target); + let version = dummy_crate.get_rustc_version(); + Ok(CargoCommandVersioned::new(cargo_command, version)) + }, + } } -/// Creates a minimal dummy crate at the given path and returns the manifest path. -fn create_minimal_crate(project_dir: &Path) -> std::path::PathBuf { - fs::create_dir_all(project_dir.join("src")).expect("Creating src dir does not fail; qed"); - - let manifest_path = project_dir.join("Cargo.toml"); - write_file_if_changed( - &manifest_path, - r#" - [package] - name = "wasm-test" - version = "1.0.0" - edition = "2021" - - [workspace] - "#, - ); - - write_file_if_changed(project_dir.join("src/main.rs"), "fn main() {}"); - manifest_path +struct DummyCrate<'a> { + cargo_command: &'a CargoCommand, + temp: tempfile::TempDir, + manifest_path: PathBuf, + target: RuntimeTarget, } -fn check_wasm_toolchain_installed( - cargo_command: CargoCommand, -) -> Result { - let temp = tempdir().expect("Creating temp dir does not fail; qed"); - let manifest_path = create_minimal_crate(temp.path()).display().to_string(); +impl<'a> DummyCrate<'a> { + /// Creates a minimal dummy crate. + fn new(cargo_command: &'a CargoCommand, target: RuntimeTarget) -> Self { + let temp = tempdir().expect("Creating temp dir does not fail; qed"); + let project_dir = temp.path(); + fs::create_dir_all(project_dir.join("src")).expect("Creating src dir does not fail; qed"); + + let manifest_path = project_dir.join("Cargo.toml"); + write_file_if_changed( + &manifest_path, + r#" + [package] + name = "dummy-crate" + version = "1.0.0" + edition = "2021" + + [workspace] + "#, + ); + + write_file_if_changed(project_dir.join("src/main.rs"), "fn main() {}"); + DummyCrate { cargo_command, temp, manifest_path, target } + } - let prepare_command = |subcommand| { - let mut cmd = cargo_command.command(); + fn prepare_command(&self, subcommand: &str) -> Command { + let mut cmd = self.cargo_command.command(); // Chdir to temp to avoid including project's .cargo/config.toml // by accident - it can happen in some CI environments. - cmd.current_dir(&temp); - cmd.args(&[ - subcommand, - "--target=wasm32-unknown-unknown", - "--manifest-path", - &manifest_path, - ]); + cmd.current_dir(&self.temp); + cmd.arg(subcommand) + .arg(format!("--target={}", self.target.rustc_target())) + .args(&["--manifest-path", &self.manifest_path.display().to_string()]); if super::color_output_enabled() { cmd.arg("--color=always"); } // manually set the `CARGO_TARGET_DIR` to prevent a cargo deadlock - let target_dir = temp.path().join("target").display().to_string(); + let target_dir = self.temp.path().join("target").display().to_string(); cmd.env("CARGO_TARGET_DIR", &target_dir); // Make sure the host's flags aren't used here, e.g. if an alternative linker is specified // in the RUSTFLAGS then the check we do here will break unless we clear these. cmd.env_remove("CARGO_ENCODED_RUSTFLAGS"); cmd.env_remove("RUSTFLAGS"); + // Make sure if we're called from within a `build.rs` the host toolchain won't override a + // rustup toolchain we've picked. + cmd.env_remove("RUSTC"); cmd - }; - - let err_msg = - print_error_message("Rust WASM toolchain is not properly installed; please install it!"); - let build_result = prepare_command("build").output().map_err(|_| err_msg.clone())?; - if !build_result.status.success() { - return match String::from_utf8(build_result.stderr) { - Ok(ref err) if err.contains("the `wasm32-unknown-unknown` target may not be installed") => - Err(print_error_message("Cannot compile the WASM runtime: the `wasm32-unknown-unknown` target is not installed!\n\ - You can install it with `rustup target add wasm32-unknown-unknown` if you're using `rustup`.")), + } - // Apparently this can happen when we're running on a non Tier 1 platform. - Ok(ref err) if err.contains("linker `rust-lld` not found") => - Err(print_error_message("Cannot compile the WASM runtime: `rust-lld` not found!")), + fn get_rustc_version(&self) -> String { + let mut run_cmd = self.prepare_command("rustc"); + run_cmd.args(&["-q", "--", "--version"]); + run_cmd + .output() + .ok() + .and_then(|o| String::from_utf8(o.stdout).ok()) + .unwrap_or_else(|| "unknown rustc version".into()) + } - Ok(ref err) => Err(format!( - "{}\n\n{}\n{}\n{}{}\n", - err_msg, - style("Further error information:").yellow().bold(), - style("-".repeat(60)).yellow().bold(), - err, - style("-".repeat(60)).yellow().bold(), - )), - - Err(_) => Err(err_msg), - }; + fn get_sysroot(&self) -> Option { + let mut sysroot_cmd = self.prepare_command("rustc"); + sysroot_cmd.args(&["-q", "--", "--print", "sysroot"]); + sysroot_cmd.output().ok().and_then(|o| String::from_utf8(o.stdout).ok()) } - let mut run_cmd = prepare_command("rustc"); - run_cmd.args(&["-q", "--", "--version"]); + fn try_build(&self) -> Result<(), Option> { + let Ok(result) = self.prepare_command("build").output() else { return Err(None) }; + if !result.status.success() { + return Err(Some(String::from_utf8_lossy(&result.stderr).into())); + } + Ok(()) + } +} - let version = run_cmd - .output() - .ok() - .and_then(|o| String::from_utf8(o.stdout).ok()) - .unwrap_or_else(|| "unknown rustc version".into()); +fn check_wasm_toolchain_installed( + cargo_command: CargoCommand, +) -> Result { + let dummy_crate = DummyCrate::new(&cargo_command, RuntimeTarget::Wasm); + + if let Err(error) = dummy_crate.try_build() { + let basic_error_message = colorize_error_message( + "Rust WASM toolchain is not properly installed; please install it!", + ); + return match error { + None => Err(basic_error_message), + Some(error) if error.contains("the `wasm32-unknown-unknown` target may not be installed") => { + Err(colorize_error_message("Cannot compile the WASM runtime: the `wasm32-unknown-unknown` target is not installed!\n\ + You can install it with `rustup target add wasm32-unknown-unknown` if you're using `rustup`.")) + }, + // Apparently this can happen when we're running on a non Tier 1 platform. + Some(ref error) if error.contains("linker `rust-lld` not found") => + Err(colorize_error_message("Cannot compile the WASM runtime: `rust-lld` not found!")), + Some(error) => Err(format!( + "{}\n\n{}\n{}\n{}{}\n", + basic_error_message, + colorize_aux_message("Further error information:"), + colorize_aux_message(&"-".repeat(60)), + error, + colorize_aux_message(&"-".repeat(60)), + )) + } + } + let version = dummy_crate.get_rustc_version(); if crate::build_std_required() { - let mut sysroot_cmd = prepare_command("rustc"); - sysroot_cmd.args(&["-q", "--", "--print", "sysroot"]); - if let Some(sysroot) = - sysroot_cmd.output().ok().and_then(|o| String::from_utf8(o.stdout).ok()) - { + if let Some(sysroot) = dummy_crate.get_sysroot() { let src_path = Path::new(sysroot.trim()).join("lib").join("rustlib").join("src").join("rust"); if !src_path.exists() { - return Err(print_error_message( + return Err(colorize_error_message( "Cannot compile the WASM runtime: no standard library sources found!\n\ You can install them with `rustup component add rust-src` if you're using `rustup`.", )) diff --git a/substrate/utils/wasm-builder/src/wasm_project.rs b/substrate/utils/wasm-builder/src/wasm_project.rs index ded6b2188b0b3..99e072f26825f 100644 --- a/substrate/utils/wasm-builder/src/wasm_project.rs +++ b/substrate/utils/wasm-builder/src/wasm_project.rs @@ -15,7 +15,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::{write_file_if_changed, CargoCommandVersioned, OFFLINE}; +use crate::{write_file_if_changed, CargoCommandVersioned, RuntimeTarget, OFFLINE}; use build_helper::rerun_if_changed; use cargo_metadata::{DependencyKind, Metadata, MetadataCommand}; @@ -112,6 +112,7 @@ fn crate_metadata(cargo_manifest: &Path) -> Metadata { /// /// The path to the compact runtime binary and the bloaty runtime binary. pub(crate) fn create_and_compile( + target: RuntimeTarget, project_cargo_toml: &Path, default_rustflags: &str, cargo_cmd: CargoCommandVersioned, @@ -120,11 +121,12 @@ pub(crate) fn create_and_compile( check_for_runtime_version_section: bool, ) -> (Option, WasmBinaryBloaty) { let runtime_workspace_root = get_wasm_workspace_root(); - let runtime_workspace = runtime_workspace_root.join("wbuild"); + let runtime_workspace = runtime_workspace_root.join(target.build_subdirectory()); let crate_metadata = crate_metadata(project_cargo_toml); let project = create_project( + target, project_cargo_toml, &runtime_workspace, &crate_metadata, @@ -132,13 +134,58 @@ pub(crate) fn create_and_compile( features_to_enable, ); - let build_config = BuildConfiguration::detect(&project); + let build_config = BuildConfiguration::detect(target, &project); // Build the bloaty runtime blob - build_bloaty_blob(&build_config.blob_build_profile, &project, default_rustflags, cargo_cmd); + let raw_blob_path = build_bloaty_blob( + target, + &build_config.blob_build_profile, + &project, + default_rustflags, + cargo_cmd, + ); + let (final_blob_binary, bloaty_blob_binary) = match target { + RuntimeTarget::Wasm => compile_wasm( + project_cargo_toml, + &project, + bloaty_blob_out_name_override, + check_for_runtime_version_section, + &build_config, + ), + RuntimeTarget::Riscv => { + let out_name = bloaty_blob_out_name_override + .unwrap_or_else(|| get_blob_name(target, project_cargo_toml)); + let out_path = project.join(format!("{out_name}.polkavm")); + fs::copy(raw_blob_path, &out_path).expect("copying the runtime blob should never fail"); + (None, WasmBinaryBloaty(out_path)) + }, + }; + + generate_rerun_if_changed_instructions( + project_cargo_toml, + &project, + &runtime_workspace, + final_blob_binary.as_ref(), + &bloaty_blob_binary, + ); + + if let Err(err) = adjust_mtime(&bloaty_blob_binary, final_blob_binary.as_ref()) { + build_helper::warning!("Error while adjusting the mtime of the blob binaries: {}", err) + } + + (final_blob_binary, bloaty_blob_binary) +} + +fn compile_wasm( + project_cargo_toml: &Path, + project: &Path, + bloaty_blob_out_name_override: Option, + check_for_runtime_version_section: bool, + build_config: &BuildConfiguration, +) -> (Option, WasmBinaryBloaty) { // Get the name of the bloaty runtime blob. - let bloaty_blob_default_name = get_blob_name(project_cargo_toml); + let bloaty_blob_default_name = get_blob_name(RuntimeTarget::Wasm, project_cargo_toml); let bloaty_blob_out_name = bloaty_blob_out_name_override.unwrap_or_else(|| bloaty_blob_default_name.clone()); @@ -183,19 +230,6 @@ pub(crate) fn create_and_compile( }); let final_blob_binary = compact_compressed_blob_path.or(compact_blob_path); - - generate_rerun_if_changed_instructions( - project_cargo_toml, - &project, - &runtime_workspace, - final_blob_binary.as_ref(), - &bloaty_blob_binary, - ); - - if let Err(err) = adjust_mtime(&bloaty_blob_binary, final_blob_binary.as_ref()) { - build_helper::warning!("Error while adjusting the mtime of the blob binaries: {}", err) - } - (final_blob_binary, bloaty_blob_binary) } @@ -314,8 +348,12 @@ fn get_crate_name(cargo_manifest: &Path) -> String { } /// Returns the name for the blob binary. -fn get_blob_name(cargo_manifest: &Path) -> String { - get_crate_name(cargo_manifest).replace('-', "_") +fn get_blob_name(target: RuntimeTarget, cargo_manifest: &Path) -> String { + let crate_name = get_crate_name(cargo_manifest); + match target { + RuntimeTarget::Wasm => crate_name.replace('-', "_"), + RuntimeTarget::Riscv => crate_name, + } } /// Returns the root path of the wasm workspace. @@ -336,6 +374,7 @@ fn get_wasm_workspace_root() -> PathBuf { } fn create_project_cargo_toml( + target: RuntimeTarget, wasm_workspace: &Path, workspace_root_path: &Path, crate_name: &str, @@ -396,17 +435,18 @@ fn create_project_cargo_toml( } let mut package = Table::new(); - package.insert("name".into(), format!("{}-wasm", crate_name).into()); + package.insert("name".into(), format!("{}-blob", crate_name).into()); package.insert("version".into(), "1.0.0".into()); package.insert("edition".into(), "2021".into()); wasm_workspace_toml.insert("package".into(), package.into()); - let mut lib = Table::new(); - lib.insert("name".into(), wasm_binary.into()); - lib.insert("crate-type".into(), vec!["cdylib".to_string()].into()); - - wasm_workspace_toml.insert("lib".into(), lib.into()); + if target == RuntimeTarget::Wasm { + let mut lib = Table::new(); + lib.insert("name".into(), wasm_binary.into()); + lib.insert("crate-type".into(), vec!["cdylib".to_string()].into()); + wasm_workspace_toml.insert("lib".into(), lib.into()); + } let mut dependencies = Table::new(); @@ -527,6 +567,7 @@ fn has_runtime_wasm_feature_declared( /// /// The path to the created wasm project. fn create_project( + target: RuntimeTarget, project_cargo_toml: &Path, wasm_workspace: &Path, crate_metadata: &Metadata, @@ -535,7 +576,7 @@ fn create_project( ) -> PathBuf { let crate_name = get_crate_name(project_cargo_toml); let crate_path = project_cargo_toml.parent().expect("Parent path exists; qed"); - let wasm_binary = get_blob_name(project_cargo_toml); + let wasm_binary = get_blob_name(target, project_cargo_toml); let wasm_project_folder = wasm_workspace.join(&crate_name); fs::create_dir_all(wasm_project_folder.join("src")) @@ -552,6 +593,7 @@ fn create_project( enabled_features.extend(features_to_enable.into_iter()); create_project_cargo_toml( + target, &wasm_project_folder, workspace_root_path, &crate_name, @@ -560,10 +602,20 @@ fn create_project( enabled_features.into_iter(), ); - write_file_if_changed( - wasm_project_folder.join("src/lib.rs"), - "#![no_std] pub use wasm_project::*;", - ); + match target { + RuntimeTarget::Wasm => { + write_file_if_changed( + wasm_project_folder.join("src/lib.rs"), + "#![no_std] pub use wasm_project::*;", + ); + }, + RuntimeTarget::Riscv => { + write_file_if_changed( + wasm_project_folder.join("src/main.rs"), + "#![no_std] #![no_main] pub use wasm_project::*;", + ); + }, + } if let Some(crate_lock_file) = find_cargo_lock(project_cargo_toml) { // Use the `Cargo.lock` of the main project. @@ -641,14 +693,15 @@ impl BuildConfiguration { /// # Note /// /// Can be overriden by setting [`crate::WASM_BUILD_TYPE_ENV`]. - fn detect(wasm_project: &Path) -> Self { + fn detect(target: RuntimeTarget, wasm_project: &Path) -> Self { let (name, overriden) = if let Ok(name) = env::var(crate::WASM_BUILD_TYPE_ENV) { (name, true) } else { // First go backwards to the beginning of the target directory. - // Then go forwards to find the "wbuild" directory. + // Then go forwards to find the build subdirectory. // We need to go backwards first because when starting from the root there - // might be a chance that someone has a "wbuild" directory somewhere in the path. + // might be a chance that someone has a directory somewhere in the path with the same + // name. let name = wasm_project .components() .rev() @@ -656,9 +709,9 @@ impl BuildConfiguration { .collect::>() .iter() .rev() - .take_while(|c| c.as_os_str() != "wbuild") + .take_while(|c| c.as_os_str() != target.build_subdirectory()) .last() - .expect("We put the wasm project within a `target/.../wbuild` path; qed") + .expect("We put the runtime project within a `target/.../[rw]build` path; qed") .as_os_str() .to_str() .expect("All our profile directory names are ascii; qed") @@ -711,22 +764,34 @@ fn offline_build() -> bool { /// Build the project and create the bloaty runtime blob. fn build_bloaty_blob( + target: RuntimeTarget, blob_build_profile: &Profile, project: &Path, default_rustflags: &str, cargo_cmd: CargoCommandVersioned, -) { +) -> PathBuf { let manifest_path = project.join("Cargo.toml"); let mut build_cmd = cargo_cmd.command(); - let rustflags = format!( - "-C target-cpu=mvp -C target-feature=-sign-ext -C link-arg=--export-table {} {}", - default_rustflags, - env::var(crate::WASM_BUILD_RUSTFLAGS_ENV).unwrap_or_default(), - ); + let mut rustflags = String::new(); + match target { + RuntimeTarget::Wasm => { + rustflags.push_str( + "-C target-cpu=mvp -C target-feature=-sign-ext -C link-arg=--export-table ", + ); + }, + RuntimeTarget::Riscv => { + rustflags.push_str("-C target-feature=+lui-addi-fusion -C relocation-model=pie -C link-arg=--emit-relocs -C link-arg=--unique "); + }, + } + + rustflags.push_str(default_rustflags); + rustflags.push_str(" --cfg substrate_runtime "); + rustflags.push_str(&env::var(crate::WASM_BUILD_RUSTFLAGS_ENV).unwrap_or_default()); build_cmd - .args(&["rustc", "--target=wasm32-unknown-unknown"]) + .arg("rustc") + .arg(format!("--target={}", target.rustc_target())) .arg(format!("--manifest-path={}", manifest_path.display())) .env("RUSTFLAGS", rustflags) // Manually set the `CARGO_TARGET_DIR` to prevent a cargo deadlock (cargo locks a target dir @@ -737,6 +802,9 @@ fn build_bloaty_blob( // our own `RUSTFLAGS` and thus, we need to remove this. Otherwise cargo favors this // env variable. .env_remove("CARGO_ENCODED_RUSTFLAGS") + // Make sure if we're called from within a `build.rs` the host toolchain won't override a + // rustup toolchain we've picked. + .env_remove("RUSTC") // We don't want to call ourselves recursively .env(crate::SKIP_BUILD_ENV, ""); @@ -778,6 +846,52 @@ fn build_bloaty_blob( if build_cmd.status().map(|s| s.success()).is_err() { process::exit(1); } + + let blob_name = get_blob_name(target, &manifest_path); + let target_directory = project + .join("target") + .join(target.rustc_target()) + .join(blob_build_profile.directory()); + match target { + RuntimeTarget::Riscv => { + let elf_path = target_directory.join(&blob_name); + let elf_metadata = match elf_path.metadata() { + Ok(path) => path, + Err(error) => + panic!("internal error: couldn't read the metadata of {elf_path:?}: {error}"), + }; + + let polkavm_path = target_directory.join(format!("{}.polkavm", blob_name)); + if polkavm_path + .metadata() + .map(|polkavm_metadata| { + polkavm_metadata.modified().unwrap() >= elf_metadata.modified().unwrap() + }) + .unwrap_or(true) + { + let blob_bytes = + std::fs::read(elf_path).expect("binary always exists after its built"); + + let mut config = polkavm_linker::Config::default(); + config.set_strip(true); // TODO: This shouldn't always be done. + + let program = match polkavm_linker::program_from_elf(config, &blob_bytes) { + Ok(program) => program, + Err(error) => { + println!("Failed to link the runtime blob; this is probably a bug!"); + println!("Linking error: {error}"); + process::exit(1); + }, + }; + + std::fs::write(&polkavm_path, program.as_bytes()) + .expect("writing the blob to a file always works"); + } + + polkavm_path + }, + RuntimeTarget::Wasm => target_directory.join(format!("{}.wasm", blob_name)), + } } fn compact_wasm( @@ -786,7 +900,7 @@ fn compact_wasm( cargo_manifest: &Path, out_name: &str, ) -> Option { - let default_out_name = get_blob_name(cargo_manifest); + let default_out_name = get_blob_name(RuntimeTarget::Wasm, cargo_manifest); let in_path = project .join("target/wasm32-unknown-unknown") .join(inner_profile.directory()) @@ -973,6 +1087,7 @@ fn generate_rerun_if_changed_instructions( println!("cargo:rerun-if-env-changed={}", crate::WASM_TARGET_DIRECTORY); println!("cargo:rerun-if-env-changed={}", crate::WASM_BUILD_TOOLCHAIN); println!("cargo:rerun-if-env-changed={}", crate::WASM_BUILD_STD); + println!("cargo:rerun-if-env-changed={}", crate::RUNTIME_TARGET); } /// Track files and paths related to the given package to rerun `build.rs` on any relevant change. @@ -1018,7 +1133,7 @@ fn copy_blob_to_target_directory(cargo_manifest: &Path, blob_binary: &WasmBinary fs::copy( blob_binary.wasm_binary_path(), - target_dir.join(format!("{}.wasm", get_blob_name(cargo_manifest))), + target_dir.join(format!("{}.wasm", get_blob_name(RuntimeTarget::Wasm, cargo_manifest))), ) .expect("Copies blob binary to `WASM_TARGET_DIRECTORY`."); }