Bazel’s flagsets, or PROJECT.scl, are an approach aimed at solving project-specific flags in a monorepo. It leverages Starlark as its language, or more precisely, a subset of Starlark. This is in contrast to the traditional .bazelrc, which uses its own line-based language and has no formal specification.

Current state of the art

At the time of writing this, we are almost certainly all using .bazelrc files to hide away command line flags as well as to define configs (de facto sets of flags). My take is that .bazelrc is fine and will continue to work well for many people in a multi-repo setup. However, .bazelrc not only does not scale well in monorepos (yes, it is possible to compose them via import) but it can also lead to incorrect builds during day-to-day development.

Say we are working in a monorepo with both iOS and Android apps and we want to build iOS by executing:

bazel build //ios:app

At first glance, nothing seems wrong. However, all the flags that we use for Android are also applied when building iOS, and vice versa. Granted, this can be fine, but there might be a feature flag for C++ that both platforms use in different ways. In that scenario we always need to make sure that we explicitly apply the given flag differently for each platform or find some other way.

I’m sure there are many more examples one could come up with.

The solution

Project-specific PROJECT.scl files, aka flagsets. They were introduced at BazelCon 2025 in Bazel 9 as an experimental feature. However, they are currently not behind a feature flag, so if you are on Bazel 9 you can start using them today and help discover interesting corner cases.

Example iOS app

Imagine we are in a monorepo where each app is its own project (separate directory) like iOS/, Android/, and so on. So for the iOS app we would want to define dev and store configurations which obviously build the app in different ways. We would create a PROJECT.scl in the iOS subdirectory and give it the following contents:

load("//:project_proto.scl", "buildable_unit_pb2", "project_pb2")

project = project_pb2.Project.create(
    buildable_units = [
        # Since this buildable unit sets "is_default = True", these flags apply
        # to any target in this package or its subpackages by default. You can
        # also request these flags explicitly with "--scl_config=dev_config" or "--scl_config=store_config".
        buildable_unit_pb2.BuildableUnit.create(
            name = "dev_config",
            flags = [
                "--compilation_mode=dbg",
            ],
            is_default = True,
            description = "Default debug configuration used for development.",
        ),

        buildable_unit_pb2.BuildableUnit.create(
            name = "store_config",
            flags = [
                "--compilation_mode=opt",
                '--ios_multi_cpus=arm64',
            ],
            description = "Store configuration.",
        ),
    ],
)

I feel like this is pretty self-explanatory and demonstrates the basic idea. When building the app, Bazel 9 will take the PROJECT.scl closest to the target on the filesystem into account when applying flags. If we want to build for the store, the only thing required is to set --scl_config=store_config, much like --config in .bazelrc.

Enforcement policies

One of the more interesting features of flagsets is that we can define enforcement policies which basically can either:

  • WARN: warns you if you add additional flags via the command line or .bazelrc. This is the default, so it doesn’t have to be explicitly specified.
  • COMPATIBLE: disallows setting conflicting flags via the command line or .bazelrc, but allows flags that do not interfere with those specified in PROJECT.scl.
  • STRICT: disallows setting any flags via the command line or .bazelrc, so everything must be defined in the relevant PROJECT.scl.

To set one of the policies above, we use the enforcement_policy attribute on project_pb2.Project.create:

load("//:project_proto.scl", "buildable_unit_pb2", "project_pb2")

project = project_pb2.Project.create(
    enforcement_policy = "STRICT",
    buildable_units = [
        buildable_unit_pb2.BuildableUnit.create(
            name = "dev_config",
            flags = [
                "--compilation_mode=dbg",
            ],
            is_default = True,
            description = "Default debug configuration used for development.",
        ),

        buildable_unit_pb2.BuildableUnit.create(
            name = "store_config",
            flags = [
                "--compilation_mode=opt",
                '--ios_multi_cpus=arm64',
            ],
            description = "Store configuration.",
        ),
    ],
)

After building the target we would get the following output if additional flags are applied other than those in PROJECT.scl:

INFO: Reading project settings from //ios:PROJECT.scl.
ERROR: Cannot parse options: This build uses a project file (//:PROJECT.scl) that does not allow output-affecting flags in the command line or user bazelrc. Found ['--macos_minimum_os=12.6', '--flag_alias=build_python_zip=@@rules_python+//python/config_settings:build_python_zip', '--ios_simulator_version=18.5', '--flag_alias=incompatible_default_to_explicit_init_py=@@rules_python+//python/config_settings:incompatible_default_to_explicit_init_py', '--modify_execution_info=^(BitcodeSymbolsCopy|BundleApp|BundleTreeApp|DsymDwarf|DsymLipo|GenerateAppleSymbolsFile|ObjcBinarySymbolStrip|CppLink|ObjcLink|ProcessAndSign|SignBinary|SwiftArchive|SwiftStdlibCopy)$=+no-remote,^(BundleResources|ImportedDynamicFrameworkProcessor)$=+no-remote-exec', '--flag_alias=python_path=@@rules_python+//python/config_settings:python_path', '--features=swift.index_while_building', '--macos_minimum_os=13', '--incompatible_strict_action_env', '--features=swift.use_global_index_store', '--flag_alias=experimental_python_import_all_repositories=@@rules_python+//python/config_settings:experimental_python_import_all_repositories', '--host_macos_minimum_os=13', '--features=swift.use_global_module_cache']. Please remove these flags or disable project file resolution via --noenforce_project_configs.

Target-specific flags

Another interesting aspect of flagsets is the ability to set flags on a per-target basis. There is an attribute called target_patterns on buildable_unit_pb2.BuildableUnit.create:

            target_patterns = [
                "//target_specific:one",
            ],

The following patterns are supported (taken from Bazel documentation and examples):

  • //some:target (specific target)
  • -//some:target (exclude //some:target from this filter)
  • //some/path/... (all targets below a path)
  • -//some/path/... (exclude all targets below a path)

In conclusion

I believe this is a great step forward for improving how Bazel flags are managed. It is still very early days and there are many unanswered questions, such as what happens if a project depends on another project, and many others I probably haven’t even thought of yet. As time goes on, we will discover best practices and flagsets as a feature of Bazel will evolve to support more scenarios.

To learn more about the decisions that went into the design of flagsets, please see this talk on Youtube by the Bazel team.