"""Rules for defining toolchains""" load("@bazel_skylib//lib:paths.bzl", "paths") load(":ghc_bindist.bzl", "haskell_register_ghc_bindists") load( ":private/actions/compile.bzl", "compile_binary", "compile_library", ) load( ":private/actions/link.bzl", "link_binary", "link_library_dynamic", "link_library_static", ) load(":private/actions/package.bzl", "package") _GHC_BINARIES = ["ghc", "ghc-pkg", "hsc2hs", "haddock", "ghci", "runghc", "hpc"] def _run_ghc(hs, cc, inputs, outputs, mnemonic, arguments, params_file = None, env = None, progress_message = None, input_manifests = None): if not env: env = hs.env args = hs.actions.args() args.add(hs.tools.ghc) # Do not use Bazel's CC toolchain on Windows, as it leads to linker and librarty compatibility issues. # XXX: We should also tether Bazel's CC toolchain to GHC's, so that we can properly mix Bazel-compiled # C libraries with Haskell targets. if not hs.toolchain.is_windows: args.add_all([ # GHC uses C compiler for assemly, linking and preprocessing as well. "-pgma", cc.tools.cc, "-pgmc", cc.tools.cc, "-pgml", cc.tools.cc, "-pgmP", cc.tools.cc, # Setting -pgm* flags explicitly has the unfortunate side effect # of resetting any program flags in the GHC settings file. So we # restore them here. See # https://ghc.haskell.org/trac/ghc/ticket/7929. "-optc-fno-stack-protector", "-optP-E", "-optP-undef", "-optP-traditional", ]) compile_flags_file = hs.actions.declare_file("compile_flags_%s_%s" % (hs.name, mnemonic)) extra_args_file = hs.actions.declare_file("extra_args_%s_%s" % (hs.name, mnemonic)) args.set_param_file_format("multiline") arguments.set_param_file_format("multiline") hs.actions.write(compile_flags_file, args) hs.actions.write(extra_args_file, arguments) extra_inputs = [ hs.tools.ghc, # Depend on the version file of the Haskell toolchain, # to ensure the version comparison check is run first. hs.toolchain.version_file, compile_flags_file, extra_args_file, ] + cc.files if params_file: params_file_src = params_file.path extra_inputs.append(params_file) else: params_file_src = "<(:)" # a temporary file with no contents script = """ export PATH=${PATH:-} # otherwise GCC fails on Windows # this is equivalent to 'readarray'. We do not use 'readarray' in order to # support older bash versions. while IFS= read -r line; do compile_flags+=("$line"); done < %s while IFS= read -r line; do extra_args+=("$line"); done < %s while IFS= read -r line; do param_file_args+=("$line"); done < %s "${compile_flags[@]}" "${extra_args[@]}" ${param_file_args+"${param_file_args[@]}"} """ % (compile_flags_file.path, extra_args_file.path, params_file_src) ghc_wrapper_name = "ghc_wrapper_%s_%s" % (hs.name, mnemonic) ghc_wrapper = hs.actions.declare_file(ghc_wrapper_name) hs.actions.write(ghc_wrapper, script, is_executable = True) extra_inputs.append(ghc_wrapper) if type(inputs) == type(depset()): inputs = depset(extra_inputs, transitive = [inputs]) else: inputs += extra_inputs hs.actions.run_shell( inputs = inputs, input_manifests = input_manifests, outputs = outputs, command = ghc_wrapper.path, mnemonic = mnemonic, progress_message = progress_message, env = env, arguments = [], ) return args def _haskell_toolchain_impl(ctx): # Store the binaries of interest in ghc_binaries. ghc_binaries = {} for tool in _GHC_BINARIES: for file in ctx.files.tools: if tool in ghc_binaries: continue basename_no_ext = paths.split_extension(file.basename)[0] if tool == basename_no_ext: ghc_binaries[tool] = file elif "%s-%s" % (tool, ctx.attr.version) == basename_no_ext: ghc_binaries[tool] = file if not tool in ghc_binaries: fail("Cannot find {} in {}".format(tool, ctx.attr.tools.label)) # Run a version check on the compiler. version_file = ctx.actions.declare_file("ghc-version") ghc = ghc_binaries["ghc"] ctx.actions.run_shell( inputs = [ghc], outputs = [version_file], mnemonic = "HaskellVersionCheck", command = """ {ghc} --numeric-version > {version_file} if [[ "{expected_version}" != "$(< {version_file})" ]] then echo ERROR: GHC version does not match expected version. echo Your haskell_toolchain specifies {expected_version}, echo but you have $(< {version_file}) in your environment. exit 1 fi """.format( ghc = ghc.path, version_file = version_file.path, expected_version = ctx.attr.version, ), ) # Get the versions of every prebuilt package. ghc_pkg = ghc_binaries["ghc-pkg"] pkgdb_file = ctx.actions.declare_file("ghc-global-pkgdb") ctx.actions.run_shell( inputs = [ghc_pkg], outputs = [pkgdb_file], mnemonic = "HaskellPackageDatabaseDump", command = "{ghc_pkg} dump --global > {output}".format( ghc_pkg = ghc_pkg.path, output = pkgdb_file.path, ), ) tools_struct_args = { name.replace("-", "_"): file for name, file in ghc_binaries.items() } locale_archive = None if ctx.attr.locale_archive != None: locale_archive = ctx.file.locale_archive return [ platform_common.ToolchainInfo( name = ctx.label.name, tools = struct(**tools_struct_args), compiler_flags = ctx.attr.compiler_flags, repl_ghci_args = ctx.attr.repl_ghci_args, haddock_flags = ctx.attr.haddock_flags, locale = ctx.attr.locale, locale_archive = locale_archive, osx_cc_wrapper_tpl = ctx.file._osx_cc_wrapper_tpl, mode = ctx.var["COMPILATION_MODE"], actions = struct( compile_binary = compile_binary, compile_library = compile_library, link_binary = link_binary, link_library_dynamic = link_library_dynamic, link_library_static = link_library_static, package = package, run_ghc = _run_ghc, ), is_darwin = ctx.attr.is_darwin, is_windows = ctx.attr.is_windows, version = ctx.attr.version, # Pass through the version_file, that it can be required as # input in _run_ghc, to make every call to GHC depend on a # successful version check. version_file = version_file, global_pkg_db = pkgdb_file, ), ] _haskell_toolchain = rule( _haskell_toolchain_impl, attrs = { "tools": attr.label_list( doc = "GHC and executables that come with it. First item take precedance.", mandatory = True, ), "compiler_flags": attr.string_list( doc = "A collection of flags that will be passed to GHC on every invocation.", ), "repl_ghci_args": attr.string_list( doc = "A collection of flags that will be passed to GHCI on repl invocation. It extends the `compiler_flags` collection. Flags set here have precedance over `compiler_flags`.", ), "haddock_flags": attr.string_list( doc = "A collection of flags that will be passed to haddock.", ), "version": attr.string( doc = "Version of your GHC compiler. It has to match the version reported by the GHC used by bazel.", mandatory = True, ), "is_darwin": attr.bool( doc = "Whether compile on and for Darwin (macOS).", mandatory = True, ), "is_windows": attr.bool( doc = "Whether compile on and for Windows.", mandatory = True, ), "locale": attr.string( default = "en_US.UTF-8", doc = "Locale that will be set during compiler invocations.", ), "locale_archive": attr.label( allow_single_file = True, doc = """ Label pointing to the locale archive file to use. Mostly useful on NixOS. """, ), "_osx_cc_wrapper_tpl": attr.label( allow_single_file = True, default = Label("@io_tweag_rules_haskell//haskell:private/osx_cc_wrapper.sh.tpl"), ), }, ) def haskell_toolchain( name, version, tools, exec_compatible_with = None, target_compatible_with = None, compiler_flags = [], repl_ghci_args = [], haddock_flags = [], locale_archive = None, **kwargs): """Declare a compiler toolchain. You need at least one of these declared somewhere in your `BUILD` files for the other rules to work. Once declared, you then need to *register* the toolchain using `register_toolchains` in your `WORKSPACE` file (see example below). Example: In a `BUILD` file: ```bzl haskell_toolchain( name = "ghc", version = "1.2.3", tools = ["@sys_ghc//:bin"], compiler_flags = ["-Wall"], ) ``` where `@sys_ghc` is an external repository defined in the `WORKSPACE`, e.g. using: ```bzl nixpkgs_package( name = 'sys_ghc', attribute_path = 'haskell.compiler.ghc822', ) register_toolchains("//:ghc") ``` """ if exec_compatible_with and not target_compatible_with: target_compatible_with = exec_compatible_with elif target_compatible_with and not exec_compatible_with: exec_compatible_with = target_compatible_with impl_name = name + "-impl" corrected_ghci_args = repl_ghci_args + ["-no-user-package-db"] _haskell_toolchain( name = impl_name, version = version, tools = tools, compiler_flags = compiler_flags, repl_ghci_args = corrected_ghci_args, haddock_flags = haddock_flags, visibility = ["//visibility:public"], is_darwin = select({ "@io_tweag_rules_haskell//haskell/platforms:darwin": True, "//conditions:default": False, }), is_windows = select({ "@io_tweag_rules_haskell//haskell/platforms:mingw32": True, "//conditions:default": False, }), # Ignore this attribute on any platform that is not Linux. The # LOCALE_ARCHIVE environment variable is a Linux-specific # Nixpkgs hack. locale_archive = select({ "@io_tweag_rules_haskell//haskell/platforms:linux": locale_archive, "//conditions:default": None, }), **kwargs ) native.toolchain( name = name, toolchain_type = "@io_tweag_rules_haskell//haskell:toolchain", toolchain = ":" + impl_name, exec_compatible_with = exec_compatible_with, target_compatible_with = target_compatible_with, ) def haskell_register_toolchains(version): """Download the binary distribution of GHC for your current platform and register it as a toolchain. This currently has the same effect as just `haskell_register_ghc_bindists(version)`. """ haskell_register_ghc_bindists(version)