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