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