changeset 247:70f2a3dafc1c

[build] Bundle offline Tectonic runtime Co-authored-by: Copilot <[email protected]>
author MrJuneJune <me@mrjunejune.com>
date Mon, 03 Aug 2026 20:20:06 -0700
parents 4f2b50bc78e7
children b8b6e726964a
files MODULE.bazel config/BUILD gui_ze/gui_ze.bzl mrjunejune/BUILD mrjunejune/README.md mrjunejune/latex_renderer.c mrjunejune/test/latex_editor_test.js mrjunejune/test/latex_renderer_test.c third_party/tectonic/BUILD third_party/tectonic/LICENSE third_party/tectonic/README.md third_party/tectonic/THIRD_PARTY_NOTICES.md third_party/tectonic/fonts.conf third_party/tectonic/runtime-cache.tar.gz third_party/tectonic/runtime.bzl
diffstat 15 files changed, 377 insertions(+), 56 deletions(-) [+]
line wrap: on
line diff
--- a/MODULE.bazel	Mon Aug 03 18:30:45 2026 -0700
+++ b/MODULE.bazel	Mon Aug 03 20:20:06 2026 -0700
@@ -18,6 +18,18 @@
 http_archive = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
 
 http_archive(
+  name = "tectonic_linux_x86_64",
+  urls = ["https://github.com/tectonic-typesetting/tectonic/releases/download/tectonic%400.17.0/tectonic-0.17.0-x86_64-unknown-linux-musl.tar.gz"],
+  sha256 = "8533d07f9ccbd7a65824b9e0459041bca34af1eb33daba48f59215593753a3b7",
+  build_file_content = """
+exports_files(
+    ["tectonic"],
+    visibility = ["//visibility:public"],
+)
+""",
+)
+
+http_archive(
   name = "playwright_chromium_linux",
   urls = ["https://playwright.azureedge.net/builds/chromium/1140/chromium-linux.zip"],
   sha256 = "e78cda52fa7e847abcd36a48ffabda43e71e2220a3e9a398f7c9a179529570a5",
--- a/config/BUILD	Mon Aug 03 18:30:45 2026 -0700
+++ b/config/BUILD	Mon Aug 03 20:20:06 2026 -0700
@@ -9,6 +9,14 @@
 )
 
 config_setting(
+  name = "linux_x86_64",
+  constraint_values = [
+    "@platforms//cpu:x86_64",
+    "@platforms//os:linux",
+  ],
+)
+
+config_setting(
   name        = "windows",
   constraint_values = ["@platforms//os:windows"],
 )
--- a/gui_ze/gui_ze.bzl	Mon Aug 03 18:30:45 2026 -0700
+++ b/gui_ze/gui_ze.bzl	Mon Aug 03 20:20:06 2026 -0700
@@ -28,17 +28,10 @@
     if f.path == binary.path:
       continue
 
-    start = 0
-    for directory in f.path.split("/"):
-      if directory == binary.short_path.split("/")[0]:
-        break
-      start += 1
-
-    # Remove the first folder (output) and last file (actaul files that needed to be copied)
-    paths = "/".join(f.path.split("/")[start:-1])
+    paths = "/".join(f.short_path.split("/")[:-1])
     full_path = "{}/{}".format(out_dir.path, paths)
     copy_cmd.append("mkdir -p {}".format(full_path))
-    copy_cmd.append("cp {} {}".format(f.path, full_path))
+    copy_cmd.append("cp -rL {} {}".format(f.path, full_path))
 
   copy_cmd.append("cp {} {}".format(binary.path, out_dir.path))
 
--- a/mrjunejune/BUILD	Mon Aug 03 18:30:45 2026 -0700
+++ b/mrjunejune/BUILD	Mon Aug 03 20:20:06 2026 -0700
@@ -176,9 +176,21 @@
   srcs = ["latex_renderer.c"],
   hdrs = ["latex_renderer.h"],
   copts = ["-D_GNU_SOURCE"],
+  data = select({
+    "//config:linux_x86_64": ["//third_party/tectonic:runtime"],
+    "//conditions:default": [],
+  }),
   visibility = ["//mrjunejune/test:__pkg__"],
 )
 
+filegroup(
+  name = "tectonic_runtime_data",
+  srcs = select({
+    "//config:linux_x86_64": ["//third_party/tectonic:runtime"],
+    "//conditions:default": [],
+  }),
+)
+
 # Server binary
 cc_binary(
   name = "mrjunejune_server",
@@ -195,6 +207,7 @@
   data = [
     ":src_files",
     ":config_file",
+    ":tectonic_runtime_data",
     "//:env_file",
   ],
   visibility = ["//mrjunejune/test:__pkg__"],
@@ -216,6 +229,7 @@
   data = [
     ":src_files",
     ":config_file",
+    ":tectonic_runtime_data",
     "//:env_file",
   ],
 )
--- a/mrjunejune/README.md	Mon Aug 03 18:30:45 2026 -0700
+++ b/mrjunejune/README.md	Mon Aug 03 20:20:06 2026 -0700
@@ -42,12 +42,9 @@
 ## LaTeX editor
 
 `/tools/latex_editor` sends source to the C server, which compiles it with the
-host's `/usr/bin/pdflatex` and returns the PDF directly to the browser. Install
-TeX Live on production hosts before deploying:
-
-```bash
-sudo apt-get install texlive-latex-base texlive-latex-recommended
-```
+offline Tectonic runtime from `//third_party/tectonic:runtime` and returns the
+PDF directly to the browser. Bazel pins the static compiler, prewarms common
+packages, and includes both in the server runfiles and release bundle.
 
 The compiler child uses restricted TeX file access, Linux Landlock, a seccomp
 network filter, and CPU, memory, file-size, process, and wall-clock limits.
--- a/mrjunejune/latex_renderer.c	Mon Aug 03 18:30:45 2026 -0700
+++ b/mrjunejune/latex_renderer.c	Mon Aug 03 20:20:06 2026 -0700
@@ -44,6 +44,10 @@
 #define LATEX_DIAGNOSTICS_MAX_BYTES (16 * 1024)
 #define LATEX_CPU_TIMEOUT_SECONDS 5
 #define LATEX_WALL_TIMEOUT_SECONDS 8
+#define TECTONIC_BUNDLE_URL "https://data1.fullyjustified.net/tlextras-2022.0r0.tar"
+#define TECTONIC_CACHE_PATH "third_party/tectonic/runtime/cache"
+#define TECTONIC_COMPILER_PATH "third_party/tectonic/runtime/bin/tectonic"
+#define TECTONIC_FONTCONFIG_SYSROOT_PATH "third_party/tectonic/runtime/sysroot"
 
 static char *duplicate_message(const char *message)
 {
@@ -166,7 +170,56 @@
   return ok;
 }
 
-static bool apply_filesystem_sandbox(const char *job_directory)
+static bool add_landlock_tree_rules(
+    int ruleset_fd,
+    const char *path,
+    uint64_t access,
+    unsigned depth)
+{
+  if (depth > 32 ||
+      !add_landlock_path_rule(ruleset_fd, path, access, true))
+    return false;
+
+  struct stat status;
+  if (lstat(path, &status) != 0)
+    return false;
+  if (!S_ISDIR(status.st_mode))
+    return true;
+
+  DIR *directory = opendir(path);
+  if (!directory)
+    return false;
+  bool ok = true;
+  struct dirent *entry;
+  while (ok && (entry = readdir(directory)) != NULL)
+  {
+    if (strcmp(entry->d_name, ".") == 0 ||
+        strcmp(entry->d_name, "..") == 0)
+      continue;
+    char child[PATH_MAX];
+    int length = snprintf(
+        child,
+        sizeof(child),
+        "%s/%s",
+        path,
+        entry->d_name);
+    ok = length >= 0 &&
+        (size_t)length < sizeof(child) &&
+        add_landlock_tree_rules(
+            ruleset_fd,
+            child,
+            access,
+            depth + 1);
+  }
+  closedir(directory);
+  return ok;
+}
+
+static bool apply_filesystem_sandbox(
+    const char *job_directory,
+    const char *compiler,
+    const char *cache_directory,
+    const char *fontconfig_sysroot)
 {
   int abi = syscall(
       SYS_landlock_create_ruleset,
@@ -207,32 +260,25 @@
   if (ruleset_fd < 0)
     return false;
 
-  const char *read_only_directories[] = {
-    "/usr",
-    "/lib",
-    "/lib64",
-    "/bin",
-    "/var/lib/texmf",
-    "/etc/texmf",
-    "/etc/fonts",
-  };
   bool ok = true;
-  for (size_t i = 0;
-       ok && i < sizeof(read_only_directories) / sizeof(read_only_directories[0]);
-       i++)
-  {
-    ok = add_landlock_path_rule(
-        ruleset_fd,
-        read_only_directories[i],
-        read_access,
-        false);
-  }
   if (ok)
     ok = add_landlock_path_rule(
         ruleset_fd,
-        "/etc/ld.so.cache",
-        LANDLOCK_ACCESS_FS_READ_FILE,
-        false);
+        compiler,
+        LANDLOCK_ACCESS_FS_EXECUTE | LANDLOCK_ACCESS_FS_READ_FILE,
+        true);
+  if (ok)
+    ok = add_landlock_tree_rules(
+        ruleset_fd,
+        cache_directory,
+        read_access | LANDLOCK_ACCESS_FS_MAKE_DIR,
+        0);
+  if (ok)
+    ok = add_landlock_tree_rules(
+        ruleset_fd,
+        fontconfig_sysroot,
+        read_access,
+        0);
   if (ok)
     ok = add_landlock_path_rule(
         ruleset_fd,
@@ -310,17 +356,28 @@
   return prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &program) == 0;
 }
 
-static void configure_child_environment(const char *job_directory)
+static void configure_child_environment(
+    const char *job_directory,
+    const char *cache_directory,
+    const char *fontconfig_sysroot)
 {
   clearenv();
   setenv("HOME", job_directory, 1);
   setenv("TMPDIR", job_directory, 1);
-  setenv("TEXMFVAR", job_directory, 1);
-  setenv("TEXMFCONFIG", job_directory, 1);
-  setenv("openin_any", "p", 1);
-  setenv("openout_any", "p", 1);
-  setenv("shell_escape", "0", 1);
+  setenv("XDG_CACHE_HOME", job_directory, 1);
+  char tectonic_cache[PATH_MAX];
+  snprintf(
+      tectonic_cache,
+      sizeof(tectonic_cache),
+      "%s/tectonic",
+      cache_directory);
+  setenv("TECTONIC_CACHE_DIR", tectonic_cache, 1);
+  setenv("FONTCONFIG_FILE", "fonts.conf", 1);
+  setenv("FONTCONFIG_PATH", "/etc/fonts", 1);
+  setenv("FONTCONFIG_SYSROOT", fontconfig_sysroot, 1);
+  setenv("TECTONIC_UNTRUSTED_MODE", "1", 1);
   setenv("SOURCE_DATE_EPOCH", "0", 1);
+  setenv("TZ", "UTC", 1);
   setenv("LANG", "C.UTF-8", 1);
 }
 
@@ -342,6 +399,8 @@
 
 static void run_compiler_child(
     const char *compiler,
+    const char *cache_directory,
+    const char *fontconfig_sysroot,
     const char *job_directory,
     int output_fd)
 {
@@ -358,24 +417,38 @@
   if (chdir(job_directory) != 0)
     _exit(121);
 
-  configure_child_environment(job_directory);
+  configure_child_environment(
+      job_directory,
+      cache_directory,
+      fontconfig_sysroot);
   if (!set_limit(RLIMIT_CPU, LATEX_CPU_TIMEOUT_SECONDS) ||
       !set_limit(RLIMIT_AS, 768 * 1024 * 1024) ||
       !set_limit(RLIMIT_FSIZE, LATEX_PDF_MAX_BYTES) ||
       !set_limit(RLIMIT_NOFILE, 64) ||
       !set_limit(RLIMIT_NPROC, 1))
     _exit(122);
-  if (!apply_filesystem_sandbox(job_directory) ||
+  if (!apply_filesystem_sandbox(
+          job_directory,
+          compiler,
+          cache_directory,
+          fontconfig_sysroot) ||
       !apply_network_sandbox())
     _exit(123);
 
   char *const arguments[] = {
     (char *)compiler,
-    "-interaction=nonstopmode",
-    "-halt-on-error",
-    "-no-shell-escape",
-    "-file-line-error",
-    "-output-directory=.",
+    "--untrusted",
+    "--only-cached",
+    "--chatter",
+    "minimal",
+    "--color",
+    "never",
+    "--reruns",
+    "1",
+    "--outdir",
+    ".",
+    "--bundle",
+    TECTONIC_BUNDLE_URL,
     "document.tex",
     NULL,
   };
@@ -518,6 +591,71 @@
   return true;
 }
 
+static bool resolve_runtime_path(
+    const char *relative_path,
+    char resolved[PATH_MAX])
+{
+  if (realpath(relative_path, resolved))
+    return true;
+
+  const char *runfiles = getenv("RUNFILES_DIR");
+  const char *workspace = getenv("TEST_WORKSPACE");
+  char candidate[PATH_MAX];
+  if (runfiles && workspace)
+  {
+    int length = snprintf(
+        candidate,
+        sizeof(candidate),
+        "%s/%s/%s",
+        runfiles,
+        workspace,
+        relative_path);
+    if (length >= 0 &&
+        (size_t)length < sizeof(candidate) &&
+        realpath(candidate, resolved))
+      return true;
+  }
+
+  char executable[PATH_MAX];
+  ssize_t executable_length = readlink(
+      "/proc/self/exe",
+      executable,
+      sizeof(executable) - 1);
+  if (executable_length <= 0)
+    return false;
+  executable[executable_length] = '\0';
+
+  char *last_slash = strrchr(executable, '/');
+  if (last_slash)
+  {
+    *last_slash = '\0';
+    int length = snprintf(
+        candidate,
+        sizeof(candidate),
+        "%s/%s",
+        executable,
+        relative_path);
+    if (length >= 0 &&
+        (size_t)length < sizeof(candidate) &&
+        realpath(candidate, resolved))
+      return true;
+  }
+
+  char *package = strstr(executable, "/mrjunejune/");
+  if (!package)
+    return false;
+  *package = '\0';
+  int length = snprintf(
+      candidate,
+      sizeof(candidate),
+      "%s/%s",
+      executable,
+      relative_path);
+  return length >= 0 &&
+      (size_t)length < sizeof(candidate) &&
+      realpath(candidate, resolved) != NULL;
+}
+
 Latex_Render_Result Latex_Render(const uint8_t *source, size_t source_size)
 {
   if (!source || source_size == 0)
@@ -533,11 +671,18 @@
         LATEX_RENDER_INVALID_INPUT,
         "LaTeX source cannot contain NUL bytes.");
 
-  const char *compiler = "/usr/bin/pdflatex";
-  if (access(compiler, X_OK) != 0)
+  char compiler[PATH_MAX];
+  char cache_directory[PATH_MAX];
+  char fontconfig_sysroot[PATH_MAX];
+  if (!resolve_runtime_path(TECTONIC_COMPILER_PATH, compiler) ||
+      !resolve_runtime_path(TECTONIC_CACHE_PATH, cache_directory) ||
+      !resolve_runtime_path(
+          TECTONIC_FONTCONFIG_SYSROOT_PATH,
+          fontconfig_sysroot) ||
+      access(compiler, X_OK) != 0)
     return result_with_message(
         LATEX_RENDER_COMPILER_UNAVAILABLE,
-        "The server LaTeX compiler is unavailable.");
+        "The Bazel Tectonic runtime is unavailable.");
 
   char job_directory[] = "/tmp/mrjunejune-latex-XXXXXX";
   if (!mkdtemp(job_directory))
@@ -588,7 +733,12 @@
   if (child == 0)
   {
     close(output_pipe[0]);
-    run_compiler_child(compiler, job_directory, output_pipe[1]);
+    run_compiler_child(
+        compiler,
+        cache_directory,
+        fontconfig_sysroot,
+        job_directory,
+        output_pipe[1]);
   }
   setpgid(child, child);
 
--- a/mrjunejune/test/latex_editor_test.js	Mon Aug 03 18:30:45 2026 -0700
+++ b/mrjunejune/test/latex_editor_test.js	Mon Aug 03 20:20:06 2026 -0700
@@ -224,9 +224,17 @@
     await page.goto(`${baseUrl}/tools/latex_editor`, {
       waitUntil: 'networkidle',
     });
-    await page.locator('#latexStatus[data-state="ready"]').waitFor({
+    await page.waitForFunction(() => {
+      const state = document.querySelector('#latexStatus')?.dataset.state;
+      return state === 'ready' || state === 'error';
+    }, null, {
       timeout: 15000,
     });
+    assert.equal(
+      await page.locator('#latexStatus').getAttribute('data-state'),
+      'ready',
+      await page.locator('#latexDiagnostics').textContent(),
+    );
     const editorAppearance = await page.locator('#latexSource').evaluate(element => {
       const style = getComputedStyle(element);
       const bounds = element.getBoundingClientRect();
--- a/mrjunejune/test/latex_renderer_test.c	Mon Aug 03 18:30:45 2026 -0700
+++ b/mrjunejune/test/latex_renderer_test.c	Mon Aug 03 20:20:06 2026 -0700
@@ -23,6 +23,7 @@
 
 int main(void)
 {
+  assert(chdir("/") == 0);
   size_t initial_workspaces = latex_workspace_count();
 
   const char *valid =
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/third_party/tectonic/BUILD	Mon Aug 03 20:20:06 2026 -0700
@@ -0,0 +1,15 @@
+load(":runtime.bzl", "tectonic_runtime")
+
+tectonic_runtime(
+    name = "runtime",
+    binary = "@tectonic_linux_x86_64//:tectonic",
+    cache_archive = "runtime-cache.tar.gz",
+    fontconfig = "fonts.conf",
+    license = "LICENSE",
+    notices = "THIRD_PARTY_NOTICES.md",
+    target_compatible_with = [
+        "@platforms//cpu:x86_64",
+        "@platforms//os:linux",
+    ],
+    visibility = ["//visibility:public"],
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/third_party/tectonic/LICENSE	Mon Aug 03 20:20:06 2026 -0700
@@ -0,0 +1,21 @@
+(Tectonic is licensed under the MIT License. Elements of the system from which
+it is derived are licensed under an extremely wide variety of open-source
+licenses.)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/third_party/tectonic/README.md	Mon Aug 03 20:20:06 2026 -0700
@@ -0,0 +1,17 @@
+# Tectonic
+
+The personal-site LaTeX editor uses the SHA-pinned Tectonic 0.17.0 static Linux
+binary. `runtime-cache.tar.gz` is a deterministic, revision-controlled cache of
+the common packages and fonts used by the editor and its tests.
+
+The assembled `//third_party/tectonic:runtime` tree contains:
+
+- `bin/tectonic`
+- `cache/tectonic/...`
+- `sysroot/etc/fonts/fonts.conf`
+- `LICENSE`
+- `THIRD_PARTY_NOTICES.md`
+
+The runtime tree is included in the `mrjunejune_server` runfiles and deployment
+bundle. Production does not require a system TeX installation or network access
+while compiling user documents.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/third_party/tectonic/THIRD_PARTY_NOTICES.md	Mon Aug 03 20:20:06 2026 -0700
@@ -0,0 +1,17 @@
+# Third-party notices
+
+The offline cache contains selected support files and fonts from TeX Live 2022,
+obtained through the official Tectonic bundle. TeX Live is an aggregation of
+independently licensed free software; there is no single license covering every
+file.
+
+- LaTeX base and many packages use the LaTeX Project Public License.
+- Computer Modern fonts are distributed under Donald Knuth's font terms.
+- Latin Modern fonts use the GUST Font License.
+- Individual package source files retain their upstream copyright and license
+  notices.
+
+Package-level licensing is catalogued by the TeX Catalogue:
+https://ctan.org/pkg/catalogue
+
+The Tectonic compiler's MIT license is included alongside this notice.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/third_party/tectonic/fonts.conf	Mon Aug 03 20:20:06 2026 -0700
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<!DOCTYPE fontconfig SYSTEM "urn:fontconfig:fonts.dtd">
+<fontconfig>
+  <dir>/nonexistent</dir>
+  <cachedir prefix="xdg">fontconfig</cachedir>
+</fontconfig>
Binary file third_party/tectonic/runtime-cache.tar.gz has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/third_party/tectonic/runtime.bzl	Mon Aug 03 20:20:06 2026 -0700
@@ -0,0 +1,62 @@
+def _tectonic_runtime_impl(ctx):
+    output = ctx.actions.declare_directory(ctx.label.name)
+    commands = [
+        "set -e",
+        "mkdir -p {}/bin {}/cache {}/sysroot/etc/fonts {}/sysroot/usr/share/fontconfig/conf.avail".format(
+            output.path,
+            output.path,
+            output.path,
+            output.path,
+        ),
+        "cp {} {}/bin/tectonic".format(ctx.file.binary.path, output.path),
+        "cp {} {}/sysroot/etc/fonts/fonts.conf".format(
+            ctx.file.fontconfig.path,
+            output.path,
+        ),
+        "cp {} {}/LICENSE".format(ctx.file.license.path, output.path),
+        "cp {} {}/THIRD_PARTY_NOTICES.md".format(
+            ctx.file.notices.path,
+            output.path,
+        ),
+        "tar -xzf {} -C {}/cache".format(
+            ctx.file.cache_archive.path,
+            output.path,
+        ),
+        "chmod 0555 {}/bin/tectonic".format(output.path),
+        "chmod 0444 {}/sysroot/etc/fonts/fonts.conf".format(output.path),
+        "chmod 0444 {}/LICENSE {}/THIRD_PARTY_NOTICES.md".format(
+            output.path,
+            output.path,
+        ),
+    ]
+    commands.append("chmod -R a-w {}/cache".format(output.path))
+
+    ctx.actions.run_shell(
+        inputs = [
+            ctx.file.binary,
+            ctx.file.cache_archive,
+            ctx.file.fontconfig,
+            ctx.file.license,
+            ctx.file.notices,
+        ],
+        outputs = [output],
+        command = "\n".join(commands),
+        progress_message = "Assembling offline Tectonic runtime",
+    )
+    return [
+        DefaultInfo(
+            files = depset([output]),
+            runfiles = ctx.runfiles(files = [output]),
+        ),
+    ]
+
+tectonic_runtime = rule(
+    implementation = _tectonic_runtime_impl,
+    attrs = {
+        "binary": attr.label(allow_single_file = True, mandatory = True),
+        "cache_archive": attr.label(allow_single_file = True, mandatory = True),
+        "fontconfig": attr.label(allow_single_file = True, mandatory = True),
+        "license": attr.label(allow_single_file = True, mandatory = True),
+        "notices": attr.label(allow_single_file = True, mandatory = True),
+    },
+)