Path: blob/main/src/command/render/latexmk/texlive.ts
6447 views
/*1* texlive.ts2*3* Copyright (C) 2020-2022 Posit Software, PBC4*/5import * as ld from "../../../core/lodash.ts";67import { execProcess } from "../../../core/process.ts";8import { lines } from "../../../core/text.ts";9import { requireQuoting, safeWindowsExec } from "../../../core/windows.ts";10import { hasTinyTex, tinyTexBinDir } from "../../../tools/impl/tinytex-info.ts";11import { join } from "../../../deno_ral/path.ts";12import { logProgress } from "../../../core/log.ts";13import { isWindows } from "../../../deno_ral/platform.ts";1415export interface TexLiveContext {16preferTinyTex: boolean;17hasTinyTex: boolean;18hasTexLive: boolean;19usingGlobal: boolean;20binDir?: string;21}2223export async function texLiveContext(24preferTinyTex: boolean,25): Promise<TexLiveContext> {26const hasTiny = hasTinyTex();27const hasTex = await hasTexLive();28const binDir = tinyTexBinDir();29const usingGlobal = await texLiveInPath() && !hasTiny;30return {31preferTinyTex,32hasTinyTex: hasTiny,33hasTexLive: hasTex,34usingGlobal,35binDir,36};37}3839function systemTexLiveContext(): TexLiveContext {40return {41preferTinyTex: false,42hasTinyTex: false,43hasTexLive: false,44usingGlobal: true,45};46}4748// Determines whether TexLive is installed and callable on this system49export async function hasTexLive(): Promise<boolean> {50if (hasTinyTex()) {51return true;52} else {53if (await texLiveInPath()) {54return true;55} else {56return false;57}58}59}6061export async function texLiveInPath(): Promise<boolean> {62try {63const systemContext = systemTexLiveContext();64const result = await tlmgrCommand("--version", [], systemContext);65return result.code === 0;66} catch {67return false;68}69}7071// Searches TexLive remote for packages that match a given search term.72// searchTerms are interpreted as a (Perl) regular expression73export async function findPackages(74searchTerms: string[],75context: TexLiveContext,76opts?: string[],77quiet?: boolean,78): Promise<string[]> {79const results: string[] = [];80const args = ["--file", "--global"];8182for (const searchTerm of searchTerms) {83if (!quiet) {84logProgress(85`finding package for ${searchTerm}`,86);87}88// Special cases for known packages where tlmgr file search doesn't work89// https://github.com/rstudio/tinytex/blob/33cbe601ff671fae47c594250de1d22bbf293b27/R/latex.R#L47090const knownPackages = ["fandol", "latex-lab", "colorprofiles"];91if (knownPackages.includes(searchTerm)) {92results.push(searchTerm);93} else {94const result = await tlmgrCommand(95"search",96[...args, ...(opts || []), searchTerm],97context,98true,99);100101if (result.code === 0 && result.stdout) {102const text = result.stdout;103104// Regexes for reading packages and search matches105const packageNameRegex = /^(.+)\:$/;106const searchTermRegex = new RegExp(`\/${searchTerm}$`);107108// Inspect each line- if it is a package name, collect it and begin109// looking at each line to see if they end with the search term110// When we find a line matching the search term, put the package name111// into the results and continue112let currentPackage: string | undefined = undefined;113lines(text).forEach((line) => {114const packageMatch = line.match(packageNameRegex);115if (packageMatch) {116const packageName = packageMatch[1];117// If the packagename contains a dot, the prefix is the package name118// the portion after the dot is the architecture119if (packageName.includes(".")) {120currentPackage = packageName.split(".")[0];121} else {122currentPackage = packageName;123}124} else {125// We are in the context of a package, look at the line and126// if it ends with /<searchterm>, this package is a good match127if (currentPackage) {128const searchTermMatch = line.match(searchTermRegex);129if (searchTermMatch) {130results.push(currentPackage);131currentPackage = undefined;132}133}134}135});136} else {137const errorMessage = tlMgrError(result.stderr);138if (errorMessage) {139throw new Error(errorMessage);140}141}142}143}144return ld.uniq(results);145}146147// Update TexLive.148// all = update installed packages149// self = update TexLive (tlmgr) itself150export function updatePackages(151all: boolean,152self: boolean,153context: TexLiveContext,154opts?: string[],155quiet?: boolean,156) {157const args = [];158// Add any tlmg args159if (opts) {160args.push(...opts);161}162163if (all) {164args.push("--all");165}166167if (self) {168args.push("--self");169}170171return tlmgrCommand("update", args || [], context, quiet);172}173174// Install packages using TexLive175export async function installPackages(176pkgs: string[],177context: TexLiveContext,178opts?: string[],179quiet?: boolean,180) {181if (!quiet) {182logProgress(183`> ${pkgs.length} ${184pkgs.length === 1 ? "package" : "packages"185} to install`,186);187}188let count = 1;189for (const pkg of pkgs) {190if (!quiet) {191logProgress(192`> installing ${pkg} (${count} of ${pkgs.length})`,193);194}195196await installPackage(pkg, context, opts, quiet);197count = count + 1;198}199if (context.usingGlobal) {200await addPath(context);201}202}203204// Add Symlinks for TexLive executables205function addPath(context: TexLiveContext, opts?: string[]) {206// Add symlinks for executables, man pages,207// and info pages in the system directories208//209// This is only required for binary files installed with tlmgr210// but will not hurt each time a package is installed211return tlmgrCommand("path", ["add", ...(opts || [])], context, true);212}213214// Remove Symlinks for TexLive executables and commands215export function removePath(216context: TexLiveContext,217opts?: string[],218quiet?: boolean,219) {220return tlmgrCommand("path", ["remove", ...(opts || [])], context, quiet);221}222223async function installPackage(224pkg: string,225context: TexLiveContext,226opts?: string[],227quiet?: boolean,228) {229// if any packages have been installed already, update packages first230let isInstalled = await verifyPackageInstalled(pkg, context);231if (isInstalled) {232// update tlmgr itself233const updateResult = await updatePackages(234true,235true,236context,237opts,238quiet,239);240if (updateResult.code !== 0) {241return Promise.reject("Problem running `tlmgr update`.");242}243244// Rebuild format tree245const fmtutilResult = await fmtutilCommand(context);246if (fmtutilResult.code !== 0) {247return Promise.reject(248"Problem running `fmtutil-sys --all` to rebuild format tree.",249);250}251}252253// Run the install command254let installResult = await tlmgrCommand(255"install",256[...(opts || []), pkg],257context,258quiet,259);260261// Failed to even run tlmgr262if (installResult.code !== 0 && installResult.code !== 255) {263return Promise.reject(264`tlmgr returned a non zero status code\n${installResult.stderr}`,265);266}267268// Check whether we should update again and retry the install269isInstalled = await verifyPackageInstalled(pkg, context);270if (!isInstalled) {271// update tlmgr itself272const updateResult = await updatePackages(273false,274true,275context,276opts,277quiet,278);279if (updateResult.code !== 0) {280return Promise.reject("Problem running `tlmgr update`.");281}282283// Rebuild format tree284const fmtutilResult = await fmtutilCommand(context);285if (fmtutilResult.code !== 0) {286return Promise.reject(287"Problem running `fmtutil-sys --all` to rebuild format tree.",288);289}290291// Rerun the install command292installResult = await tlmgrCommand(293"install",294[...(opts || []), pkg],295context,296quiet,297);298}299300return installResult;301}302303export async function removePackage(304pkg: string,305context: TexLiveContext,306opts?: string[],307quiet?: boolean,308) {309// Run the install command310const result = await tlmgrCommand(311"remove",312[...(opts || []), pkg],313context,314quiet,315);316317// Failed to even run tlmgr318if (!result.success) {319return Promise.reject();320}321return result;322}323324// Removes texlive itself325export async function removeAll(326context: TexLiveContext,327opts?: string[],328quiet?: boolean,329) {330// remove symlinks331const result = await tlmgrCommand(332"remove",333[...(opts || []), "--all", "--force"],334context,335quiet,336);337// Failed to even run tlmgr338if (!result.success) {339return Promise.reject();340}341return result;342}343344export async function tlVersion(context: TexLiveContext) {345try {346const result = await tlmgrCommand(347"--version",348["--machine-readable"],349context,350true,351);352353if (result.success) {354const versionStr = result.stdout;355const match = versionStr && versionStr.match(/tlversion (\d*)/);356if (match) {357return match[1];358} else {359return undefined;360}361} else {362return undefined;363}364} catch {365return undefined;366}367}368369export type TexLiveCmd = {370cmd: string;371fullPath: string;372};373374export function texLiveCmd(cmd: string, context: TexLiveContext): TexLiveCmd {375if (context.preferTinyTex && context.hasTinyTex) {376if (context.binDir) {377return {378cmd,379fullPath: join(context.binDir, cmd),380};381} else {382return { cmd, fullPath: cmd };383}384} else {385return { cmd, fullPath: cmd };386}387}388389function tlMgrError(msg?: string) {390if (msg && msg.indexOf("is older than remote repository") > -1) {391const message =392`Your TexLive version is not updated enough to connect to the remote repository and download packages. Please update your installation of TexLive or TinyTex.\n\nUnderlying message:`;393return `${message} ${msg.replace("\ntlmgr: ", "")}`;394} else {395return undefined;396}397}398399// Verifies whether the package has been installed400async function verifyPackageInstalled(401pkg: string,402context: TexLiveContext,403opts?: string[],404): Promise<boolean> {405const result = await tlmgrCommand(406"info",407[408"--list",409"--only-installed",410"--data",411"name",412...(opts || []),413pkg,414],415context,416);417return result.stdout?.trim() === pkg;418}419420// Execute correctly tlmgr <cmd> <args>421function tlmgrCommand(422tlmgrCmd: string,423args: string[],424context: TexLiveContext,425_quiet?: boolean,426) {427const execTlmgr = (tlmgrCmd: string[]) => {428return execProcess(429{430cmd: tlmgrCmd[0],431args: tlmgrCmd.slice(1),432stdout: "piped",433stderr: "piped",434},435);436};437438// If TinyTex is here, prefer that439const tlmgr = texLiveCmd("tlmgr", context);440441// On windows, we always want to call tlmgr through the 'safe'442// cmd /c approach since it is a bat file443if (isWindows) {444const quoted = requireQuoting(args);445return safeWindowsExec(446tlmgr.fullPath,447[tlmgrCmd, ...quoted.args],448execTlmgr,449);450} else {451return execTlmgr([tlmgr.fullPath, tlmgrCmd, ...args]);452}453}454455// Execute fmtutil456// https://tug.org/texlive/doc/fmtutil.html457function fmtutilCommand(context: TexLiveContext) {458const fmtutil = texLiveCmd("fmtutil-sys", context);459return execProcess(460{461cmd: fmtutil.fullPath,462args: ["--all"],463stdout: "piped",464stderr: "piped",465},466);467}468469470