Homebrew 许可证指南:Formula 的 SPDX 声明语法与 HOMEBREW_FORBIDDEN_LICENSES 合规拦截全解析
【免费下载链接】brew🍺 The Package Manager for Everywhere项目地址: https://gitcode.com/GitHub_Trending/br/brew
Homebrew 为进入homebrew/core的每一个 Formula 都建立了一套基于 SPDX License List 的许可证(licence)声明规范,并把这套声明与安装流程深度绑定——不仅用于元数据展示与审计,还能通过HOMEBREW_FORBIDDEN_LICENSES环境变量在安装阶段直接拦截不符合许可策略的软件及其依赖。本文以仓库中的 docs/Licence-Guidelines.md 为主体,结合 Homebrew 的 Ruby 源码与测试用例,系统讲解许可证的声明语法、复杂 SPDX 表达式的语义,以及禁装策略的真实判定逻辑。读完你既能写出规范合法的license声明,也能在企业或合规场景下精确配置许可证黑名单。
总体要求:DFSG 兼容是进入homebrew/core的前提
根据官方指南,homebrew/core中的 Formula 所使用的许可证必须与 Debian Free Software Guidelines(DFSG)兼容,或者依据 DFSG 的公共领域(public domain)判定标准正式释入公共领域。也就是说,许可证审查是 Homebrew 上游合并公式时的硬性门槛,它约束的是"这个软件能不能进官方核心仓库",而不是"用户能不能在本机安装"。
在实现层面,Homebrew 并不自带全量许可证知识,而是把 SPDX License List 的官方 JSON 数据本地缓存为 Library/Homebrew/data/spdx/spdx_licenses.json 与 Library/Homebrew/data/spdx/spdx_exceptions.json。Library/Homebrew/utils/spdx.rb 中的SPDX模块统一负责读取、校验与逻辑判断(对应函数如license_data、valid_license?、licenses_forbid_installation?等),这意味着你写下的每一个标识符最终都会与该离线数据做大小写不敏感比对。
为 Formula 声明许可证:三种基础写法
license是 Formula 的公开 API 方法,其完整定义位于 Library/Homebrew/formula.rb#L4104-L4110:无参调用时返回当前已设置的@licenses,传参时则完成设置,类型为SPDX::LicenseExpression。
写法一:SPDX 标识符字符串
绝大多数情况直接传 SPDX License List 中的标识符字符串即可:
license "MIT"写法二:公共领域使用符号
公共领域不能用普通字符串表达,需用:public_domain符号:
license :public_domain写法三:无法用 SPDX 表达
如果该 Formula 的许可证实在无法用任何 SPDX 表达式表示(例如多份相互冲突的自定义许可),则显式声明:cannot_represent:
license :cannot_represent从 Library/Homebrew/utils/spdx.rb#L14-L18 的源码可以看到,Homebrew 共定义了三个被允许的符号常量:ALLOWED_LICENSE_SYMBOLS = [:public_domain, :cannot_represent, :truncated]。其中:truncated是内部用途——当许可证文本超过 OCI 注解的长度限制(默认 255 字符)时,SPDX.truncate_license会在保留一段合法的AND前缀后追加该标记,避免整体丢弃(见 Library/Homebrew/utils/spdx.rb#L210-L240)。
声明之后的校验链路
写错的标识符不会只是"摆着难看"。SPDX.valid_license?(Library/Homebrew/utils/spdx.rb#L87-L92)会去掉末尾的+后与本地 SPDX 数据做忽略大小写的匹配;SPDX.deprecated_license?还会标记已经过时的 SPDX 标识符。这些校验服务于brew audit等检查流程,用于在提交公式前发现笔误。
此外,符号类许可证在序列化时会统一映射为LicenseRef-Homebrew-*前缀的文本形式(license_expression_to_string中Symbol分支的实现),例如:public_domain会展开为LicenseRef-Homebrew-public-domain,确保与 SPDX 体系外的语义也能在标准文本通道中传递。
复杂 SPDX 许可证表达式:组合、例外与嵌套
很多软件由多份许可证共同约束,甚至允许使用者在若干许可证之间做选择。Homebrew 支持用 Ruby 原生数据结构还原 SPDX License Expression Guidelines 的全部组合能力。
加号+:允许更高版本
+表示使用者可以选择同一许可证的后续版本:
license "EPL-1.0+"GNU 系必须显式后缀:-only与-or-later
GPL、LGPL、AGPL、GFDL这四类 GNU 许可证不允许使用+,必须用-only(仅该版本)或-or-later(该版本或更高版本)明确表述是否允许后续版本:
license "LGPL-2.1-only"license "GPL-1.0-or-later"any_of:任选其一即可
表示使用方可以在列出的许可证中选择任何一个来遵守:
license any_of: ["MIT", "0BSD"]all_of:必须同时满足
表示使用方必须同时遵守列出的全部许可证:
license all_of: ["MIT", "0BSD"]with:许可证例外条款
表示某个许可证附带一个 SPDX 例外(exception),典型如 LLVM 编译器的Apache-2.0+LLVM-exception组合:
license "MIT" => { with: "LLVM-exception" }例外标识符同样需要真实存在于本地 Library/Homebrew/data/spdx/spdx_exceptions.json 中,SPDX.valid_license_exception?会做逐条校验。
自由嵌套:一张"足够复杂"的表达式
以上结构可以无限制嵌套组合,官方指南给出的完整示例即是:
license any_of: [ "MIT", :public_domain, { all_of: ["0BSD", "Zlib", "Artistic-1.0+"], "Apache-2.0" => { with: "LLVM-exception" } }, ]语义解读:使用方要么选MIT,要么选公共领域,要么同时满足0BSD、Zlib、Artistic-1.0+并且在使用Apache-2.0时附带LLVM-exception例外。
Ruby 结构如何翻译成 SPDX 文本
如果你好奇这些 Hash 与数组最终在输出/报错时是什么样子,看 Library/Homebrew/utils/spdx.rb#L119-L156 的license_expression_to_string即可:any_of:被拼接为OR,all_of:被拼接为AND,{ "X" => { with: "Y" } }被拼接为X WITH Y,必要时用括号包裹嵌套子表达式(例如(0BSD AND Zlib AND Artistic-1.0+ AND Apache-2.0 WITH LLVM-exception))。这意味着错误提示、日志和文档输出中的表达式始终符合标准 SPDX 语法,可被其他工具直接读取。
用HOMEBREW_FORBIDDEN_LICENSES拦截安装
如果说上面的声明语法是"上游如何标注",那么禁装变量就是"下游如何执行"。HOMEBREW_FORBIDDEN_LICENSES让使用者(组织策略执行者)声明哪些许可证的软件及其依赖一律不得安装。
设置方式与基本语义
在 shell 环境中把它设为一个以空格分隔的许可证列表即可。若要禁止MIT、Artistic-1.0以及所有:public_domain声明的公式:
export HOMEBREW_FORBIDDEN_LICENSES="MIT Artistic-1.0 public_domain"设置后,Homebrew 会拒绝安装任何声明了MIT的公式;更重要的是,禁止会沿着依赖树传播——即使主公式本身许可证合规,只要它声明了对某个MIT公式的依赖,同样会被拒绝(源码路径见 Library/Homebrew/formula_installer.rb#L1823-L1833,错误信息中会明确报告"The installation ofXhas a dependency onYwhere all its licenses were forbidden")。
复杂表达式下的判定语义:any_of看全部、all_of看任一
禁装判断不是简单的字符串包含,而是对整套表达式做布尔推理,其核心算法在SPDX.licenses_forbid_installation?(Library/Homebrew/utils/spdx.rb#L266-L287):
- 对
any_of:列表,只有当每一个候选许可证都被禁时才禁止安装(源码取.all?)——因为只要还有一条合规出路,使用方就可选它,安装依然合法; - 对
all_of:列表,只要任意一个被禁即禁止安装(源码取.any?)——因为全部条款都必须同时满足,缺一不可; - 对
with:例外结构,直接判断基础许可证是否被禁。
沿用上例(禁MIT、Artistic-1.0、public_domain)来验证:
被拒绝安装的声明:
license any_of: ["MIT", "Artistic-1.0"] # 两条出路全部被禁 → 拒绝license all_of: ["MIT", "0BSD"] # 存在被禁的 MIT → 拒绝被允许安装的声明:
license any_of: ["MIT", "0BSD"] # 仍有 0BSD 可选 → 放行以上判定行为在 Library/Homebrew/test/utils/spdx_spec.rb 的.licenses_forbid_installation?测试组中有大量对应断言(例如any_of在单个禁项下返回false、在多项全禁时返回true;all_of含任一禁项即返回true),可作为行为契约参考。
版本语义:+与 GNU-only/-or-later的黑名单写法
黑名单同样支持版本策略。HOMEBREW_FORBIDDEN_LICENSES中带+的条目会禁掉该许可证及其所有未来更高版本。例如要禁止Artistic-1.0、Artistic-2.0及今后任何 Artistic 版本:
export HOMEBREW_FORBIDDEN_LICENSES="Artistic-1.0+"对 GNU 系许可证则用-only/-or-later表达版本范围。下面的配置会禁止GPL-2.0、LGPL-2.1与LGPL-3.0,但放行GPL-3.0:
export HOMEBREW_FORBIDDEN_LICENSES="GPL-2.0-only LGPL-2.1-or-later"这里的关键在于"or-later 会覆盖其后的所有版本":LGPL-2.1-or-later被禁等价于 2.1 及其后全部 LGPL 都被禁。实现位于SPDX.license_version_info与SPDX.forbidden_licenses_include?(Library/Homebrew/utils/spdx.rb#L252-L309):解析器用正则把GPL-2.0-or-later拆成["GPL", "2.0", true](名字、版本、是否允许更高版本)三元组,随后执行字符串版本比较——当黑名单条目允许更高版本且其版本号 ≤ 公式声明版本号时判定命中,否则仅在版本号完全相等时命中。注意该实现依据版本号字符串比较,判断前会先做名称对齐(GPL对GPL-3.0)。
非法的黑名单条目会怎样
如果黑名单里混入了 SPDX 中不存在的标识符(比如拼写错误),Homebrew 并不会直接崩溃,而是用opoo橙色警告提示哪些条目无效、不会被生效,并指引你通过brew info <formula>查看公式的真实许可证,或对照官方 SPDX License List 修正(见 Library/Homebrew/formula_installer.rb#L1806-L1814)。
触发时机与可选关联变量
许可证校验发生在 Formula 安装过程中,对应方法为FormulaInstaller#forbidden_license_check(Library/Homebrew/formula_installer.rb#L1786-L1844)。它先解析环境变量、把public_domain等字面量规整为符号,再按"先依赖、后本体"的顺序执行判定,命中时抛出CannotInstallFormulaError终止安装。如果只想单独安装公式而不做依赖级联检查,可使用安装时与依赖相关的选项(如--ignore-dependencies);判定逻辑对"仅依赖"模式也有独立分支。
HOMEBREW_FORBIDDEN_LICENSES的官方定义与说明记录在 Library/Homebrew/env_config.rb#L376-L379。与之配套的还有两个归属类变量:HOMEBREW_FORBIDDEN_OWNER(设置禁项的责任人,默认you)与HOMEBREW_FORBIDDEN_OWNER_CONTACT(联系方式),它们会被拼进拒绝安装的错误信息里,方便在组织内多人共用同一台构建机时,让报错者知道"该找谁、怎么找"(见 Library/Homebrew/formula_installer.rb#L1818-L1821)。安装集成侧的整套行为(含错误消息断言)由 Library/Homebrew/test/formula_installer_spec.rb#L1281 的#forbidden_license_check用例覆盖。
实战排查速查
- 查看某公式实际声明的许可证:运行
brew info <formula>,输出中会列出其许可证标识与依赖的许可证信息,这是核对黑名单误判时最直接的命令。 - 验证复杂表达式是否会被拦截:参照上文
any_of(全禁才禁)、all_of(任一即禁)的判定规则逐层展开;若表达式被嵌套,规则递归应用于每一层。 - 合规提交上游 Formula:确保许可证满足 DFSG 兼容要求,使用规范的 SPDX 标识符,GNU 系许可证务必写
-only/-or-later而非裸写版本号。 - 临时放开某项限制:在启动
brew install的同一 shell 中unset HOMEBREW_FORBIDDEN_LICENSES(或修正列表),注意该变量按进程继承,需在真正执行 brew 的会话中生效。
小结
许可证声明与合规拦截在 Homebrew 中是一体两面:license方法(Library/Homebrew/formula.rb#L4104-L4110)负责以标准 SPDX 语法标注公式的授权方式,支撑homebrew/core的 DFSG 合规审查;而HOMEBREW_FORBIDDEN_LICENSES则把同一份声明在安装期变成可执行的安全边界——精确到复杂表达式的any_of/all_of语义、+与-or-later的版本覆盖范围,并沿依赖树向下传播。理解 Library/Homebrew/utils/spdx.rb 中那套递归判定算法,你就能准确预判任何一条黑名单配置对任意 Formula 的实际影响,把"许可证合规"从人工审查真正落到自动化防线之上。
【免费下载链接】brew🍺 The Package Manager for Everywhere项目地址: https://gitcode.com/GitHub_Trending/br/brew
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考