Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions BUCK
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Shared helpers used by rules in haskell.bzl - kept in their own BUCK
# file (rather than folded into an existing one) since they're not
# specific to any one package, just infrastructure haskell_test() itself
# depends on.

sh_binary(
name = "run_in_cwd",
main = "run_in_cwd.sh",
visibility = ["PUBLIC"],
)
3 changes: 0 additions & 3 deletions example/BUCK
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ haskell_library(
name = "example-th",
srcs = ["Example/TH.hs"],
packages = ["template-haskell"],
fb_haskell = False,
visibility = ["PUBLIC"],
)

Expand All @@ -39,13 +38,11 @@ haskell_library(
],
packages = ["template-haskell", "safe"],
deps = [":example-th", ":shim"],
fb_haskell = False,
visibility = ["PUBLIC"],
)

haskell_test(
name = "example-test",
srcs = {"Main.hs": "test/Main.hs"},
deps = [":example"],
fb_haskell = False,
)
91 changes: 23 additions & 68 deletions haskell.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,6 @@
# - package deps: `packages = ["text", ...]` instead of explicit
# `"@third-party-haskell//:text"` entries in `deps`.
# - a standard set of packages (base, rts) added to every target.
# - the fb-haskell extension set enabled by default, so individual
# rules don't need to repeat it. Pass fb_haskell = False for a
# package that doesn't import that common stanza (e.g. mangle,
# which declares its own minimal default-extensions) -
# compiler_flags is then used as-is instead of appended to
# FB_HASKELL_EXTENSIONS.
# - hsc2hs: any `.hsc` file in `srcs` is automatically preprocessed, with
# include paths derived from `deps` (see buck2/hsc2hs.bzl) - so a `.hsc`
# file that needs a C++ dependency's headers just needs that dependency
Expand All @@ -31,38 +25,6 @@ load("//buck2:hsc2hs.bzl", "hsc2hs")
# Packages implicitly needed by every Haskell target.
AUTO_PACKAGES = ["base", "rts"]

# Extensions enabled by the `fb-haskell` common stanza in glean.cabal.in.
FB_HASKELL_EXTENSIONS = [
"-XHaskell2010",
"-XBangPatterns",
"-XBinaryLiterals",
"-XDataKinds",
"-XDeriveDataTypeable",
"-XDeriveGeneric",
"-XEmptyCase",
"-XExistentialQuantification",
"-XFlexibleContexts",
"-XFlexibleInstances",
"-XGADTs",
"-XGeneralizedNewtypeDeriving",
"-XLambdaCase",
"-XMultiParamTypeClasses",
"-XMultiWayIf",
"-XNamedFieldPuns",
"-XNoMonomorphismRestriction",
"-XOverloadedStrings",
"-XPatternSynonyms",
"-XRankNTypes",
"-XRecordWildCards",
"-XScopedTypeVariables",
"-XStandaloneDeriving",
"-XTupleSections",
"-XTypeFamilies",
"-XTypeSynonymInstances",
"-XNondecreasingIndentation",
"-XTypeOperators",
]

def _package_deps(packages):
all_pkgs = {p: None for p in (AUTO_PACKAGES + packages)}
return [("@third-party-haskell//:" + p) for p in sorted(all_pkgs.keys())]
Expand Down Expand Up @@ -342,7 +304,6 @@ def haskell_library(
packages = [],
deps = [],
compiler_flags = [],
fb_haskell = True,
# Extra -C-style flags for every .hsc file in `srcs` (see
# hsc2hs.bzl's `extra_flags`) - the buck2 equivalent of Cabal's
# per-library `hsc2hs-options` field.
Expand All @@ -358,12 +319,11 @@ def haskell_library(
# (see `_BUILD_MODE_PREFERRED_LINKAGE`'s own comment for why "both, by
# default" was real, measurable wasted work in `dev` mode specifically).
all_deps = deps + _package_deps(packages)
all_compiler_flags = (FB_HASKELL_EXTENSIONS + compiler_flags) if fb_haskell else compiler_flags
kwargs.setdefault("preferred_linkage", _BUILD_MODE_PREFERRED_LINKAGE)
native.haskell_library(
name = name,
srcs = _resolve_srcs(name, srcs, all_deps, hsc_flags),
compiler_flags = all_compiler_flags + _BUILD_MODE_HASKELL_FLAGS,
compiler_flags = compiler_flags + _BUILD_MODE_HASKELL_FLAGS,
deps = all_deps,
**kwargs
)
Expand All @@ -374,12 +334,10 @@ def haskell_binary(
packages = [],
deps = [],
compiler_flags = [],
fb_haskell = True,
hsc_flags = [],
linker_flags = [],
**kwargs):
all_deps = deps + _package_deps(packages)
all_compiler_flags = (FB_HASKELL_EXTENSIONS + compiler_flags) if fb_haskell else compiler_flags
kwargs.setdefault("link_style", _BUILD_MODE_LINK_STYLE)
kwargs.setdefault("enable_profiling", _PROF_ENABLED)
kwargs.setdefault("exec_compatible_with", _BUILD_MODE_EXEC_COMPATIBLE_WITH)
Expand All @@ -396,23 +354,13 @@ def haskell_binary(
# (`-optl-...` during a compile-only invocation, `-optc-...` when
# nothing needs the C compiler), so adding both everywhere is the
# faithful equivalent, not redundant belt-and-braces.
compiler_flags = all_compiler_flags + _BUILD_MODE_HASKELL_FLAGS + _ASAN_LINKER_FLAGS,
compiler_flags = compiler_flags + _BUILD_MODE_HASKELL_FLAGS + _ASAN_LINKER_FLAGS,
deps = all_deps,
# Every Cabal executable/test-suite gets `-threaded -rtsopts` for
# free via glean.cabal.in's `common exe` stanza - not opt-in, so
# this shouldn't be either. Without it, anything that blocks its
# main thread in a synchronous FFI/subprocess call while needing a
# background thread to make progress concurrently (e.g. an
# embedded Warp server servicing a request while `callCommand`
# waits on an external tool - see glean-snapshot-{,codemarkup-}
# haskell) hangs until it times out, even though it compiles and
# links fine. Merged with, not replaced by, a caller's own
# `linker_flags` (e.g. gleancli's `-with-rtsopts=-I0`).
linker_flags = ["-threaded", "-rtsopts"] + _ASAN_LINKER_FLAGS + linker_flags,
linker_flags = _ASAN_LINKER_FLAGS + linker_flags,
**kwargs
)

# Cabal's test-suites (glean.cabal.in) are all `type: exitcode-stdio-1.0` -
# Cabal's test-suites are all `type: exitcode-stdio-1.0` -
# a plain executable, exit code is the result - so `buck2 test` support
# needs nothing Haskell-specific: this builds the exact same
# haskell_binary() `name` would (so `buck2 run :name` is unaffected), plus
Expand Down Expand Up @@ -441,22 +389,29 @@ def haskell_binary(
# from 226)" - 226 = 0xE2, a UTF-8 lead byte). `C.UTF-8` is a glibc
# locale alias needing no locale-generation step, so it's available
# without depending on whatever locales happen to be installed.
def haskell_test(name, test_args = [], test_env = {}, **kwargs):
def haskell_test(name, test_args = [], test_env = {}, cwd = None, **kwargs):
bin = name + "-bin"
haskell_binary(name = bin, **kwargs)
test_target = ":" + bin
args = test_args

if cwd != None:
test_target = "//buck2:run_in_cwd"
args = [cwd, "$(exe :" + bin + ")"] + test_args

native.sh_test(
name = name,
test = ":" + bin,
args = test_args,
test = test_target,
args = args,
env = {"LANG": "C.UTF-8"} | test_env,
# `test_args`'s own `$(exe ...)` macros (e.g. clang-index,
# hie-indexer - see buck2/platforms/BUCK) are resolved for
# *this* target, not the `:bin` haskell_binary() above - it's
# this sh_test() whose own exec_compatible_with governs which
# execution platform those tools get built under, so the same
# build-mode-inheriting default needs to be set here too, not
# just on haskell_binary() (which already gets it via its own
# kwargs.setdefault, but that's a separate target from this
# one).
# `args`'s own `$(exe ...)` macros (the cd-wrapper case just
# above, or test_args' own e.g. clang-index, hie-indexer - see
# buck2/platforms/BUCK) are resolved for *this* target, not the
# `:bin` haskell_binary() above - it's this sh_test() whose own
# exec_compatible_with governs which execution platform those
# tools get built under, so the same build-mode-inheriting
# default needs to be set here too, not just on haskell_binary()
# (which already gets it via its own kwargs.setdefault, but
# that's a separate target from this one).
exec_compatible_with = _BUILD_MODE_EXEC_COMPATIBLE_WITH,
)
17 changes: 17 additions & 0 deletions prelude/haskell/compile.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,23 @@ def compile_args(ctx: AnalysisContext, link_style: LinkStyle, enable_profiling:
compile_args.add("-no-link", "-i")
compile_args.add("-package-env=-")

# Without this, GHC's own implicit global package db (always loaded,
# independent of anything passed via -package-db below) leaks in
# alongside the deps get_packages_info() computes - normally harmless
# (nothing there collides with an ordinary project's own package
# names), but GHC ships boot libraries under names a project can
# itself build from source under the same name (e.g. `Cabal`,
# `Cabal-syntax`) - then both the boot copy (auto-exposed globally)
# and this target's own explicitly `-package-id`-exposed one export
# the same module name, and GHC fails with "Ambiguous module name"
# for any of that module's importers, even though the -package-id
# given to *this* compile is perfectly unambiguous on its own.
# get_packages_info() already computes exactly the exposure set this
# compile needs (direct deps + base, see its own comment) - with
# `-hide-all-packages` GHC uses only that, ignoring the global db's
# own default exposure entirely.
compile_args.add("-hide-all-packages")

if enable_profiling:
compile_args.add("-prof")

Expand Down
14 changes: 13 additions & 1 deletion prelude/haskell/haskell.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -892,7 +892,19 @@ def haskell_library_impl(ctx: AnalysisContext) -> list[Provider]:
indexing_tsets = {}
sub_targets = {}

libname = repr(ctx.label.path).replace("//", "_").replace("/", "_").removesuffix("_") + "_" + ctx.label.name
# A package path containing a literal "." (e.g. a directory unpacked
# from a versioned tarball, like "hackage-security-0.6.4.0") isn't
# just cosmetic here: ghc-pkg's own .conf parser rejects a "name:"/
# "id:"/"key:" token containing *any* hyphen-delimited component
# that's purely numeric unless it's the token's very last component
# (it's parsing for a trailing name-version split, same as any
# ordinary "foo-1.2.3" package identifier) - confirmed empirically:
# both a literal "." and a plain "_"-for-"." substitution (still
# hyphenated into an all-digits component by the final "_" -> "-"
# below) fail the same way ("unexpected Empty component, after ..."),
# while splicing in a letter so the component is never purely numeric
# (e.g. "0.6.4.0" -> "0d6d4d0") is accepted regardless of position.
libname = repr(ctx.label.path).replace("//", "_").replace("/", "_").replace(".", "d").removesuffix("_") + "_" + ctx.label.name
pkgname = libname.replace("_", "-")

haskell_toolchain = ctx.attrs._haskell_toolchain[HaskellToolchainInfo]
Expand Down
24 changes: 24 additions & 0 deletions run_in_cwd.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/bin/sh
# Runs a binary with its cwd set to a given directory - used by
# haskell_test()'s own `cwd` attribute (see haskell.bzl) to match real
# `cabal test`'s own invariant (it always runs a test-suite with its cwd
# set to the package's own directory), which buck2's sh_test() has no
# built-in concept of.
#
# Invoked as: run_in_cwd.sh <cwd> <binary> [args...]
set -eu

cwd=$1
bin=$2
shift 2

# $bin (typically a `$(exe ...)` macro expansion) is only guaranteed
# valid from the cwd this script itself started in - not from `$cwd`,
# about to be cd'd into - so it's resolved to an absolute path first.
case "$bin" in
/*) ;;
*) bin="$PWD/$bin" ;;
esac

cd "$cwd"
exec "$bin" "$@"
Loading