about summary refs log tree commit diff
path: root/third_party/bazel/rules_haskell/haskell/private/path_utils.bzl
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/bazel/rules_haskell/haskell/private/path_utils.bzl')
-rw-r--r--third_party/bazel/rules_haskell/haskell/private/path_utils.bzl471
1 files changed, 471 insertions, 0 deletions
diff --git a/third_party/bazel/rules_haskell/haskell/private/path_utils.bzl b/third_party/bazel/rules_haskell/haskell/private/path_utils.bzl
new file mode 100644
index 000000000000..1162a95aebe1
--- /dev/null
+++ b/third_party/bazel/rules_haskell/haskell/private/path_utils.bzl
@@ -0,0 +1,471 @@
+"""Utilities for module and path manipulations."""
+
+load("@bazel_skylib//lib:paths.bzl", "paths")
+load(":private/set.bzl", "set")
+
+def module_name(hs, f, rel_path = None):
+    """Given Haskell source file path, turn it into a dot-separated module name.
+
+    module_name(
+      hs,
+      "some-workspace/some-package/src/Foo/Bar/Baz.hs",
+    ) => "Foo.Bar.Baz"
+
+    Args:
+      hs:  Haskell context.
+      f:   Haskell source file.
+      rel_path: Explicit relative path from import root to the module, or None
+        if it should be deduced.
+
+    Returns:
+      string: Haskell module name.
+    """
+
+    rpath = rel_path
+
+    if not rpath:
+        rpath = _rel_path_to_module(hs, f)
+
+    (hsmod, _) = paths.split_extension(rpath.replace("/", "."))
+    return hsmod
+
+def target_unique_name(hs, name_prefix):
+    """Make a target-unique name.
+
+    `name_prefix` is made target-unique by adding a rule name
+    suffix to it. This means that given two different rules, the same
+    `name_prefix` is distinct. Note that this is does not disambiguate two
+    names within the same rule. Given a haskell_library with name foo
+    you could expect:
+
+    target_unique_name(hs, "libdir") => "libdir-foo"
+
+    This allows two rules using same name_prefix being built in same
+    environment to avoid name clashes of their output files and directories.
+
+    Args:
+      hs:          Haskell context.
+      name_prefix: Template for the name.
+
+    Returns:
+      string: Target-unique name_prefix.
+    """
+    return "{0}-{1}".format(name_prefix, hs.name)
+
+def module_unique_name(hs, source_file, name_prefix):
+    """Make a target- and module- unique name.
+
+    module_unique_name(
+      hs,
+      "some-workspace/some-package/src/Foo/Bar/Baz.hs",
+      "libdir"
+    ) => "libdir-foo-Foo.Bar.Baz"
+
+    This is quite similar to `target_unique_name` but also uses a path built
+    from `source_file` to prevent clashes with other names produced using the
+    same `name_prefix`.
+
+    Args:
+      hs:          Haskell context.
+      source_file: Source file name.
+      name_prefix: Template for the name.
+
+    Returns:
+      string: Target- and source-unique name.
+    """
+    return "{0}-{1}".format(
+        target_unique_name(hs, name_prefix),
+        module_name(hs, source_file),
+    )
+
+def declare_compiled(hs, src, ext, directory = None, rel_path = None):
+    """Given a Haskell-ish source file, declare its output.
+
+    Args:
+      hs: Haskell context.
+      src: Haskell source file.
+      ext: New extension.
+      directory: String, directory prefix the new file should live in.
+      rel_path: Explicit relative path from import root to the module, or None
+        if it should be deduced.
+
+    Returns:
+      File: Declared output file living in `directory` with given `ext`.
+    """
+
+    rpath = rel_path
+
+    if not rpath:
+        rpath = _rel_path_to_module(hs, src)
+
+    fp = paths.replace_extension(rpath, ext)
+    fp_with_dir = fp if directory == None else paths.join(directory, fp)
+
+    return hs.actions.declare_file(fp_with_dir)
+
+def make_path(libs, prefix = None, sep = None):
+    """Return a string value for using as LD_LIBRARY_PATH or similar.
+
+    Args:
+      libs: List of library files that should be available
+      prefix: String, an optional prefix to add to every path.
+      sep: String, the path separator, defaults to ":".
+
+    Returns:
+      String: paths to the given library directories separated by ":".
+    """
+    r = set.empty()
+
+    sep = sep if sep else ":"
+
+    for lib in libs:
+        lib_dir = paths.dirname(lib.path)
+        if prefix:
+            lib_dir = paths.join(prefix, lib_dir)
+
+        set.mutable_insert(r, lib_dir)
+
+    return sep.join(set.to_list(r))
+
+def darwin_convert_to_dylibs(hs, libs):
+    """Convert .so dynamic libraries to .dylib.
+
+    Bazel's cc_library rule will create .so files for dynamic libraries even
+    on MacOS. GHC's builtin linker, which is used during compilation, GHCi,
+    or doctests, hard-codes the assumption that all dynamic libraries on MacOS
+    end on .dylib. This function serves as an adaptor and produces symlinks
+    from a .dylib version to the .so version for every dynamic library
+    dependencies that does not end on .dylib.
+
+    Args:
+      hs: Haskell context.
+      libs: List of library files dynamic or static.
+
+    Returns:
+      List of library files where all dynamic libraries end on .dylib.
+    """
+    lib_prefix = "_dylibs"
+    new_libs = []
+    for lib in libs:
+        if is_shared_library(lib) and lib.extension != "dylib":
+            dylib_name = paths.join(
+                target_unique_name(hs, lib_prefix),
+                lib.dirname,
+                "lib" + get_lib_name(lib) + ".dylib",
+            )
+            dylib = hs.actions.declare_file(dylib_name)
+            ln(hs, lib, dylib)
+            new_libs.append(dylib)
+        else:
+            new_libs.append(lib)
+    return new_libs
+
+def windows_convert_to_dlls(hs, libs):
+    """Convert .so dynamic libraries to .dll.
+
+    Bazel's cc_library rule will create .so files for dynamic libraries even
+    on Windows. GHC's builtin linker, which is used during compilation, GHCi,
+    or doctests, hard-codes the assumption that all dynamic libraries on Windows
+    end on .dll. This function serves as an adaptor and produces symlinks
+    from a .dll version to the .so version for every dynamic library
+    dependencies that does not end on .dll.
+
+    Args:
+      hs: Haskell context.
+      libs: List of library files dynamic or static.
+
+    Returns:
+      List of library files where all dynamic libraries end on .dll.
+    """
+    lib_prefix = "_dlls"
+    new_libs = []
+    for lib in libs:
+        if is_shared_library(lib) and lib.extension != "dll":
+            dll_name = paths.join(
+                target_unique_name(hs, lib_prefix),
+                paths.dirname(lib.short_path),
+                "lib" + get_lib_name(lib) + ".dll",
+            )
+            dll = hs.actions.declare_file(dll_name)
+            ln(hs, lib, dll)
+            new_libs.append(dll)
+        else:
+            new_libs.append(lib)
+    return new_libs
+
+def get_lib_name(lib):
+    """Return name of library by dropping extension and "lib" prefix.
+
+    Args:
+      lib: The library File.
+
+    Returns:
+      String: name of library.
+    """
+
+    base = lib.basename[3:] if lib.basename[:3] == "lib" else lib.basename
+    n = base.find(".so.")
+    end = paths.replace_extension(base, "") if n == -1 else base[:n]
+    return end
+
+def link_libraries(libs_to_link, args):
+    """Add linker flags to link against the given libraries.
+
+    Args:
+      libs_to_link: List of library Files.
+      args: Append arguments to this list.
+
+    Returns:
+      List of library names that were linked.
+
+    """
+    seen_libs = set.empty()
+    libraries = []
+    for lib in libs_to_link:
+        lib_name = get_lib_name(lib)
+        if not set.is_member(seen_libs, lib_name):
+            set.mutable_insert(seen_libs, lib_name)
+            args += ["-l{0}".format(lib_name)]
+            libraries.append(lib_name)
+
+def is_shared_library(f):
+    """Check if the given File is a shared library.
+
+    Args:
+      f: The File to check.
+
+    Returns:
+      Bool: True if the given file `f` is a shared library, False otherwise.
+    """
+    return f.extension in ["so", "dylib"] or f.basename.find(".so.") != -1
+
+def is_static_library(f):
+    """Check if the given File is a static library.
+
+    Args:
+      f: The File to check.
+
+    Returns:
+      Bool: True if the given file `f` is a static library, False otherwise.
+    """
+    return f.extension in ["a"]
+
+def _rel_path_to_module(hs, f):
+    """Make given file name relative to the directory where the module hierarchy
+    starts.
+
+    _rel_path_to_module(
+      "some-workspace/some-package/src/Foo/Bar/Baz.hs"
+    ) => "Foo/Bar/Baz.hs"
+
+    Args:
+      hs:  Haskell context.
+      f:   Haskell source file.
+
+    Returns:
+      string: Relative path to module file.
+    """
+
+    # If it's a generated file, strip off the bin or genfiles prefix.
+    path = f.path
+    if path.startswith(hs.bin_dir.path):
+        path = paths.relativize(path, hs.bin_dir.path)
+    elif path.startswith(hs.genfiles_dir.path):
+        path = paths.relativize(path, hs.genfiles_dir.path)
+
+    return paths.relativize(path, hs.src_root)
+
+# TODO Consider merging with paths.relativize. See
+# https://github.com/bazelbuild/bazel-skylib/pull/44.
+def _truly_relativize(target, relative_to):
+    """Return a relative path to `target` from `relative_to`.
+
+    Args:
+      target: string, path to directory we want to get relative path to.
+      relative_to: string, path to directory from which we are starting.
+
+    Returns:
+      string: relative path to `target`.
+    """
+    t_pieces = target.split("/")
+    r_pieces = relative_to.split("/")
+    common_part_len = 0
+
+    for tp, rp in zip(t_pieces, r_pieces):
+        if tp == rp:
+            common_part_len += 1
+        else:
+            break
+
+    result = [".."] * (len(r_pieces) - common_part_len)
+    result += t_pieces[common_part_len:]
+
+    return "/".join(result)
+
+def ln(hs, target, link, extra_inputs = depset()):
+    """Create a symlink to target.
+
+    Args:
+      hs: Haskell context.
+      extra_inputs: extra phony dependencies of symlink.
+
+    Returns:
+      None
+    """
+    relative_target = _truly_relativize(target.path, link.dirname)
+    hs.actions.run_shell(
+        inputs = depset([target], transitive = [extra_inputs]),
+        outputs = [link],
+        mnemonic = "Symlink",
+        command = "ln -s {target} {link}".format(
+            target = relative_target,
+            link = link.path,
+        ),
+        use_default_shell_env = True,
+    )
+
+def link_forest(ctx, srcs, basePath = ".", **kwargs):
+    """Write a symlink to each file in `srcs` into a destination directory
+    defined using the same arguments as `ctx.actions.declare_directory`"""
+    local_files = []
+    for src in srcs.to_list():
+        dest = ctx.actions.declare_file(
+            paths.join(basePath, src.basename),
+            **kwargs
+        )
+        local_files.append(dest)
+        ln(ctx, src, dest)
+    return local_files
+
+def copy_all(ctx, srcs, dest):
+    """Copy all the files in `srcs` into `dest`"""
+    if list(srcs.to_list()) == []:
+        ctx.actions.run_shell(
+            command = "mkdir -p {dest}".format(dest = dest.path),
+            outputs = [dest],
+        )
+    else:
+        args = ctx.actions.args()
+        args.add_all(srcs)
+        ctx.actions.run_shell(
+            inputs = depset(srcs),
+            outputs = [dest],
+            mnemonic = "Copy",
+            command = "mkdir -p {dest} && cp -L -R \"$@\" {dest}".format(dest = dest.path),
+            arguments = [args],
+        )
+
+def parse_pattern(ctx, pattern_str):
+    """Parses a string label pattern.
+
+    Args:
+      ctx: Standard Bazel Rule context.
+
+      pattern_str: The pattern to parse.
+        Patterns are absolute labels in the local workspace. E.g.
+        `//some/package:some_target`. The following wild-cards are allowed:
+        `...`, `:all`, and `:*`. Also the `//some/package` shortcut is allowed.
+
+    Returns:
+      A struct of
+        package: A list of package path components. May end on the wildcard `...`.
+        target: The target name. None if the package ends on `...`. May be one
+          of the wildcards `all` or `*`.
+
+    NOTE: it would be better if Bazel itself exposed this functionality to Starlark.
+
+    Any feature using this function should be marked as experimental, until the
+    resolution of https://github.com/bazelbuild/bazel/issues/7763.
+    """
+
+    # We only load targets in the local workspace anyway. So, it's never
+    # necessary to specify a workspace. Therefore, we don't allow it.
+    if pattern_str.startswith("@"):
+        fail("Invalid haskell_repl pattern. Patterns may not specify a workspace. They only apply to the current workspace")
+
+    # To keep things simple, all patterns have to be absolute.
+    if not pattern_str.startswith("//"):
+        if not pattern_str.startswith(":"):
+            fail("Invalid haskell_repl pattern. Patterns must start with either '//' or ':'.")
+
+        # if the pattern string doesn't start with a package (it starts with :, e.g. :two),
+        # then we prepend the contextual package
+        pattern_str = "//{package}{target}".format(package = ctx.label.package, target = pattern_str)
+
+    # Separate package and target (if present).
+    package_target = pattern_str[2:].split(":", maxsplit = 2)
+    package_str = package_target[0]
+    target_str = None
+    if len(package_target) == 2:
+        target_str = package_target[1]
+
+    # Parse package pattern.
+    package = []
+    dotdotdot = False  # ... has to be last component in the pattern.
+    for s in package_str.split("/"):
+        if dotdotdot:
+            fail("Invalid haskell_repl pattern. ... has to appear at the end.")
+        if s == "...":
+            dotdotdot = True
+        package.append(s)
+
+    # Parse target pattern.
+    if dotdotdot:
+        if target_str != None:
+            fail("Invalid haskell_repl pattern. ... has to appear at the end.")
+    elif target_str == None:
+        if len(package) > 0 and package[-1] != "":
+            target_str = package[-1]
+        else:
+            fail("Invalid haskell_repl pattern. The empty string is not a valid target.")
+
+    return struct(
+        package = package,
+        target = target_str,
+    )
+
+def match_label(patterns, label):
+    """Whether the given local workspace label matches any of the patterns.
+
+    Args:
+      patterns: A list of parsed patterns to match the label against.
+        Apply `parse_pattern` before passing patterns into this function.
+      label: Match this label against the patterns.
+
+    Returns:
+      A boolean. True if the label is in the local workspace and matches any of
+      the given patterns. False otherwise.
+
+    NOTE: it would be better if Bazel itself exposed this functionality to Starlark.
+
+    Any feature using this function should be marked as experimental, until the
+    resolution of https://github.com/bazelbuild/bazel/issues/7763.
+    """
+
+    # Only local workspace labels can match.
+    # Despite the docs saying otherwise, labels don't have a workspace_name
+    # attribute. So, we use the workspace_root. If it's empty, the target is in
+    # the local workspace. Otherwise, it's an external target.
+    if label.workspace_root != "":
+        return False
+
+    package = label.package.split("/")
+    target = label.name
+
+    # Match package components.
+    for i in range(min(len(patterns.package), len(package))):
+        if patterns.package[i] == "...":
+            return True
+        elif patterns.package[i] != package[i]:
+            return False
+
+    # If no wild-card or mismatch was encountered, the lengths must match.
+    # Otherwise, the label's package is not covered.
+    if len(patterns.package) != len(package):
+        return False
+
+    # Match target.
+    if patterns.target == "all" or patterns.target == "*":
+        return True
+    else:
+        return patterns.target == target