Starship 贡献者开发指南:架构入口、模块编写、测试模拟与新模块清单
本文基于 Starship 仓库的 CONTRIBUTING.md 展开,系统梳理该项目面向贡献者的完整开发工作流:从项目术语、架构入口(clap 命令分发与 rayon 并行模块计算),到模块开发中环境变量读取、外部命令执行、绝对路径处理的三大约定,再到测试体系(ModuleRenderer、命令 mock 匹配机制、测试编写准则)、日志与 Lint/格式化流程,以及新增一个模块必须完成的完整检查清单。读完本文,你可以独立地在 Starship 代码库中定位功能入口、规范地编写和测试一个新模块,并通过项目的 CI 要求(clippy、rustfmt、dprint、配置 schema 更新)。
1. 术语与贡献理念
Starship 的文档和源码中反复使用两个核心概念,理解它们是阅读后续所有开发约定(尤其是测试约定)的前提:
- Module(模块):提示符中一个基于操作系统上下文信息给出信息的组件。例如
rust模块会在当前目录是一个 Rust 项目时,显示当前安装的 Rust 版本。 - Segment(片段):组成模块的更小子组件。例如
rust模块中的symbol片段就是版本号前显示的那个字符(默认是🦀)。
项目的贡献理念(Philosophy)在 CONTRIBUTING.md 中明确写道:让 Starship 尽可能快速、健壮、可靠,同时保留高度可定制性。其手段包括:
- 利用 Rust 语言内建的安全特性;
- 彻底的跨平台测试;
- 在显示提示符时消除不必要的开销——通过减少重复计算、善用缓存。
文档特别指出:如果你发现任何可以进一步节省时间或降低提示符渲染负载的地方,项目非常欢迎相应的 issue 或 PR。这直接决定了后文的性能取向约定(例如外部命令默认超时、路径扫描缓存等)。
2. 架构:从 main.rs 到并行模块计算
CONTRIBUTING.md 对架构的官方描述是:
项目从
main.rs开始,根据通过 clap 传入的参数,调用相应的print::方法;打印完整提示符时,使用 rayon 对模块的计算进行并行化。任何应用到模块上的样式都会被其片段继承;模块的前缀/后缀默认不应用任何样式。
对照当前仓库源码,这条架构描述可以得到逐点印证:
- 入口 src/main.rs 中定义了
Cli结构体(基于clap的Parser派生宏),subcommand_required = true意味着每次调用必须给出子命令。Commands枚举(src/main.rs#L66-L166)列出了全部子命令:prompt、module、init、preset、print-config、toggle、bug-report、timings、explain等。 main()函数(src/main.rs#L168-L316)完成参数解析后,按子命令分派到print::prompt、print::module、configure::update_configuration等函数——这正是文档所说"根据参数调用相应的print::方法"。- 并行化在启动阶段就已建立:
main()一开始就调用init_global_threadpool()(src/main.rs#L319-L324),通过rayon::ThreadPoolBuilder::new().num_threads(num_rayon_threads()).build_global()初始化全局线程池。Cargo.toml中可见rayon = "1.12.0"与clap(4.x,含derive特性)均为直接依赖(Cargo.toml#L37-L40、Cargo.toml#L72)。
代码库的整体分层也可以从目录结构直接看出:
- src/configs/:每个模块的配置结构体(struct/Default 实现),如 src/configs/rust.rs、src/configs/starship_root.rs(全局
PROMPT_ORDER定义处); - src/modules/:每个模块的渲染实现,
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>>是统一签名; - src/context/:
Context类型,是模块函数接收的上下文对象,封装了环境、路径、配置与命令执行; - src/formatter/:格式串解析器(pest 语法)与
StringFormatter,负责把{{ }}模板渲染成带样式的输出; - src/test/:测试基础设施,核心是
ModuleRenderer。
3. 模块开发三大约定:环境、命令、绝对路径
CONTRIBUTING.md 为模块实现规定了三个必须使用的自定义函数/机制,目的都是"让模块在测试环境中可被 mock"。下面逐一说明其用法,并结合源码解释底层实现。
3.1 读取环境变量:context.get_env
use super::{Context, Module, RootModuleConfig};
use crate::configs::php::PhpConfig;
use crate::formatter::StringFormatter;
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
// 这里 `my_env_var` 要么是变量的值,
// 如果变量未设置则函数(通过 `?`)直接返回。
let my_env_var = context.get_env("MY_VAR")?;
// 然后就可以放心地使用这个值
}
实现上,Context::get_env 只是一个薄封装(src/context/mod.rs#L226-L230):
// Retrieves a environment variable from the os or from a table if in testing mode
#[inline]
pub fn get_env<K: AsRef<str>>(&self, key: K) -> Option<String> {
self.env.get_env(key)
}
注意注释里"from the os or from a table if in testing mode"——测试模式下环境变量从一张表(即 ModuleRenderer::env 注入的 mock 表)中读取,生产模式下才真正读操作系统。这就是文档示例中"未设置就通过 ? 退出"的语义在两种模式下都成立的原因。
3.2 执行外部命令:context.exec_cmd
use super::{Context, Module, ModuleConfig};
use crate::configs::php::PhpConfig;
use crate::formatter::StringFormatter;
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
// 这里 `output` 要么是所调用命令的 stdout,
// 要么(当程序未安装或无法运行时)函数直接退出。
let output = context.exec_cmd("my_command", &["first_arg", "second_arg"])?.stdout;
// 然后就可以放心地使用 output
}
文档同时给出了一条硬性规定:如果无法使用 context.exec_cmd,请使用 crate::utils::create_command 代替 std::process::Command::new。
从源码看,这条规定的理由很具体(src/utils/mod.rs#L165-L190):
/// Attempt to resolve `binary_name` from and creates a new `Command` pointing at it
/// This allows executing cmd files on Windows and prevents running executable from cwd on Windows
/// This function also initializes std{err,out,in} to protect against processes changing the console mode
pub fn create_command<T: AsRef<OsStr>>(binary_name: T) -> Result<Command> {
即 create_command 会先通过 which 解析二进制完整路径(允许在 Windows 上执行 .cmd 文件、防止从当前目录误执行可执行文件),并显式初始化 stdin/stdout/stderr 管道(stdin 置空,防止子进程篡改控制台模式)。
Context::exec_cmd(src/context/mod.rs#L463-L489)在其内部还叠加了两层测试/性能逻辑:
- 测试 mock 优先:
#[cfg(test)]分支下,先查self.cmd表(即ModuleRenderer::cmd注入的 per-test mock),再回退到全局crate::utils::mock_cmd; - 超时控制:非测试路径下用
exec_timeout执行,超时时间来自配置项command_timeout(默认值见 src/utils/mod.rs#L17-L18 中的DEFAULT_COMMAND_TIMEOUT_MS: u64 = 500)。命令超时时会记录警告并建议用户调高command_timeout(src/utils/mod.rs#L751-L757)——这正是贡献理念中"减少提示符渲染负载"的具体体现。
另一个细节:CommandOutput 同时保留 stdout 与 stderr 两个字段(src/utils/mod.rs#L192-L196),而 get_command_string_output 会在 stdout 为空时回退到 stderr(src/utils/mod.rs#L156-L163),因此写模块时不必假定目标程序把版本信息打到 stdout。
3.3 绝对文件名:crate::utils::context_path
在模块中处理绝对路径时,文档要求使用 crate::utils::context_path() 来由绝对路径名构造 PathBuf:在测试环境下,根目录会被替换为一个 Tempdir,可通过 ModuleRenderer::root_path() 获取,你可以在这个 mock 根目录中放入任意文件。
use crate::utils::context_path;
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
if !context_path(context, "/run/test/testfile").exists() {
return None
}
// ..
}
测试侧对应的写法:
#[test]
fn test_testfile() {
let renderer = ModuleRenderer::new("mymodule");
let root_path = renderer.root_path();
// 这会在 $TEMPDIR 下创建 run/test/testfile
let mut absolute_test_file = PathBuf::from(root_path);
absolute_test_file.push("run");
absolute_test_file.push("test");
std::fs::DirBuilder::new()
.recursive(true)
.create(&absolute_test_file)?;
absolute_test_file.push("testfile");
std::fs::File::create(&absolute_test_file)?;
// ...
}
该机制的底层实现是一个编译期切换(src/utils/mod.rs#L20-L41):非测试构建中 context_path 是 #[inline] 的空操作(直接 PathBuf::from);#[cfg(test)] 版本则把绝对路径的首个组件替换为 context.root_dir.path()(Tempdir)。root_path() 即 self.context.root_dir.path()(src/test/mod.rs#L109-L111)。这套机制使得"读取 /proc/...、/run/... 等系统绝对路径"的模块也能在任何机器上被确定性测试。
4. 测试体系:ModuleRenderer、命令 Mock 与编写准则
文档强调"测试对于确保 starship 在大小各种系统上按预期工作至关重要":Starship 在生成提示符时会与大量应用和系统 API 交互,bug 容易混入。单元测试使用 Rust 内建测试框架,与被测实现写在同一个文件里,通过 cargo test 运行,并作为 CI 的一部分在各平台上运行。
4.1 渲染输出测试一律使用 ModuleRenderer
文档给出的标准测试骨架(完整继承自 CONTRIBUTING.md):
use super::{Context, Module, ModuleConfig};
use crate::configs::php::PhpConfig;
use crate::formatter::StringFormatter;
use crate::utils;
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
/* 你的模块代码写在这里 */
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test::ModuleRenderer;
use nu_ansi_term::Color;
use std::fs::File;
use std::io;
#[test]
fn should_render() -> io::Result<()> {
// 搭建测试环境
let tempdir = tempfile::tempdir()?;
// 创建模块渲染所需的文件
File::create(tempdir.path().join("YOUR_FILE"))?.sync_all()?;
// 模块的输出
let actual = ModuleRenderer::new("YOUR_MODULE_NAME")
// 自定义路径
.path(&tempdir.path())
// 自定义配置
.config(toml::toml!{
[YOUR_MODULE_NAME]
val = 1
})
// 环境变量 mock
.env("KEY","VALUE")
// 运行模块并收集输出
.collect();
// 模块应当渲染出的值
let expected = Some(format!("{} ",Color::Black.paint("THIS SHOULD BE RENDERED")));
// 断言实际值与期望值相同
assert_eq!(actual, expected);
// 关闭 tempdir
tempdir.close()
}
}
从 src/test/mod.rs 可以看到该构建器实际提供的能力,比示例更多:
path():设置context.current_dir与logical_dir(src/test/mod.rs#L98-L107);config(toml::toml!{...}):注入 TOML 配置(src/test/mod.rs#L132-L135);env(key, val):写入环境 mock 表(src/test/mod.rs#L138-L141);cmd(key, Option<CommandOutput>):为特定命令字符串注入 mock 输出(src/test/mod.rs#L143-L147);new_with_home():把HOME设置为一个TempDir,用于依赖主目录的模块(src/test/mod.rs#L90-L96);- 此外还有
jj_repo()(模拟 jj 仓库状态)、shell()(模拟 shell 类型)等,覆盖不同版本控制系统与终端场景。
4.2 外部命令输出必须注册到 mock 表
文档规定:如果一个模块依赖某个外部程序的输出,那么该输出必须添加到 src/utils/mod.rs 中的 match 语句里。match 的键必须与 utils::exec_cmd() 的调用完全一致,包括位置参数与 flag——参数数组用 " " 连接,因此 utils::exec_cmd("program", &["arg", "more_args"]) 对应的 match 是 program arg more_args。
源码印证(src/utils/mod.rs#L204-L228):display_command 正是把命令与参数以空格 join 成字符串;exec_cmd 在 #[cfg(test)] 下先查 mock_cmd(&cmd, args),命中则直接返回,否则才走真实执行 internal_exec_cmd。当前 mock_cmd 的 match 表(src/utils/mod.rs#L231-L647)覆盖了绝大多数模块依赖的探测命令,例如 "go version"、"ruby -v"、"dotnet --list-sdks"、"python3 --version"、"pijul channel" 等,每条返回固定的 CommandOutput { stdout, stderr }。因此给新模块添加 mock 时,你需要:
- 确定模块中
exec_cmd调用的准确命令串; - 在
mock_cmd的 match 中新增一条完全一致的字面量(或前缀/后缀模式,表中已有s if s.ends_with(...)这类写法); - 填入与真实程序输出格式一致的 stdout/stderr 样例。
对于无法 mock 的程序(例如它会读写文件),文档规定:把它加入项目的 CI workflow 文件(.github/workflows/workflow.yml)预装/预配置,并在测试上标注 #[ignored](按文档原文表述)。这样任何人都能在本地不预先配置环境就运行测试套件;该属性在 CI 运行中被绕过。
4.3 测试编写准则(Test Programming Guidelines)
- 完全隔离、可复现:单元测试只针对"给定特定输入时某函数的期望输出",必须在任何机器上可复现,不得假设运行机器处于任何特定状态——包括预装了某些应用、设置了某些环境变量等。
- 警惕"看起来无害"的假设:文档特别强调,像"能看到目录就能读目录"或"没人会把主目录做成 git 仓库"这类想法都曾实际坑过项目。哪怕只有一个测试失败,就可能在某些平台上彻底破坏安装流程,务必小心。
- 文件 I/O 必须
sync_all():任何涉及文件创建或写入的测试,都要调用sync_all()(可对照 src/test/mod.rs#L29-L43 中为测试 git 显式设置core.fsync = all的做法,注释明确说这是为了让 Windows 上的 I/O 竞争测试结果稳定)。 tempfile::tempdir用完必须dir.close():以便目录生命周期可被推理。这同样适用于fixture_repo()——它返回的TempDir也需要关闭。
5. 日志:自定义 logger 与 STARSHIP_LOG
Starship 的调试日志使用自定义 logger 实现(StarshipLogger,见 src/logger.rs)。开启调试日志的方法是设置 STARSHIP_LOG 环境变量为所需日志级别:
# 运行已安装的 starship
STARSHIP_LOG=trace starship
# 用 cargo 运行
STARSHIP_LOG=trace cargo run
从源码补充两点实现细节:日志目录由 get_log_dir() 决定,优先取 STARSHIP_CACHE,否则为 $HOME/.cache/starship(回退到系统缓存目录/临时目录)(src/logger.rs#L22-L34);main() 启动时会用 rayon::spawn 异步清理超过 24 小时的旧日志文件(src/main.rs#L175-L179、src/logger.rs#L36-L80)。因此排查"某模块为何没显示"时,STARSHIP_LOG=trace 加上日志文件是最直接的诊断手段。
6. Lint 与格式化:CI 的硬性门槛
文档明确:未通过 lint/format 的代码会直接导致 CI 失败,建议本地先跑:
Clippy(Rust lint)
rustup component add clippy
cargo clippy --all-targets --all-features
rustfmt(Rust 格式化)与 dprint(Markdown、TOML 等文件格式化)
rustup component add rustfmt
cargo fmt
cargo install dprint
dprint fmt
文档还建议利用编辑器插件自动运行这些工具,避免提交导致 CI 失败的 PR。
配置 schema 更新:如果你的改动影响了配置项,必须重新生成配置 schema 文件 .github/config-schema.json:
cargo run --features config-schema -- config-schema > .github/config-schema.json
对应源码依据:config-schema 是一个 cargo feature(启用 schemars 依赖,见 Cargo.toml#L31-L34),且 Commands::ConfigSchema 子命令仅在 #[cfg(feature = "config-schema")] 下存在(src/main.rs#L163-L165、src/main.rs#L313-L314);Cargo.toml 的 include 列表也把 .github/config-schema.json 列为发布内容(Cargo.toml#L11-L20)。当前仓库版本为 1.26.0,MSRV 提示为 Rust 1.95(Cargo.toml#L3、Cargo.toml#L25-L26),本地构建请使用较新的 Rust 工具链。
7. 文档工作流:Crowdin 翻译页与本地 VitePress
7.1 翻译页面由 Crowdin 托管,禁止直接编辑
大量文档页面存在非英语版本,这些翻译页面由 Crowdin 托管生成。文档明确要求:不要直接编辑这些页面——即使是无需翻译的改动(如空格、emoji)也不要,因为这可能导致合并失败。如果要贡献翻译或更正,应到项目的 Crowdin 站点进行。仓库中可以看到这一结构的直接体现:docs/ 下按语言代码(de-DE/、ja-JP/、zh-CN/ 等)镜像了完整的文档子树,而唯一需要手工维护的是英文源目录 docs/ 根下的 config/、guide/、presets/ 等。
7.2 本地运行文档网站
PR 页面底部 CI 区域提供 "deploy preview" 可预览渲染结果;如想本地查看,文档给出基于 VitePress 的步骤(docs/package.json 定义了对应脚本):
-
cd进入/docs目录; -
安装依赖:
npm install -
以开发模式启动:
npm run dev
文档同时提示,VitePress 的具体用法可参考其官方入门指南(此处不附外部链接)。
8. Git/GitHub 协作流程
项目推荐的标准 PR 流程(完整继承自 CONTRIBUTING.md):
- Fork 仓库;
- 从
main切出工作分支:git checkout -b my-feature-branch; - 进行改动,沿途提交;
- 改动就绪后推送分支:
git push origin my-feature-branch; - 创建从你的分支到
starship/main的 pull request; - 无需把 PR 指派给任何人,维护者有空时评审;
- 评审通过后,由维护者替你 squash and merge。
另外,文档开头约定:本项目以 贡献者行为准则 发布,参与即表示同意遵守;本文档未覆盖的问题,欢迎开 issue 或到项目 Discord 提问。
9. 新模块检查清单(New Module Checklist)
项目对新模块持开放态度,且刻意保持低门槛,但 Starship 为模块提供了大量功能,因此新增模块需要完成若干步骤。以下是文档给出的完整清单,建议逐项核对:
- [ ] 在 docs/config/README.md 中为该模块添加说明章节,描述模块及其配置选项/变量(文档注明:更多文档通常更合适——这只是最低要求)。
- [ ] 在文档的 "Default Prompt Format" 章节中,把该模块变量加到合适位置。
- [ ] 为 docs/public/presets/ 目录下的每个 preset 加入合适的选项(仓库中 12 个 preset
.toml文件即位于该目录)。 - [ ] 重新生成配置 schema:
cargo run --features config-schema -- config-schema > .github/config-schema.json。 - [ ] 在
src/configs/<module>.rs中创建配置的 struct/trait,并补上:- [ ] 在
PROMPT_ORDER中添加条目(src/configs/starship_root.rs); - [ ] 在
FullConfig及其Default实现中添加条目(src/configs/mod.rs); - [ ] 在
ALL_MODULES中添加条目(src/module.rs); - [ ] 在 src/modules/mod.rs 顶部添加
mod声明; - [ ] 在 src/modules/mod.rs 的
handle()中添加条目; - [ ] 为
description()函数补充描述(src/modules/mod.rs)。
- [ ] 在
最后,把模块实现代码写入 src/modules/,并把测试中需要 mock 的命令加入 src/utils/mod.rs;命令输出也可以在单个测试中用 ModuleRenderer::cmd 直接 mock。
小结:贡献 Starship 的核心纪律
把 CONTRIBUTING.md 的所有约定归纳起来,就是四条可执行纪律:
- 一切经由 Context:读环境用
context.get_env,跑外部命令用context.exec_cmd(兜底utils::create_command),构造绝对路径用utils::context_path——这三者共同保证了测试可 mock、根目录可替换、执行有超时保护(默认 500ms); - 测试即契约:所有渲染测试走
ModuleRenderer,外部命令输出必须逐字注册进mock_cmd的 match 表,文件写入必须sync_all(),TempDir 必须显式close(),测试不得假设运行环境的任何状态; - CI 零容忍:clippy、rustfmt、dprint 三项本地先跑,配置变更必须同步重新生成
.github/config-schema.json; - 清单不遗漏:新模块按第 9 节的 6+ 项注册点逐一登记(
PROMPT_ORDER、FullConfig、ALL_MODULES、mod/handle()/description()),并补齐英文文档与 preset 配置。
按这套流程,你的模块既能复用 Starship 的样式继承、并行渲染与路径扫描等基础设施,也能保证在任意平台的 CI 上稳定通过。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00