From 89bf57f33a9f0f5a96fc50e272c83d926fa35c4a Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Mon, 17 Aug 2026 19:21:52 +0800 Subject: refactor!: replace build.rs with compile-time macro build steps BREAKING CHANGE: Replace the `build`/`builds` feature system with compile-time macro-driven generation. `gen_program!()` now automatically invokes `build_comp!()` and `build_pathf!()` when the `comp`/`pathf` features are enabled, eliminating the need for `build.rs` and `[build-dependencies]` blocks. This removes the `build` feature, the `mingling::build` module, and all related build-time API functions. Completion scripts are now written to `{target_directory}/mingling/` instead of `target/release/`. The `mingling_cli` uses `build_comp!("mling")` for its custom binary name. --- mingling_macros/src/build.rs | 56 +++++++++++ mingling_macros/src/build/comp.rs | 158 ++++++++++++++++++++++++++++++++ mingling_macros/src/build/pathf.rs | 19 ++++ mingling_macros/src/func/gen_program.rs | 65 +++++++++---- mingling_macros/src/lib.rs | 47 ++++++++++ 5 files changed, 326 insertions(+), 19 deletions(-) create mode 100644 mingling_macros/src/build.rs create mode 100644 mingling_macros/src/build/comp.rs create mode 100644 mingling_macros/src/build/pathf.rs (limited to 'mingling_macros/src') diff --git a/mingling_macros/src/build.rs b/mingling_macros/src/build.rs new file mode 100644 index 0000000..8f2949d --- /dev/null +++ b/mingling_macros/src/build.rs @@ -0,0 +1,56 @@ +//! Compile-time build logic for `build_comp!()` and `build_pathf!()`. +//! +//! The build steps run as a side effect of macro expansion (during `gen_program!`), +//! writing artifacts under `{target_directory}/mingling/`. + +#[doc(hidden)] +#[cfg(feature = "comp")] +pub(crate) mod comp; + +#[doc(hidden)] +#[cfg(feature = "pathf")] +pub(crate) mod pathf; + +/// Shared implementation behind `build_comp!()`. +/// +/// Accepts an optional string literal (the binary name); defaults to +/// `CARGO_PKG_NAME`. Returns an empty token stream on success, or a +/// `compile_error!` token stream on failure. +#[cfg(feature = "comp")] +pub(crate) fn comp_build_impl(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let bin_name: String = if input.is_empty() { + std::env::var("CARGO_PKG_NAME").unwrap_or_default() + } else { + match syn::parse::(input) { + Ok(lit) => lit.value(), + Err(e) => return e.to_compile_error().into(), + } + }; + + match comp::build_comp_scripts(&bin_name) { + Ok(()) => proc_macro::TokenStream::new(), + Err(e) => { + let msg = format!("build_comp: failed to generate completion scripts: {e}"); + syn::Error::new(proc_macro2::Span::call_site(), msg) + .to_compile_error() + .into() + } + } +} + +/// Shared implementation behind `build_pathf!()`. +/// +/// Runs the pathf type-mapping analysis. Returns an empty token stream on +/// success, or a `compile_error!` token stream on failure. +#[cfg(feature = "pathf")] +pub(crate) fn pathf_build_impl(_input: proc_macro::TokenStream) -> proc_macro::TokenStream { + match pathf::analyze_and_build_type_mapping() { + Ok(()) => proc_macro::TokenStream::new(), + Err(e) => { + let msg = format!("build_pathf: type mapping analysis failed: {e}"); + syn::Error::new(proc_macro2::Span::call_site(), msg) + .to_compile_error() + .into() + } + } +} diff --git a/mingling_macros/src/build/comp.rs b/mingling_macros/src/build/comp.rs new file mode 100644 index 0000000..81a26e8 --- /dev/null +++ b/mingling_macros/src/build/comp.rs @@ -0,0 +1,158 @@ +use std::path::PathBuf; + +use just_template::tmpl; + +/// Represents the shell environment for which the output format is intended. +/// +/// This is an internal copy of `mingling_core::ShellFlag`, kept private to the +/// build module because the macros crate must not depend on `mingling_core`. +/// Which variants are constructed depends on the target OS (`#[cfg]`), so +/// platform-gated variants may be unused on any given host. +#[allow(dead_code)] +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub(crate) enum ShellFlag { + /// Represents the Bash shell. + #[default] + Bash, + /// Represents the Zsh shell. + Zsh, + /// Represents the Fish shell. + Fish, + /// Represents `PowerShell`. + Powershell, + /// A custom or unsupported shell type, identified by the provided string. + Other(String), +} + +const TMPL_COMP_BASH: &str = include_str!("../../tmpls/comps/bash.sh"); +const TMPL_COMP_ZSH: &str = include_str!("../../tmpls/comps/zsh.zsh"); +const TMPL_COMP_FISH: &str = include_str!("../../tmpls/comps/fish.fish"); +const TMPL_COMP_PWSH: &str = include_str!("../../tmpls/comps/pwsh.ps1"); + +/// Generate shell completion scripts for a given binary name. +/// +/// On Windows, generates `PowerShell` completion. +/// On Linux, generates Zsh, Bash, and Fish completions. +/// Scripts are written to the `OUT_DIR` (or `target/` if `OUT_DIR` is not set). +/// +/// # Errors +/// +/// Returns an [`std::io::Error`] if a script cannot be written. +pub(crate) fn build_comp_scripts(name: &str) -> Result<(), std::io::Error> { + #[cfg(target_os = "windows")] + { + build_comp_script(&ShellFlag::Powershell, name)?; + Ok(()) + } + + #[cfg(target_os = "linux")] + { + build_comp_script(&ShellFlag::Zsh, name)?; + build_comp_script(&ShellFlag::Bash, name)?; + build_comp_script(&ShellFlag::Fish, name)?; + Ok(()) + } + + #[cfg(target_os = "macos")] + { + build_comp_script(&ShellFlag::Zsh, name)?; + build_comp_script(&ShellFlag::Bash, name)?; + build_comp_script(&ShellFlag::Fish, name)?; + Ok(()) + } +} + +/// Generate a shell completion script for a specific shell. +/// +/// This function takes a shell flag and a binary name, selects the appropriate +/// template, substitutes the binary name into the template, and writes the +/// resulting completion script to the Mingling build directory +/// (`{target_directory}/mingling/`, resolved via `cargo metadata`). +/// +/// # Errors +/// +/// Returns an [`std::io::Error`] if the script cannot be written. +pub(crate) fn build_comp_script( + shell_flag: &ShellFlag, + bin_name: &str, +) -> Result<(), std::io::Error> { + let output_dir = comp_output_dir()?; + build_comp_script_to(shell_flag, bin_name, &output_dir.to_string_lossy()) +} + +/// The directory where completion scripts are written: `{target_directory}/mingling/`. +fn comp_output_dir() -> Result { + mingling_pathf::build_output_dir().map_err(|e| std::io::Error::other(e.to_string())) +} + +/// Generate a shell completion script to a specified directory. +/// +/// This function takes a shell flag, a binary name, and a target directory path, +/// selects the appropriate template, substitutes the binary name into the template, +/// and writes the resulting completion script to the specified directory. +/// +/// # Errors +/// +/// Returns an [`std::io::Error`] if the script cannot be written. +pub(crate) fn build_comp_script_to( + shell_flag: &ShellFlag, + bin_name: &str, + target_dir: &str, +) -> Result<(), std::io::Error> { + let (tmpl_str, ext) = get_tmpl(shell_flag); + let mut tmpl = just_template::Template::from(tmpl_str); + tmpl!(bin_name = bin_name); + let target_path = std::path::PathBuf::from(target_dir); + std::fs::create_dir_all(&target_path)?; + let output_path = target_path.join(format!("{bin_name}_comp{ext}")); + std::fs::write(&output_path, tmpl.to_string()) +} + +const fn get_tmpl(shell_flag: &ShellFlag) -> (&'static str, &'static str) { + match shell_flag { + ShellFlag::Bash | ShellFlag::Other(_) => (TMPL_COMP_BASH, ".sh"), + ShellFlag::Zsh => (TMPL_COMP_ZSH, ".zsh"), + ShellFlag::Fish => (TMPL_COMP_FISH, ".fish"), + ShellFlag::Powershell => (TMPL_COMP_PWSH, ".ps1"), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn get_tmpl_bash() { + let (tmpl, ext) = get_tmpl(&ShellFlag::Bash); + assert_eq!(ext, ".sh"); + assert!(!tmpl.is_empty(), "bash template should not be empty"); + } + + #[test] + fn get_tmpl_zsh() { + let (tmpl, ext) = get_tmpl(&ShellFlag::Zsh); + assert_eq!(ext, ".zsh"); + assert!(!tmpl.is_empty(), "zsh template should not be empty"); + } + + #[test] + fn get_tmpl_fish() { + let (tmpl, ext) = get_tmpl(&ShellFlag::Fish); + assert_eq!(ext, ".fish"); + assert!(!tmpl.is_empty(), "fish template should not be empty"); + } + + #[test] + fn get_tmpl_powershell() { + let (tmpl, ext) = get_tmpl(&ShellFlag::Powershell); + assert_eq!(ext, ".ps1"); + assert!(!tmpl.is_empty(), "powershell template should not be empty"); + } + + #[test] + fn get_tmpl_other() { + let (tmpl, ext) = get_tmpl(&ShellFlag::Other("custom".to_string())); + assert_eq!(ext, ".sh"); + assert!(!tmpl.is_empty(), "fallback template should not be empty"); + } +} diff --git a/mingling_macros/src/build/pathf.rs b/mingling_macros/src/build/pathf.rs new file mode 100644 index 0000000..788f527 --- /dev/null +++ b/mingling_macros/src/build/pathf.rs @@ -0,0 +1,19 @@ +use std::path::PathBuf; + +use mingling_pathf::error::MinglingPathfinderError; + +/// The directory where pathf's build artifacts are stored for the current +/// crate: `{target_directory}/mingling/{CARGO_PKG_NAME}`. +pub fn output_dir() -> Result { + Ok(mingling_pathf::build_output_dir()?.join(crate_name())) +} + +/// Runs the pathf type-mapping analysis for the current crate at compile time +/// (replacing the previous `build.rs` call). +pub fn analyze_and_build_type_mapping() -> Result<(), MinglingPathfinderError> { + mingling_pathf::analyze_and_build_type_mapping() +} + +fn crate_name() -> String { + std::env::var("CARGO_PKG_NAME").unwrap_or_default() +} diff --git a/mingling_macros/src/func/gen_program.rs b/mingling_macros/src/func/gen_program.rs index c0a7ea8..35e8352 100644 --- a/mingling_macros/src/func/gen_program.rs +++ b/mingling_macros/src/func/gen_program.rs @@ -7,6 +7,11 @@ use quote::quote; /// Generates the `Next` type alias, `Routable` impl for `ChainProcess`, /// and delegates to `program_comp_gen!()`, `program_fallback_gen!()`, /// and `program_final_gen!()`. +/// +/// When the `comp` / `pathf` features are enabled, the expansion begins by +/// invoking `build_comp!()` / `build_pathf!()`, which run the build steps +/// (previously done in `build.rs`) as a compile-time side effect and expand +/// to nothing. pub(crate) fn gen_program_impl(_input: TokenStream) -> TokenStream { #[cfg(feature = "comp")] let comp_gen = quote! { @@ -16,18 +21,46 @@ pub(crate) fn gen_program_impl(_input: TokenStream) -> TokenStream { #[cfg(not(feature = "comp"))] let comp_gen = quote! {}; - // When pathf is enabled, load the type_using.rs generated by the build script + // `build_pathf!()` / `build_comp!()` are invoked at the very beginning of the + // expansion: they run the build logic at compile time and expand to nothing. + #[cfg(feature = "comp")] + let comp_build = quote! { + ::mingling::macros::build_comp!(); + }; + + #[cfg(not(feature = "comp"))] + let comp_build = quote! {}; + + #[cfg(feature = "pathf")] + let pathf_build = quote! { + ::mingling::macros::build_pathf!(); + }; + + #[cfg(not(feature = "pathf"))] + let pathf_build = quote! {}; + + // When pathf is enabled, load the type_using.rs generated by the build logic // and emit its use statements so types from submodules are in scope. #[cfg(feature = "pathf")] let pathf_uses: Vec = { + // The `build_pathf!()` macro emitted above will (re-)run the analysis + // during expansion, but the `use` statements are needed right now, so + // make sure the mapping exists before reading it. + if let Err(e) = crate::build::pathf::analyze_and_build_type_mapping() { + let msg = format!("pathf: type mapping analysis failed: {e}"); + return syn::Error::new(proc_macro2::Span::call_site(), msg) + .to_compile_error() + .into(); + } let uses = load_pathf_uses(); if uses.is_empty() { - // The file might not exist yet — emit a clear hint + // The analyzer found nothing — emit a clear hint let hint: proc_macro2::TokenStream = syn::parse_quote! { compile_error!( - "pathf: `{}` not found or empty.\n\ - Make sure `build.rs` calls `mingling::build::analyze_and_build_type_mapping().unwrap();`\n\ - with features [\"build\", \"pathf\"] enabled." + "pathf: no types were found by the analyzer.\n\ + Make sure the `pathf` feature is enabled (which also enables\n\ + the `build_pathf!()` macro) and that `gen_program!()` is called\n\ + in a crate with a `src/` directory." ); }; vec![hint] @@ -47,6 +80,8 @@ pub(crate) fn gen_program_impl(_input: TokenStream) -> TokenStream { }; TokenStream::from(quote! { + #comp_build + #pathf_build pub use __this_program_impl::*; #[doc(hidden)] @@ -88,24 +123,16 @@ pub(crate) fn gen_program_impl(_input: TokenStream) -> TokenStream { }) } -/// Loads `type_using.rs` generated by the pathf build script and returns each +/// Loads `type_using.rs` generated by the pathf build logic and returns each /// `use ...;` line as a token stream, ready to be emitted in the generated output. #[cfg(feature = "pathf")] fn load_pathf_uses() -> Vec { - let out_dir = match std::env::var("OUT_DIR") { - Ok(d) => d, - Err(_) => return Vec::new(), - }; - let crate_name = match std::env::var("CARGO_PKG_NAME") { - Ok(n) => n, - Err(_) => return Vec::new(), + let Ok(output_dir) = crate::build::pathf::output_dir() else { + return Vec::new(); }; - let path = std::path::Path::new(&out_dir) - .join(&crate_name) - .join("type_using.rs"); - let content = match std::fs::read_to_string(&path) { - Ok(c) => c, - Err(_) => return Vec::new(), + let path = output_dir.join("type_using.rs"); + let Ok(content) = std::fs::read_to_string(&path) else { + return Vec::new(); }; content .lines() diff --git a/mingling_macros/src/lib.rs b/mingling_macros/src/lib.rs index d00773c..1aaedb1 100644 --- a/mingling_macros/src/lib.rs +++ b/mingling_macros/src/lib.rs @@ -20,6 +20,9 @@ mod derive; mod func; mod systems; +#[cfg(any(feature = "comp", feature = "pathf"))] +mod build; + mod extensions; mod utils; @@ -1653,6 +1656,50 @@ pub fn gen_program(input: TokenStream) -> TokenStream { func::gen_program::gen_program_impl(input) } +/// Executes the completion-script build at compile time and expands to nothing. +/// +/// **This macro is only available with the `comp` feature.** +/// +/// The completion scripts are written to `{target_directory}/mingling/` (the +/// target directory is resolved via `cargo metadata`). +/// +/// `gen_program!()` calls this macro automatically when the `comp` feature is +/// enabled. It can also be invoked manually to customize the binary name: +/// +/// - `build_comp!()` — uses the current package name (`CARGO_PKG_NAME`). +/// - `build_comp!("mybin")` — uses the given binary name. +/// +/// ```rust,ignore +/// mingling::macros::build_comp!(); +/// // or: +/// mingling::macros::build_comp!("mybin"); +/// ``` +#[cfg(feature = "comp")] +#[proc_macro] +pub fn build_comp(input: TokenStream) -> TokenStream { + build::comp_build_impl(input) +} + +/// Executes the pathf type-mapping build at compile time and expands to nothing. +/// +/// **This macro is only available with the `pathf` feature.** +/// +/// The mapping files are written to `{target_directory}/mingling/{CARGO_PKG_NAME}/` +/// (the target directory is resolved via `cargo metadata`), and are consumed by +/// `gen_program!()` so that types defined in submodules are resolved automatically. +/// +/// `gen_program!()` calls this macro automatically when the `pathf` feature is +/// enabled. +/// +/// ```rust,ignore +/// mingling::macros::build_pathf!(); +/// ``` +#[cfg(feature = "pathf")] +#[proc_macro] +pub fn build_pathf(input: TokenStream) -> TokenStream { + build::pathf_build_impl(input) +} + /// Internal macro used by `gen_program!` to generate the completion infrastructure for /// shell completion support. /// -- cgit