🚧 Experimental. This module is under active development and may change a lot. No guarantees are made about the current shape of its directives, flags, generated targets or APIs.
A Gazelle extension for supply-chain metadata in Bazel. It:
- detects the SPDX license of each package from its license files (including symlinked ones), with configurable detectors and confidence thresholds;
- generates
license()/license_kind()targets and appends them to thepackage_metadatagenerated by Gazelle, so any ruleset's repository rules that run Gazelle (such asgo_repository) can tag their dependencies without patching them; - tags packages with
default_applicable_licenses, so a repo can carry different licenses for different subtrees (e.g. MIT everywhere but for a subpath); - enforces license policy at three strictness levels (full, relaxed, off);
- produces the metadata that tools can use to produce their SBOM reports.
# gazelle:license_default //enterprise:package_metadata- Applies to the directory's BUILD file and every BUILD file beneath it.
- A nested directive overrides the parent;
# gazelle:license_default nonestops tagging for that subtree. # gazelle:license_subtree <dir> <label>assigns a license to a directory and below from any BUILD file (usually the root), for when subtree BUILD files can't be edited, such as external repos. It beatslicense_defaultthere.- A target can be followed by the SPDX id it stands for:
# gazelle:license_default //:mit MIT. Full strict mode needs it; relaxed and off don't. - Every directory Gazelle visits is tagged (except with
detect, which writes nopackage(); see below); creating new BUILD files is governed bybuild_file_generation(go_repository). - The
package_metadatatarget named by the directive must exist, or be created by this extension when the declaration is made in its own package (see Generated license targets).
license_strict has three levels, set with the directive (for a directory and below) or the -license_strict flag:
| Level | Directive / flag | Behavior |
|---|---|---|
| full (default) | true, -license_strict |
Every license file is checked. See below. |
| relaxed | relaxed, -license_strict=relaxed |
Only a declaration that contradicts the license files fails. A license that can't be determined is left out, so reports show NOASSERTION and concluding a license is up to the report tooling. |
| off | false, -license_strict=false |
Nothing is enforced. |
# gazelle:license_strict relaxedRelaxed suits dependencies you don't control: a module without a license file, with a non-standard one, or with no declaration passes, and nothing is generated for what can't be determined. A declared SPDX id that the license files contradict still fails.
In full mode Gazelle reads every license file it visits (see Which files are scanned), matches it against known license texts with the configured detector (see Detectors and confidence), and fails when:
- the file doesn't match a standard license (below the detector's confidence). Both detectors do extract every license found in a file, so a file made only of several standard license texts passes. A file that mixes them with other text (a custom clause, or prose between the texts) is rejected, because the extra text can't be told apart from non-standard terms. Declare such a file yourself, lower its confidence, or split it (see
examples/MODULE.bazelforgo.yaml.in/yaml/v3); - nothing declares its license: the repo root needs
license_default, and a subdirectory needs alicense_subtreecovering it or alicense_defaultin its own BUILD file (a default inherited from a parent does not count); - the declaration has no SPDX id, or the id isn't among the licenses found in the file;
- a
detectdeclaration finds no license file.
A declaration whose id starts with LicenseRef- names a custom license (for example -license_subtree="enterprise=//ent:meta LicenseRef-Acme"). Its license files have no SPDX text to match, so they are skipped, as long as the declaration is explicit.
Instead of an SPDX id, a declaration can say detect: -license_default="//_gazelle_generated:package_metadata detect". The id is read from each module's own license files, one license() is generated per file (per id if a file holds several), and they are appended to the package_metadata that go_repository generates. Files that match no standard license are logged and skipped, unless strict mode is full, which then fails on them. A module with no license file at all fails full strict mode too, since there is nothing to detect: declare it yourself, for example -license_default="//_gazelle_generated:package_metadata LicenseRef-Unlicensed". No package() call is written, since REPO.bazel already applies that metadata to every package. With go_deps.gazelle_default_attributes(build_extra_args = [...]) this covers all Go modules; see examples/MODULE.bazel.
By default: LICENSE and LICENCE (plain, .txt, .md, .rst), COPYING (plain, .txt, .md) and UNLICENSE (plain, .txt), case-insensitively. NOTICE and PATENTS are not scanned: they are attribution and patent grants, not license texts.
# gazelle:license_files default NOTICE LICENSE-*license_files replaces the list for the directory and below. Names are case-insensitive globs, separated by spaces or commas, and default stands for the standard list. As a flag: -license_files="default NOTICE".
The plugin never invents a package identity. Next to each scanned license file, in the directory where the declaration was made, it writes one license() named gazelle_generated_license (or gazelle_generated_license_<file suffix>) with text set to that file, plus a license_kind() for a LicenseRef- id.
Target names are always gazelle_generated_ followed by a suffix:
license():gazelle_generated_<suffix>, where<suffix>is the license file name lowercased, with every character that isn'ta-zor0-9replaced by_, and leading and trailing_trimmed.LICENSEgivesgazelle_generated_license,LICENSE.txtgivesgazelle_generated_license_txt,LICENSE-APACHEgivesgazelle_generated_license_apache, andCOPYINGgivesgazelle_generated_copying. If the directory has no matching file, a singlegazelle_generated_licenseis written withouttext.license_kind()(only forLicenseRef-ids):gazelle_generated_kind.package_metadata()(only when created, see below): the name in the declared label, for examplepackage_metadatain//enterprise:package_metadata.
Symlinked license files are followed. If a scanned file such as LICENSE-extra is a symlink to docs/terms/LICENSE, the license() (and license_kind()) is written in docs/terms with text = "LICENSE", because text can only name a file of its own package. The declared package_metadata still lists it, so it applies to everything the declaration covers. The target name comes from the symlink's name (gazelle_generated_license_extra). Symlinks that leave the repository or are broken are skipped.
A symlink is a pointer, not always a statement about the whole repository: a project may link a file at its root only for reference while another file there is the one that applies. Only files matching license_files are scanned, so leave such a symlink out of that list, or turn following off for the directory and below with # gazelle:license_follow_symlinks false (flag -license_follow_symlinks=false), in which case symlinked license files are ignored.
The declared label must name a package_metadata. If it already exists (for example the //_gazelle_generated:package_metadata that go_repository writes before running Gazelle), the licenses are appended to its attributes and its purl is kept. If it doesn't exist and the declaration was made in that package, one is created with the purl of the repository's generated metadata plus #<package> as subpath (fallback pkg:golang/<gazelle:prefix>). Otherwise nothing is generated. The repo needs @package_metadata visible. Every package except _gazelle_generated gets package(default_applicable_licenses = [label]), unless the declaration is detect.
This requires a go_repository that writes its metadata before Gazelle runs.
Two detectors are available and you pick one:
| Detector | How it decides | Default confidence | Dependency |
|---|---|---|---|
licensecheck (default) |
share of the file's words covered by SPDX license texts | 90 | github.com/google/licensecheck |
licenseclassifier |
match confidence; the matches must also cover that share of the file's lines | 80 (also the minimum) | github.com/google/licenseclassifier |
# gazelle:license_detector licenseclassifier
# gazelle:license_confidence licensecheck 95
# gazelle:license_confidence licenseclassifier 85Both directives take an optional trailing file, relative to the BUILD file, to apply to that one file only:
# gazelle:license_detector licenseclassifier enterprise/LICENSE
# gazelle:license_confidence licensecheck 65 LICENSEA per-file setting wins over the global one. The confidence used for a file is the one set for the detector that ends up reading it. As flags, quote the pair: -license_confidence="licensecheck=65 LICENSE", -license_detector="licenseclassifier LICENSE" (paths relative to the repo root).
Confidence is set per detector, so switching detectors keeps each one's own value. Flags: -license_detector=<name> and -license_confidence=<detector>=<percent> (repeatable). licenseclassifier can't go below 80 because its embedded corpus only reports matches at or above that.
See examples/ for a real-world project using strict mode, per-file confidence and LicenseRef- declarations.
There is no override here. A declaration must match what the project ships; correcting it is a concluded license, to be handled by supply_chain_tools (not defined yet).
load("@gazelle//:def.bzl", "gazelle", "gazelle_binary")
gazelle_binary(
name = "gazelle_supply_chain_bin",
languages = ["@gazelle_supply_chain//:gazelle_supply_chain"],
)
gazelle(name = "gazelle", gazelle = ":gazelle_supply_chain_bin")Run bazel run //:gazelle. Re-running is idempotent. If other Gazelle languages
are enabled, add them to languages too.
The same settings are available as flags: -license_default=<label>, -license_subtree=<dir>=<label> (repeatable) and -license_strict (true by default, also relaxed or false); a label may be followed by its SPDX id, e.g. -license_default="//:mit MIT". The remaining settings also have flags: -license_files, -license_follow_symlinks, -license_detector and -license_confidence. Combine with -lang=license to only append tags to existing BUILD files.
go_repository always runs the Gazelle binary of the repository @bazel_gazelle_go_repository_tools, so it can't see this plugin. The mechanism that makes it work is to replace that repository: tools_repo.bzl builds the same Gazelle with the license plugin compiled in (gazelle_tools_with_license), and override_repo on gazelle's non_module_deps extension swaps it in. From then on every go_repository produced by go_deps runs the plugin.
A complete MODULE.bazel (a runnable version lives in examples/):
module(name = "my_project")
bazel_dep(name = "rules_go", version = "0.60.0")
bazel_dep(name = "gazelle", version = "2.0.0-beta.2")
bazel_dep(name = "package_metadata", version = "0.0.13")
# go_repository must write its package metadata before running Gazelle.
# Until that is released, use the merged upstream commit.
git_override(
module_name = "gazelle",
commit = "5cc8299af468de4290cf40f93b3ef046e866446d",
remote = "https://github.com/bazel-contrib/bazel-gazelle.git",
)
# This plugin.
bazel_dep(name = "gazelle_supply_chain", version = "HEAD")
git_override(
module_name = "gazelle_supply_chain",
commit = "<commit>",
remote = "https://github.com/hermeticbuild/gazelle_supply_chain.git",
)
# The Go SDK must be at least the one your go.mod asks for.
go_sdk = use_extension("@rules_go//go:extensions.bzl", "go_sdk")
go_sdk.download(version = "1.24.2")
go_deps = use_extension("@gazelle//:extensions.bzl", "go_deps")
go_deps.from_file(go_mod = "//:go.mod")
# Extra flags for every Go module: read each module's SPDX id from its own
# license files. Full strict mode is the default and fails on anything the
# plugin can't classify; add -license_strict=relaxed to report NOASSERTION
# for those instead.
go_deps.gazelle_default_attributes(
build_extra_args = [
"-license_default=//_gazelle_generated:package_metadata detect",
],
)
# A module that needs something else replaces those flags with its own.
go_deps.gazelle_override(
build_extra_args = [
"-license_default=//_gazelle_generated:package_metadata MIT",
"-license_subtree=enterprise=//enterprise:package_metadata LicenseRef-Acme",
],
path = "github.com/example/module",
)
use_repo(go_deps, "com_github_example_module")
# 1. Build a Gazelle that includes the license plugin...
gazelle_tools = use_repo_rule("@gazelle_supply_chain//:tools_repo.bzl", "gazelle_tools_with_license")
gazelle_tools(name = "gazelle_with_license")
# 2. ...and make go_repository run it instead of the stock one.
non_module_deps = use_extension("@gazelle//internal/bzlmod:non_module_deps.bzl", "non_module_deps")
override_repo(
non_module_deps,
bazel_gazelle_go_repository_tools = "gazelle_with_license",
)Notes:
gazelle_tools_with_licensebuilds Gazelle with the hostgo(the same requirement as the stock tools repo) and takes the Gazelle version from thegazellemodule you depend on (v1 and v2 releases are handled).go_depspassesbuild_extra_argsto the Gazelle run of each module.gazelle_default_attributessets them for every module, andgazelle_overridereplaces them for one module, so repeat the flags that must stay (such as-license_default).- A module that ships its own BUILD files keeps them: pass
-lang=licensein itsbuild_extra_argsto only append the license tags, as the BuildBuddy override inexamples/MODULE.bazeldoes. - The license targets are generated in the fetched repository, so after changing flags Bazel refetches the affected modules.
bazel test //...