From 11a8d6b24dfc13d807a211ad3e2f67dbaf05626e Mon Sep 17 00:00:00 2001 From: solsjo Date: Wed, 31 Aug 2022 02:09:27 +0200 Subject: [PATCH] Add support for generating svgs from tex files This can be a nice feature when a .tex file contains a tikz figure. This is enabled by exposing the dvisvgm binary in the defined latex texlive toolchan. dvisvgm is dependent on ghostscript as a dynamic lib, which is why it has been added. The previously referenced bazel docker image in cirrus.yaml was not actively updated by google. The last supported version was 3.5. So the docker image was changed to the newly provided image. See: https://github.com/bazelbuild/continuous-integration/issues/1060 https://github.com/bazelbuild/continuous-integration/pull/1401 ghostscript is built from source using rules_foreign_cc, which can't be entirely bootstrapped at this point. (It can't build autoconf) So it needs that to be installed from e.g. apt. The bazel docker container did as far as I investigated not allow to install packages in the container, I could be wrong though. Instead I created a github workflow, which depends on bazelisk. bazelisk is a downloader for bazel. You can configure it always run with a specific version. But sense cirrus.yaml uses latest, we don't state any version meaning it will always download the latest bazel. It also enables a bazel cache which "should" speed up execution in the ci pipeline. --- .cirrus.yml | 4 +- .github/workflows/ci.yaml | 22 +++++++++ WORKSPACE | 17 +++++++ example/BUILD.bazel | 13 +++++- example/tikz.tex | 12 +++++ latex.bzl | 94 ++++++++++++++++++++++++++++++++++++++- repositories.bzl | 25 ++++++++++- third_party/BUILD.bazel | 24 ++++++++++ toolchain.bzl | 9 +++- 9 files changed, 214 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/ci.yaml create mode 100644 example/tikz.tex create mode 100644 third_party/BUILD.bazel diff --git a/.cirrus.yml b/.cirrus.yml index 021c004..e827a83 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -1,8 +1,8 @@ container: - image: l.gcr.io/google/bazel:latest + image: gcr.io/bazel-public/bazel:latest task: name: Build the example document - build_script: bazel build //example:all + build_script: bazel build //example:my_report //example:my_dvi_report task: name: Build all package tests build_script: bazel build //packages:all diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..22df597 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,22 @@ +name: Build and Deploy +on: [push] +permissions: + contents: write +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout 🛎️ + uses: actions/checkout@v3 + - uses: mishas/setup-bazelisk-action@v1 + - name: Mount bazel cache # Optional + uses: actions/cache@v1 + with: + path: "~/.cache/bazel" + key: bazel + - name: build + shell: bash + run: > + bazelisk build //example:example_svg; + bazelisk build //...; + diff --git a/WORKSPACE b/WORKSPACE index d26aa7b..9be1a81 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -2,6 +2,15 @@ workspace(name = "bazel_latex") load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") +http_archive( + name = "platforms", + urls = [ + "https://mirror.bazel.build/github.com/bazelbuild/platforms/releases/download/0.0.6/platforms-0.0.6.tar.gz", + "https://github.com/bazelbuild/platforms/releases/download/0.0.6/platforms-0.0.6.tar.gz", + ], + sha256 = "5308fc1d8865406a49427ba24a9ab53087f17f5266a7aabbfc28823f3916e1ca", +) + http_archive( name = "bazel_toolchains", sha256 = "109a99384f9d08f9e75136d218ebaebc68cc810c56897aea2224c57932052d30", @@ -22,3 +31,11 @@ load("@bazel_latex//:repositories.bzl", "latex_repositories") latex_repositories() +# Needed for building ghostscript +# Which is needed by dvisvgm, +# dvisvgm is part of the texlive toolchain, +# but cannot produce correct svg files without dynamically +# linking to ghostscript. +load("@rules_foreign_cc//foreign_cc:repositories.bzl", "rules_foreign_cc_dependencies") + +rules_foreign_cc_dependencies() diff --git a/example/BUILD.bazel b/example/BUILD.bazel index 3694b55..09da9b7 100644 --- a/example/BUILD.bazel +++ b/example/BUILD.bazel @@ -1,4 +1,4 @@ -load("@bazel_latex//:latex.bzl", "latex_document") +load("@bazel_latex//:latex.bzl", "latex_document", "latex_to_svg") latex_document( name = "my_report", @@ -26,3 +26,14 @@ latex_document( cmd_flags = ["--bibtex-cmd=biber"], main = "my_report.tex", ) + +latex_document( + name = "my_standalone_figure", + srcs = ["@bazel_latex//packages:drawstack"], + main = "tikz.tex", +) + +latex_to_svg( + name = "example_svg", + src = ":my_standalone_figure", +) diff --git a/example/tikz.tex b/example/tikz.tex new file mode 100644 index 0000000..3a01f46 --- /dev/null +++ b/example/tikz.tex @@ -0,0 +1,12 @@ +\documentclass{article} +\usepackage{tikz} +\begin{document} +\begin{tikzpicture} + +\draw (-2,0) -- (2,0); +\filldraw [gray] (0,0) circle (2pt); +\draw (-2,-2) .. controls (0,0) .. (2,-2); +\draw (-2,2) .. controls (-1,0) and (1,0) .. (2,2); + +\end{tikzpicture} +\end{document} diff --git a/latex.bzl b/latex.bzl index 4d4e811..5fd79fa 100644 --- a/latex.bzl +++ b/latex.bzl @@ -1,3 +1,5 @@ +load("@rules_foreign_cc//foreign_cc:providers.bzl", "ForeignCcArtifactInfo", "ForeignCcDepsInfo") + LatexOutputInfo = provider(fields = ['format', 'file']) def _latex_impl(ctx): @@ -79,8 +81,81 @@ _latex = rule( implementation = _latex_impl, ) -def latex_document(name, main, srcs = [], tags = [], cmd_flags = [], format="pdf"): +def _latex_to_svg_impl(ctx): + toolchain = ctx.toolchains["@bazel_latex//:latex_toolchain_type"].latexinfo + + custom_dependencies = [] + for deps in ctx.attr.deps: + for file in deps.files.to_list(): + if file.dirname not in custom_dependencies: + custom_dependencies.append(file.dirname) + custom_dependencies = ','.join(custom_dependencies) + + src = ctx.attr.src + if LatexOutputInfo in src: + input_file = src[LatexOutputInfo].file + input_format = src[LatexOutputInfo].format + else: + fail("LatexOutputInfo provider not available in src") + + flags = [] + if "pdf" in input_format: + flags.append("--flag=--pdf") + if ctx.attr.box: + flags.append("--flag=-b{}".format(ctx.attr.box)) + artifact = ctx.attr._libgs[ForeignCcDepsInfo].artifacts.to_list()[0] + libgs_path = ctx.attr.libgs_ext.format(artifact.lib_dir_name) + libgs = ["--env=LIBGS" + ":" + ctx.files._libgs[0].dirname + libgs_path] + + ctx.actions.run( + mnemonic = "DviSvgM", + use_default_shell_env = True, + executable = ctx.executable._tool, + arguments = [ + "--dep-tool=" + toolchain.kpsewhich.files.to_list()[0].path, + "--tool=" + toolchain.dvisvgm.files.to_list()[0].path, + "--input=" + input_file.path, + "--output=" + ctx.outputs.out.path, + "--tool-output=" + input_file.basename.rsplit(".", 1)[0] + ".svg", + "--inputs=" + custom_dependencies, + ] + flags + libgs, + inputs = depset( + direct = ctx.files.src + + ctx.files.deps + + [toolchain.dvisvgm.files.to_list()[0]] + + ctx.files._libgs, + transitive = [ + toolchain.kpsewhich.files, + toolchain.dvisvgm.files, + ], + ), + outputs = [ctx.outputs.out], + tools = [ctx.executable._tool], + ) + +_latex_to_svg = rule( + attrs = { + "src": attr.label(), + "deps": attr.label_list(allow_files = True), + "box": attr.string( + default = "", + values = ["", "exact", "min"], + ), + "libgs_ext": attr.string(), + "_libgs": attr.label(default="@bazel_latex//third_party:lib_ghost_script_configure"), + "_tool": attr.label( + default = Label("@bazel_latex//:tool_wrapper_py"), + executable = True, + cfg = "host", + ), + }, + outputs = {"out": "%{name}.svg"}, + toolchains = ["@bazel_latex//:latex_toolchain_type"], + implementation = _latex_to_svg_impl, +) + +def latex_document(name, main, srcs = [], tags = [], cmd_flags = [], format="pdf"): _latex( name = name, srcs = srcs + ["@bazel_latex//:core_dependencies"], @@ -107,3 +182,20 @@ def latex_document(name, main, srcs = [], tags = [], cmd_flags = [], format="pdf args = ["None"], tags = tags, ) + +def latex_to_svg(name, src, deps = [], **kwargs): + + _latex_to_svg( + name = name, + src = src, + deps = deps + [ + "@bazel_latex//:core_dependencies", + "@bazel_latex//third_party:ghostscript_dependencies" + ], + libgs_ext = select({ + "@platforms//os:macos": "/{}/libgs.dylib", + "@platforms//os:windows": "\\{}\\libgs.dll", + "//conditions:default": "/{}/libgs.so", + }), + **kwargs + ) diff --git a/repositories.bzl b/repositories.bzl index 8ae14de..6b855b8 100644 --- a/repositories.bzl +++ b/repositories.bzl @@ -5644,7 +5644,7 @@ def latex_repositories(): name = name, build_file_content = """ exports_files( - ["kpsewhich", "luatex", "bibtex", "biber"], + ["kpsewhich", "luatex", "bibtex", "biber", "dvisvgm"], visibility = ["//visibility:public"], ) """, @@ -5691,7 +5691,30 @@ filegroup( strip_prefix = "latexrun-38ff6ec2815654513c91f64bdf2a5760c85da26e", url = "https://github.com/aclements/latexrun/archive/38ff6ec2815654513c91f64bdf2a5760c85da26e.tar.gz", ) + + http_archive( + name = "rules_foreign_cc", + sha256 = "2a4d07cd64b0719b39a7c12218a3e507672b82a97b98c6a89d38565894cf7c51", + strip_prefix = "rules_foreign_cc-0.9.0", + url = "https://github.com/bazelbuild/rules_foreign_cc/archive/refs/tags/0.9.0.tar.gz", + ) + + _ALL_CONTENT = """\ +filegroup( + name = "all_srcs", + srcs = glob(["**"]), + visibility = ["//visibility:public"], +) +""" + http_archive( + name = "ghost_script_source", + sha256 = "89a4e73be3edc3e40c46892332f29b0f7ed6003b0b952697e708aed2444d9ed1", + strip_prefix = "ghostpdl-ghostpdl-9.26", + build_file_content = _ALL_CONTENT, + url = "https://github.com/ArtifexSoftware/ghostpdl/archive/refs/tags/ghostpdl-9.26.tar.gz", + ) + native.register_toolchains( "@bazel_latex//:latex_toolchain_amd64-freebsd", "@bazel_latex//:latex_toolchain_x86_64-darwin", diff --git a/third_party/BUILD.bazel b/third_party/BUILD.bazel new file mode 100644 index 0000000..cd3b400 --- /dev/null +++ b/third_party/BUILD.bazel @@ -0,0 +1,24 @@ +load("@rules_foreign_cc//foreign_cc:defs.bzl", "configure_make") + +filegroup( + name = "ghostscript_dependencies", + srcs = [ + "@texlive_texmf__texmf-dist__dvips__base", + ], + visibility = ["//visibility:public"], +) + +configure_make( + name = "lib_ghost_script_configure", + configure_in_place = True, + autogen = True, + install_prefix = "output", + lib_source = "@ghost_script_source//:all_srcs", + targets = ["so", "soinstall"], + out_shared_libs = select({ + "@platforms//os:macos": ["libgs.dylib"], + "@platforms//os:windows": ["libgs.dll"], + "//conditions:default": ["libgs.so"], + }), + visibility = ["//visibility:public"] +) diff --git a/toolchain.bzl b/toolchain.bzl index e299e62..d5a2243 100644 --- a/toolchain.bzl +++ b/toolchain.bzl @@ -1,6 +1,6 @@ LatexInfo = provider( doc = "Information about how to invoke the latex compiler", - fields = ["kpsewhich", "luatex", "bibtex", "biber"], + fields = ["kpsewhich", "luatex", "bibtex", "biber", "dvisvgm"], ) def _latex_toolchain_info_impl(ctx): @@ -11,6 +11,7 @@ def _latex_toolchain_info_impl(ctx): luatex = ctx.attr.luatex, bibtex = ctx.attr.bibtex, biber = ctx.attr.biber, + dvisvgm = ctx.attr.dvisvgm, ), ), ] @@ -37,6 +38,11 @@ _latex_toolchain_info = rule( cfg = "host", executable = True, ), + "dvisvgm": attr.label( + allow_single_file = True, + cfg = "host", + executable = True, + ), }, implementation = _latex_toolchain_info_impl, ) @@ -48,6 +54,7 @@ def latex_toolchain(platform, exec_compatible_with): luatex = "@texlive_bin__%s//:luatex" % platform, bibtex = "@texlive_bin__%s//:bibtex" % platform, biber = "@texlive_bin__%s//:biber" % platform, + dvisvgm = "@texlive_bin__%s//:dvisvgm" % platform, visibility = ["//visibility:public"], )