tracers-codegen 0.1.0

Contains the compile-time code generation logic which powers the `probe` and `tracers` macros. Do not use this crate directly; see "tracers" for more information.
Documentation
//! This module contains the code that is used within `tracers`s build.rs` file to select
//! the suitable tracing implementation at build time, and within a dependent crate's `build.rs`
//! file to perform the build-time code generation to support the selected tracing implementation

use crate::cargo;
use crate::error::{TracersError, TracersResult};
use crate::gen;
use crate::gen::NativeLib;
use crate::TracingImplementation;
use failure::ResultExt;
use serde::{Deserialize, Serialize};
use std::env;
use std::fs::File;
use std::io::Write;
use std::io::{BufReader, BufWriter};
use std::path::{Path, PathBuf};

/// Captures the features enabled for the build.  There are various combinations of them which
/// influence the logic related to what implementation is preferred
#[derive(Debug, Clone)]
struct FeatureFlags {
    enable_dynamic_tracing: bool,
    enable_static_tracing: bool,
    force_dyn_stap: bool,
    force_dyn_noop: bool,
    force_static_stap: bool,
    force_static_lttng: bool,
    force_static_noop: bool,
}

impl FeatureFlags {
    /// Read the feature flags from the environment variables set by Cargo at build time.
    ///
    /// Fails with an error if the combination of features is not valid
    pub fn from_env() -> TracersResult<FeatureFlags> {
        Self::new(
            Self::is_feature_enabled("dynamic-tracing"),
            Self::is_feature_enabled("static-tracing"),
            Self::is_feature_enabled("force-dyn-stap"),
            Self::is_feature_enabled("force-dyn-noop"),
            Self::is_feature_enabled("force-static-stap"),
            Self::is_feature_enabled("force-static-lttng"),
            Self::is_feature_enabled("force-static-noop"),
        )
    }

    /// Creates a feature flag structure from explicit arguments.  Mostly used for testing
    pub fn new(
        enable_dynamic_tracing: bool,
        enable_static_tracing: bool,
        force_dyn_stap: bool,
        force_dyn_noop: bool,
        force_static_stap: bool,
        force_static_lttng: bool,
        force_static_noop: bool,
    ) -> TracersResult<FeatureFlags> {
        if enable_dynamic_tracing && enable_static_tracing {
            return Err(TracersError::code_generation_error("The features `dynamic-tracing` and `static-tracing` are mutually exclusive; please choose one"));
        }

        if force_dyn_stap && force_dyn_noop {
            return Err(TracersError::code_generation_error("The features `force-dyn-stap` and `force_dyn_noop` are mutually exclusive; please choose one"));
        }

        if force_static_stap && force_static_noop {
            return Err(TracersError::code_generation_error("The features `force-static-stap` and `force_static_noop` are mutually exclusive; please choose one"));
        }

        if force_static_lttng && force_static_noop {
            return Err(TracersError::code_generation_error("The features `force-static-lttng` and `force_static_noop` are mutually exclusive; please choose one"));
        }

        Ok(FeatureFlags {
            enable_dynamic_tracing,
            enable_static_tracing,
            force_dyn_stap,
            force_dyn_noop,
            force_static_stap,
            force_static_lttng,
            force_static_noop,
        })
    }

    pub fn enable_tracing(&self) -> bool {
        self.enable_dynamic() || self.enable_static()
    }

    pub fn enable_dynamic(&self) -> bool {
        self.enable_dynamic_tracing || self.force_dyn_noop || self.force_dyn_stap
    }

    pub fn enable_static(&self) -> bool {
        self.enable_static_tracing
    }

    pub fn force_dyn_stap(&self) -> bool {
        //Should the dynamic stap be required on pain of build failure?
        self.force_dyn_stap
    }

    pub fn force_dyn_noop(&self) -> bool {
        //Should the dynamic stap be required on pain of build failure?
        self.force_dyn_noop
    }

    pub fn force_static_stap(&self) -> bool {
        //Should the staticamic stap be required on pain of build failure?
        self.force_static_stap
    }

    pub fn force_static_lttng(&self) -> bool {
        //Should the static lttng be required on pain of build failure?
        self.force_static_lttng
    }

    fn is_feature_enabled(name: &str) -> bool {
        env::var(&format!(
            "CARGO_FEATURE_{}",
            name.to_uppercase().replace("-", "_")
        ))
        .is_ok()
    }
}

/// Serializable struct which is populated in `build.rs` to indicate to the proc macros which
/// tracing implementation they should use.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub(crate) struct BuildInfo {
    pub package_name: String,
    pub implementation: TracingImplementation,
}

impl BuildInfo {
    pub fn new(package_name: String, implementation: TracingImplementation) -> BuildInfo {
        BuildInfo {
            package_name,
            implementation,
        }
    }

    pub fn load() -> TracersResult<BuildInfo> {
        let path = Self::get_build_path()?;

        let file = File::open(&path)
            .map_err(|e| TracersError::build_info_read_error(path.clone(), e.into()))?;
        let reader = BufReader::new(file);

        serde_json::from_reader(reader)
            .map_err(|e| TracersError::build_info_read_error(path.clone(), e.into()))
    }

    pub fn save(&self) -> TracersResult<PathBuf> {
        let path = Self::get_build_path()?;

        //Make sure the directory exists
        path.parent()
            .map(|p| {
                std::fs::create_dir_all(p)
                    .map_err(|e| TracersError::build_info_write_error(path.clone(), e.into()))
            })
            .unwrap_or(Ok(()))?;

        let file = File::create(&path)
            .map_err(|e| TracersError::build_info_write_error(path.clone(), e.into()))?;
        let writer = BufWriter::new(file);
        serde_json::to_writer(writer, self)
            .map_err(|e| TracersError::build_info_write_error(path.clone(), e.into()))?;

        Ok(path)
    }

    fn get_build_path() -> TracersResult<PathBuf> {
        //HACK: This is...not the most elegant solution.  This code gets used in three contexts:
        //
        //1. When `tracers` itself is being built, its `build.rs` calls into `tracers-build` to
        //   decide which probing implementation to use based on the feature flags specified by the
        //   caller.  In that case, `OUT_DIR` is set by `cargo` to the out directory for the
        //   `tracers` crate.  In that situation the `BuildInfo` file should go somewhere in
        //   `$OUT_DIR`, in a subdirectory named with the `tracers` package name and version
        //
        //2. When some other crate is using `tracers`, its `build.rs` calls into `tracers-build` to
        //   perform the build-time code generation tasks, which first require knowing which
        //   implementation `tracers` is using, hence requires reading the `BuildInfo` file.  In
        //   this case, `$OUT_DIR` is set to that dependent crate's output directory and is not
        //   what we want.  We want to know the path to the `BuildInfo` file produced when
        //   `tracers` was built.  Fortunately this information is passed on to the dependent crate
        //   by `tracers`s `build.rs` in the form of a cargo variable which cargo propagates to
        //   dependent crates as `DEP_TRACERS_BUILD_INFO_PATH`
        //
        //3. When one of the proc macros is invoked during compilation of some crate which is using
        //   `tracers`.  In this case, the other crate's `build.rs` should have already been run,
        //   and it should have called into the `tracers_build::build()` (that call is what creates
        //   context #2 above).  `tracers_build::build()` will instruct Cargo to set an environment
        //   variable `TRACERS_BUILD_INFO_PATH` during compilation, which this method will then
        //   example in order to get the path of the `BuidlInfo` build.
        //
        //I'm not proud of this code.  But `tracers` pushes the Rust build system just about to the
        //breaking point as it is.  We're lucky it's possible at all, hacks notwithstanding
        if "tracers" == env::var("CARGO_PKG_NAME").ok().unwrap_or_default() {
            //This is context #1 in the comment above: we're being called from within the `tracers`
            //build.rs
            let rel_path = PathBuf::from(&format!(
                "{}-{}/buildinfo.json",
                env::var("CARGO_PKG_NAME").context("CARGO_PKG_NAME")?,
                env::var("CARGO_PKG_VERSION").context("CARGO_PKG_VERSION")?
            ));

            Ok(PathBuf::from(env::var("OUT_DIR").context("OUT_DIR")?).join(rel_path))
        } else if let Ok(build_info_path) = env::var("DEP_TRACERS_BUILD_INFO_PATH") {
            //This is context #2 in the comment above
            Ok(PathBuf::from(build_info_path))
        } else if let Ok(build_info_path) = env::var("TRACERS_BUILD_INFO_PATH") {
            //This is context #3 in the comment above
            Ok(PathBuf::from(build_info_path))
        } else {
            //Since the first context happens in the `tracers` code itself and we know it's
            //implemented correctly, it means that this is either context #2 or #3 and the caller
            //did something wrong.  Most likely they forgot to add the call to
            //`tracers_build::build()` to their `build.rs`.  Since this is an easy mistake to make
            //we want an ergonomic error message here
            Err(TracersError::missing_call_in_build_rs())
        }
    }
}

/// Called from the `build.rs` of all crates which have a direct dependency on `tracers` and
/// `tracers_macros`.  This determines the compile-time configuration of the `tracers` crate, and
/// performs any build-time code generation necessary to support the code generated by the
/// `tracers_macros` macros.
///
/// It should be the first line in the `main()` function, etc:
///
/// ```no_execute
/// // build.rs
/// use tracers_build::build;
///
/// fn main() {
///     build();
///
///     //....
/// }
/// ```
pub fn build() {
    let mut stdout = std::io::stdout();
    let mut stderr = std::io::stderr();

    match build_internal(&mut stdout) {
        Ok(_) => writeln!(stdout, "probes build succeeded").unwrap(),
        Err(e) => {
            //An error that propagates all the way up to here is serious enough that it means we
            //cannot proceed.  Fail the build by exiting the process forcefully
            writeln!(stderr, "Error building probes: {}", e).unwrap();

            std::process::exit(-1);
        }
    };
}

fn build_internal<OUT: Write>(out: &mut OUT) -> TracersResult<()> {
    //First things first; get the BuildInfo from the `tracers` build, and tell Cargo to make that
    //available to the proc macros at compile time via an environment variable
    let build_info_path = BuildInfo::get_build_path()?;
    writeln!(
        out,
        "cargo:rustc-env=TRACERS_BUILD_INFO_PATH={}",
        build_info_path.display()
    )
    .unwrap();

    generate_native_code(out)
}

/// This function is the counterpart to `build`, which is intended to be invoked in the `tracers`
/// `build.rs` script.  It reads the feature flags enabled on `tracers`, and from those flags and
/// other information about the target sytem and the local build environment selects an
/// implementation to use, or panics if no suitable implementation is possible
pub fn tracers_build() {
    let mut stdout = std::io::stdout();
    let mut stderr = std::io::stderr();

    let features = FeatureFlags::from_env().expect("Invalid feature flags");

    match tracers_build_internal(&mut stdout, features) {
        Ok(_) => {}
        Err(e) => {
            //failure here doesn't just mean one of the tracing impls failed to compile; when that
            //happens we can always fall back to the no-op impl.  This means something happened
            //which prevents us from proceeding with the build
            writeln!(stderr, "{}", e).unwrap();
            panic!("tracers build failed: {}", e);
        }
    }
}

fn tracers_build_internal<OUT: Write>(out: &mut OUT, features: FeatureFlags) -> TracersResult<()> {
    writeln!(out, "Detected features: \n{:?}", features).unwrap();

    select_implementation(&features).map(|implementation| {
            // Some implementation was selected, but it's possible that the selected
            // "implementation" is to completely disable tracing.  If that's not the case, set the
            // appropriate features for the compiler to use when compiling the `tracers` code.
            if implementation.is_enabled() {
                writeln!(out, "cargo:rustc-cfg=enabled").unwrap();
                writeln!(out,
                    "cargo:rustc-cfg={}_enabled",
                    if implementation.is_static() {
                        "static"
                    } else {
                        "dynamic"
                    }
                ).unwrap(); //this category of tracing is enabled
                writeln!(out, "cargo:rustc-cfg={}_enabled", implementation.as_ref()).unwrap(); //this specific impl is enabled
            }

            //All downstream creates from `tracers` will just call `tracers_build::build`, but this
            //is a special case because we've already decided above which implementation to use.
            //
            //This decision needs to be saved to the OUT_DIR somewhere, so that all of our tests,
            //examples, binaries, and benchmarks which use the proc macros will be able to generate
            //the correct runtime tracing code to match the implementation we've chosen here
            let build_info = BuildInfo::new(env::var("CARGO_PKG_NAME").expect("CARGO_PKG_NAME"), implementation);
            match build_info.save() {
                Ok(build_info_path) => {
                    //The above statements set compile-time features to the compiler knows which modules to
                    //include.  The below will set environment variables DEP_TRACERS_(VARNAME) in dependent
                    //builds
                    //
                    //The codegen stuff in `tracers_build::build` will use this to determine what code
                    //generator to use
                    writeln!(out, "cargo:build-info-path={}", build_info_path.display()).unwrap();
                }
                Err(e) => {
                    writeln!(out, "cargo:warning=Error saving build info file; some targets may fail to build.  Error details: {}", e).unwrap();
                }
            }
    })?;

    //Generate native code for the `tracers` crate.  Nothing in the actual `tracers`
    //library code contains any `#[tracer]` traits, but the tests and examples do, so if we
    //want them to work propertly we need to run codegen for them just like on any other
    //crate
    generate_native_code(out)
}

/// Selects a `tracers` implementation given a set of feature flags specified by the user
fn select_implementation(features: &FeatureFlags) -> TracersResult<TracingImplementation> {
    if !features.enable_tracing() {
        return Ok(TracingImplementation::Disabled);
    }

    //If any implementation is forced, then see if it's available and if so then accept it
    if features.enable_dynamic() {
        // Pick some dynamic tracing impl
        if features.force_dyn_stap() {
            if env::var("DEP_TRACERS_DYN_STAP_SUCCEEDED").is_err() {
                return Err(TracersError::code_generation_error(
                    "force-dyn-stap is enabled but the dyn_stap library is not available",
                ));
            } else {
                return Ok(TracingImplementation::DynamicStap);
            }
        } else if features.force_dyn_noop() {
            //no-op is always available on all platforms
            return Ok(TracingImplementation::DynamicNoOp);
        }

        //Else no tracing impl has been forced so we get to decide
        if env::var("DEP_TRACERS_DYN_STAP_SUCCEEDED").is_ok() {
            //use dyn_stap when it savailable
            Ok(TracingImplementation::DynamicStap)
        } else {
            //else, fall back to noop
            Ok(TracingImplementation::DynamicNoOp)
        }
    } else {
        // Pick some static tracing impl
        assert!(features.enable_static());

        //TODO: Be a bit smarter about this
        if features.force_static_stap() {
            Ok(TracingImplementation::StaticStap)
        } else if features.force_static_lttng() {
            Ok(TracingImplementation::StaticLttng)
        } else {
            Ok(TracingImplementation::StaticNoOp)
        }
    }
}

fn generate_native_code(out: &mut dyn Write) -> TracersResult<()> {
    let manifest_dir = env::var("CARGO_MANIFEST_DIR").context(
        "CARGO_MANIFEST_DIR is not set; are you sure you're calling this from within build.rs?",
    )?;

    let manifest_path = PathBuf::from(manifest_dir).join("Cargo.toml");
    let package_name = env::var("CARGO_PKG_NAME").unwrap();
    let targets = cargo::get_targets(&manifest_path, &package_name).context("get_targets")?;
    let out_path = &PathBuf::from(env::var("OUT_DIR").context("OUT_DIR")?);

    let mut native_libs = gen::code_generator()?.generate_native_code(
        out,
        &Path::new(&manifest_path),
        &out_path,
        &package_name,
        targets,
    );

    //There is usually some repetition when multiple providers are generated.  Filter that out for
    //better build performance
    native_libs.sort_by(|a, b| a.partial_cmp(b).unwrap());
    native_libs.dedup();

    //Scan through all of the native libs output and send the info to cargo as applicable
    for native_lib in native_libs.into_iter() {
        match native_lib {
            NativeLib::StaticWrapperLib(_) => {
                //This is the name of a generated native wrapper.  Ignore it here; the `tracers`
                //attribute macro will use this to generate a `link` attribute at the actual call
                //site
            }
            NativeLib::StaticWrapperLibPath(path) | NativeLib::SupportLibPath(path) => {
                //This is the path to a directory where either the static wrapper or a support lib
                //will be found.  Make sure cargo adds that to the library path
                println!("cargo:rustc-link-search=native={}", path.display());
            }
            NativeLib::DynamicSupportLib(lib) => {
                //This is a dynamically-linked support library which should be linked exactly once
                //for the crate, to support any number of generated native wrappers
                println!("cargo:rustc-link-lib=dylib={}", lib);
            }
            NativeLib::StaticSupportLib(lib) => {
                //This is a statically-linked support library which should be linked exactly once
                //for the crate, to support any number of generated native wrappers
                println!("cargo:rustc-link-lib=static={}", lib);
            }
        };
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::testdata;
    use crate::TracingType;

    #[test]
    #[should_panic]
    fn tracers_build_panics_invalid_features() {
        //These two feature flags are mutually exclusive
        let guard = testdata::with_env_vars(vec![
            ("CARGO_FEATURE_STATIC_TRACING", "1"),
            ("CARGO_FEATURE_DYNAMIC_TRACING", "1"),
        ]);

        tracers_build();

        drop(guard);
    }

    #[test]
    fn build_rs_workflow_tests() {
        // Simulates the entire process, starting with `tracers_build` choosing an implementation
        // based on the selected feature flags, then the dependent crate calling `build` to query
        // the build info generated by `tracers_build` and to perform  perform
        // pre-processing of its code, then the proc macros reading the build info persisted by
        // `build` to generate the right implementation.
        //
        // This doesn't actually integrate all those systems in a test, but it simulates the
        // relevant calls into the `build_rs` code
        let test_cases = vec![
            //features, expected_impl
            (
                // Tracing disabled entirely
                FeatureFlags::new(false, false, false, false, false, false, false).unwrap(),
                TracingImplementation::Disabled,
            ),
            (
                // Tracing enabled, dynamic mode enabled with auto-detect, static disabled
                FeatureFlags::new(true, false, false, false, false, false, false).unwrap(),
                TracingImplementation::DynamicNoOp,
            ),
            (
                // Tracing enabled, dynamic disabled, static enabled with auto-detect
                FeatureFlags::new(false, true, false, false, false, false, false).unwrap(),
                TracingImplementation::StaticNoOp,
            ),
            (
                // Tracing enabled, dynamic disabled, static enabled with force-static-noop
                FeatureFlags::new(false, true, false, false, false, false, true).unwrap(),
                TracingImplementation::StaticNoOp,
            ),
            (
                // Tracing enabled, dynamic disabled, static enabled with force-static-stap
                FeatureFlags::new(false, true, false, false, true, false, false).unwrap(),
                TracingImplementation::StaticStap,
            ),
            (
                // Tracing enabled, dynamic disabled, static enabled with force-static-lttng
                FeatureFlags::new(false, true, false, false, false, true, false).unwrap(),
                TracingImplementation::StaticLttng,
            ),
        ];

        let temp_dir = tempfile::tempdir().unwrap();
        let manifest_dir = env!("CARGO_MANIFEST_DIR");
        let out_dir = temp_dir.path().join("out");

        for (features, expected_impl) in test_cases.into_iter() {
            let context = format!(
                "features: {:?}\nexpected_impl: {}",
                features,
                expected_impl.as_ref()
            );

            //First let's pretend we're in `tracers/build.rs`, and cargo has set the relevant env
            //vars
            let guard = testdata::with_env_vars(vec![
                ("CARGO_PKG_NAME", "tracers"),
                ("CARGO_PKG_VERSION", "1.2.3"),
                ("CARGO_MANIFEST_DIR", manifest_dir),
                ("OUT_DIR", out_dir.to_str().unwrap()),
            ]);

            let mut stdout = Vec::new();

            tracers_build_internal(&mut stdout, features.clone())
                .expect(&format!("Unexpected failure with features: {:?}", features));

            //That worked.  The resulting build info should have been written out
            let build_info_path = BuildInfo::get_build_path().unwrap();

            let step1_build_info = BuildInfo::load().expect(&format!(
                "Failed to load build info for features: {:?}",
                features
            ));

            assert_eq!(
                expected_impl, step1_build_info.implementation,
                "context: {}",
                context
            );

            //And the path to this should have been written to stdout such that cargo will treat it
            //as a variable that is passed to dependent crates' `build.rs`:
            let output = String::from_utf8(stdout).unwrap();
            assert!(
                output.contains(&format!(
                    "cargo:build-info-path={}",
                    build_info_path.display()
                )),
                context
            );

            //and the features used to compile `tracers` should correspond to the implementation
            match expected_impl.tracing_type() {
                TracingType::Disabled => assert!(!output.contains("enabled")),
                TracingType::Dynamic => assert!(output.contains("cargo:rustc-cfg=dynamic_enabled")),
                TracingType::Static => assert!(output.contains("cargo:rustc-cfg=static_enabled")),
            }

            if expected_impl.is_enabled() {
                assert!(
                    output.contains(&format!(
                        "cargo:rustc-cfg={}_enabled",
                        expected_impl.as_ref()
                    )),
                    context
                );
            }

            //Next, the user crate's `build.rs` will want to know what the selected impl was
            drop(guard);
            let guard = testdata::with_env_vars(vec![
                (
                    "DEP_TRACERS_BUILD_INFO_PATH",
                    build_info_path.to_str().unwrap(),
                ),
                ("OUT_DIR", out_dir.to_str().unwrap()),
            ]);

            let step2_build_info = BuildInfo::load().expect(&format!(
                "Failed to load build info for features: {:?}",
                features
            ));

            assert_eq!(step1_build_info, step2_build_info, "context: {}", context);
            drop(guard);

            //At this point in the process if this were a real build, the `build.rs` code would be
            //generating code for a real crate.  We're not going to simulate all of that here,
            //however we can invoke the code gen for all of our test crates at this point, and the
            //code gen should work using the currently selected implementatoin
            for test_case in testdata::TEST_CRATES.iter() {
                let context = format!(
                    "features: {:?} test_case: {}",
                    features,
                    test_case.root_directory.display()
                );

                let mut stdout = Vec::new();

                let guard = testdata::with_env_vars(vec![
                    (
                        "DEP_TRACERS_BUILD_INFO_PATH",
                        build_info_path.to_str().unwrap(),
                    ),
                    ("OUT_DIR", out_dir.to_str().unwrap()),
                    ("CARGO_PKG_NAME", test_case.package_name),
                    (
                        "CARGO_MANIFEST_DIR",
                        test_case.root_directory.to_str().unwrap(),
                    ),
                    ("TARGET", "x86_64-linux-gnu"),
                    ("HOST", "x86_64-linux-gnu"),
                    ("OPT_LEVEL", "1"),
                ]);

                build_internal(&mut stdout).expect(&context);

                //After the build, it should output something on stdout to tell Cargo to set a
                //compiler-visible env var telling the proc macros where the `BuildInfo` file is
                let output = String::from_utf8(stdout).unwrap();
                assert!(output.contains(&format!(
                    "cargo:rustc-env=TRACERS_BUILD_INFO_PATH={}",
                    build_info_path.display()
                )));

                drop(guard);
            }

            //That worked, next the proc macros will be run by `rustc` while it builds the user
            //crate.
            let guard = testdata::with_env_vars(vec![(
                "TRACERS_BUILD_INFO_PATH",
                build_info_path.to_str().unwrap(),
            )]);

            let step3_build_info = BuildInfo::load().expect(&format!(
                "Failed to load build info for features: {:?}",
                features
            ));

            assert_eq!(step1_build_info, step3_build_info, "context: {}", context);

            drop(guard);
        }
    }
}