Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Clue 构建器

clue 是 Riddle 当前的包管理器和项目构建器。主要命令如下:

clue init|new <path> [--bin|--lib|--workspace]
clue check|build [path] [-p <package>|--workspace] [--bin <name>] [--features a,b|--all-features] [--all-targets] [--locked]
clue run [path] [-p <package>] [--bin <name>|--example <name>] [--features a,b|--all-features] [-- <args>...]
clue test|bench [path] [-p <package>|--workspace] [--test|--bench <name>] [--features a,b|--all-features]
clue add <name> [--version <req>|--path <path>|--git <url>] [--dev]
clue remove <name> [--dev]
clue fetch|update|tree|metadata [path]
clue package [--list] [path]
clue publish [--dry-run] [--registry <name>] [path]
clue install [<package>@<version-req>] [--path <path>|--git <url>]
clue uninstall <name>
clue clean [path]

全局 --offline 只使用缓存,-j/--jobs 控制并行任务数。clue init 在指定目录中初始化项目,clue new 创建新目录和项目;二者都会生成清单、入口源码和忽略文件。它们不会覆盖已有的 Clue.toml 或目标入口源码。clue check 检查项目但不生成 C,clue build 构建项目,clue run 先构建二进制或 example 再运行。

二进制项目会按 bin 名称保留 .clue/build/<bin-name>.c 和默认的 <bin-name>.runtime.c,并在同一目录生成 <bin-name>;Windows 下扩展名为 .exe--release 使用 .clue/build/<target>/release,与 debug 缓存隔离。设置 CC 时 Clue 会严格使用它,失败时不会静默回退;未设置时,会先尝试目标组件 c-toolchain.toml 中配置的编译器(由 ridup target configure 设置),再按候选顺序探测:Linux/macOS 目标依次尝试 clangccgcc;Windows 目标依次尝试 clang-clclangccgcccl(非 Windows 宿主上的交叉目标会把 clang-cl 放到最后),最后追加带版本后缀的 GCC/Clang。候选必须能够完成一次 C11 编译和链接。库项目生成 C、目标文件、.rmeta 和默认 .rlibcrate-type 还可请求静态库和动态库。

目标平台

Clue 按 --targetRIDDLE_TARGETClue.toml[build].target、宿主平台的顺序选择目标:

[build]
target = "aarch64-unknown-linux-gnu"

当前严格限制为以下 7 个目标:

  • x86_64-unknown-linux-gnu
  • aarch64-unknown-linux-gnu
  • i686-unknown-linux-gnu
  • x86_64-pc-windows-msvc
  • i686-pc-windows-msvc
  • aarch64-pc-windows-msvc
  • aarch64-apple-darwin

交叉构建二进制项目之前需要运行 ridup target add <triple>。目标组件提供 Riddle runtime,但不等于 C 工具链已经就绪:Linux 需要 sysroot,Windows MSVC 目标需要 Windows SDK 与 MSVC 库,macOS 需要 Apple SDK。clue run 只允许运行宿主目标;交叉产物应复制到目标系统运行。

项目布局

推荐布局:

hello/
  .gitignore
  Clue.toml
  src/
    main.rid

二进制项目的清单包含一个 [[bin]] 目标:

[package]
name = "hello"
version = "0.1.0"

[[bin]]
name = "hello"
path = "src/main.rid"

[dependencies]

库项目使用 [lib]src/lib.rid。如果清单没有显式目标,clue build 会按包类型寻找入口。二进制包依次检查 src/main.ridsrc/lib.rid<package-name>.ridmain.rid;库和过程宏包依次检查 src/lib.rid<package-name>.ridlib.ridsrc/main.rid

旧项目仍可以在 [package] 中使用 entry 指定入口:

[package]
name = "hello"
entry = "src/bin/hello.rid"
version = "0.1.0"

单个二进制目标时,旧式 entry 的优先级高于 [[bin]].path / [lib].path。多个 [[bin]] 各自使用 pathcheckbuild 默认处理全部目标,也可以通过 --bin <name> 只处理一个,run 在多目标时要求指定 --bin。未写 name 时从入口文件名推导,重名会被拒绝。

运行时与分配器

二进制项目默认链接 Riddle 自带的保守式非移动 GC。要替换为自定义 GC、arena 或其他分配器,在项目根目录的 Clue.toml 中指定一个 C 源文件:

[runtime]
source = "runtime/custom_gc.c"

路径相对项目根目录解析。运行时源码必须实现以下 ABI:

void rgc_init(void *stack_bottom);
void *rgc_alloc(size_t size);
void *rgc_realloc(void *ptr, size_t size);
void rgc_free(void *ptr);
void rgc_collect(void);

Clue 会单独链接平台进程参数运行时;自定义内存运行时不需要实现 std::env 的参数函数。

rgc_alloc 返回的地址必须满足普通 C 对象的对齐要求,并且在引用仍可能存在时不能移动。rgc_reallocrgc_free 供标准库容器(如 Vector)显式管理缓冲区:rgc_realloc 迁移并保留原内容,rgc_free 立即释放且必须接受空指针;基于 malloc 的分配器可直接委托给 realloc/free。无回收分配器可以忽略 stack_bottom,并把 rgc_collect 实现为空函数。当前 ABI 不支持移动式 GC、finalizer 或多线程栈注册。

要像 Rust 一样完全关闭 GC,在二进制包中设置:

[runtime]
gc = false

这不是把 rgc_collect 留空,而是从生成结果中移除收集器、根扫描和全部 rgc_* 符号,改用 riddle_allocriddle_reallocriddle_free 管理有所有者的堆值。闭包环境和容器缓冲区在所有者结束时确定性释放;需要让栈上值活过其作用域的引用会报告 E0310。输入引用仍可在不延长生命周期的情况下转发。gc = false 不能与 source 同时声明。

运行时属于最终进程,因此 [runtime] 只允许出现在二进制包;库和依赖包只生成 ABI 调用,不能选择运行时。

依赖

[dependencies] 支持 path、git 和 sparse registry。版本使用 semver 约束:

[dependencies]
math = { path = "../math", version = "^1.0" }
json = "^1.2"
codec = { git = "https://example.com/codec.git", tag = "v1.0.0" }
log = { version = "^1", optional = true, default-features = false }

[dev-dependencies]
assertions = { path = "../assertions" }

表格式依赖还支持 package 重命名、branchtagrevregistryfeaturesdefault-featuresoptional。依赖键会成为当前包里的模块名,package 指向依赖包自己的 [package].name

[dependencies]
math = { package = "math-core", path = "../math-core" }

源码中按模块路径使用依赖键:

fun main() -> i32 {
    math::one()
}

clue addclue remove 会修改清单并保留无关布局。[dev-dependencies] 只在 test、example 和 bench 目标中加载。依赖键必须是合法模块名,也就是字母或 _ 开头,后面跟字母、数字或 _

作为依赖加载时,Clue 会优先使用 [lib].path;没有 [lib] 目标时,再依次寻找 src/lib.rid<package-name>.ridlib.ridsrc/main.rid。依赖包需要用 pub 导出给调用方使用的函数、类型、模块或 use 重新导出。

工作区

根目录可以用虚拟工作区清单注册子 crate:

[workspace]
crates = ["hello", "math"]

每个注册目录都维护自己的 Clue.toml。根目录执行 clue checkclue build 会按依赖顺序处理所有 crate;在子目录执行时默认只处理当前 crate,--workspace 处理全部,--package <name> 选择单个 crate。工作区和包内的二进制分别用 --package <name>--bin <name> 选择。

单包项目在项目根目录生成 Clue.lock v3。工作区只维护根目录的一个锁文件,子 crate 不单独生成;工作区内的 path 依赖必须在 workspace.crates 中注册。锁文件记录包名、版本、source、依赖、启用的 feature、registry checksum、git revision 和源码指纹。普通 checkbuildfetch 优先复用已锁定版本,clue update 才重新选择满足约束的版本;--locked 会拒绝缺失或过期的锁文件,--offline 只读取缓存。registry 包下载后会校验 SHA-256,并按安全路径规则解包。

过程宏

过程宏包使用 [lib] proc-macro = true

[package]
name = "answer-macros"

[lib]
path = "src/lib.rid"
proc-macro = true

宏函数由 Riddle 编写并且必须公开。derive 宏和函数式宏使用 TokenStream -> TokenStream 签名,属性宏接收属性参数与被标记条目两个 TokenStream

#[proc_macro_derive(Answer, attributes(answer))]
pub fun derive_answer(input: TokenStream) -> TokenStream {
    TokenStream::from_str("fun generated_answer() -> i32 { 42 }")
        .unwrap_or(TokenStream::new())
}

#[proc_macro]
pub fun answer(input: TokenStream) -> TokenStream {
    TokenStream::from_str("42").unwrap_or(TokenStream::new())
}

#[proc_macro_attribute]
pub fun replace(args: TokenStream, item: TokenStream) -> TokenStream {
    item
}

TokenStreamTokenTree 序列组成,而不是保存原始源码字符串。TokenTree 分为 GroupIdentPunctLiteral;分组递归包含另一个 TokenStream,每个 token 都带有输入中的字节范围。宏可以借用或取得 token 的所有权进行迭代:

for tree in &input {
    match tree {
        TokenTree::Ident(ident) => println!("{}", ident.as_str()),
        TokenTree::Group(group) => {
            for nested in group.stream() {
                let span = nested.span();
            }
        },
        _ => {},
    }
}

TokenStream::from_str 会执行词法分析,失败时返回 LexError,并把新 token 的位置设为当前宏调用点;to_string() 则提供源码文本视图。空白和注释不属于 token,因此不会逐字保留。TokenStream::clone() 共享底层 token,首次修改时才复制。Clue 与宏宿主之间传递结构化 token tree,编译器也直接把输出 token 送回解析器,不会把整个展开结果重新做一次词法分析。

过程宏包还会自动获得内置 syn 模块和 quote!,不需要在 Clue.toml 中添加依赖。 syn 可以把输入解析为 DeriveInputItemStmtExprTypePatquote! 则通过 #name 插入实现了 ToTokens 的值:

use syn::{DeriveInput, parse};

#[proc_macro_derive(Answer)]
pub fun derive_answer(input: TokenStream) -> TokenStream {
    let parsed = match parse::<DeriveInput>(input) {
        Result::Ok(value) => value,
        Result::Err(error) => {
            error.emit();
            return TokenStream::new();
        },
    };
    let generated = Ident::new("generated_answer", parsed.ident.span());
    quote! { fun #generated() -> i32 { 42 } }
}

结构化 derive 输入、通用语法节点、自定义 ParseVisitFoldquote! 重复语法见内置 synquote!

使用方把宏包声明为依赖,再把 derive 宏导入独立的宏命名空间;下例使用本地 path:

[dependencies]
answer_macros = { package = "answer-macros", path = "../answer-macros" }
use answer_macros::{Answer, answer, replace};

#[answer]
#[derive(Answer)]
struct Value {}

#[replace]
fun old_value() -> i32 { 0 }

fun main() -> i32 { answer!() }

宏导入支持 use answer_macros::{Answer as GenerateAnswer};use answer_macros::*;。宏名可以与类型、trait 或值同名而不冲突;不导入时仍可使用 #[derive(answer_macros::Answer)] 限定写法。derive 只能用于结构体和枚举;Riddle 当前没有 union 条目。pub use 可以在模块中重导出过程宏,混合导入会保留同一条 use 中的普通名称。 函数式宏可用于表达式、条目、类型和模式位置。attributes(answer) 注册的 helper 属性只在 对应 derive 的输入条目、枚举变体和字段上有效,未注册的 helper 属性会在调用宏之前报错。

Clue 会把过程宏包编译为宿主平台进程,而不会把它拼入目标程序。每个过程宏包第一次调用时懒启动一个独立宿主进程,并在后续调用中复用它;单次调用最多运行 10 秒,输入和输出各受 16 MiB 上限保护。宿主崩溃或超时只会终止当前 worker,下一次调用会重新启动,Clue 不会被直接带崩。derive 和属性宏输出必须是顶层条目,函数式宏输出必须适合调用位置。宏诊断会使用传入的 Span,复制到输出的 token 也会把后续编译错误映回原位置;使用 Span::call_site() 创建的 token 和诊断则指向宏调用。生成代码中的宏会继续展开,最大深度为 32。过程宏包可以通过本地 path 依赖使用另一个过程宏包,clue check 也能直接检查过程宏包自身。LSP 会识别宏导入和调用,并提供分类高亮、悬停、定义跳转、引用、别名重命名和按宏种类过滤的补全。

模块、usepub use 和可见性规则见模块、use 与包

项目诊断

clue check

Clue 会展开入口文件声明的外部模块和锁定依赖,再运行完整的编译检查。错误位置会映射回实际的 .rid 文件,而不是统一显示为项目入口文件。

riddle-lsp 使用相同的项目加载规则。打开 Clue 项目中的文件时,诊断会包含模块和本地依赖,并优先使用编辑器中尚未保存的内容;任一文件变化后,所有已打开文档的诊断都会刷新。

构建缓存

Clue 会缓存构建指纹。Clue.toml、展开后的源码、运行时源码、当前 Riddle 编译器版本、目标平台,或者 C 编译器的实际路径与版本发生变化时会重新构建;没有变化且输出文件仍存在时会输出 fresh。成功的 C11 兼容性探测也按编译器身份缓存。

运行项目

clue run
clue run path/to/project -- arg1 arg2

run 只接受二进制项目,并把 -- 后的参数原样传给程序。程序退出码会成为 clue run 的退出码。

特性和目标模型

可选依赖可以在 [features] 中通过 dep:<name> 启用;name/feature 转发依赖 feature,name?/feature 只在依赖已启用时转发。--features a,b 启用命名 feature,--all-features 启用清单中的全部 feature,--no-default-features 禁用 default。未显式声明目标时,Clue 会发现 src/main.ridsrc/bin/*.ridtests/*.ridexamples/*.ridbenches/*.rid--all-targets 还会检查或构建 test、example 和 bench。

获取、发布与安装

clue fetch 获取依赖并更新缓存,clue tree -e features 显示锁定的 feature,clue metadata 输出清单和锁图 JSON。缓存位于 $CLUE_HOME/registry$CLUE_HOME/git;默认 CLUE_HOME 是用户目录下的 .clue

clue package.clue/package 生成 .cluepkg--list 只列出将被打包的文件。clue publish --dry-run 执行打包与发布权限校验但不联网;实际发布使用所选 registry API。clue install --path .clue install calculator@^1clue install --git <url> 会构建二进制并安装到 $CLUE_HOME/binclue uninstall <name> 删除它。

registry 和构建设置可写入 $CLUE_HOME/config.toml 或项目的 .clue/config.toml,项目配置优先。CLUE_OFFLINECLUE_JOBSCLUE_REGISTRY_INDEXCLUE_REGISTRY_TOKEN 可以覆盖配置文件。

作为 Rust 库使用

clue crate 公开项目创建、检查、构建和项目分析 API。

use clue::{ProjectKind, build, check, new, run};
use std::path::Path;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let root = Path::new("hello");
    new(root, ProjectKind::Binary)?;
    check(root)?;
    build(root)?;
    run(root, &[])?;
    Ok(())
}

ProjectKind::Library 用于库项目。需要初始化已有目录时使用 init,其覆盖保护与命令行版本相同。

当前限制

  • 二进制、静态库和动态库的最终链接需要受支持的系统 C 工具链;
  • clue run 只能运行宿主目标,交叉构建产物需要复制到目标系统;
  • 交叉链接除 ridup 目标组件外,仍需要目标平台的 sysroot、SDK 和系统库。