Cargo feature 管理:AI 工具不要默认打开所有能力

Cargo feature 管理:AI 工具不要默认打开所有能力
Cargo feature 管理AI 工具不要默认打开所有能力刚开始学 Rust 的时候我对 Cargo.toml 里的[features]段几乎没什么概念。需要什么依赖就往[dependencies]里加能用就行。等我的 AI CLI 工具长了几个月依赖表拉到四十多行——wasmtime、tokio、reqwest、tracing、clap、各种编解码库——每次编译都够我喝完一整杯咖啡。更糟糕的是有些用户只想要一个简单的命令行工具根本不需要 WASM 插件系统。但他们也不得不把整个 wasmtime 编译一遍下载几百兆的依赖等好几分钟。那一刻我才理解 Cargo feature 的意义不是让你创建一百种编译组合而是让不同用户拿到刚好够用的二进制。在自学的过程中我以前的思路是一个二进制解决所有问题。但 feature 系统让我学会了克制——默认只打开最需要的能力其他功能让用户按需开启。一、先把能力拆开画出边界在动手写 feature 之前我习惯先把项目的组件能力画成一张图flowchart TD A[核心 CLI Core CLI] -- B{Feature 开关 Features} B --|provider-http| C[远程 HTTP 调用 Remote Provider] B --|provider-local| D[本地模型推理 Local Inference] B --|wasm-plugin| E[WASM 插件系统 Plugin System] B --|tracing| F[日志追踪 Tracing Log] B --|rich-cli| G[彩色输出与进度条 Rich Output] C --|依赖 reqwest| C1[reqwest / hyper] E --|依赖 wasmtime| E1[wasmtime] F --|依赖 tracing-subscriber| F1[tracing-subscriber] G --|依赖 indicatif/owo-colors| G1[indicatif] style A fill:#bbf,stroke:#333,stroke-width:2px style C1 fill:#ddd,stroke:#999 style E1 fill:#ddd,stroke:#999 style F1 fill:#ddd,stroke:#999 style G1 fill:#ddd,stroke:#999核心 CLI 本身应该轻到几乎只有参数解析、配置加载和任务编排。所有重量级能力都通过 feature 开关控制。这样普通用户编译时只拿到一个轻量的二进制高级用户打开对应 feature 就有了完整能力。二、Cargo.toml 的 feature 配置下面是一个实际的 feature 配置示例。关键原则是默认功能要克制Optional dependency 要跟 feature 绑定互斥能力要在编译期给清楚错误[package] name ai-cli version 0.5.0 edition 2021 [features] # 默认只打开 HTTP provider这是最常用的能力 default [provider-http] # 各个 feature 的定义和关联依赖 provider-http [dep:reqwest, dep:serde_json] provider-local [dep:candle-core, dep:tokenizers] wasm-plugin [dep:wasmtime] tracing [dep:tracing, dep:tracing-subscriber] rich-cli [dep:indicatif, dep:owo-colors] [dependencies] # 核心依赖总是编译 clap { version 4, features [derive] } serde { version 1, features [derive] } tokio { version 1, features [full] } # 可选依赖由 feature 控制是否参与编译 reqwest { version 0.12, features [json], optional true } serde_json { version 1, optional true } candle-core { version 0.6, optional true } tokenizers { version 0.19, optional true } wasmtime { version 20, optional true } tracing { version 0.1, optional true } tracing-subscriber { version 0.3, optional true } indicatif { version 0.17, optional true } owo-colors { version 4, optional true }需要注意两个细节一是用dep:xxx语法来在 feature 中引用 optional dependency这是 Rust 1.60 以后的标准写法二是不要把 feature 名字起得像技术债——provider-http比use-reqwest-backend更好因为未来你可能换掉 reqwest 但 feature 名不用变。三、代码里用 cfg 条件编译控制模块有了 feature 定义代码里就可以用#[cfg(feature ...)]来控制哪些代码参与编译// 将插件系统整个模块挂到 feature 开关下 #[cfg(feature wasm-plugin)] pub mod plugin; // 初始化日志系统仅在 tracing feature 开启时可用 #[cfg(feature tracing)] pub fn init_tracing() { tracing_subscriber::fmt() .with_env_filter(ai_clidebug) .init(); } // 无 tracing feature 时提供一个空实现避免编译错误 #[cfg(not(feature tracing))] pub fn init_tracing() { // 不做任何事 } /// 根据编译时 feature 选择 Provider 实现 pub fn create_default_provider(config: Config) - Boxdyn AiClient { // 注意这两个 feature 是互斥的Cargo 会保证只有一个开启 #[cfg(feature provider-http)] { Box::new(HttpAiClient::new(config)) } #[cfg(feature provider-local)] { Box::new(LocalAiClient::new(config)) } }如果两个 feature 互斥比如不能同时开启 HTTP 和 Local provider可以在代码里加编译期检查#[cfg(all(feature provider-http, feature provider-local))] compile_error!(provider-http 和 provider-local 不能同时开启请只选择其中一个);这样用户在编译时就会看到清晰的错误信息而不是一百行不知所云的类型推导失败。四、CI 要测关键 feature 组合feature 一多很容易出现某个冷门组合编译不过的情况。我给自己定的 CI 最低检查清单是# 无默认功能 — 验证核心 CLI 的纯粹性 cargo check --no-default-features # 默认组合 — 大多数用户的使用方式 cargo check # 全部功能如果有互斥则选最大可用集 cargo check --features provider-http,wasm-plugin,tracing,rich-cli # 本地模型用户组合 cargo check --no-default-features --features provider-local,rich-cli # 测试 cargo test --no-default-features cargo test安装文档里也要写清楚不同用户应该用哪个命令# 普通用户默认安装只需 HTTP provider cargo install ai-cli # 插件用户额外开启 WASM 支持 cargo install ai-cli --features wasm-plugin # 极简用户只要核心功能连 HTTP 都不用 cargo install ai-cli --no-default-features让用户在安装时就选择能力比给他们一个 200MB 的二进制然后说很多功能你不用可以忽略要好得多。给个真实数字我的 CLI 工具默认不开 wasm-plugin 时编译时间大约 15 秒开启后要 90 秒二进制体积也从 4MB 涨到 28MB。对只想问个问题的用户来说多耗的这 75 秒和 24MB 就是不必要的代价。Feature 开关帮我们省下的不是代码行数是用户的时间。还有一点容易被忽略feature 之间如果有间接依赖--all-features可能会引入你根本没想要的 crate。比如只开了provider-http但wasm-plugin的一个可选依赖悄悄拉进了wasmtime编译时间翻倍。建议定期跑cargo tree --edges features审计看看哪些 feature 在污染依赖树。五、总结Cargo feature 管理的核心不是把功能拆得越细越好而是默认能力要克制做到用户需要时才开启。核心 CLI 保持极简重量级功能做成 optional feature互斥能力在编译期报错CI 覆盖关键组合。作为自学者我以前喜欢一个二进制打天下的感觉。但 feature 让我学会了另一种思路做减法也是一种能力。少一点默认依赖编译就快一点二进制就小一点供应链风险也少一点。给用户刚好够用的工具比塞给用户一个万能瑞士军刀更负责任。