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