Skip to content

Add Bazel-style visibility() limiting for bzl loads - #1593

Open
cormacrelf wants to merge 10 commits into
meta:mainfrom
cormacrelf:load-visibility
Open

cormacrelf wants to merge 10 commits into
meta:mainfrom
cormacrelf:load-visibility

Conversation

@cormacrelf

@cormacrelf cormacrelf commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator

This essentially implements https://bazel.build/concepts/visibility#load-visibility.

Differences:

  1. Buck2's starlark allows top level if/else in bzl files. Very minor
  2. We support visibility(["//pkg:"]) but not visibility(["//pkg"]), matching the rest of buck2 visibility

Since no Buck2 code in existence uses visibility() as it didn't exist until now, and the default when omitted is equivalent to visibility("public"), we just enforce visibility by default. You can turn it off if you need to with [buck2] check_load_visibility = false.

Idea from #1287 (comment)

Note on package vs directory semantics

pkg
├── BUCK
├── defs.bzl
├── sub
│   └── defs.bzl
└── subpackage
    └── BUCK
  • Package semantics: visibility(["//pkg:"]) is visible from pkg/defs.bzl AND pkg/sub/defs.bzl but NOT pkg/subpackage/.... This is what bazel does.
  • Directory semantics: visibility(["//pkg:"]) is visible from pkg/defs.bzl BUT NOT any subdirectories or subpackages thereof. You would have to write //pkg/... which would include pkg/subpackage/BUCK subpackages. To include both //pkg and //pkg/sub, write both.

The original version of my code compared the cell paths' directories, not packages (dir trees rooted by BUCK files) -- this I was not attached to, but it matched how we write load()s. In bazel if you have pkg/BUILD and no other build files, you have load("//pkg:sub/defs.bzl", "my_rule") whereas we have load("//pkg/sub:defs.bzl", "my_rule"). Unfortunately combined with point (2) it can be a bit confusing because //pkg: is a target pattern rooted in pkg/BUCK and doesn't match //pkg/sub:*.bzl visually. So I'm not 100% sure what the best path is. Maybe if we wanted to match bazel's package visibility semantics we could add support for bazel's load syntax as well, which is in fact a literal one line change from FileName->ForwardRelativePath in an import parser? Not the biggest deal.

It makes more sense to me to be package based rather than directory. If you have a module of bzl code you can put some in a subdirectory called private/, write visibility("private") at the top of each, and then import those from a public interface. If directory-based, we would require you to write visibility("//pkg/...") in that very common scenario, "private" would be much less useful and this feature would be worse. In fact yeah, I think we should use package-based

Package-based will require some fancier dice keys so as not to be slower. I added some commits on the end that change this to package semantics and hopefully improve the perf of owner() and other similar queries.

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Oct 30, 2026
@meta-codesync

meta-codesync Bot commented Oct 30, 2026

Copy link
Copy Markdown
Contributor

This pull request has been imported. If you are a Meta employee, you can view this in D128510197. (Because this pull request was imported automatically, there will not be any future comments.)

@cormacrelf
cormacrelf force-pushed the load-visibility branch 2 times, most recently from 14b99d9 to 00ef912 Compare October 30, 2026 05:16
Add `HasBuildfileKey` and `EnclosingPackageKey`, which only change when a
buildfile is added or removed, and use them for `get_enclosing_packages`
(`owner()`) and the new `find_enclosing_package`, instead of reading every
directory up to the package.

The basic problem being solved is that checking load-visibility using
package semantics rather than directory semantics requires finding the
package of the importing bzl file and also the one it's importing. A
naive implementation would recursively read_dir each parent directory
until it found a BUCK/buildfile, but that would be bad because then every
single load would have to be re-checked whenever you added or removed
any file basically anywhere. This limits re-checking to the addition and
deletion of buildfiles i.e. changes to the package structure.

A subsequent commit will alter the semantics. For now, we swap out all
the places we currently try to find the owning buildfile. This should be
a perf win under fs changes.

- `get_enclosing_package` (bxl, `audit file-package`, `buildfile()`)
- `get_enclosing_packages` (`owner()`)

I ran a benchmark with large numbers of loads nested within a package
etc + package semantics (the later commit), and yeah you can make a
dumb benchmark where it's twice as fast at re-evaluating stuff. That
should help show we aren't introducing perf regressions.
Package semantics: visibility(["//pkg:"]) is visible from pkg/defs.bzl AND
pkg/sub/defs.bzl.

Directory semantics: visibility(["//pkg:"]) is visible from
pkg/defs.bzl BUT NOT subdirectories thereof. You would have to write //pkg/...
which would include pkg/subpackage/BUCK subpackages.

We choose package because:

> It makes more sense to me to be package based rather than directory. If you
> have a module of bzl code you can put some in a subdirectory called private/,
> write visibility("private") at the top of each, and then import those from a
> public interface. If directory-based, we would require you to write
> visibility("//pkg/...") in that very common scenario, "private" would be much
> less useful and this feature would be worse.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant