rules_scala

repository·master·Indexed 18 days ago

https://github.com/bazel-contrib/rules_scala

Core Bazel build rules for Scala projects, providing capabilities to build, test, and package Scala software at scale. It includes rules such as scala_library, scala_binary, scala_test, and scala_toolchain, as well as support for SemanticDB, protocol buffers via scala_proto_library, and combined coverage reports using lcov.

Tokens
30K
Snippets
86
Records
117
Agent score
62%

What's inside rules_scala

  1. Understand Scala dependency modes

    master

    The dependency_mode option determines which jars are included on the classpath during compilation. This helps balance between strictness and the need to satisfy scalac requirements.

    • direct: Only includes dependencies explicitly listed in the deps attribute. This can lead to cryptic errors if a transitive dependency is required by scalac but not explicitly declared.
    • plus-one: Includes deps and the immediate deps of those dependencies. This is the recommended balance, as it prevents most missing dependency errors without significantly increasing incremental build costs.
    • transitive: Includes all transitive dependencies (the entire dependency graph). This is the most permissive but results in the highest incremental build cost.

    Caveats for plus-one and transitive modes:

    • Extra builds: More dependencies act as inputs to the compilation action, potentially triggering more rebuilds when cross-ijar boundaries change.
    • Label propagation: Error messages for target labels may be less precise due to current limitations in how JavaInfo propagates labels.
  2. How phases and customizable rules work

    master

    In rules_scala, rule implementations are organized into a sequence of phases. Each phase represents a specific step (e.g., compilation, JAR creation).

    Phases provide two main benefits:

    1. Modularity: Breaking complex rule logic into smaller, readable groups.
    2. Customizability: Users can replace default phases with their own (e.g., using a custom Scala compiler) or extend the phase list (e.g., adding a Scala formatting check).

    Key Concepts:

    • Default Rules: If you don't need customization, simply load @rules_scala//scala:scala.bzl.
    • Customizable Rules: Rules prefixed with make_ (e.g., make_scala_binary) allow users to pass a configuration dictionary to modify the rule's behavior.
    • Phase Provider: ScalaRulePhase is used to pass custom phases into these rules.
  3. How the Phase Architecture works

    master

    To provide flexibility, rules_scala uses a phase architecture. Rule implementations are defined as a sequential list of phases. This allows:

    • Consumers to define new phases within their workspace to customize rules for specific use cases.
    • Contributors to add new default functionality by implementing new phases.
    • Clarity in understanding which steps are shared across different rules.
  4. How dependency providers and toolchains work together

    master

    In rules_scala, toolchains provide an indirection layer to configure dependencies (like compiler classpaths) without hardcoding labels. This is achieved through a pattern where dependencies are encapsulated in DepsInfo providers, which are then attached to a toolchain.

    When designing rules, the preferred approach is to use dependency providers on toolchains. This is suitable when a rule implementation is 'toolchain aware' and knows how to look up information from a toolchain.

    1. Define Providers: Use declare_deps_provider to create targets that hold a list of dependency labels. Each provider is associated with a deps_id used by rules to look up specific dependency sets (e.g., runtime_deps vs compile_deps).
    2. Create the Toolchain: Use declare_deps_toolchain to bundle these providers into a toolchain implementation.
    3. Register the Toolchain: Use the standard Bazel toolchain rule to map your implementation to a specific toolchain_type.
    load("@rules_scala//scala:providers.bzl", "declare_deps_provider")
    load("@rules_scala//scala/toolchains:toolchains.bzl", "declare_deps_toolchain")
    
    # 1. Declare the provider
    declare_deps_provider(
        name = "my_compile_deps_provider",
        deps_id = "compile_deps",
        visibility = ["//visibility:public"],
        deps = [
            "@com_lihaoyi_fastparse_2_12",
            "@org_scala_lang_scala_library",
        ],
    )
    
    # 2. Declare the toolchain implementation
    declare_deps_toolchain(
        name = "my_deps_toolchain_impl",
        dep_providers = [":my_compile_deps_provider"],
        visibility = ["//visibility:public"],
    )
    
    # 3. Register the toolchain
    toolchain(
        name = "my_deps_toolchain",
        toolchain = ":my_deps_toolchain_impl",
        toolchain_type = "//my_rules/toolchain:my_toolchain_type",
        visibility = ["//visibility:public"],
    )
  5. Define custom Scala toolchains without default Scala toolchains

    master

    If you are defining your own custom Scala toolchain using setup_scala_toolchain() (with custom compiler JARs) and do not want to instantiate the default Scala toolchain or compiler JAR repositories, follow these rules:

    • Bzlmod: Only instantiate the specific tag classes you need from the scala_deps extension.
    • WORKSPACE: Set scala = False in the scala_toolchains() call.

    This prevents version check failures and avoids unnecessary repository instantiation.

    # WORKSPACE: Disable default Scala toolchain
    scala_toolchains(
        scala = False,
        scala_proto = True,
        twitter_scrooge = True,
        # ...other toolchain parameters...
    )
  6. Access data from previous phases

    master

    The second argument p in a phase function is a global provider. It accumulates information from all previous phases. You can access data from a previous phase using the pattern p.<PHASE_NAME>.<FIELD_NAME>.

    Example: If a previous phase named jar (with phase_name="jar") returns:

    return struct(
        class_jar = class_jar,
        ijar = ijar,
    )

    You can access these values in your current phase via p.jar.class_jar or p.jar.ijar.

  7. How JaCoCo coverage works in rules_scala

    master

    Coverage is powered by the JaCoCo library, which is managed via rules_java and java_tools.

    rules_scala $\rightarrow$ rules_java $\rightarrow$ java_tools $\rightarrow$ JaCoCo.

    Because java_tools and rules_java are released independently of Bazel, the JaCoCo version may vary depending on your rules_java version. To find your current JaCoCo version, you can inspect the java_tools archive associated with your rules_java installation.

  8. Use strict dependency checking

    master

    The strict_deps_mode requires that any type referenced in Scala source code must be explicitly declared in the target's deps. This prevents relying on transitive dependencies that might disappear.

    Modes:

    • off: No checking.
    • warn: Issues a warning for violations.
    • error: Fails the build on violations.

    Handling Violations: When a violation occurs, you will receive an error message suggesting a buildozer command to fix it automatically:

    Target '//some_package:transitive_dependency' is used but isn't explicitly declared, please add it to the deps.
    You can use the following buildozer command:
    buildozer 'add deps //some_package:transitive_dependency' //some_other_package:transitive_dependency_user

    Note: This option only applies to Scala code. Java code in scala_library is still controlled by the standard --strict_java_deps flag.

  9. Use unused dependency checking

    master

    The unused_dependency_checker_mode ensures that all targets specified in deps are actually used in the code, helping to minimize the classpath and improve build caching.

    Modes:

    • off: No checking.
    • warn: Warns about unused dependencies.
    • error: Fails the build on unused dependencies.

    Handling Violations: If a dependency is unused, the error message provides a buildozer command to remove it:

    error: Target '//some_package:unused_dep' is specified as a dependency to //target:target but isn't used, please remove it from the deps.
    You can use the following buildozer command:
    buildozer 'remove deps //some_package:unused_dep' //target:target

    Configuration:

    • This can be enabled globally via a Scala toolchain.
    • It can be enabled for individual targets using the unused_dependency_checker_mode attribute.
    • If the checker incorrectly flags a target, you can exclude it using the unused_dependency_checker_ignored_targets attribute (a list of labels).
  10. Select the Scala version

    master

    You can manage Scala versions in rules_scala using three different approaches:

    1. Built-in Toolchains: Supports the last two released minor versions for Scala 2.11, 2.12, and 2.13. 2.12 is the default.
    2. Custom Toolchains: Define your own scala_toolchain by calling setup_scala_toolchain() with your specified dependencies. This is the preferred, more flexible method.
    3. Multiple Versions (Cross-compilation): Configure multiple Scala versions and use target-level control to decide which version a specific target uses.
  11. How interface jars (ijar) work in scala_library

    master

    By default, scala_library sets build_ijar = True. This generates an interface jar that contains only the signatures of the compiled code, not the implementation. This prevents downstream targets from recompiling if you only change the internal implementation of a class without changing its public API.

    When to disable build_ijar: If you want to enable inlining of compiled code when it is used as a dependency for another Scala target, you must set build_ijar = False. Because an ijar contains no implementation, it cannot be used for inlining.

    Note for Macros: If you are writing macro code, do not use scala_library with build_ijar = False. Instead, use the specific scala_macro_library rule.