diff --git a/haskell/BUILD.bazel b/haskell/BUILD.bazel index be61737af..0c0fbebe4 100644 --- a/haskell/BUILD.bazel +++ b/haskell/BUILD.bazel @@ -6,11 +6,16 @@ load( "@rules_haskell//haskell:private/cc_wrapper.bzl", "cc_wrapper", ) +load( + "@rules_haskell//haskell:cabal_wrapper.bzl", + "cabal_wrapper", +) exports_files( glob(["*.bzl"]) + [ "assets/ghci_script", "private/cabal_wrapper.sh.tpl", + "private/cabal_wrapper.py.tpl", "private/coverage_wrapper.sh.tpl", "private/ghci_repl_wrapper.sh", "private/haddock_wrapper.sh.tpl", @@ -31,6 +36,11 @@ cc_wrapper( visibility = ["//visibility:public"], ) +cabal_wrapper( + name = "cabal_wrapper", + visibility = ["//visibility:public"], +) + py_binary( name = "pkgdb_to_bzl", srcs = ["private/pkgdb_to_bzl.py"], diff --git a/haskell/cabal.bzl b/haskell/cabal.bzl index 63feebfab..9856d787c 100644 --- a/haskell/cabal.bzl +++ b/haskell/cabal.bzl @@ -86,38 +86,18 @@ def _cabal_tool_flag(tool): return "--with-{}={}".format(tool.basename, tool.path) def _make_path(hs, binaries): - return ":".join([binary.dirname for binary in binaries.to_list()] + ["$PATH"]) + return ":".join([binary.dirname for binary in binaries.to_list()]) -def _prepare_cabal_inputs(hs, cc, unix, dep_info, cc_info, component, package_id, tool_inputs, tool_input_manifests, cabal, setup, srcs, flags, cabal_wrapper_tpl, package_database): +def _prepare_cabal_inputs(hs, cc, unix, dep_info, cc_info, component, package_id, tool_inputs, tool_input_manifests, cabal, setup, srcs, flags, cabal_wrapper, package_database): """Compute Cabal wrapper, arguments, inputs.""" with_profiling = is_profiling_enabled(hs) (ghci_extra_libs, env) = get_ghci_extra_libs(hs, cc_info) + env.update(**hs.env) env["PATH"] = _make_path(hs, tool_inputs) + ":" + ":".join(unix.paths) if hs.toolchain.is_darwin: env["SDKROOT"] = "macosx" # See haskell/private/actions/link.bzl - # TODO Instantiating this template could be done just once in the - # toolchain rule. - cabal_wrapper = hs.actions.declare_file("cabal_wrapper-{}.sh".format(hs.label.name)) - hs.actions.expand_template( - template = cabal_wrapper_tpl, - output = cabal_wrapper, - is_executable = True, - substitutions = { - "%{ghc}": hs.tools.ghc.path, - "%{ghc_pkg}": hs.tools.ghc_pkg.path, - "%{runghc}": hs.tools.runghc.path, - "%{ar}": cc.tools.ar, - "%{cc}": cc.tools.cc, - "%{strip}": cc.tools.strip, - # XXX Workaround - # https://github.com/bazelbuild/bazel/issues/5980. - "%{env}": render_env(env), - "%{is_windows}": str(hs.toolchain.is_windows), - }, - ) - args = hs.actions.args() package_databases = dep_info.package_databases extra_headers = cc_info.compilation_context.headers @@ -147,7 +127,7 @@ def _prepare_cabal_inputs(hs, cc, unix, dep_info, cc_info, component, package_id args.add_all(tool_inputs, map_each = _cabal_tool_flag) inputs = depset( - [cabal_wrapper, setup, hs.tools.ghc, hs.tools.ghc_pkg, hs.tools.runghc], + [setup, hs.tools.ghc, hs.tools.ghc_pkg, hs.tools.runghc], transitive = [ depset(srcs), depset(cc.files), @@ -233,14 +213,16 @@ def _haskell_cabal_library_impl(ctx): setup = setup, srcs = ctx.files.srcs, flags = ctx.attr.flags, - cabal_wrapper_tpl = ctx.file._cabal_wrapper_tpl, + cabal_wrapper = ctx.executable._cabal_wrapper, package_database = package_database, ) - ctx.actions.run_shell( - command = '{} "$@"'.format(c.cabal_wrapper.path), + ctx.actions.run( + # command = '{} "$@"'.format(c.cabal_wrapper.path), + executable = c.cabal_wrapper, arguments = [c.args], inputs = c.inputs, input_manifests = c.input_manifests, + tools = [c.cabal_wrapper], outputs = [ package_database, interfaces_dir, @@ -250,7 +232,6 @@ def _haskell_cabal_library_impl(ctx): env = c.env, mnemonic = "HaskellCabalLibrary", progress_message = "HaskellCabalLibrary {}".format(hs.label), - use_default_shell_env = True, ) default_info = DefaultInfo( @@ -329,9 +310,10 @@ haskell_cabal_library = rule( "flags": attr.string_list( doc = "List of Cabal flags, will be passed to `Setup.hs configure --flags=...`.", ), - "_cabal_wrapper_tpl": attr.label( - allow_single_file = True, - default = Label("@rules_haskell//haskell:private/cabal_wrapper.sh.tpl"), + "_cabal_wrapper": attr.label( + executable = True, + cfg = "host", + default = Label("@rules_haskell//haskell:cabal_wrapper"), ), "_cc_toolchain": attr.label( default = Label("@bazel_tools//tools/cpp:current_cc_toolchain"), @@ -413,11 +395,12 @@ def _haskell_cabal_binary_impl(ctx): setup = setup, srcs = ctx.files.srcs, flags = ctx.attr.flags, - cabal_wrapper_tpl = ctx.file._cabal_wrapper_tpl, + cabal_wrapper = ctx.executable._cabal_wrapper, package_database = package_database, ) - ctx.actions.run_shell( - command = '{} "$@"'.format(c.cabal_wrapper.path), + ctx.actions.run( + # command = '{} "$@"'.format(c.cabal_wrapper.path), + executable = c.cabal_wrapper, arguments = [c.args], inputs = c.inputs, input_manifests = c.input_manifests, @@ -426,10 +409,10 @@ def _haskell_cabal_binary_impl(ctx): binary, data_dir, ], + tools = [c.cabal_wrapper], env = c.env, mnemonic = "HaskellCabalBinary", progress_message = "HaskellCabalBinary {}".format(hs.label), - use_default_shell_env = True, ) hs_info = HaskellInfo( @@ -468,9 +451,10 @@ haskell_cabal_binary = rule( "flags": attr.string_list( doc = "List of Cabal flags, will be passed to `Setup.hs configure --flags=...`.", ), - "_cabal_wrapper_tpl": attr.label( - allow_single_file = True, - default = Label("@rules_haskell//haskell:private/cabal_wrapper.sh.tpl"), + "_cabal_wrapper": attr.label( + executable = True, + cfg = "host", + default = Label("@rules_haskell//haskell:cabal_wrapper"), ), "_cc_toolchain": attr.label( default = Label("@bazel_tools//tools/cpp:current_cc_toolchain"), diff --git a/haskell/cabal_wrapper.bzl b/haskell/cabal_wrapper.bzl new file mode 100644 index 000000000..3fd8ce68e --- /dev/null +++ b/haskell/cabal_wrapper.bzl @@ -0,0 +1,56 @@ +load(":private/context.bzl", "haskell_context", "render_env") +load(":cc.bzl", "cc_interop_info") +load("@bazel_tools//tools/cpp:toolchain_utils.bzl", "find_cpp_toolchain") + +def _cabal_wrapper_impl(ctx): + hs = haskell_context(ctx) + hs_toolchain = ctx.toolchains["@rules_haskell//haskell:toolchain"] + cc_toolchain = ctx.attr._cc_toolchain[cc_common.CcToolchainInfo] + + cabal_wrapper_tpl = ctx.file._cabal_wrapper_tpl + cabal_wrapper = hs.actions.declare_file("cabal_wrapper.py") + hs.actions.expand_template( + template = cabal_wrapper_tpl, + output = cabal_wrapper, + is_executable = True, + substitutions = { + "%{ghc}": hs.tools.ghc.path, + "%{ghc_pkg}": hs.tools.ghc_pkg.path, + "%{runghc}": hs.tools.runghc.path, + "%{ar}": cc_toolchain.ar_executable(), + "%{strip}": cc_toolchain.strip_executable(), + "%{is_windows}": str(hs.toolchain.is_windows), + }, + ) + return [DefaultInfo( + files = depset([cabal_wrapper]), + )] + +_cabal_wrapper = rule( + implementation = _cabal_wrapper_impl, + attrs = { + "_cabal_wrapper_tpl": attr.label( + allow_single_file = True, + default = Label("@rules_haskell//haskell:private/cabal_wrapper.py.tpl"), + ), + "_cc_toolchain": attr.label( + default = Label("@bazel_tools//tools/cpp:current_cc_toolchain"), + ), + }, + toolchains = ["@rules_haskell//haskell:toolchain"], + fragments = ["cpp"], +) + +def cabal_wrapper(name, **kwargs): + _cabal_wrapper( + name = name + ".py", + ) + native.py_binary( + name = name, + srcs = [name + ".py"], + python_version = "PY3", + deps = [ + "@bazel_tools//tools/python/runfiles", + ], + **kwargs + ) diff --git a/haskell/private/cabal_wrapper.py.tpl b/haskell/private/cabal_wrapper.py.tpl new file mode 100755 index 000000000..4d12536af --- /dev/null +++ b/haskell/private/cabal_wrapper.py.tpl @@ -0,0 +1,130 @@ +#!/usr/bin/env python3 + +from glob import glob +import os +import os.path +import re +import shlex +import subprocess +import sys +import tempfile + +def run(cmd, *args, **kwargs): + # print("+ " + " ".join([shlex.quote(arg) for arg in cmd]), file=sys.stderr) + # sys.stderr.flush() + subprocess.run(cmd, *args, **kwargs) + +def canonicalize_path(path): + return ":".join([ + os.path.abspath(entry) + for entry in path.split(":") + if entry != "" + ]) + +# Remove any relative entries, because we'll be changing CWD shortly. +os.environ["LD_LIBRARY_PATH"] = canonicalize_path(os.getenv("LD_LIBRARY_PATH", "")) +os.environ["LIBRARY_PATH"] = canonicalize_path(os.getenv("LIBRARY_PATH", "")) +os.environ["PATH"] = canonicalize_path(os.getenv("PATH", "")) + +component = sys.argv.pop(1) +name = sys.argv.pop(1) +execroot = os.getcwd() +setup = os.path.join(execroot, sys.argv.pop(1)) +srcdir = os.path.join(execroot, sys.argv.pop(1)) +pkgroot = os.path.realpath(os.path.join(execroot, os.path.dirname(sys.argv.pop(1)))) +libdir = os.path.join(pkgroot, "iface") +dynlibdir = os.path.join(pkgroot, "lib") +bindir = os.path.join(pkgroot, "bin") +datadir = os.path.join(pkgroot, "data") +package_database = os.path.join(pkgroot, "package.conf.d") + +ghc_pkg = "%{ghc_pkg}" +runghc = os.path.join(execroot, "%{runghc}") +ghc = os.path.join(execroot, "%{ghc}") +ghc_pkg = os.path.join(execroot, "%{ghc_pkg}") + +extra_args = [] +current_arg = sys.argv.pop(1) +while current_arg != "--": + extra_args.append(current_arg) + current_arg = sys.argv.pop(1) + +path_args = sys.argv[1:] + +ar = os.path.realpath("%{ar}") +strip = os.path.realpath("%{strip}") + +def recache_db(): + run([ghc_pkg, "recache", "--package-db=" + package_database]) + +recache_db() + +with tempfile.TemporaryDirectory() as distdir: + enable_relocatable_flags = ["--enable-relocatable"] \ + if "%{is_windows}" != "True" else [] + + old_cwd = os.getcwd() + os.chdir(srcdir) + os.putenv("HOME", "/var/empty") + run([runghc, setup, "configure", \ + component, \ + "--verbose=0", \ + "--user", \ + "--with-compiler=" + ghc, + "--with-hc-pkg=" + ghc_pkg, + "--with-ar=" + ar, + "--with-strip=" + strip, + "--enable-deterministic", \ + "--builddir=" + distdir, \ + "--prefix=" + pkgroot, \ + "--libdir=" + libdir, \ + "--dynlibdir=" + dynlibdir, \ + "--libsubdir=", \ + "--bindir=" + bindir, \ + "--datadir=" + datadir, \ + "--package-db=clear", \ + "--package-db=global", \ + ] + \ + enable_relocatable_flags + \ + extra_args + \ + [ arg.replace("=", "=" + execroot + "/") for arg in path_args ] + \ + [ "--package-db=" + package_database ], # This arg must come last. + ) + run([runghc, setup, "build", "--verbose=0", "--builddir=" + distdir]) + run([runghc, setup, "install", "--verbose=0", "--builddir=" + distdir]) + os.chdir(old_cwd) + +# XXX Cabal has a bizarre layout that we can't control directly. It +# confounds the library-dir and the import-dir (but not the +# dynamic-library-dir). That's pretty annoying, because Bazel won't +# allow overlap in the path to the interface files directory and the +# path to the static library. So we move the static library elsewhere +# and patch the .conf file accordingly. +# +# There were plans for controlling this, but they died. See: +# https://github.com/haskell/cabal/pull/3982#issuecomment-254038734 +libraries=glob(os.path.join(libdir, "libHS*.a")) +package_conf_file = os.path.join(package_database, name + ".conf") + +def make_relocatable_paths(line): + line = re.sub("library-dirs:.*", "library-dirs: ${pkgroot}/lib", line) + + def make_relative_to_pkgroot(matchobj): + abspath=matchobj.group(0) + return os.path.join("${pkgroot}", os.path.relpath(abspath, start=pkgroot)) + + line = re.sub(execroot + '\S*', make_relative_to_pkgroot, line) + return line + +if libraries != [] and os.path.isfile(package_conf_file): + for lib in libraries: + os.rename(lib, os.path.join(dynlibdir, os.path.basename(lib))) + + tmp_package_conf_file = package_conf_file + ".tmp" + with open(package_conf_file, 'r') as package_conf: + with open(tmp_package_conf_file, 'w') as tmp_package_conf: + for line in package_conf.readlines(): + print(make_relocatable_paths(line), file=tmp_package_conf) + os.remove(package_conf_file) + os.rename(tmp_package_conf_file, package_conf_file) + recache_db() diff --git a/haskell/private/cabal_wrapper.sh.tpl b/haskell/private/cabal_wrapper.sh.tpl deleted file mode 100644 index d9add4ed5..000000000 --- a/haskell/private/cabal_wrapper.sh.tpl +++ /dev/null @@ -1,185 +0,0 @@ -#!/usr/bin/env bash -# -# cabal_wrapper.sh [EXTRA_ARGS...] -- [PATH_ARGS...] -# -# This wrapper calls Cabal's configure/build/install steps one big -# action so that we don't have to track all inputs explicitly between -# steps. -# -# COMPONENT: Cabal component to build. -# PKG_NAME: Package ID of the resulting package. -# SETUP_PATH: Path to Setup.hs -# PKG_DIR: Directory containing the Cabal file -# PACKAGE_DB_PATH: Output package DB path. -# EXTRA_ARGS: Additional args to Setup.hs configure. -# PATH_ARGS: Additional args to Setup.hs configure where paths need to be prefixed with execroot. - -# TODO Remove once https://github.com/bazelbuild/bazel/issues/5980 is -# fixed. -%{env} - -set -euo pipefail -shopt -s nullglob - -# Poor man's realpath, because realpath is not available on macOS. -function realpath() -{ - [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}" -} - -function canonicalize_path() -{ - new_path="" - while IFS=: read -r -d: entry - do - if [[ -n "$entry" ]] - then - new_path="$new_path${new_path:+:}$(realpath "$entry")" - fi - done <<< "${1:-}:" - echo $new_path -} - -# relative_to ORIGIN PATH -# Compute the relative path from ORIGIN to PATH. -function relative_to() { - local out= - # Split path into components - local -a relto; IFS="/\\" read -ra relto <<<"$1" - local -a path; IFS="/\\" read -ra path <<<"$2" - local off=0 - while [[ "${relto[$off]}" == "${path[$off]}" ]]; do - : $((off++)) - if [[ $off -eq ${#relto[@]} || $off -eq ${#path[@]} ]]; then - break - fi - done - for ((i=$off; i < ${#relto[@]}; i++)); do - out="$out${out:+/}.." - done - for ((i=$off; i < ${#path[@]}; i++)); do - out="$out${out:+/}${path[$i]}" - done - echo "$out" -} - -# Remove any relative entries, because we'll be changing CWD shortly. -LD_LIBRARY_PATH=$(canonicalize_path $LD_LIBRARY_PATH) -LIBRARY_PATH=$(canonicalize_path $LIBRARY_PATH) -PATH=$(canonicalize_path $PATH) - -component=$1 -name=$2 -execroot="$(pwd)" -setup=$execroot/$3 -srcdir=$execroot/$4 -pkgroot="$(realpath $execroot/$(dirname $5))" # By definition (see ghc-pkg source code). -shift 5 - -declare -a extra_args -while [[ $1 != -- ]]; do - extra_args+=("$1") - shift 1 -done -shift 1 - -ar=$(realpath %{ar}) -cc=$(realpath %{cc}) -strip=$(realpath %{strip}) -distdir=$(mktemp -d) -libdir="$pkgroot/${name}_iface" -dynlibdir=$pkgroot/lib -bindir=$pkgroot/bin -datadir="$pkgroot/${name}_data" -package_database="$pkgroot/${name}.conf.d" - -cleanup () { - rm -rf "$distdir" -} -trap cleanup EXIT - -%{ghc_pkg} recache --package-db=$package_database - -WITH_GCC= -if [[ %{is_windows} != True ]]; then - # Use the cc-wrapper for Cabal builds. - # Note, we cannot currently use it on Windows because the solution to the - # following issue is not released, yet. - # https://github.com/bazelbuild/bazel/issues/9390 - WITH_GCC="--with-gcc=$cc" -fi - -ENABLE_RELOCATABLE= -if [[ %{is_windows} != True ]]; then - ENABLE_RELOCATABLE=--enable-relocatable -fi - -# Cabal really wants the current working directory to be directory -# where the .cabal file is located. So we have no choice but to chance -# cd into it, but then we have to rewrite all relative references into -# absolute ones before doing so (using $execroot). -cd $srcdir -export HOME=/var/empty -# Note, setting --datasubdir is required to work around -# https://github.com/haskell/cabal/issues/6235 -$execroot/%{runghc} $setup configure \ - $component \ - --verbose=0 \ - --user \ - --with-compiler=$execroot/%{ghc} \ - --with-hc-pkg=$execroot/%{ghc_pkg} \ - --with-ar=$ar \ - $WITH_GCC \ - --with-strip=$strip \ - --enable-deterministic \ - $ENABLE_RELOCATABLE \ - --builddir=$distdir \ - --prefix=$pkgroot \ - --libdir=$libdir \ - --dynlibdir=$dynlibdir \ - --libsubdir= \ - --bindir=$bindir \ - --datadir=$datadir \ - --datasubdir= \ - --package-db=clear \ - --package-db=global \ - "${extra_args[@]}" \ - "${@/=/=$execroot/}" \ - --package-db=$package_database # This arg must come last. -$execroot/%{runghc} $setup build --verbose=0 --builddir=$distdir -$execroot/%{runghc} $setup install --verbose=0 --builddir=$distdir -cd - >/dev/null - -# XXX Cabal has a bizarre layout that we can't control directly. It -# confounds the library-dir and the import-dir (but not the -# dynamic-library-dir). That's pretty annoying, because Bazel won't -# allow overlap in the path to the interface files directory and the -# path to the static library. So we move the static library elsewhere -# and patch the .conf file accordingly. -# -# There were plans for controlling this, but they died. See: -# https://github.com/haskell/cabal/pull/3982#issuecomment-254038734 -library=($libdir/libHS*.a) -if [[ -n ${library+x} && -f $package_database/$name.conf ]] -then - mv $libdir/libHS*.a $dynlibdir - # The $execroot is an absolute path and should not leak into the output. - # Replace each ocurrence of execroot by a path relative to ${pkgroot}. - function replace_execroot() { - local line - local relpath - while IFS="" read -r line; do - while [[ $line =~ ("$execroot"[^[:space:]]*) ]]; do - relpath="$(relative_to "$pkgroot" "${BASH_REMATCH[1]}")" - line="${line/${BASH_REMATCH[1]}/\$\{pkgroot\}\/$relpath/}" - done - echo "$line" - done - } - sed -e 's,library-dirs:.*,library-dirs: ${pkgroot}/lib,' \ - $package_database/$name.conf \ - | replace_execroot \ - > $package_database/$name.conf.tmp - mv $package_database/$name.conf.tmp $package_database/$name.conf - %{ghc_pkg} recache --package-db=$package_database -fi