diff options
author | Vincent Ambo <tazjin@google.com> | 2019-07-04T10·18+0100 |
---|---|---|
committer | Vincent Ambo <tazjin@google.com> | 2019-07-04T10·18+0100 |
commit | f723b8b878a3c4a4687b9e337a875500bebb39b1 (patch) | |
tree | e85204cf042c355e90cff61c111e7d8cd15df311 /third_party/bazel/rules_haskell/haskell/private | |
parent | 2eb1dc26e42ffbdc168f05ef744bd4b4f3e4c36f (diff) |
feat(third_party/bazel): Check in rules_haskell from Tweag r/17
Diffstat (limited to 'third_party/bazel/rules_haskell/haskell/private')
22 files changed, 4358 insertions, 0 deletions
diff --git a/third_party/bazel/rules_haskell/haskell/private/actions/compile.bzl b/third_party/bazel/rules_haskell/haskell/private/actions/compile.bzl new file mode 100644 index 000000000000..530b23a04b0f --- /dev/null +++ b/third_party/bazel/rules_haskell/haskell/private/actions/compile.bzl @@ -0,0 +1,563 @@ +"""Actions for compiling Haskell source code""" + +load(":private/packages.bzl", "expose_packages", "pkg_info_to_compile_flags") +load("@bazel_skylib//lib:dicts.bzl", "dicts") +load("@bazel_skylib//lib:paths.bzl", "paths") +load( + ":private/path_utils.bzl", + "declare_compiled", + "module_name", + "target_unique_name", +) +load(":private/pkg_id.bzl", "pkg_id") +load(":private/version_macros.bzl", "version_macro_includes") +load( + ":providers.bzl", + "GhcPluginInfo", + "get_libs_for_ghc_linker", + "merge_HaskellCcInfo", +) +load(":private/set.bzl", "set") + +def _process_hsc_file(hs, cc, hsc_flags, hsc_inputs, hsc_file): + """Process a single hsc file. + + Args: + hs: Haskell context. + cc: CcInteropInfo, information about C dependencies. + hsc_flags: extra flags to pass to hsc2hs + hsc_inputs: extra file inputs for the hsc2hs command + hsc_file: hsc file to process. + + Returns: + (File, string): Haskell source file created by processing hsc_file and + new import directory containing the produced file. + """ + args = hs.actions.args() + + # Output a Haskell source file. + hsc_dir_raw = paths.join("_hsc", hs.name) + hs_out = declare_compiled(hs, hsc_file, ".hs", directory = hsc_dir_raw) + args.add_all([hsc_file.path, "-o", hs_out.path]) + + args.add_all(["-c", cc.tools.cc]) + args.add_all(["-l", cc.tools.cc]) + args.add("-ighcplatform.h") + args.add("-ighcversion.h") + args.add_all(["--cflag=" + f for f in cc.cpp_flags]) + args.add_all(["--cflag=" + f for f in cc.compiler_flags]) + args.add_all(["--cflag=" + f for f in cc.include_args]) + args.add_all(["--lflag=" + f for f in cc.linker_flags]) + args.add_all(hsc_flags) + + # Add an empty PATH variable if not already specified in hs.env. + # Needed to avoid a "Couldn't read PATH" error on Windows. + # + # On Unix platforms, though, we musn't set PATH as it is automatically set up + # by the run action, unless already set in the env parameter. This triggers + # build errors when using GHC bindists on Linux. + if hs.env.get("PATH") == None and hs.toolchain.is_windows: + hs.env["PATH"] = "" + + hs.actions.run( + inputs = depset(transitive = [ + depset(cc.hdrs), + depset([hsc_file]), + depset(cc.files), + depset(hsc_inputs), + ]), + outputs = [hs_out], + mnemonic = "HaskellHsc2hs", + executable = hs.tools.hsc2hs, + arguments = [args], + env = hs.env, + ) + + idir = paths.join( + hs.bin_dir.path, + hs.label.package, + hsc_dir_raw, + ) + + return hs_out, idir + +def _compilation_defaults(hs, cc, java, dep_info, plugin_dep_info, srcs, import_dir_map, extra_srcs, user_compile_flags, with_profiling, my_pkg_id, version, plugins): + """Compute variables common to all compilation targets (binary and library). + + Returns: + struct with the following fields: + args: default argument list + compile_flags: arguments that were used to compile the package + inputs: default inputs + input_manifests: input manifests + outputs: default outputs + objects_dir: object files directory + interfaces_dir: interface files directory + source_files: set of files that contain Haskell modules + extra_source_files: depset of non-Haskell source files + import_dirs: c2hs Import hierarchy roots + env: default environment variables + """ + + compile_flags = [] + + # GHC expects the CC compiler as the assembler, but segregates the + # set of flags to pass to it when used as an assembler. So we have + # to set both -optc and -opta. + cc_args = [ + "-optc" + f + for f in cc.compiler_flags + ] + [ + "-opta" + f + for f in cc.compiler_flags + ] + compile_flags += cc_args + + interface_dir_raw = "_iface_prof" if with_profiling else "_iface" + object_dir_raw = "_obj_prof" if with_profiling else "_obj" + + # Declare file directories. + # + # NOTE: We could have used -outputdir here and a single output + # directory. But keeping interface and object files separate has + # one advantage: if interface files are invariant under + # a particular code change, then we don't need to rebuild + # downstream. + if my_pkg_id: + # If we're compiling a package, put the interfaces inside the + # package directory. + interfaces_dir = hs.actions.declare_directory( + paths.join( + pkg_id.to_string(my_pkg_id), + interface_dir_raw, + ), + ) + else: + interfaces_dir = hs.actions.declare_directory( + paths.join(interface_dir_raw, hs.name), + ) + objects_dir = hs.actions.declare_directory( + paths.join(object_dir_raw, hs.name), + ) + + # Default compiler flags. + compile_flags += hs.toolchain.compiler_flags + compile_flags += user_compile_flags + + # Work around macOS linker limits. This fix has landed in GHC HEAD, but is + # not yet in a release; plus, we still want to support older versions of + # GHC. For details, see: https://phabricator.haskell.org/D4714 + if hs.toolchain.is_darwin: + compile_flags += ["-optl-Wl,-dead_strip_dylibs"] + + compile_flags.extend( + pkg_info_to_compile_flags( + expose_packages( + dep_info, + lib_info = None, + use_direct = True, + use_my_pkg_id = my_pkg_id, + custom_package_databases = None, + version = version, + ), + ), + ) + compile_flags.extend( + pkg_info_to_compile_flags( + expose_packages( + plugin_dep_info, + lib_info = None, + use_direct = True, + use_my_pkg_id = my_pkg_id, + custom_package_databases = None, + version = version, + ), + for_plugin = True, + ), + ) + + header_files = [] + boot_files = [] + source_files = set.empty() + + # Forward all "-D" and "-optP-D" flags to hsc2hs + hsc_flags = [] + hsc_flags += ["--cflag=" + x for x in user_compile_flags if x.startswith("-D")] + hsc_flags += ["--cflag=" + x[len("-optP"):] for x in user_compile_flags if x.startswith("-optP-D")] + + hsc_inputs = [] + if version: + (version_macro_headers, version_macro_flags) = version_macro_includes(dep_info) + hsc_flags += ["--cflag=" + x for x in version_macro_flags] + hsc_inputs += set.to_list(version_macro_headers) + + # Add import hierarchy root. + # Note that this is not perfect, since GHC requires hs-boot files + # to be in the same directory as the corresponding .hs file. Thus + # the two must both have the same root; i.e., both plain files, + # both in bin_dir, or both in genfiles_dir. + + import_dirs = set.from_list([ + hs.src_root, + paths.join(hs.bin_dir.path, hs.src_root), + paths.join(hs.genfiles_dir.path, hs.src_root), + ]) + + for s in srcs: + if s.extension == "h": + header_files.append(s) + elif s.extension == "hsc": + s0, idir = _process_hsc_file(hs, cc, hsc_flags, hsc_inputs, s) + set.mutable_insert(source_files, s0) + set.mutable_insert(import_dirs, idir) + elif s.extension in ["hs-boot", "lhs-boot"]: + boot_files.append(s) + else: + set.mutable_insert(source_files, s) + + if s in import_dir_map: + idir = import_dir_map[s] + set.mutable_insert(import_dirs, idir) + + compile_flags += ["-i{0}".format(d) for d in set.to_list(import_dirs)] + + # Write the -optP flags to a parameter file because they can be very long on Windows + # e.g. 27Kb for grpc-haskell + # Equivalent to: compile_flags += ["-optP" + f for f in cc.cpp_flags] + optp_args_file = hs.actions.declare_file("optp_args_%s" % hs.name) + optp_args = hs.actions.args() + optp_args.add_all(cc.cpp_flags) + optp_args.set_param_file_format("multiline") + hs.actions.write(optp_args_file, optp_args) + compile_flags += ["-optP@" + optp_args_file.path] + + compile_flags += cc.include_args + + locale_archive_depset = ( + depset([hs.toolchain.locale_archive]) if hs.toolchain.locale_archive != None else depset() + ) + + # This is absolutely required otherwise GHC doesn't know what package it's + # creating `Name`s for to put them in Haddock interface files which then + # results in Haddock not being able to find names for linking in + # environment after reading its interface file later. + if my_pkg_id != None: + unit_id_args = [ + "-this-unit-id", + pkg_id.to_string(my_pkg_id), + "-optP-DCURRENT_PACKAGE_KEY=\"{}\"".format(pkg_id.to_string(my_pkg_id)), + ] + compile_flags += unit_id_args + + args = hs.actions.args() + + # Compilation mode. Allow rule-supplied compiler flags to override it. + if hs.mode == "opt": + args.add("-O2") + + args.add("-static") + if with_profiling: + args.add("-prof", "-fexternal-interpreter") + + # Common flags + args.add_all([ + "-v0", + "-no-link", + "-fPIC", + "-hide-all-packages", + # Should never trigger in sandboxed builds, but can be useful + # to debug issues in non-sandboxed builds. + "-Wmissing-home-modules", + ]) + + # Output directories + args.add_all([ + "-odir", + objects_dir.path, + "-hidir", + interfaces_dir.path, + ]) + + # Interface files with profiling have to have the extension "p_hi": + # https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/packages.html#installedpackageinfo-a-package-specification + # otherwise we won't be able to register them with ghc-pkg. + if with_profiling: + args.add_all([ + "-hisuf", + "p_hi", + "-osuf", + "p_o", + ]) + + args.add_all(compile_flags) + + # Plugins + for plugin in plugins: + args.add("-fplugin={}".format(plugin[GhcPluginInfo].module)) + for opt in plugin[GhcPluginInfo].args: + args.add_all(["-fplugin-opt", "{}:{}".format(plugin[GhcPluginInfo].module, opt)]) + + plugin_tool_inputs = [plugin[GhcPluginInfo].tool_inputs for plugin in plugins] + plugin_tool_input_manifests = [ + manifest + for plugin in plugins + for manifest in plugin[GhcPluginInfo].tool_input_manifests + ] + + # Pass source files + for f in set.to_list(source_files): + args.add(f) + + extra_source_files = depset( + transitive = [extra_srcs, depset(header_files), depset(boot_files)], + ) + + # Transitive library dependencies for runtime. + (library_deps, ld_library_deps, ghc_env) = get_libs_for_ghc_linker( + hs, + merge_HaskellCcInfo( + dep_info.transitive_cc_dependencies, + plugin_dep_info.transitive_cc_dependencies, + ), + ) + + return struct( + args = args, + compile_flags = compile_flags, + inputs = depset(transitive = [ + depset(header_files), + depset(boot_files), + set.to_depset(source_files), + extra_source_files, + depset(cc.hdrs), + set.to_depset(dep_info.package_databases), + set.to_depset(dep_info.interface_dirs), + depset(dep_info.static_libraries), + depset(dep_info.static_libraries_prof), + set.to_depset(dep_info.dynamic_libraries), + set.to_depset(plugin_dep_info.package_databases), + set.to_depset(plugin_dep_info.interface_dirs), + depset(plugin_dep_info.static_libraries), + depset(plugin_dep_info.static_libraries_prof), + set.to_depset(plugin_dep_info.dynamic_libraries), + depset(library_deps), + depset(ld_library_deps), + java.inputs, + locale_archive_depset, + depset(transitive = plugin_tool_inputs), + depset([optp_args_file]), + ]), + input_manifests = plugin_tool_input_manifests, + objects_dir = objects_dir, + interfaces_dir = interfaces_dir, + outputs = [objects_dir, interfaces_dir], + source_files = source_files, + extra_source_files = depset(transitive = [extra_source_files, depset([optp_args_file])]), + import_dirs = import_dirs, + env = dicts.add( + ghc_env, + java.env, + hs.env, + ), + ) + +def _hpc_compiler_args(hs): + hpcdir = "{}/{}/.hpc".format(hs.bin_dir.path, hs.package_root) + return ["-fhpc", "-hpcdir", hpcdir] + +def _coverage_datum(mix_file, src_file, target_label): + return struct( + mix_file = mix_file, + src_file = src_file, + target_label = target_label, + ) + +def compile_binary( + hs, + cc, + java, + dep_info, + plugin_dep_info, + srcs, + ls_modules, + import_dir_map, + extra_srcs, + user_compile_flags, + dynamic, + with_profiling, + main_function, + version, + inspect_coverage = False, + plugins = []): + """Compile a Haskell target into object files suitable for linking. + + Returns: + struct with the following fields: + object_files: list of static object files + object_dyn_files: list of dynamic object files + modules: set of module names + source_files: set of Haskell source files + """ + c = _compilation_defaults(hs, cc, java, dep_info, plugin_dep_info, srcs, import_dir_map, extra_srcs, user_compile_flags, with_profiling, my_pkg_id = None, version = version, plugins = plugins) + c.args.add_all(["-main-is", main_function]) + if dynamic: + # For binaries, GHC creates .o files even for code to be + # linked dynamically. So we have to force the object suffix to + # be consistent with the dynamic object suffix in the library + # case. + c.args.add_all(["-dynamic", "-osuf dyn_o"]) + + coverage_data = [] + if inspect_coverage: + c.args.add_all(_hpc_compiler_args(hs)) + for src_file in srcs: + module = module_name(hs, src_file) + mix_file = hs.actions.declare_file(".hpc/{module}.mix".format(module = module)) + coverage_data.append(_coverage_datum(mix_file, src_file, hs.label)) + + hs.toolchain.actions.run_ghc( + hs, + cc, + inputs = c.inputs, + input_manifests = c.input_manifests, + outputs = c.outputs + [datum.mix_file for datum in coverage_data], + mnemonic = "HaskellBuildBinary" + ("Prof" if with_profiling else ""), + progress_message = "HaskellBuildBinary {}".format(hs.label), + env = c.env, + arguments = c.args, + ) + + if with_profiling: + exposed_modules_file = None + else: + exposed_modules_file = hs.actions.declare_file( + target_unique_name(hs, "exposed-modules"), + ) + hs.actions.run( + inputs = [c.interfaces_dir, hs.toolchain.global_pkg_db], + outputs = [exposed_modules_file], + executable = ls_modules, + arguments = [ + c.interfaces_dir.path, + hs.toolchain.global_pkg_db.path, + "/dev/null", # no hidden modules + "/dev/null", # no reexported modules + exposed_modules_file.path, + ], + use_default_shell_env = True, + ) + + return struct( + objects_dir = c.objects_dir, + source_files = c.source_files, + extra_source_files = c.extra_source_files, + import_dirs = c.import_dirs, + compile_flags = c.compile_flags, + exposed_modules_file = exposed_modules_file, + coverage_data = coverage_data, + ) + +def compile_library( + hs, + cc, + java, + dep_info, + plugin_dep_info, + srcs, + ls_modules, + other_modules, + exposed_modules_reexports, + import_dir_map, + extra_srcs, + user_compile_flags, + with_shared, + with_profiling, + my_pkg_id, + plugins = []): + """Build arguments for Haskell package build. + + Returns: + struct with the following fields: + interfaces_dir: directory containing interface files + interface_files: list of interface files + object_files: list of static object files + object_dyn_files: list of dynamic object files + compile_flags: list of string arguments suitable for Haddock + modules: set of module names + source_files: set of Haskell module files + import_dirs: import directories that should make all modules visible (for GHCi) + """ + c = _compilation_defaults(hs, cc, java, dep_info, plugin_dep_info, srcs, import_dir_map, extra_srcs, user_compile_flags, with_profiling, my_pkg_id = my_pkg_id, version = my_pkg_id.version, plugins = plugins) + if with_shared: + c.args.add("-dynamic-too") + + coverage_data = [] + if hs.coverage_enabled: + c.args.add_all(_hpc_compiler_args(hs)) + for src_file in srcs: + pkg_id_string = pkg_id.to_string(my_pkg_id) + module = module_name(hs, src_file) + mix_file = hs.actions.declare_file(".hpc/{pkg}/{module}.mix".format(pkg = pkg_id_string, module = module)) + coverage_data.append(_coverage_datum(mix_file, src_file, hs.label)) + + hs.toolchain.actions.run_ghc( + hs, + cc, + inputs = c.inputs, + input_manifests = c.input_manifests, + outputs = c.outputs + [datum.mix_file for datum in coverage_data], + mnemonic = "HaskellBuildLibrary" + ("Prof" if with_profiling else ""), + progress_message = "HaskellBuildLibrary {}".format(hs.label), + env = c.env, + arguments = c.args, + ) + + if with_profiling: + exposed_modules_file = None + else: + hidden_modules_file = hs.actions.declare_file( + target_unique_name(hs, "hidden-modules"), + ) + hs.actions.write( + output = hidden_modules_file, + content = ", ".join(other_modules), + ) + reexported_modules_file = hs.actions.declare_file( + target_unique_name(hs, "reexported-modules"), + ) + hs.actions.write( + output = reexported_modules_file, + content = ", ".join(exposed_modules_reexports), + ) + exposed_modules_file = hs.actions.declare_file( + target_unique_name(hs, "exposed-modules"), + ) + hs.actions.run( + inputs = [ + c.interfaces_dir, + hs.toolchain.global_pkg_db, + hidden_modules_file, + reexported_modules_file, + ], + outputs = [exposed_modules_file], + executable = ls_modules, + arguments = [ + c.interfaces_dir.path, + hs.toolchain.global_pkg_db.path, + hidden_modules_file.path, + reexported_modules_file.path, + exposed_modules_file.path, + ], + use_default_shell_env = True, + ) + + return struct( + interfaces_dir = c.interfaces_dir, + objects_dir = c.objects_dir, + compile_flags = c.compile_flags, + source_files = c.source_files, + extra_source_files = c.extra_source_files, + import_dirs = c.import_dirs, + exposed_modules_file = exposed_modules_file, + coverage_data = coverage_data, + ) diff --git a/third_party/bazel/rules_haskell/haskell/private/actions/link.bzl b/third_party/bazel/rules_haskell/haskell/private/actions/link.bzl new file mode 100644 index 000000000000..65cd2c6e4327 --- /dev/null +++ b/third_party/bazel/rules_haskell/haskell/private/actions/link.bzl @@ -0,0 +1,667 @@ +"""Actions for linking object code produced by compilation""" + +load(":private/packages.bzl", "expose_packages", "pkg_info_to_compile_flags") +load("@bazel_skylib//lib:paths.bzl", "paths") +load( + ":private/path_utils.bzl", + "get_lib_name", + "is_shared_library", + "is_static_library", + "ln", +) +load(":private/pkg_id.bzl", "pkg_id") +load(":private/set.bzl", "set") +load(":private/list.bzl", "list") + +# tests in /tests/unit_tests/BUILD +def parent_dir_path(path): + """Returns the path of the parent directory. + For a relative path with just a file, "." is returned. + The path is not normalized. + + foo => . + foo/ => foo + foo/bar => foo + foo/bar/baz => foo/bar + foo/../bar => foo/.. + + Args: + a path string + + Returns: + A path list of the form `["foo", "bar"]` + """ + path_dir = paths.dirname(path) + + # dirname returns "" if there is no parent directory + # In that case we return the identity path, which is ".". + if path_dir == "": + return ["."] + else: + return path_dir.split("/") + +def __check_dots(target, path): + # there’s still (non-leading) .. in split + if ".." in path: + fail("the short_path of target {} (which is {}) contains more dots than loading `../`. We can’t handle that.".format( + target, + target.short_path, + )) + +# skylark doesn’t allow nested defs, which is a mystery. +def _get_target_parent_dir(target): + """get the parent dir and handle leading short_path dots, + which signify that the target is in an external repository. + + Args: + target: a target, .short_path is used + Returns: + (is_external, parent_dir) + `is_external`: Bool whether the path points to an external repository + `parent_dir`: The parent directory, either up to the runfiles toplel, + up to the external repository toplevel. + Is `[]` if there is no parent dir. + """ + + parent_dir = parent_dir_path(target.short_path) + + if parent_dir[0] == "..": + __check_dots(target, parent_dir[1:]) + return (True, parent_dir[1:]) + elif parent_dir[0] == ".": + return (False, []) + else: + __check_dots(target, parent_dir) + return (False, parent_dir) + +# tests in /tests/unit_tests/BUILD +def create_rpath_entry( + binary, + dependency, + keep_filename, + prefix = ""): + """Return a (relative) path that points from `binary` to `dependecy` + while not leaving the current bazel runpath, taking into account weird + corner cases of `.short_path` concerning external repositories. + The resulting entry should be able to be inserted into rpath or similar. + + Examples: + + bin.short_path=foo/a.so and dep.short_path=bar/b.so + => create_rpath_entry(bin, dep, False) = ../bar + and + create_rpath_entry(bin, dep, True) = ../bar/b.so + and + create_rpath_entry(bin, dep, True, "$ORIGIN") = $ORIGIN/../bar/b.so + + Args: + binary: target of current binary + dependency: target of dependency to relatively point to + keep_filename: whether to point to the filename or its parent dir + prefix: string path prefix to add before the relative path + + Returns: + relative path string + """ + + (bin_is_external, bin_parent_dir) = _get_target_parent_dir(binary) + (dep_is_external, dep_parent_dir) = _get_target_parent_dir(dependency) + + # backup through parent directories of the binary, + # to the runfiles directory + bin_backup = [".."] * len(bin_parent_dir) + + # external repositories live in `target.runfiles/external`, + # while the internal repository lives in `target.runfiles`. + # The `.short_path`s of external repositories are strange, + # they start with `../`, but you cannot just append that in + # order to find the correct runpath. Instead you have to use + # the following logic to construct the correct runpaths: + if bin_is_external: + if dep_is_external: + # stay in `external` + path_segments = bin_backup + else: + # backup out of `external` + path_segments = [".."] + bin_backup + elif dep_is_external: + # go into `external` + path_segments = bin_backup + ["external"] + else: + # no special external traversal + path_segments = bin_backup + + # then add the parent dir to our dependency + path_segments.extend(dep_parent_dir) + + # optionally add the filename + if keep_filename: + path_segments.append( + paths.basename(dependency.short_path), + ) + + # normalize for good measure and create the final path + path = paths.normalize("/".join(path_segments)) + + # and add the prefix if applicable + if prefix == "": + return path + else: + return prefix + "/" + path + +def _merge_parameter_files(hs, file1, file2): + """Merge two GHC parameter files into one. + + Args: + hs: Haskell context. + file1: The first parameter file. + file2: The second parameter file. + + Returns: + File: A new parameter file containing the parameters of both input files. + The file name is based on the file names of the input files. The file + is located next to the first input file. + """ + params_file = hs.actions.declare_file( + file1.basename + ".and." + file2.basename, + sibling = file1, + ) + hs.actions.run_shell( + inputs = [file1, file2], + outputs = [params_file], + command = """ + cat {file1} {file2} > {out} + """.format( + file1 = file1.path, + file2 = file2.path, + out = params_file.path, + ), + ) + return params_file + +def _darwin_create_extra_linker_flags_file(hs, cc, objects_dir, executable, dynamic, solibs): + """Write additional linker flags required on MacOS to a parameter file. + + Args: + hs: Haskell context. + cc: CcInteropInfo, information about C dependencies. + objects_dir: Directory storing object files. + Used to determine output file location. + executable: The executable being built. + dynamic: Bool: Whether to link dynamically or statically. + solibs: List of dynamic library dependencies. + + Returns: + File: Parameter file with additional linker flags. To be passed to GHC. + """ + + # On Darwin GHC will pass the dead_strip_dylibs flag to the linker. This + # flag will remove any shared library loads from the binary's header that + # are not directly resolving undefined symbols in the binary. I.e. any + # indirect shared library dependencies will be removed. This conflicts with + # Bazel's builtin cc rules, which assume that the final binary will load + # all transitive shared library dependencies. In particlar shared libraries + # produced by Bazel's cc rules never load shared libraries themselves. This + # causes missing symbols at runtime on MacOS, see #170. + # + # The following work-around applies the `-u` flag to the linker for any + # symbol that is undefined in any transitive shared library dependency. + # This forces the linker to resolve these undefined symbols in all + # transitive shared library dependencies and keep the corresponding load + # commands in the binary's header. + # + # Unfortunately, this prohibits elimination of any truly redundant shared + # library dependencies. Furthermore, the transitive closure of shared + # library dependencies can be large, so this makes it more likely to exceed + # the MACH-O header size limit on MacOS. + # + # This is a horrendous hack, but it seems to be forced on us by how Bazel + # builds dynamic cc libraries. + suffix = ".dynamic.linker_flags" if dynamic else ".static.linker_flags" + linker_flags_file = hs.actions.declare_file( + executable.basename + suffix, + sibling = objects_dir, + ) + + hs.actions.run_shell( + inputs = solibs, + outputs = [linker_flags_file], + command = """ + touch {out} + for lib in {solibs}; do + {nm} -u "$lib" | sed 's/^/-optl-Wl,-u,/' >> {out} + done + """.format( + nm = cc.tools.nm, + solibs = " ".join(["\"" + l.path + "\"" for l in solibs]), + out = linker_flags_file.path, + ), + ) + return linker_flags_file + +def _create_objects_dir_manifest(hs, objects_dir, dynamic, with_profiling): + suffix = ".dynamic.manifest" if dynamic else ".static.manifest" + objects_dir_manifest = hs.actions.declare_file( + objects_dir.basename + suffix, + sibling = objects_dir, + ) + + if with_profiling: + ext = "p_o" + elif dynamic: + ext = "dyn_o" + else: + ext = "o" + hs.actions.run_shell( + inputs = [objects_dir], + outputs = [objects_dir_manifest], + command = """ + find {dir} -name '*.{ext}' > {out} + """.format( + dir = objects_dir.path, + ext = ext, + out = objects_dir_manifest.path, + ), + use_default_shell_env = True, + ) + + return objects_dir_manifest + +def _link_dependencies(hs, dep_info, dynamic, binary, args): + """Configure linker flags and inputs. + + Configure linker flags for C library dependencies and runtime dynamic + library dependencies. And collect the C libraries to pass as inputs to + the linking action. + + Args: + hs: Haskell context. + dep_info: HaskellInfo provider. + dynamic: Bool: Whether to link dynamically, or statically. + binary: Final linked binary. + args: Arguments to the linking action. + + Returns: + depset: C library dependencies to provide as input to the linking action. + """ + + # Pick linking context based on linking mode. + if dynamic: + link_ctx = dep_info.cc_dependencies.dynamic_linking + trans_link_ctx = dep_info.transitive_cc_dependencies.dynamic_linking + else: + link_ctx = dep_info.cc_dependencies.static_linking + trans_link_ctx = dep_info.transitive_cc_dependencies.static_linking + + # Direct C library dependencies to link. + # I.e. not indirect through another Haskell dependency. + # Such indirect dependencies are linked by GHC based on the extra-libraries + # fields in the dependency's package configuration file. + libs_to_link = link_ctx.libraries_to_link.to_list() + _add_external_libraries(args, libs_to_link) + + # Transitive library dependencies to have in scope for linking. + trans_libs_to_link = trans_link_ctx.libraries_to_link.to_list() + + # Libraries to pass as inputs to linking action. + cc_link_libs = depset(transitive = [ + depset(trans_libs_to_link), + ]) + + # Transitive dynamic library dependencies to have in RUNPATH. + cc_solibs = trans_link_ctx.dynamic_libraries_for_runtime.to_list() + + # Collect Haskell dynamic library dependencies in common RUNPATH. + # This is to keep the number of RUNPATH entries low, for faster loading + # and to avoid exceeding the MACH-O header size limit on MacOS. + hs_solibs = [] + if dynamic: + hs_solibs_prefix = "_hssolib_%s" % hs.name + for dep in set.to_list(dep_info.dynamic_libraries): + dep_link = hs.actions.declare_file( + paths.join(hs_solibs_prefix, dep.basename), + sibling = binary, + ) + ln(hs, dep, dep_link) + hs_solibs.append(dep_link) + + # Configure RUNPATH. + rpaths = _infer_rpaths( + hs.toolchain.is_darwin, + binary, + trans_link_ctx.dynamic_libraries_for_runtime.to_list() + + hs_solibs, + ) + for rpath in set.to_list(rpaths): + args.add("-optl-Wl,-rpath," + rpath) + + return (cc_link_libs, cc_solibs, hs_solibs) + +def link_binary( + hs, + cc, + dep_info, + extra_srcs, + compiler_flags, + objects_dir, + dynamic, + with_profiling, + version): + """Link Haskell binary from static object files. + + Returns: + File: produced executable + """ + + exe_name = hs.name + (".exe" if hs.toolchain.is_windows else "") + executable = hs.actions.declare_file(exe_name) + + args = hs.actions.args() + args.add_all(["-optl" + f for f in cc.linker_flags]) + if with_profiling: + args.add("-prof") + args.add_all(hs.toolchain.compiler_flags) + args.add_all(compiler_flags) + + # By default, GHC will produce mostly-static binaries, i.e. in which all + # Haskell code is statically linked and foreign libraries and system + # dependencies are dynamically linked. If linkstatic is false, i.e. the user + # has requested fully dynamic linking, we must therefore add flags to make + # sure that GHC dynamically links Haskell code too. The one exception to + # this is when we are compiling for profiling, which currently does not play + # nicely with dynamic linking. + if dynamic: + if with_profiling: + print("WARNING: dynamic linking and profiling don't mix. Omitting -dynamic.\nSee https://ghc.haskell.org/trac/ghc/ticket/15394") + else: + args.add_all(["-pie", "-dynamic"]) + + # When compiling with `-threaded`, GHC needs to link against + # the pthread library when linking against static archives (.a). + # We assume it’s not a problem to pass it for other cases, + # so we just default to passing it. + args.add("-optl-pthread") + + args.add_all(["-o", executable.path]) + + # De-duplicate optl calls while preserving ordering: we want last + # invocation of an object to remain last. That is `-optl foo -optl + # bar -optl foo` becomes `-optl bar -optl foo`. Do this by counting + # number of occurrences. That way we only build dict and add to args + # directly rather than doing multiple reversals with temporary + # lists. + + args.add_all(pkg_info_to_compile_flags(expose_packages( + dep_info, + lib_info = None, + use_direct = True, + use_my_pkg_id = None, + custom_package_databases = None, + version = version, + ))) + + (cc_link_libs, cc_solibs, hs_solibs) = _link_dependencies( + hs = hs, + dep_info = dep_info, + dynamic = dynamic, + binary = executable, + args = args, + ) + + # XXX: Suppress a warning that Clang prints due to GHC automatically passing + # "-pie" or "-no-pie" to the C compiler. + # This is linked to https://ghc.haskell.org/trac/ghc/ticket/15319 + args.add_all([ + "-optc-Wno-unused-command-line-argument", + "-optl-Wno-unused-command-line-argument", + ]) + + objects_dir_manifest = _create_objects_dir_manifest( + hs, + objects_dir, + dynamic = dynamic, + with_profiling = with_profiling, + ) + + extra_linker_flags_file = None + if hs.toolchain.is_darwin: + args.add("-optl-Wl,-headerpad_max_install_names") + + # Nixpkgs commit 3513034208a introduces -liconv in NIX_LDFLAGS on + # Darwin. We don't currently handle NIX_LDFLAGS in any special + # way, so a hack is to simply do what NIX_LDFLAGS is telling us we + # should do always when using a toolchain from Nixpkgs. + # TODO remove this gross hack. + args.add("-liconv") + + extra_linker_flags_file = _darwin_create_extra_linker_flags_file( + hs, + cc, + objects_dir, + executable, + dynamic, + cc_solibs, + ) + + if extra_linker_flags_file != None: + params_file = _merge_parameter_files(hs, objects_dir_manifest, extra_linker_flags_file) + else: + params_file = objects_dir_manifest + + hs.toolchain.actions.run_ghc( + hs, + cc, + inputs = depset(transitive = [ + depset(extra_srcs), + set.to_depset(dep_info.package_databases), + set.to_depset(dep_info.dynamic_libraries), + depset(dep_info.static_libraries), + depset(dep_info.static_libraries_prof), + depset([objects_dir]), + cc_link_libs, + ]), + outputs = [executable], + mnemonic = "HaskellLinkBinary", + arguments = args, + params_file = params_file, + ) + + return (executable, cc_solibs + hs_solibs) + +def _add_external_libraries(args, ext_libs): + """Add options to `args` that allow us to link to `ext_libs`. + + Args: + args: Args object. + ext_libs: C library dependencies. + """ + + # Deduplicate the list of ext_libs based on their + # library name (file name stripped of lib prefix and endings). + # This keeps the command lines short, e.g. when a C library + # like `liblz4.so` appears in multiple dependencies. + # XXX: this is only done in here + # Shouldn’t the deduplication be applied to *all* external libraries? + deduped = list.dedup_on(get_lib_name, ext_libs) + + for lib in deduped: + args.add_all([ + "-L{0}".format( + paths.dirname(lib.path), + ), + "-l{0}".format( + # technically this is the second call to get_lib_name, + # but the added clarity makes up for it. + get_lib_name(lib), + ), + ]) + +def _infer_rpaths(is_darwin, target, solibs): + """Return set of RPATH values to be added to target so it can find all + solibs + + The resulting paths look like: + $ORIGIN/../../path/to/solib/dir + This means: "go upwards to your runfiles directory, then descend into + the parent folder of the solib". + + Args: + is_darwin: Whether we're compiling on and for Darwin. + target: File, executable or library we're linking. + solibs: A list of Files, shared objects that the target needs. + + Returns: + Set of strings: rpaths to add to target. + """ + r = set.empty() + + if is_darwin: + prefix = "@loader_path" + else: + prefix = "$ORIGIN" + + for solib in solibs: + rpath = create_rpath_entry( + binary = target, + dependency = solib, + keep_filename = False, + prefix = prefix, + ) + set.mutable_insert(r, rpath) + + return r + +def _so_extension(hs): + """Returns the extension for shared libraries. + + Args: + hs: Haskell rule context. + + Returns: + string of extension. + """ + return "dylib" if hs.toolchain.is_darwin else "so" + +def link_library_static(hs, cc, dep_info, objects_dir, my_pkg_id, with_profiling): + """Link a static library for the package using given object files. + + Returns: + File: Produced static library. + """ + static_library = hs.actions.declare_file( + "lib{0}.a".format(pkg_id.library_name(hs, my_pkg_id, prof_suffix = with_profiling)), + ) + objects_dir_manifest = _create_objects_dir_manifest( + hs, + objects_dir, + dynamic = False, + with_profiling = with_profiling, + ) + args = hs.actions.args() + inputs = [objects_dir, objects_dir_manifest] + cc.files + + if hs.toolchain.is_darwin: + # On Darwin, ar doesn't support params files. + args.add_all([ + static_library, + objects_dir_manifest.path, + ]) + + # TODO Get ar location from the CC toolchain. This is + # complicated by the fact that the CC toolchain does not + # always use ar, and libtool has an entirely different CLI. + # See https://github.com/bazelbuild/bazel/issues/5127 + hs.actions.run_shell( + inputs = inputs, + outputs = [static_library], + mnemonic = "HaskellLinkStaticLibrary", + command = "{ar} qc $1 $(< $2)".format(ar = cc.tools.ar), + arguments = [args], + + # Use the default macosx toolchain + env = {"SDKROOT": "macosx"}, + ) + else: + args.add_all([ + "qc", + static_library, + "@" + objects_dir_manifest.path, + ]) + hs.actions.run( + inputs = inputs, + outputs = [static_library], + mnemonic = "HaskellLinkStaticLibrary", + executable = cc.tools.ar, + arguments = [args], + ) + + return static_library + +def link_library_dynamic(hs, cc, dep_info, extra_srcs, objects_dir, my_pkg_id): + """Link a dynamic library for the package using given object files. + + Returns: + File: Produced dynamic library. + """ + + dynamic_library = hs.actions.declare_file( + "lib{0}-ghc{1}.{2}".format( + pkg_id.library_name(hs, my_pkg_id), + hs.toolchain.version, + _so_extension(hs), + ), + ) + + args = hs.actions.args() + args.add_all(["-optl" + f for f in cc.linker_flags]) + args.add_all(["-shared", "-dynamic"]) + + # Work around macOS linker limits. This fix has landed in GHC HEAD, but is + # not yet in a release; plus, we still want to support older versions of + # GHC. For details, see: https://phabricator.haskell.org/D4714 + if hs.toolchain.is_darwin: + args.add("-optl-Wl,-dead_strip_dylibs") + + args.add_all(pkg_info_to_compile_flags(expose_packages( + dep_info, + lib_info = None, + use_direct = True, + use_my_pkg_id = None, + custom_package_databases = None, + version = my_pkg_id.version if my_pkg_id else None, + ))) + + (cc_link_libs, _cc_solibs, _hs_solibs) = _link_dependencies( + hs = hs, + dep_info = dep_info, + dynamic = True, + binary = dynamic_library, + args = args, + ) + + args.add_all(["-o", dynamic_library.path]) + + # Profiling not supported for dynamic libraries. + objects_dir_manifest = _create_objects_dir_manifest( + hs, + objects_dir, + dynamic = True, + with_profiling = False, + ) + + hs.toolchain.actions.run_ghc( + hs, + cc, + inputs = depset([objects_dir], transitive = [ + depset(extra_srcs), + set.to_depset(dep_info.package_databases), + set.to_depset(dep_info.dynamic_libraries), + cc_link_libs, + ]), + outputs = [dynamic_library], + mnemonic = "HaskellLinkDynamicLibrary", + arguments = args, + params_file = objects_dir_manifest, + ) + + return dynamic_library diff --git a/third_party/bazel/rules_haskell/haskell/private/actions/package.bzl b/third_party/bazel/rules_haskell/haskell/private/actions/package.bzl new file mode 100644 index 000000000000..1c438e8445a5 --- /dev/null +++ b/third_party/bazel/rules_haskell/haskell/private/actions/package.bzl @@ -0,0 +1,210 @@ +"""Action for creating packages and registering them with ghc-pkg""" + +load("@bazel_skylib//lib:paths.bzl", "paths") +load(":private/path_utils.bzl", "target_unique_name") +load(":private/pkg_id.bzl", "pkg_id") +load(":private/set.bzl", "set") +load(":private/path_utils.bzl", "get_lib_name") + +def _get_extra_libraries(dep_info): + """Get directories and library names for extra library dependencies. + + Args: + dep_info: HaskellInfo provider of the package. + + Returns: + (dirs, libs): + dirs: list: Library search directories for extra library dependencies. + libs: list: Extra library dependencies. + """ + cc_libs = dep_info.cc_dependencies.dynamic_linking.libraries_to_link.to_list() + + # The order in which library dependencies are listed is relevant when + # linking static archives. To maintain the order defined by the input + # depset we collect the library dependencies in a list, and use a separate + # set to deduplicate entries. + seen_libs = set.empty() + extra_libs = [] + extra_lib_dirs = set.empty() + for lib in cc_libs: + lib_name = get_lib_name(lib) + if not set.is_member(seen_libs, lib_name): + set.mutable_insert(seen_libs, lib_name) + extra_libs.append(lib_name) + set.mutable_insert(extra_lib_dirs, lib.dirname) + return (set.to_list(extra_lib_dirs), extra_libs) + +def package( + hs, + dep_info, + interfaces_dir, + interfaces_dir_prof, + static_library, + dynamic_library, + exposed_modules_file, + other_modules, + my_pkg_id, + static_library_prof): + """Create GHC package using ghc-pkg. + + Args: + hs: Haskell context. + interfaces_dir: Directory containing interface files. + static_library: Static library of the package. + dynamic_library: Dynamic library of the package. + static_library_prof: Static library compiled with profiling or None. + + Returns: + (File, File): GHC package conf file, GHC package cache file + """ + pkg_db_dir = pkg_id.to_string(my_pkg_id) + conf_file = hs.actions.declare_file( + paths.join(pkg_db_dir, "{0}.conf".format(pkg_db_dir)), + ) + cache_file = hs.actions.declare_file("package.cache", sibling = conf_file) + + import_dir = paths.join( + "${pkgroot}", + paths.join(pkg_db_dir, "_iface"), + ) + interfaces_dirs = [interfaces_dir] + + if interfaces_dir_prof != None: + import_dir_prof = paths.join( + "${pkgroot}", + paths.join(pkg_db_dir, "_iface_prof"), + ) + interfaces_dirs.append(interfaces_dir_prof) + else: + import_dir_prof = "" + + (extra_lib_dirs, extra_libs) = _get_extra_libraries(dep_info) + + metadata_entries = { + "name": my_pkg_id.name, + "version": my_pkg_id.version, + "id": pkg_id.to_string(my_pkg_id), + "key": pkg_id.to_string(my_pkg_id), + "exposed": "True", + "hidden-modules": " ".join(other_modules), + "import-dirs": " ".join([import_dir, import_dir_prof]), + "library-dirs": " ".join(["${pkgroot}"] + extra_lib_dirs), + "dynamic-library-dirs": " ".join(["${pkgroot}"] + extra_lib_dirs), + "hs-libraries": pkg_id.library_name(hs, my_pkg_id), + "extra-libraries": " ".join(extra_libs), + "depends": ", ".join( + # Prebuilt dependencies are added further down, since their + # package-ids are not available as strings but in build outputs. + set.to_list(dep_info.package_ids), + ), + } + + # Create a file from which ghc-pkg will create the actual package + # from. List of exposed modules generated below. + metadata_file = hs.actions.declare_file(target_unique_name(hs, "metadata")) + hs.actions.write( + output = metadata_file, + content = "\n".join([ + "{0}: {1}".format(k, v) + for k, v in metadata_entries.items() + if v + ]) + "\n", + ) + + # Collect the package id files of all prebuilt dependencies. + prebuilt_deps_id_files = [ + dep.id_file + for dep in set.to_list(dep_info.prebuilt_dependencies) + ] + + # Combine exposed modules and other metadata to form the package + # configuration file. + + prebuilt_deps_args = hs.actions.args() + prebuilt_deps_args.add_all([f.path for f in prebuilt_deps_id_files]) + prebuilt_deps_args.use_param_file("%s", use_always = True) + prebuilt_deps_args.set_param_file_format("multiline") + + hs.actions.run_shell( + inputs = [metadata_file, exposed_modules_file] + prebuilt_deps_id_files, + outputs = [conf_file], + command = """ + cat $1 > $4 + echo "exposed-modules: `cat $2`" >> $4 + + # this is equivalent to 'readarray'. We do use 'readarray' in order to + # support older bash versions. + while IFS= read -r line; do deps_id_files+=("$line"); done < $3 + + if [ ${#deps_id_files[@]} -eq 0 ]; then + deps="" + else + deps=$(cat "${deps_id_files[@]}" | tr '\n' " ") + fi + echo "depends: $deps" >> $4 +""", + arguments = [ + metadata_file.path, + exposed_modules_file.path, + prebuilt_deps_args, + conf_file.path, + ], + use_default_shell_env = True, + ) + + # Make the call to ghc-pkg and use the package configuration file + package_path = ":".join([c.dirname for c in set.to_list(dep_info.package_databases)]) + ":" + hs.actions.run( + inputs = depset(transitive = [ + set.to_depset(dep_info.package_databases), + depset(interfaces_dirs), + depset([ + input + for input in [ + static_library, + conf_file, + dynamic_library, + static_library_prof, + ] + if input + ]), + ]), + outputs = [cache_file], + env = { + "GHC_PACKAGE_PATH": package_path, + }, + mnemonic = "HaskellRegisterPackage", + progress_message = "HaskellRegisterPackage {}".format(hs.label), + executable = hs.tools.ghc_pkg, + # Registration of a new package consists in, + # + # 1. copying the registration file into the package db, + # 2. performing some validation on the registration file content, + # 3. recaching, i.e. regenerating the package db cache file. + # + # Normally, this is all done by `ghc-pkg register`. But in our + # case, `ghc-pkg register` is painful, because the validation + # it performs is slow, somewhat redundant but especially, too + # strict (see e.g. + # https://ghc.haskell.org/trac/ghc/ticket/15478). So we do (1) + # and (3) manually, by copying then calling `ghc-pkg recache` + # directly. + # + # The downside is that we do lose the few validations that + # `ghc-pkg register` was doing that was useful. e.g. when + # reexporting modules, validation checks that the source + # module does exist. + # + # TODO Go back to using `ghc-pkg register`. Blocked by + # https://ghc.haskell.org/trac/ghc/ticket/15478 + arguments = [ + "recache", + "--package-db={0}".format(conf_file.dirname), + "-v0", + "--no-expand-pkgroot", + ], + # XXX: Seems required for this to work on Windows + use_default_shell_env = True, + ) + + return conf_file, cache_file diff --git a/third_party/bazel/rules_haskell/haskell/private/actions/repl.bzl b/third_party/bazel/rules_haskell/haskell/private/actions/repl.bzl new file mode 100644 index 000000000000..5de64955d0af --- /dev/null +++ b/third_party/bazel/rules_haskell/haskell/private/actions/repl.bzl @@ -0,0 +1,175 @@ +"""GHCi REPL support""" + +load(":private/context.bzl", "render_env") +load(":private/packages.bzl", "expose_packages", "pkg_info_to_compile_flags") +load( + ":private/path_utils.bzl", + "get_lib_name", + "is_shared_library", + "link_libraries", + "ln", + "target_unique_name", +) +load(":providers.bzl", "get_libs_for_ghc_linker") +load( + ":private/set.bzl", + "set", +) +load("@bazel_skylib//lib:paths.bzl", "paths") +load("@bazel_skylib//lib:shell.bzl", "shell") + +def build_haskell_repl( + hs, + ghci_script, + ghci_repl_wrapper, + user_compile_flags, + repl_ghci_args, + hs_info, + output, + package_databases, + version, + lib_info = None): + """Build REPL script. + + Args: + hs: Haskell context. + hs_info: HaskellInfo. + + package_databases: package caches excluding the cache file of the package + we're creating a REPL for. + lib_info: If we're building REPL for a library target, pass + HaskellLibraryInfo here, otherwise it should be None. + + Returns: + None. + """ + + # The base and directory packages are necessary for the GHCi script we use + # (loads source files and brings in scope the corresponding modules). + args = ["-package", "base", "-package", "directory"] + + pkg_ghc_info = expose_packages( + hs_info, + lib_info, + use_direct = False, + use_my_pkg_id = None, + custom_package_databases = package_databases, + version = version, + ) + args += pkg_info_to_compile_flags(pkg_ghc_info) + + lib_imports = [] + if lib_info != None: + for idir in set.to_list(hs_info.import_dirs): + args += ["-i{0}".format(idir)] + lib_imports.append(idir) + + link_ctx = hs_info.cc_dependencies.dynamic_linking + libs_to_link = link_ctx.dynamic_libraries_for_runtime.to_list() + + # External C libraries that we need to make available to the REPL. + libraries = link_libraries(libs_to_link, args) + + # Transitive library dependencies to have in runfiles. + (library_deps, ld_library_deps, ghc_env) = get_libs_for_ghc_linker( + hs, + hs_info.transitive_cc_dependencies, + path_prefix = "$RULES_HASKELL_EXEC_ROOT", + ) + library_path = [paths.dirname(lib.path) for lib in library_deps] + ld_library_path = [paths.dirname(lib.path) for lib in ld_library_deps] + + repl_file = hs.actions.declare_file(target_unique_name(hs, "repl")) + + add_sources = ["*" + f.path for f in set.to_list(hs_info.source_files)] + + ghci_repl_script = hs.actions.declare_file( + target_unique_name(hs, "ghci-repl-script"), + ) + hs.actions.expand_template( + template = ghci_script, + output = ghci_repl_script, + substitutions = { + "{ADD_SOURCES}": " ".join(add_sources), + "{COMMANDS}": "", + }, + ) + + # Extra arguments. + # `compiler flags` is the default set of arguments for the repl, + # augmented by `repl_ghci_args`. + # The ordering is important, first compiler flags (from toolchain + # and local rule), then from `repl_ghci_args`. This way the more + # specific arguments are listed last, and then have more priority in + # GHC. + # Note that most flags for GHCI do have their negative value, so a + # negative flag in `repl_ghci_args` can disable a positive flag set + # in `user_compile_flags`, such as `-XNoOverloadedStrings` will disable + # `-XOverloadedStrings`. + args += hs.toolchain.compiler_flags + user_compile_flags + hs.toolchain.repl_ghci_args + repl_ghci_args + + hs.actions.expand_template( + template = ghci_repl_wrapper, + output = repl_file, + substitutions = { + "{ENV}": render_env(ghc_env), + "{TOOL}": hs.tools.ghci.path, + "{ARGS}": " ".join( + [ + "-ghci-script", + paths.join("$RULES_HASKELL_EXEC_ROOT", ghci_repl_script.path), + ] + [ + shell.quote(a) + for a in args + ], + ), + }, + is_executable = True, + ) + + ghc_info = struct( + has_version = pkg_ghc_info.has_version, + library_path = library_path, + ld_library_path = ld_library_path, + packages = pkg_ghc_info.packages, + package_ids = pkg_ghc_info.package_ids, + package_dbs = pkg_ghc_info.package_dbs, + lib_imports = lib_imports, + libraries = libraries, + execs = struct( + ghc = hs.tools.ghc.path, + ghci = hs.tools.ghci.path, + runghc = hs.tools.runghc.path, + ), + flags = struct( + compiler = user_compile_flags, + toolchain_compiler = hs.toolchain.compiler_flags, + repl = repl_ghci_args, + toolchain_repl = hs.toolchain.repl_ghci_args, + ), + ) + ghc_info_file = hs.actions.declare_file( + target_unique_name(hs, "ghc-info"), + ) + hs.actions.write( + output = ghc_info_file, + content = ghc_info.to_json(), + ) + + # XXX We create a symlink here because we need to force + # hs.tools.ghci and ghci_script and the best way to do that is + # to use hs.actions.run. That action, in turn must produce + # a result, so using ln seems to be the only sane choice. + extra_inputs = depset(transitive = [ + depset([ + hs.tools.ghci, + ghci_repl_script, + repl_file, + ghc_info_file, + ]), + set.to_depset(package_databases), + depset(library_deps), + depset(ld_library_deps), + set.to_depset(hs_info.source_files), + ]) + ln(hs, repl_file, output, extra_inputs) diff --git a/third_party/bazel/rules_haskell/haskell/private/actions/runghc.bzl b/third_party/bazel/rules_haskell/haskell/private/actions/runghc.bzl new file mode 100644 index 000000000000..da855a3adb3c --- /dev/null +++ b/third_party/bazel/rules_haskell/haskell/private/actions/runghc.bzl @@ -0,0 +1,115 @@ +"""runghc support""" + +load(":private/context.bzl", "render_env") +load(":private/packages.bzl", "expose_packages", "pkg_info_to_compile_flags") +load( + ":private/path_utils.bzl", + "get_lib_name", + "is_shared_library", + "link_libraries", + "ln", + "target_unique_name", +) +load( + ":private/set.bzl", + "set", +) +load(":providers.bzl", "get_libs_for_ghc_linker") +load("@bazel_skylib//lib:shell.bzl", "shell") + +def build_haskell_runghc( + hs, + runghc_wrapper, + user_compile_flags, + extra_args, + hs_info, + output, + package_databases, + version, + lib_info = None): + """Build runghc script. + + Args: + hs: Haskell context. + hs_info: HaskellInfo. + + package_databases: package caches excluding the cache file of the package + we're creating a runghc for. + lib_info: If we're building runghc for a library target, pass + HaskellLibraryInfo here, otherwise it should be None. + + Returns: + None. + """ + + args = pkg_info_to_compile_flags(expose_packages( + hs_info, + lib_info, + use_direct = False, + use_my_pkg_id = None, + custom_package_databases = package_databases, + version = version, + )) + + if lib_info != None: + for idir in set.to_list(hs_info.import_dirs): + args += ["-i{0}".format(idir)] + + link_ctx = hs_info.cc_dependencies.dynamic_linking + libs_to_link = link_ctx.dynamic_libraries_for_runtime.to_list() + + # External C libraries that we need to make available to runghc. + link_libraries(libs_to_link, args) + + # Transitive library dependencies to have in runfiles. + (library_deps, ld_library_deps, ghc_env) = get_libs_for_ghc_linker( + hs, + hs_info.transitive_cc_dependencies, + path_prefix = "$RULES_HASKELL_EXEC_ROOT", + ) + + runghc_file = hs.actions.declare_file(target_unique_name(hs, "runghc")) + + # Extra arguments. + # `compiler flags` is the default set of arguments for runghc, + # augmented by `extra_args`. + # The ordering is important, first compiler flags (from toolchain + # and local rule), then from `extra_args`. This way the more + # specific arguments are listed last, and then have more priority in + # GHC. + # Note that most flags for GHCI do have their negative value, so a + # negative flag in `extra_args` can disable a positive flag set + # in `user_compile_flags`, such as `-XNoOverloadedStrings` will disable + # `-XOverloadedStrings`. + args += hs.toolchain.compiler_flags + user_compile_flags + hs.toolchain.repl_ghci_args + + # ghc args need to be wrapped up in "--ghc-arg=" when passing to runghc + runcompile_flags = ["--ghc-arg=%s" % a for a in args] + runcompile_flags += extra_args + + hs.actions.expand_template( + template = runghc_wrapper, + output = runghc_file, + substitutions = { + "{ENV}": render_env(ghc_env), + "{TOOL}": hs.tools.runghc.path, + "{ARGS}": " ".join([shell.quote(a) for a in runcompile_flags]), + }, + is_executable = True, + ) + + # XXX We create a symlink here because we need to force + # hs.tools.runghc and the best way to do that is + # to use hs.actions.run. That action, in turn must produce + # a result, so using ln seems to be the only sane choice. + extra_inputs = depset(transitive = [ + depset([ + hs.tools.runghc, + runghc_file, + ]), + set.to_depset(package_databases), + depset(library_deps), + depset(ld_library_deps), + set.to_depset(hs_info.source_files), + ]) + ln(hs, runghc_file, output, extra_inputs) diff --git a/third_party/bazel/rules_haskell/haskell/private/context.bzl b/third_party/bazel/rules_haskell/haskell/private/context.bzl new file mode 100644 index 000000000000..3cd3ff92cb65 --- /dev/null +++ b/third_party/bazel/rules_haskell/haskell/private/context.bzl @@ -0,0 +1,64 @@ +"""Derived context with Haskell-specific fields and methods""" + +load("@bazel_skylib//lib:paths.bzl", "paths") + +HaskellContext = provider() + +def haskell_context(ctx, attr = None): + toolchain = ctx.toolchains["@io_tweag_rules_haskell//haskell:toolchain"] + + if not attr: + attr = ctx.attr + + if hasattr(attr, "src_strip_prefix"): + src_strip_prefix = attr.src_strip_prefix + else: + src_strip_prefix = "" + + src_root = paths.join( + ctx.label.workspace_root, + ctx.label.package, + src_strip_prefix, + ) + + env = { + "LANG": toolchain.locale, + } + + if toolchain.locale_archive != None: + env["LOCALE_ARCHIVE"] = toolchain.locale_archive.path + + coverage_enabled = False + if hasattr(ctx, "configuration"): + coverage_enabled = ctx.configuration.coverage_enabled + + return HaskellContext( + # Fields + name = attr.name, + label = ctx.label, + toolchain = toolchain, + tools = toolchain.tools, + src_root = src_root, + package_root = ctx.label.workspace_root + ctx.label.package, + env = env, + mode = ctx.var["COMPILATION_MODE"], + actions = ctx.actions, + bin_dir = ctx.bin_dir, + genfiles_dir = ctx.genfiles_dir, + coverage_enabled = coverage_enabled, + ) + +def render_env(env): + """Render environment dict to shell exports. + + Example: + + >>> render_env({"PATH": "foo:bar", "LANG": "lang"}) + export PATH=foo:bar + export LANG=lang + + """ + return "\n".join([ + "export {}={}".format(k, v) + for k, v in env.items() + ]) diff --git a/third_party/bazel/rules_haskell/haskell/private/coverage_wrapper.sh.tpl b/third_party/bazel/rules_haskell/haskell/private/coverage_wrapper.sh.tpl new file mode 100644 index 000000000000..8bc12187f4e8 --- /dev/null +++ b/third_party/bazel/rules_haskell/haskell/private/coverage_wrapper.sh.tpl @@ -0,0 +1,128 @@ +#!/usr/bin/env bash +# A wrapper for Haskell binaries which have been instrumented for hpc code coverage. + +# Copy-pasted from Bazel's Bash runfiles library (tools/bash/runfiles/runfiles.bash). +set -euo pipefail +if [[ ! -d "${RUNFILES_DIR:-/dev/null}" && ! -f "${RUNFILES_MANIFEST_FILE:-/dev/null}" ]]; then + if [[ -f "$0.runfiles_manifest" ]]; then + export RUNFILES_MANIFEST_FILE="$0.runfiles_manifest" + elif [[ -f "$0.runfiles/MANIFEST" ]]; then + export RUNFILES_MANIFEST_FILE="$0.runfiles/MANIFEST" + elif [[ -f "$0.runfiles/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then + export RUNFILES_DIR="$0.runfiles" + fi +fi +if [[ -f "${RUNFILES_DIR:-/dev/null}/bazel_tools/tools/bash/runfiles/runfiles.bash" ]]; then + source "${RUNFILES_DIR}/bazel_tools/tools/bash/runfiles/runfiles.bash" +elif [[ -f "${RUNFILES_MANIFEST_FILE:-/dev/null}" ]]; then + source "$(grep -m1 "^bazel_tools/tools/bash/runfiles/runfiles.bash " \ + "$RUNFILES_MANIFEST_FILE" | cut -d ' ' -f 2-)" +else + echo >&2 "ERROR: cannot find @bazel_tools//tools/bash/runfiles:runfiles.bash" + exit 1 +fi +# --- end runfiles.bash initialization --- + +ERRORCOLOR='\033[1;31m' +CLEARCOLOR='\033[0m' +binary_path=$(rlocation {binary_path}) +hpc_path=$(rlocation {hpc_path}) +tix_file_path={tix_file_path} +coverage_report_format={coverage_report_format} +strict_coverage_analysis={strict_coverage_analysis} +package_path={package_path} + +# either of the two expected coverage metrics should be set to -1 if they're meant to be unused +expected_covered_expressions_percentage={expected_covered_expressions_percentage} +expected_uncovered_expression_count={expected_uncovered_expression_count} + +# gather the hpc directories +hpc_dir_args="" +mix_file_paths={mix_file_paths} +for m in "${mix_file_paths[@]}" +do + absolute_mix_file_path=$(rlocation $m) + hpc_parent_dir=$(dirname $absolute_mix_file_path) + trimmed_hpc_parent_dir=$(echo "${hpc_parent_dir%%.hpc*}") + hpc_dir_args="$hpc_dir_args --hpcdir=$trimmed_hpc_parent_dir.hpc" +done + +# gather the modules to exclude from the coverage analysis +hpc_exclude_args="" +modules_to_exclude={modules_to_exclude} +for m in "${modules_to_exclude[@]}" +do + hpc_exclude_args="$hpc_exclude_args --exclude=$m" +done + +# run the test binary, and then generate the report +$binary_path "$@" > /dev/null 2>&1 +$hpc_path report "$tix_file_path" $hpc_dir_args $hpc_exclude_args \ + --srcdir "." --srcdir "$package_path" > __hpc_coverage_report + +# if we want a text report, just output the file generated in the previous step +if [ "$coverage_report_format" == "text" ] +then + echo "Overall report" + cat __hpc_coverage_report +fi + +# check the covered expression percentage, and if it matches our expectations +if [ "$expected_covered_expressions_percentage" -ne -1 ] +then + covered_expression_percentage=$(grep "expressions used" __hpc_coverage_report | cut -c 1-3) + if [ "$covered_expression_percentage" -lt "$expected_covered_expressions_percentage" ] + then + echo -e "\n==>$ERRORCOLOR Inadequate expression coverage percentage.$CLEARCOLOR" + echo -e "==> Expected $expected_covered_expressions_percentage%, but the actual coverage was $ERRORCOLOR$(($covered_expression_percentage))%$CLEARCOLOR.\n" + exit 1 + elif [ "$strict_coverage_analysis" == "True" ] && [ "$covered_expression_percentage" -gt "$expected_covered_expressions_percentage" ] + then + echo -e "\n==>$ERRORCOLOR ** BECAUSE STRICT COVERAGE ANALYSIS IS ENABLED **$CLEARCOLOR" + echo -e "==> Your coverage percentage is now higher than expected.$CLEARCOLOR" + echo -e "==> Expected $expected_covered_expressions_percentage% of expressions covered, but the actual value is $ERRORCOLOR$(($covered_expression_percentage))%$CLEARCOLOR." + echo -e "==> Please increase the expected coverage percentage to match.\n" + exit 1 + fi +fi + +# check how many uncovered expressions there are, and if that number matches our expectations +if [ "$expected_uncovered_expression_count" -ne -1 ] +then + coverage_numerator=$(grep "expressions used" __hpc_coverage_report | sed s:.*\(::g | cut -f1 -d "/") + coverage_denominator=$(grep "expressions used" __hpc_coverage_report | sed s:.*/::g | cut -f1 -d ")") + uncovered_expression_count="$(($coverage_denominator - $coverage_numerator))" + if [ "$uncovered_expression_count" -gt "$expected_uncovered_expression_count" ] + then + echo -e "\n==>$ERRORCOLOR Too many uncovered expressions.$CLEARCOLOR" + echo -e "==> Expected $expected_uncovered_expression_count uncovered expressions, but the actual count was $ERRORCOLOR$(($uncovered_expression_count))$CLEARCOLOR.\n" + exit 1 + elif [ "$strict_coverage_analysis" == "True" ] && [ "$uncovered_expression_count" -lt "$expected_uncovered_expression_count" ] + then + echo -e "\n==>$ERRORCOLOR ** BECAUSE STRICT COVERAGE ANALYSIS IS ENABLED **$CLEARCOLOR" + echo -e "==>$ERRORCOLOR Your uncovered expression count is now lower than expected.$CLEARCOLOR" + echo -e "==> Expected $expected_uncovered_expression_count uncovered expressions, but there is $ERRORCOLOR$(($uncovered_expression_count))$CLEARCOLOR." + echo -e "==> Please lower the expected uncovered expression count to match.\n" + exit 1 + fi +fi + +# if we want an html report, run the hpc binary again with the "markup" command, +# and feed its generated files into stdout, wrapped in XML tags +if [ "$coverage_report_format" == "html" ] +then + $hpc_path markup "$tix_file_path" $hpc_dir_args $hpc_exclude_args \ + --srcdir "." --srcdir "$package_path" --destdir=hpc_out > /dev/null 2>&1 + cd hpc_out + echo "COVERAGE REPORT BELOW" + echo "%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%" + for file in *.html **/*.hs.html; do + [ -e "$file" ] || continue + echo "<coverage-report-part name=\"$file\">" + echo '<![CDATA[' + cat $file + echo ']]>' + echo "</coverage-report-part>" + done + echo "%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%" +fi \ No newline at end of file diff --git a/third_party/bazel/rules_haskell/haskell/private/dependencies.bzl b/third_party/bazel/rules_haskell/haskell/private/dependencies.bzl new file mode 100644 index 000000000000..6a2a797872fe --- /dev/null +++ b/third_party/bazel/rules_haskell/haskell/private/dependencies.bzl @@ -0,0 +1,222 @@ +load("@bazel_skylib//lib:dicts.bzl", "dicts") +load("@bazel_skylib//lib:paths.bzl", "paths") +load( + "@io_tweag_rules_haskell//haskell:providers.bzl", + "HaskellCcInfo", + "HaskellInfo", + "HaskellLibraryInfo", + "HaskellPrebuiltPackageInfo", + "empty_HaskellCcInfo", + "merge_HaskellCcInfo", +) +load( + ":private/path_utils.bzl", + "get_lib_name", + "is_shared_library", + "is_static_library", + "ln", +) +load(":private/set.bzl", "set") + +def _cc_get_static_lib(lib_info): + """Return the library to use in static linking mode. + + This returns the first available library artifact in the following order: + - static_library + - pic_static_library + - dynamic_library + - interface_library + + Args: + lib_info: LibraryToLink provider. + + Returns: + File: The library to link against in static mode. + """ + if lib_info.static_library: + return lib_info.static_library + elif lib_info.pic_static_library: + return lib_info.pic_static_library + elif lib_info.dynamic_library: + return lib_info.dynamic_library + else: + return lib_info.interface_library + +def _cc_get_dynamic_lib(lib_info): + """Return the library to use in dynamic linking mode. + + This returns the first available library artifact in the following order: + - dynamic_library + - interface_library + - pic_static_library + - static_library + + Args: + lib_info: LibraryToLink provider. + + Returns: + File: The library to link against in dynamic mode. + """ + if lib_info.dynamic_library: + return lib_info.dynamic_library + elif lib_info.interface_library: + return lib_info.interface_library + elif lib_info.pic_static_library: + return lib_info.pic_static_library + else: + return lib_info.static_library + +def _HaskellCcInfo_from_CcInfo(ctx, cc_info): + libs_to_link = cc_info.linking_context.libraries_to_link + static_libs_to_link = [] + dynamic_libs_to_link = [] + static_libs_for_runtime = [] + dynamic_libs_for_runtime = [] + for l in libs_to_link: + _static_lib = _cc_get_static_lib(l) + dynamic_lib = _cc_get_dynamic_lib(l) + + # Bazel itself only mangles dynamic libraries, not static libraries. + # However, we need the library name of the static and dynamic version + # of a library to match so that we can refer to both with one entry in + # the package configuration file. Here we rename any static archives + # with mismatching mangled dynamic library name. + static_name = get_lib_name(_static_lib) + dynamic_name = get_lib_name(dynamic_lib) + if static_name != dynamic_name: + ext = _static_lib.extension + static_lib = ctx.actions.declare_file( + "lib%s.%s" % (dynamic_name, ext), + ) + ln(ctx, _static_lib, static_lib) + else: + static_lib = _static_lib + + static_libs_to_link.append(static_lib) + if is_shared_library(static_lib): + static_libs_for_runtime.append(static_lib) + dynamic_libs_to_link.append(dynamic_lib) + if is_shared_library(dynamic_lib): + dynamic_libs_for_runtime.append(dynamic_lib) + + return HaskellCcInfo( + static_linking = struct( + libraries_to_link = depset( + direct = static_libs_to_link, + order = "topological", + ), + dynamic_libraries_for_runtime = depset( + direct = static_libs_for_runtime, + order = "topological", + ), + user_link_flags = depset( + direct = cc_info.linking_context.user_link_flags, + order = "topological", + ), + ), + dynamic_linking = struct( + libraries_to_link = depset( + direct = dynamic_libs_to_link, + order = "topological", + ), + dynamic_libraries_for_runtime = depset( + direct = dynamic_libs_for_runtime, + order = "topological", + ), + user_link_flags = depset( + direct = cc_info.linking_context.user_link_flags, + order = "topological", + ), + ), + ) + +def gather_dep_info(ctx, deps): + """Collapse dependencies into a single `HaskellInfo`. + + Note that the field `prebuilt_dependencies` also includes + prebuilt_dependencies of current target. + + Args: + ctx: Rule context. + deps: deps attribute. + + Returns: + HaskellInfo: Unified information about all dependencies. + """ + + acc = HaskellInfo( + package_ids = set.empty(), + package_databases = set.empty(), + version_macros = set.empty(), + static_libraries = [], + static_libraries_prof = [], + dynamic_libraries = set.empty(), + interface_dirs = set.empty(), + prebuilt_dependencies = set.empty(), + direct_prebuilt_deps = set.empty(), + cc_dependencies = empty_HaskellCcInfo(), + transitive_cc_dependencies = empty_HaskellCcInfo(), + ) + + for dep in deps: + if HaskellInfo in dep: + binfo = dep[HaskellInfo] + package_ids = acc.package_ids + if HaskellLibraryInfo not in dep: + fail("Target {0} cannot depend on binary".format(ctx.attr.name)) + if HaskellLibraryInfo in dep: + set.mutable_insert(package_ids, dep[HaskellLibraryInfo].package_id) + acc = HaskellInfo( + package_ids = package_ids, + package_databases = set.mutable_union(acc.package_databases, binfo.package_databases), + version_macros = set.mutable_union(acc.version_macros, binfo.version_macros), + static_libraries = acc.static_libraries + binfo.static_libraries, + static_libraries_prof = acc.static_libraries_prof + binfo.static_libraries_prof, + dynamic_libraries = set.mutable_union(acc.dynamic_libraries, binfo.dynamic_libraries), + interface_dirs = set.mutable_union(acc.interface_dirs, binfo.interface_dirs), + prebuilt_dependencies = set.mutable_union(acc.prebuilt_dependencies, binfo.prebuilt_dependencies), + direct_prebuilt_deps = acc.direct_prebuilt_deps, + cc_dependencies = acc.cc_dependencies, + transitive_cc_dependencies = merge_HaskellCcInfo(acc.transitive_cc_dependencies, binfo.transitive_cc_dependencies), + ) + elif HaskellPrebuiltPackageInfo in dep: + pkg = dep[HaskellPrebuiltPackageInfo] + acc = HaskellInfo( + package_ids = acc.package_ids, + package_databases = acc.package_databases, + version_macros = set.mutable_insert(acc.version_macros, pkg.version_macros_file), + static_libraries = acc.static_libraries, + static_libraries_prof = acc.static_libraries_prof, + dynamic_libraries = acc.dynamic_libraries, + interface_dirs = acc.interface_dirs, + prebuilt_dependencies = set.mutable_insert(acc.prebuilt_dependencies, pkg), + direct_prebuilt_deps = set.mutable_insert(acc.direct_prebuilt_deps, pkg), + cc_dependencies = acc.cc_dependencies, + transitive_cc_dependencies = acc.transitive_cc_dependencies, + ) + elif CcInfo in dep and HaskellInfo not in dep: + # The final link of a binary must include all static libraries we + # depend on, including transitives ones. Theses libs are provided + # in the `CcInfo` provider. + hs_cc_info = _HaskellCcInfo_from_CcInfo(ctx, dep[CcInfo]) + acc = HaskellInfo( + package_ids = acc.package_ids, + package_databases = acc.package_databases, + version_macros = acc.version_macros, + static_libraries = acc.static_libraries, + static_libraries_prof = acc.static_libraries_prof, + dynamic_libraries = acc.dynamic_libraries, + interface_dirs = acc.interface_dirs, + prebuilt_dependencies = acc.prebuilt_dependencies, + direct_prebuilt_deps = acc.direct_prebuilt_deps, + cc_dependencies = merge_HaskellCcInfo( + acc.cc_dependencies, + hs_cc_info, + ), + transitive_cc_dependencies = merge_HaskellCcInfo( + acc.transitive_cc_dependencies, + hs_cc_info, + ), + ) + + return acc diff --git a/third_party/bazel/rules_haskell/haskell/private/ghci_repl_wrapper.sh b/third_party/bazel/rules_haskell/haskell/private/ghci_repl_wrapper.sh new file mode 100644 index 000000000000..cd6acefc7a85 --- /dev/null +++ b/third_party/bazel/rules_haskell/haskell/private/ghci_repl_wrapper.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# +# Usage: ghci_repl_wrapper.sh <ARGS> + +# this variable is set by `bazel run` +if [ "$BUILD_WORKSPACE_DIRECTORY" = "" ] +then + cat <<EOF +It looks like you are trying to invoke the REPL incorrectly. +We only support calling the repl script with + +$ bazel run <target> + +for now. + +If you are on bazel < 0.15 you must invoke as follows: + +$ bazel run --direct_run <target> +EOF + exit 1 +fi + +# Derived from Bazel's Bash runfiles library (tools/bash/runfiles/runfiles.bash). +if [[ -z "$RUNFILES_DIR" ]]; then + if [[ -d "$0.runfiles" ]]; then + export RUNFILES_DIR="$0.runfiles" + fi +fi +if [[ -z "$RUNFILES_MANIFEST_FILE" ]]; then + if [[ -f "$0.runfiles_manifest" ]]; then + export RUNFILES_MANIFEST_FILE="$0.runfiles_manifest" + elif [[ -f "$0.runfiles/MANIFEST" ]]; then + export RUNFILES_MANIFEST_FILE="$0.runfiles/MANIFEST" + fi +fi + +# GHCi script and libraries are loaded relative to workspace directory. +# bazel run //some:target@repl will be executed from the workspace directory. +# bazel run //some:haskell_repl will be executed from its execroot. +# Explicitly change into the workspace root in that case. +cd "$BUILD_WORKSPACE_DIRECTORY" + +# This is a workaround for https://github.com/bazelbuild/bazel/issues/5506 +# and also for the fact that REPL script relies on so-called “convenience +# links” and the names of those links are controlled by the --symlink_prefix +# option, which can be set by the user to something unpredictable. +# +# It seems that we can't locate the files of interest/build outputs in +# general. However, due to “internal issues” in Bazel mentioned e.g. +# https://github.com/bazelbuild/bazel/issues/3796, the directory bazel-out +# is always created under the workspace directory. We exploit this to get +# location of exec root reliably and then prefix locations of various +# components, such as shared libraries with that exec root. + +RULES_HASKELL_EXEC_ROOT=$(dirname $(readlink ${BUILD_WORKSPACE_DIRECTORY}/bazel-out)) +TOOL_LOCATION="$RULES_HASKELL_EXEC_ROOT/{TOOL}" + +{ENV} +"$TOOL_LOCATION" {ARGS} "$@" diff --git a/third_party/bazel/rules_haskell/haskell/private/haddock_wrapper.sh.tpl b/third_party/bazel/rules_haskell/haskell/private/haddock_wrapper.sh.tpl new file mode 100755 index 000000000000..c359da1c321d --- /dev/null +++ b/third_party/bazel/rules_haskell/haskell/private/haddock_wrapper.sh.tpl @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# +# Usage: haddock-wrapper.sh <PREBUILD_DEPS_FILE> <HADDOCK_ARGS> + +set -eo pipefail + +%{env} + +PREBUILT_DEPS_FILE=$1 +shift + +extra_args=() + +for pkg in $(< $PREBUILT_DEPS_FILE) +do + # Assumption: the `haddock-interfaces` field always only contains + # exactly one file name. This seems to hold in practice, though the + # ghc documentation defines it as: + # > (string list) A list of filenames containing Haddock interface files + # > (.haddock files) for this package. + # If there were more than one file, going by the output for the `depends`, + # the file names would be separated by a space character. + # https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/packages.html#installedpackageinfo-a-package-specification + haddock_interfaces=$(%{ghc-pkg} --simple-output field $pkg haddock-interfaces) + haddock_html=$(%{ghc-pkg} --simple-output field $pkg haddock-html) + + # Sometimes the referenced `.haddock` file does not exist + # (e.g. for `nixpkgs.haskellPackages` deps with haddock disabled). + # In that case, skip this package with a warning. + if [[ -f "$haddock_interfaces" ]] + then + # TODO: link source code, + # `--read-interface=$haddock_html,$pkg_src,$haddock_interfaces + # https://haskell-haddock.readthedocs.io/en/latest/invoking.html#cmdoption-read-interface + extra_args+=("--read-interface=$haddock_html,$haddock_interfaces") + else + echo "Warning: haddock missing for package $pkg" 1>&2 + fi +done + +# BSD and GNU mktemp are very different; attempt GNU first +TEMP=$(mktemp -d 2>/dev/null || mktemp -d -t 'haddock_wrapper') +trap cleanup 1 2 3 6 +cleanup() { rmdir "$TEMP"; } +# XXX Override TMPDIR to prevent race conditions on certain platforms. +# This is a workaround for +# https://github.com/haskell/haddock/issues/894. +TMPDIR=$TEMP %{haddock} "${extra_args[@]}" "$@" +cleanup diff --git a/third_party/bazel/rules_haskell/haskell/private/haskell_impl.bzl b/third_party/bazel/rules_haskell/haskell/private/haskell_impl.bzl new file mode 100644 index 000000000000..a58239fad1d5 --- /dev/null +++ b/third_party/bazel/rules_haskell/haskell/private/haskell_impl.bzl @@ -0,0 +1,668 @@ +"""Implementation of core Haskell rules""" + +load( + "@io_tweag_rules_haskell//haskell:providers.bzl", + "C2hsLibraryInfo", + "HaskellInfo", + "HaskellLibraryInfo", + "HaskellPrebuiltPackageInfo", +) +load(":cc.bzl", "cc_interop_info") +load( + ":private/actions/link.bzl", + "link_binary", + "link_library_dynamic", + "link_library_static", +) +load(":private/actions/package.bzl", "package") +load(":private/actions/repl.bzl", "build_haskell_repl") +load(":private/actions/runghc.bzl", "build_haskell_runghc") +load(":private/context.bzl", "haskell_context") +load(":private/dependencies.bzl", "gather_dep_info") +load(":private/java.bzl", "java_interop_info") +load(":private/mode.bzl", "is_profiling_enabled") +load( + ":private/path_utils.bzl", + "ln", + "match_label", + "parse_pattern", + "target_unique_name", +) +load(":private/pkg_id.bzl", "pkg_id") +load(":private/set.bzl", "set") +load(":private/version_macros.bzl", "generate_version_macros") +load(":providers.bzl", "GhcPluginInfo", "HaskellCoverageInfo") +load("@bazel_skylib//lib:paths.bzl", "paths") +load("@bazel_skylib//lib:collections.bzl", "collections") +load("@bazel_skylib//lib:shell.bzl", "shell") + +def _prepare_srcs(srcs): + srcs_files = [] + import_dir_map = {} + + for src in srcs: + # If it has the "files" attribute, it must be a Target + if hasattr(src, "files"): + if C2hsLibraryInfo in src: + srcs_files += src.files.to_list() + for f in src.files.to_list(): + import_dir_map[f] = src[C2hsLibraryInfo].import_dir + else: + srcs_files += src.files.to_list() + + # otherwise it's just a file + + else: + srcs_files.append(src) + + return srcs_files, import_dir_map + +def haskell_test_impl(ctx): + return _haskell_binary_common_impl(ctx, is_test = True) + +def haskell_binary_impl(ctx): + return _haskell_binary_common_impl(ctx, is_test = False) + +def _should_inspect_coverage(ctx, hs, is_test): + return hs.coverage_enabled and is_test + +def _coverage_enabled_for_target(coverage_source_patterns, label): + for pat in coverage_source_patterns: + if match_label(pat, label): + return True + + return False + +# Mix files refer to genfile srcs including their root. Therefore, we +# must condition the src filepaths passed in for coverage to match. +def _condition_coverage_src(hs, src): + if not src.path.startswith(hs.genfiles_dir.path): + return src + + """ Genfiles have the genfile directory as part of their path, + so declaring a file with the sample path actually makes the new + file double-qualified by the genfile directory. + + This is necessary because mix files capture the genfile + path before compilation, and then expect those files to be + qualified by the genfile directory when `hpc report` or + `hpc markup` are used. But, genfiles included as runfiles + are no longer qualified. So, double-qualifying them results in + only one level of qualification as runfiles. + """ + conditioned_src = hs.actions.declare_file(src.path) + hs.actions.run_shell( + inputs = [src], + outputs = [conditioned_src], + arguments = [ + src.path, + conditioned_src.path, + ], + command = """ + mkdir -p $(dirname "$2") && cp "$1" "$2" + """, + ) + + return conditioned_src + +def _haskell_binary_common_impl(ctx, is_test): + hs = haskell_context(ctx) + dep_info = gather_dep_info(ctx, ctx.attr.deps) + plugin_dep_info = gather_dep_info( + ctx, + [dep for plugin in ctx.attr.plugins for dep in plugin[GhcPluginInfo].deps], + ) + + # Add any interop info for other languages. + cc = cc_interop_info(ctx) + java = java_interop_info(ctx) + + with_profiling = is_profiling_enabled(hs) + srcs_files, import_dir_map = _prepare_srcs(ctx.attr.srcs) + inspect_coverage = _should_inspect_coverage(ctx, hs, is_test) + + c = hs.toolchain.actions.compile_binary( + hs, + cc, + java, + dep_info, + plugin_dep_info, + srcs = srcs_files, + ls_modules = ctx.executable._ls_modules, + import_dir_map = import_dir_map, + extra_srcs = depset(ctx.files.extra_srcs), + user_compile_flags = ctx.attr.compiler_flags, + dynamic = False if hs.toolchain.is_windows else not ctx.attr.linkstatic, + with_profiling = False, + main_function = ctx.attr.main_function, + version = ctx.attr.version, + inspect_coverage = inspect_coverage, + plugins = ctx.attr.plugins, + ) + + # gather intermediary code coverage instrumentation data + coverage_data = c.coverage_data + for dep in ctx.attr.deps: + if HaskellCoverageInfo in dep: + coverage_data += dep[HaskellCoverageInfo].coverage_data + + c_p = None + + if with_profiling: + c_p = hs.toolchain.actions.compile_binary( + hs, + cc, + java, + dep_info, + plugin_dep_info, + srcs = srcs_files, + ls_modules = ctx.executable._ls_modules, + import_dir_map = import_dir_map, + # NOTE We must make the object files compiled without profiling + # available to this step for TH to work, presumably because GHC is + # linked against RTS without profiling. + extra_srcs = depset(transitive = [ + depset(ctx.files.extra_srcs), + depset([c.objects_dir]), + ]), + user_compile_flags = ctx.attr.compiler_flags, + # NOTE We can't have profiling and dynamic code at the + # same time, see: + # https://ghc.haskell.org/trac/ghc/ticket/15394 + dynamic = False, + with_profiling = True, + main_function = ctx.attr.main_function, + version = ctx.attr.version, + plugins = ctx.attr.plugins, + ) + + (binary, solibs) = link_binary( + hs, + cc, + dep_info, + ctx.files.extra_srcs, + ctx.attr.compiler_flags, + c_p.objects_dir if with_profiling else c.objects_dir, + dynamic = False if hs.toolchain.is_windows else not ctx.attr.linkstatic, + with_profiling = with_profiling, + version = ctx.attr.version, + ) + + hs_info = HaskellInfo( + package_ids = dep_info.package_ids, + package_databases = dep_info.package_databases, + version_macros = set.empty(), + source_files = c.source_files, + extra_source_files = c.extra_source_files, + import_dirs = c.import_dirs, + static_libraries = dep_info.static_libraries, + static_libraries_prof = dep_info.static_libraries_prof, + dynamic_libraries = dep_info.dynamic_libraries, + interface_dirs = dep_info.interface_dirs, + compile_flags = c.compile_flags, + prebuilt_dependencies = dep_info.prebuilt_dependencies, + cc_dependencies = dep_info.cc_dependencies, + transitive_cc_dependencies = dep_info.transitive_cc_dependencies, + ) + cc_info = cc_common.merge_cc_infos( + cc_infos = [dep[CcInfo] for dep in ctx.attr.deps if CcInfo in dep], + ) + + target_files = depset([binary]) + + build_haskell_repl( + hs, + ghci_script = ctx.file._ghci_script, + ghci_repl_wrapper = ctx.file._ghci_repl_wrapper, + user_compile_flags = ctx.attr.compiler_flags, + repl_ghci_args = ctx.attr.repl_ghci_args, + output = ctx.outputs.repl, + package_databases = dep_info.package_databases, + version = ctx.attr.version, + hs_info = hs_info, + ) + + # XXX Temporary backwards compatibility hack. Remove eventually. + # See https://github.com/tweag/rules_haskell/pull/460. + ln(hs, ctx.outputs.repl, ctx.outputs.repl_deprecated) + + build_haskell_runghc( + hs, + runghc_wrapper = ctx.file._ghci_repl_wrapper, + extra_args = ctx.attr.runcompile_flags, + user_compile_flags = ctx.attr.compiler_flags, + output = ctx.outputs.runghc, + package_databases = dep_info.package_databases, + version = ctx.attr.version, + hs_info = hs_info, + ) + + executable = binary + extra_runfiles = [] + + if inspect_coverage: + binary_path = paths.join(ctx.workspace_name, binary.short_path) + hpc_path = paths.join(ctx.workspace_name, hs.toolchain.tools.hpc.short_path) + tix_file_path = hs.label.name + ".tix" + mix_file_paths = [ + paths.join(ctx.workspace_name, datum.mix_file.short_path) + for datum in coverage_data + ] + mix_file_paths = collections.uniq(mix_file_paths) # remove duplicates + + # find which modules to exclude from coverage analysis, by using the specified source patterns + raw_coverage_source_patterns = ctx.attr.experimental_coverage_source_patterns + coverage_source_patterns = [parse_pattern(ctx, pat) for pat in raw_coverage_source_patterns] + modules_to_exclude = [paths.split_extension(datum.mix_file.basename)[0] for datum in coverage_data if not _coverage_enabled_for_target(coverage_source_patterns, datum.target_label)] + modules_to_exclude = collections.uniq(modules_to_exclude) # remove duplicates + + expected_covered_expressions_percentage = ctx.attr.expected_covered_expressions_percentage + expected_uncovered_expression_count = ctx.attr.expected_uncovered_expression_count + strict_coverage_analysis = ctx.attr.strict_coverage_analysis + coverage_report_format = ctx.attr.coverage_report_format + + if coverage_report_format != "text" and coverage_report_format != "html": + fail("""haskell_test attribute "coverage_report_format" must be one of "text" or "html".""") + + wrapper = hs.actions.declare_file("{}_coverage/coverage_wrapper.sh".format(ctx.label.name)) + ctx.actions.expand_template( + template = ctx.file._coverage_wrapper_template, + output = wrapper, + substitutions = { + "{binary_path}": shell.quote(binary_path), + "{hpc_path}": shell.quote(hpc_path), + "{tix_file_path}": shell.quote(tix_file_path), + "{expected_covered_expressions_percentage}": str(expected_covered_expressions_percentage), + "{expected_uncovered_expression_count}": str(expected_uncovered_expression_count), + "{mix_file_paths}": shell.array_literal(mix_file_paths), + "{modules_to_exclude}": shell.array_literal(modules_to_exclude), + "{strict_coverage_analysis}": str(strict_coverage_analysis), + "{coverage_report_format}": shell.quote(ctx.attr.coverage_report_format), + "{package_path}": shell.quote(ctx.label.package), + }, + is_executable = True, + ) + executable = wrapper + mix_runfiles = [datum.mix_file for datum in coverage_data] + srcs_runfiles = [_condition_coverage_src(hs, datum.src_file) for datum in coverage_data] + extra_runfiles = [ + ctx.file._bash_runfiles, + hs.toolchain.tools.hpc, + binary, + ] + mix_runfiles + srcs_runfiles + + return [ + hs_info, + cc_info, + DefaultInfo( + executable = executable, + files = target_files, + runfiles = ctx.runfiles( + files = + solibs + + extra_runfiles, + collect_data = True, + ), + ), + ] + +def haskell_library_impl(ctx): + hs = haskell_context(ctx) + dep_info = gather_dep_info(ctx, ctx.attr.deps) + plugin_dep_info = gather_dep_info( + ctx, + [dep for plugin in ctx.attr.plugins for dep in plugin[GhcPluginInfo].deps], + ) + version = ctx.attr.version if ctx.attr.version else None + my_pkg_id = pkg_id.new(ctx.label, version) + with_profiling = is_profiling_enabled(hs) + with_shared = False if hs.toolchain.is_windows else not ctx.attr.linkstatic + + # Add any interop info for other languages. + cc = cc_interop_info(ctx) + java = java_interop_info(ctx) + + srcs_files, import_dir_map = _prepare_srcs(ctx.attr.srcs) + other_modules = ctx.attr.hidden_modules + exposed_modules_reexports = _exposed_modules_reexports(ctx.attr.exports) + + c = hs.toolchain.actions.compile_library( + hs, + cc, + java, + dep_info, + plugin_dep_info, + srcs = srcs_files, + ls_modules = ctx.executable._ls_modules, + other_modules = other_modules, + exposed_modules_reexports = exposed_modules_reexports, + import_dir_map = import_dir_map, + extra_srcs = depset(ctx.files.extra_srcs), + user_compile_flags = ctx.attr.compiler_flags, + with_shared = with_shared, + with_profiling = False, + my_pkg_id = my_pkg_id, + plugins = ctx.attr.plugins, + ) + + c_p = None + + if with_profiling: + c_p = hs.toolchain.actions.compile_library( + hs, + cc, + java, + dep_info, + plugin_dep_info, + srcs = srcs_files, + ls_modules = ctx.executable._ls_modules, + other_modules = other_modules, + exposed_modules_reexports = exposed_modules_reexports, + import_dir_map = import_dir_map, + # NOTE We must make the object files compiled without profiling + # available to this step for TH to work, presumably because GHC is + # linked against RTS without profiling. + extra_srcs = depset(transitive = [ + depset(ctx.files.extra_srcs), + depset([c.objects_dir]), + ]), + user_compile_flags = ctx.attr.compiler_flags, + # NOTE We can't have profiling and dynamic code at the + # same time, see: + # https://ghc.haskell.org/trac/ghc/ticket/15394 + with_shared = False, + with_profiling = True, + my_pkg_id = my_pkg_id, + plugins = ctx.attr.plugins, + ) + + static_library = link_library_static( + hs, + cc, + dep_info, + c.objects_dir, + my_pkg_id, + with_profiling = False, + ) + + if with_shared: + dynamic_library = link_library_dynamic( + hs, + cc, + dep_info, + depset(ctx.files.extra_srcs), + c.objects_dir, + my_pkg_id, + ) + dynamic_libraries = set.insert( + dep_info.dynamic_libraries, + dynamic_library, + ) + else: + dynamic_library = None + dynamic_libraries = dep_info.dynamic_libraries + + static_library_prof = None + if with_profiling: + static_library_prof = link_library_static( + hs, + cc, + dep_info, + c_p.objects_dir, + my_pkg_id, + with_profiling = True, + ) + + conf_file, cache_file = package( + hs, + dep_info, + c.interfaces_dir, + c_p.interfaces_dir if c_p != None else None, + static_library, + dynamic_library, + c.exposed_modules_file, + other_modules, + my_pkg_id, + static_library_prof = static_library_prof, + ) + + static_libraries_prof = dep_info.static_libraries_prof + + if static_library_prof != None: + static_libraries_prof = [static_library_prof] + dep_info.static_libraries_prof + + interface_dirs = set.union( + dep_info.interface_dirs, + set.singleton(c.interfaces_dir), + ) + + if c_p != None: + interface_dirs = set.mutable_union( + interface_dirs, + set.singleton(c_p.interfaces_dir), + ) + + version_macros = set.empty() + if version != None: + version_macros = set.singleton( + generate_version_macros(ctx, hs.name, version), + ) + + hs_info = HaskellInfo( + package_ids = set.insert(dep_info.package_ids, pkg_id.to_string(my_pkg_id)), + package_databases = set.insert(dep_info.package_databases, cache_file), + version_macros = version_macros, + source_files = c.source_files, + extra_source_files = c.extra_source_files, + import_dirs = c.import_dirs, + # NOTE We have to use lists for static libraries because the order is + # important for linker. Linker searches for unresolved symbols to the + # left, i.e. you first feed a library which has unresolved symbols and + # then you feed the library which resolves the symbols. + static_libraries = [static_library] + dep_info.static_libraries, + static_libraries_prof = static_libraries_prof, + dynamic_libraries = dynamic_libraries, + interface_dirs = interface_dirs, + compile_flags = c.compile_flags, + prebuilt_dependencies = dep_info.prebuilt_dependencies, + cc_dependencies = dep_info.cc_dependencies, + transitive_cc_dependencies = dep_info.transitive_cc_dependencies, + ) + lib_info = HaskellLibraryInfo( + package_id = pkg_id.to_string(my_pkg_id), + version = version, + ) + + dep_coverage_data = [] + for dep in ctx.attr.deps: + if HaskellCoverageInfo in dep: + dep_coverage_data += dep[HaskellCoverageInfo].coverage_data + + coverage_info = HaskellCoverageInfo( + coverage_data = dep_coverage_data + c.coverage_data, + ) + + target_files = depset([file for file in [static_library, dynamic_library] if file]) + + if hasattr(ctx, "outputs"): + build_haskell_repl( + hs, + ghci_script = ctx.file._ghci_script, + ghci_repl_wrapper = ctx.file._ghci_repl_wrapper, + repl_ghci_args = ctx.attr.repl_ghci_args, + user_compile_flags = ctx.attr.compiler_flags, + output = ctx.outputs.repl, + package_databases = dep_info.package_databases, + version = ctx.attr.version, + hs_info = hs_info, + lib_info = lib_info, + ) + + # XXX Temporary backwards compatibility hack. Remove eventually. + # See https://github.com/tweag/rules_haskell/pull/460. + ln(hs, ctx.outputs.repl, ctx.outputs.repl_deprecated) + + build_haskell_runghc( + hs, + runghc_wrapper = ctx.file._ghci_repl_wrapper, + extra_args = ctx.attr.runcompile_flags, + user_compile_flags = ctx.attr.compiler_flags, + output = ctx.outputs.runghc, + package_databases = dep_info.package_databases, + version = ctx.attr.version, + hs_info = hs_info, + lib_info = lib_info, + ) + + default_info = None + + if hasattr(ctx, "runfiles"): + default_info = DefaultInfo( + files = target_files, + runfiles = ctx.runfiles(collect_data = True), + ) + else: + default_info = DefaultInfo( + files = target_files, + ) + + # Create a CcInfo provider so that CC rules can work with + # a haskell library as if it was a regular CC one. + + # XXX Workaround https://github.com/bazelbuild/bazel/issues/6874. + # Should be find_cpp_toolchain() instead. + cc_toolchain = ctx.attr._cc_toolchain[cc_common.CcToolchainInfo] + feature_configuration = cc_common.configure_features( + cc_toolchain = cc_toolchain, + requested_features = ctx.features, + unsupported_features = ctx.disabled_features, + ) + library_to_link = cc_common.create_library_to_link( + actions = ctx.actions, + feature_configuration = feature_configuration, + dynamic_library = dynamic_library, + static_library = static_library, + cc_toolchain = cc_toolchain, + ) + compilation_context = cc_common.create_compilation_context() + linking_context = cc_common.create_linking_context( + libraries_to_link = [library_to_link], + ) + cc_info = cc_common.merge_cc_infos( + cc_infos = [ + CcInfo( + compilation_context = compilation_context, + linking_context = linking_context, + ), + ] + [dep[CcInfo] for dep in ctx.attr.deps if CcInfo in dep], + ) + + return [ + hs_info, + cc_info, + coverage_info, + default_info, + lib_info, + ] + +def haskell_toolchain_library_impl(ctx): + hs = haskell_context(ctx) + + if ctx.attr.package: + package = ctx.attr.package + else: + package = ctx.label.name + + id_file = hs.actions.declare_file(target_unique_name(hs, "id")) + hs.actions.run_shell( + inputs = [hs.tools.ghc_pkg], + outputs = [id_file], + command = """ + "$1" --simple-output -v1 field "$2" id > "$3" + """, + arguments = [ + hs.tools.ghc_pkg.path, + package, + id_file.path, + ], + ) + + version_macros_file = hs.actions.declare_file("{}_version_macros.h".format(hs.name)) + hs.actions.run_shell( + inputs = [hs.tools.ghc_pkg, ctx.executable._version_macros], + outputs = [version_macros_file], + command = """ + "$1" \\ + `"$2" --simple-output -v1 field "$3" name` \\ + `"$2" --simple-output -v1 field "$3" version` \\ + > "$4" + """, + arguments = [ + ctx.executable._version_macros.path, + hs.tools.ghc_pkg.path, + package, + version_macros_file.path, + ], + ) + + prebuilt_package_info = HaskellPrebuiltPackageInfo( + package = package, + id_file = id_file, + version_macros_file = version_macros_file, + ) + + return [prebuilt_package_info] + +def _exposed_modules_reexports(exports): + """Creates a ghc-pkg-compatible list of reexport declarations. + + A ghc-pkg registration file declares reexports as part of the + exposed-modules field in the following format: + + exposed-modules: A, B, C from pkg-c:C, D from pkg-d:Original.D + + Here, the Original.D module from pkg-d is renamed by virtue of a + different name being used before the "from" keyword. + + This function creates a ghc-pkg-compatible list of reexport declarations + (as shown above) from a dictionary mapping package targets to "Cabal-style" + reexported-modules declarations. That is, something like: + + { + ":pkg-c": "C", + ":pkg-d": "Original.D as D", + ":pkg-e": "E1, Original.E2 as E2", + } + + Args: + exports: a dictionary mapping package targets to "Cabal-style" + reexported-modules declarations. + + Returns: + a ghc-pkg-compatible list of reexport declarations. + """ + exposed_reexports = [] + for dep, cabal_decls in exports.items(): + for cabal_decl in cabal_decls.split(","): + stripped_cabal_decl = cabal_decl.strip() + cabal_decl_parts = stripped_cabal_decl.split(" as ") + original = cabal_decl_parts[0] + if len(cabal_decl_parts) == 2: + reexported = cabal_decl_parts[1] + else: + reexported = cabal_decl_parts[0] + + if HaskellPrebuiltPackageInfo in dep: + pkg = dep[HaskellPrebuiltPackageInfo].package + elif HaskellLibraryInfo in dep: + pkg = dep[HaskellLibraryInfo].package_id + + exposed_reexport = "{reexported} from {pkg}:{original}".format( + reexported = reexported, + pkg = pkg, + original = original, + ) + + exposed_reexports.append(exposed_reexport) + + return exposed_reexports diff --git a/third_party/bazel/rules_haskell/haskell/private/java.bzl b/third_party/bazel/rules_haskell/haskell/private/java.bzl new file mode 100644 index 000000000000..44c4e114d7ba --- /dev/null +++ b/third_party/bazel/rules_haskell/haskell/private/java.bzl @@ -0,0 +1,48 @@ +"""Interop with Java.""" + +load("@bazel_skylib//lib:collections.bzl", "collections") + +JavaInteropInfo = provider( + doc = "Information needed for interop with Java rules.", + fields = { + "inputs": "Files needed during build.", + "env": "Dict with env variables that should be set during build.", + }, +) + +def java_interop_info(ctx): + """Gather information from any Java dependencies. + + Args: + ctx: Rule context. + + Returns: + JavaInteropInfo: Information needed for Java interop. + """ + + inputs = depset( + transitive = [ + # We only expose direct dependencies, though we could + # expose transitive ones as well. Only exposing the direct + # ones corresponds to Bazel's "strict Java dependencies" + # mode. See + # https://github.com/tweag/rules_haskell/issues/96. + dep[JavaInfo].compile_jars + for dep in ctx.attr.deps + if JavaInfo in dep + ], + ) + + env_dict = dict() + uniq_classpath = collections.uniq([ + f.path + for f in inputs + ]) + + if len(uniq_classpath) > 0: + env_dict["CLASSPATH"] = ":".join(uniq_classpath) + + return JavaInteropInfo( + inputs = inputs, + env = env_dict, + ) diff --git a/third_party/bazel/rules_haskell/haskell/private/list.bzl b/third_party/bazel/rules_haskell/haskell/private/list.bzl new file mode 100644 index 000000000000..14ffd5f06876 --- /dev/null +++ b/third_party/bazel/rules_haskell/haskell/private/list.bzl @@ -0,0 +1,26 @@ +"""Helper functions on lists.""" + +load(":private/set.bzl", "set") + +def _dedup_on(f, list_): + """deduplicate `list_` by comparing the result of applying + f to each element (e.g. comparing sub fields) + + def compare_x(el): + return el.x + + dedup_on([struct(x=3), struct(x=4), struct(x=3)], compare_x) + => [struct(x=3), struct(x=4)] + """ + seen = set.empty() + deduped = [] + for el in list_: + by = f(el) + if not set.is_member(seen, by): + set.mutable_insert(seen, by) + deduped.append(el) + return deduped + +list = struct( + dedup_on = _dedup_on, +) diff --git a/third_party/bazel/rules_haskell/haskell/private/ls_modules.py b/third_party/bazel/rules_haskell/haskell/private/ls_modules.py new file mode 100755 index 000000000000..8e281366cca9 --- /dev/null +++ b/third_party/bazel/rules_haskell/haskell/private/ls_modules.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python +# +# Create a list of exposed modules (including reexported modules) +# given a directory full of interface files and the content of the +# global package database (to mine the versions of all prebuilt +# dependencies). The exposed modules are filtered using a provided +# list of hidden modules, and augmented with reexport declarations. + +from __future__ import unicode_literals, print_function + +import collections +import fnmatch +import itertools +import os +import re +import sys +import io + +if len(sys.argv) != 6: + sys.exit("Usage: %s <DIRECTORY> <GLOBAL_PKG_DB> <HIDDEN_MODS_FILE> <REEXPORTED_MODS_FILE> <RESULT_FILE>" % sys.argv[0]) + +root = sys.argv[1] +global_pkg_db_dump = sys.argv[2] +hidden_modules_file = sys.argv[3] +reexported_modules_file = sys.argv[4] +results_file = sys.argv[5] + +with io.open(global_pkg_db_dump, "r", encoding='utf8') as f: + names = [line.split()[1] for line in f if line.startswith("name:")] + f.seek(0) + ids = [line.split()[1] for line in f if line.startswith("id:")] + + # A few sanity checks. + assert len(names) == len(ids) + + # compute duplicate, i.e. package name associated with multiples ids + duplicates = set() + if len(names) != len(set(names)): + duplicates = set([ + name for name, count in collections.Counter(names).items() + if count > 1 + ]) + + # This associate pkg name to pkg id + pkg_ids_map = dict(zip(names, ids)) + +with io.open(hidden_modules_file, "r", encoding='utf8') as f: + hidden_modules = [mod.strip() for mod in f.read().split(",")] + +with io.open(reexported_modules_file, "r", encoding='utf8') as f: + raw_reexported_modules = ( + mod.strip() for mod in f.read().split(",") if mod.strip() + ) + # Substitute package ids for package names in reexports, because + # GHC really wants package ids. + regexp = re.compile("from (%s):" % "|".join(map(re.escape, pkg_ids_map))) + + def replace_pkg_by_pkgid(match): + pkgname = match.group(1) + + if pkgname in duplicates: + sys.exit( + "\n".join([ + "Multiple versions of the following packages installed: ", + ", ".join(duplicates), + "\nThe following was explictly used: " + pkgname, + "\nThis is not currently supported.", + ]) + ) + + return "from %s:" % pkg_ids_map[pkgname] + + reexported_modules = ( + regexp.sub(replace_pkg_by_pkgid, mod) + for mod in raw_reexported_modules + ) + +def handle_walk_error(e): + print(""" +Failed to list interface files: + {} +On Windows you may need to enable long file path support: + Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' -Name 'LongPathsEnabled' -Value 1 + """.strip().format(e), file=sys.stderr) + exit(1) + +interface_files = ( + os.path.join(path, f) + for path, dirs, files in os.walk(root, onerror=handle_walk_error) + for f in fnmatch.filter(files, '*.hi') +) + +modules = ( + # replace directory separators by . to generate module names + # / and \ are respectively the separators for unix (linux / darwin) and windows systems + os.path.splitext(os.path.relpath(f, start=root))[0] + .replace("/",".") + .replace("\\",".") + for f in interface_files +) + +exposed_modules = ( + m + for m in modules + if m not in hidden_modules +) + +with io.open(results_file, "w", encoding='utf8') as f: + f.write(", ".join(itertools.chain(exposed_modules, reexported_modules))) diff --git a/third_party/bazel/rules_haskell/haskell/private/mode.bzl b/third_party/bazel/rules_haskell/haskell/private/mode.bzl new file mode 100644 index 000000000000..8058f09eb536 --- /dev/null +++ b/third_party/bazel/rules_haskell/haskell/private/mode.bzl @@ -0,0 +1,12 @@ +"""Compilation modes.""" + +def is_profiling_enabled(hs): + """Check whether profiling mode is enabled. + + Args: + hs: Haskell context. + + Returns: + bool: True if the mode is enabled, False otherwise. + """ + return hs.mode == "dbg" diff --git a/third_party/bazel/rules_haskell/haskell/private/osx_cc_wrapper.sh.tpl b/third_party/bazel/rules_haskell/haskell/private/osx_cc_wrapper.sh.tpl new file mode 100644 index 000000000000..9abf9ce9a1a2 --- /dev/null +++ b/third_party/bazel/rules_haskell/haskell/private/osx_cc_wrapper.sh.tpl @@ -0,0 +1,313 @@ +#!/bin/bash +# +# Copyright 2015 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# This is a wrapper script around gcc/clang that adjusts linker flags for +# Haskell library and binary targets. +# +# Load commands that attempt to load dynamic libraries relative to the working +# directory in their package output path (bazel-out/...) are converted to load +# commands relative to @rpath. rules_haskell passes the corresponding +# -Wl,-rpath,... flags itself. +# +# rpath commands that attempt to add rpaths relative to the working directory +# to look for libraries in their package output path (bazel-out/...) are +# omitted, since rules_haskell adds more appropriate rpaths itself. +# +# GHC generates intermediate dynamic libraries outside the build tree. +# Additional RPATH entries are provided for those to make dynamic library +# dependencies in the Bazel build tree available at runtime. +# +# See https://blogs.oracle.com/dipol/entry/dynamic_libraries_rpath_and_mac +# on how to set those paths for Mach-O binaries. +# +set -euo pipefail + +INSTALL_NAME_TOOL="/usr/bin/install_name_tool" +OTOOL="/usr/bin/otool" + +# Collect arguments to forward in a fresh response file. +RESPONSE_FILE="$(mktemp osx_cc_args_XXXX.rsp)" +rm_response_file() { + rm -f "$RESPONSE_FILE" +} +trap rm_response_file EXIT + +add_args() { + # Add the given arguments to the fresh response file. We follow GHC's + # example in storing one argument per line, wrapped in double quotes. Double + # quotes in the argument itself are escaped. + for arg in "$@"; do + printf '"%s"\n' "${arg//\"/\\\"}" >> "$RESPONSE_FILE" + done +} + +# Collect library, library dir, and rpath arguments. +LIBS=() +LIB_DIRS=() +RPATHS=() + +# Parser state. +# Parsing response file - unquote arguments. +QUOTES= +# Upcoming linker argument. +LINKER= +# Upcoming rpath argument. +RPATH= +# Upcoming install-name argument. +INSTALL= +# Upcoming output argument. +OUTPUT= + +parse_arg() { + # Parse the given argument. Decide whether to pass it on to the compiler, + # and how it affects the parser state. + local arg="$1" + # Unquote response file arguments. + if [[ "$QUOTES" = "1" && "$arg" =~ ^\"(.*)\"$ ]]; then + # Take GHC's argument quoting into account when parsing a response + # file. Note, no indication was found that GHC would pass multiline + # arguments, or insert escape codes into the quoted arguments. If you + # observe ill-formed arguments being passed to the compiler, then this + # logic may need to be extended. + arg="${BASH_REMATCH[1]}" + fi + # Parse given argument. + if [[ "$OUTPUT" = "1" ]]; then + # The previous argument was -o. Read output file. + OUTPUT="$arg" + add_args "$arg" + elif [[ "$LINKER" = "1" ]]; then + # The previous argument was -Xlinker. Read linker argument. + if [[ "$RPATH" = "1" ]]; then + # The previous argument was -rpath. Read RPATH. + parse_rpath "$arg" + RPATH=0 + elif [[ "$arg" = "-rpath" ]]; then + # rpath is coming + RPATH=1 + else + # Unrecognized linker argument. Pass it on. + add_args "-Xlinker" "$arg" + fi + LINKER= + elif [[ "$INSTALL" = "1" ]]; then + INSTALL= + add_args "$arg" + elif [[ "$arg" =~ ^@(.*)$ ]]; then + # Handle response file argument. Parse the arguments contained in the + # response file one by one. Take GHC's argument quoting into account. + # Note, assumes that response file arguments are not nested in other + # response files. + QUOTES=1 + while read line; do + parse_arg "$line" + done < "${BASH_REMATCH[1]}" + QUOTES= + elif [[ "$arg" = "-install_name" ]]; then + # Install name is coming. We don't use it, but it can start with an @ + # and be mistaken for a response file. + INSTALL=1 + add_args "$arg" + elif [[ "$arg" = "-o" ]]; then + # output is coming + OUTPUT=1 + add_args "$arg" + elif [[ "$arg" = "-Xlinker" ]]; then + # linker flag is coming + LINKER=1 + elif [[ "$arg" =~ ^-l(.*)$ ]]; then + LIBS+=("${BASH_REMATCH[1]}") + add_args "$arg" + elif [[ "$arg" =~ ^-L(.*)$ ]]; then + LIB_DIRS+=("${BASH_REMATCH[1]}") + add_args "$arg" + elif [[ "$arg" =~ ^-Wl,-rpath,(.*)$ ]]; then + parse_rpath "${BASH_REMATCH[1]}" + else + # Unrecognized argument. Pass it on. + add_args "$arg" + fi +} + +parse_rpath() { + # Parse the given -rpath argument and decide whether it should be + # forwarded to the compiler/linker. + local rpath="$1" + if [[ "$rpath" =~ ^/ || "$rpath" =~ ^@ ]]; then + # Absolute rpaths or rpaths relative to @loader_path or similar, are + # passed on to the linker. Other relative rpaths are dropped, these + # are auto-generated by GHC, but are useless because rules_haskell + # constructs dedicated rpaths to the _solib or _hssolib directory. + # See https://github.com/tweag/rules_haskell/issues/689 + add_args "-Wl,-rpath,$rpath" + RPATHS+=("$rpath") + fi +} + +# Parse all given arguments. +for arg in "$@"; do + parse_arg "$arg" +done + +get_library_in() { + # Find the given library in the given directory. + # Returns empty string if the library is not found. + local lib="$1" + local dir="$2" + local solib="${dir}${dir:+/}lib${lib}.so" + local dylib="${dir}${dir:+/}lib${lib}.dylib" + if [[ -f "$solib" ]]; then + echo "$solib" + elif [[ -f "$dylib" ]]; then + echo "$dylib" + fi +} + +get_library_path() { + # Find the given library in the specified library search paths. + # Returns empty string if the library is not found. + if [[ ${#LIB_DIRS[@]} -gt 0 ]]; then + local libpath + for libdir in "${LIB_DIRS[@]}"; do + libpath="$(get_library_in "$1" "$libdir")" + if [[ -n "$libpath" ]]; then + echo "$libpath" + return + fi + done + fi +} + +resolve_rpath() { + # Resolve the given rpath. I.e. if it is an absolute path, just return it. + # If it is relative to the output, then prepend the output path. + local rpath="$1" + if [[ "$rpath" =~ ^/ ]]; then + echo "$rpath" + elif [[ "$rpath" =~ ^@loader_path/(.*)$ || "$rpath" =~ ^@executable_path/(.*)$ ]]; then + echo "$(dirname "$OUTPUT")/${BASH_REMATCH[1]}" + else + echo "$rpath" + fi +} + +get_library_rpath() { + # Find the given library in the specified rpaths. + # Returns empty string if the library is not found. + if [[ ${#RPATHS[@]} -gt 0 ]]; then + local libdir libpath + for rpath in "${RPATHS[@]}"; do + libdir="$(resolve_rpath "$rpath")" + libpath="$(get_library_in "$1" "$libdir")" + if [[ -n "$libpath" ]]; then + echo "$libpath" + return + fi + done + fi +} + +get_library_name() { + # Get the "library name" of the given library. + "$OTOOL" -D "$1" | tail -1 +} + +relpath() { + # Find relative path from the first to the second path. Assuming the first + # is a directory. If either is an absolute path, then we return the + # absolute path to the second. + local from="$1" + local to="$2" + if [[ "$to" =~ ^/ ]]; then + echo "$to" + elif [[ "$from" =~ ^/ ]]; then + echo "$PWD/$to" + else + # Split path and store components in bash array. + IFS=/ read -a fromarr <<<"$from" + IFS=/ read -a toarr <<<"$to" + # Drop common prefix. + for ((i=0; i < ${#fromarr[@]}; ++i)); do + if [[ "${fromarr[$i]}" != "${toarr[$i]}" ]]; then + break + fi + done + # Construct relative path. + local common=$i + local out= + for ((i=$common; i < ${#fromarr[@]}; ++i)); do + out="$out${out:+/}.." + done + for ((i=$common; i < ${#toarr[@]}; ++i)); do + out="$out${out:+/}${toarr[$i]}" + done + echo $out + fi +} + +generate_rpath() { + # Generate an rpath entry for the given library path. + local rpath="$(relpath "$(dirname "$OUTPUT")" "$(dirname "$1")")" + if [[ "$rpath" =~ ^/ ]]; then + echo "$rpath" + else + # Relative rpaths are relative to the binary. + echo "@loader_path${rpath:+/}$rpath" + fi +} + +if [[ ! "$OUTPUT" =~ ^bazel-out/ && ${#LIBS[@]} -gt 0 ]]; then + # GHC generates temporary dynamic libraries during compilation outside of + # the build directory. References to dynamic C libraries are broken in this + # case. Here we add additional RPATHs to fix these references. The Hazel + # package for swagger2 is an example that triggers this issue. + for lib in "${LIBS[@]}"; do + librpath="$(get_library_rpath "$lib")" + if [[ -z "$librpath" ]]; then + # The given library was not found in any of the rpaths. + # Find it in the library search paths. + libpath="$(get_library_path "$lib")" + if [[ "$libpath" =~ ^bazel-out/ ]]; then + # The library is Bazel generated and loaded relative to PWD. + # Add an RPATH entry, so it is found at runtime. + rpath="$(generate_rpath "$libpath")" + parse_rpath "$rpath" + fi + fi + done +fi + +# Call the C++ compiler with the fresh response file. +%{cc} "@$RESPONSE_FILE" + +if [[ ${#LIBS[@]} -gt 0 ]]; then + # Replace load commands relative to the working directory, by load commands + # relative to the rpath, if the library can be found relative to an rpath. + for lib in "${LIBS[@]}"; do + librpath="$(get_library_rpath "$lib")" + if [[ -n "$librpath" ]]; then + libname="$(get_library_name "$librpath")" + if [[ "$libname" =~ ^bazel-out/ ]]; then + "${INSTALL_NAME_TOOL}" -change \ + "$libname" \ + "@rpath/$(basename "$librpath")" \ + "$OUTPUT" + fi + fi + done +fi + +# vim: ft=sh diff --git a/third_party/bazel/rules_haskell/haskell/private/packages.bzl b/third_party/bazel/rules_haskell/haskell/private/packages.bzl new file mode 100644 index 000000000000..e35fbb2820b1 --- /dev/null +++ b/third_party/bazel/rules_haskell/haskell/private/packages.bzl @@ -0,0 +1,94 @@ +"""Package list handling""" + +load(":private/set.bzl", "set") + +def pkg_info_to_compile_flags(pkg_info, for_plugin = False): + """Map package info to GHC command-line arguments. + + Args: + pkg_info: Package info collected by `ghc_info()`. + for_plugin: Whether the package is a plugin dependency. + + Returns: + The list of command-line arguments that should be passed to GHC. + """ + namespace = "plugin-" if for_plugin else "" + args = [ + # In compile.bzl, we pass this just before all -package-id + # arguments. Not doing so leads to bizarre compile-time failures. + # It turns out that equally, not doing so leads to bizarre + # link-time failures. See + # https://github.com/tweag/rules_haskell/issues/395. + "-hide-all-{}packages".format(namespace), + ] + + if not pkg_info.has_version: + args.extend([ + # Macro version are disabled for all packages by default + # and enabled for package with version + # see https://github.com/tweag/rules_haskell/issues/414 + "-fno-version-macros", + ]) + + for package in pkg_info.packages: + args.extend(["-{}package".format(namespace), package]) + + for package_id in pkg_info.package_ids: + args.extend(["-{}package-id".format(namespace), package_id]) + + for package_db in pkg_info.package_dbs: + args.extend(["-package-db", package_db]) + + return args + +def expose_packages(hs_info, lib_info, use_direct, use_my_pkg_id, custom_package_databases, version): + """ + Returns the information that is needed by GHC in order to enable haskell + packages. + + hs_info: is common to all builds + version: if the rule contains a version, we will export the CPP version macro + + All the other arguments are not understood well: + + lib_info: only used for repl and linter + use_direct: only used for repl and linter + use_my_pkg_id: only used for one specific task in compile.bzl + custom_package_databases: override the package_databases of hs_info, used only by the repl + """ + has_version = version != None and version != "" + + # Expose all prebuilt dependencies + # + # We have to remember to specify all (transitive) wired-in + # dependencies or we can't find objects for linking + # + # Set use_direct if hs_info does not have a direct_prebuilt_deps field. + packages = [] + for prebuilt_dep in set.to_list(hs_info.direct_prebuilt_deps if use_direct else hs_info.prebuilt_dependencies): + packages.append(prebuilt_dep.package) + + # Expose all bazel dependencies + package_ids = [] + for package in set.to_list(hs_info.package_ids): + # XXX: repl and lint uses this lib_info flags + # It is set to None in all other usage of this function + # TODO: find the meaning of this flag + if lib_info == None or package != lib_info.package_id: + # XXX: use_my_pkg_id is not None only in compile.bzl + if (use_my_pkg_id == None) or package != use_my_pkg_id: + package_ids.append(package) + + # Only include package DBs for deps, prebuilt deps should be found + # auto-magically by GHC + package_dbs = [] + for cache in set.to_list(hs_info.package_databases if not custom_package_databases else custom_package_databases): + package_dbs.append(cache.dirname) + + ghc_info = struct( + has_version = has_version, + packages = packages, + package_ids = package_ids, + package_dbs = package_dbs, + ) + return ghc_info 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 diff --git a/third_party/bazel/rules_haskell/haskell/private/pkg_id.bzl b/third_party/bazel/rules_haskell/haskell/private/pkg_id.bzl new file mode 100644 index 000000000000..0a3c5fa439d2 --- /dev/null +++ b/third_party/bazel/rules_haskell/haskell/private/pkg_id.bzl @@ -0,0 +1,67 @@ +"""Package identifiers""" + +load(":private/mode.bzl", "is_profiling_enabled") +load("@bazel_skylib//lib:paths.bzl", "paths") + +def _zencode(s): + """Z-escape special characters to make a valid GHC package identifier. + + Args: + s: string + """ + return s.replace("Z", "ZZ").replace("_", "ZU").replace("/", "ZS") + +def _to_string(my_pkg_id): + """Get a globally unique package identifier. + + The identifier is required to be unique for each Haskell rule. + It includes the Bazel package and the name of this component. + We can't use just the latter because then two components with + the same names in different packages would clash. + """ + return _zencode( + paths.join( + my_pkg_id.label.workspace_root, + my_pkg_id.label.package, + my_pkg_id.name, + ), + ) + +def _new(label, version = None): + """Create a new package identifier. + + Package identifiers should be globally unique. This is why we use + a label to identify them. + + Args: + label: The label of the rule declaring the package. + version: an optional version annotation. + + Returns: + string: GHC package ID to use. + + """ + return struct( + label = label, + name = label.name.replace("_", "-"), + version = version, + ) + +def _library_name(hs, my_pkg_id, prof_suffix = False): + """Get library name. + + Args: + hs: Haskell context. + my_pkg_id: pkg_id struct. + prof_suffix: whether to automatically add profiling suffix. + """ + library_name = "HS" + _to_string(my_pkg_id) + if is_profiling_enabled(hs) and prof_suffix: + library_name += "_p" + return library_name + +pkg_id = struct( + new = _new, + to_string = _to_string, + library_name = _library_name, +) diff --git a/third_party/bazel/rules_haskell/haskell/private/set.bzl b/third_party/bazel/rules_haskell/haskell/private/set.bzl new file mode 100644 index 000000000000..f5c6220f79d6 --- /dev/null +++ b/third_party/bazel/rules_haskell/haskell/private/set.bzl @@ -0,0 +1,150 @@ +"""Immutable sets that support efficient merging, traversal, and membership +check. +""" + +def _empty(): + """Create an empty set. + + Returns: + set, new empty set. + """ + return struct(_set_items = dict()) + +def _singleton(e): + """Create a set with single element `e` inside. + + Args: + e: The element to put in the set. + + Returns: + set, new set. + """ + r = dict() + r[e] = None + return struct(_set_items = r) + +def _is_member(s, e): + """Return true if `e` is in the set `s`. + + Args: + s: The set to inspect. + e: The element to search for. + + Result: + Bool, true if `e` is in `s`, false otherwise. + """ + return e in s._set_items + +def _insert(s, e): + """Insert an element into the set. + + Args: + s: Set to insert new element into. + e: The element to insert. + + Result: + A copy of set `s` with `s` element added. + """ + r = dict(s._set_items) + r[e] = None + return struct(_set_items = r) + +def _mutable_insert(s, e): + """The same as `set.insert`, but modifies the first argument in place. + + Args: + s: Set to insert new element into. + e: The element to insert. + + Result: + set `s` with `s` element added. + """ + s._set_items[e] = None + return s + +def _union(s0, s1): + """Return union of two sets. + + Args: + s0: One set. + s1: Another set. + + Result: + set, union of the two sets. + """ + r = dict(s0._set_items) + r.update(s1._set_items) + return struct(_set_items = r) + +def _mutable_union(s0, s1): + """Modify set `s0` adding elements from `s1` to it. + + Args: + s0: One set. + s1: Another set. + + Result: + set, union of the two sets. + """ + s0._set_items.update(s1._set_items) + return s0 + +def _map(s, f): + """Map elements of given set using a function. + + Args: + s: Original set. + f: Function to apply to elements of the set. + + Result: + set with elements obtained by application of function `f` to the + elements of `s`. + """ + return struct(_set_items = {f(x): None for x in s._set_items.keys()}) + +def _from_list(l): + """Create a set containing elements from given list. + + Args: + l: List, source of the elements for the new set. + + Result: + set containing elements from given list. + """ + return (struct(_set_items = {x: None for x in l})) + +def _to_list(s): + """Convert set into a list of its elements. + + Args: + s: Set to convert. + + Returns: + List of elements of the set. + """ + return s._set_items.keys() + +def _to_depset(s): + """Similar to `set.to_list`, but produces a depset. + + Args: + s: Set to convert. + + Returns: + Depset of elements from the set. + """ + return depset(_to_list(s)) + +set = struct( + empty = _empty, + singleton = _singleton, + is_member = _is_member, + insert = _insert, + mutable_insert = _mutable_insert, + union = _union, + mutable_union = _mutable_union, + map = _map, + from_list = _from_list, + to_list = _to_list, + to_depset = _to_depset, +) diff --git a/third_party/bazel/rules_haskell/haskell/private/version_macros.bzl b/third_party/bazel/rules_haskell/haskell/private/version_macros.bzl new file mode 100644 index 000000000000..35f913f26b01 --- /dev/null +++ b/third_party/bazel/rules_haskell/haskell/private/version_macros.bzl @@ -0,0 +1,47 @@ +load(":private/set.bzl", "set") + +def generate_version_macros(ctx, name, version): + """Generate a version macros header file. + + Args: + ctx: Rule context. Needs to define a _version_macros executable attribute. + name: The package name. + version: The package version. + + Returns: + Version macros header File. + """ + version_macros_file = ctx.actions.declare_file("{}_version_macros.h".format(name)) + ctx.actions.run_shell( + inputs = [ctx.executable._version_macros], + outputs = [version_macros_file], + command = """ + "$1" "$2" "$3" > "$4" + """, + arguments = [ + ctx.executable._version_macros.path, + name, + version, + version_macros_file.path, + ], + ) + return version_macros_file + +def version_macro_includes(hs_info): + """Generate a list of version macro header includes. + + Args: + hs_info: HaskellInfo provider. + + Returns: + (files, flags): + files: Set of version macros header files. + flags: List of C preprocessor flags to include version macros. + """ + files = hs_info.version_macros + flags = [ + f + for include in set.to_list(files) + for f in ["-include", include.path] + ] + return (files, flags) diff --git a/third_party/bazel/rules_haskell/haskell/private/version_macros.py b/third_party/bazel/rules_haskell/haskell/private/version_macros.py new file mode 100755 index 000000000000..4bc6052cb032 --- /dev/null +++ b/third_party/bazel/rules_haskell/haskell/private/version_macros.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +"""Generate Cabal version macros. + +Generates the content of a C header file for the given library name and version +and prints it to standard output. +""" + +import argparse + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("name", help="The package name.") + parser.add_argument("version", help="The package version.") + args = parser.parse_args() + + print(version_macros(args.name, args.version)) + + +def version_macros(name, version): + """Generate Cabal version macros. + + Based on Cabal's version macro generation, see [1]. + + [1]: http://hackage.haskell.org/package/Cabal-2.4.1.0/docs/src/Distribution.Simple.Build.Macros.html#generatePackageVersionMacros + """ + (major1, major2, minor) = version_components(version) + escaped_name = cpp_escape_name(name) + return "\n".join([ + # #define VERSION_pkg "1.2.3" + cpp_ifndef_define( + "VERSION_" + escaped_name, + [], + '"{}"'.format(version), + ), + # #define MIN_VERSION_pkg(major1, major2, minor) ... + cpp_ifndef_define( + "MIN_VERSION_" + escaped_name, + ["major1", "major2", "minor"], + " \\\n".join([ + "(", + " (major1) < {} ||".format(major1), + " (major1) == {} && (major2) < {} ||".format(major1, major2), + " (major1) == {} && (major2) == {} && (minor) <= {} )".format( + major1, major2, minor), + ])), + ]) + + +def version_components(version): + """Split version string into major1.major2.minor components.""" + components = version.split(".") + num = len(components) + + if num < 1: + raise ValueError("version should have at least one component.") + + major1 = components[0] + + if num >= 2: + major2 = components[1] + else: + major2 = "0" + + if num >= 3: + minor = components[2] + else: + minor = "0" + + return (major1, major2, minor) + + +def cpp_escape_name(name): + """Escape package name to be CPP macro safe.""" + return name.replace("-", "_") + + +def cpp_define(macro, params, val): + """CPP macro definition, optionally with parameters.""" + return "#define {macro}{params} {val}".format( + macro = macro, + params = "({})".format(",".join(params)) if params else "", + val = val, + ) + + +def cpp_ifndef(macro, body): + """CPP ifndef block.""" + return "#ifndef {macro}\n{body}\n#endif /* {macro} */".format( + macro = macro, + body = body, + ) + + +def cpp_ifndef_define(macro, params, val): + """CPP macro definition, if not previously defined.""" + return cpp_ifndef(macro, cpp_define(macro, params, val)) + + +if __name__ == "__main__": + main() |