"""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