diff options
Diffstat (limited to 'third_party/bazel/rules_haskell/haskell/private/actions/compile.bzl')
-rw-r--r-- | third_party/bazel/rules_haskell/haskell/private/actions/compile.bzl | 563 |
1 files changed, 563 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, + ) |