Skip to content

Commit

Permalink
Rewrite the cabal wrapper in python
Browse files Browse the repository at this point in the history
Solves #1096 by not needing access to the standard unix tools
(and also make it easier to maintain)
  • Loading branch information
Théophane Hufschmitt authored and aherrmann committed Oct 10, 2019
1 parent aad357d commit fdb20bc
Show file tree
Hide file tree
Showing 5 changed files with 218 additions and 223 deletions.
10 changes: 10 additions & 0 deletions haskell/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"],
Expand Down
60 changes: 22 additions & 38 deletions haskell/cabal.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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,
Expand All @@ -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(
Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -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,
Expand All @@ -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(
Expand Down Expand Up @@ -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"),
Expand Down
56 changes: 56 additions & 0 deletions haskell/cabal_wrapper.bzl
Original file line number Diff line number Diff line change
@@ -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
)
130 changes: 130 additions & 0 deletions haskell/private/cabal_wrapper.py.tpl
Original file line number Diff line number Diff line change
@@ -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()
Loading

0 comments on commit fdb20bc

Please sign in to comment.