tweag / gazelle_haskell_modules

A gazelle extension to generate haskell_module rules

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

gazelle_haskell_modules

Continuous Integration

This is a gazelle extension that generates haskell_module rules from haskell_library, haskell_binary, and haskell_test as defined in Haskell rules for Bazel. Moreover, it updates the dependencies of the generated rules whenever the import declarions are changed in the source files.

For each haskell_library, haskell_binary, and haskell_test rule, haskell_module rules are generated in the same BUILD file for all modules listed in the srcs attribute. For instance,

haskell_library(
    name = "lib",
    srcs = [
        "src/A/B.hs",
        "src/C/D.hs",
    ],
    deps = [":base"],
    ghcopts = ["-threaded"],
)

is updated to

haskell_library(
    name = "lib",
    modules = [
        "lib.A.B",
        "lib.C.D",
    ],
    deps = [":base"],
    ghcopts = ["-threaded"],
)

haskell_module(
    name = "lib.A.B",
    src = "src/A/B.hs",
    src_strip_prefix = "src",
    deps = ["lib.C.D"],
)

haskell_module(
    name = "lib.C.D",
    src = "src/C/D.hs",
    src_strip_prefix = "src",
)

This example project shows it in action.

Configuration

Firstly, setup gazelle and rules_haskell. Then import gazelle_haskell_modules.

http_archive(
    name = "io_tweag_gazelle_haskell_modules",
    strip_prefix = "gazelle_haskell_modules-main",
    url = "https://github.com/tweag/gazelle_haskell_modules/archive/main.zip",
)

Additionally, some Haskell packages(currently json) are needed to build gazelle_haskell_modules. The simplest way to bring them is to use the stack_snapshot rule in the WORKSPACE file as follows.

load("@rules_haskell//haskell:cabal.bzl", "stack_snapshot")
load("@io_tweag_gazelle_haskell_modules//:defs.bzl", "gazelle_haskell_modules_dependencies")
gazelle_haskell_modules_dependencies()

stack_snapshot(
    name = "stackage",
    packages = [
        "json",
    ],
    # Most snapshots of your choice might do
    snapshot = "lts-18.28",
)

gazelle_haskell_modules implicitly depends on the stackage external workspace. Should Haskell packages need to be grabbed from elsewhere, alternative labels can be provided to gazelle_haskell_modules_dependencies.

You can generate or update build rules by adding the following to one of your BUILD files.

load(
    "@bazel_gazelle//:def.bzl",
    "DEFAULT_LANGUAGES",
    "gazelle",
    "gazelle_binary",
)

gazelle(
    name = "gazelle",
    gazelle = ":gazelle_binary",
)

gazelle_binary(
    name = "gazelle_binary",
    languages = DEFAULT_LANGUAGES + ["@io_tweag_gazelle_haskell_modules//gazelle_haskell_modules"],
)

Using GHC version 9.2

Due to a regression in cabal, which badly handles relocatable build now, one has to use a patched version of cabal.

Hence, one should declare a snapshot.yaml file:

resolver: nightly-2022-06-06

packages:
- git: https://github.com/tweag/cabal
  commit: 42f04c3f639f10dc3c7981a0c663bfe08ad833cb
  subdirs:
  - Cabal

However, it then requires to explicit depend on the patched version of cabal for every module requiring it, hence the WORKSPACE file should contain lines like:

load("@rules_haskell//haskell:cabal.bzl", "stack_snapshot")
load("@io_tweag_gazelle_haskell_modules//:defs.bzl", "gazelle_haskell_modules_dependencies")
gazelle_haskell_modules_dependencies()

stack_snapshot(
    name = "stackage",
    setup_deps = {
        "transformers-compat": ["@stackage//:Cabal"],
        "hspec-discover": ["@stackage//:Cabal"],
        "call-stack": ["@stackage//:Cabal"],
        "HUnit": ["@stackage//:Cabal"],
        "quickcheck": ["@stackage//:Cabal"],
        "hspec-expectations": ["@stackage//:Cabal"],
        "quickcheck-io": ["@stackage//:Cabal"],
        "tasty-discover": ["@stackage//:Cabal"],
        "hspec-core": ["@stackage//:Cabal"],
        "bifunctors": ["@stackage//:Cabal"],
        "hspec": ["@stackage//:Cabal"],
    },
    packages = [
        "Cabal",
        "hspec",
    ],
    local_snapshot = "//:snapshot.yaml",
)

Running

Build and run gazelle with

bazel run //:gazelle

Gazelle's fix command can be used to delete haskell_module rules when they have no enclosing library, binary, or test. At the moment, the fix command only looks for enclosing rules in the same BUILD file containing the haskell_module rule.

Additionally, fix will also remove haskell_modules whose src files have been deleted, as well as removing those modules from places where they are referenced.

Rule generation

Finding source files

There are two ways for gazelle_haskell_modules to find source files containing modules.

Autodetect directive

By attaching the gazelle_haskell_modules:srcs: <folders..> directive to a rule, gazelle_haskell_modules will recursively search <folders..> to find Haskell source files to generate haskell_modules with. After finding files, gazelle_haskell_modules will proceed as if the files were manually specified, as documented below.

Example:

# gazelle_haskell_modules:srcs: src/
haskell_library(
    name = "package-c",
    ...
)

More examples of this usage can be found in package-c

If srcs are also explicitly specified in the rule, the results of the directive and the files listed in the explicit srcs are combined.

Specified in srcs

Each module listed in the srcs attribute of a Haskell rule originates a haskell_module rule with name <pkg>.<module>. The dependencies of the haskell_module rule are populated with labels corresponding to other haskell_module rules originating from the same library, binary, or test.

Dependencies of non-haskell_module rules

The srcs attributes of haskell_library, haskell_binary, and haskell_test are cleared. The modules attribute is populated with the labels of the corresponding haskell_module rules.

Updating dependencies of haskell_module rules

When the imports in a module are changed, the corresponding haskell_module rule might need to be updated. Removed imports are removed from the deps attribute, and added imports might originate new dependencies. Adding an import to a module that is defined in the current repo, will add that module to the dependencies if the importer and the imported come from the same library, binary, or test.

If no enclosing library, binary, or test can be found for a haskell_module rule, then it won't be updated.

Implementation

gazelle_haskell_modules extracts module imports from Haskell modules using himportscan, a command line tool written in Haskell, which presents the extracted data in json format to the go part.

The go part consists of a set of functions written in the go programming language, which gazelle will invoke to generate haskell_module rules. The most important functions are:

  • GenerateRules: calls the himportscan command-line tool and produces rules that contain no information about dependencies.

  • Imports: indexes the rules for dependency resolution.

  • Resolve: adds to the previously generated rules all the information about dependencies (deps, plugins, and tools).

Limitations

Support for hidden_modules

Currently, the generator allows modules to depend on hidden modules of dependencies. This is something that should be changed eventually so the generator fails in these cases.

CPP support

CPP directives in source files are ignored when scanning for imports. That is, himportscan would always pick up both imports in the next example.

#if COND
import SomeModule
#else
import SomeOtherModule
#endif

Preprocessor support

Imports may not be possible to extract in files that need preprocessors which generate those same imports (e.g. tasty-discover). In these cases, generation of haskell_module rules can be avoided by using # gazelle_haskell_modules:keep comments on the given rule.

# The contents of Spec.hs are generated by tasty-discover
# gazelle_haskell_modules:keep
haskell_test(
    name = "tests"
    srcs = [
        "tests/Main.hs",
        "tests/Spec.hs",
        "tests/Other.hs"
    ]
    deps = ...,
    tools = ["@stackage-exe//tasty-discover"],
)

In the above case, the rule won't originate any haskell_module rules.

export and reexported_modules

haskell_library has attributes export and reexported_modules which affect the dependencies of rules that depend on the Haskell library. gazelle_haskell_modules makes no effort to honor those attributes when generating rules or resolving imports.

Detection of TemplateHaskell

gazelle_haskell_modules detects modules that use TemplateHaskell by looking at the LANGUAGE pragmas and the ghcopts attribute of the haskell_module rule. But the internal or external interpreter could be activated by using ANN pragmas in the module source, or by using -XTemplateHaskell in the ghcopts attribute of the enclosing library. In these cases, enable_th won't be set on the haskell_module rule and complains about missing libraries or object files will ensue.

To workaround this, you could set enable_th = True manually on the haskell_module rule and use a #keep comment.

haskell_module(
    name = "...",
    ...
    #keep
    enable_th = True,
    ...
)

Sponsors

             Symbiont          Tweag I/O

gazelle_haskell_modules was funded by Symbiont and is maintained by Tweag I/O.

Have questions? Need help? Tweet at @tweagio.

About

A gazelle extension to generate haskell_module rules

License:Apache License 2.0


Languages

Language:Go 35.9%Language:Haskell 32.5%Language:Starlark 31.0%Language:Nix 0.5%