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