diff --git a/Cargo.lock b/Cargo.lock index d89bfe8f..d4ba167f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -989,10 +989,12 @@ dependencies = [ "conduwuit_build_metadata", "conduwuit_core", "conduwuit_database", + "conduwuit_macros", "conduwuit_router", "conduwuit_service", "console-subscriber", "const-str", + "ctor", "hardened_malloc-rs", "log", "opentelemetry", @@ -1021,6 +1023,7 @@ dependencies = [ "conduwuit_macros", "conduwuit_service", "const-str", + "ctor", "futures", "log", "ruma", @@ -1042,8 +1045,10 @@ dependencies = [ "base64 0.22.1", "bytes", "conduwuit_core", + "conduwuit_macros", "conduwuit_service", "const-str", + "ctor", "futures", "hmac", "http", @@ -1068,6 +1073,7 @@ name = "conduwuit_build_metadata" version = "0.5.3" dependencies = [ "built", + "cargo_metadata", ] [[package]] @@ -1137,7 +1143,9 @@ version = "0.5.3" dependencies = [ "async-channel", "conduwuit_core", + "conduwuit_macros", "const-str", + "ctor", "futures", "log", "minicbor", @@ -1153,6 +1161,7 @@ dependencies = [ name = "conduwuit_macros" version = "0.5.3" dependencies = [ + "cargo_toml", "itertools 0.14.0", "proc-macro2", "quote", @@ -1171,9 +1180,11 @@ dependencies = [ "conduwuit_admin", "conduwuit_api", "conduwuit_core", + "conduwuit_macros", "conduwuit_service", "conduwuit_web", "const-str", + "ctor", "futures", "http", "http-body-util", @@ -1203,7 +1214,9 @@ dependencies = [ "bytes", "conduwuit_core", "conduwuit_database", + "conduwuit_macros", "const-str", + "ctor", "either", "futures", "hickory-resolver", diff --git a/src/admin/Cargo.toml b/src/admin/Cargo.toml index 3bf924ba..82c73dfb 100644 --- a/src/admin/Cargo.toml +++ b/src/admin/Cargo.toml @@ -2,6 +2,7 @@ name = "conduwuit_admin" description.workspace = true edition.workspace = true +homepage.workspace = true license.workspace = true readme.workspace = true repository.workspace = true @@ -79,6 +80,7 @@ conduwuit-database.workspace = true conduwuit-macros.workspace = true conduwuit-service.workspace = true const-str.workspace = true +ctor.workspace = true futures.workspace = true log.workspace = true ruma.workspace = true diff --git a/src/admin/mod.rs b/src/admin/mod.rs index b343fd2e..3c47f485 100644 --- a/src/admin/mod.rs +++ b/src/admin/mod.rs @@ -3,6 +3,8 @@ #![allow(clippy::enum_glob_use)] #![allow(clippy::too_many_arguments)] +conduwuit_macros::introspect_crate! {} + pub(crate) mod admin; pub(crate) mod context; pub(crate) mod processor; diff --git a/src/admin/server/commands.rs b/src/admin/server/commands.rs index f8195df6..c4298bed 100644 --- a/src/admin/server/commands.rs +++ b/src/admin/server/commands.rs @@ -1,4 +1,4 @@ -use std::{path::PathBuf, sync::Arc}; +use std::{fmt::Write, path::PathBuf, sync::Arc}; use conduwuit::{ Err, Result, @@ -153,3 +153,97 @@ pub(super) async fn shutdown(&self) -> Result { self.write_str("Shutting down server...").await } + +#[admin_command] +pub(super) async fn list_features(&self) -> Result { + let mut enabled_features = conduwuit::info::introspection::ENABLED_FEATURES + .lock() + .expect("locked") + .iter() + .flat_map(|(_, f)| f.iter()) + .collect::>(); + + enabled_features.sort_unstable(); + enabled_features.dedup(); + + let mut available_features = conduwuit::build_metadata::WORKSPACE_FEATURES + .iter() + .flat_map(|(_, f)| f.iter()) + .collect::>(); + + available_features.sort_unstable(); + available_features.dedup(); + + let mut features = String::new(); + + for feature in available_features { + let active = enabled_features.contains(&feature); + let emoji = if active { "✅" } else { "❌" }; + let remark = if active { "[enabled]" } else { "" }; + writeln!(features, "{emoji} {feature} {remark}")?; + } + + self.write_str(&features).await +} + +#[admin_command] +pub(super) async fn build_info(&self) -> Result { + use conduwuit::build_metadata::built; + + let mut info = String::new(); + + // Version information + writeln!(info, "# Build Information\n")?; + writeln!(info, "**Version:** {}", built::PKG_VERSION)?; + writeln!(info, "**Package:** {}", built::PKG_NAME)?; + writeln!(info, "**Description:** {}", built::PKG_DESCRIPTION)?; + + // Git information + writeln!(info, "\n## Git Information\n")?; + if let Some(hash) = conduwuit::build_metadata::GIT_COMMIT_HASH { + writeln!(info, "**Commit Hash:** {hash}")?; + } + if let Some(hash) = conduwuit::build_metadata::GIT_COMMIT_HASH_SHORT { + writeln!(info, "**Commit Hash (short):** {hash}")?; + } + if let Some(url) = conduwuit::build_metadata::GIT_REMOTE_WEB_URL { + writeln!(info, "**Repository:** {url}")?; + } + if let Some(url) = conduwuit::build_metadata::GIT_REMOTE_COMMIT_URL { + writeln!(info, "**Commit URL:** {url}")?; + } + + // Build environment + writeln!(info, "\n## Build Environment\n")?; + writeln!(info, "**Profile:** {}", built::PROFILE)?; + writeln!(info, "**Optimization Level:** {}", built::OPT_LEVEL)?; + writeln!(info, "**Debug:** {}", built::DEBUG)?; + writeln!(info, "**Target:** {}", built::TARGET)?; + writeln!(info, "**Host:** {}", built::HOST)?; + + // Rust compiler information + writeln!(info, "\n## Compiler Information\n")?; + writeln!(info, "**Rustc Version:** {}", built::RUSTC_VERSION)?; + if !built::RUSTDOC_VERSION.is_empty() { + writeln!(info, "**Rustdoc Version:** {}", built::RUSTDOC_VERSION)?; + } + + // Target configuration + writeln!(info, "\n## Target Configuration\n")?; + writeln!(info, "**Architecture:** {}", built::CFG_TARGET_ARCH)?; + writeln!(info, "**OS:** {}", built::CFG_OS)?; + writeln!(info, "**Family:** {}", built::CFG_FAMILY)?; + writeln!(info, "**Endianness:** {}", built::CFG_ENDIAN)?; + writeln!(info, "**Pointer Width:** {} bits", built::CFG_POINTER_WIDTH)?; + if !built::CFG_ENV.is_empty() { + writeln!(info, "**Environment:** {}", built::CFG_ENV)?; + } + + // CI information + if let Some(ci) = built::CI_PLATFORM { + writeln!(info, "\n## CI Platform\n")?; + writeln!(info, "**Platform:** {ci}")?; + } + + self.write_str(&info).await +} diff --git a/src/admin/server/mod.rs b/src/admin/server/mod.rs index 4838aa31..2ea86799 100644 --- a/src/admin/server/mod.rs +++ b/src/admin/server/mod.rs @@ -52,4 +52,10 @@ pub enum ServerCommand { /// Shutdown the server Shutdown, + + /// List features built into the server + ListFeatures {}, + + /// Build information + BuildInfo {}, } diff --git a/src/api/Cargo.toml b/src/api/Cargo.toml index 393ceecf..90ef94d4 100644 --- a/src/api/Cargo.toml +++ b/src/api/Cargo.toml @@ -2,6 +2,7 @@ name = "conduwuit_api" description.workspace = true edition.workspace = true +homepage.workspace = true license.workspace = true readme.workspace = true repository.workspace = true @@ -72,8 +73,10 @@ axum.workspace = true base64.workspace = true bytes.workspace = true conduwuit-core.workspace = true +conduwuit-macros.workspace = true conduwuit-service.workspace = true const-str.workspace = true +ctor.workspace = true futures.workspace = true hmac.workspace = true http.workspace = true diff --git a/src/api/mod.rs b/src/api/mod.rs index a2be1c79..89bdc5c3 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -3,6 +3,9 @@ extern crate conduwuit_core as conduwuit; extern crate conduwuit_service as service; + +conduwuit_macros::introspect_crate! {} + pub mod client; pub mod router; pub mod server; diff --git a/src/build_metadata/Cargo.toml b/src/build_metadata/Cargo.toml index 1e0bd50f..d6ea31a3 100644 --- a/src/build_metadata/Cargo.toml +++ b/src/build_metadata/Cargo.toml @@ -2,6 +2,7 @@ name = "conduwuit_build_metadata" description.workspace = true edition.workspace = true +homepage.workspace = true license.workspace = true readme.workspace = true repository.workspace = true @@ -27,6 +28,6 @@ crate-type = [ [build-dependencies] built = { version = "0.8", features = [] } - +cargo_metadata = { version = "0.23.1" } [lints] workspace = true diff --git a/src/build_metadata/build.rs b/src/build_metadata/build.rs index bf84d508..0da02dd5 100644 --- a/src/build_metadata/build.rs +++ b/src/build_metadata/build.rs @@ -1,5 +1,9 @@ -use std::process::Command; +use std::{ + collections::BTreeMap, env, fmt::Write as FmtWrite, fs, io::Write, path::Path, + process::Command, +}; +use cargo_metadata::MetadataCommand; fn run_git_command(args: &[&str]) -> Option { Command::new("git") .args(args) @@ -11,12 +15,60 @@ fn run_git_command(args: &[&str]) -> Option { .filter(|s| !s.is_empty()) } fn get_env(env_var: &str) -> Option { - match std::env::var(env_var) { + match env::var(env_var) { | Ok(val) if !val.is_empty() => Some(val), | _ => None, } } fn main() { + println!("cargo:rerun-if-changed=Cargo.toml"); + + let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); // Cargo.toml path + let manifest_path = Path::new(&manifest_dir).join("Cargo.toml"); + + let metadata = MetadataCommand::new() + .manifest_path(&manifest_path) + .no_deps() + .exec() + .expect("failed to parse `cargo metadata`"); + + let workspace_packages = metadata + .workspace_members + .iter() + .map(|package| { + let package = metadata.packages.iter().find(|p| p.id == *package).unwrap(); + println!("cargo:rerun-if-changed={}", package.manifest_path.as_str()); + package + }) + .collect::>(); + + // Extract available features from workspace packages + let mut available_features: BTreeMap> = BTreeMap::new(); + for package in &workspace_packages { + let crate_name = package + .name + .trim_start_matches("conduwuit-") + .replace('-', "_"); + let features: Vec = package.features.keys().cloned().collect(); + if !features.is_empty() { + available_features.insert(crate_name, features); + } + } + + // Generate Rust code for available features + let features_code = generate_features_code(&available_features); + let features_dst = + Path::new(&env::var("OUT_DIR").expect("OUT_DIR not set")).join("available_features.rs"); + let mut features_file = fs::File::create(features_dst).unwrap(); + features_file.write_all(features_code.as_bytes()).unwrap(); + + let dst = Path::new(&env::var("OUT_DIR").expect("OUT_DIR not set")).join("pkg.json"); + + let mut out_file = fs::File::create(dst).unwrap(); + out_file + .write_all(format!("{workspace_packages:?}").as_bytes()) + .unwrap(); + // built gets the default crate from the workspace. Not sure if this is intended // behavior, but it's what we want. built::write_built_file().expect("Failed to acquire build-time information"); @@ -91,3 +143,30 @@ fn main() { println!("cargo:rerun-if-env-changed=GIT_REMOTE_URL"); println!("cargo:rerun-if-env-changed=GIT_REMOTE_COMMIT_URL"); } + +fn generate_features_code(features: &BTreeMap>) -> String { + let mut code = String::from( + r#" +/// All available features for workspace crates +pub const WORKSPACE_FEATURES: &[(&str, &[&str])] = &[ +"#, + ); + + for (crate_name, feature_list) in features { + write!(code, " (\"{crate_name}\", &[").unwrap(); + for (i, feature) in feature_list.iter().enumerate() { + if i > 0 { + code.push_str(", "); + } + write!(code, "\"{feature}\"").unwrap(); + } + code.push_str("]),\n"); + } + + code.push_str( + r#"]; +"#, + ); + + code +} diff --git a/src/build_metadata/mod.rs b/src/build_metadata/mod.rs index 86a8a800..f32a38b7 100644 --- a/src/build_metadata/mod.rs +++ b/src/build_metadata/mod.rs @@ -2,6 +2,10 @@ pub mod built { include!(concat!(env!("OUT_DIR"), "/built.rs")); } +// Include generated available features +// This provides: pub const WORKSPACE_FEATURES: &[(&str, &[&str])] +include!(concat!(env!("OUT_DIR"), "/available_features.rs")); + pub static GIT_COMMIT_HASH: Option<&str> = option_env!("GIT_COMMIT_HASH"); pub static GIT_COMMIT_HASH_SHORT: Option<&str> = option_env!("GIT_COMMIT_HASH_SHORT"); diff --git a/src/core/Cargo.toml b/src/core/Cargo.toml index 8139f2d3..53329598 100644 --- a/src/core/Cargo.toml +++ b/src/core/Cargo.toml @@ -2,6 +2,7 @@ name = "conduwuit_core" description.workspace = true edition.workspace = true +homepage.workspace = true license.workspace = true readme.workspace = true repository.workspace = true diff --git a/src/core/info/introspection.rs b/src/core/info/introspection.rs new file mode 100644 index 00000000..9bd17976 --- /dev/null +++ b/src/core/info/introspection.rs @@ -0,0 +1,7 @@ +//! Information about features the crates were compiled with. +//! Only available for crates that have called the `introspect_crate` macro + +use std::collections::BTreeMap; + +pub static ENABLED_FEATURES: std::sync::Mutex> = + std::sync::Mutex::new(BTreeMap::new()); diff --git a/src/core/info/mod.rs b/src/core/info/mod.rs index bd4fd69d..4e1101d0 100644 --- a/src/core/info/mod.rs +++ b/src/core/info/mod.rs @@ -1,3 +1,4 @@ +pub mod introspection; pub mod room_version; pub mod version; diff --git a/src/core/mod.rs b/src/core/mod.rs index bcd6af83..6dfd61a4 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -19,6 +19,7 @@ pub use ::smallstr; pub use ::smallvec; pub use ::toml; pub use ::tracing; +pub use conduwuit_build_metadata as build_metadata; pub use config::Config; pub use error::Error; pub use info::{ @@ -34,6 +35,8 @@ pub use utils::{implement, result, result::Result}; pub use crate as conduwuit_core; +conduwuit_macros::introspect_crate! {} + #[cfg(any(not(conduwuit_mods), not(feature = "conduwuit_mods")))] pub mod mods { #[macro_export] diff --git a/src/database/Cargo.toml b/src/database/Cargo.toml index e17aa0b9..a32be35e 100644 --- a/src/database/Cargo.toml +++ b/src/database/Cargo.toml @@ -2,6 +2,7 @@ name = "conduwuit_database" description.workspace = true edition.workspace = true +homepage.workspace = true license.workspace = true readme.workspace = true repository.workspace = true @@ -54,7 +55,9 @@ bindgen-runtime = [ [dependencies] async-channel.workspace = true conduwuit-core.workspace = true +conduwuit-macros.workspace = true const-str.workspace = true +ctor.workspace = true futures.workspace = true log.workspace = true minicbor.workspace = true diff --git a/src/database/mod.rs b/src/database/mod.rs index 71f1ba83..499bd9f2 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -3,6 +3,8 @@ extern crate conduwuit_core as conduwuit; extern crate rust_rocksdb as rocksdb; +conduwuit_macros::introspect_crate! {} + conduwuit::mod_ctor! {} conduwuit::mod_dtor! {} diff --git a/src/macros/Cargo.toml b/src/macros/Cargo.toml index e691944f..4913b9d1 100644 --- a/src/macros/Cargo.toml +++ b/src/macros/Cargo.toml @@ -2,6 +2,7 @@ name = "conduwuit_macros" description.workspace = true edition.workspace = true +homepage.workspace = true license.workspace = true readme.workspace = true repository.workspace = true @@ -17,6 +18,7 @@ syn.workspace = true quote.workspace = true proc-macro2.workspace = true itertools.workspace = true +cargo_toml.workspace = true [lints] workspace = true diff --git a/src/macros/build_info.rs b/src/macros/build_info.rs new file mode 100644 index 00000000..10d99d2d --- /dev/null +++ b/src/macros/build_info.rs @@ -0,0 +1,63 @@ +use proc_macro2::TokenStream; +use quote::quote; + +use crate::Result; + +pub(super) fn introspect(_args: TokenStream) -> Result { + let cargo_crate_name = std::env::var("CARGO_CRATE_NAME").unwrap(); + let crate_name = cargo_crate_name.trim_start_matches("conduwuit_"); + let is_core = cargo_crate_name == "conduwuit_core"; + + let flags = std::env::args().collect::>(); + + let mut enabled_features = Vec::new(); + append_features(&mut enabled_features, flags); + + let enabled_count = enabled_features.len(); + + let import_path = if is_core { + quote! { use crate::conduwuit_core; } + } else { + quote! { use ::conduwuit_core; } + }; + + let ret = quote! { + #[doc(hidden)] + mod __compile_introspection { + #import_path + + /// Features that were enabled when this crate was compiled + const ENABLED: [&str; #enabled_count] = [#( #enabled_features ),*]; + + const CRATE_NAME: &str = #crate_name; + + /// Register this crate's features with the global registry during static initialization + #[::ctor::ctor] + fn register() { + conduwuit_core::info::introspection::ENABLED_FEATURES.lock().unwrap().insert(#crate_name, &ENABLED); + } + #[::ctor::dtor] + fn unregister() { + conduwuit_core::info::introspection::ENABLED_FEATURES.lock().unwrap().remove(#crate_name); + } + } + }; + + Ok(ret) +} + +fn append_features(features: &mut Vec, flags: Vec) { + let mut next_is_cfg = false; + for flag in flags { + let is_cfg = flag == "--cfg"; + let is_feature = flag.starts_with("feature="); + if std::mem::replace(&mut next_is_cfg, is_cfg) && is_feature { + if let Some(feature) = flag + .split_once('=') + .map(|(_, feature)| feature.trim_matches('"')) + { + features.push(feature.to_owned()); + } + } + } +} diff --git a/src/macros/mod.rs b/src/macros/mod.rs index 5488af1e..90618827 100644 --- a/src/macros/mod.rs +++ b/src/macros/mod.rs @@ -1,4 +1,5 @@ mod admin; +mod build_info; mod config; mod debug; mod implement; @@ -44,6 +45,13 @@ pub fn config_example_generator(args: TokenStream, input: TokenStream) -> TokenS attribute_macro::(args, input, config::example_generator) } +#[proc_macro] +pub fn introspect_crate(input: TokenStream) -> TokenStream { + build_info::introspect(input.into()) + .unwrap_or_else(|e| e.to_compile_error()) + .into() +} + fn attribute_macro(args: TokenStream, input: TokenStream, func: F) -> TokenStream where F: Fn(I, &[Meta]) -> Result, diff --git a/src/main/Cargo.toml b/src/main/Cargo.toml index 6c073c85..b4a77030 100644 --- a/src/main/Cargo.toml +++ b/src/main/Cargo.toml @@ -202,8 +202,10 @@ conduwuit-database.workspace = true conduwuit-router.workspace = true conduwuit-service.workspace = true conduwuit-build-metadata.workspace = true +conduwuit-macros.workspace = true clap.workspace = true +ctor.workspace = true console-subscriber.optional = true console-subscriber.workspace = true const-str.workspace = true diff --git a/src/main/mod.rs b/src/main/mod.rs index d3108e53..016fe0c8 100644 --- a/src/main/mod.rs +++ b/src/main/mod.rs @@ -4,6 +4,8 @@ use std::sync::{Arc, atomic::Ordering}; use conduwuit_core::{debug_info, error}; +conduwuit_macros::introspect_crate! {} + mod clap; mod logging; mod mods; diff --git a/src/router/Cargo.toml b/src/router/Cargo.toml index f25eba57..142ce8e4 100644 --- a/src/router/Cargo.toml +++ b/src/router/Cargo.toml @@ -2,6 +2,7 @@ name = "conduwuit_router" description.workspace = true edition.workspace = true +homepage.workspace = true license.workspace = true readme.workspace = true repository.workspace = true @@ -99,9 +100,11 @@ bytes.workspace = true conduwuit-admin.workspace = true conduwuit-api.workspace = true conduwuit-core.workspace = true +conduwuit-macros.workspace = true conduwuit-service.workspace = true conduwuit-web.workspace = true const-str.workspace = true +ctor.workspace = true futures.workspace = true http.workspace = true http-body-util.workspace = true diff --git a/src/router/mod.rs b/src/router/mod.rs index 78c22621..c6f909d3 100644 --- a/src/router/mod.rs +++ b/src/router/mod.rs @@ -8,6 +8,8 @@ mod serve; extern crate conduwuit_core as conduwuit; +conduwuit_macros::introspect_crate! {} + use std::{panic::AssertUnwindSafe, pin::Pin, sync::Arc}; use conduwuit::{Error, Result, Server}; diff --git a/src/service/Cargo.toml b/src/service/Cargo.toml index f8fc932f..cbbce0ff 100644 --- a/src/service/Cargo.toml +++ b/src/service/Cargo.toml @@ -2,6 +2,7 @@ name = "conduwuit_service" description.workspace = true edition.workspace = true +homepage.workspace = true license.workspace = true readme.workspace = true repository.workspace = true @@ -84,7 +85,9 @@ base64.workspace = true bytes.workspace = true conduwuit-core.workspace = true conduwuit-database.workspace = true +conduwuit-macros.workspace = true const-str.workspace = true +ctor.workspace = true either.workspace = true futures.workspace = true hickory-resolver.workspace = true diff --git a/src/service/mod.rs b/src/service/mod.rs index b0222c3e..5389f71a 100644 --- a/src/service/mod.rs +++ b/src/service/mod.rs @@ -3,6 +3,9 @@ extern crate conduwuit_core as conduwuit; extern crate conduwuit_database as database; + +conduwuit_macros::introspect_crate! {} + mod manager; mod migrations; mod service; diff --git a/src/web/Cargo.toml b/src/web/Cargo.toml index c5c6a06a..222c5826 100644 --- a/src/web/Cargo.toml +++ b/src/web/Cargo.toml @@ -2,6 +2,7 @@ name = "conduwuit_web" description.workspace = true edition.workspace = true +homepage.workspace = true license.workspace = true readme.workspace = true repository.workspace = true