diff --git a/Cargo.lock b/Cargo.lock index 9d5e5ad87dbb95..d7fdc36bb0fbd8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -440,6 +440,17 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "cargo_metadata" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5a5f7b42f606b7f23674f6f4d877628350682bc40687d3fae65679a58d55345" +dependencies = [ + "semver 0.11.0", + "serde", + "serde_json", +] + [[package]] name = "cast" version = "0.2.3" @@ -499,9 +510,9 @@ dependencies = [ [[package]] name = "clap" -version = "2.33.1" +version = "2.33.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdfa80d47f954d53a35a64987ca1422f495b8d6483c0fe9f7117b36c2a792129" +checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" dependencies = [ "ansi_term", "atty", @@ -3158,6 +3169,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" dependencies = [ "semver-parser 0.10.1", + "serde", ] [[package]] @@ -3216,9 +3228,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.56" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3433e879a558dde8b5e8feb2a04899cf34fdde1fafb894687e52105fc1162ac3" +checksum = "dcac07dbffa1c65e7f816ab9eba78eb142c6d44410f4eeba1e26e4f5dfa56b95" dependencies = [ "itoa", "ryu", @@ -3599,6 +3611,14 @@ dependencies = [ "thiserror", ] +[[package]] +name = "solana-cargo-build-bpf" +version = "1.5.0" +dependencies = [ + "cargo_metadata", + "clap", +] + [[package]] name = "solana-clap-utils" version = "1.5.0" diff --git a/Cargo.toml b/Cargo.toml index 97daafeb1a3b96..310c4c50b2a4d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,6 +53,7 @@ members = [ "ramp-tps", "runtime", "sdk", + "sdk/cargo-build-bpf", "scripts", "stake-accounts", "stake-monitor", diff --git a/cargo-build-bpf b/cargo-build-bpf new file mode 100755 index 00000000000000..cae0bbf4d8e4b9 --- /dev/null +++ b/cargo-build-bpf @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +here=$(dirname "$0") +set -x +exec cargo run --bin cargo-build-bpf -- --bpf-sdk $here/sdk/bpf "$@" diff --git a/frozen-abi/Xargo.toml b/frozen-abi/Xargo.toml new file mode 100644 index 00000000000000..475fb71ed15ac2 --- /dev/null +++ b/frozen-abi/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/scripts/cargo-install-all.sh b/scripts/cargo-install-all.sh index 7f3c1f09167dc5..22a6906be295ae 100755 --- a/scripts/cargo-install-all.sh +++ b/scripts/cargo-install-all.sh @@ -75,6 +75,7 @@ else BINS=( + cargo-build-bpf solana solana-bench-exchange solana-bench-tps diff --git a/sdk/bpf/env.sh b/sdk/bpf/env.sh new file mode 100644 index 00000000000000..821727769447ef --- /dev/null +++ b/sdk/bpf/env.sh @@ -0,0 +1,39 @@ +# +# Configures the BPF SDK environment +# + +if [ -z "$bpf_sdk" ]; then + bpf_sdk=. +fi + +# Ensure the sdk is installed +"$bpf_sdk"/scripts/install.sh + +# Use the SDK's version of llvm to build the compiler-builtins for BPF +export CC="$bpf_sdk/dependencies/llvm-native/bin/clang" +export AR="$bpf_sdk/dependencies/llvm-native/bin/llvm-ar" +export OBJDUMP="$bpf_sdk/dependencies/llvm-native/bin/llvm-objdump" +export OBJCOPY="$bpf_sdk/dependencies/llvm-native/bin/llvm-objcopy" + +# Use the SDK's version of Rust to build for BPF +export RUSTUP_TOOLCHAIN=bpf +export RUSTFLAGS=" + -C lto=no \ + -C opt-level=2 \ + -C link-arg=-z -C link-arg=notext \ + -C link-arg=-T$bpf_sdk/rust/bpf.ld \ + -C link-arg=--Bdynamic \ + -C link-arg=-shared \ + -C link-arg=--entry=entrypoint \ + -C link-arg=-no-threads \ + -C linker=$bpf_sdk/dependencies/llvm-native/bin/ld.lld" + +# CARGO may be set if run from within cargo, causing +# incompatibilities between cargo and xargo versions +unset CARGO + +export XARGO="$bpf_sdk"/dependencies/bin/xargo +export XARGO_TARGET=bpfel-unknown-unknown +export XARGO_HOME="$bpf_sdk/dependencies/xargo" +export XARGO_RUST_SRC="$bpf_sdk/dependencies/rust-bpf-sysroot/src" +export RUST_COMPILER_RT_ROOT="$bpf_sdk/dependencies/rust-bpf-sysroot/src/compiler-rt" diff --git a/sdk/bpf/rust/build.sh b/sdk/bpf/rust/build.sh index baff4ae7d0a5b8..11ab8c158126b2 100755 --- a/sdk/bpf/rust/build.sh +++ b/sdk/bpf/rust/build.sh @@ -1,49 +1,21 @@ #!/usr/bin/env bash -if [ "$#" -ne 1 ]; then +if [[ "$#" -ne 1 ]]; then echo "Error: Must provide the full path to the project to build" exit 1 fi -if [ ! -f "$1/Cargo.toml" ]; then +if [[ ! -f "$1/Cargo.toml" ]]; then echo "Error: Cannot find project: $1" exit 1 fi +bpf_sdk=$(cd "$(dirname "$0")/.." && pwd) echo "Building $1" set -e - -bpf_sdk=$(cd "$(dirname "$0")/.." && pwd) - -# Ensure the sdk is installed -"$bpf_sdk"/scripts/install.sh - -# Use the SDK's version of llvm to build the compiler-builtins for BPF -export CC="$bpf_sdk/dependencies/llvm-native/bin/clang" -export AR="$bpf_sdk/dependencies/llvm-native/bin/llvm-ar" - -# Use the SDK's version of Rust to build for BPF -export RUSTUP_TOOLCHAIN=bpf -export RUSTFLAGS=" - -C lto=no \ - -C opt-level=2 \ - -C link-arg=-z -C link-arg=notext \ - -C link-arg=-T$bpf_sdk/rust/bpf.ld \ - -C link-arg=--Bdynamic \ - -C link-arg=-shared \ - -C link-arg=--entry=entrypoint \ - -C link-arg=-no-threads \ - -C linker=$bpf_sdk/dependencies/llvm-native/bin/ld.lld" - -# CARGO may be set if build.sh is run from within cargo, causing -# incompatibilities between cargo and xargo versions -unset CARGO - -# Setup xargo -export XARGO_HOME="$bpf_sdk/dependencies/xargo" -export XARGO_RUST_SRC="$bpf_sdk/dependencies/rust-bpf-sysroot/src" -export RUST_COMPILER_RT_ROOT="$bpf_sdk/dependencies/rust-bpf-sysroot/src/compiler-rt" +# shellcheck source=sdk/bpf/env.sh +source "$bpf_sdk"/env.sh cd "$1" -xargo build --target bpfel-unknown-unknown --release --no-default-features --features program +"$XARGO" build --target "$XARGO_TARGET" --release --no-default-features --features program { { set +x; } 2>/dev/null; echo Success; } diff --git a/sdk/bpf/rust/xargo-build.sh b/sdk/bpf/rust/xargo-build.sh new file mode 100755 index 00000000000000..0d860444221b1c --- /dev/null +++ b/sdk/bpf/rust/xargo-build.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash + +bpf_sdk=$(cd "$(dirname "$0")/.." && pwd) +# shellcheck source=sdk/bpf/env.sh +source "$bpf_sdk"/env.sh + +set -e +( + while true; do + if [[ -r Xargo.toml ]]; then + break; + fi + if [[ $PWD = / ]]; then + cat < /dev/null; then + echo "Error: rustfilt not found. It can be installed by running: cargo install rustfilt" + exit 1 +fi +if ! command -v readelf > /dev/null; then + if [[ $(uname) = Darwin ]]; then + echo "Error: readelf not found. It can be installed by running: brew install binutils" + else + echo "Error: readelf not found." + fi + exit 1 +fi + +dump_mangled=$dump.mangled + +( + set -ex + ls -la "$so" > "$dump_mangled" + readelf -aW "$so" >>"$dump_mangled" + "$OBJDUMP" -print-imm-hex --source --disassemble "$so" >> "$dump_mangled" + sed s/://g < "$dump_mangled" | rustfilt > "$dump" +) +rm -f "$dump_mangled" + +if [[ ! -f "$dump" ]]; then + echo "Error: Failed to create $dump" + exit 1 +fi diff --git a/sdk/bpf/scripts/install.sh b/sdk/bpf/scripts/install.sh index 41a7bb158c6afc..cb345a82450a4f 100755 --- a/sdk/bpf/scripts/install.sh +++ b/sdk/bpf/scripts/install.sh @@ -84,19 +84,24 @@ clone() { } # Install xargo -( - set -ex - # shellcheck disable=SC2154 - if [[ -n $rust_stable ]]; then - cargo +"$rust_stable" install xargo - else - cargo install xargo +version=0.3.22 +if [[ ! -e xargo-$version.md ]] || [[ ! -x bin/xargo ]]; then + ( + args=() + # shellcheck disable=SC2154 + if [[ -n $rust_stable ]]; then + args+=(+"$rust_stable") + fi + args+=(install xargo --version "$version" --root .) + set -ex + cargo "${args[@]}" + ./bin/xargo --version >xargo-$version.md 2>&1 + ) + exitcode=$? + if [[ $exitcode -ne 0 ]]; then + rm -rf xargo-$version.md + exit 1 fi - xargo --version >xargo.md 2>&1 -) -# shellcheck disable=SC2181 -if [[ $? -ne 0 ]]; then - exit 1 fi # Install Criterion diff --git a/sdk/bpf/scripts/objcopy.sh b/sdk/bpf/scripts/objcopy.sh new file mode 100755 index 00000000000000..1e44719e77724f --- /dev/null +++ b/sdk/bpf/scripts/objcopy.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +bpf_sdk=$(cd "$(dirname "$0")/.." && pwd) +# shellcheck source=sdk/bpf/env.sh +source "$bpf_sdk"/env.sh +exec "$bpf_sdk"/dependencies/llvm-native/bin/llvm-objcopy "$@" diff --git a/sdk/bpf/scripts/strip.sh b/sdk/bpf/scripts/strip.sh new file mode 100755 index 00000000000000..6c579ff0801b6b --- /dev/null +++ b/sdk/bpf/scripts/strip.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +so=$1 +if [[ ! -r $so ]]; then + echo "Error: file not found: $so" + exit 1 +fi +so_stripped=$2 +if [[ -z $so_stripped ]]; then + echo "Usage: $0 unstripped.so stripped.so" + exit 1 +fi + +bpf_sdk=$(cd "$(dirname "$0")/.." && pwd) +# shellcheck source=sdk/bpf/env.sh +source "$bpf_sdk"/env.sh +"$bpf_sdk"/dependencies/llvm-native/bin/llvm-objcopy --strip-all "$so" "$so_stripped" diff --git a/sdk/cargo-build-bpf/Cargo.toml b/sdk/cargo-build-bpf/Cargo.toml new file mode 100644 index 00000000000000..62dbea565f77a8 --- /dev/null +++ b/sdk/cargo-build-bpf/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "solana-cargo-build-bpf" +version = "1.5.0" +description = "Compile a local package and all of its dependencies using the Solana BPF SDK" +authors = ["Solana Maintainers "] +repository = "https://github.com/solana-labs/solana" +homepage = "https://solana.com/" +license = "Apache-2.0" +edition = "2018" + +[dependencies] +clap = "2.33.3" +cargo_metadata = "0.12.0" + +[features] +program = [] + +[[bin]] +name = "cargo-build-bpf" +path = "src/main.rs" diff --git a/sdk/cargo-build-bpf/src/main.rs b/sdk/cargo-build-bpf/src/main.rs new file mode 100644 index 00000000000000..b37922d99b2359 --- /dev/null +++ b/sdk/cargo-build-bpf/src/main.rs @@ -0,0 +1,290 @@ +use clap::{ + crate_description, crate_name, crate_version, value_t, value_t_or_exit, values_t, App, Arg, +}; +use std::{ + env, + ffi::OsStr, + fs, + path::{Path, PathBuf}, + process::exit, + process::Command, +}; + +struct Config { + bpf_sdk: PathBuf, + dump: bool, + features: Vec, + manifest_path: Option, + no_default_features: bool, + verbose: bool, +} + +impl Default for Config { + fn default() -> Self { + Self { + bpf_sdk: env::current_exe() + .expect("Unable to get current directory") + .parent() + .expect("Unable to get parent directory") + .to_path_buf() + .join("sdk/bpf"), + features: vec![], + manifest_path: None, + no_default_features: false, + verbose: true, + dump: true, + } + } +} + +fn spawn(program: &Path, args: I) +where + I: IntoIterator, + S: AsRef, +{ + let args = args.into_iter().collect::>(); + print!("Running: {}", program.display()); + for arg in args.iter() { + print!(" {}", arg.as_ref().to_str().unwrap_or("?")); + } + println!(); + + let mut child = Command::new(program) + .args(&args) + .spawn() + .unwrap_or_else(|err| { + eprintln!("Failed to execute {}: {}", program.display(), err); + exit(1); + }); + + let exit_status = child.wait().expect("failed to wait on child"); + if !exit_status.success() { + exit(1); + } +} + +fn build_bpf(config: Config) { + let mut metadata_command = cargo_metadata::MetadataCommand::new(); + if let Some(manifest_path) = config.manifest_path { + metadata_command.manifest_path(manifest_path); + } + + let metadata = metadata_command.exec().unwrap_or_else(|err| { + eprintln!("Failed to obtain package metadata: {}", err); + exit(1); + }); + + let root_package = metadata.root_package().unwrap_or_else(|| { + eprintln!( + "Workspace does not have a root package: {}", + metadata.workspace_root.display() + ); + exit(1); + }); + + let program_name = { + let cdylib_targets = root_package + .targets + .iter() + .filter_map(|target| { + if target.crate_types.contains(&"cdylib".to_string()) { + Some(&target.name) + } else { + None + } + }) + .collect::>(); + + match cdylib_targets.len() { + 0 => { + println!( + "Note: {} crate does not contain a cdylib target", + root_package.name + ); + None + } + 1 => Some(cdylib_targets[0].replace("-", "_")), + _ => { + eprintln!( + "{} crate contains multiple cdylib targets: {:?}", + root_package.name, cdylib_targets + ); + exit(1); + } + } + }; + + let legacy_program_feature_present = root_package.features.contains_key("program"); + let root_package_dir = &root_package.manifest_path.parent().unwrap_or_else(|| { + eprintln!( + "Unable to get directory of {}", + root_package.manifest_path.display() + ); + exit(1); + }); + + let target_build_directory = metadata + .target_directory + .join("bpfel-unknown-unknown/release"); + + let target_deploy_directory = metadata + .target_directory + .join("bpfel-unknown-unknown/deploy"); + + fs::create_dir_all(&target_deploy_directory).unwrap_or_else(|err| { + eprintln!( + "Unable to create deploy directory {}: {}", + target_deploy_directory.display(), + err + ); + exit(1); + }); + + env::set_current_dir(&root_package_dir).unwrap_or_else(|err| { + eprintln!( + "Unable to set current directory to {}: {}", + root_package_dir.display(), + err + ); + exit(1); + }); + + if config.verbose { + println!("BPF SDK: {}", config.bpf_sdk.display()); + if config.no_default_features { + println!("No default features"); + } + if !config.features.is_empty() { + println!("Features: {}", config.features.join(" ")); + } + if legacy_program_feature_present { + println!("Legacy program feature detected"); + } + } + + let xargo_build = config.bpf_sdk.join("rust/xargo-build.sh"); + let mut spawn_args = vec![]; + + if config.no_default_features { + spawn_args.push("--no-default-features"); + } + for feature in &config.features { + spawn_args.push("--features"); + spawn_args.push(feature); + } + if legacy_program_feature_present { + if !config.no_default_features { + spawn_args.push("--no-default-features"); + } + spawn_args.push("--features=program"); + } + spawn(&config.bpf_sdk.join(xargo_build), &spawn_args); + + if let Some(program_name) = program_name { + let program_unstripped_so = target_build_directory.join(&format!("{}.so", program_name)); + let program_dump = target_build_directory.join(&format!("{}-dump.txt", program_name)); + let program_so = target_deploy_directory.join(&format!("{}.so", program_name)); + if config.verbose { + println!("Program name: {}", program_name); + println!("Program shared object: {}", program_so.display()); + } + + spawn( + &config.bpf_sdk.join("scripts/strip.sh"), + &[&program_unstripped_so, &program_so], + ); + + if config.dump { + if config.verbose { + println!("Program dump: {}", program_dump.display()); + } + spawn( + &config.bpf_sdk.join("scripts/dump.sh"), + &[&program_unstripped_so, &program_dump], + ); + } + } else if config.dump { + println!("Note: --dump is only available for crates with a cdylib target"); + } +} + +fn main() { + let default_bpf_sdk = format!("{}", Config::default().bpf_sdk.display()); + + let mut args = env::args().collect::>(); + // When run as a cargo subcommand, the first program argument is the subcommand name. + // Remove it + if let Some(arg1) = args.get(1) { + if arg1 == "build-bpf" { + args.remove(1); + } + } + + let matches = App::new(crate_name!()) + .about(crate_description!()) + .version(crate_version!()) + .arg( + Arg::with_name("bpf_sdk") + .long("bpf-sdk") + .value_name("PATH") + .takes_value(true) + .default_value(&default_bpf_sdk) + .help("Path to the Solana BPF SDK"), + ) + .arg( + Arg::with_name("dump") + .long("dump") + .takes_value(false) + .help("Dump ELF information to a text file on success"), + ) + .arg( + Arg::with_name("features") + .long("features") + .value_name("FEATURES") + .takes_value(true) + .multiple(true) + .help("Space-separated list of features to activate"), + ) + .arg( + Arg::with_name("no_default_features") + .long("no-default-features") + .takes_value(false) + .help("Do not activate the `default` feature"), + ) + .arg( + Arg::with_name("manifest_path") + .long("manifest-path") + .value_name("PATH") + .takes_value(true) + .help("Path to Cargo.toml"), + ) + .arg( + Arg::with_name("verbose") + .long("verbose") + .short("v") + .takes_value(false) + .help("Show additional information"), + ) + .get_matches_from(args); + + let bpf_sdk = value_t_or_exit!(matches, "bpf_sdk", PathBuf); + + let config = Config { + bpf_sdk: fs::canonicalize(&bpf_sdk).unwrap_or_else(|err| { + eprintln!( + "BPF SDK path does not exist: {}: {}", + bpf_sdk.display(), + err + ); + exit(1); + }), + dump: matches.is_present("dump"), + features: values_t!(matches, "features", String) + .ok() + .unwrap_or_else(Vec::new), + manifest_path: value_t!(matches, "manifest_path", PathBuf).ok(), + verbose: matches.is_present("verbose"), + no_default_features: matches.is_present("no_default_features"), + }; + build_bpf(config); +}