Compare commits

...
Sign in to create a new pull request.

1 commit

Author SHA1 Message Date
Jade Ellis
3ffe955fcc
feat: Add ability to inspect build information and features at runtime
Also re-adds ability to inspect used features
2026-01-20 21:01:21 +00:00
26 changed files with 317 additions and 4 deletions

13
Cargo.lock generated
View file

@ -989,10 +989,12 @@ dependencies = [
"conduwuit_build_metadata", "conduwuit_build_metadata",
"conduwuit_core", "conduwuit_core",
"conduwuit_database", "conduwuit_database",
"conduwuit_macros",
"conduwuit_router", "conduwuit_router",
"conduwuit_service", "conduwuit_service",
"console-subscriber", "console-subscriber",
"const-str", "const-str",
"ctor",
"hardened_malloc-rs", "hardened_malloc-rs",
"log", "log",
"opentelemetry", "opentelemetry",
@ -1021,6 +1023,7 @@ dependencies = [
"conduwuit_macros", "conduwuit_macros",
"conduwuit_service", "conduwuit_service",
"const-str", "const-str",
"ctor",
"futures", "futures",
"log", "log",
"ruma", "ruma",
@ -1042,8 +1045,10 @@ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"bytes", "bytes",
"conduwuit_core", "conduwuit_core",
"conduwuit_macros",
"conduwuit_service", "conduwuit_service",
"const-str", "const-str",
"ctor",
"futures", "futures",
"hmac", "hmac",
"http", "http",
@ -1068,6 +1073,7 @@ name = "conduwuit_build_metadata"
version = "0.5.3" version = "0.5.3"
dependencies = [ dependencies = [
"built", "built",
"cargo_metadata",
] ]
[[package]] [[package]]
@ -1137,7 +1143,9 @@ version = "0.5.3"
dependencies = [ dependencies = [
"async-channel", "async-channel",
"conduwuit_core", "conduwuit_core",
"conduwuit_macros",
"const-str", "const-str",
"ctor",
"futures", "futures",
"log", "log",
"minicbor", "minicbor",
@ -1153,6 +1161,7 @@ dependencies = [
name = "conduwuit_macros" name = "conduwuit_macros"
version = "0.5.3" version = "0.5.3"
dependencies = [ dependencies = [
"cargo_toml",
"itertools 0.14.0", "itertools 0.14.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1171,9 +1180,11 @@ dependencies = [
"conduwuit_admin", "conduwuit_admin",
"conduwuit_api", "conduwuit_api",
"conduwuit_core", "conduwuit_core",
"conduwuit_macros",
"conduwuit_service", "conduwuit_service",
"conduwuit_web", "conduwuit_web",
"const-str", "const-str",
"ctor",
"futures", "futures",
"http", "http",
"http-body-util", "http-body-util",
@ -1203,7 +1214,9 @@ dependencies = [
"bytes", "bytes",
"conduwuit_core", "conduwuit_core",
"conduwuit_database", "conduwuit_database",
"conduwuit_macros",
"const-str", "const-str",
"ctor",
"either", "either",
"futures", "futures",
"hickory-resolver", "hickory-resolver",

View file

@ -2,6 +2,7 @@
name = "conduwuit_admin" name = "conduwuit_admin"
description.workspace = true description.workspace = true
edition.workspace = true edition.workspace = true
homepage.workspace = true
license.workspace = true license.workspace = true
readme.workspace = true readme.workspace = true
repository.workspace = true repository.workspace = true
@ -79,6 +80,7 @@ conduwuit-database.workspace = true
conduwuit-macros.workspace = true conduwuit-macros.workspace = true
conduwuit-service.workspace = true conduwuit-service.workspace = true
const-str.workspace = true const-str.workspace = true
ctor.workspace = true
futures.workspace = true futures.workspace = true
log.workspace = true log.workspace = true
ruma.workspace = true ruma.workspace = true

View file

@ -3,6 +3,8 @@
#![allow(clippy::enum_glob_use)] #![allow(clippy::enum_glob_use)]
#![allow(clippy::too_many_arguments)] #![allow(clippy::too_many_arguments)]
conduwuit_macros::introspect_crate! {}
pub(crate) mod admin; pub(crate) mod admin;
pub(crate) mod context; pub(crate) mod context;
pub(crate) mod processor; pub(crate) mod processor;

View file

@ -1,4 +1,4 @@
use std::{path::PathBuf, sync::Arc}; use std::{fmt::Write, path::PathBuf, sync::Arc};
use conduwuit::{ use conduwuit::{
Err, Result, Err, Result,
@ -153,3 +153,97 @@ pub(super) async fn shutdown(&self) -> Result {
self.write_str("Shutting down server...").await 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::<Vec<_>>();
enabled_features.sort_unstable();
enabled_features.dedup();
let mut available_features = conduwuit::build_metadata::WORKSPACE_FEATURES
.iter()
.flat_map(|(_, f)| f.iter())
.collect::<Vec<_>>();
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
}

View file

@ -52,4 +52,10 @@ pub enum ServerCommand {
/// Shutdown the server /// Shutdown the server
Shutdown, Shutdown,
/// List features built into the server
ListFeatures {},
/// Build information
BuildInfo {},
} }

View file

@ -2,6 +2,7 @@
name = "conduwuit_api" name = "conduwuit_api"
description.workspace = true description.workspace = true
edition.workspace = true edition.workspace = true
homepage.workspace = true
license.workspace = true license.workspace = true
readme.workspace = true readme.workspace = true
repository.workspace = true repository.workspace = true
@ -72,8 +73,10 @@ axum.workspace = true
base64.workspace = true base64.workspace = true
bytes.workspace = true bytes.workspace = true
conduwuit-core.workspace = true conduwuit-core.workspace = true
conduwuit-macros.workspace = true
conduwuit-service.workspace = true conduwuit-service.workspace = true
const-str.workspace = true const-str.workspace = true
ctor.workspace = true
futures.workspace = true futures.workspace = true
hmac.workspace = true hmac.workspace = true
http.workspace = true http.workspace = true

View file

@ -3,6 +3,9 @@
extern crate conduwuit_core as conduwuit; extern crate conduwuit_core as conduwuit;
extern crate conduwuit_service as service; extern crate conduwuit_service as service;
conduwuit_macros::introspect_crate! {}
pub mod client; pub mod client;
pub mod router; pub mod router;
pub mod server; pub mod server;

View file

@ -2,6 +2,7 @@
name = "conduwuit_build_metadata" name = "conduwuit_build_metadata"
description.workspace = true description.workspace = true
edition.workspace = true edition.workspace = true
homepage.workspace = true
license.workspace = true license.workspace = true
readme.workspace = true readme.workspace = true
repository.workspace = true repository.workspace = true
@ -27,6 +28,6 @@ crate-type = [
[build-dependencies] [build-dependencies]
built = { version = "0.8", features = [] } built = { version = "0.8", features = [] }
cargo_metadata = { version = "0.23.1" }
[lints] [lints]
workspace = true workspace = true

View file

@ -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<String> { fn run_git_command(args: &[&str]) -> Option<String> {
Command::new("git") Command::new("git")
.args(args) .args(args)
@ -11,12 +15,60 @@ fn run_git_command(args: &[&str]) -> Option<String> {
.filter(|s| !s.is_empty()) .filter(|s| !s.is_empty())
} }
fn get_env(env_var: &str) -> Option<String> { fn get_env(env_var: &str) -> Option<String> {
match std::env::var(env_var) { match env::var(env_var) {
| Ok(val) if !val.is_empty() => Some(val), | Ok(val) if !val.is_empty() => Some(val),
| _ => None, | _ => None,
} }
} }
fn main() { 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::<Vec<_>>();
// Extract available features from workspace packages
let mut available_features: BTreeMap<String, Vec<String>> = BTreeMap::new();
for package in &workspace_packages {
let crate_name = package
.name
.trim_start_matches("conduwuit-")
.replace('-', "_");
let features: Vec<String> = 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 // built gets the default crate from the workspace. Not sure if this is intended
// behavior, but it's what we want. // behavior, but it's what we want.
built::write_built_file().expect("Failed to acquire build-time information"); 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_URL");
println!("cargo:rerun-if-env-changed=GIT_REMOTE_COMMIT_URL"); println!("cargo:rerun-if-env-changed=GIT_REMOTE_COMMIT_URL");
} }
fn generate_features_code(features: &BTreeMap<String, Vec<String>>) -> 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
}

View file

@ -2,6 +2,10 @@ pub mod built {
include!(concat!(env!("OUT_DIR"), "/built.rs")); 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: Option<&str> = option_env!("GIT_COMMIT_HASH");
pub static GIT_COMMIT_HASH_SHORT: Option<&str> = option_env!("GIT_COMMIT_HASH_SHORT"); pub static GIT_COMMIT_HASH_SHORT: Option<&str> = option_env!("GIT_COMMIT_HASH_SHORT");

View file

@ -2,6 +2,7 @@
name = "conduwuit_core" name = "conduwuit_core"
description.workspace = true description.workspace = true
edition.workspace = true edition.workspace = true
homepage.workspace = true
license.workspace = true license.workspace = true
readme.workspace = true readme.workspace = true
repository.workspace = true repository.workspace = true

View file

@ -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<BTreeMap<&str, &[&str]>> =
std::sync::Mutex::new(BTreeMap::new());

View file

@ -1,3 +1,4 @@
pub mod introspection;
pub mod room_version; pub mod room_version;
pub mod version; pub mod version;

View file

@ -19,6 +19,7 @@ pub use ::smallstr;
pub use ::smallvec; pub use ::smallvec;
pub use ::toml; pub use ::toml;
pub use ::tracing; pub use ::tracing;
pub use conduwuit_build_metadata as build_metadata;
pub use config::Config; pub use config::Config;
pub use error::Error; pub use error::Error;
pub use info::{ pub use info::{
@ -34,6 +35,8 @@ pub use utils::{implement, result, result::Result};
pub use crate as conduwuit_core; pub use crate as conduwuit_core;
conduwuit_macros::introspect_crate! {}
#[cfg(any(not(conduwuit_mods), not(feature = "conduwuit_mods")))] #[cfg(any(not(conduwuit_mods), not(feature = "conduwuit_mods")))]
pub mod mods { pub mod mods {
#[macro_export] #[macro_export]

View file

@ -2,6 +2,7 @@
name = "conduwuit_database" name = "conduwuit_database"
description.workspace = true description.workspace = true
edition.workspace = true edition.workspace = true
homepage.workspace = true
license.workspace = true license.workspace = true
readme.workspace = true readme.workspace = true
repository.workspace = true repository.workspace = true
@ -54,7 +55,9 @@ bindgen-runtime = [
[dependencies] [dependencies]
async-channel.workspace = true async-channel.workspace = true
conduwuit-core.workspace = true conduwuit-core.workspace = true
conduwuit-macros.workspace = true
const-str.workspace = true const-str.workspace = true
ctor.workspace = true
futures.workspace = true futures.workspace = true
log.workspace = true log.workspace = true
minicbor.workspace = true minicbor.workspace = true

View file

@ -3,6 +3,8 @@
extern crate conduwuit_core as conduwuit; extern crate conduwuit_core as conduwuit;
extern crate rust_rocksdb as rocksdb; extern crate rust_rocksdb as rocksdb;
conduwuit_macros::introspect_crate! {}
conduwuit::mod_ctor! {} conduwuit::mod_ctor! {}
conduwuit::mod_dtor! {} conduwuit::mod_dtor! {}

View file

@ -2,6 +2,7 @@
name = "conduwuit_macros" name = "conduwuit_macros"
description.workspace = true description.workspace = true
edition.workspace = true edition.workspace = true
homepage.workspace = true
license.workspace = true license.workspace = true
readme.workspace = true readme.workspace = true
repository.workspace = true repository.workspace = true
@ -17,6 +18,7 @@ syn.workspace = true
quote.workspace = true quote.workspace = true
proc-macro2.workspace = true proc-macro2.workspace = true
itertools.workspace = true itertools.workspace = true
cargo_toml.workspace = true
[lints] [lints]
workspace = true workspace = true

63
src/macros/build_info.rs Normal file
View file

@ -0,0 +1,63 @@
use proc_macro2::TokenStream;
use quote::quote;
use crate::Result;
pub(super) fn introspect(_args: TokenStream) -> Result<TokenStream> {
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::<Vec<_>>();
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<String>, flags: Vec<String>) {
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());
}
}
}
}

View file

@ -1,4 +1,5 @@
mod admin; mod admin;
mod build_info;
mod config; mod config;
mod debug; mod debug;
mod implement; mod implement;
@ -44,6 +45,13 @@ pub fn config_example_generator(args: TokenStream, input: TokenStream) -> TokenS
attribute_macro::<ItemStruct, _>(args, input, config::example_generator) attribute_macro::<ItemStruct, _>(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<I, F>(args: TokenStream, input: TokenStream, func: F) -> TokenStream fn attribute_macro<I, F>(args: TokenStream, input: TokenStream, func: F) -> TokenStream
where where
F: Fn(I, &[Meta]) -> Result<TokenStream>, F: Fn(I, &[Meta]) -> Result<TokenStream>,

View file

@ -202,8 +202,10 @@ conduwuit-database.workspace = true
conduwuit-router.workspace = true conduwuit-router.workspace = true
conduwuit-service.workspace = true conduwuit-service.workspace = true
conduwuit-build-metadata.workspace = true conduwuit-build-metadata.workspace = true
conduwuit-macros.workspace = true
clap.workspace = true clap.workspace = true
ctor.workspace = true
console-subscriber.optional = true console-subscriber.optional = true
console-subscriber.workspace = true console-subscriber.workspace = true
const-str.workspace = true const-str.workspace = true

View file

@ -4,6 +4,8 @@ use std::sync::{Arc, atomic::Ordering};
use conduwuit_core::{debug_info, error}; use conduwuit_core::{debug_info, error};
conduwuit_macros::introspect_crate! {}
mod clap; mod clap;
mod logging; mod logging;
mod mods; mod mods;

View file

@ -2,6 +2,7 @@
name = "conduwuit_router" name = "conduwuit_router"
description.workspace = true description.workspace = true
edition.workspace = true edition.workspace = true
homepage.workspace = true
license.workspace = true license.workspace = true
readme.workspace = true readme.workspace = true
repository.workspace = true repository.workspace = true
@ -99,9 +100,11 @@ bytes.workspace = true
conduwuit-admin.workspace = true conduwuit-admin.workspace = true
conduwuit-api.workspace = true conduwuit-api.workspace = true
conduwuit-core.workspace = true conduwuit-core.workspace = true
conduwuit-macros.workspace = true
conduwuit-service.workspace = true conduwuit-service.workspace = true
conduwuit-web.workspace = true conduwuit-web.workspace = true
const-str.workspace = true const-str.workspace = true
ctor.workspace = true
futures.workspace = true futures.workspace = true
http.workspace = true http.workspace = true
http-body-util.workspace = true http-body-util.workspace = true

View file

@ -8,6 +8,8 @@ mod serve;
extern crate conduwuit_core as conduwuit; extern crate conduwuit_core as conduwuit;
conduwuit_macros::introspect_crate! {}
use std::{panic::AssertUnwindSafe, pin::Pin, sync::Arc}; use std::{panic::AssertUnwindSafe, pin::Pin, sync::Arc};
use conduwuit::{Error, Result, Server}; use conduwuit::{Error, Result, Server};

View file

@ -2,6 +2,7 @@
name = "conduwuit_service" name = "conduwuit_service"
description.workspace = true description.workspace = true
edition.workspace = true edition.workspace = true
homepage.workspace = true
license.workspace = true license.workspace = true
readme.workspace = true readme.workspace = true
repository.workspace = true repository.workspace = true
@ -84,7 +85,9 @@ base64.workspace = true
bytes.workspace = true bytes.workspace = true
conduwuit-core.workspace = true conduwuit-core.workspace = true
conduwuit-database.workspace = true conduwuit-database.workspace = true
conduwuit-macros.workspace = true
const-str.workspace = true const-str.workspace = true
ctor.workspace = true
either.workspace = true either.workspace = true
futures.workspace = true futures.workspace = true
hickory-resolver.workspace = true hickory-resolver.workspace = true

View file

@ -3,6 +3,9 @@
extern crate conduwuit_core as conduwuit; extern crate conduwuit_core as conduwuit;
extern crate conduwuit_database as database; extern crate conduwuit_database as database;
conduwuit_macros::introspect_crate! {}
mod manager; mod manager;
mod migrations; mod migrations;
mod service; mod service;

View file

@ -2,6 +2,7 @@
name = "conduwuit_web" name = "conduwuit_web"
description.workspace = true description.workspace = true
edition.workspace = true edition.workspace = true
homepage.workspace = true
license.workspace = true license.workspace = true
readme.workspace = true readme.workspace = true
repository.workspace = true repository.workspace = true