Skip to content

About

Provides gazelle plugin to generate supply-chain related targets in build files

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

gazelle_supply_chain

🚧 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 the package_metadata generated by Gazelle, so any ruleset's repository rules that run Gazelle (such as go_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.

Directive

# 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 none stops 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 beats license_default there.
  • 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 no package(); see below); creating new BUILD files is governed by build_file_generation (go_repository).
  • The package_metadata target 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).

Strict mode

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 relaxed

Relaxed 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.bazel for go.yaml.in/yaml/v3);
  • nothing declares its license: the repo root needs license_default, and a subdirectory needs a license_subtree covering it or a license_default in 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 detect declaration 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.

Detecting the license of every module

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.

Which files are scanned

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".

Generated license targets

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't a-z or 0-9 replaced by _, and leading and trailing _ trimmed. LICENSE gives gazelle_generated_license, LICENSE.txt gives gazelle_generated_license_txt, LICENSE-APACHE gives gazelle_generated_license_apache, and COPYING gives gazelle_generated_copying. If the directory has no matching file, a single gazelle_generated_license is written without text.
  • license_kind() (only for LicenseRef- ids): gazelle_generated_kind.
  • package_metadata() (only when created, see below): the name in the declared label, for example package_metadata in //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.

Detectors and confidence

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 85

Both 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 LICENSE

A 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).

Usage

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.

External repos (go_deps)

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_license builds Gazelle with the host go (the same requirement as the stock tools repo) and takes the Gazelle version from the gazelle module you depend on (v1 and v2 releases are handled).
  • go_deps passes build_extra_args to the Gazelle run of each module. gazelle_default_attributes sets them for every module, and gazelle_override replaces 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=license in its build_extra_args to only append the license tags, as the BuildBuddy override in examples/MODULE.bazel does.
  • The license targets are generated in the fetched repository, so after changing flags Bazel refetches the affected modules.

Development

bazel test //...

About

Provides gazelle plugin to generate supply-chain related targets in build files

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages