aboutsummaryrefslogtreecommitdiff
path: root/mingling_macros/src
diff options
context:
space:
mode:
Diffstat (limited to 'mingling_macros/src')
-rw-r--r--mingling_macros/src/build.rs56
-rw-r--r--mingling_macros/src/build/comp.rs158
-rw-r--r--mingling_macros/src/build/pathf.rs19
-rw-r--r--mingling_macros/src/func/gen_program.rs65
-rw-r--r--mingling_macros/src/lib.rs47
5 files changed, 326 insertions, 19 deletions
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::<syn::LitStr>(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<PathBuf, std::io::Error> {
+ 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<PathBuf, MinglingPathfinderError> {
+ 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<proc_macro2::TokenStream> = {
+ // 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<proc_macro2::TokenStream> {
- 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.
///