Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/command/check/check.ts
6449 views
1
/*
2
* check.ts
3
*
4
* Copyright (C) 2021-2022 Posit Software, PBC
5
*/
6
7
import { info } from "../../deno_ral/log.ts";
8
9
import { render } from "../render/render-shared.ts";
10
import { renderServices } from "../render/render-services.ts";
11
12
import { completeMessage, withSpinner } from "../../core/console.ts";
13
import { quartoConfig } from "../../core/quarto.ts";
14
import {
15
cacheCodePage,
16
clearCodePageCache,
17
readCodePage,
18
} from "../../core/windows.ts";
19
import { RenderServiceWithLifetime } from "../render/types.ts";
20
import { execProcess } from "../../core/process.ts";
21
import { pandocBinaryPath } from "../../core/resources.ts";
22
import { lines } from "../../core/text.ts";
23
import { satisfies } from "semver/mod.ts";
24
import { dartCommand } from "../../core/dart-sass.ts";
25
import { allTools } from "../../tools/tools.ts";
26
import { texLiveContext, tlVersion } from "../render/latexmk/texlive.ts";
27
import { which } from "../../core/path.ts";
28
import { dirname } from "../../deno_ral/path.ts";
29
import { notebookContext } from "../../render/notebook/notebook-context.ts";
30
import { typstBinaryPath } from "../../core/typst.ts";
31
import { quartoCacheDir } from "../../core/appdirs.ts";
32
import { isWindows } from "../../deno_ral/platform.ts";
33
import { makeStringEnumTypeEnforcer } from "../../typing/dynamic.ts";
34
import { findChrome } from "../../core/puppeteer.ts";
35
import { executionEngines } from "../../execute/engine.ts";
36
37
export function getTargets(): readonly string[] {
38
const checkableEngineNames = executionEngines()
39
.filter((engine) => engine.checkInstallation)
40
.map((engine) => engine.name);
41
42
return ["install", "info", ...checkableEngineNames, "versions", "all"];
43
}
44
45
export type Target = string;
46
export function enforceTargetType(value: unknown): Target {
47
const targets = getTargets();
48
return makeStringEnumTypeEnforcer(...targets)(value);
49
}
50
51
const kIndent = " ";
52
53
type CheckJsonResult = Record<string, unknown>;
54
55
export type CheckConfiguration = {
56
strict: boolean;
57
target: Target;
58
output: string | undefined;
59
services: RenderServiceWithLifetime;
60
jsonResult: CheckJsonResult | undefined;
61
};
62
63
function checkCompleteMessage(conf: CheckConfiguration, message: string) {
64
if (!conf.jsonResult) {
65
completeMessage(message);
66
}
67
}
68
69
function checkInfoMsg(conf: CheckConfiguration, message: string) {
70
if (!conf.jsonResult) {
71
info(message);
72
}
73
}
74
75
export async function check(
76
target: Target,
77
strict?: boolean,
78
output?: string,
79
): Promise<void> {
80
const services = renderServices(notebookContext());
81
const conf: CheckConfiguration = {
82
strict: !!strict,
83
target: target,
84
output,
85
services,
86
jsonResult: undefined,
87
};
88
if (conf.output) {
89
conf.jsonResult = {
90
strict,
91
};
92
}
93
try {
94
if (conf.jsonResult) {
95
conf.jsonResult.version = quartoConfig.version();
96
}
97
checkInfoMsg(conf, `Quarto ${quartoConfig.version()}`);
98
99
// Fixed checks (non-engine)
100
for (
101
const [name, checker] of [
102
["info", checkInfo],
103
["versions", checkVersions],
104
["install", checkInstall],
105
] as const
106
) {
107
if (target === name || target === "all") {
108
await checker(conf);
109
}
110
}
111
112
// Dynamic engine checks
113
for (const engine of executionEngines()) {
114
if (
115
engine.checkInstallation && (target === engine.name || target === "all")
116
) {
117
await engine.checkInstallation(conf);
118
}
119
}
120
121
if (conf.jsonResult && conf.output) {
122
await Deno.writeTextFile(
123
conf.output,
124
JSON.stringify(conf.jsonResult, null, 2),
125
);
126
}
127
} finally {
128
services.cleanup();
129
}
130
}
131
132
// Currently this doesn't check anything
133
// but it's a placeholder for future checks
134
// and the message is useful for troubleshooting
135
async function checkInfo(conf: CheckConfiguration) {
136
const cacheDir = quartoCacheDir();
137
if (conf.jsonResult) {
138
conf.jsonResult!.info = { cacheDir };
139
}
140
checkCompleteMessage(conf, "Checking environment information...");
141
checkInfoMsg(conf, kIndent + "Quarto cache location: " + cacheDir);
142
}
143
144
async function checkVersions(conf: CheckConfiguration) {
145
const {
146
strict,
147
} = conf;
148
const checkVersion = (
149
version: string | undefined,
150
constraint: string,
151
name: string,
152
) => {
153
if (typeof version !== "string") {
154
throw new Error(`Unable to determine ${name} version`);
155
}
156
const good = satisfies(version, constraint);
157
if (conf.jsonResult) {
158
if (conf.jsonResult.dependencies === undefined) {
159
conf.jsonResult.dependencies = {};
160
}
161
(conf.jsonResult.dependencies as Record<string, unknown>)[name] = {
162
version,
163
constraint,
164
satisfies: good,
165
};
166
}
167
if (!good) {
168
checkInfoMsg(
169
conf,
170
` NOTE: ${name} version ${version} is too old. Please upgrade to ${
171
constraint.slice(2)
172
} or later.`,
173
);
174
} else {
175
checkInfoMsg(conf, ` ${name} version ${version}: OK`);
176
}
177
};
178
179
const strictCheckVersion = (
180
version: string,
181
constraint: string,
182
name: string,
183
) => {
184
const good = version === constraint;
185
if (conf.jsonResult) {
186
if (conf.jsonResult.dependencies === undefined) {
187
conf.jsonResult.dependencies = {};
188
}
189
(conf.jsonResult.dependencies as Record<string, unknown>)[name] = {
190
version,
191
constraint,
192
satisfies: good,
193
};
194
}
195
if (!good) {
196
checkInfoMsg(
197
conf,
198
` NOTE: ${name} version ${version} does not strictly match ${constraint} and strict checking is enabled. Please use ${constraint}.`,
199
);
200
} else {
201
checkInfoMsg(conf, ` ${name} version ${version}: OK`);
202
}
203
};
204
205
checkCompleteMessage(
206
conf,
207
"Checking versions of quarto binary dependencies...",
208
);
209
210
let pandocVersion = lines(
211
(await execProcess({
212
cmd: pandocBinaryPath(),
213
args: ["--version"],
214
stdout: "piped",
215
})).stdout!,
216
)[0]?.split(" ")[1];
217
const sassVersion = (await dartCommand(["--version"]))?.trim();
218
const denoVersion = Deno.version.deno;
219
const typstVersion = lines(
220
(await execProcess({
221
cmd: typstBinaryPath(),
222
args: ["--version"],
223
stdout: "piped",
224
})).stdout!,
225
)[0].split(" ")[1];
226
227
// We hack around pandocVersion to build a sem-verish string
228
// that satisfies the semver package
229
// if pandoc reports more than three version numbers, pick the first three
230
// if pandoc reports fewer than three version numbers, pad with zeros
231
if (pandocVersion) {
232
const versionParts = pandocVersion.split(".");
233
if (versionParts.length > 3) {
234
pandocVersion = versionParts.slice(0, 3).join(".");
235
} else if (versionParts.length < 3) {
236
pandocVersion = versionParts.concat(
237
Array(3 - versionParts.length).fill("0"),
238
).join(".");
239
}
240
}
241
242
// FIXME: all of these strict checks should be done by
243
// loading the configuration file directly, but that
244
// file is in an awkward format and it is not packaged
245
// with our installers
246
const versionConstraints: [string | undefined, string, string][] = [
247
[pandocVersion, "3.8.3", "Pandoc"],
248
[sassVersion, "1.87.0", "Dart Sass"],
249
[denoVersion, "2.4.5", "Deno"],
250
[typstVersion, "0.14.2", "Typst"],
251
];
252
const checkData: [string | undefined, string, string][] = versionConstraints
253
.map(([version, ver, name]) => [
254
version,
255
strict ? ver : `>=${ver}`,
256
name,
257
]);
258
const fun = strict ? strictCheckVersion : checkVersion;
259
for (const [version, constraint, name] of checkData) {
260
if (version === undefined) {
261
if (conf.jsonResult) {
262
if (conf.jsonResult.dependencies === undefined) {
263
conf.jsonResult.dependencies = {};
264
}
265
(conf.jsonResult.dependencies as Record<string, unknown>)[name] = {
266
version,
267
constraint,
268
found: false,
269
};
270
}
271
checkInfoMsg(conf, ` ${name} version: (not detected)`);
272
} else {
273
fun(version, constraint, name);
274
}
275
}
276
277
checkCompleteMessage(
278
conf,
279
"Checking versions of quarto dependencies......OK",
280
);
281
}
282
283
async function checkInstall(conf: CheckConfiguration) {
284
const {
285
services,
286
} = conf;
287
checkCompleteMessage(conf, "Checking Quarto installation......OK");
288
checkInfoMsg(conf, `${kIndent}Version: ${quartoConfig.version()}`);
289
if (quartoConfig.version() === "99.9.9") {
290
// if they're running a dev version, we assume git is installed
291
// and QUARTO_ROOT is set to the root of the quarto-cli repo
292
// print the output of git rev-parse HEAD
293
const quartoRoot = Deno.env.get("QUARTO_ROOT");
294
if (quartoRoot) {
295
const gitHead = await execProcess({
296
cmd: "git",
297
args: ["-C", quartoRoot, "rev-parse", "HEAD"],
298
stdout: "piped",
299
stderr: "piped", // to not show error if not in a git repo
300
});
301
if (gitHead.success && gitHead.stdout) {
302
checkInfoMsg(conf, `${kIndent}commit: ${gitHead.stdout.trim()}`);
303
if (conf.jsonResult) {
304
conf.jsonResult["quarto-dev-version"] = gitHead.stdout.trim();
305
}
306
}
307
}
308
}
309
checkInfoMsg(conf, `${kIndent}Path: ${quartoConfig.binPath()}`);
310
if (conf.jsonResult) {
311
conf.jsonResult["quarto-path"] = quartoConfig.binPath();
312
}
313
314
if (isWindows) {
315
const json: Record<string, unknown> = {};
316
if (conf.jsonResult) {
317
conf.jsonResult.windows = json;
318
}
319
try {
320
const codePage = readCodePage();
321
clearCodePageCache();
322
await cacheCodePage();
323
const codePage2 = readCodePage();
324
325
checkInfoMsg(conf, `${kIndent}CodePage: ${codePage2 || "unknown"}`);
326
json["code-page"] = codePage2 || "unknown";
327
if (codePage && codePage !== codePage2) {
328
checkInfoMsg(
329
conf,
330
`${kIndent}NOTE: Code page updated from ${codePage} to ${codePage2}. Previous rendering may have been affected.`,
331
);
332
json["code-page-updated-from"] = codePage;
333
}
334
// if non-standard code page, check for non-ascii characters in path
335
// deno-lint-ignore no-control-regex
336
const nonAscii = /[^\x00-\x7F]+/;
337
if (nonAscii.test(quartoConfig.binPath())) {
338
checkInfoMsg(
339
conf,
340
`${kIndent}ERROR: Non-ASCII characters in Quarto path causes rendering problems.`,
341
);
342
json["non-ascii-in-path"] = true;
343
}
344
} catch {
345
checkInfoMsg(conf, `${kIndent}CodePage: Unable to read code page`);
346
json["error"] = "Unable to read code page";
347
}
348
}
349
350
checkInfoMsg(conf, "");
351
const toolsMessage = "Checking tools....................";
352
const toolsOutput: string[] = [];
353
let tools: Awaited<ReturnType<typeof allTools>>;
354
const toolsJson: Record<string, unknown> = {};
355
if (conf.jsonResult) {
356
conf.jsonResult.tools = toolsJson;
357
}
358
const toolsCb = async () => {
359
tools = await allTools();
360
361
for (const tool of tools.installed) {
362
const version = await tool.installedVersion() || "(external install)";
363
toolsOutput.push(`${kIndent}${tool.name}: ${version}`);
364
toolsJson[tool.name] = {
365
version,
366
};
367
}
368
for (const tool of tools.notInstalled) {
369
toolsOutput.push(`${kIndent}${tool.name}: (not installed)`);
370
toolsJson[tool.name] = {
371
installed: false,
372
};
373
}
374
};
375
if (conf.jsonResult) {
376
await toolsCb();
377
} else {
378
await withSpinner({
379
message: toolsMessage,
380
doneMessage: toolsMessage + "OK",
381
}, toolsCb);
382
}
383
toolsOutput.forEach((out) => checkInfoMsg(conf, out));
384
checkInfoMsg(conf, "");
385
386
const latexMessage = "Checking LaTeX....................";
387
const latexOutput: string[] = [];
388
const latexJson: Record<string, unknown> = {};
389
if (conf.jsonResult) {
390
conf.jsonResult.latex = latexJson;
391
}
392
const latexCb = async () => {
393
const tlContext = await texLiveContext(true);
394
if (tlContext.hasTexLive) {
395
const version = await tlVersion(tlContext);
396
397
if (tlContext.usingGlobal) {
398
const tlMgrPath = await which("tlmgr");
399
400
latexOutput.push(`${kIndent}Using: Installation From Path`);
401
if (tlMgrPath) {
402
latexOutput.push(`${kIndent}Path: ${dirname(tlMgrPath)}`);
403
latexJson["path"] = dirname(tlMgrPath);
404
latexJson["source"] = "global";
405
}
406
} else {
407
latexOutput.push(`${kIndent}Using: TinyTex`);
408
if (tlContext.binDir) {
409
latexOutput.push(`${kIndent}Path: ${tlContext.binDir}`);
410
latexJson["path"] = tlContext.binDir;
411
latexJson["source"] = "tinytex";
412
}
413
}
414
latexOutput.push(`${kIndent}Version: ${version}`);
415
latexJson["version"] = version;
416
} else {
417
latexOutput.push(`${kIndent}Tex: (not detected)`);
418
latexJson["installed"] = false;
419
}
420
};
421
if (conf.jsonResult) {
422
await latexCb();
423
} else {
424
await withSpinner({
425
message: latexMessage,
426
doneMessage: latexMessage + "OK",
427
}, latexCb);
428
}
429
latexOutput.forEach((out) => checkInfoMsg(conf, out));
430
checkInfoMsg(conf, "");
431
432
const chromeHeadlessMessage = "Checking Chrome Headless....................";
433
const chromeHeadlessOutput: string[] = [];
434
const chromeJson: Record<string, unknown> = {};
435
if (conf.jsonResult) {
436
conf.jsonResult.chrome = chromeJson;
437
}
438
const chromeCb = async () => {
439
const chromeDetected = await findChrome();
440
const chromiumQuarto = tools.installed.find((tool) =>
441
tool.name === "chromium"
442
);
443
if (chromeDetected.path !== undefined) {
444
chromeHeadlessOutput.push(`${kIndent}Using: Chrome found on system`);
445
chromeHeadlessOutput.push(
446
`${kIndent}Path: ${chromeDetected.path}`,
447
);
448
if (chromeDetected.source) {
449
chromeHeadlessOutput.push(`${kIndent}Source: ${chromeDetected.source}`);
450
}
451
chromeJson["path"] = chromeDetected.path;
452
chromeJson["source"] = chromeDetected.source;
453
} else if (chromiumQuarto !== undefined) {
454
chromeJson["source"] = "quarto";
455
chromeHeadlessOutput.push(
456
`${kIndent}Using: Chromium installed by Quarto`,
457
);
458
if (chromiumQuarto?.binDir) {
459
chromeHeadlessOutput.push(
460
`${kIndent}Path: ${chromiumQuarto?.binDir}`,
461
);
462
chromeJson["path"] = chromiumQuarto?.binDir;
463
}
464
chromeHeadlessOutput.push(
465
`${kIndent}Version: ${chromiumQuarto.installedVersion}`,
466
);
467
chromeJson["version"] = chromiumQuarto.installedVersion;
468
} else {
469
chromeHeadlessOutput.push(`${kIndent}Chrome: (not detected)`);
470
chromeJson["installed"] = false;
471
}
472
};
473
if (conf.jsonResult) {
474
await chromeCb();
475
} else {
476
await withSpinner({
477
message: chromeHeadlessMessage,
478
doneMessage: chromeHeadlessMessage + "OK",
479
}, chromeCb);
480
}
481
chromeHeadlessOutput.forEach((out) => checkInfoMsg(conf, out));
482
checkInfoMsg(conf, "");
483
484
const kMessage = "Checking basic markdown render....";
485
const markdownRenderJson: Record<string, unknown> = {};
486
if (conf.jsonResult) {
487
conf.jsonResult.render = {
488
markdown: markdownRenderJson,
489
};
490
}
491
const markdownRenderCb = async () => {
492
const mdPath = services.temp.createFile({ suffix: "check.md" });
493
Deno.writeTextFileSync(
494
mdPath,
495
`
496
---
497
title: "Title"
498
---
499
500
## Header
501
`,
502
);
503
const result = await render(mdPath, {
504
services,
505
flags: { quiet: true },
506
});
507
if (result.error) {
508
if (!conf.jsonResult) {
509
throw result.error;
510
} else {
511
markdownRenderJson["error"] = result.error;
512
}
513
} else {
514
markdownRenderJson["ok"] = true;
515
}
516
};
517
518
if (conf.jsonResult) {
519
await markdownRenderCb();
520
} else {
521
await withSpinner({
522
message: kMessage,
523
doneMessage: kMessage + "OK\n",
524
}, markdownRenderCb);
525
}
526
}
527
528