diff --git a/Cargo.toml b/Cargo.toml index a7361df..16d0159 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "shadow-rs" -version = "0.36.1" +version = "0.37.0" authors = ["baoyachi "] edition = "2021" description = "A build-time information stored in your rust project" diff --git a/README.md b/README.md index 7427626..4bb0379 100644 --- a/README.md +++ b/README.md @@ -37,12 +37,16 @@ link [example_wasm](https://github.com/baoyachi/shadow-rs/tree/master/example_wa ![build_module](./build_module.png) -# Note on Caching +# BuildPattern +The BuildPattern enum defines strategies for triggering package rebuilding. **Default mode is `Lazy`**. -`shadow-rs` build information **is not always rebuilt** when you build a project. `shadow-rs` outputs several hints to -Cargo in order to force rebuilds when required, but this does not always work. You can enforce up-to-date build -information by running `cargo clean` before the build, or use a CI/CD pipeline tool. For more details, -see . +* `Lazy`: The lazy mode. In this mode, if the current Rust environment is set to `debug`, + the rebuild package will not run every time the build script is triggered. + If the environment is set to `release`, it behaves the same as the `RealTime` mode. +* `RealTime`: The real-time mode. It will always trigger rebuilding a package upon any change, + regardless of whether the Rust environment is set to `debug` or `release`. +* `Custom`: The custom build mode, an enhanced version of `RealTime` mode, allowing for user-defined conditions + to trigger rebuilding a package. # Examples @@ -80,8 +84,8 @@ shadow-rs = "{latest version}" Now in the root of your project (same directory as `Cargo.toml`) add a file `build.rs`: ```rust -fn main() -> shadow_rs::SdResult<()> { - shadow_rs::new() +fn main() { + ShadowBuilder::builder().build().unwrap(); } ``` diff --git a/example_shadow/build.rs b/example_shadow/build.rs index 51e5960..18039b8 100644 --- a/example_shadow/build.rs +++ b/example_shadow/build.rs @@ -1,5 +1,8 @@ -fn main() -> shadow_rs::SdResult<()> { - // shadow_rs::new() +use shadow_rs::ShadowBuilder; - shadow_rs::new_deny(Default::default()) +fn main() { + ShadowBuilder::builder() + .deny_const(Default::default()) + .build() + .unwrap(); } diff --git a/example_shadow_hook/build.rs b/example_shadow_hook/build.rs index e559049..d17a8c1 100644 --- a/example_shadow_hook/build.rs +++ b/example_shadow_hook/build.rs @@ -1,9 +1,9 @@ -use shadow_rs::SdResult; +use shadow_rs::{SdResult, ShadowBuilder}; use std::fs::File; use std::io::Write; -fn main() -> SdResult<()> { - shadow_rs::new_hook(hook) +fn main() { + ShadowBuilder::builder().hook(hook).build().unwrap(); } fn hook(file: &File) -> SdResult<()> { diff --git a/src/build.rs b/src/build.rs index c943a36..62778b1 100644 --- a/src/build.rs +++ b/src/build.rs @@ -1,3 +1,7 @@ +use crate::hook::HookExt; +use crate::{default_deny, SdResult, Shadow}; +use is_debug::is_debug; +use std::collections::BTreeSet; use std::fmt::{Display, Formatter}; /// `shadow-rs` build constant identifiers. @@ -16,6 +20,7 @@ pub struct ConstVal { impl ConstVal { pub fn new>(desc: S) -> ConstVal { + // Creates a new `ConstVal` with an empty string as its value and `Str` as its type. ConstVal { desc: desc.into(), v: "".to_string(), @@ -24,6 +29,7 @@ impl ConstVal { } pub fn new_bool>(desc: S) -> ConstVal { + // Creates a new `ConstVal` with "true" as its value and `Bool` as its type. ConstVal { desc: desc.into(), v: "true".to_string(), @@ -32,6 +38,7 @@ impl ConstVal { } pub fn new_slice>(desc: S) -> ConstVal { + // Creates a new `ConstVal` with an empty string as its value and `Slice` as its type. ConstVal { desc: desc.into(), v: "".to_string(), @@ -60,3 +67,241 @@ impl Display for ConstType { } } } + +/// The BuildPattern enum defines strategies for triggering package rebuilding. +/// +/// Default mode is `Lazy`. +/// +/// * `Lazy`: The lazy mode. In this mode, if the current Rust environment is set to `debug`, +/// the rebuild package will not run every time the build script is triggered. +/// If the environment is set to `release`, it behaves the same as the `RealTime` mode. +/// * `RealTime`: The real-time mode. It will always trigger rebuilding a package upon any change, +/// regardless of whether the Rust environment is set to `debug` or `release`. +/// * `Custom`: The custom build mode, an enhanced version of `RealTime` mode, allowing for user-defined conditions +/// to trigger rebuilding a package. +/// +#[derive(Debug, Default, Clone)] +pub enum BuildPattern { + #[default] + Lazy, + RealTime, + Custom { + /// A list of paths that, if changed, will trigger a rebuild. + /// See + if_path_changed: Vec, + /// A list of environment variables that, if changed, will trigger a rebuild. + /// See + if_env_changed: Vec, + }, +} + +impl BuildPattern { + /// Determines when Cargo should rerun the build script based on the configured pattern. + /// + /// # Arguments + /// + /// * `other_keys` - An iterator over additional keys that should trigger a rebuild if they change. + /// * `out_dir` - The output directory where generated files are placed. + pub(crate) fn rerun_if<'a>( + &self, + other_keys: impl Iterator, + out_dir: &str, + ) { + match self { + BuildPattern::Lazy => { + if is_debug() { + return; + } + } + BuildPattern::RealTime => {} + BuildPattern::Custom { + if_path_changed, + if_env_changed, + } => { + if_env_changed + .iter() + .for_each(|key| println!("cargo:rerun-if-env-changed={key}")); + if_path_changed + .iter() + .for_each(|p| println!("cargo:rerun-if-changed={p}")); + } + } + + other_keys.for_each(|key| println!("cargo:rerun-if-env-changed={key}")); + println!("cargo:rerun-if-env-changed=SOURCE_DATE_EPOCH"); + println!("cargo:rerun-if-changed={}/shadow.rs", out_dir); + } +} + +/// A builder pattern structure to construct a `Shadow` instance. +/// +/// This struct allows for configuring various aspects of how shadow-rs will be built into your Rust project. +/// It provides methods to set up hooks, specify build patterns, define paths, and deny certain build constants. +/// +/// # Fields +/// +/// * `hook`: An optional hook that can be used during the build process. Hooks implement the `HookExt` trait. +/// * `build_pattern`: Determines the strategy for triggering package rebuilds (`Lazy`, `RealTime`, or `Custom`). +/// * `deny_const`: A set of build constant identifiers that should not be included in the build. +/// * `src_path`: The source path from which files are read for building. +/// * `out_path`: The output path where generated files will be placed. +/// +pub struct ShadowBuilder<'a> { + hook: Option>, + build_pattern: BuildPattern, + deny_const: BTreeSet, + src_path: Option, + out_path: Option, +} + +impl<'a> ShadowBuilder<'a> { + /// Creates a new `ShadowBuilder` with default settings. + /// + /// Initializes the builder with the following defaults: + /// - `hook`: None + /// - `build_pattern`: `BuildPattern::Lazy` + /// - `deny_const`: Uses the result from `default_deny()` + /// - `src_path`: Attempts to get the manifest directory using `CARGO_MANIFEST_DIR` environment variable. + /// - `out_path`: Attempts to get the output directory using `OUT_DIR` environment variable. + /// + /// # Returns + /// + /// A new instance of `ShadowBuilder`. + pub fn builder() -> Self { + let default_src_path = std::env::var("CARGO_MANIFEST_DIR").ok(); + let default_out_path = std::env::var("OUT_DIR").ok(); + Self { + hook: None, + build_pattern: BuildPattern::default(), + deny_const: default_deny(), + src_path: default_src_path, + out_path: default_out_path, + } + } + + /// Sets the build hook for this builder. + /// + /// # Arguments + /// + /// * `hook` - An object implementing the `HookExt` trait that defines custom behavior for the build process. + /// + /// # Returns + /// + /// A new `ShadowBuilder` instance with the specified hook applied. + pub fn hook(mut self, hook: impl HookExt + 'a) -> Self { + self.hook = Some(Box::new(hook)); + self + } + + /// Sets the source path for this builder. + /// + /// # Arguments + /// + /// * `src_path` - A string reference that specifies the source directory for the build. + /// + /// # Returns + /// + /// A new `ShadowBuilder` instance with the specified source path. + pub fn src_path>(mut self, src_path: P) -> Self { + self.src_path = Some(src_path.as_ref().to_owned()); + self + } + + /// Sets the output path for this builder. + /// + /// # Arguments + /// + /// * `out_path` - A string reference that specifies the output directory for the build. + /// + /// # Returns + /// + /// A new `ShadowBuilder` instance with the specified output path. + pub fn out_path>(mut self, out_path: P) -> Self { + self.out_path = Some(out_path.as_ref().to_owned()); + self + } + + /// Sets the build pattern for this builder. + /// + /// # Arguments + /// + /// * `pattern` - A `BuildPattern` that determines when the package should be rebuilt. + /// + /// # Returns + /// + /// A new `ShadowBuilder` instance with the specified build pattern. + pub fn build_pattern(mut self, pattern: BuildPattern) -> Self { + self.build_pattern = pattern; + self + } + + /// Sets the denied constants for this builder. + /// + /// # Arguments + /// + /// * `deny_const` - A set of `ShadowConst` that should be excluded from the build. + /// + /// # Returns + /// + /// A new `ShadowBuilder` instance with the specified denied constants. + pub fn deny_const(mut self, deny_const: BTreeSet) -> Self { + self.deny_const = deny_const; + self + } + + /// Builds a `Shadow` instance based on the current configuration. + /// + /// # Returns + /// + /// A `SdResult` that represents the outcome of the build operation. + pub fn build(self) -> SdResult { + Shadow::build_inner(self) + } + + /// Gets the source path if it has been set. + /// + /// # Returns + /// + /// A `SdResult<&String>` containing the source path or an error if the path is missing. + pub fn get_src_path(&self) -> SdResult<&String> { + let src_path = self.src_path.as_ref().ok_or("missing `src_path`")?; + Ok(src_path) + } + + /// Gets the output path if it has been set. + /// + /// # Returns + /// + /// A `SdResult<&String>` containing the output path or an error if the path is missing. + pub fn get_out_path(&self) -> SdResult<&String> { + let out_path = self.out_path.as_ref().ok_or("missing `out_path`")?; + Ok(out_path) + } + + /// Gets the build pattern. + /// + /// # Returns + /// + /// A reference to the `BuildPattern` currently configured for this builder. + pub fn get_build_pattern(&self) -> &BuildPattern { + &self.build_pattern + } + + /// Gets the denied constants. + /// + /// # Returns + /// + /// A reference to the set of `ShadowConst` that are denied for this build. + pub fn get_deny_const(&self) -> &BTreeSet { + &self.deny_const + } + + /// Gets the build hook if it has been set. + /// + /// # Returns + /// + /// An option containing a reference to the hook if one is present. + pub fn get_hook(&'a self) -> Option<&'a (dyn HookExt + 'a)> { + self.hook.as_deref() + } +} diff --git a/src/date_time.rs b/src/date_time.rs index 55a7353..4de68f8 100644 --- a/src/date_time.rs +++ b/src/date_time.rs @@ -15,7 +15,6 @@ pub fn now_date_time() -> DateTime { // `SOURCE_DATE_EPOCH` env variable. // // https://reproducible-builds.org/docs/source-date-epoch/ - println!("cargo:rerun-if-env-changed=SOURCE_DATE_EPOCH"); match std::env::var_os("SOURCE_DATE_EPOCH") { None => DateTime::now(), Some(timestamp) => { diff --git a/src/lib.rs b/src/lib.rs index 7dc6c70..f0f0c1c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,8 +38,10 @@ //! Now in the root of your project (same directory as `Cargo.toml`) add a file `build.rs`: //! //! ```ignore -//! fn main() -> shadow_rs::SdResult<()> { -//! shadow_rs::new() +//! fn main() { +//! ShadowBuilder::builder() +//! .build_pattern(BuildPattern::RealTime) +//! .build().unwrap(); //! } //! ``` //! @@ -179,6 +181,7 @@ use crate::gen_const::{ }; pub use err::{SdResult, ShadowError}; +pub use crate::build::{BuildPattern, ShadowBuilder}; use crate::hook::HookExt; pub use {build::ShadowConst, env::*, git::*}; @@ -188,7 +191,7 @@ pub trait Format { const SHADOW_RS: &str = "shadow.rs"; -pub(crate) const CARGO_CLIPPY_ALLOW_ALL: &str = +pub const CARGO_CLIPPY_ALLOW_ALL: &str = "#[allow(clippy::all, clippy::pedantic, clippy::restriction, clippy::nursery)]"; /// Add a module with the provided name which contains the build information generated by `shadow-rs`. @@ -228,8 +231,12 @@ macro_rules! shadow { /// shadow_rs::new() /// } /// ``` +#[deprecated( + since = "0.37.0", + note = "Please use [`ShadowBuilder::builder`] instead" +)] pub fn new() -> SdResult<()> { - Shadow::build(default_deny())?; + ShadowBuilder::builder().build()?; Ok(()) } @@ -264,8 +271,12 @@ pub fn default_deny() -> BTreeSet { /// shadow_rs::new_deny(deny) /// } /// ``` +#[deprecated( + since = "0.37.0", + note = "Please use [`ShadowBuilder::builder`] instead" +)] pub fn new_deny(deny_const: BTreeSet) -> SdResult<()> { - Shadow::build(deny_const)?; + ShadowBuilder::builder().deny_const(deny_const).build()?; Ok(()) } @@ -292,12 +303,19 @@ pub fn new_deny(deny_const: BTreeSet) -> SdResult<()> { /// /// ``` /// +#[deprecated( + since = "0.37.0", + note = "Please use [`ShadowBuilder::builder`] instead" +)] pub fn new_hook(f: F) -> SdResult<()> where F: HookExt, { - let shadow = Shadow::build(f.default_deny())?; - shadow.hook(f.hook_inner()) + ShadowBuilder::builder() + .deny_const(f.default_deny()) + .hook(f.hook_inner()) + .build()?; + Ok(()) } /// Returns the contents of [`std::env::vars`] as an ordered map. @@ -311,18 +329,66 @@ pub(crate) fn get_std_env() -> BTreeMap { /// `shadow-rs` configuration. /// -/// If you use the recommended utility functions [`new`], [`new_deny`], or [`new_hook`], you do not have to handle [`Shadow`] configurations themselves. -/// However, this struct provides more fine-grained access to `shadow-rs` configuration, such as using a denylist and a hook function at the same time. +/// This struct encapsulates the configuration for the `shadow-rs` build process. It allows for fine-grained control over +/// various aspects of the build, including file output, build constants, environment variables, deny lists, and build patterns. +/// +/// While it is possible to construct a [`Shadow`] instance manually, it is highly recommended to use the [`ShadowBuilder`] builder pattern structure +/// provided by `shadow-rs`. The builder pattern simplifies the setup process and ensures that all necessary configurations are properly set up, +/// allowing you to customize multiple aspects simultaneously, such as using a denylist and a hook function at the same time. +/// +/// # Fields +/// +/// * `f`: The file that `shadow-rs` writes build information to. This file will contain serialized build constants and other metadata. +/// * `map`: A map of build constant identifiers to their corresponding `ConstVal`. These are the values that will be written into the file. +/// * `std_env`: A map of environment variables obtained through [`std::env::vars`]. These variables can influence the build process. +/// * `deny_const`: A set of build constant identifiers that should be excluded from the build process. This can be populated via [`ShadowBuilder::deny_const`]. +/// * `out_path`: The path where the generated files will be placed. This is usually derived from the `OUT_DIR` environment variable but can be customized via [`ShadowBuilder::out_path`]. +/// * `build_pattern`: Determines the strategy for triggering package rebuilds (`Lazy`, `RealTime`, or `Custom`). This affects when Cargo will rerun the build script and can be configured via [`ShadowBuilder::build_pattern`]. +/// +/// # Example +/// +/// ```no_run +/// use std::collections::BTreeSet; +/// use shadow_rs::{ShadowBuilder, BuildPattern, CARGO_TREE, CARGO_METADATA}; +/// +/// ShadowBuilder::builder() +/// .build_pattern(BuildPattern::RealTime) +/// .deny_const(BTreeSet::from([CARGO_TREE, CARGO_METADATA])) +/// .build().unwrap(); +/// ``` +/// #[derive(Debug)] pub struct Shadow { /// The file that `shadow-rs` writes build information to. + /// + /// This file will contain all the necessary information about the build, including serialized build constants and other metadata. pub f: File, + /// The values of build constants to be written. + /// + /// This is a mapping from `ShadowConst` identifiers to their corresponding `ConstVal` objects. Each entry in this map represents a build constant that will be included in the final build. pub map: BTreeMap, + /// Build environment variables, obtained through [`std::env::vars`]. + /// + /// These environment variables can affect the build process and are captured here for consistency and reproducibility. pub std_env: BTreeMap, - /// Constants in the deny list, passed through [`new_deny`] or [`Shadow::build`]. + + /// Constants in the deny list, passed through [`ShadowBuilder::deny_const`]. + /// + /// This set contains build constant identifiers that should be excluded from the build process. By specifying these, you can prevent certain constants from being written into the build file. pub deny_const: BTreeSet, + + /// The output path where generated files will be placed. + /// + /// This specifies the directory where the build script will write its output. It's typically set using the `OUT_DIR` environment variable but can be customized using [`ShadowBuilder::out_path`]. + pub out_path: String, + + /// Determines the strategy for triggering package rebuilds. + /// + /// This field sets the pattern for how often the package should be rebuilt. Options include `Lazy`, `RealTime`, and `Custom`, each with its own implications on the build frequency and conditions under which a rebuild is triggered. + /// It can be configured using [`ShadowBuilder::build_pattern`]. + pub build_pattern: BuildPattern, } impl Shadow { @@ -370,19 +436,38 @@ impl Shadow { /// Create a new [`Shadow`] configuration with a provided denylist. /// The project source path and output file are automatically derived from Cargo build environment variables. + #[deprecated( + since = "0.37.0", + note = "Please use [`ShadowBuilder::builder`] instead" + )] pub fn build(deny_const: BTreeSet) -> SdResult { - let src_path = std::env::var("CARGO_MANIFEST_DIR")?; - let out_path = std::env::var("OUT_DIR")?; - Self::build_with(src_path, out_path, deny_const) + ShadowBuilder::builder().deny_const(deny_const).build() } + #[deprecated( + since = "0.37.0", + note = "Please use [`ShadowBuilder::builder`] instead" + )] pub fn build_with( src_path: String, out_path: String, deny_const: BTreeSet, ) -> SdResult { + ShadowBuilder::builder() + .deny_const(deny_const) + .src_path(src_path) + .out_path(out_path) + .build() + } + + fn build_inner(builder: ShadowBuilder) -> SdResult { + let out_path = builder.get_out_path()?; + let src_path = builder.get_src_path()?; + let build_pattern = builder.get_build_pattern().clone(); + let deny_const = builder.get_deny_const().clone(); + let out = { - let path = Path::new(out_path.as_str()); + let path = Path::new(out_path); if !out_path.ends_with('/') { path.join(format!("{out_path}/{SHADOW_RS}")) } else { @@ -395,6 +480,8 @@ impl Shadow { map: Default::default(), std_env: Default::default(), deny_const, + out_path: out_path.to_string(), + build_pattern, }; shadow.std_env = get_std_env(); @@ -415,6 +502,11 @@ impl Shadow { shadow.write_all()?; + // handle hook + if let Some(h) = builder.get_hook() { + shadow.hook(h.hook_inner())? + } + Ok(shadow) } @@ -438,6 +530,10 @@ impl Shadow { } /// Request Cargo to re-run the build script if any environment variable observed by this [`Shadow`] configuration changes. + #[deprecated( + since = "0.37.0", + note = "Please use [`ShadowBuilder::build_pattern`] instead" + )] pub fn cargo_rerun_if_env_changed(&self) { for k in self.std_env.keys() { println!("cargo:rerun-if-env-changed={k}"); @@ -446,6 +542,10 @@ impl Shadow { /// Request Cargo to re-run the build script if any of the specified environment variables change. /// This function is not influenced by this [`Shadow`] configuration. + #[deprecated( + since = "0.37.0", + note = "Please use [`ShadowBuilder::build_pattern`] instead" + )] pub fn cargo_rerun_env_inject(&self, env: &[&str]) { for k in env { println!("cargo:rerun-if-env-changed={}", *k); @@ -453,8 +553,10 @@ impl Shadow { } fn gen_const(&mut self) -> SdResult<()> { + let out_dir = &self.out_path; + self.build_pattern.rerun_if(self.map.keys(), out_dir); + for (k, v) in self.map.clone() { - println!("cargo:rerun-if-env-changed={k}"); self.write_const(k, v)?; } Ok(()) @@ -466,7 +568,7 @@ impl Shadow { // Author: https://www.github.com/baoyachi // Generation time: {} "#, - DateTime::now().human_format() + DateTime::now().to_rfc2822() ); writeln!(&self.f, "{desc}\n\n")?; Ok(()) @@ -584,7 +686,10 @@ mod tests { #[test] fn test_build() -> SdResult<()> { - Shadow::build_with("./".into(), "./".into(), Default::default())?; + ShadowBuilder::builder() + .src_path("./") + .out_path("./") + .build()?; let shadow = fs::read_to_string("./shadow.rs")?; assert!(!shadow.is_empty()); assert!(shadow.lines().count() > 0); @@ -593,13 +698,16 @@ mod tests { #[test] fn test_build_deny() -> SdResult<()> { - let mut deny = BTreeSet::new(); - deny.insert(CARGO_TREE); - Shadow::build_with("./".into(), "./".into(), deny)?; + ShadowBuilder::builder() + .src_path("./") + .out_path("./") + .deny_const(BTreeSet::from([CARGO_TREE])) + .build()?; + let shadow = fs::read_to_string("./shadow.rs")?; assert!(!shadow.is_empty()); assert!(shadow.lines().count() > 0); - println!("{shadow}"); + // println!("{shadow}"); let expect = "pub const CARGO_TREE :&str"; assert!(!shadow.contains(expect)); Ok(())