- 编程语言
- AI Agent
- 编译器
- CLI
- 人工智能
【免费下载链接】baml
The programming language for agents
导读
BAML 是面向 Agent 的编程语言,而com.boundaryml.bamlGradle 插件让 Java/Kotlin 项目可以在构建期(build time)直接从baml_src/目录生成类型安全的 BAML Java SDK,无需把任何生成代码提交进仓库。本文以 gradle-plugin/README.md 为主体,结合仓库内的插件源码、功能测试与 quickstart 示例,完整讲解插件的接入方式、baml { … }全部配置项、generateBaml任务的增量与缓存机制、依赖自动注入原理,以及插件的发布流程。读完本文,你将能在一行插件声明内完成 BAML Java 工程的搭建,并理解其底层"protobuf-gradle-plugin 模式"的工程决策。
1. 插件定位:构建期生成,protobuf-gradle-plugin 模式
com.boundaryml.baml是 Java 打包方案中pattern C(构建期生成)的具体实现,模型参照 protobuf-gradle-plugin:一个可缓存的generateBaml任务运行已安装的bamlCLI,把 SDK 写入build/generated/sources/baml/java/main,并把这个目录接入主 source set。
这一设计带来三个直接收益:
- 零提交:生成代码永远不会进入仓库,
.gitignore无需额外维护; - 增量:当
baml_src/下的文件、baml.toml或 CLI 版本都没有变化时,Gradle 将任务标记为UP-TO-DATE并跳过; - 单一事实来源:BAML 源码(
.baml文件)是唯一需要维护的类型定义。
在 BamlPlugin.java 的类注释中,可以读到插件职责的完整清单:应用java插件、注册baml { … }扩展、注册可缓存的GenerateBamlTask、把任务输出注册为 Java 源码根与资源、并自动管理 BAML 运行时依赖。
2. 接入:一行插件声明即完成全部配置
插件本身就是全部配置。在build.gradle.kts中加入:
plugins { id("com.boundaryml.baml") version "0.15.0-nightly.1" } repositories { mavenCentral() }应用该插件后,插件会替你完成三件事(BamlPlugin.java#L88-L163):
- 自动应用
java插件——因此一个只有baml.toml和baml_src/目录的项目无需再声明任何插件(java/java-library/application已存在时幂等); - 注入
implementation("com.boundaryml:baml-bridge:<pluginVersion>")——生成 SDK 编译所依赖的 BAML 运行时,版本锁定为插件自身版本。插件与baml-bridge由同一条发布流水线以同一版本发布,因此两者永远匹配; - 注入
runtimeOnly("com.boundaryml:baml-bridge:<pluginVersion>:natives-<platform>")——为构建机器平台自动注入对应平台的原生 jar,平台由os.name/os.arch自动探测(这正是运行时原生库加载器会查找的 classifier)。
之后正常构建即可:
gradle build # 先运行 generateBaml,再执行 compileJava仓库内的 examples/quickstart 演示了最简形态——plugins块一行 +repositories { mavenCentral() },然后gradle run即可运行(示例输出add(2, 3) = 5)。该示例要求 JDK 17+,且bamlCLI 的版本与插件版本一致。
2.1 从 Maven Central 解析插件(nightly 版本)
夜间版(nightly)只发布到 Maven Central,而不发布到 Gradle Plugin Portal。如果你从 Central 而非 Plugin Portal 解析插件,需要在settings.gradle.kts中同时声明两个仓库:
pluginManagement { repositories { gradlePluginPortal() mavenCentral() } }这与 examples/quickstart/settings.gradle.kts 的写法一致——示例注释指出,实际使用中 Portal 端点也会代理 Central,但示例不应依赖基础设施行为,因此显式声明了两个仓库。
3. 配置项:baml { … }扩展块
插件的全部可调项集中在可选的baml { … }块中(完整定义见 BamlExtension.java):
| 选项 | 类型 | 默认值 | 含义 |
|---|---|---|---|
srcDir | DirectoryProperty | 项目目录 | 包含baml.toml与baml_src/的目录;作为--from传给 CLI |
bamlExecutable | Property<String> | "baml" | 要运行的 CLI——可以是PATH上的裸命令名,也可以是特定二进制文件的绝对路径 |
outputType | Property<String> | "java" | 仅作信息展示。真正的生成器配置位于baml.toml的[generator.<name>]中 |
nativePlatforms | ListProperty<String> | (空 → 探测宿主机) | 要依赖的原生 jar classifier 列表。空则自动探测构建机器;显式列表替换探测;["all"]则添加全部已知平台 |
manageDependencies | Property<Boolean> | true | 是否由插件自动注入baml-bridge运行时 + 原生 jar。设为false表示这些依赖由你自己管理 |
一个典型的配置块:
baml { srcDir.set(layout.projectDirectory) bamlExecutable.set("baml") // 跨平台产物:依赖每个平台的原生 jar。安全——运行时加载器会按 // os/arch 挑选正确的一个,其余是惰性的(inert)。 nativePlatforms.set(listOf("all")) // 或者显式点名:listOf("linux-x86_64", "macos-aarch64")。 }已知的nativePlatformsclassifier:linux-x86_64、linux-aarch64、macos-x86_64、macos-aarch64、windows-x86_64、windows-aarch64。实验性的 musl classifier 永远不会被自动探测,也不属于"all"——在 Alpine 上需要显式请求linux-<arch>-musl。
3.1 平台探测与"all"展开的源码细节
从 BamlPlugin.java#L233-L252 的resolveNativePlatforms可以看到三态逻辑:空列表 →detectPlatform()只取宿主机一个平台;列表含"all"→ 展开为ALL_PLATFORMS六个平台(linux-x86_64、linux-aarch64、macos-x86_64、macos-aarch64、windows-x86_64、windows-aarch64);其他显式列表 → 按原样去重、保序。
detectPlatform()的映射逻辑(mapOs+mapArch)在 BamlPlugin.java#L322-L363 中,它与运行时baml_bridge的 NativeLibraryLoader.java 完全一致(macOS 判断必须排在 Windows 之前,因为"darwin"包含子串"win";amd64/x64映射为x86_64,arm64/aarch64映射为aarch64)。这样插件依赖的 jar 恰好就是运行时加载器要查找的那个。
"all"之所以"安全",是因为运行时加载器按 JVM 自身的os.name/os.arch在/native/<os>-<arch>/classpath 资源上做 first-hit-wins 选择(见 NativeLibraryLoader.java#L43-L81),多余平台的原生 jar 只是闲置在运行时 classpath 上。代价仅仅是下载体积(每个平台一份引擎 cdylib),而非正确性。
3.2 退出依赖管理(opt-out)
如果你已经在依赖中声明了com.boundaryml:baml-bridge,插件会检测到并什么都不注入(以info级别日志记录,让位于你的声明)。如果需要完全手动控制——自定义运行时的坐标、自带的 jar、或非常规平台——则设置manageDependencies.set(false)并自行添加依赖:
baml { manageDependencies.set(false) } dependencies { implementation("com.boundaryml:baml-bridge:0.15.0-nightly.1") runtimeOnly("com.boundaryml:baml-bridge:0.15.0-nightly.1:natives-linux-x86_64") }插件注入逻辑的完整规则(含"defer to explicit"检测与 Kotlin 消费者额外注入baml-bridge-kotlin的分支)见 BamlPlugin.java#L176-L277。其中值得注意的细节:Kotlin JVM 插件(org.jetbrains.kotlin.jvm)被应用时,插件会额外注入同版本的com.boundaryml:baml-bridge-kotlin(Kotlin 人体工学层,其 POM 以api依赖传递引入baml-bridge);纯 Java 消费者永远不会拉到 Kotlin 运行时。
4.generateBaml任务:做了什么
generateBaml(group 为baml,@CacheableTask)的定义在 GenerateBamlTask.java 中。它声明了三类输入和一个输出:
- Inputs——
baml.toml(@InputFile,路径敏感RELATIVE)、srcDir/baml_src/下每个文件(@InputFiles,路径敏感,因此重命名/删除也会被追踪)、以及解析出的 CLI 版本(baml --version,作为@Input,工具链升级会触发重新生成); - Output——
build/generated/sources/baml/java/main; - 执行体——先删除输出目录,再以
baml generate --from <srcDir> -o <outputDir>/baml_sdk的形式运行 CLI。
4.1 输出布局与baml_sdk子目录
生成的.java声明package baml_sdk.*,因此 emitter 的输出根必须是一个baml_sdk/子目录(baml generate --from <srcDir> -o <outputDir>/baml_sdk),而<outputDir>本身注册为 Java 源码根(GenerateBamlTask.java#L107-L124)。
任务执行前总是清理输出目录——因为 BAML 自身的generate不会移除陈旧文件,一个被重命名或删除的 BAML 类若不清理,就会留下一个仍然能编译通过的过期.java。
4.2 布线(Wiring)三件事
插件把生成结果接入构建图的方式(BamlPlugin.java#L123-L151):
- 源码:生成的
.java加入sourceSets.main.java。注意这里是注册任务 Provider而非裸目录——srcDir(generateBaml)会让 Gradle 从 source set 本身推断出generateBaml → compile的依赖,这正是 IntelliJ 在 Gradlesync时就能生成源码的原因(IDE 读取 source-set 模型,而模型此时指向一个任务输出); - 资源:
baml_sdk/**/*.b64字节码被打包为资源——它必须位于运行时 classpath 的/baml_sdk/inlinedbaml.b64。include 范围限定在该fromspec 内,以免触碰消费者自己的资源; - 依赖:
compileJava与processResources都dependsOn(generateBaml)(前者作为保险丝,即使 source set 推断已覆盖也保留)。
4.3 增量与 UP-TO-DATE
因为输入与输出都已声明,Gradle 只在.baml源文件、baml.toml或 CLI 版本变化时才真正执行generateBaml;否则任务为UP-TO-DATE,任务体——包括 CLI 调用——被整体跳过。运行已构建的程序永远不会触发生成。
这条行为由功能测试直接验证:在 BamlPluginFunctionalTest.java 中,第一次构建generateBaml结果为SUCCESS(同时断言baml_sdk/Baml.java生成、baml_sdk/Baml.class编译、resources/main/baml_sdk/inlinedbaml.b64打包),第二次无变化的构建结果为UP_TO_DATE。
4.4 CLI 缺失:配置期永远成功,执行期给出安装提示
如果baml可执行文件找不到或无法运行,任务会在执行期失败(配置期总是成功),并附带安装提示:
curl -fsSL https://pkg.boundaryml.com/install.sh | sh(安装脚本见仓库根目录 scripts/install.sh。)修复方式是baml { bamlExecutable.set("/path/to/baml") }或将baml加入PATH。
这个"延迟失败"的机制很精巧:插件通过一个 provider 在输入快照时刻(执行期)解析 CLI 版本,缺失时返回空字符串哨兵(BamlPlugin.java#L371-L392),GenerateBamlTask.generate()在版本为空时抛出带安装提示的GradleException(GenerateBamlTask.java#L82-L85)。功能测试 missingExecutableFailsTaskWithInstallHint 断言了FAILED结果与错误输出包含安装命令。
5. 从 CLI 到 classpath:一条完整链路
把以上机制串起来,一次gradle build的实际链路是:
- Gradle 对
generateBaml做输入快照(baml.toml+baml_src/**+ CLI 版本); - 快照未变 →
UP-TO-DATE跳过;变了 → 清理输出目录,调用baml generate --from <srcDir> -o <outputDir>/baml_sdk; - 生成的
baml_sdk/**/*.java进入编译:compileJava直接编译它们(它们编译期依赖注入的com.boundaryml:baml-bridge); baml_sdk/**/*.b64经processResources进入resources/main,运行时随 classpath 加载;- 运行时
NativeLibraryLoader按宿主os.name/os.arch从natives-<platform>jar 中提取并System.load对应的bridge_javacdylib(NativeLibraryLoader.java#L43-L81),BAML 引擎由此生效。
5.1 quickstart 的完整工程形态
仓库中的 examples/quickstart 给出了可直接运行的最小工程,三份关键文件相互印证:
- baml.toml——真正的生成器配置在这里,
[generator.java_client]声明output_type = "java"、output_dir = "."、naming_convention = "preserve-case"。这正是上文outputType选项"仅作信息展示"的原因:真正的生成器配置由baml.toml拥有; - main.baml——两个极简函数
add(a: int, b: int) -> int与greet(name: string) -> string,生成 SDK 后即可在 Java 中类型安全地调用; - settings.gradle.kts——标准
pluginManagement双仓库声明。
6. 插件自身的发布:双渠道与本地排练
插件从发布流水线发布到两个地方(README 的 Publishing 一节对此有完整说明):
- Gradle Plugin Portal(
com.gradle.plugin-publish→publishPlugins)——plugins { id("com.boundaryml.baml") version "X" }的规范家园,仅发布稳定渠道(canary/stable);publishPlugins任务原生地从GRADLE_PUBLISH_KEY/GRADLE_PUBLISH_SECRET环境变量读取 Portal API 密钥; - Maven Central——Portal 要求的元数据(
displayName、description、website/vcsUrl、tags)加上 marker POM(com.boundaryml.baml:com.boundaryml.baml.gradle.plugin),与baml-bridge一同搭载在同一份签名 Central bundle 上,因此nightly(Portal 不接受)能经由mavenCentral()到达 Gradle 消费者。
产物坐标为com.boundaryml:baml-gradle-plugin;marker 发布由java-gradle-plugin自动生成(见 gradle-plugin/settings.gradle.kts)。本地排练命令:
# 发布到 ~/.m2(无需任何凭据) gradle publishToMavenLocal -PbamlVersion=0.15.0-nightly.1 # 仅校验 Portal 元数据而不发布(校验阶段需要 Portal 凭据;不发布任何东西) gradle publishPlugins --validate-only -PbamlVersion=0.15.0-nightly.1 # 暂存签名后的 Central 布局(写入 build/staging-deploy,或用 -PbamlStagingDir # 指向共享目录树,让插件 + marker 与 baml-bridge 加入同一 bundle) gradle publishAllPublicationsToStagingRepository -PbamlVersion=0.15.0-nightly.1相关属性:
| 属性 | 默认值 | 含义 |
|---|---|---|
bamlVersion | 0.0.0-dev | 发布版本(canary 为纯版本号,nightly 带后缀) |
bamlStagingDir | build/staging-deploy | 暂存发布的 file-repo 目的地(可指向共享的 Central 树) |
bamlSign | (未设置) | 存在时通过本地 gpg agent 对每个发布签名(Maven Central) |
签名有两条路径(与baml_bridge对称,详见 baml_bridge/PUBLISHING.md):CI 内存密钥(GPG_PRIVATE_KEY/GPG_PASSPHRASE环境变量)优先;否则-PbamlSign使用本地 gpg agent——此时务必传-Psigning.gnupg.keyName=<KEYID>。未配置密钥时,自动创建的sign*任务会跳过,因此publishToMavenLocal、publishPlugins与 TestKit 都不受阻碍。
首次提交 Portal 需要 Gradle 团队对插件 id 的一次性人工审批,通过publish-gradle-plugin-manual.yml工作流(workflow_dispatch+ 版本输入)触发。
7. 测试保障:功能测试覆盖的行为矩阵
gradle-plugin/src/test 使用 Gradle TestKit(GradleRunner)覆盖了五类行为:
| 测试 | 验证点 |
|---|---|
pluginAppliesAndRegistersGenerateTask | 插件应用并注册generateBaml(无 CLI 也须成功配置) |
generatesCompilesAndIsUpToDateOnRerun | 端到端生成 + 编译 + 资源打包 + 二次构建UP-TO-DATE |
missingExecutableFailsTaskWithInstallHint | 缺失可执行文件在执行期失败并带安装提示(配置期不失败) |
generatedSourceRootIsBackedByGenerateBamlTask | 生成源码根由任务 Provider 支撑(IntelliJ sync 生成的依据) |
| 依赖管理系列(5 个用例) | 默认注入baml-bridge+ 宿主原生 jar;Kotlin 消费者额外获得baml-bridge-kotlin;显式依赖压制注入;nativePlatforms显式列表替换探测;"all"展开全部平台;manageDependencies=false零注入 |
测试中还使用了一个 fakebaml脚本模拟真实 CLI(应答--version并写出自包含的baml_sdk树),使端到端编译测试保持封闭(BamlPluginFunctionalTest.java#L518-L561)。这组测试本身就是插件契约的可执行文档。
8. 常见问题速查
baml命令找不到:任务失败并输出安装提示。先执行curl -fsSL https://pkg.boundaryml.com/install.sh | sh安装 CLI,或bamlExecutable.set("/absolute/path/to/baml"),或把baml加入PATH。- 版本不匹配:插件、
baml-bridge、CLI 在同一流水线以同一版本发布。plugins块中的版本应与安装的 CLI 版本一致(见 quickstart README 的版本说明)。 - 需要在 Alpine/musl 上运行:显式请求
linux-<arch>-muslclassifier——它不在自动探测与"all"之列。 - 需要跨平台发布产物:
nativePlatforms.set(listOf("all")),运行时加载器会按宿主平台挑选正确的原生 jar,多余平台 jar 是惰性的。 - 想自己管理运行时依赖:声明自己的
com.boundaryml:baml-bridge(插件自动让位),或manageDependencies.set(false)完全接管。 - IDE 不识别生成的 SDK:生成源码根注册的是任务 Provider(
srcDir(generateBaml)),因此 Gradle sync 时 IntelliJ 就会生成源码;若仍异常,先执行一次gradle build或gradle generateBaml。
进一步阅读(仓库内)
- 插件实现:BamlPlugin.java、GenerateBamlTask.java、BamlExtension.java
- 功能测试:BamlPluginFunctionalTest.java
- 可运行示例:examples/quickstart(含 baml.toml 与 main.baml)
- 运行时原生库加载器:NativeLibraryLoader.java
- 发布说明:baml_bridge/PUBLISHING.md
- CLI 安装脚本:scripts/install.sh
- 编程语言
- AI Agent
- 编译器
- CLI
- 人工智能
【免费下载链接】baml
The programming language for agents
相关推荐
public-image-mirror:容器镜像加速 Mirror 的完整使用指南——前缀映射、白名单机制与 K8s/Docker 加速实战
public image mirror:容器镜像加速 Mirror 的完整使用指南——前缀映射、白名单机制与 K8s/Docker 加速实战 很多镜像源仓库(如
编程语言AI Agent编译器CLI人工智能gRPC-Java 编译构建完全指南:Gradle、代码生成插件与 Bazel 双构建体系详解
gRPC Java 编译构建完全指南:Gradle、代码生成插件与 Bazel 双构建体系详解 导读 本文以 grpc java 仓库根目录下的 COMPILI
后端RPC框架大麦自动抢票工具:3 个脚本完成移动端自动购票的方法
大麦自动抢票工具:3 个脚本完成移动端自动购票的方法 这是一个开源的大麦自动抢票项目,用 Python 实现。它把自动化拆成两条路线:Web 端用 Seleni
GUI 自动化RPA
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考