diff --git a/.gitignore b/.gitignore index 9368553ffd..c870ff8a2c 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,5 @@ log-*/ /spl_*.so .DS_Store +# scripts that may be generated by cargo *-bpf commands +**/cargo-*-bpf-child-script-*.sh diff --git a/sdk/cargo-build-bpf/src/main.rs b/sdk/cargo-build-bpf/src/main.rs index c831159a8a..a99078aa19 100644 --- a/sdk/cargo-build-bpf/src/main.rs +++ b/sdk/cargo-build-bpf/src/main.rs @@ -26,6 +26,7 @@ struct Config<'a> { bpf_sdk: PathBuf, dump: bool, features: Vec, + generate_child_script_on_failure: bool, no_default_features: bool, offline: bool, verbose: bool, @@ -46,6 +47,7 @@ impl Default for Config<'_> { bpf_out_dir: None, dump: false, features: vec![], + generate_child_script_on_failure: false, no_default_features: false, offline: false, verbose: false, @@ -54,13 +56,13 @@ impl Default for Config<'_> { } } -fn spawn(program: &Path, args: I) -> String +fn spawn(program: &Path, args: I, generate_child_script_on_failure: bool) -> String where I: IntoIterator, S: AsRef, { let args = args.into_iter().collect::>(); - print!("Running: {}", program.display()); + print!("cargo-build-bpf child: {}", program.display()); for arg in args.iter() { print!(" {}", arg.as_ref().to_str().unwrap_or("?")); } @@ -77,6 +79,29 @@ where let output = child.wait_with_output().expect("failed to wait on child"); if !output.status.success() { + if !generate_child_script_on_failure { + exit(1); + } + eprintln!("cargo-build-bpf exited on command execution failure"); + let script_name = format!( + "cargo-build-bpf-child-script-{}.sh", + program.file_name().unwrap().to_str().unwrap(), + ); + let file = File::create(&script_name).unwrap(); + let mut out = BufWriter::new(file); + for (key, value) in env::vars() { + writeln!(out, "{}=\"{}\" \\", key, value).unwrap(); + } + write!(out, "{}", program.display()).unwrap(); + for arg in args.iter() { + write!(out, " {}", arg.as_ref().to_str().unwrap_or("?")).unwrap(); + } + writeln!(out).unwrap(); + out.flush().unwrap(); + eprintln!( + "To rerun the failed command for debugging use {}", + script_name, + ); exit(1); } output @@ -276,7 +301,11 @@ fn check_undefined_symbols(config: &Config, program: &Path) { .join("llvm-readelf"); let mut readelf_args = vec!["--dyn-symbols"]; readelf_args.push(program.to_str().unwrap()); - let output = spawn(&readelf, &readelf_args); + let output = spawn( + &readelf, + &readelf_args, + config.generate_child_script_on_failure, + ); if config.verbose { println!("{}", output); } @@ -309,7 +338,11 @@ fn link_bpf_toolchain(config: &Config) { .join("rust"); let rustup = PathBuf::from("rustup"); let rustup_args = vec!["toolchain", "list", "-v"]; - let rustup_output = spawn(&rustup, &rustup_args); + let rustup_output = spawn( + &rustup, + &rustup_args, + config.generate_child_script_on_failure, + ); if config.verbose { println!("{}", rustup_output); } @@ -321,7 +354,11 @@ fn link_bpf_toolchain(config: &Config) { let path = it.next(); if path.unwrap() != toolchain_path.to_str().unwrap() { let rustup_args = vec!["toolchain", "uninstall", "bpf"]; - let output = spawn(&rustup, &rustup_args); + let output = spawn( + &rustup, + &rustup_args, + config.generate_child_script_on_failure, + ); if config.verbose { println!("{}", output); } @@ -333,7 +370,11 @@ fn link_bpf_toolchain(config: &Config) { } if do_link { let rustup_args = vec!["toolchain", "link", "bpf", toolchain_path.to_str().unwrap()]; - let output = spawn(&rustup, &rustup_args); + let output = spawn( + &rustup, + &rustup_args, + config.generate_child_script_on_failure, + ); if config.verbose { println!("{}", output); } @@ -475,7 +516,11 @@ fn build_bpf_package(config: &Config, target_directory: &Path, package: &cargo_m cargo_build_args.push(arg); } } - let output = spawn(&cargo_build, &cargo_build_args); + let output = spawn( + &cargo_build, + &cargo_build_args, + config.generate_child_script_on_failure, + ); if config.verbose { println!("{}", output); } @@ -520,6 +565,7 @@ fn build_bpf_package(config: &Config, target_directory: &Path, package: &cargo_m let output = spawn( &config.bpf_sdk.join("scripts").join("strip.sh"), &[&program_unstripped_so, &program_so], + config.generate_child_script_on_failure, ); if config.verbose { println!("{}", output); @@ -530,6 +576,7 @@ fn build_bpf_package(config: &Config, target_directory: &Path, package: &cargo_m let output = spawn( &config.bpf_sdk.join("scripts").join("dump.sh"), &[&program_unstripped_so, &program_dump], + config.generate_child_script_on_failure, ); if config.verbose { println!("{}", output); @@ -643,6 +690,12 @@ fn main() { .multiple(true) .help("Space-separated list of features to activate"), ) + .arg( + Arg::with_name("generate_child_script_on_failure") + .long("generate-child-script-on-failure") + .takes_value(false) + .help("Generate rerun-script.sh to rerun test command on failure"), + ) .arg( Arg::with_name("manifest_path") .long("manifest-path") @@ -706,6 +759,7 @@ fn main() { features: values_t!(matches, "features", String) .ok() .unwrap_or_else(Vec::new), + generate_child_script_on_failure: matches.is_present("generate_child_script_on_failure"), no_default_features: matches.is_present("no_default_features"), offline: matches.is_present("offline"), verbose: matches.is_present("verbose"), diff --git a/sdk/cargo-build-bpf/tests/crates.rs b/sdk/cargo-build-bpf/tests/crates.rs index 56c1272094..0cf6cb1fba 100644 --- a/sdk/cargo-build-bpf/tests/crates.rs +++ b/sdk/cargo-build-bpf/tests/crates.rs @@ -7,7 +7,7 @@ use std::{ #[macro_use] extern crate serial_test; -fn run_cargo_build(extra_args: &[&str]) -> Output { +fn run_cargo_build(crate_name: &str, extra_args: &[&str]) -> Output { let cwd = env::current_dir().expect("Unable to get current working directory"); let root = cwd .parent() @@ -17,7 +17,7 @@ fn run_cargo_build(extra_args: &[&str]) -> Output { let toml = cwd .join("tests") .join("crates") - .join("noop") + .join(crate_name) .join("Cargo.toml"); let toml = format!("{}", toml.display()); let mut args = vec!["--bpf-sdk", "../bpf", "--manifest-path", &toml]; @@ -42,7 +42,7 @@ fn run_cargo_build(extra_args: &[&str]) -> Output { #[test] #[serial] fn test_build() { - let output = run_cargo_build(&[]); + let output = run_cargo_build("noop", &[]); assert!(output.status.success()); } @@ -55,7 +55,7 @@ fn test_dump() { .status() .expect("Unable to install rustfilt required for --dump option") .success()); - let output = run_cargo_build(&["--dump"]); + let output = run_cargo_build("noop", &["--dump"]); assert!(output.status.success()); let cwd = env::current_dir().expect("Unable to get current working directory"); let dump = cwd @@ -71,10 +71,25 @@ fn test_dump() { #[test] #[serial] fn test_out_dir() { - let output = run_cargo_build(&["--bpf-out-dir", "tmp_out"]); + let output = run_cargo_build("noop", &["--bpf-out-dir", "tmp_out"]); assert!(output.status.success()); let cwd = env::current_dir().expect("Unable to get current working directory"); let dir = cwd.join("tmp_out"); assert!(dir.exists()); fs::remove_dir_all("tmp_out").expect("Failed to remove tmp_out dir"); } + +#[test] +#[serial] +fn test_generate_child_script_on_failre() { + let output = run_cargo_build("fail", &["--generate-child-script-on-failure"]); + assert!(!output.status.success()); + let cwd = env::current_dir().expect("Unable to get current working directory"); + let scr = cwd + .join("tests") + .join("crates") + .join("fail") + .join("cargo-build-bpf-child-script-cargo.sh"); + assert!(scr.exists()); + fs::remove_file(scr).expect("Failed to remove script"); +} diff --git a/sdk/cargo-build-bpf/tests/crates/fail/Cargo.toml b/sdk/cargo-build-bpf/tests/crates/fail/Cargo.toml new file mode 100644 index 0000000000..2ec35bc4f0 --- /dev/null +++ b/sdk/cargo-build-bpf/tests/crates/fail/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "fail" +version = "1.8.0" +description = "Solana BPF test program written in Rust" +authors = ["Solana Maintainers "] +repository = "https://github.com/solana-labs/solana" +license = "Apache-2.0" +homepage = "https://solana.com/" +edition = "2018" +publish = false + +[dependencies] +solana-program = { path = "../../../../program", version = "=1.8.0" } + +[lib] +crate-type = ["cdylib"] + +[workspace] diff --git a/sdk/cargo-build-bpf/tests/crates/fail/src/lib.rs b/sdk/cargo-build-bpf/tests/crates/fail/src/lib.rs new file mode 100644 index 0000000000..da12227da5 --- /dev/null +++ b/sdk/cargo-build-bpf/tests/crates/fail/src/lib.rs @@ -0,0 +1,15 @@ +//! @brief Example Rust-based BPF noop program + +extern crate solana_program; +use solana_program::{ + account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, pubkey::Pubkey, +}; + +entrypoint!(process_instruction); +fn process_instruction( + _program_id: &Pubkey, + _accounts: &[AccountInfo], + _instruction_data: &[u8], +) -> ProgramResult { + // error to make build fail: no return value +} diff --git a/sdk/cargo-test-bpf/src/main.rs b/sdk/cargo-test-bpf/src/main.rs index 49b39f5487..8599f60cab 100644 --- a/sdk/cargo-test-bpf/src/main.rs +++ b/sdk/cargo-test-bpf/src/main.rs @@ -4,6 +4,8 @@ use clap::{ use std::{ env, ffi::OsStr, + fs::File, + io::{prelude::*, BufWriter}, path::{Path, PathBuf}, process::exit, process::Command, @@ -16,6 +18,7 @@ struct Config { cargo_build_bpf: PathBuf, extra_cargo_test_args: Vec, features: Vec, + generate_child_script_on_failure: bool, test_name: Option, no_default_features: bool, no_run: bool, @@ -33,6 +36,7 @@ impl Default for Config { cargo_build_bpf: PathBuf::from("cargo-build-bpf"), extra_cargo_test_args: vec![], features: vec![], + generate_child_script_on_failure: false, test_name: None, no_default_features: false, no_run: false, @@ -43,13 +47,13 @@ impl Default for Config { } } -fn spawn(program: &Path, args: I) +fn spawn(program: &Path, args: I, generate_child_script_on_failure: bool) where I: IntoIterator, S: AsRef, { let args = args.into_iter().collect::>(); - print!("Running: {}", program.display()); + print!("cargo-test-bpf child: {}", program.display()); for arg in args.iter() { print!(" {}", arg.as_ref().to_str().unwrap_or("?")); } @@ -65,6 +69,29 @@ where let exit_status = child.wait().expect("failed to wait on child"); if !exit_status.success() { + if !generate_child_script_on_failure { + exit(1); + } + eprintln!("cargo-test-bpf exited on command execution failure"); + let script_name = format!( + "cargo-test-bpf-child-script-{}.sh", + program.file_name().unwrap().to_str().unwrap(), + ); + let file = File::create(&script_name).unwrap(); + let mut out = BufWriter::new(file); + for (key, value) in env::vars() { + writeln!(out, "{}=\"{}\" \\", key, value).unwrap(); + } + write!(out, "{}", program.display()).unwrap(); + for arg in args.iter() { + write!(out, " {}", arg.as_ref().to_str().unwrap_or("?")).unwrap(); + } + writeln!(out).unwrap(); + out.flush().unwrap(); + eprintln!( + "To rerun the failed command for debugging use {}", + script_name, + ); exit(1); } } @@ -99,7 +126,11 @@ fn test_bpf_package(config: &Config, target_directory: &Path, package: &cargo_me build_bpf_args.push("--bpf-out-dir"); build_bpf_args.push(&bpf_out_dir); - spawn(&config.cargo_build_bpf, &build_bpf_args); + spawn( + &config.cargo_build_bpf, + &build_bpf_args, + config.generate_child_script_on_failure, + ); // Pass --bpf-out-dir along to the solana-program-test crate env::set_var("BPF_OUT_DIR", bpf_out_dir); @@ -124,7 +155,11 @@ fn test_bpf_package(config: &Config, target_directory: &Path, package: &cargo_me for extra_cargo_test_arg in &config.extra_cargo_test_args { cargo_args.push(extra_cargo_test_arg); } - spawn(&config.cargo, &cargo_args); + spawn( + &config.cargo, + &cargo_args, + config.generate_child_script_on_failure, + ); } fn test_bpf(config: Config, manifest_path: Option) { @@ -239,6 +274,12 @@ fn main() { .takes_value(false) .help("Run without accessing the network"), ) + .arg( + Arg::with_name("generate_child_script_on_failure") + .long("generate-child-script-on-failure") + .takes_value(false) + .help("Generate rerun-script.sh to rerun test command on failure"), + ) .arg( Arg::with_name("verbose") .short("v") @@ -271,6 +312,7 @@ fn main() { features: values_t!(matches, "features", String) .ok() .unwrap_or_else(Vec::new), + generate_child_script_on_failure: matches.is_present("generate_child_script_on_failure"), test_name: value_t!(matches, "test", String).ok(), no_default_features: matches.is_present("no_default_features"), no_run: matches.is_present("no_run"),