Path: blob/main/src/command/render/latexmk/texlive.ts
3587 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 case for a known package89// https://github.com/rstudio/tinytex/blob/33cbe601ff671fae47c594250de1d22bbf293b27/R/latex.R#L47090if (searchTerm === "fandol") {91results.push("fandol");92} else {93const result = await tlmgrCommand(94"search",95[...args, ...(opts || []), searchTerm],96context,97true,98);99100if (result.code === 0 && result.stdout) {101const text = result.stdout;102103// Regexes for reading packages and search matches104const packageNameRegex = /^(.+)\:$/;105const searchTermRegex = new RegExp(`\/${searchTerm}$`);106107// Inspect each line- if it is a package name, collect it and begin108// looking at each line to see if they end with the search term109// When we find a line matching the search term, put the package name110// into the results and continue111let currentPackage: string | undefined = undefined;112lines(text).forEach((line) => {113const packageMatch = line.match(packageNameRegex);114if (packageMatch) {115const packageName = packageMatch[1];116// If the packagename contains a dot, the prefix is the package name117// the portion after the dot is the architecture118if (packageName.includes(".")) {119currentPackage = packageName.split(".")[0];120} else {121currentPackage = packageName;122}123} else {124// We are in the context of a package, look at the line and125// if it ends with /<searchterm>, this package is a good match126if (currentPackage) {127const searchTermMatch = line.match(searchTermRegex);128if (searchTermMatch) {129results.push(currentPackage);130currentPackage = undefined;131}132}133}134});135} else {136const errorMessage = tlMgrError(result.stderr);137if (errorMessage) {138throw new Error(errorMessage);139}140}141}142}143return ld.uniq(results);144}145146// Update TexLive.147// all = update installed packages148// self = update TexLive (tlmgr) itself149export function updatePackages(150all: boolean,151self: boolean,152context: TexLiveContext,153opts?: string[],154quiet?: boolean,155) {156const args = [];157// Add any tlmg args158if (opts) {159args.push(...opts);160}161162if (all) {163args.push("--all");164}165166if (self) {167args.push("--self");168}169170return tlmgrCommand("update", args || [], context, quiet);171}172173// Install packages using TexLive174export async function installPackages(175pkgs: string[],176context: TexLiveContext,177opts?: string[],178quiet?: boolean,179) {180if (!quiet) {181logProgress(182`> ${pkgs.length} ${183pkgs.length === 1 ? "package" : "packages"184} to install`,185);186}187let count = 1;188for (const pkg of pkgs) {189if (!quiet) {190logProgress(191`> installing ${pkg} (${count} of ${pkgs.length})`,192);193}194195await installPackage(pkg, context, opts, quiet);196count = count + 1;197}198if (context.usingGlobal) {199await addPath(context);200}201}202203// Add Symlinks for TexLive executables204function addPath(context: TexLiveContext, opts?: string[]) {205// Add symlinks for executables, man pages,206// and info pages in the system directories207//208// This is only required for binary files installed with tlmgr209// but will not hurt each time a package is installed210return tlmgrCommand("path", ["add", ...(opts || [])], context, true);211}212213// Remove Symlinks for TexLive executables and commands214export function removePath(215context: TexLiveContext,216opts?: string[],217quiet?: boolean,218) {219return tlmgrCommand("path", ["remove", ...(opts || [])], context, quiet);220}221222async function installPackage(223pkg: string,224context: TexLiveContext,225opts?: string[],226quiet?: boolean,227) {228// if any packages have been installed already, update packages first229let isInstalled = await verifyPackageInstalled(pkg, context);230if (isInstalled) {231// update tlmgr itself232const updateResult = await updatePackages(233true,234true,235context,236opts,237quiet,238);239if (updateResult.code !== 0) {240return Promise.reject("Problem running `tlgmr update`.");241}242243// Rebuild format tree244const fmtutilResult = await fmtutilCommand(context);245if (fmtutilResult.code !== 0) {246return Promise.reject(247"Problem running `fmtutil-sys --all` to rebuild format tree.",248);249}250}251252// Run the install command253let installResult = await tlmgrCommand(254"install",255[...(opts || []), pkg],256context,257quiet,258);259260// Failed to even run tlmgr261if (installResult.code !== 0 && installResult.code !== 255) {262return Promise.reject(263`tlmgr returned a non zero status code\n${installResult.stderr}`,264);265}266267// Check whether we should update again and retry the install268isInstalled = await verifyPackageInstalled(pkg, context);269if (!isInstalled) {270// update tlmgr itself271const updateResult = await updatePackages(272false,273true,274context,275opts,276quiet,277);278if (updateResult.code !== 0) {279return Promise.reject("Problem running `tlgmr update`.");280}281282// Rebuild format tree283const fmtutilResult = await fmtutilCommand(context);284if (fmtutilResult.code !== 0) {285return Promise.reject(286"Problem running `fmtutil-sys --all` to rebuild format tree.",287);288}289290// Rerun the install command291installResult = await tlmgrCommand(292"install",293[...(opts || []), pkg],294context,295quiet,296);297}298299return installResult;300}301302export async function removePackage(303pkg: string,304context: TexLiveContext,305opts?: string[],306quiet?: boolean,307) {308// Run the install command309const result = await tlmgrCommand(310"remove",311[...(opts || []), pkg],312context,313quiet,314);315316// Failed to even run tlmgr317if (!result.success) {318return Promise.reject();319}320return result;321}322323// Removes texlive itself324export async function removeAll(325context: TexLiveContext,326opts?: string[],327quiet?: boolean,328) {329// remove symlinks330const result = await tlmgrCommand(331"remove",332[...(opts || []), "--all", "--force"],333context,334quiet,335);336// Failed to even run tlmgr337if (!result.success) {338return Promise.reject();339}340return result;341}342343export async function tlVersion(context: TexLiveContext) {344try {345const result = await tlmgrCommand(346"--version",347["--machine-readable"],348context,349true,350);351352if (result.success) {353const versionStr = result.stdout;354const match = versionStr && versionStr.match(/tlversion (\d*)/);355if (match) {356return match[1];357} else {358return undefined;359}360} else {361return undefined;362}363} catch {364return undefined;365}366}367368export type TexLiveCmd = {369cmd: string;370fullPath: string;371};372373export function texLiveCmd(cmd: string, context: TexLiveContext): TexLiveCmd {374if (context.preferTinyTex && context.hasTinyTex) {375if (context.binDir) {376return {377cmd,378fullPath: join(context.binDir, cmd),379};380} else {381return { cmd, fullPath: cmd };382}383} else {384return { cmd, fullPath: cmd };385}386}387388function tlMgrError(msg?: string) {389if (msg && msg.indexOf("is older than remote repository") > -1) {390const message =391`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:`;392return `${message} ${msg.replace("\ntlmgr: ", "")}`;393} else {394return undefined;395}396}397398// Verifies whether the package has been installed399async function verifyPackageInstalled(400pkg: string,401context: TexLiveContext,402opts?: string[],403): Promise<boolean> {404const result = await tlmgrCommand(405"info",406[407"--list",408"--only-installed",409"--data",410"name",411...(opts || []),412pkg,413],414context,415);416return result.stdout?.trim() === pkg;417}418419// Execute correctly tlmgr <cmd> <args>420function tlmgrCommand(421tlmgrCmd: string,422args: string[],423context: TexLiveContext,424_quiet?: boolean,425) {426const execTlmgr = (tlmgrCmd: string[]) => {427return execProcess(428{429cmd: tlmgrCmd[0],430args: tlmgrCmd.slice(1),431stdout: "piped",432stderr: "piped",433},434);435};436437// If TinyTex is here, prefer that438const tlmgr = texLiveCmd("tlmgr", context);439440// On windows, we always want to call tlmgr through the 'safe'441// cmd /c approach since it is a bat file442if (isWindows) {443const quoted = requireQuoting(args);444return safeWindowsExec(445tlmgr.fullPath,446[tlmgrCmd, ...quoted.args],447execTlmgr,448);449} else {450return execTlmgr([tlmgr.fullPath, tlmgrCmd, ...args]);451}452}453454// Execute fmtutil455// https://tug.org/texlive/doc/fmtutil.html456function fmtutilCommand(context: TexLiveContext) {457const fmtutil = texLiveCmd("fmtutil-sys", context);458return execProcess(459{460cmd: fmtutil.fullPath,461args: ["--all"],462stdout: "piped",463stderr: "piped",464},465);466}467468469