diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4add5ba..0312aa0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,30 +10,17 @@ env: CARGO_TERM_COLOR: always jobs: - host-test: + all-tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Run disassembler test suite - run: cargo test + run: cargo test --bin cargo-call-stack - firmware-test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - name: Set up nightly toolchain (~1.64) - run: | - rustup default nightly-2022-09-20 - rustup target add thumbv6m-none-eabi thumbv7m-none-eabi - rustup component add rust-src - - - name: Install cargo-call-stack - run: cargo install --path . --debug - - - name: Analyze example firmware - run: cargo test + # one test at a time because rustup does not handle well concurrency + - name: Run firmware tests + run: cargo test --test firmware -- --test-threads 1 # Refs: https://github.com/rust-lang/crater/blob/9ab6f9697c901c4a44025cf0a39b73ad5b37d198/.github/workflows/bors.yml#L125-L149 # bors.tech integration @@ -41,8 +28,7 @@ jobs: name: CI if: ${{ success() }} needs: - - host-test - - firmware-test + - all-tests runs-on: ubuntu-20.04 steps: - name: CI succeeded diff --git a/CHANGELOG.md b/CHANGELOG.md index e320390..aca104e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ This project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] +## [v0.1.15] - 2024-10-28 + +### Added + +- note at invocation time which toolchain is known to work + +### Changed + +- bump known working version to nightly-2023-11-13 + ## [v0.1.14] - 2022-11-24 ### Fixed diff --git a/Cargo.lock b/Cargo.lock index 23b337f..946e67f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -43,7 +43,7 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "cargo-call-stack" -version = "0.1.14" +version = "0.1.15" dependencies = [ "anyhow", "ar", diff --git a/Cargo.toml b/Cargo.toml index e26f1ae..163def4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ license = "MIT OR Apache-2.0" name = "cargo-call-stack" readme = "README.md" repository = "https://github.com/japaric/cargo-call-stack" -version = "0.1.14" +version = "0.1.15" [dependencies] anyhow = "1" diff --git a/README.md b/README.md index 9a759d2..b8d690e 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Other examples: **HEADS UP**: This tool relies on an experimental feature (`-Z stack-sizes`) and implementation details of `rustc` (like symbol mangling) and could stop -working with a nightly toolchain at any time. You have been warned! Last tested nightly: 2022-09-20. +working with a nightly toolchain at any time. You have been warned! Last tested nightly: 2023-11-13. **NOTE**: This tool main use case are embedded (microcontroller) programs that lack, or have very little, indirect function calls and recursion. This tool is of very limited use -- specially its diff --git a/firmware/rust-toolchain.toml b/firmware/rust-toolchain.toml new file mode 100644 index 0000000..a1c0581 --- /dev/null +++ b/firmware/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "nightly-2023-11-13" # ~1.74.0 +components = ["rust-src"] +profile = "minimal" diff --git a/src/ir.rs b/src/ir.rs index 1ac705f..3c0b36d 100644 --- a/src/ir.rs +++ b/src/ir.rs @@ -18,11 +18,7 @@ mod item; mod ty; use crate::ir::ty::type_; -pub use crate::ir::{ - define::Stmt, - item::{Declare, Item}, - ty::Type, -}; +pub use crate::ir::{define::Stmt, item::Item, ty::Type}; #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub struct FnSig<'a> { @@ -156,9 +152,6 @@ fn null(i: &str) -> IResult<&str, Null> { Ok((i, Null)) } -#[derive(Clone, Copy, Debug, PartialEq)] -struct Undef; - fn undef(i: &str) -> IResult<&str, Null> { let i = tag("undef")(i)?.0; Ok((i, Null)) diff --git a/src/ir/item.rs b/src/ir/item.rs index 91b21af..5807ba5 100644 --- a/src/ir/item.rs +++ b/src/ir/item.rs @@ -217,7 +217,7 @@ pub fn item(i: &str) -> IResult<&str, Item> { #[cfg(test)] mod tests { - use crate::ir::{Declare, FnSig, Item, Type}; + use crate::ir::{item::Declare, FnSig, Item, Type}; #[test] fn alias() { diff --git a/src/main.rs b/src/main.rs index 36eb167..36c34a0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,3 @@ -#![deny(warnings)] - use core::{ cmp, fmt::{self, Write as _}, @@ -96,12 +94,37 @@ fn main() -> anyhow::Result<()> { // Font used in the dot graphs const FONT: &str = "monospace"; +const SUPPORTED_NIGHTLY_HASH: &str = "2b603f95a48f10f931a61dd208fe3e5ffd64e491"; +const SUPPORTED_NIGHTLY_NAME: &str = "nightly-2023-11-13"; +const UNSUPPORTED_MODE_KEY: &str = "CARGO_CALL_STACK_UNSPPORTED_NIGHTLY"; +const UNSUPPORTED_MODE_VALUE: &str = "I won't open issues about unsupported toolchains"; + #[allow(deprecated)] fn run() -> anyhow::Result { if env::var_os("CARGO_CALL_STACK_RUSTC_WRAPPER").is_some() { return wrapper::wrapper(); } + let meta = rustc_version::version_meta()?; + + if meta.commit_hash.as_deref() != Some(SUPPORTED_NIGHTLY_HASH) + && env::var(UNSUPPORTED_MODE_KEY).as_deref() != Ok(UNSUPPORTED_MODE_VALUE) + { + eprintln!("Your rust toolchain does not match the last known working version, which is {SUPPORTED_NIGHTLY_NAME}. + +You can override the toolchain that cargo-call-stack uses like this `cargo +{SUPPORTED_NIGHTLY_NAME} call-stack (..)`. +See the rustup documentation for other methods to change / pin the toolchain version. +Note that the `rust-src` component must be available for the specified toolchain; +that is you may want to run `rustup component add --toolchain {SUPPORTED_NIGHTLY_NAME} rust-src` first. + +If you would like to use cargo-call-stack with your current toolchain, which most likely won't work, set the following environment variable as shown below + + export {UNSUPPORTED_MODE_KEY}=\"{UNSUPPORTED_MODE_VALUE}\" +"); + + bail!("unsupported rust toolchain") + } + Builder::from_env(Env::default().default_filter_or("warn")).init(); let args = Args::parse(); @@ -113,7 +136,6 @@ fn run() -> anyhow::Result { _ => bail!("Please specify either --example or --bin ."), }; - let meta = rustc_version::version_meta()?; let host = meta.host; let cwd = env::current_dir()?; let project = Project::query(cwd)?; diff --git a/tests/firmware.rs b/tests/firmware.rs index 73544c5..95823d7 100644 --- a/tests/firmware.rs +++ b/tests/firmware.rs @@ -1,7 +1,5 @@ use std::{env, process::Command}; -use rustc_version::Channel; - const ALL_TARGETS: &[&str] = &[ "thumbv6m-none-eabi", "thumbv7m-none-eabi", @@ -17,280 +15,268 @@ fn for_all_targets(mut f: impl FnMut(&str)) { #[test] fn cycle() { - if channel_is_nightly() { - // function calls on ARMv6-M use the stack - let dot = call_stack("cycle", "thumbv7m-none-eabi"); - - let mut found = false; - for line in dot.lines() { - if line.contains("label=\"_start\\n") { - found = true; - // worst-case stack usage must be exact - assert!(line.contains("max = ")); - } + // function calls on ARMv6-M use the stack + let dot = call_stack("cycle", "thumbv7m-none-eabi"); + + let mut found = false; + for line in dot.lines() { + if line.contains("label=\"_start\\n") { + found = true; + // worst-case stack usage must be exact + assert!(line.contains("max = ")); } - - assert!(found); } + + assert!(found); } #[test] fn fmul() { - if channel_is_nightly() { - for target in FMUL_TARGETS { - let dot = call_stack("fmul", target); - - let mut entry_point = None; - let mut fmul = None; - - for line in dot.lines() { - if line.contains("label=\"_start\\n") { - entry_point = Some( - line.split_whitespace() - .next() - .unwrap() - .parse::() - .unwrap(), - ); - } else if line.contains("label=\"__aeabi_fmul\\n") { - fmul = Some( - line.split_whitespace() - .next() - .unwrap() - .parse::() - .unwrap(), - ); - } - } + for target in FMUL_TARGETS { + let dot = call_stack("fmul", target); - let main = entry_point.unwrap(); - let fmul = fmul.unwrap(); + let mut entry_point = None; + let mut fmul = None; - // there must be an edge between the entry point and `__aeabi_fmul` - assert!(dot.contains(&format!("{} -> {}", main, fmul))); + for line in dot.lines() { + if line.contains("label=\"_start\\n") { + entry_point = Some( + line.split_whitespace() + .next() + .unwrap() + .parse::() + .unwrap(), + ); + } else if line.contains("label=\"__aeabi_fmul\\n") { + fmul = Some( + line.split_whitespace() + .next() + .unwrap() + .parse::() + .unwrap(), + ); + } } + + let main = entry_point.unwrap(); + let fmul = fmul.unwrap(); + + // there must be an edge between the entry point and `__aeabi_fmul` + assert!(dot.contains(&format!("{} -> {}", main, fmul))); } } #[test] fn function_pointer() { - if channel_is_nightly() { - for_all_targets(|target| { - let dot = call_stack("function-pointer", target); - - let mut foo = None; - let mut bar = None; - let mut fn_call = None; - - for line in dot.lines() { - if line.contains("label=\"function_pointer::foo\\n") { - foo = Some( - line.split_whitespace() - .next() - .unwrap() - .parse::() - .unwrap(), - ); - } else if line.contains("label=\"function_pointer::bar\\n") { - bar = Some( - line.split_whitespace() - .next() - .unwrap() - .parse::() - .unwrap(), - ); - } else if line.contains("label=\"i1 ()*\\n") { - fn_call = Some( - line.split_whitespace() - .next() - .unwrap() - .parse::() - .unwrap(), - ); - } + for_all_targets(|target| { + let dot = call_stack("function-pointer", target); + + let mut foo = None; + let mut bar = None; + let mut fn_call = None; + + for line in dot.lines() { + if line.contains("label=\"function_pointer::foo\\n") { + foo = Some( + line.split_whitespace() + .next() + .unwrap() + .parse::() + .unwrap(), + ); + } else if line.contains("label=\"function_pointer::bar\\n") { + bar = Some( + line.split_whitespace() + .next() + .unwrap() + .parse::() + .unwrap(), + ); + } else if line.contains("label=\"i1 ()*\\n") { + fn_call = Some( + line.split_whitespace() + .next() + .unwrap() + .parse::() + .unwrap(), + ); } + } - let fn_call = fn_call.unwrap(); - let foo = foo.unwrap(); - let bar = bar.unwrap(); + let fn_call = fn_call.unwrap(); + let foo = foo.unwrap(); + let bar = bar.unwrap(); - // there must be an edge from the fictitious node to both `foo` and `bar` - assert!(dot.contains(&format!("{} -> {}", fn_call, foo))); - assert!(dot.contains(&format!("{} -> {}", fn_call, bar))); - }) - } + // there must be an edge from the fictitious node to both `foo` and `bar` + assert!(dot.contains(&format!("{} -> {}", fn_call, foo))); + assert!(dot.contains(&format!("{} -> {}", fn_call, bar))); + }) } #[test] fn function_pointer_ptr() { - if channel_is_nightly() { - for_all_targets(|target| { - let dot = call_stack("function-pointer-ptr", target); - - let mut foo = None; - let mut bar = None; - let mut fn_call = None; - - for line in dot.lines() { - if line.contains("label=\"function_pointer_ptr::foo\\n") { - foo = Some( - line.split_whitespace() - .next() - .unwrap() - .parse::() - .unwrap(), - ); - } else if line.contains("label=\"function_pointer_ptr::bar\\n") { - bar = Some( - line.split_whitespace() - .next() - .unwrap() - .parse::() - .unwrap(), - ); - } else if line.contains("label=\"i1 (ptr)*\\n") { - fn_call = Some( - line.split_whitespace() - .next() - .unwrap() - .parse::() - .unwrap(), - ); - } + for_all_targets(|target| { + let dot = call_stack("function-pointer-ptr", target); + + let mut foo = None; + let mut bar = None; + let mut fn_call = None; + + for line in dot.lines() { + if line.contains("label=\"function_pointer_ptr::foo\\n") { + foo = Some( + line.split_whitespace() + .next() + .unwrap() + .parse::() + .unwrap(), + ); + } else if line.contains("label=\"function_pointer_ptr::bar\\n") { + bar = Some( + line.split_whitespace() + .next() + .unwrap() + .parse::() + .unwrap(), + ); + } else if line.contains("label=\"i1 (ptr)*\\n") { + fn_call = Some( + line.split_whitespace() + .next() + .unwrap() + .parse::() + .unwrap(), + ); } + } - let fn_call = fn_call.unwrap(); - let foo = foo.unwrap(); - let bar = bar.unwrap(); + let fn_call = fn_call.unwrap(); + let foo = foo.unwrap(); + let bar = bar.unwrap(); - // there must be an edge from the fictitious node to both `foo` and `bar` - assert!(dot.contains(&format!("{} -> {}", fn_call, foo))); - assert!(dot.contains(&format!("{} -> {}", fn_call, bar))); - }) - } + // there must be an edge from the fictitious node to both `foo` and `bar` + assert!(dot.contains(&format!("{} -> {}", fn_call, foo))); + assert!(dot.contains(&format!("{} -> {}", fn_call, bar))); + }) } #[test] fn dynamic_dispatch() { - if channel_is_nightly() { - for_all_targets(|target| { - let dot = call_stack("dynamic-dispatch", target); - - let mut bar = None; - let mut baz = None; - let mut quux = None; - let mut dyn_call = None; - - for line in dot.lines() { - if line.contains("label=\"dynamic_dispatch::Foo::foo\\n") { - bar = Some( - line.split_whitespace() - .next() - .unwrap() - .parse::() - .unwrap(), - ); - } else if line - .contains("label=\"::foo\\n") - { - baz = Some( - line.split_whitespace() - .next() - .unwrap() - .parse::() - .unwrap(), - ); - } else if line.contains("label=\"dynamic_dispatch::Quux::foo\\n") { - quux = Some( - line.split_whitespace() - .next() - .unwrap() - .parse::() - .unwrap(), - ); - } else if line.contains("label=\"i1 (ptr)*\\n") { - dyn_call = Some( - line.split_whitespace() - .next() - .unwrap() - .parse::() - .unwrap(), - ); - } + for_all_targets(|target| { + let dot = call_stack("dynamic-dispatch", target); + + let mut bar = None; + let mut baz = None; + let mut quux = None; + let mut dyn_call = None; + + for line in dot.lines() { + if line.contains("label=\"dynamic_dispatch::Foo::foo\\n") { + bar = Some( + line.split_whitespace() + .next() + .unwrap() + .parse::() + .unwrap(), + ); + } else if line + .contains("label=\"::foo\\n") + { + baz = Some( + line.split_whitespace() + .next() + .unwrap() + .parse::() + .unwrap(), + ); + } else if line.contains("label=\"dynamic_dispatch::Quux::foo\\n") { + quux = Some( + line.split_whitespace() + .next() + .unwrap() + .parse::() + .unwrap(), + ); + } else if line.contains("label=\"i1 (ptr)*\\n") { + dyn_call = Some( + line.split_whitespace() + .next() + .unwrap() + .parse::() + .unwrap(), + ); } + } - let bar = bar.unwrap(); - let baz = baz.unwrap(); - let quux = quux.unwrap(); - let dyn_call = dyn_call.unwrap(); + let bar = bar.unwrap(); + let baz = baz.unwrap(); + let quux = quux.unwrap(); + let dyn_call = dyn_call.unwrap(); - // there must be an edge from the fictitious node to both `Bar` and `Baz` - assert!(dot.contains(&format!("{} -> {}", dyn_call, bar))); - assert!(dot.contains(&format!("{} -> {}", dyn_call, baz))); + // there must be an edge from the fictitious node to both `Bar` and `Baz` + assert!(dot.contains(&format!("{} -> {}", dyn_call, bar))); + assert!(dot.contains(&format!("{} -> {}", dyn_call, baz))); - // but there must not be an edge from the fictitious node and `Quux` - assert!(!dot.contains(&format!("{} -> {}", dyn_call, quux))); - }) - } + // but there must not be an edge from the fictitious node and `Quux` + assert!(!dot.contains(&format!("{} -> {}", dyn_call, quux))); + }) } #[test] fn core_fmt() { - if channel_is_nightly() { - for_all_targets(|target| { - let _should_not_error = call_stack("core-fmt", target); - }) - } + for_all_targets(|target| { + let _should_not_error = call_stack("core-fmt", target); + }) } #[test] fn panic_fmt() { - if channel_is_nightly() { - for_all_targets(|target| { - let _should_not_error = call_stack("panic-fmt", target); - }) - } + for_all_targets(|target| { + let _should_not_error = call_stack("panic-fmt", target); + }) } #[test] fn div64() { - if channel_is_nightly() { - for_all_targets(|target| { - let _should_not_error = call_stack("div64", target); - }) - } + for_all_targets(|target| { + let _should_not_error = call_stack("div64", target); + }) } #[test] fn gh63() { - if channel_is_nightly() { - for_all_targets(|target| { - let _should_not_error = call_stack("memcmp-ir-no-call", target); - }) - } + for_all_targets(|target| { + let _should_not_error = call_stack("memcmp-ir-no-call", target); + }) } #[test] fn gh74() { - if channel_is_nightly() { - for_all_targets(|target| { - let _should_not_error = call_stack("abs-i32", target); - }) - } -} - -fn channel_is_nightly() -> bool { - rustc_version::version_meta().map(|m| m.channel).ok() == Some(Channel::Nightly) + for_all_targets(|target| { + let _should_not_error = call_stack("abs-i32", target); + }) } fn call_stack(ex: &str, target: &str) -> String { - let output = Command::new("cargo") - .args(&["call-stack", "--example", ex, "--target", target]) + // target/debug/deps/firmware-$HASH + let mut current_exe = env::current_exe().unwrap(); + current_exe.pop(); + current_exe.pop(); + let output = Command::new(current_exe.join("cargo-call-stack")) + .args(&["--example", ex, "--target", target]) .current_dir(env::current_dir().unwrap().join("firmware")) + // (env_remove) do not inherit the parent toolchain + // without this `firmware/rust-toolchain.toml` is ignored + .env_remove("RUSTUP_TOOLCHAIN") + .env_remove("CARGO") .output() .unwrap(); if !output.status.success() { - panic!("{}", String::from_utf8(output.stderr).unwrap()); + panic!( + "stdout:\n{}\n\nstderr:\n{}", + String::from_utf8(output.stdout).unwrap(), + String::from_utf8(output.stderr).unwrap() + ); } String::from_utf8(output.stdout).unwrap() }