Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/tools/tools.ts
12924 views
1
/*
2
* install.ts
3
*
4
* Copyright (C) 2020-2022 Posit Software, PBC
5
*/
6
7
import { info, warning } from "../deno_ral/log.ts";
8
import { withSpinner } from "../core/console.ts";
9
import { logError } from "../core/log.ts";
10
import { os as platformOs } from "../deno_ral/platform.ts";
11
12
import {
13
InstallableTool,
14
InstallContext,
15
kUpdatePath,
16
ToolConfigurationState,
17
ToolSummaryData,
18
} from "./types.ts";
19
import { tinyTexInstallable } from "./impl/tinytex.ts";
20
import { chromiumInstallable } from "./impl/chromium.ts";
21
import { chromeHeadlessShellInstallable } from "./impl/chrome-headless-shell.ts";
22
import { verapdfInstallable } from "./impl/verapdf.ts";
23
import { downloadWithProgress } from "../core/download.ts";
24
import { Confirm } from "cliffy/prompt/mod.ts";
25
import { isWSL } from "../core/platform.ts";
26
import { ensureDirSync, existsSync, safeRemoveSync } from "../deno_ral/fs.ts";
27
import { join } from "../deno_ral/path.ts";
28
import { expandPath, suggestUserBinPaths } from "../core/path.ts";
29
import { isWindows } from "../deno_ral/platform.ts";
30
31
// The tools that are available to install
32
const kInstallableTools: { [key: string]: InstallableTool } = {
33
tinytex: tinyTexInstallable,
34
// temporarily disabled until deno 1.28.* gets puppeteer support
35
chromium: chromiumInstallable,
36
"chrome-headless-shell": chromeHeadlessShellInstallable,
37
verapdf: verapdfInstallable,
38
};
39
40
export async function allTools(): Promise<{
41
installed: InstallableTool[];
42
notInstalled: InstallableTool[];
43
}> {
44
const installed: InstallableTool[] = [];
45
const notInstalled: InstallableTool[] = [];
46
const tools = installableTools();
47
for (const name of tools) {
48
// Find the tool
49
const tool = installableTool(name);
50
const isInstalled = await tool.installed();
51
if (isInstalled) {
52
installed.push(tool);
53
} else {
54
notInstalled.push(tool);
55
}
56
}
57
return {
58
installed,
59
notInstalled,
60
};
61
}
62
63
export function installableTools(): string[] {
64
return Object.keys(kInstallableTools);
65
}
66
67
export function installableToolNames(): string[] {
68
return Object.values(kInstallableTools).map((tool) => tool.name);
69
}
70
71
export async function printToolInfo(name: string) {
72
name = name || "";
73
// Run the install
74
const tool = installableTool(name);
75
if (tool) {
76
const response: Record<string, unknown> = {
77
name: tool.name,
78
installed: await tool.installed(),
79
version: await tool.installedVersion(),
80
directory: await tool.installDir(),
81
};
82
if (tool.binDir) {
83
response["bin-directory"] = await tool.binDir();
84
}
85
if (response.installed && tool.verifyConfiguration) {
86
response["configuration"] = await tool.verifyConfiguration();
87
}
88
Deno.stdout.writeSync(
89
new TextEncoder().encode(JSON.stringify(response, null, 2) + "\n"),
90
);
91
}
92
}
93
94
export function checkToolRequirement(name: string) {
95
if (name.toLowerCase() === "chromium" && isWSL()) {
96
// TODO: Change to a quarto-web url page ?
97
const troubleshootUrl =
98
"https://pptr.dev/next/troubleshooting#running-puppeteer-on-wsl-windows-subsystem-for-linux.";
99
warning([
100
`${name} can't be installed fully on WSL with Quarto as system requirements could be missing.`,
101
`- Please do a manual installation following recommandations at ${troubleshootUrl}`,
102
"- See https://github.com/quarto-dev/quarto-cli/issues/1822 for more context.",
103
].join("\n"));
104
return false;
105
} else if (name.toLowerCase() === "chrome-headless-shell" && isWSL()) {
106
info(
107
"Note: chrome-headless-shell is a headless-only binary and should work on WSL without additional system dependencies.",
108
);
109
return true;
110
} else {
111
return true;
112
}
113
}
114
115
export async function installTool(name: string, updatePath?: boolean) {
116
name = name || "";
117
// Run the install
118
const tool = installableTool(name);
119
if (tool) {
120
if (checkToolRequirement(name)) {
121
// Create a working directory for the installer to use
122
const workingDir = Deno.makeTempDirSync();
123
try {
124
// The context for the installers
125
const context = installContext(workingDir, updatePath);
126
127
context.info(`Installing ${name}`);
128
129
// See if it is already installed
130
const alreadyInstalled = await tool.installed();
131
if (alreadyInstalled) {
132
// Already installed, do nothing
133
context.error(`Install canceled - ${name} is already installed.`);
134
Deno.exit(1);
135
} else {
136
// Prereqs for this platform
137
const platformPrereqs = tool.prereqs.filter((prereq) =>
138
prereq.os.includes(platformOs)
139
);
140
141
// Check to see whether any prerequisites are satisfied
142
for (const prereq of platformPrereqs) {
143
const met = await prereq.check(context);
144
if (!met) {
145
const message = typeof prereq.message === "function"
146
? await prereq.message(context)
147
: prereq.message;
148
context.error(message);
149
Deno.exit(1);
150
}
151
}
152
153
// Fetch the package information
154
const pkgInfo = await tool.preparePackage(context);
155
156
// Do the install
157
await tool.install(pkgInfo, context);
158
159
// post install
160
const restartRequired = await tool.afterInstall(context);
161
162
context.info("Installation successful");
163
if (restartRequired) {
164
context.info(
165
"To complete this installation, please restart your system.",
166
);
167
}
168
}
169
} finally {
170
// Cleanup the working directory
171
safeRemoveSync(workingDir, { recursive: true });
172
}
173
}
174
} else {
175
// No tool found
176
info(
177
`Could not install '${name}'- try again with one of the following:`,
178
);
179
installableTools().forEach((name) =>
180
info("quarto install " + name, { indent: 2 })
181
);
182
}
183
}
184
185
export async function uninstallTool(name: string, updatePath?: boolean) {
186
const tool = installableTool(name);
187
if (tool) {
188
const installed = await tool.installed();
189
if (installed) {
190
const workingDir = Deno.makeTempDirSync();
191
const context = installContext(workingDir, updatePath);
192
193
// Emit initial message
194
context.info(`Uninstalling ${name}`);
195
196
try {
197
// The context for the installers
198
await tool.uninstall(context);
199
info(`Uninstallation successful`);
200
} catch (e) {
201
logError(e);
202
} finally {
203
safeRemoveSync(workingDir, { recursive: true });
204
}
205
} else {
206
info(
207
`${name} is not installed use 'quarto install ${name} to install it.`,
208
);
209
}
210
}
211
}
212
213
export async function updateTool(name: string) {
214
const summary = await toolSummary(name);
215
const tool = installableTool(name);
216
217
if (tool && summary && summary.installed) {
218
const workingDir = Deno.makeTempDirSync();
219
const context = installContext(workingDir);
220
try {
221
context.info(
222
`Updating ${tool.name} from ${summary.installedVersion} to ${summary.latestRelease.version}`,
223
);
224
225
// Fetch the package
226
const pkgInfo = await tool.preparePackage(context);
227
228
context.info(`Removing ${summary.installedVersion}`);
229
230
// Uninstall the existing version of the tool
231
await tool.uninstall(context);
232
233
context.info(`Installing ${summary.latestRelease.version}`);
234
235
// Install the new package
236
await tool.install(pkgInfo, context);
237
238
context.info("Finishing update");
239
// post install
240
const restartRequired = await tool.afterInstall(context);
241
242
context.info("Update successful");
243
if (restartRequired) {
244
context.info(
245
"To complete this update, please restart your system.",
246
);
247
}
248
} catch (e) {
249
logError(e);
250
} finally {
251
safeRemoveSync(workingDir, { recursive: true });
252
}
253
} else {
254
info(
255
`${name} is not installed use 'quarto install ${name.toLowerCase()} to install it.`,
256
);
257
}
258
}
259
260
export async function toolSummary(
261
name: string,
262
): Promise<ToolSummaryData | undefined> {
263
// Find the tool
264
const tool = installableTool(name);
265
266
// Information about the potential update
267
if (tool) {
268
const installed = await tool.installed();
269
const installedVersion = await tool.installedVersion();
270
const latestRelease = await tool.latestRelease();
271
const configuration = tool.verifyConfiguration && installed
272
? await tool.verifyConfiguration()
273
: { status: "ok" } as ToolConfigurationState;
274
return { installed, installedVersion, latestRelease, configuration };
275
} else {
276
return undefined;
277
}
278
}
279
280
export function installableTool(name: string) {
281
return kInstallableTools[name.toLowerCase()];
282
}
283
284
const installContext = (
285
workingDir: string,
286
updatePath?: boolean,
287
): InstallContext => {
288
const installMessaging = {
289
info: (msg: string) => {
290
info(msg);
291
},
292
error: (msg: string) => {
293
info(msg);
294
},
295
confirm: (msg: string, def?: boolean) => {
296
if (def !== undefined) {
297
return Confirm.prompt({ message: msg, default: def });
298
} else {
299
return Confirm.prompt(msg);
300
}
301
},
302
withSpinner,
303
};
304
305
return {
306
download: async (
307
name: string,
308
url: string,
309
target: string,
310
) => {
311
try {
312
await downloadWithProgress(url, `Downloading ${name}`, target);
313
} catch (error) {
314
// shouldn't happen, but this appeases the typechecker
315
if (!(error instanceof Error)) {
316
throw error;
317
}
318
installMessaging.error(
319
error.message,
320
);
321
Deno.exit(1);
322
}
323
},
324
workingDir,
325
...installMessaging,
326
props: {},
327
flags: {
328
[kUpdatePath]: updatePath,
329
},
330
};
331
};
332
333
// Shared utility functions for --update-path functionality
334
335
/**
336
* Creates a symlink for a tool binary in a user bin directory.
337
* Returns true if successful, false otherwise.
338
*/
339
export async function createToolSymlink(
340
binaryPath: string,
341
symlinkName: string,
342
context: InstallContext,
343
): Promise<boolean> {
344
if (isWindows) {
345
context.info(
346
`Add the tool's directory to your PATH to use ${symlinkName} from anywhere.`,
347
);
348
return false;
349
}
350
351
const binPaths = suggestUserBinPaths();
352
if (binPaths.length === 0) {
353
context.info(
354
`No suitable bin directory found in PATH. Add the tool's directory to your PATH manually.`,
355
);
356
return false;
357
}
358
359
for (const binPath of binPaths) {
360
const expandedBinPath = expandPath(binPath);
361
ensureDirSync(expandedBinPath);
362
const symlinkPath = join(expandedBinPath, symlinkName);
363
364
try {
365
// Remove existing symlink if present
366
if (existsSync(symlinkPath)) {
367
await Deno.remove(symlinkPath);
368
}
369
// Create new symlink
370
await Deno.symlink(binaryPath, symlinkPath);
371
return true;
372
} catch {
373
// Try next path
374
continue;
375
}
376
}
377
378
context.info(
379
`Could not create symlink. Add the tool's directory to your PATH manually.`,
380
);
381
return false;
382
}
383
384
/**
385
* Removes a tool's symlink from user bin directories.
386
*/
387
export async function removeToolSymlink(symlinkName: string): Promise<void> {
388
if (isWindows) {
389
return;
390
}
391
392
const binPaths = suggestUserBinPaths();
393
for (const binPath of binPaths) {
394
const symlinkPath = join(expandPath(binPath), symlinkName);
395
try {
396
const stat = await Deno.lstat(symlinkPath);
397
if (stat.isSymlink) {
398
await Deno.remove(symlinkPath);
399
}
400
} catch {
401
// Symlink doesn't exist, continue
402
}
403
}
404
}
405
406