Path: blob/main/src/command/render/latexmk/latex.ts
3587 views
/*1* latex.ts2*3* Copyright (C) 2020-2022 Posit Software, PBC4*/56import { basename, join } from "../../../deno_ral/path.ts";7import { existsSync, safeRemoveSync } from "../../../deno_ral/fs.ts";8import { error, info } from "../../../deno_ral/log.ts";910import { PdfEngine } from "../../../config/types.ts";1112import { dirAndStem } from "../../../core/path.ts";13import { execProcess, ExecProcessOptions } from "../../../core/process.ts";14import { ProcessResult } from "../../../core/process-types.ts";1516import { PackageManager } from "./pkgmgr.ts";17import { kLatexBodyMessageOptions } from "./types.ts";18import { hasTexLive, texLiveCmd, TexLiveContext } from "./texlive.ts";19import { withPath } from "../../../core/env.ts";20import { logProgress } from "../../../core/log.ts";2122export interface LatexCommandReponse {23log: string;24result: ProcessResult;25output?: string;26}2728export async function hasLatexDistribution() {29try {30const result = await execProcess({31cmd: "pdftex",32args: ["--version"],33stdout: "piped",34stderr: "piped",35});36return result.code === 0;37} catch {38return false;39}40}4142const kLatexMkEngineFlags = [43"-pdf",44"-pdfdvi",45"-pdfps",46"-pdflua",47"-pdfxe",48"-pdf-",49];5051// Runs the Pdf engine52export async function runPdfEngine(53input: string,54engine: PdfEngine,55texLive: TexLiveContext,56outputDir?: string,57texInputDirs?: string[],58pkgMgr?: PackageManager,59quiet?: boolean,60): Promise<LatexCommandReponse> {61// Input and log paths62const [cwd, stem] = dirAndStem(input);63const targetDir = outputDir ? join(cwd, outputDir) : cwd;64const output = join(targetDir, `${stem}.pdf`);65const log = join(targetDir, `${stem}.log`);6667// Clean any log file or output from previous runs68[log, output].forEach((file) => {69if (existsSync(file)) {70safeRemoveSync(file);71}72});7374// build pdf engine command line75// ensure that we provide latexmk with its require custom options76// Note that users may control the latexmk engine options, but77// if not specified, we should provide a default78const computeEngineArgs = () => {79if (engine.pdfEngine === "latexmk") {80const engineArgs = ["-interaction=batchmode", "-halt-on-error"];81if (82!engine.pdfEngineOpts || engine.pdfEngineOpts.find((opt) => {83return kLatexMkEngineFlags.includes(opt);84}) === undefined85) {86engineArgs.push("-pdf");87}88engineArgs.push("-quiet");89return engineArgs;90} else {91return ["-interaction=batchmode", "-halt-on-error"];92}93};94const args = computeEngineArgs();9596// output directory97if (outputDir !== undefined) {98args.push(`-output-directory=${outputDir}`);99}100101// pdf engine opts102if (engine.pdfEngineOpts) {103args.push(...engine.pdfEngineOpts);104}105106// input file107args.push(basename(input));108109// Run the command110const result = await runLatexCommand(111engine.pdfEngine,112args,113{114pkgMgr,115cwd,116texInputDirs,117texLive,118},119quiet,120);121122// Success, return result123return {124result,125output,126log,127};128}129130// Run the index generation engine (currently hard coded to makeindex)131export async function runIndexEngine(132input: string,133texLive: TexLiveContext,134engine?: string,135args?: string[],136pkgMgr?: PackageManager,137quiet?: boolean,138) {139const [cwd, stem] = dirAndStem(input);140const log = join(cwd, `${stem}.ilg`);141142// Clean any log file from previous runs143if (existsSync(log)) {144safeRemoveSync(log);145}146147const result = await runLatexCommand(148engine || "makeindex",149[...(args || []), basename(input)],150{151cwd,152pkgMgr,153texLive,154},155quiet,156);157158return {159result,160log,161};162}163164// Runs the bibengine to process citations165export async function runBibEngine(166engine: string,167input: string,168cwd: string,169texLive: TexLiveContext,170pkgMgr?: PackageManager,171texInputDirs?: string[],172quiet?: boolean,173): Promise<LatexCommandReponse> {174const [dir, stem] = dirAndStem(input);175const log = join(dir, `${stem}.blg`);176177// Clean any log file from previous runs178if (existsSync(log)) {179safeRemoveSync(log);180}181182const result = await runLatexCommand(183engine,184[input],185{186pkgMgr,187cwd,188texInputDirs,189texLive,190},191quiet,192);193return {194result,195log,196};197}198199export interface LatexCommandContext {200pkgMgr?: PackageManager;201cwd?: string;202texInputDirs?: string[];203texLive: TexLiveContext;204}205206async function runLatexCommand(207latexCmd: string,208args: string[],209context: LatexCommandContext,210quiet?: boolean,211): Promise<ProcessResult> {212const fullLatexCmd = texLiveCmd(latexCmd, context.texLive);213214const runOptions: ExecProcessOptions = {215cmd: fullLatexCmd.fullPath,216args,217stdout: "piped",218stderr: "piped",219};220221//Ensure that the bin directory is available as a part of PDF compilation222if (context.texLive.binDir) {223runOptions.env = runOptions.env || {};224runOptions.env["PATH"] = withPath({ prepend: [context.texLive.binDir] });225}226227// Set the working directory228if (context.cwd) {229runOptions.cwd = context.cwd;230}231232// Add a tex search path233// The // means that TeX programs will search recursively in that folder;234// the trailing colon means "append the standard value of TEXINPUTS" (which you don't need to provide).235if (context.texInputDirs && context.texInputDirs.length > 0) {236// note this //237runOptions.env = runOptions.env || {};238runOptions.env["TEXINPUTS"] = `${context.texInputDirs.join(";")};`;239runOptions.env["BSTINPUTS"] = `${context.texInputDirs.join(";")};`;240}241242// Run the command243const runCmd = async () => {244const result = await execProcess(runOptions, undefined, "stdout>stderr");245if (!quiet && result.stderr) {246info(result.stderr, kLatexBodyMessageOptions);247}248return result;249};250251try {252// Try running the command253return await runCmd();254} catch (_e) {255// First confirm that there is a TeX installation available256const tex = await hasTexLive() || await hasLatexDistribution();257if (!tex) {258info(259"\nNo TeX installation was detected.\n\nPlease run 'quarto install tinytex' to install TinyTex.\nIf you prefer, you may install TexLive or another TeX distribution.\n",260);261return Promise.reject();262} else if (context.pkgMgr && context.pkgMgr.autoInstall) {263// If the command itself can't be found, try installing the command264// if auto installation is enabled265if (!quiet) {266logProgress(267`command ${latexCmd} not found, attempting install`,268);269}270271// Search for a package for this command272const packageForCommand = await context.pkgMgr.searchPackages([latexCmd]);273if (packageForCommand) {274// try to install it275await context.pkgMgr.installPackages(packagesForCommand(latexCmd));276}277// Try running the command again278return await runCmd();279} else {280// Some other error has occurred281error(282`Error executing ${latexCmd}`,283);284285return Promise.reject();286}287}288}289290// Convert any commands to their291function packagesForCommand(cmd: string): string[] {292if (cmd === "texindy") {293return ["xindy"];294} else {295return [cmd];296}297}298299300