Skip to content

Support Cabal conditions and flags#236

Open
avdv wants to merge 18 commits intotweag:mainfrom
avdv:conditional-cabal
Open

Support Cabal conditions and flags#236
avdv wants to merge 18 commits intotweag:mainfrom
avdv:conditional-cabal

Conversation

@avdv
Copy link
Copy Markdown
Member

@avdv avdv commented Mar 26, 2026

First off, sorry to whoever is looking at this PR it is such a big change. I have been working on this on and off for a few weeks...

This adds support for conditional blocks in .cabal files, generating corresponding select statements in BUILD files.

For example, the cabal project in example/package-d (see https://github.com/avdv/gazelle_cabal/blob/02193153cceeeb5dc900596caf6e43848ff3b5df/example/package-d/package-d.cabal) defines flags and uses them in conditional blocks. Running gazelle generates the following BUILD file:

load("@bazel_skylib//lib:selects.bzl", "selects")
load("@bazel_skylib//rules:common_settings.bzl", "bool_flag")
load("@rules_haskell//haskell:defs.bzl", "haskell_binary", "haskell_library")

bool_flag(
    name = "database",
    build_setting_default = False,
    visibility = ["//visibility:public"],
)

config_setting(
    name = "flag_database",
    flag_values = {
        ":database": "True",
    },
    visibility = ["//visibility:public"],
)

bool_flag(
    name = "experimental",
    build_setting_default = False,
    visibility = ["//visibility:public"],
)

config_setting(
    name = "flag_experimental",
    flag_values = {
        ":experimental": "True",
    },
    visibility = ["//visibility:public"],
)

bool_flag(
    name = "network-support",
    build_setting_default = True,
    visibility = ["//visibility:public"],
)

config_setting(
    name = "flag_network-support",
    flag_values = {
        ":network-support": "True",
    },
    visibility = ["//visibility:public"],
)

# AND-chained config settings
selects.config_setting_group(
    name = "database_and_network-support",
    match_all = [
        ":flag_database",
        ":flag_network-support",
    ],
    visibility = ["//visibility:public"],
)

# rule generated from package-d/package-d.cabal by gazelle_cabal
haskell_library(
    name = "package-d",
    srcs = [
        "src/PackageD/Core.hs",
        "src/PackageD/Internal/Base.hs",
    ] + select({
        ":flag_network-support": [
            "src/PackageD/Internal/NetworkImpl.hs",
            "src/PackageD/Network.hs",
        ],
        "//conditions:default": [],
    }) + select({
        ":flag_experimental": [
            "src/PackageD/Experimental.hs",
        ],
        "//conditions:default": [],
    }) + select({
        ":flag_database": [
            "src/PackageD/Database.hs",
            "src/PackageD/Internal/DatabaseImpl.hs",
        ],
        "//conditions:default": [],
    }) + select({
        ":database_and_network-support": [
            "src/PackageD/NetworkDatabase.hs",
        ],
        "//conditions:default": [],
    }),
    ghcopts = [
        "-DVERSION_package_d=\"0.1.0.0\"",
        "-Wall",
    ],
    hidden_modules = ["PackageD.Internal.Base"] + select({
        ":flag_network-support": [
            "PackageD.Internal.NetworkImpl",
        ],
        "//conditions:default": [],
    }) + select({
        ":flag_database": [
            "PackageD.Internal.DatabaseImpl",
        ],
        "//conditions:default": [],
    }),
    version = "0.1.0.0",
    visibility = ["//visibility:public"],
    deps = ["@stackage//:base"] + select({
        ":flag_network-support": [
            "@stackage//:network",
        ],
        "//conditions:default": [],
    }) + select({
        ":flag_experimental": [
            "@stackage//:text",
        ],
        "//conditions:default": [],
    }) + select({
        ":flag_database": [
            "@stackage//:direct-sqlite",
        ],
        "//conditions:default": [],
    }),
)

# rule generated from package-d/package-d.cabal by gazelle_cabal
haskell_binary(
    name = "package-d-cli",
    srcs = ["app/Main.hs"],
    ghcopts = ["-DVERSION_package_d=\"0.1.0.0\""] + select({
        ":flag_network-support": [
            "-DNETWORK_SUPPORT",
        ],
        "//conditions:default": [],
    }) + select({
        ":flag_database": [
            "-DDATABASE_SUPPORT",
        ],
        "//conditions:default": [],
    }) + select({
        ":database_and_network-support": [
            "-DDATABASE_AND_NETWORK_SUPPORT",
        ],
        "//conditions:default": [],
    }),
    main_file = "app/Main.hs",
    version = "0.1.0.0",
    visibility = ["//visibility:public"],
    deps = [
        ":package-d",
        "@stackage//:base",
    ] + select({
        ":flag_network-support": [
            "@stackage//:optparse-applicative",
        ],
        "//conditions:default": [],
    }),
)

I have used this successfully on the xmobar project which makes extensive use of flags and conditions.

  • Add DNF implementation
  • Format .go
  • Support conditional cabal files
  • Add example project using flags and conditions

@avdv avdv force-pushed the conditional-cabal branch 2 times, most recently from 31c2893 to fa34858 Compare March 31, 2026 18:41
avdv added 5 commits April 3, 2026 10:48
This adds support for flags and conditions in .cabal files which are mapped
(if possible) to select statements (configurable attributes) in Bazel.
The former depends on attoparsec which is more complicated to set up (for
different GHC versions / stackage snapshots).
@avdv avdv force-pushed the conditional-cabal branch from fa34858 to 69967f5 Compare April 3, 2026 08:49
avdv added 4 commits April 3, 2026 12:49
The archive tarballs might change.
Some modules do not yet exist in the BCR and we use overrides for them.

This makes sure the versions of rules_nixpkgs_core (in BCR) and rules_nixpkgs_posix /
rules_nixpkgs_cc (non-BCR) as well as rules_haskell (in BCR) and rules_haskell_nix (non-BCR)
are actually consistent.
@avdv avdv force-pushed the conditional-cabal branch from 8b05a4f to f65a31a Compare April 3, 2026 11:57
avdv added 3 commits April 4, 2026 11:58
Expose CPP defines for the different flags and print if they are
enabled or disabled in the app.

Add a test script to check if building with different flags enabled /
disabled has the desired effect.
@avdv avdv force-pushed the conditional-cabal branch from d2f6a4d to 88dfd44 Compare April 4, 2026 14:05
@avdv avdv marked this pull request as ready for review April 5, 2026 17:15
@avdv avdv changed the title Support conditions and flags Support Cabal conditions and flags Apr 7, 2026
@avdv avdv requested a review from Copilot April 21, 2026 10:30
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds first-class support for Cabal conditional blocks and flags by extending cabalscan to emit configurable attributes and gazelle_cabal to generate corresponding Bazel bool_flag, config_setting, select(...), and selects.config_setting_group(...) constructs, plus an example project to validate the behavior end-to-end.

Changes:

  • Extend Cabal scanning output to include flags and conditional/select-shaped attributes (DNF-based condition handling).
  • Teach Gazelle rule generation + dependency resolution to carry configurable lists through to generated BUILD attributes.
  • Add a new example (example/package-d) and CI coverage to validate flag-driven selects.

Reviewed changes

Copilot reviewed 32 out of 32 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
gazelle_cabal/lang_test.go Formatting/import order adjustments in tests.
gazelle_cabal/lang.go Generates bool_flag/config_setting/config_setting_group and adds configurable list handling.
gazelle_cabal/dependency_resolution_test.go Updates tests to use configurable list structures.
gazelle_cabal/dependency_resolution.go Updates dependency resolution to support configurable deps/opts/tools and expression walking.
gazelle_cabal/BUILD.bazel Adds buildtools dependency for manipulating bzl AST.
example/package-d/test_flags.sh Adds a script to validate different flag combinations via bazel run.
example/package-d/src/PackageD/NetworkDatabase.hs Example module gated by combined flags.
example/package-d/src/PackageD/Network.hs Example module gated by network flag.
example/package-d/src/PackageD/Internal/NetworkImpl.hs Example internal module for network support.
example/package-d/src/PackageD/Internal/DatabaseImpl.hs Example internal module for database support.
example/package-d/src/PackageD/Internal/Base.hs Core example module always included.
example/package-d/src/PackageD/Experimental.hs Example module gated by experimental flag.
example/package-d/src/PackageD/Database.hs Example module gated by database flag.
example/package-d/src/PackageD/Core.hs Core library module.
example/package-d/package-d.cabal New example Cabal file defining flags + conditional stanzas.
example/package-d/app/Main.hs Example executable using CPP macros driven by flags.
example/package-d/CHANGELOG.md Example package changelog.
example/MODULE.bazel Adds bazel_skylib and extra packages for the new example.
cabalscan/tests/CabalScan/RuleGeneratorSpec.hs Minor formatting cleanup in existing test.
cabalscan/tests/CabalScan/DNFSpec.hs Adds tests for DNF simplification/negation behavior.
cabalscan/src/CabalScan/Rules.hs Introduces PackageOutput, Flag, and configurable/select representations in JSON output.
cabalscan/src/CabalScan/RuleGenerator.hs Adds conditional traversal and DNF→select/config-group translation.
cabalscan/src/CabalScan/DNF.hs New DNF representation + simplification utilities.
cabalscan/src/Cabal/Cabal_9_4.hs Switches to GenericPackageDescription APIs needed for conditionals/flags.
cabalscan/src/Cabal/Cabal_9_2.hs Same as above for Cabal 9.2.
cabalscan/src/Cabal/Cabal_9_0.hs Same as above for Cabal 9.0.
cabalscan/src/Cabal/Cabal_8_10.hs Same as above for Cabal 8.10 (+ flag/ConfVar compatibility).
cabalscan/exe/Main.hs Outputs aggregated PackageOutput instead of [RuleInfo].
cabalscan/BUILD.bazel Tweaks test timeout and deps for new DNF tests.
MODULE.bazel Refactors versions/overrides and aligns rules_nixpkgs version usage.
.github/workflows/workflow.yaml Updates CI to rely on .bazelrc.local and adds package-d flag check.
.bazelrc Sets locale env for tests involving Unicode output.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread gazelle_cabal/lang.go
Comment on lines +397 to +400
var keptElems []bzl.Expr
var keptWholeExprs []bzl.Expr
var keptSelects []*bzl.CallExpr

Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

keptSelects is declared but never used, which will fail go test/go vet compilation. Please remove it or use it (e.g., if you intended to treat only select() calls specially).

Copilot uses AI. Check for mistakes.
Comment thread gazelle_cabal/lang.go
Comment on lines +406 to +410
// If the whole select is marked keep, save it as a whole expression
if selectExpr, ok := expr.(*bzl.CallExpr); ok {
keptSelects = append(keptSelects, selectExpr)
keptWholeExprs = append(keptWholeExprs, selectExpr)
return
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Merge, any *bzl.CallExpr that is marked # keep is treated as a “select” and preserved wholesale, even if it’s not actually a select(...) call (e.g. glob(...)). This can unintentionally keep unrelated call expressions and change merge behavior. Consider checking e.X is Ident{Name:"select"} (or selects.config_setting_group if needed) before treating it as a keep-worthy select expression, otherwise fall back to the element/whole-expression logic.

Copilot uses AI. Check for mistakes.
Comment thread gazelle_cabal/dependency_resolution.go Outdated
Comment thread cabalscan/src/CabalScan/RuleGenerator.hs Outdated
Comment on lines +323 to +327
-- Case 5: Complex condition not yet implemented
| otherwise = unsupported
where
unsupported = (traceShow cs $ Select (Map.singleton "not-implemented" trueVal) falseVal, [])

Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For unsupported/complex conditions, dnfToSelect currently emits a Select with a key of "not-implemented" and uses traceShow. This will produce invalid/non-resolvable select conditions in generated BUILD files and also introduces debug logging in normal runs. Prefer failing generation with a clear error (or returning a best-effort default branch only), and remove traceShow/traceStack debugging from production output paths.

Copilot uses AI. Check for mistakes.
Comment thread .github/workflows/workflow.yaml
avdv and others added 5 commits April 21, 2026 22:09
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Cabal auto-generates a Paths_<package-name>.hs file when referenced in the cabal file.

This file does not exist, so we do not generate an entry in `srcs` for it.

On the usage side, this file could be generated and added to srcs with a `keep` comment.
There should be no duplicates in attributes after selects are evaluated.

Merge and split selects such that Bazel's select invariants are maintained.

Remove values from selects that are already part of the non-configurable
part of the attribute.
@avdv avdv force-pushed the conditional-cabal branch from 668188c to b73d778 Compare May 4, 2026 18:07
@avdv avdv requested a review from facundominguez May 6, 2026 18:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants