|
| 1 | +# gazelle\_haskell\_modules |
| 2 | + |
| 3 | +This is a [gazelle][gazelle] extension that generates `haskell_module` |
| 4 | +rules from `haskell_library`, `haskell_binary`, and `haskell_test` as |
| 5 | +defined in [Haskell rules][rules_haskell] for [Bazel][bazel]. |
| 6 | + |
| 7 | +For each `haskell_library` rule, `haskell_module` rules are generated |
| 8 | +in the same `BUILD` for all modules listed in the `srcs` attribute. |
| 9 | + |
| 10 | +This [example repo][example] shows it in action. |
| 11 | + |
| 12 | +Right now, this is work in progress. The features that are implemented |
| 13 | +include a scanner of imports in Haskell modules, and the generation of |
| 14 | +some `haskell_module` rules. See the **What's next** section below. |
| 15 | + |
| 16 | +## Configuration |
| 17 | + |
| 18 | +Firstly, setup [gazelle][gazelle] and [rules_haskell][rules_haskell]. |
| 19 | +Then import `gazelle_haskell_modules`. |
| 20 | + |
| 21 | +```python |
| 22 | +http_archive( |
| 23 | + name = "io_tweag_gazelle_haskell_modules", |
| 24 | + strip_prefix = "gazelle_haskell_modules-main", |
| 25 | + url = "https://github.com/tweag/gazelle_haskell_modules/archive/main.zip", |
| 26 | +) |
| 27 | +``` |
| 28 | + |
| 29 | +Additionally, some Haskell packages are needed to build |
| 30 | +`gazelle_haskell_modules`. The simplest way to bring them is to use the |
| 31 | +`stack_snapshot` rule in the `WORKSPACE` file as follows. |
| 32 | + |
| 33 | +```python |
| 34 | +load("@rules_haskell//haskell:cabal.bzl", "stack_snapshot") |
| 35 | +load("@io_tweag_gazelle_haskell_modules//:defs.bzl", "gazelle_haskell_modules_dependencies") |
| 36 | +gazelle_haskell_modules_dependencies() |
| 37 | + |
| 38 | +stack_snapshot( |
| 39 | + name = "stackage", |
| 40 | + packages = [ |
| 41 | + "aeson", # keep |
| 42 | + "parsec", # keep |
| 43 | + ], |
| 44 | + # Most snapshots of your choice might do |
| 45 | + snapshot = "lts-18.1", |
| 46 | +) |
| 47 | +``` |
| 48 | +Should Haskell packages need to be grabbed from elsewhere, alternative |
| 49 | +labels can be provided to [gazelle_haskell_modules_dependencies][gazelle_haskell_modules_dependencies]. |
| 50 | + |
| 51 | +You can generate or update build rules by adding the following to |
| 52 | +one of your `BUILD` files. |
| 53 | + |
| 54 | +```python |
| 55 | +load( |
| 56 | + "@bazel_gazelle//:def.bzl", |
| 57 | + "DEFAULT_LANGUAGES", |
| 58 | + "gazelle", |
| 59 | + "gazelle_binary", |
| 60 | +) |
| 61 | + |
| 62 | +gazelle( |
| 63 | + name = "gazelle", |
| 64 | + data = ["@io_tweag_gazelle_haskell_modules//himportscan"], |
| 65 | + gazelle = ":gazelle_binary", |
| 66 | +) |
| 67 | + |
| 68 | +gazelle_binary( |
| 69 | + name = "gazelle_binary", |
| 70 | + languages = DEFAULT_LANGUAGES + ["@io_tweag_gazelle_haskell_modules//gazelle_haskell_modules"], |
| 71 | +) |
| 72 | +``` |
| 73 | + |
| 74 | +## Running |
| 75 | + |
| 76 | +Build and run gazelle with |
| 77 | +```bash |
| 78 | +bazel run //:gazelle |
| 79 | +``` |
| 80 | + |
| 81 | +## Rule generation |
| 82 | + |
| 83 | +Each module listed in the `srcs` attribute of a Haskell rule originates |
| 84 | +a `haskell_module` rule with name `<pkg>.<module>`. Some attributes |
| 85 | +are copied from the originating rule to the `haskell_module` rule, like |
| 86 | +`ghcopts`, `tools`, and `extra_srcs`. |
| 87 | + |
| 88 | +The `deps` attribute is managed in a special way though. Each dependency |
| 89 | +is considered for addition to the `deps` attribute of a `haskell_module` |
| 90 | +rule. If the dependency is defined in the current repository, and it is |
| 91 | +defined with a Haskell rule, then the dependency is dropped since the |
| 92 | +`haskell_module` rule is expected to depend directly on the modules of |
| 93 | +that dependency if at all. |
| 94 | + |
| 95 | +If the dependency isn't defined in the local repo, or isn't defined with |
| 96 | +Haskell rules, then the dependcy is added since the source module might |
| 97 | +depend on it without `gazelle_haskell_modules` being able to determine it. |
| 98 | + |
| 99 | +### Dependencies of non-haskell\_module rules |
| 100 | + |
| 101 | +The `srcs` attributes of `haskell_library`, `haskell_binary`, and |
| 102 | +`haskell_test` are cleared. The `deps` attribute is augmented with |
| 103 | +the labels of the corresponding `haskell_module` rules. |
| 104 | + |
| 105 | +Additionally, `gazelle_haskell_modules` removes from `deps` the |
| 106 | +dependencies defined with other `haskell_library` rules. These |
| 107 | +dependencies are unnecessary because the `haskell_module` rules will |
| 108 | +depend on modules from them if needed. |
| 109 | + |
| 110 | +### Updating dependencies of haskell\_module rules |
| 111 | + |
| 112 | +When the imports in a module are changed, the corresponding |
| 113 | +`haskell_module` rule might need to be updated. Removed imports are |
| 114 | +removed from the `deps` attribute, and added imports might originate new |
| 115 | +dependencies. Adding an import to a module that is defined in the current |
| 116 | +repo, will add that module to the dependencies as long as the name of the |
| 117 | +module uniquely identifies a rule that defines it. If two different rules |
| 118 | +defined the same module name, `gazelle_haskell_modules` will produce an |
| 119 | +error. |
| 120 | + |
| 121 | +Note that `gazelle_haskell_modules` will always chose the right module |
| 122 | +when generating new `haskell_module` rules because the list of |
| 123 | +dependencies in the originating rule allows to determine from which |
| 124 | +library imports are coming from. Some of these dependencies are erased |
| 125 | +later on, though, so they aren't available when the `haskell_module` |
| 126 | +rules need to be updated. |
| 127 | + |
| 128 | +## Implementation |
| 129 | + |
| 130 | +`gazelle_haskell_modules` extracts module imports from Haskell modules |
| 131 | +using [himportscan][himportscan], a command line tool written in Haskell, |
| 132 | +which presents the extracted data in json format to the `go` part. |
| 133 | + |
| 134 | +The [go part][go-part] consists of a set of functions written in the |
| 135 | +[go][go] programming language, which `gazelle` will invoke to |
| 136 | +generate `haskell_module` rules. The most important functions are: |
| 137 | + |
| 138 | +* `GenerateRules`: calls the `himportscan` command-line tool and |
| 139 | + produces rules that contain no information about dependencies. |
| 140 | + |
| 141 | +* `Imports`: indexes the rules for dependency resolution. |
| 142 | + |
| 143 | +* `Resolve`: adds to the previously generated rules all the |
| 144 | + information about dependencies (`deps`, `plugins`, and `tools`). |
| 145 | + |
| 146 | +## Limitations |
| 147 | + |
| 148 | +### CPP support |
| 149 | + |
| 150 | +CPP directives in source files are ignored when scanning for imports. |
| 151 | +That is, `himportscan` would always pick up both imports in the next example. |
| 152 | + |
| 153 | +```Haskell |
| 154 | +#if COND |
| 155 | +import SomeModule |
| 156 | +#else |
| 157 | +import SomeOtherModule |
| 158 | +#endif |
| 159 | +``` |
| 160 | + |
| 161 | +### Preprocessor support |
| 162 | + |
| 163 | +Imports may not be possible to extract in files that need preprocessors which |
| 164 | +generate those same imports (e.g. `tasty-discover`). In these cases, generation |
| 165 | +of `haskell_module` rules can be avoided by using `# keep` comments on the |
| 166 | +given files, and any sources that import them. |
| 167 | + |
| 168 | +```python |
| 169 | +# The contents of Spec.hs are generated by tasty-discover |
| 170 | +haskell_test( |
| 171 | + name = "tests" |
| 172 | + srcs = [ |
| 173 | + "tests/Main.hs", # keep |
| 174 | + "tests/Spec.hs", # keep |
| 175 | + "tests/Other.hs" |
| 176 | + ] |
| 177 | + deps = ..., |
| 178 | + tools = ["@stackage-exe//tasty-discover"], |
| 179 | +) |
| 180 | +``` |
| 181 | + |
| 182 | +In the above case, only a `haskell_module` rule for `tests/Other.hs` would |
| 183 | +be produced. |
| 184 | + |
| 185 | +At the moment, this only works on `haskell_binary` and `haskell_test` rules. |
| 186 | +If used in a `haskell_library`, rules for modules in other targets that |
| 187 | +import the preprocessed source files would miss them in the dependencies. |
| 188 | + |
| 189 | +### export and reexported\_modules |
| 190 | + |
| 191 | +`haskell_library` has attributes `export` and `reexported_modules` which |
| 192 | +affect the dependencies of rules that depend on the Haskell library. |
| 193 | +`gazelle_haskell_modules` makes no effort to honor those attributes when |
| 194 | +generating rules or resolving imports. |
| 195 | + |
| 196 | +### Package imports |
| 197 | + |
| 198 | +`gazelle_haskell_modules` ignores package imports as implemented in GHC |
| 199 | +with `-XPackageImports`. |
| 200 | + |
| 201 | +## What's next |
| 202 | + |
| 203 | +- [X] Have `haskell_module` rules depend on each other. |
| 204 | +- [X] Investigate how to index external repositories, which would be needed |
| 205 | + to recognize the provenance of modules coming from external dependencies. |
| 206 | +- [X] Have Haskell rules depend on `haskell_module` rules |
| 207 | +- [X] Copy `extra_srcs` from the originating rule |
| 208 | +- [X] Copy `tools` from the originating rule |
| 209 | +- [X] Copy `plugins` from the originating rule |
| 210 | +- [X] Skip CPP directives when scanning for imports |
| 211 | +- [X] Have a story for preprocessed modules like those needing tasty-discover |
| 212 | +- [X] Update `haskell_module` rules when imports change |
| 213 | +- [X] Solve interferences with `gazelle_cabal` (removes keep comments on srcs, libsodium label is not understood, deps attribute is reset) |
| 214 | +- [X] Lift restriction to name only `haskell_module` rules with dots. |
| 215 | +- [X] Lift restriction to name custom `haskell_module` rules as `<package>.<module>` |
| 216 | +- [X] Propagate compiler flags and other attributes of libraries, tests, and binaries, to `haskell_module` rules listed in the dependencies. |
| 217 | +- [X] Use the `modules` attribute of `haskell_library` instead of `deps` |
| 218 | +- [ ] Check if linker options need special treatment |
| 219 | +- [ ] Copy `data` from the originating rule (?) |
| 220 | +- [X] Document how `gazelle_haskell_modules` works |
| 221 | +- [ ] Support the fix command to remove outdated `haskell_module` rules |
| 222 | +- [X] See whether we can run `gazelle_haskell_modules` and `gazelle_cabal` in one pass |
| 223 | +- [ ] Implement tests |
| 224 | +- [ ] Setup CI |
| 225 | + |
| 226 | +## Sponsors |
| 227 | + |
| 228 | + |
| 229 | +[](https://symbiont.io) |
| 230 | + |
| 231 | +[](http://tweag.io) |
| 232 | + |
| 233 | +`gazelle_haskell_modules` was funded by [Symbiont](https://www.symbiont.io/) |
| 234 | +and is maintained by [Tweag I/O](http://tweag.io/). |
| 235 | + |
| 236 | +Have questions? Need help? Tweet at |
| 237 | +[@tweagio](http://twitter.com/tweagio). |
| 238 | + |
| 239 | +[bazel]: https://bazel.build |
| 240 | +[himportscan]: himportscan/exe/Main.hs |
| 241 | +[gazelle_haskell_module_dependencies]: defs.bzl |
| 242 | +[example]: example |
| 243 | +[fix-command]: https://github.com/bazelbuild/bazel-gazelle#fix-and-update |
| 244 | +[gazelle]: https://github.com/bazelbuild/bazel-gazelle |
| 245 | +[go-part]: gazelle_haskell_modules/lang.go |
| 246 | +[go]: https://golang.org |
| 247 | +[rules_haskell]: https://github.com/tweag/rules_haskell |
0 commit comments