summary refs log blame commit diff
path: root/third_party/bazel/rules_haskell/haskell/haddock.bzl
blob: 2e9d5709ac3966f2fbacf5033591df0d654d31cf (plain) (tree)























































































































































































































































































































                                                                                                  
"""Haddock support"""

load("@bazel_skylib//lib:paths.bzl", "paths")
load(
    "@io_tweag_rules_haskell//haskell:providers.bzl",
    "HaddockInfo",
    "HaskellInfo",
    "HaskellLibraryInfo",
)
load(":private/context.bzl", "haskell_context", "render_env")
load(":private/set.bzl", "set")

def _get_haddock_path(package_id):
    """Get path to Haddock file of a package given its id.

    Args:
      package_id: string, package id.

    Returns:
      string: relative path to haddock file.
    """
    return package_id + ".haddock"

def _haskell_doc_aspect_impl(target, ctx):
    if HaskellInfo not in target or HaskellLibraryInfo not in target:
        return []

    # Packages imported via `//haskell:import.bzl%haskell_import` already
    # contain an `HaddockInfo` provider, so we just forward it
    if HaddockInfo in target:
        return []

    hs = haskell_context(ctx, ctx.rule.attr)

    package_id = target[HaskellLibraryInfo].package_id
    html_dir_raw = "doc-{0}".format(package_id)
    html_dir = ctx.actions.declare_directory(html_dir_raw)
    haddock_file = ctx.actions.declare_file(_get_haddock_path(package_id))

    # XXX Haddock really wants a version number, so invent one from
    # thin air. See https://github.com/haskell/haddock/issues/898.
    if target[HaskellLibraryInfo].version:
        version = target[HaskellLibraryInfo].version
    else:
        version = "0"

    args = ctx.actions.args()
    args.add("--package-name={0}".format(package_id))
    args.add("--package-version={0}".format(version))
    args.add_all([
        "-D",
        haddock_file.path,
        "-o",
        html_dir.path,
        "--html",
        "--hoogle",
        "--title={0}".format(package_id),
        "--hyperlinked-source",
    ])

    transitive_haddocks = {}
    transitive_html = {}

    for dep in ctx.rule.attr.deps:
        if HaddockInfo in dep:
            transitive_haddocks.update(dep[HaddockInfo].transitive_haddocks)
            transitive_html.update(dep[HaddockInfo].transitive_html)

    for pid in transitive_haddocks:
        args.add("--read-interface=../{0},{1}".format(
            pid,
            transitive_haddocks[pid].path,
        ))

    prebuilt_deps = ctx.actions.args()
    for dep in set.to_list(target[HaskellInfo].prebuilt_dependencies):
        prebuilt_deps.add(dep.package)
    prebuilt_deps.use_param_file(param_file_arg = "%s", use_always = True)

    compile_flags = ctx.actions.args()
    for x in target[HaskellInfo].compile_flags:
        compile_flags.add_all(["--optghc", x])
    compile_flags.add_all([x.path for x in set.to_list(target[HaskellInfo].source_files)])
    compile_flags.add("-v0")

    # haddock flags should take precedence over ghc args, hence are in
    # last position
    compile_flags.add_all(hs.toolchain.haddock_flags)

    locale_archive_depset = (
        depset([hs.toolchain.locale_archive]) if hs.toolchain.locale_archive != None else depset()
    )

    # TODO(mboes): we should be able to instantiate this template only
    # once per toolchain instance, rather than here.
    haddock_wrapper = ctx.actions.declare_file("haddock_wrapper-{}".format(hs.name))
    ctx.actions.expand_template(
        template = ctx.file._haddock_wrapper_tpl,
        output = haddock_wrapper,
        substitutions = {
            "%{ghc-pkg}": hs.tools.ghc_pkg.path,
            "%{haddock}": hs.tools.haddock.path,
            # XXX Workaround
            # https://github.com/bazelbuild/bazel/issues/5980.
            "%{env}": render_env(hs.env),
        },
        is_executable = True,
    )

    # Transitive library dependencies for runtime.
    trans_link_ctx = target[HaskellInfo].transitive_cc_dependencies.dynamic_linking
    trans_libs = trans_link_ctx.libraries_to_link.to_list()

    ctx.actions.run(
        inputs = depset(transitive = [
            set.to_depset(target[HaskellInfo].package_databases),
            set.to_depset(target[HaskellInfo].interface_dirs),
            set.to_depset(target[HaskellInfo].source_files),
            target[HaskellInfo].extra_source_files,
            set.to_depset(target[HaskellInfo].dynamic_libraries),
            depset(trans_libs),
            depset(transitive_haddocks.values()),
            depset(transitive_html.values()),
            target[CcInfo].compilation_context.headers,
            depset([
                hs.tools.ghc_pkg,
                hs.tools.haddock,
            ]),
            locale_archive_depset,
        ]),
        outputs = [haddock_file, html_dir],
        mnemonic = "HaskellHaddock",
        progress_message = "HaskellHaddock {}".format(ctx.label),
        executable = haddock_wrapper,
        arguments = [
            prebuilt_deps,
            args,
            compile_flags,
        ],
        use_default_shell_env = True,
    )

    transitive_html.update({package_id: html_dir})
    transitive_haddocks.update({package_id: haddock_file})

    haddock_info = HaddockInfo(
        package_id = package_id,
        transitive_html = transitive_html,
        transitive_haddocks = transitive_haddocks,
    )
    output_files = OutputGroupInfo(default = transitive_html.values())

    return [haddock_info, output_files]

haskell_doc_aspect = aspect(
    _haskell_doc_aspect_impl,
    attrs = {
        "_haddock_wrapper_tpl": attr.label(
            allow_single_file = True,
            default = Label("@io_tweag_rules_haskell//haskell:private/haddock_wrapper.sh.tpl"),
        ),
    },
    attr_aspects = ["deps"],
    toolchains = ["@io_tweag_rules_haskell//haskell:toolchain"],
)

def _haskell_doc_rule_impl(ctx):
    hs = haskell_context(ctx)

    # Reject cases when number of dependencies is 0.

    if not ctx.attr.deps:
        fail("haskell_doc needs at least one haskell_library component in deps")

    doc_root_raw = ctx.attr.name
    haddock_dict = {}
    html_dict_original = {}
    all_caches = set.empty()

    for dep in ctx.attr.deps:
        if HaddockInfo in dep:
            html_dict_original.update(dep[HaddockInfo].transitive_html)
            haddock_dict.update(dep[HaddockInfo].transitive_haddocks)
        if HaskellInfo in dep:
            set.mutable_union(
                all_caches,
                dep[HaskellInfo].package_databases,
            )

    # Copy docs of Bazel deps into predefined locations under the root doc
    # directory.

    html_dict_copied = {}
    doc_root_path = ""

    for package_id in html_dict_original:
        html_dir = html_dict_original[package_id]
        output_dir = ctx.actions.declare_directory(
            paths.join(
                doc_root_raw,
                package_id,
            ),
        )
        doc_root_path = paths.dirname(output_dir.path)

        html_dict_copied[package_id] = output_dir

        ctx.actions.run_shell(
            inputs = [html_dir],
            outputs = [output_dir],
            command = """
      mkdir -p "{doc_dir}"
      # Copy Haddocks of a dependency.
      cp -R -L "{html_dir}/." "{target_dir}"
      """.format(
                doc_dir = doc_root_path,
                html_dir = html_dir.path,
                target_dir = output_dir.path,
            ),
        )

    # Do one more Haddock call to generate the unified index

    index_root_raw = paths.join(doc_root_raw, "index")
    index_root = ctx.actions.declare_directory(index_root_raw)

    args = ctx.actions.args()
    args.add_all([
        "-o",
        index_root.path,
        "--title={0}".format(ctx.attr.name),
        "--gen-index",
        "--gen-contents",
    ])

    if ctx.attr.index_transitive_deps:
        # Include all packages in the unified index.
        for package_id in html_dict_copied:
            args.add("--read-interface=../{0},{1}".format(
                package_id,
                haddock_dict[package_id].path,
            ))
    else:
        # Include only direct dependencies.
        for dep in ctx.attr.deps:
            if HaddockInfo in dep:
                package_id = dep[HaddockInfo].package_id
                args.add("--read-interface=../{0},{1}".format(
                    package_id,
                    haddock_dict[package_id].path,
                ))

    for cache in set.to_list(all_caches):
        args.add("--optghc=-package-db={0}".format(cache.dirname))

    locale_archive_depset = (
        depset([hs.toolchain.locale_archive]) if hs.toolchain.locale_archive != None else depset()
    )

    ctx.actions.run(
        inputs = depset(transitive = [
            set.to_depset(all_caches),
            depset(html_dict_copied.values()),
            depset(haddock_dict.values()),
            locale_archive_depset,
        ]),
        outputs = [index_root],
        mnemonic = "HaskellHaddockIndex",
        executable = hs.tools.haddock,
        arguments = [args],
    )

    return [DefaultInfo(
        files = depset(html_dict_copied.values() + [index_root]),
    )]

haskell_doc = rule(
    _haskell_doc_rule_impl,
    attrs = {
        "deps": attr.label_list(
            aspects = [haskell_doc_aspect],
            doc = "List of Haskell libraries to generate documentation for.",
        ),
        "index_transitive_deps": attr.bool(
            default = False,
            doc = "Whether to include documentation of transitive dependencies in index.",
        ),
    },
    toolchains = ["@io_tweag_rules_haskell//haskell:toolchain"],
)
"""Create API documentation.

Builds API documentation (using [Haddock][haddock]) for the given
Haskell libraries. It will automatically build documentation for any
transitive dependencies to allow for cross-package documentation
linking.

Example:
  ```bzl
  haskell_library(
    name = "my-lib",
    ...
  )

  haskell_doc(
    name = "my-lib-doc",
    deps = [":my-lib"],
  )
  ```

[haddock]: http://haskell-haddock.readthedocs.io/en/latest/
"""