diff --git a/server-ce/Dockerfile-base b/server-ce/Dockerfile-base index f63cc9a8c8..8fb816d278 100644 --- a/server-ce/Dockerfile-base +++ b/server-ce/Dockerfile-base @@ -4,6 +4,10 @@ FROM phusion/baseimage:noble-1.0.3 +# Makes sure LuaTeX cache is writable +# ----------------------------------- +ENV TEXMFVAR=/var/lib/overleaf/tmp/texmf-var + # Update to ensure dependencies are updated # ------------------------------------------ ENV REBUILT_AFTER="2026-05-21" @@ -65,6 +69,26 @@ RUN mkdir -p /opt/quarto-extensions \ \ && chown -R www-data:www-data /opt/quarto-extensions +# Install TeX Live (for compiling .tex projects with latexmk) +# ----------------------------------------------------------------------- +# Verso compiles .qmd with Quarto and .tex with latexmk; both engines live +# side by side. We install (almost) all of texlive-full, excluding the -doc +# and -lang- packages to keep the download from being needlessly huge while +# still providing a complete LaTeX toolchain (latexmk, xetex, lualatex, +# biber, texcount, chktex, synctex, etc.). +# ----------------------------------------------------------------------- +RUN apt-get update \ +&& apt-cache depends texlive-full \ + | grep "Depends: " \ + | grep -v -- "-doc" \ + | grep -v -- "-lang-" \ + | sed 's/Depends: //' \ + | xargs apt-get install -y --no-install-recommends \ +&& apt-get install -y --no-install-recommends \ + texlive-xetex texlive-luatex texlive-bibtex-extra biber \ + latexmk texcount chktex fontconfig inkscape python3-pygments \ +&& rm -rf /var/lib/apt/lists/* + # Set up overleaf user and home directory # ----------------------------------------- diff --git a/services/clsi/app/js/CompileManager.js b/services/clsi/app/js/CompileManager.js index d406bf95ad..4c8799fb1d 100644 --- a/services/clsi/app/js/CompileManager.js +++ b/services/clsi/app/js/CompileManager.js @@ -6,6 +6,7 @@ import logger from '@overleaf/logger' import OError from '@overleaf/o-error' import ResourceWriter from './ResourceWriter.js' import QuartoRunner from './QuartoRunner.js' +import LatexRunner from './LatexRunner.js' import OutputFileFinder from './OutputFileFinder.js' import OutputCacheManager from './OutputCacheManager.js' import ClsiMetrics from './Metrics.js' @@ -39,6 +40,30 @@ const KNOWN_LATEXMK_RULES = new Set([ const LATEX_PASSES_RULES = new Set(['latex', 'lualatex', 'xelatex', 'pdflatex']) +// Quarto handles .qmd/.md/.Rmd sources; everything else (.tex, .ltx, .Rtex, +// .Rnw) is compiled with latexmk via LatexRunner. Dispatch is by the root +// file's extension, so LaTeX and Quarto projects can coexist on one server. +function _isQuartoFile(rootResourcePath) { + return /\.(qmd|md|rmd)$/i.test(rootResourcePath || '') +} + +// Return a runner with a uniform { run, isRunning, kill } interface so the +// rest of CompileManager doesn't need to know which engine is in use. +function _getRunner(rootResourcePath) { + if (_isQuartoFile(rootResourcePath)) { + return { + run: (name, opts) => QuartoRunner.promises.runQuarto(name, opts), + isRunning: name => QuartoRunner.isRunning(name), + kill: name => QuartoRunner.promises.killQuarto(name), + } + } + return { + run: (name, opts) => LatexRunner.promises.runLatex(name, opts), + isRunning: name => LatexRunner.isRunning(name), + kill: name => LatexRunner.promises.killLatex(name), + } +} + function getCompileName(projectId, userId) { if (userId != null) { return `${projectId}-${userId}` @@ -195,8 +220,10 @@ async function doCompile(request, stats, timings) { const compileName = getCompileName(request.project_id, request.user_id) + const runner = _getRunner(request.rootResourcePath) + try { - await QuartoRunner.promises.runQuarto(compileName, { + await runner.run(compileName, { directory: compileDir, mainFile: request.rootResourcePath, compiler: request.compiler, @@ -321,16 +348,21 @@ async function _saveOutputFiles({ async function stopCompile(projectId, userId) { const compileName = getCompileName(projectId, userId) + // stopCompile has no root path, so check both runners — only one can be + // active for a given compileName at a time. + const isRunning = + QuartoRunner.isRunning(compileName) || LatexRunner.isRunning(compileName) const lock = LockManager.getExistingLock(getCompileDir(projectId, userId)) let lockReleased if (lock) { lockReleased = lock.waitForRelease() } else { - if (!QuartoRunner.isRunning(compileName)) return + if (!isRunning) return logger.warn({ projectId, userId }, 'found running compile without lock') lockReleased = Promise.resolve() } await QuartoRunner.promises.killQuarto(compileName) + await LatexRunner.promises.killLatex(compileName) await lockReleased } diff --git a/services/web/frontend/js/features/settings/components/compiler-settings/compiler-setting.tsx b/services/web/frontend/js/features/settings/components/compiler-settings/compiler-setting.tsx index f0e7a38e0b..e84e1cc799 100644 --- a/services/web/frontend/js/features/settings/components/compiler-settings/compiler-setting.tsx +++ b/services/web/frontend/js/features/settings/components/compiler-settings/compiler-setting.tsx @@ -7,7 +7,16 @@ import { usePermissionsContext } from '@/features/ide-react/context/permissions- import { ProjectCompiler } from '@ol-types/project-settings' import { useSetCompilationSettingWithEvent } from '@/features/editor-left-menu/hooks/use-set-compilation-setting' function getCompilerOptions(): Option[] { - return [{ value: 'quarto', label: 'Quarto' }] + // The actual compiler engine is chosen in CLSI by the root file's + // extension (.qmd → Quarto, .tex → latexmk). This dropdown still lets + // LaTeX users pick which TeX engine latexmk should use. + return [ + { value: 'quarto', label: 'Quarto' }, + { value: 'pdflatex', label: 'pdfLaTeX' }, + { value: 'latex', label: 'LaTeX' }, + { value: 'xelatex', label: 'XeLaTeX' }, + { value: 'lualatex', label: 'LuaLaTeX' }, + ] } export default function CompilerSetting() {