Skip to content

About

Bazel rules for the Coco language

Topics

Resources

Stars

2 stars

Watchers

1 watching

Forks

Repository files navigation

rules_coco

Bazel rules for the Coco language that can use Popili to:

  • Generate code from Coco packages.
  • Run verification on Coco packages as part of Bazel test.

The rules are fully compatible with remote caching and execution.

Requirements

  • Bazel 8.0.0 or higher.
  • Popili 1.5.0 or higher.
  • A valid Coco/Popili license.

Setup

Follow the installation instructions from the latest release.

bzlmod (Recommended)

Add the following to your MODULE.bazel file:

bazel_dep(name = "rules_coco")
archive_override(
    module_name = "rules_coco",
    urls = [
        "https://github.com/cocotec/rules_coco/releases/download/VERSION/rules_coco_VERSION.tar.gz",
        "https://dl.cocotec.io/rules_coco/rules_coco_VERSION.tar.gz",
    ],
    integrity = "sha256-HASH",  # See releases page
)

coco = use_extension("@rules_coco//coco:extensions.bzl", "coco")
coco.toolchain(
    versions = ["stable"],  # Or specify explicit versions like ["1.5.1"]
    c = True,   # Enable C runtime (for coco_c_library)
    cc = True,  # Enable C++ runtime (for coco_cc_library)
)

Every module declaring coco.toolchain contributes its versions and runtimes. When no module in the dependency graph declares one, stable is registered with both runtimes.

Get the exact version and integrity hash from the releases page.

WORKSPACE (Deprecated)

Warning

Beware that WORKSPACE mode is deprecated and will be removed in a future version. Please migrate to bzlmod.

Add the following to your WORKSPACE file:

load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")

http_archive(
    name = "rules_coco",
    integrity = "sha256-HASH",  # See releases page
    urls = [
        "https://github.com/cocotec/rules_coco/releases/download/VERSION/rules_coco_VERSION.tar.gz",
        "https://dl.cocotec.io/rules_coco/rules_coco_VERSION.tar.gz",
    ],
)

load("@rules_coco//coco:repositories.bzl", "coco_repositories")

coco_repositories(
    versions = ["stable"],  # Or explicit versions, e.g. ["1.5.1"] or ["1.5.1", "1.5.0"]
    c = True,   # Enable C runtime (for coco_c_library)
    cc = True,  # Enable C++ runtime (for coco_cc_library)
)

Get the exact version and integrity hash from the releases page.

In WORKSPACE mode the repository must be named rules_coco: the generated toolchain hub refers to @rules_coco//:version and @rules_coco//coco:toolchain_type by absolute label, and repositories created by a repository rule have no repository mapping to rewrite them.

Configuration

Licensing

To use the rules, the host where the build actions executes will need to have a license. If the build runs locally, then this means that local host requires a license. If remote execution is being used, then the remote executor will need a license.

When using these rules in a CI environment you will need to provide a machine token. This can either be done by:

  • (Preferred) Using workload identity federation: in this, tokens from your existing identity provider can be used to authenticate to Cocotec. This works with AWS or any OIDC-compatible identity provider, such as Github Actions and Google Cloud.
  • Using a secret API token for the Cocotec services.

Contact Cocotec Support to set this up or discuss options.

rules_coco supports several different license modes:

  • action_environment: Suitable credentials will be provided in the execution environment of each action. What is supported will depend on the version of Popili, but could include COCOTEC_AUTH_TOKEN being injected into the build action environment via some non-bazel mechanism.

    This is the recommended mode when using remote execution, as it will not interfere with bazel's cache, and also keeps the license token most secure.

  • action_file: An auth token file path is provided that is guaranteed to be available in the execution environment of each action (e.g., mounted on remote runners). The path must be specified via --@rules_coco//:auth_token_path or in the toolchain configuration. Popili will be invoked with --machine-auth-token pointing to this file.

    This mode is suitable for remote execution environments where auth token files are pre-mounted or made available through other mechanisms. Requires popili 1.5.2 or later.

  • local_acquire: A license will be acquired on the local machine as part of the build using COCOTEC_AUTH_TOKEN. This is not compatible with remote execution.

  • local_user: The user's existing license on this machine will be reused. This is not compatible with remote execution. Each popili version gets the license file it reads (licenses_<n>.lic in popili's data directory, n being its licence protocol version); the local toolchain, of unknown version, gets the file of the newest protocol version, unless it sets license_protocol_version.

  • token: The explicitly provided token should be used as COCOTEC_AUTH_TOKEN. In this case, --@rules_coco//:license_token must be set as well. This works with remote execution but it is only recommended when using workload identity federation as COCOTEC_AUTH_TOKEN is not a secret in that case.

This can be configured in three ways (in order of precedence):

  1. Via command-line flag (highest precedence):

    bazel build --@rules_coco//:license_source=local_acquire //...
    
    # For action_file mode, also specify the auth token path:
    bazel build --@rules_coco//:license_source=action_file --@rules_coco//:auth_token_path=/path/to/token //...
  2. In MODULE.bazel (for bzlmod users):

    coco = use_extension("@rules_coco//coco:extensions.bzl", "coco")
    coco.toolchain(
        versions = ["stable"],
        license_source = "local_acquire",  # Optional: set repository default
        # license_token = "...",  # Optional: only needed when license_source = "token"
        # auth_token_path = "/path/to/token",  # Optional: only needed when license_source = "action_file"
    )
  3. In WORKSPACE (for WORKSPACE users):

    coco_repositories(
        versions = ["stable"],
        license_source = "local_acquire",  # Optional: set repository default
        # license_token = "...",  # Optional: only needed when license_source = "token"
        # auth_token_path = "/path/to/token",  # Optional: only needed when license_source = "action_file"
    )

The repository-level configuration (MODULE.bazel or WORKSPACE) sets a default that applies to all builds, while the command-line flag can be used to override it for specific builds. If neither is specified, the default is local_user.

⚠️ Security Warning for license_token:

The license_token parameter is available in repository configuration for convenience, but:

  • The license_token parameter should only be used in repository configuration when using workload identity federation, where the token is not a secret.
  • For secret tokens, use the command-line flag instead: --@rules_coco//:license_token=<token>, or better yet ensure that your bazel execution environment injects COCOTEC_AUTH_TOKEN directly into actions.

Popili Version

Register every Popili version you need; the first one is the default. In MODULE.bazel:

coco = use_extension("@rules_coco//coco:extensions.bzl", "coco")
coco.toolchain(
    c = True,
    cc = True,
    versions = ["1.5.0", "1.5.1"],
)

or, in WORKSPACE:

coco_repositories(
    c = True,
    cc = True,
    versions = ["1.5.0", "1.5.1"],
)

To register every stable release your rules_coco version knows of (1.5.0 onwards, no betas or release candidates), load AVAILABLE_STABLE_VERSIONS from @rules_coco//coco:repositories.bzl. It lists them newest first, so the newest is the default:

load("@rules_coco//coco:repositories.bzl", "AVAILABLE_STABLE_VERSIONS", "coco_repositories")

coco_repositories(
    c = True,
    cc = True,
    versions = AVAILABLE_STABLE_VERSIONS,
)

MODULE.bazel cannot load .bzl files, so in bzlmod list the versions manually in coco.toolchain.

Registering versions is cheap: Popili and its runtimes are downloaded only for the versions targets actually use. Two things to be aware of, though. The default is the newest version your rules_coco release knows of, so upgrading rules_coco moves unpinned targets to a new Popili. List explicit versions, or pin targets with popili_version (see below), if you need generated code and verification results to stay the same across upgrades. And bazel sync fetches every declared repository, so with this list it downloads every stable release for every platform.

Then choose the version per target with popili_version on the rules that run Popili: coco_generate, coco_verify_test, coco_fmt_test (its <name>.format binary formats with the same version), coco_architecture_diagram, coco_state_diagram and coco_counterexample_diagram. A pin applies to that target and everything beneath it, so the coco_package it reads is analysed with the same version. Aliases such as "stable" are allowed.

coco_cc_library, coco_cc_test_library, coco_c_library and coco_c_test_library take popili_version too. On a coco_cc_library it selects which version's runtime the library links, and it also applies to the unpinned coco_generate targets the library builds from. The library's own srcs, deps and gmock are not affected.

A coco_cc_library only builds from code generated by the same version. So if you pin a coco_generate, pin the coco_cc_library that uses it to the same version; otherwise the build fails with a message saying what to pin. You can skip this when the coco_generate is pinned to the version unpinned targets get anyway: the default, or the value of --@rules_coco//:version.

All the coco_generate targets one coco_cc_library builds from must use the same version. For code generated by several versions, use one coco_cc_library per version. A cc_binary that links coco_cc_library targets of different versions links the runtime of each version, so only do that when those runtimes can be linked together.

coco_package(
    name = "modern",
    srcs = ["modern/src/Example.coco"],
    package = "modern/Coco.toml",
)

coco_generate(
    name = "modern_cpp",
    language = "cpp",
    package = ":modern",
    popili_version = "1.5.1",
)

coco_cc_library(
    name = "modern_cc",
    generated_package = ":modern_cpp",
    popili_version = "1.5.1",
)

coco_verify_test(
    name = "modern_verify",
    package = ":modern",
    popili_version = "1.5.1",
)

A coco_package can list the popili versions it is compatible with:

coco_package(
    name = "modern",
    srcs = ["modern/src/Example.coco"],
    package = "modern/Coco.toml",
    popili_versions = ["1.5.1"], # only popili 1.5.1 may be used with this package
)

A target that runs another version on the package fails, saying which pin to set or which version to add to the list. Matching is exact after alias resolution: "stable" gives the most recent stable version that your rules_coco version is aware of. Add "local" to the list for the local toolchain. A package checks the lists of its deps itself, whether or not it typechecks, and they check theirs, so the list of every transitive dep is checked too. Under --@rules_coco//:force_version (see below) the check only warns.

The list also steers the package's own build (bazel build //:modern, or beneath a consumer): it keeps the version it would get (the default, the flag, or a consumer's pin) when its list has it, and otherwise is analysed and typechecked with the first listed release that is registered, so order only matters then; local is never picked, only selected explicitly. A consumer pinned to the version the standalone build uses shares the package with it, so it is typechecked once. When no listed release is registered, the package fails saying which versions to register. A bring-your-own toolchain serves every target, so the list must name its version, and the flag and pins must still name that version or be left out. Under --@rules_coco//:force_version the package follows the forced version like everything else.

A coco_workspace claims versions for all of its members with the same attribute. A member without popili_versions of its own inherits the workspace's list, through nested workspaces' parent as well, and is checked against it; only a list written on the package itself steers the version the package is built with, though, since a transition cannot read another target. A member built on its own at a version outside an inherited list fails, naming the fix: select a listed version with --@rules_coco//:version, in .bazelrc for the whole repository, or write the list on the package. A member or nested workspace with a list of its own must include every version the workspace lists: it may support more, never fewer. To add a version, share the list through a .bzl constant:

# popili_versions.bzl
WORKSPACE_POPILI_VERSIONS = ["1.5.0", "1.5.1"]

# BUILD
coco_workspace(
    name = "workspace",
    popili_versions = WORKSPACE_POPILI_VERSIONS,
    workspace = "Coco.toml",
)

coco_package(
    name = "modern",
    srcs = ["modern/src/Example.coco"],
    package = "modern/Coco.toml",
    popili_versions = WORKSPACE_POPILI_VERSIONS + ["1.6.0"],
    workspace = ":workspace",
)

Everything without a pin uses the default, or the version --@rules_coco//:version selects, for example bazel build --@rules_coco//:version=1.5.1 //.... The flag takes a concrete version (or local); aliases such as stable are for pins only. A pin wins over the flag, unless --@rules_coco//:force_version is set (see below).

A pin equal to the version the build uses anyway (the flag, or the default when the flag is unset) costs nothing. A target pinned to any other version is built in a configuration of its own, so its outputs and test logs land under bazel-out/<cpu>-fastbuild-ST-<hash>/ rather than in bazel-bin and bazel-testlogs. In CI, collect test results through the Build Event Protocol (--build_event_json_file) or from bazel-out/*/testlogs/**/test.xml.

To try a version on the whole repository without editing BUILD files, add --@rules_coco//:force_version: every popili_version pin is ignored, and --@rules_coco//:version (or the default when it is unset) applies everywhere, for example bazel test --@rules_coco//:force_version --@rules_coco//:version=1.5.1 //....

See e2e/multi_version for a full example, in both MODULE.bazel and WORKSPACE form.

Custom rules

rules_coco has no public API for rules of your own that run popili. A hand-written rule that declares @rules_coco//coco:toolchain_type gets no popili_version, is not reached by --@rules_coco//:force_version when it transitions the version flag itself, and the fields of the toolchain it resolves are not a stable interface. CocoPackageInfo is public for rules that only read a package's files.

Using a local toolchain

To point at a popili toolchain on the local filesystem instead of a download add coco.local_toolchain to MODULE.bazel:

coco = use_extension("@rules_coco//coco:extensions.bzl", "coco")
coco.local_toolchain(
    popili = "/path/to/popili-dist",      # holds the popili + cocotec-licensing-server binaries
    cc_runtime = "/path/to/cpp-runtime",  # optional: holds the `coco/` subtree
    # c_runtime = "/path/to/c-runtime",   # optional: holds the `coco_c/` subtree
)

Each path (absolute, or workspace-relative) mirrors the extracted release archive layout — i.e. the contents of popili_<os>_<arch>.zip, coco-cpp-runtime.zip, and coco-c-runtime.zip respectively. To use it, run with bazel build --@rules_coco//:version=local //... (consider a .bazelrc --config).

Under local_user licensing, set license_protocol_version when your popili does not read the newest licence protocol version, e.g. license_protocol_version = 6 for popili 1.5.x.

For WORKSPACE mode use coco_local_repositories(path=..., cc_runtime_path=..., c_runtime_path=...), which behaves the same way: the local toolchain is selected by --@rules_coco//:version=local and never becomes the default, so a build that does not set the flag fails, saying that no default popili version is registered.

To register a local distribution alongside downloaded releases, pass it to coco_repositories instead, and switch between them with the version flag:

coco_repositories(
    cc = True,
    versions = ["1.5.1"],
    local_popili = "/path/to/popili-dist",
    local_cc_runtime = "/path/to/cpp-runtime",
)

See e2e/local_toolchain.

Bring your own toolchain

To point at a popili toolchain obtained from other bazel rules you can register your own coco_toolchain and skip rules_coco's toolchain machinery entirely. In MODULE.bazel you just need

register_toolchains("//tools:my_popili_tc")
# tools/BUILD.bazel
load("@rules_coco//coco:toolchain.bzl", "coco_toolchain")

coco_toolchain(
    name = "my_popili",
    coco = "//path/to:popili",
    cocotec_licensing_server = "//path/to:cocotec-licensing-server",
    # cc_runtime = "//path/to:cpp_runtime",  # optional cc_library, for coco_cc_library
    popili_version = "1.5.8",  # required: the version of that popili, not an alias
)

toolchain(
    name = "my_popili_tc",
    toolchain = ":my_popili",
    toolchain_type = "@rules_coco//coco:toolchain_type",
)

Use this when one popili, built in your repository, should run every target. A toolchain() without target_settings or constraints, like the one above, matches every target, so it replaces rules_coco's own toolchains and no pin or flag can select another one. Register only one such toolchain, since one registered after it would never be selected; in WORKSPACE, register it before calling coco_repositories.

popili_version is the version of the popili binary the toolchain points at, such as "1.5.8"; an alias such as "stable" is rejected. It picks the licence file under local_user licensing, and every target must ask for that version or for none in particular. A target pinned to another version, or built with --@rules_coco//:version set to another version, fails instead of running this popili:

@@//a:gen needs popili 1.5.1 (pinned by popili_version), but the bring-your-own coco_toolchain @@//tools:my_popili, which serves every target, is popili 1.5.8. ...

See e2e/byo_toolchain.

Using an older C++ compiler (Boost libraries)

Most users can skip this section. The Coco C++ runtime compiles cleanly on any toolchain that supports C++11 or later. It also includes limited support for GCC 4.x but requires some Boost dependencies to support this compiler.

rules_coco does not supply its own rules_boost; instead you need to supply your own library targets if you want to ensure compatibility with older compilers. In rules_boost terms the two targets are @boost//:integer and @boost//:type_traits.

Every build uses GCC 4.x

Depend on Boost unconditionally:

coco = use_extension("@rules_coco//coco:extensions.bzl", "coco")
coco.toolchain(versions = ["1.5.1"], cc = True)
coco.cc_runtime_deps(
    version = "1.5.1",
    deps = ["@boost//:integer", "@boost//:type_traits"],
)

Some builds use GCC 4.x, some don't

MODULE.bazel is evaluated before select() fires, so the conditional lives in a target you own:

# MODULE.bazel
coco.cc_runtime_deps(version = "1.5.1", deps = ["//third_party/boost_shim"])
# third_party/boost_shim/BUILD.bazel
cc_library(name = "empty")
cc_library(
    name = "boost_bundle",
    deps = ["@boost//:integer", "@boost//:type_traits"],
)

alias(
    name = "boost_shim",
    actual = select({
        "//config:old_libstdcxx": ":boost_bundle",
        "//conditions:default": ":empty",
    }),
    visibility = ["//visibility:public"],
)

//config:old_libstdcxx is whatever config_setting you already use to identify old-compiler builds — typically a platform constraint so the select() fires automatically off --platforms:

# //config/BUILD.bazel
constraint_setting(name = "libstdcxx_era")
constraint_value(name = "pre_gcc5", constraint_setting = ":libstdcxx_era")
config_setting(name = "old_libstdcxx", constraint_values = [":pre_gcc5"])

Same as above, and keep rules_boost out of modern-compiler workspaces

The shim above still pulls rules_boost into every workspace, because @boost//:integer is named in a select() branch Bazel must load. A label_flag defers the reference until .bazelrc sets it:

# third_party/boost_shim/BUILD.bazel
cc_library(name = "empty")
label_flag(
    name = "boost_shim",
    build_setting_default = ":empty",
    visibility = ["//visibility:public"],
)
# .bazelrc — only old-compiler configs set the flag
build:old_compiler --//third_party/boost_shim=@boost//:integer

In a real build, point the flag at an aggregate covering both @boost//:integer and @boost//:type_traits.

A coco.local_toolchain runtime is addressed as version "local":

coco.cc_runtime_deps(version = "local", deps = ["//third_party/boost_shim"])

WORKSPACE mode

A flat list applies to every registered version:

coco_repositories(
    versions = ["1.5.1"],
    cc = True,
    cc_runtime_extra_deps = ["@my_workspace//third_party/boost_shim"],
)

A dict targets one version at a time, mirroring bzlmod's per-version coco.cc_runtime_deps tag:

coco_repositories(
    versions = ["1.5.1", "1.5.0"],
    cc = True,
    cc_runtime_extra_deps = {
        "1.5.1": ["@my_workspace//third_party/boost_shim"],
        "1.5.0": ["@my_workspace//third_party/boost_shim_old"],
    },
)

Naming a version that was not registered is an error. Unlike the bzlmod tag, these labels are not resolved against your workspace before being written into the generated runtime repository, so they must be repository-absolute (@my_workspace//..., not //...).

Which target you name is an independent choice: the three patterns above describe that, and all three work here unchanged — Boost directly, a cc_library wrapping a select(), or a label_flag. Only the spelling differs between the two modes.

Verification Backend

The verification backend can be selected using the --@rules_coco//:verification_backend option:

  • local: Local verification (default). Runs on the build executor.
  • remote: Remote verification: uses the configured remote verification service.
  • attempt-remote: Attempt remote, fallback to local if it's not available.

For example:

bazel build --@rules_coco//:verification_backend=remote //...

Usage

Defining Packages

For each Coco.toml file add the following to your BUILD file:

load("@rules_coco//coco:defs.bzl", "coco_package")

coco_package(
    name = "my_package",
    srcs = glob(["*.coco"]),
    package = "Coco.toml",
)

If the Coco.toml file has dependencies then these need to be mirrored in the BUILD file:

load("@rules_coco//coco:defs.bzl", "coco_package")

coco_package(
    name = "my_package",
    srcs = glob(["*.coco"]),
    package = "Coco.toml",
    deps = [
        "//my:other_pkg",
    ]
)

To enable type checking as part of the build, add typecheck = True:

coco_package(
    name = "my_package",
    srcs = glob(["*.coco"]),
    package = "Coco.toml",
    typecheck = True,  # Validates types before code generation
)

When enabled, any target depending on this package (such as coco_generate) will wait for typecheck to pass.

Where Generated Code Goes

coco_generate writes all generated code, including test code and mocks, to a directory of its own:

bazel-bin/<package path>/_coco/<generate name>/<includePrefix>/<path below the source root>

<package path> is the directory of the BUILD file declaring the coco_generate (below external/<repo>/ for another repository) and <generate name> is its name. Every library built from the target gets the part up to the name as a quote include directory, so a header is included by what follows it: the package's includePrefix, empty by default, then the path of its source relative to its source root. That is how popili's own generated code includes it, so hand-written code includes it the same way and coco_cc_library and coco_c_library need no includes for generated headers.

With coco_generate(name = "motor_cpp") in hw/motor/BUILD, sources = ["src"] and includePrefix = "motor", hw/motor/src/Foo.coco becomes bazel-bin/hw/motor/_coco/motor_cpp/motor/Foo.h, included as #include "motor/Foo.h". Mirror includePrefix on the coco_generate with include_prefix, like the other settings under Output Paths.

outputDirectory and testOutputDirectory in Coco.toml are ignored. If one package is generated for several languages, give each coco_generate different file extensions (see Output Paths) so that their files do not clash.

Note

rules_coco 0.3 included generated headers by the path of their source file (#include "pkg/src/Foo.h"). See the changelog for how to update your include paths.

To predict these paths, coco_package needs the source roots. If all srcs (or test_srcs) are below one directory, that directory is used. Otherwise, for example when a package has several roots, set source_roots (or test_source_roots):

coco_package(
    name = "my_package",
    srcs = glob([
        "src/**/*.coco",
        "interfaces/**/*.coco",
    ]),
    package = "Coco.toml",
    source_roots = [
        "src",
        "interfaces",
    ],  # Must match package.sources in Coco.toml
)

Generating Code

C++ Code Generation

To generate C++ code:

load("@rules_coco//coco:defs.bzl", "coco_generate")

coco_generate(
    name = "my_package_cc_src",
    language = "cpp",
    package = ":my_package",
)

This is compiled using coco_cc_library, which adds the C++ runtime as a dependency:

load("@rules_coco//coco:cc.bzl", "coco_cc_library")

coco_cc_library(
    name = "my_package_cc",
    generated_package = ":my_package_cc_src",
)

If your project wraps cc_library in a macro of its own, for example to add default copts, pass it as library_rule and coco_cc_library calls it instead of rules_cc's cc_library, with the same attributes:

load("@rules_coco//coco:cc.bzl", "coco_cc_library")
load("//tools:defs.bzl", "my_cc_library")

coco_cc_library(
    name = "my_package_cc",
    generated_package = ":my_package_cc_src",
    library_rule = my_cc_library,
)

Important

Your Coco.toml file must set generator.cpp.runtimeHeaderFileExtension to .h if you use a custom value for generator.cpp.headerFileExtension.

C Code Generation

To generate C code:

load("@rules_coco//coco:defs.bzl", "coco_generate")

coco_generate(
    name = "my_package_c_src",
    language = "c",
    package = ":my_package",
)

This can be compiled using coco_c_library:

load("@rules_coco//coco:c.bzl", "coco_c_library")

coco_c_library(
    name = "my_package_c",
    generated_package = ":my_package_c_src",
)

Important

Your Coco.toml file must set generator.c.runtimeHeaderFileExtension to .h if you use a custom value for generator.c.headerFileExtension.

Output Paths

Bazel has to be able to precompute the output paths of all rules. Since there are many settings in Coco.toml that can affect output paths, these settings must be repeated in the BUILD file. If you forget to do this, you will get errors when running coco_generate such as:

ERROR: example/BUILD:10:14: output 'example/Example.h' was not created
ERROR: example/BUILD:10:14: Generating cpp example_test failed: not all outputs were created or valid

If you see these errors then you need to make sure that the BUILD files contain mirrors of the settings in the Coco.toml file. For example, suppose you have the following Coco.toml file.

[generator.cpp]
headerFileExtension = ".hpp"
implementationFileExtension = ".cpp"
fileNameMangler = "LowerUnderscore"
includePrefix = "motor"

Then this would require the following in your BUILD file:

coco_generate(
    name = "my_package_cc_src",
    language = "cpp",
    package = ":my_package",
    cpp_header_file_extension = ".hpp",        # Must match Coco.toml
    cpp_implementation_file_extension = ".cpp", # Must match Coco.toml
    cpp_file_name_mangler = "LowerUnderscore", # Must match Coco.toml
    include_prefix = "motor",                  # Must match Coco.toml
)
Coco.toml Setting coco_generate Attribute
generator.c.headerFileExtension c_header_file_extension
generator.c.implementationFileExtension c_implementation_file_extension
generator.c.headerFilePrefix c_header_file_prefix
generator.c.implementationFilePrefix c_implementation_file_prefix
generator.c.fileNameMangler c_file_name_mangler
generator.c.flatFileHierarchy c_flat_file_hierarchy
generator.c.includePrefix include_prefix
generator.c.regeneratePackages c_regenerate_packages
generator.cpp.headerFileExtension cpp_header_file_extension
generator.cpp.implementationFileExtension cpp_implementation_file_extension
generator.cpp.headerFilePrefix cpp_header_file_prefix
generator.cpp.implementationFilePrefix cpp_implementation_file_prefix
generator.cpp.fileNameMangler cpp_file_name_mangler
generator.cpp.flatFileHierarchy cpp_flat_file_hierarchy
generator.cpp.includePrefix include_prefix
generator.cpp.regeneratePackages cpp_regenerate_packages
generator.csharp.regeneratePackages csharp_regenerate_packages

Mirror the package's effective value: the one in its own Coco.toml, or else the one it inherits from its workspace's Coco.toml or receives from a dependency with exportConfig = true (see the popili documentation on includePrefix). A wrong include_prefix fails when compiling the package's own generated code, as popili wrote its includes with the Coco.toml value:

fatal error: 'motor/Foo.h' file not found

The path in the message names the value to set. Keep the two equal even for a package nothing depends on yet: a small package may compile despite a mismatch, as its sources find headers in their own directory, and the packages that later depend on it fail instead.

Two unrelated packages may both have src/Types.coco. Hand-written code including "Types.h" gets the copy from whichever include directory comes first on the compiler's command line. Generated code finds its own package's copy, as the including file's directory is searched first, but with includeGuardStyle = "IncludeGuard" both copies share the guard TYPES_H, so a file that reaches both through generated headers silently loses the second. Give one of the packages an includePrefix. The same holds for a dependency regenerated through regeneratePackages under the same prefix as the original: link only one copy. A hand-written Foo.h beside a .cc file shadows a generated one, for the same reason. shortenIncludes = "SameDirectory" does not work across packages; leave it unset.

C# Code Generation

To generate C# code:

load("@rules_coco//coco:defs.bzl", "coco_generate")

coco_generate(
    name = "my_package_csharp_src",
    language = "csharp",
    package = ":my_package",
)

The generated files are written to bazel-bin/<package path>/_coco/<generate name>/<path below the source root>, see Where Generated Code Goes.

Note: C# code generation produces .cs files but does not include compilation support. Bazel's C# rules are currently too primitive to provide a good integration. The generated C# files can be consumed by other build systems or IDEs.

Verification

These rules can be used to create bazel test targets that execute the verification when bazel test is executed.

load("@rules_coco//coco:defs.bzl", "coco_verify_test")

coco_verify_test(
    name = "my_package_test",
    package = ":my_package",
)

Formatting

Format checking can be integrated into your test suite using coco_fmt_test:

load("@rules_coco//coco:defs.bzl", "coco_fmt_test")

coco_fmt_test(
    name = "my_package_fmt_test",
    package = ":my_package",
)

This creates two targets:

  • my_package_fmt_test: Test that fails if code isn't formatted (bazel test)
  • my_package_fmt_test.format: Binary to format code in-place (bazel run)

Diagram Generation

rules_coco provides rules for generating diagrams from Coco code:

Architecture Diagrams

Generate component architecture diagrams showing the structure and connections of your components:

load("@rules_coco//coco:defs.bzl", "coco_architecture_diagram")

# Generate diagram for a single component
coco_architecture_diagram(
    name = "my_component_arch",
    package = ":my_package",
    components = {
        "my_component.svg": "MyComponent",
    },
    port_names = True,
    port_types = True,
)

# Generate diagrams for multiple components
coco_architecture_diagram(
    name = "all_components_arch",
    package = ":my_package",
    components = {
        "component1.svg": "Component1",
        "component2.svg": "Component2",
        "component3.svg": "Component3",
    },
)

State Machine Diagrams

Generate state machine diagrams:

load("@rules_coco//coco:defs.bzl", "coco_state_diagram")

coco_state_diagram(
    name = "my_state_machine",
    package = ":my_package",
    targets = ["MyComponent.MyMachine"],
    separate_edges = False,
)

Note: State diagram generation currently requires packages with a single .coco file.

Counterexample Diagrams

Generate sequence diagrams for verification failures. You must specify the expected counterexamples as a dict mapping output filenames to target declarations:

load("@rules_coco//coco:defs.bzl", "coco_counterexample_diagram")

coco_counterexample_diagram(
    name = "my_counterexamples",
    package = ":my_package",
    counterexamples = {
        "alarm_failure.svg": "Alarm",
        "safety_violation.svg": "SafetyChecker",
    },
    deterministic = True,
)

For filtering by specific assertions, use the counterexample_options() helper:

load("@rules_coco//coco:defs.bzl", "coco_counterexample_diagram", "counterexample_options")

coco_counterexample_diagram(
    name = "specific_counterexamples",
    package = ":my_package",
    counterexamples = {
        "safety.svg": counterexample_options(
            decl = "SafetyChecker",
            assertion = "Well-formedness",
        ),
        "liveness.svg": counterexample_options(
            decl = "LivenessChecker",
            assertion = "Implements Provided Port",
        ),
    },
)

API Reference

For detailed API documentation of all rules, macros, and their attributes, see the auto-generated documentation:

License

Copyright 2019- Cocotec Limited. Licensed under the Apache License, Version 2.0.

About

Bazel rules for the Coco language

Topics

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages