rules_haskell

repository·master·Indexed 43 days ago

https://github.com/tweag/rules_haskell

Bazel build rules for Haskell that enable scalable, multi-language builds. Supports GHC toolchains via binary distributions and Nixpkgs, including cross-compilation to ARM. Includes debugging utilities like ldd_test for analyzing ELF binary dependencies and examples for managing Hackage dependencies via Cabal-wrapped rules.

Tokens
16.2K
Snippets
65
Records
76
Agent score
54%

What's inside rules_haskell

  1. Overview of the `primitive` package

    master
    The primitive package provides various primitive memory-related operations for Haskell. It is used for low-level memory manipulation and is a foundational package for many Haskell libraries. For detailed API documentation and package information, refer to the official Hackage page.
  2. Explore rule_haskell examples

    master

    The rules_haskell repository provides several example workspaces to demonstrate different Haskell build scenarios with Bazel:

    • vector: Demonstrates how to build a Hackage package (specifically vector) and its transitive dependencies (primitive, transformers) without using Cabal.
    • cat_hs: A non-trivial application example featuring multiple third-party Hackage dependencies, C library dependencies, and a project structure split into multiple libraries and a binary. This example uses a rule that wraps Cabal to manage Hackage dependencies.
    • rts: Demonstrates how to handle foreign exports and how to link against GHC's Runtime System (RTS) library (e.g., libHSrts.so).
  3. How ghc-paths resolves GHC installation paths

    master

    This Bazel-compatible version of ghc-paths resolves GHC paths using the following priority:

    1. Environment Variables: It checks for the following variables:
      • RULES_HASKELL_GHC_PATH
      • RULES_HASKELL_GHC_PKG_PATH
      • RULES_HASKELL_LIBDIR_PATH
      • RULES_HASKELL_DOCDIR_PATH
    2. Bazel Runfiles: If the environment variables are empty, it queries the Bazel runfiles tree using @rules_haskell//tools/runfiles.

    Note on Implementation: The paths are exposed as pure String constants using unsafePerformIO. The values are memoized upon the first lookup and will not change even if the environment changes later during the process lifetime.

  4. Manage target visibility across packages

    master

    By default, Bazel targets are only visible to other targets within the same BUILD.bazel file (the same package). To allow a target in one package to be used by a target in a different package, you must use the visibility attribute.

    Example: Making a library visible to a specific package

    In lib/BUILD.bazel:

    haskell_library(
        name = "booleans",
        srcs = ["Bool.hs"],
        visibility = ["//main:__pkg__"],
    )

    In main/BUILD.bazel, you can then reference it using its full label:

    haskell_binary(
        name = "demorgan",
        srcs = ["Main.hs"],
        deps = ["//lib:booleans"],
    )
  5. How to discover Bazel runfiles in Haskell

    master

    The Bazel.Runfiles library provides utilities to locate files within a Bazel runfiles tree. This is essential for tests or binaries that need to access data files or executables bundled with the Bazel target. You first create a runfiles handle using Runfiles.create, and then use Runfiles.rlocation to resolve the path to a specific file relative to the workspace root.

    import qualified Bazel.Runfiles as Runfiles
    
    main :: IO ()
    main = do
        r <- Runfiles.create
        -- Resolve path using rlocation
        path <- Runfiles.rlocation r "rules_haskell/tools/runfiles/test-data.txt"
        content <- readFile path
        putStrLn content
  6. Understand Bazel labels for referencing targets

    master

    In BUILD.bazel files and when using the Bazel CLI, you reference targets using labels. The syntax depends on the scope of the target you are referencing:

    • Full Label: //path/to/package:target-name (Used to reference a target in a different package).
    • Package-relative Label: //:target-name (Used to reference a target within the same package, skipping the path).
    • Local Label: :target-name (Used to reference a target within the same BUILD.bazel file, skipping the workspace root identifier //).

    For rule targets, path/to/package is the directory containing the BUILD.bazel file, and target-name is the value of the name attribute. For file targets, path/to/package is the package root, and target-name is the name of the file including its path.

    //path/to/package:target-name
  7. Benefits of using Bazel for large-scale builds

    master

    Bazel provides several advanced features that become essential as projects grow in size and complexity. These features rely on hermetic builds (builds that do not depend on the host's system configuration) and sandboxing (ensuring all inputs to build actions are explicitly declared).

    Key benefits include:

    • Hermeticity & Reproducibility: Guarantees that builds are identical across different developer machines and OS configurations by isolating them from host system libraries and settings.
    • Sandboxing: Prevents build correctness bugs by forcing all inputs to be properly declared, reducing the need to wipe build caches to fix broken states.
    • Distributed Build Caches: Allows fast builds from fresh checkouts by downloading artifacts from a central cache (often populated by CI), making builds network-bound rather than CPU-bound.
    • Distributed Execution: Enables scaling build times by distributing actions across multiple machines, keeping build times relatively constant as the codebase grows.
    • Test Result Caching: Minimizes CI time by only rerunning tests that depend on code changes.
  8. Enable incremental builds using haskell_module

    master

    By default, rules_haskell rebuilds all modules in a library if any module changes because it does not analyze source code dependencies. To enable incremental builds and avoid unnecessary recompilation, you can use the haskell_module rule (from @rules_haskell//haskell/experimental:defs.bzl) to explicitly define module-level dependencies.

    When using haskell_module, you must use the modules attribute in haskell_library instead of srcs.

    Dependencies for a haskell_module can be:

    1. Internal modules: Listed in the deps attribute of the haskell_module rule.
    2. Library dependencies: Any library listed in the deps attribute of the enclosing haskell_library.
    3. Specific modules from other libraries: Listed in the cross_library_deps attribute of the haskell_module rule (requires the target library to be listed in narrowed_deps of the enclosing library).
    load("@rules_haskell//haskell:defs.bzl", "haskell_library")
    load("@rules_haskell//haskell/experimental:defs.bzl", "haskell_module")
    
    haskell_module(
        name = "LibMod1",
        src = "src/LibMod1.hs",
        src_strip_prefix = "src",
        deps = [":LibMod2"],
        enable_th = True,
    )
    
    haskell_module(
        name = "LibMod2",
        src = "src/LibMod2.hs",
        src_strip_prefix = "src",
    )
    
    haskell_library(
        name = "lib",
        modules = [
            "LibMod1",
            "LibMod2",
        ],
        deps = [
            "//:base",
            "//:template-haskell",
        ],
    )
  9. How the `ldd` utility works

    master

    The ldd(f, elf_path) function provides a structured, tree-based view of an ELF binary's dependencies, unlike the flat list provided by the standard ldd(1) tool.

    To use it, you pass a transformation function f which is applied to each dependency in the tree recursively from the bottom up. This allows you to programmatically filter, query, and augment the dependency tree to debug complex runtime linker failures.

    Each layer of the tree is represented by a DependencyInfo dictionary containing:

    • needed: A dictionary mapping dependency names to either LDD_MISSING, LDD_UNKNOWN, or a dictionary containing the dependency's own info (item) and where it was found (found_in).
    • runpath_dirs: A list of RunpathDir objects representing all runpath directories searched.
    import pprint
    
    # ldd(transformation_function, elf_binary_path)
    pprint.pprint(
        ldd(
            identity, 
            '/path/to/your/binary'
        )
    )
  10. Deciding between Cabal/Stack and Bazel for Haskell projects

    master

    Choosing a build system depends on your project's scale, language composition, and distribution requirements. Use the following rules of thumb to decide:

    Use Cabal or Stack if:

    • Your project is an independently publishable single library or a small set of libraries.
    • Your project is open source and has minimal static assets (suitable for Hackage).
    • Your project is almost entirely Haskell (with perhaps minimal C).
    • Your project relies heavily on Hackage packages but has few system-level dependencies (e.g., zlib, libpng).

    Use Bazel if:

    • Your project cannot be hosted on Hackage (e.g., games with large assets or proprietary code).
    • You are managing a very large codebase within a single repository (monorepo).
    • Your project is polyglot, involving two or more languages (e.g., Haskell/PureScript, Haskell/Java, or Haskell/C++/FORTRAN).
  11. Quickstart: Set up a Haskell project with rules_haskell

    master

    To quickly bootstrap a new Haskell project, use the provided installation script. This will generate the necessary WORKSPACE and BUILD files.

    Standard Setup

    Run the following command in a fresh directory:

    curl https://haskell.build/start | sh

    Bzlmod Setup

    If you want to start a project using Bazel's Bzlmod system, run:

    sh <(curl https://haskell.build/start) --with-bzlmod=true

    Nixpkgs Setup

    If you are on NixOS or want to use Nixpkgs to provision your GHC toolchain and fetch Hackage packages, use the --use-nix flag:

    sh <(curl https://haskell.build/start) --use-nix

    Once the files are generated, you can build and test your project using standard Bazel commands:

    bazel build //...
    bazel test //...
    curl https://haskell.build/start | sh
  12. Pin stack_snapshot dependencies for reproducibility

    master

    By default, stack_snapshot invokes stack on every fetch, which can be slow, require network access, and impact reproducibility. To enable pinning, which uses a lock file and allows for Bazel repository caching, follow these steps:

    1. Generate a lock file: bazel run @stackage-unpinned//:pin.
    2. Update your stack_snapshot definition to include the stack_snapshot_json attribute pointing to the generated file.
    stack_snapshot(
        ...
        stack_snapshot_json = "//:stackage_snapshot.json",
    )