diff --git a/BUCK b/BUCK new file mode 100644 index 0000000..a36bc51 --- /dev/null +++ b/BUCK @@ -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"], +) diff --git a/example/BUCK b/example/BUCK index c696f98..ff161a4 100644 --- a/example/BUCK +++ b/example/BUCK @@ -27,7 +27,6 @@ haskell_library( name = "example-th", srcs = ["Example/TH.hs"], packages = ["template-haskell"], - fb_haskell = False, visibility = ["PUBLIC"], ) @@ -39,7 +38,6 @@ haskell_library( ], packages = ["template-haskell", "safe"], deps = [":example-th", ":shim"], - fb_haskell = False, visibility = ["PUBLIC"], ) @@ -47,5 +45,4 @@ haskell_test( name = "example-test", srcs = {"Main.hs": "test/Main.hs"}, deps = [":example"], - fb_haskell = False, ) diff --git a/haskell.bzl b/haskell.bzl index b71f0b0..e1bf9fa 100644 --- a/haskell.bzl +++ b/haskell.bzl @@ -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 @@ -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())] @@ -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. @@ -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 ) @@ -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) @@ -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 @@ -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, ) diff --git a/prelude/haskell/compile.bzl b/prelude/haskell/compile.bzl index 7f28599..45c66d3 100644 --- a/prelude/haskell/compile.bzl +++ b/prelude/haskell/compile.bzl @@ -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") diff --git a/prelude/haskell/haskell.bzl b/prelude/haskell/haskell.bzl index 86dd127..70b6e98 100644 --- a/prelude/haskell/haskell.bzl +++ b/prelude/haskell/haskell.bzl @@ -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] diff --git a/run_in_cwd.sh b/run_in_cwd.sh new file mode 100755 index 0000000..be7fb90 --- /dev/null +++ b/run_in_cwd.sh @@ -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 [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" "$@"