# HG changeset patch # User MrJuneJune # Date 1785813606 25200 # Node ID 70f2a3dafc1c45d5a399d46abbd5bd4836d3b7c5 # Parent 4f2b50bc78e7baf8aa473ef719badd81f45df490 [build] Bundle offline Tectonic runtime Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> diff -r 4f2b50bc78e7 -r 70f2a3dafc1c MODULE.bazel --- 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", diff -r 4f2b50bc78e7 -r 70f2a3dafc1c config/BUILD --- 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"], ) diff -r 4f2b50bc78e7 -r 70f2a3dafc1c gui_ze/gui_ze.bzl --- 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)) diff -r 4f2b50bc78e7 -r 70f2a3dafc1c mrjunejune/BUILD --- 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", ], ) diff -r 4f2b50bc78e7 -r 70f2a3dafc1c mrjunejune/README.md --- 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. diff -r 4f2b50bc78e7 -r 70f2a3dafc1c mrjunejune/latex_renderer.c --- 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); diff -r 4f2b50bc78e7 -r 70f2a3dafc1c mrjunejune/test/latex_editor_test.js --- 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(); diff -r 4f2b50bc78e7 -r 70f2a3dafc1c mrjunejune/test/latex_renderer_test.c --- 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 = diff -r 4f2b50bc78e7 -r 70f2a3dafc1c third_party/tectonic/BUILD --- /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"], +) diff -r 4f2b50bc78e7 -r 70f2a3dafc1c third_party/tectonic/LICENSE --- /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. diff -r 4f2b50bc78e7 -r 70f2a3dafc1c third_party/tectonic/README.md --- /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. diff -r 4f2b50bc78e7 -r 70f2a3dafc1c third_party/tectonic/THIRD_PARTY_NOTICES.md --- /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. diff -r 4f2b50bc78e7 -r 70f2a3dafc1c third_party/tectonic/fonts.conf --- /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 @@ + + + + /nonexistent + fontconfig + diff -r 4f2b50bc78e7 -r 70f2a3dafc1c third_party/tectonic/runtime-cache.tar.gz Binary file third_party/tectonic/runtime-cache.tar.gz has changed diff -r 4f2b50bc78e7 -r 70f2a3dafc1c third_party/tectonic/runtime.bzl --- /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), + }, +)