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
45 changes: 43 additions & 2 deletions haskell/cabal.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@ load(":private/context.bzl", "haskell_context", "render_env")
load(":private/dependencies.bzl", "gather_dep_info")
load(":private/expansions.bzl", "expand_make_variables")
load(":private/mode.bzl", "is_profiling_enabled")
load(":private/path_utils.bzl", "join_path_list", "truly_relativize")
load(
":private/path_utils.bzl",
"create_rpath_entry",
"join_path_list",
"relative_rpath_prefix",
"truly_relativize",
)
load(":private/set.bzl", "set")
load(":haddock.bzl", "generate_unified_haddock_info")
load(
Expand Down Expand Up @@ -103,7 +109,27 @@ def _cabal_tool_flag(tool):
def _binary_paths(binaries):
return [binary.dirname for binary in binaries.to_list()]

def _prepare_cabal_inputs(hs, cc, posix, dep_info, cc_info, direct_cc_info, component, package_id, tool_inputs, tool_input_manifests, cabal, setup, srcs, compiler_flags, flags, generate_haddock, cabal_wrapper, package_database, verbose):
def _prepare_cabal_inputs(
hs,
cc,
posix,
dep_info,
cc_info,
direct_cc_info,
component,
package_id,
tool_inputs,
tool_input_manifests,
cabal,
setup,
srcs,
compiler_flags,
flags,
generate_haddock,
cabal_wrapper,
package_database,
verbose,
dynamic_binary = None):
"""Compute Cabal wrapper, arguments, inputs."""
with_profiling = is_profiling_enabled(hs)

Expand Down Expand Up @@ -133,6 +159,19 @@ def _prepare_cabal_inputs(hs, cc, posix, dep_info, cc_info, direct_cc_info, comp
args.add_all([component, package_id, generate_haddock, setup, cabal.dirname, package_database.dirname])
args.add("--flags=" + " ".join(flags))
args.add_all(compiler_flags, format_each = "--ghc-option=%s")
if dynamic_binary:
args.add_all(
[
"--ghc-option=-optl-Wl,-rpath," + create_rpath_entry(
binary = dynamic_binary,
dependency = lib,
keep_filename = False,
prefix = relative_rpath_prefix(hs.toolchain.is_darwin),
)
for lib in direct_libs
],
uniquify = True,
)
args.add("--")
args.add_all(package_databases, map_each = _dirname, format_each = "--package-db=%s")
args.add_all(direct_include_dirs, format_each = "--extra-include-dirs=%s")
Expand Down Expand Up @@ -260,6 +299,7 @@ def _haskell_cabal_library_impl(ctx):
cabal_wrapper = ctx.executable._cabal_wrapper,
package_database = package_database,
verbose = ctx.attr.verbose,
dynamic_binary = dynamic_library,
)
outputs = [
package_database,
Expand Down Expand Up @@ -497,6 +537,7 @@ def _haskell_cabal_binary_impl(ctx):
cabal_wrapper = ctx.executable._cabal_wrapper,
package_database = package_database,
verbose = ctx.attr.verbose,
dynamic_binary = binary,
)
ctx.actions.run(
executable = c.cabal_wrapper,
Expand Down
3 changes: 2 additions & 1 deletion haskell/private/cc_libraries.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ load(
"get_lib_name",
"mangle_static_library",
"rel_to_pkgroot",
"relative_rpath_prefix",
"target_unique_name",
)
load(
Expand Down Expand Up @@ -211,7 +212,7 @@ def create_link_config(hs, posix, cc_libraries_info, libraries_to_link, binary,
binary = binary,
dependency = lib,
keep_filename = False,
prefix = "@loader_path" if hs.toolchain.is_darwin else "$ORIGIN",
prefix = relative_rpath_prefix(hs.toolchain.is_darwin),
)
for lib in dynamic_libs
]),
Expand Down
14 changes: 14 additions & 0 deletions haskell/private/path_utils.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,20 @@ def _get_target_parent_dir(target):
__check_dots(target, parent_dir)
return (False, parent_dir)

def relative_rpath_prefix(is_darwin):
"""Returns the prefix for relative RUNPATH entries.

Args:
is_darwin: Whether the target platform is Darwin.

Returns:
string, `@loader_path` on Darwin, `$ORIGIN` otherwise.
"""
if is_darwin:
return "@loader_path"
else:
return "$ORIGIN"

# tests in /tests/unit_tests/BUILD
def create_rpath_entry(
binary,
Expand Down
122 changes: 122 additions & 0 deletions tests/stackage_zlib_runpath/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
load(
"//tests:inline_tests.bzl",
"py_inline_test",
)
load("dynamic_libraries.bzl", "dynamic_libraries")

dynamic_libraries(
name = "libz",
srcs = ["@zlib.dev//:zlib"],
filter = "libz",
solib_names = "libz_soname",
tags = ["requires_zlib"],
)

dynamic_libraries(
name = "libHSzlib",
srcs = ["@stackage-zlib//:zlib"],
filter = "libHSz",
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.
#
# 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
# the libz library files underneath the `_solib_<cpu>` directory.
#
# 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.
py_inline_test(
name = "stackage_zlib_runpath",
args = [
"$(rootpath :libz_soname)",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are solib_names exported as targets?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's an output attribute that means that it becomes accessible as a label.

"$(rootpath :libHSzlib)",
],
data = [
":libHSzlib",
":libz_soname",
],
script = """\
from bazel_tools.tools.python.runfiles import runfiles as bazel_runfiles
import itertools
import os
import platform
import subprocess
import sys
r = bazel_runfiles.Create()

# Determine libz solib directory
libz_soname = r.Rlocation(os.path.join(
os.environ["TEST_WORKSPACE"],
sys.argv[1],
))
with open(libz_soname) as fh:
sofile = fh.read().splitlines()[1]
sodir = os.path.dirname(sofile)

# Determine libHSzlib RUNPATH
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
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", libHSzlib]).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(":"))

# 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)

if not found:
print("Did not find a relative RUNPATH entry for %s among %s." % (sodir, runpaths))
sys.exit(1)
""",
tags = ["requires_zlib"],
)
38 changes: 38 additions & 0 deletions tests/stackage_zlib_runpath/dynamic_libraries.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
def _dynamic_libraries_impl(ctx):
outputs = []
solib_names = []
for target in ctx.attr.srcs:
cc_info = target[CcInfo]
for library_to_link in cc_info.linking_context.libraries_to_link.to_list():
library = library_to_link.resolved_symlink_dynamic_library
if not library or library.basename.find(ctx.attr.filter) == -1:
continue
outputs.append(library)
if library_to_link.dynamic_library:
solib_names.append(library_to_link.dynamic_library.short_path)
if ctx.attr.solib_names:
ctx.actions.write(
ctx.outputs.solib_names,
"\n".join(solib_names),
)
return [DefaultInfo(
files = depset(outputs),
runfiles = ctx.runfiles(files = outputs),
)]

dynamic_libraries = rule(
Comment thread
Profpatsch marked this conversation as resolved.
_dynamic_libraries_impl,
attrs = {
"filter": attr.string(
doc = "Skip libraries that do not contain this string in their name.",
),
"srcs": attr.label_list(
doc = "Extract dynamic libraries from these targets",
providers = [CcInfo],
),
"solib_names": attr.output(
doc = "Write the `_solib_<cpu>` paths of the dynamic libraries to this file.",
),
},
doc = "Extract the dynamic libraries from cc_library targets.",
)