diff options
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.bzl | 471 |
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 |