Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/command/render/latexmk/texlive.ts
3587 views
1
/*
2
* texlive.ts
3
*
4
* Copyright (C) 2020-2022 Posit Software, PBC
5
*/
6
import * as ld from "../../../core/lodash.ts";
7
8
import { execProcess } from "../../../core/process.ts";
9
import { lines } from "../../../core/text.ts";
10
import { requireQuoting, safeWindowsExec } from "../../../core/windows.ts";
11
import { hasTinyTex, tinyTexBinDir } from "../../../tools/impl/tinytex-info.ts";
12
import { join } from "../../../deno_ral/path.ts";
13
import { logProgress } from "../../../core/log.ts";
14
import { isWindows } from "../../../deno_ral/platform.ts";
15
16
export interface TexLiveContext {
17
preferTinyTex: boolean;
18
hasTinyTex: boolean;
19
hasTexLive: boolean;
20
usingGlobal: boolean;
21
binDir?: string;
22
}
23
24
export async function texLiveContext(
25
preferTinyTex: boolean,
26
): Promise<TexLiveContext> {
27
const hasTiny = hasTinyTex();
28
const hasTex = await hasTexLive();
29
const binDir = tinyTexBinDir();
30
const usingGlobal = await texLiveInPath() && !hasTiny;
31
return {
32
preferTinyTex,
33
hasTinyTex: hasTiny,
34
hasTexLive: hasTex,
35
usingGlobal,
36
binDir,
37
};
38
}
39
40
function systemTexLiveContext(): TexLiveContext {
41
return {
42
preferTinyTex: false,
43
hasTinyTex: false,
44
hasTexLive: false,
45
usingGlobal: true,
46
};
47
}
48
49
// Determines whether TexLive is installed and callable on this system
50
export async function hasTexLive(): Promise<boolean> {
51
if (hasTinyTex()) {
52
return true;
53
} else {
54
if (await texLiveInPath()) {
55
return true;
56
} else {
57
return false;
58
}
59
}
60
}
61
62
export async function texLiveInPath(): Promise<boolean> {
63
try {
64
const systemContext = systemTexLiveContext();
65
const result = await tlmgrCommand("--version", [], systemContext);
66
return result.code === 0;
67
} catch {
68
return false;
69
}
70
}
71
72
// Searches TexLive remote for packages that match a given search term.
73
// searchTerms are interpreted as a (Perl) regular expression
74
export async function findPackages(
75
searchTerms: string[],
76
context: TexLiveContext,
77
opts?: string[],
78
quiet?: boolean,
79
): Promise<string[]> {
80
const results: string[] = [];
81
const args = ["--file", "--global"];
82
83
for (const searchTerm of searchTerms) {
84
if (!quiet) {
85
logProgress(
86
`finding package for ${searchTerm}`,
87
);
88
}
89
// Special case for a known package
90
// https://github.com/rstudio/tinytex/blob/33cbe601ff671fae47c594250de1d22bbf293b27/R/latex.R#L470
91
if (searchTerm === "fandol") {
92
results.push("fandol");
93
} else {
94
const result = await tlmgrCommand(
95
"search",
96
[...args, ...(opts || []), searchTerm],
97
context,
98
true,
99
);
100
101
if (result.code === 0 && result.stdout) {
102
const text = result.stdout;
103
104
// Regexes for reading packages and search matches
105
const packageNameRegex = /^(.+)\:$/;
106
const searchTermRegex = new RegExp(`\/${searchTerm}$`);
107
108
// Inspect each line- if it is a package name, collect it and begin
109
// looking at each line to see if they end with the search term
110
// When we find a line matching the search term, put the package name
111
// into the results and continue
112
let currentPackage: string | undefined = undefined;
113
lines(text).forEach((line) => {
114
const packageMatch = line.match(packageNameRegex);
115
if (packageMatch) {
116
const packageName = packageMatch[1];
117
// If the packagename contains a dot, the prefix is the package name
118
// the portion after the dot is the architecture
119
if (packageName.includes(".")) {
120
currentPackage = packageName.split(".")[0];
121
} else {
122
currentPackage = packageName;
123
}
124
} else {
125
// We are in the context of a package, look at the line and
126
// if it ends with /<searchterm>, this package is a good match
127
if (currentPackage) {
128
const searchTermMatch = line.match(searchTermRegex);
129
if (searchTermMatch) {
130
results.push(currentPackage);
131
currentPackage = undefined;
132
}
133
}
134
}
135
});
136
} else {
137
const errorMessage = tlMgrError(result.stderr);
138
if (errorMessage) {
139
throw new Error(errorMessage);
140
}
141
}
142
}
143
}
144
return ld.uniq(results);
145
}
146
147
// Update TexLive.
148
// all = update installed packages
149
// self = update TexLive (tlmgr) itself
150
export function updatePackages(
151
all: boolean,
152
self: boolean,
153
context: TexLiveContext,
154
opts?: string[],
155
quiet?: boolean,
156
) {
157
const args = [];
158
// Add any tlmg args
159
if (opts) {
160
args.push(...opts);
161
}
162
163
if (all) {
164
args.push("--all");
165
}
166
167
if (self) {
168
args.push("--self");
169
}
170
171
return tlmgrCommand("update", args || [], context, quiet);
172
}
173
174
// Install packages using TexLive
175
export async function installPackages(
176
pkgs: string[],
177
context: TexLiveContext,
178
opts?: string[],
179
quiet?: boolean,
180
) {
181
if (!quiet) {
182
logProgress(
183
`> ${pkgs.length} ${
184
pkgs.length === 1 ? "package" : "packages"
185
} to install`,
186
);
187
}
188
let count = 1;
189
for (const pkg of pkgs) {
190
if (!quiet) {
191
logProgress(
192
`> installing ${pkg} (${count} of ${pkgs.length})`,
193
);
194
}
195
196
await installPackage(pkg, context, opts, quiet);
197
count = count + 1;
198
}
199
if (context.usingGlobal) {
200
await addPath(context);
201
}
202
}
203
204
// Add Symlinks for TexLive executables
205
function addPath(context: TexLiveContext, opts?: string[]) {
206
// Add symlinks for executables, man pages,
207
// and info pages in the system directories
208
//
209
// This is only required for binary files installed with tlmgr
210
// but will not hurt each time a package is installed
211
return tlmgrCommand("path", ["add", ...(opts || [])], context, true);
212
}
213
214
// Remove Symlinks for TexLive executables and commands
215
export function removePath(
216
context: TexLiveContext,
217
opts?: string[],
218
quiet?: boolean,
219
) {
220
return tlmgrCommand("path", ["remove", ...(opts || [])], context, quiet);
221
}
222
223
async function installPackage(
224
pkg: string,
225
context: TexLiveContext,
226
opts?: string[],
227
quiet?: boolean,
228
) {
229
// if any packages have been installed already, update packages first
230
let isInstalled = await verifyPackageInstalled(pkg, context);
231
if (isInstalled) {
232
// update tlmgr itself
233
const updateResult = await updatePackages(
234
true,
235
true,
236
context,
237
opts,
238
quiet,
239
);
240
if (updateResult.code !== 0) {
241
return Promise.reject("Problem running `tlgmr update`.");
242
}
243
244
// Rebuild format tree
245
const fmtutilResult = await fmtutilCommand(context);
246
if (fmtutilResult.code !== 0) {
247
return Promise.reject(
248
"Problem running `fmtutil-sys --all` to rebuild format tree.",
249
);
250
}
251
}
252
253
// Run the install command
254
let installResult = await tlmgrCommand(
255
"install",
256
[...(opts || []), pkg],
257
context,
258
quiet,
259
);
260
261
// Failed to even run tlmgr
262
if (installResult.code !== 0 && installResult.code !== 255) {
263
return Promise.reject(
264
`tlmgr returned a non zero status code\n${installResult.stderr}`,
265
);
266
}
267
268
// Check whether we should update again and retry the install
269
isInstalled = await verifyPackageInstalled(pkg, context);
270
if (!isInstalled) {
271
// update tlmgr itself
272
const updateResult = await updatePackages(
273
false,
274
true,
275
context,
276
opts,
277
quiet,
278
);
279
if (updateResult.code !== 0) {
280
return Promise.reject("Problem running `tlgmr update`.");
281
}
282
283
// Rebuild format tree
284
const fmtutilResult = await fmtutilCommand(context);
285
if (fmtutilResult.code !== 0) {
286
return Promise.reject(
287
"Problem running `fmtutil-sys --all` to rebuild format tree.",
288
);
289
}
290
291
// Rerun the install command
292
installResult = await tlmgrCommand(
293
"install",
294
[...(opts || []), pkg],
295
context,
296
quiet,
297
);
298
}
299
300
return installResult;
301
}
302
303
export async function removePackage(
304
pkg: string,
305
context: TexLiveContext,
306
opts?: string[],
307
quiet?: boolean,
308
) {
309
// Run the install command
310
const result = await tlmgrCommand(
311
"remove",
312
[...(opts || []), pkg],
313
context,
314
quiet,
315
);
316
317
// Failed to even run tlmgr
318
if (!result.success) {
319
return Promise.reject();
320
}
321
return result;
322
}
323
324
// Removes texlive itself
325
export async function removeAll(
326
context: TexLiveContext,
327
opts?: string[],
328
quiet?: boolean,
329
) {
330
// remove symlinks
331
const result = await tlmgrCommand(
332
"remove",
333
[...(opts || []), "--all", "--force"],
334
context,
335
quiet,
336
);
337
// Failed to even run tlmgr
338
if (!result.success) {
339
return Promise.reject();
340
}
341
return result;
342
}
343
344
export async function tlVersion(context: TexLiveContext) {
345
try {
346
const result = await tlmgrCommand(
347
"--version",
348
["--machine-readable"],
349
context,
350
true,
351
);
352
353
if (result.success) {
354
const versionStr = result.stdout;
355
const match = versionStr && versionStr.match(/tlversion (\d*)/);
356
if (match) {
357
return match[1];
358
} else {
359
return undefined;
360
}
361
} else {
362
return undefined;
363
}
364
} catch {
365
return undefined;
366
}
367
}
368
369
export type TexLiveCmd = {
370
cmd: string;
371
fullPath: string;
372
};
373
374
export function texLiveCmd(cmd: string, context: TexLiveContext): TexLiveCmd {
375
if (context.preferTinyTex && context.hasTinyTex) {
376
if (context.binDir) {
377
return {
378
cmd,
379
fullPath: join(context.binDir, cmd),
380
};
381
} else {
382
return { cmd, fullPath: cmd };
383
}
384
} else {
385
return { cmd, fullPath: cmd };
386
}
387
}
388
389
function tlMgrError(msg?: string) {
390
if (msg && msg.indexOf("is older than remote repository") > -1) {
391
const 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:`;
393
return `${message} ${msg.replace("\ntlmgr: ", "")}`;
394
} else {
395
return undefined;
396
}
397
}
398
399
// Verifies whether the package has been installed
400
async function verifyPackageInstalled(
401
pkg: string,
402
context: TexLiveContext,
403
opts?: string[],
404
): Promise<boolean> {
405
const result = await tlmgrCommand(
406
"info",
407
[
408
"--list",
409
"--only-installed",
410
"--data",
411
"name",
412
...(opts || []),
413
pkg,
414
],
415
context,
416
);
417
return result.stdout?.trim() === pkg;
418
}
419
420
// Execute correctly tlmgr <cmd> <args>
421
function tlmgrCommand(
422
tlmgrCmd: string,
423
args: string[],
424
context: TexLiveContext,
425
_quiet?: boolean,
426
) {
427
const execTlmgr = (tlmgrCmd: string[]) => {
428
return execProcess(
429
{
430
cmd: tlmgrCmd[0],
431
args: tlmgrCmd.slice(1),
432
stdout: "piped",
433
stderr: "piped",
434
},
435
);
436
};
437
438
// If TinyTex is here, prefer that
439
const tlmgr = texLiveCmd("tlmgr", context);
440
441
// On windows, we always want to call tlmgr through the 'safe'
442
// cmd /c approach since it is a bat file
443
if (isWindows) {
444
const quoted = requireQuoting(args);
445
return safeWindowsExec(
446
tlmgr.fullPath,
447
[tlmgrCmd, ...quoted.args],
448
execTlmgr,
449
);
450
} else {
451
return execTlmgr([tlmgr.fullPath, tlmgrCmd, ...args]);
452
}
453
}
454
455
// Execute fmtutil
456
// https://tug.org/texlive/doc/fmtutil.html
457
function fmtutilCommand(context: TexLiveContext) {
458
const fmtutil = texLiveCmd("fmtutil-sys", context);
459
return execProcess(
460
{
461
cmd: fmtutil.fullPath,
462
args: ["--all"],
463
stdout: "piped",
464
stderr: "piped",
465
},
466
);
467
}
468
469