Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cabal: Add RUNPATH entries for transitive C library dependencies #1282

Merged
merged 2 commits into from
Mar 30, 2020
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
33 changes: 30 additions & 3 deletions haskell/cabal.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ load(
load(
":private/cc_libraries.bzl",
"deps_HaskellCcLibrariesInfo",
"get_cc_libraries",
"get_ghci_library_files",
"get_library_files",
"haskell_cc_libraries_aspect",
)

Expand Down Expand Up @@ -110,6 +112,9 @@ def _cabal_tool_flag(tool):
def _binary_paths(binaries):
return [binary.dirname for binary in binaries.to_list()]

def _concat(sequences):
return [item for sequence in sequences for item in sequence]

def _prepare_cabal_inputs(
hs,
cc,
Expand Down Expand Up @@ -139,7 +144,26 @@ def _prepare_cabal_inputs(
# to add libraries and headers for direct C library dependencies to the
# command line.
direct_libs = get_ghci_library_files(hs, cc.cc_libraries_info, cc.cc_libraries)
transitive_libs = get_ghci_library_files(hs, cc.cc_libraries_info, cc.transitive_libraries)

# The regular Haskell rules perform mostly static linking, i.e. where
# possible all C library dependencies are linked statically. Cabal has no
# such mode, and since we have to provide dynamic C libraries for
# compilation, they will also be used for linking. Hence, we need to add
# RUNPATH flags for all dynamic C library dependencies.
(_, dynamic_libs) = get_library_files(
hs,
cc.cc_libraries_info,
get_cc_libraries(cc.cc_libraries_info, cc.transitive_libraries),
dynamic = True,
)

# The regular Haskell rules have separate actions for linking and
# compilation to which we pass different sets of libraries as inputs. The
# Cabal rules, in contrast, only have a single action for compilation and
# linking, so we must provide both sets of libraries as inputs to the same
# action.
transitive_compile_libs = get_ghci_library_files(hs, cc.cc_libraries_info, cc.transitive_libraries)
transitive_link_libs = _concat(get_library_files(hs, cc.cc_libraries_info, cc.transitive_libraries))
env = dict(hs.env)
env["PATH"] = join_path_list(hs, _binary_paths(tool_inputs) + posix.paths)
if hs.toolchain.is_darwin:
Expand Down Expand Up @@ -169,7 +193,7 @@ def _prepare_cabal_inputs(
keep_filename = False,
prefix = relative_rpath_prefix(hs.toolchain.is_darwin),
)
for lib in direct_libs
for lib in dynamic_libs
],
uniquify = True,
)
Expand All @@ -190,7 +214,8 @@ def _prepare_cabal_inputs(
depset(cc.files),
package_databases,
transitive_headers,
depset(transitive_libs),
depset(transitive_compile_libs),
depset(transitive_link_libs),
dep_info.interface_dirs,
dep_info.static_libraries,
dep_info.dynamic_libraries,
Expand All @@ -205,6 +230,7 @@ def _prepare_cabal_inputs(
inputs = inputs,
input_manifests = input_manifests,
env = env,
runfiles = depset(direct = dynamic_libs),
)

def _haskell_cabal_library_impl(ctx):
Expand Down Expand Up @@ -572,6 +598,7 @@ def _haskell_cabal_binary_impl(ctx):
executable = binary,
runfiles = ctx.runfiles(
files = [data_dir],
transitive_files = c.runfiles,
collect_default = True,
),
)
Expand Down
138 changes: 86 additions & 52 deletions tests/stackage_zlib_runpath/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
load(
"@rules_haskell//haskell:cabal.bzl",
"haskell_cabal_binary",
)
load(
"//tests:inline_tests.bzl",
"py_inline_test",
Expand All @@ -19,12 +23,23 @@ dynamic_libraries(
tags = ["requires_zlib"],
)

# Tests that haskell_cabal_library will generate a relative RUNPATH entry for
# the dependency on the nixpkgs provided libz. Relative meaning an entry that
# starts with $ORIGIN (Linux) or @loader_path (MacOS). The alternative is an
# absolute path, which would be wrong for the nixpkgs provided libz, as we want
# the RUNPATH entry to point to Bazel's _solib_<cpu> directory and its absolute
# path depends on the output root or execroot.
haskell_cabal_binary(
name = "cabal-binary",
srcs = glob(["cabal-binary/**"]),
tags = ["requires_zlib"],
deps = [
"//tests/hackage:base",
# Depend transitively on libz.
"@stackage-zlib//:zlib",
],
)

# Tests that haskell_cabal_library|binary will generate a relative RUNPATH
# entry for the dependency on the nixpkgs provided libz. Relative meaning an
# entry that starts with $ORIGIN (Linux) or @loader_path (MacOS). The
# alternative is an absolute path, which would be wrong for the nixpkgs
# provided libz, as we want the RUNPATH entry to point to Bazel's _solib_<cpu>
# directory and its absolute path depends on the output root or execroot.
#
# It uses :libz_soname generated above to determine the expected RUNPATH entry
# for the libz dependency. The :libz_soname file will contain the file names of
Expand All @@ -33,17 +48,22 @@ dynamic_libraries(
# It uses :libHSzlib to access the dynamic library output of
# haskell_cabal_library and read the RUNPATH entries.
#
# Note, ideally we would test that haskell_cabal_library _only_ generates a
# relative RUNPATH entry and no absolute entries that leak the execroot into
# the cache. Unfortunately, haskell_cabal_library generates such an entry at
# the moment. See https://github.com/tweag/rules_haskell/issues/1130.
# It uses :cabal-binary to access a binary that transitively depends on libz.
#
# Note, ideally we would test that haskell_cabal_library|binary _only_
# generates a relative RUNPATH entry and no absolute entries that leak the
# execroot into the cache. Unfortunately, haskell_cabal_library|binary
# generates such an entry at the moment. See
# https://github.com/tweag/rules_haskell/issues/1130.
py_inline_test(
name = "stackage_zlib_runpath",
args = [
"$(rootpath :libz_soname)",
"$(rootpath :libHSzlib)",
"$(rootpath :cabal-binary)",
],
data = [
":cabal-binary",
":libHSzlib",
":libz_soname",
],
Expand All @@ -65,57 +85,71 @@ with open(libz_soname) as fh:
sofile = fh.read().splitlines()[1]
sodir = os.path.dirname(sofile)
# Determine libHSzlib RUNPATH
# Locate test artifacts.
libHSzlib = r.Rlocation(os.path.join(
os.environ["TEST_WORKSPACE"],
sys.argv[2],
))
runpaths = []
if platform.system() == "Darwin":
dynamic_section = iter(subprocess.check_output(["otool", "-l", libHSzlib]).decode().splitlines())
# otool produces lines of the form
#
# Load command ...
# cmd LC_RPATH
# cmdsize ...
# path ...
#
for line in dynamic_section:
# Find LC_RPATH entry
if line.find("cmd LC_RPATH") != -1:
break
# Skip until path field
cabal_binary = r.Rlocation(os.path.join(
os.environ["TEST_WORKSPACE"],
sys.argv[3],
))
def read_runpaths(binary):
runpaths = []
if platform.system() == "Darwin":
dynamic_section = iter(subprocess.check_output(["otool", "-l", binary]).decode().splitlines())
# otool produces lines of the form
#
# Load command ...
# cmd LC_RPATH
# cmdsize ...
# path ...
#
for line in dynamic_section:
if line.strip().startswith("path"):
# Find LC_RPATH entry
if line.find("cmd LC_RPATH") != -1:
break
runpaths.append(line.split()[1])
else:
dynamic_section = subprocess.check_output(["objdump", "--private-headers", libHSzlib]).decode().splitlines()
# objdump produces lines of the form
#
# Dynamic Section:
# ...
# RUNPATH ...
# ...
for line in dynamic_section:
if not line.strip().startswith("RUNPATH"):
# Skip until path field
for line in dynamic_section:
if line.strip().startswith("path"):
break
runpaths.append(line.split()[1])
else:
dynamic_section = subprocess.check_output(["objdump", "--private-headers", binary]).decode().splitlines()
# objdump produces lines of the form
#
# Dynamic Section:
# ...
# RUNPATH ...
# ...
for line in dynamic_section:
if not line.strip().startswith("RUNPATH"):
continue
runpaths.extend(line.split()[1].split(":"))
return runpaths
def test_binary(binary, sodir):
runpaths = read_runpaths(binary)
# Check that the binary contains a relative RUNPATH for sodir.
found = False
for runpath in runpaths:
if runpath.find(sodir) == -1:
continue
runpaths.extend(line.split()[1].split(":"))
if runpath.startswith("$ORIGIN") or runpath.startswith("@loader_path"):
found = True
# XXX: Enable once #1130 is fixed.
#if os.path.isabs(runpath):
# print("Absolute RUNPATH entry discovered for %s: %s" % (sodir, runpath))
# sys.exit(1)
if not found:
print("Did not find a relative RUNPATH entry for %s among %s." % (sodir, runpaths))
# Check that the binary contains a relative RUNPATH for sodir.
found = False
for runpath in runpaths:
if runpath.find(sodir) == -1:
continue
if runpath.startswith("$ORIGIN") or runpath.startswith("@loader_path"):
found = True
# XXX: Enable once #1130 is fixed.
#if os.path.isabs(runpath):
# print("Absolute RUNPATH entry discovered for %s: %s" % (sodir, runpath))
# sys.exit(1)
return found
if not found:
print("Did not find a relative RUNPATH entry for %s among %s." % (sodir, runpaths))
if not all(test_binary(binary, sodir) for binary in [libHSzlib, cabal_binary]):
sys.exit(1)
""",
tags = ["requires_zlib"],
Expand Down
4 changes: 4 additions & 0 deletions tests/stackage_zlib_runpath/cabal-binary/Main.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module Main where

main :: IO ()
main = pure ()
11 changes: 11 additions & 0 deletions tests/stackage_zlib_runpath/cabal-binary/cabal-binary.cabal
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
cabal-version: >=1.10
name: cabal-binary
version: 0.1.0.0
build-type: Simple

executable cabal-binary
build-depends:
base,
zlib
default-language: Haskell2010
main-is: Main.hs