import { info, warning } from "../deno_ral/log.ts";
import { withSpinner } from "../core/console.ts";
import { logError } from "../core/log.ts";
import { os as platformOs } from "../deno_ral/platform.ts";
import {
InstallableTool,
InstallContext,
kUpdatePath,
ToolConfigurationState,
ToolSummaryData,
} from "./types.ts";
import { tinyTexInstallable } from "./impl/tinytex.ts";
import { chromiumInstallable } from "./impl/chromium.ts";
import { verapdfInstallable } from "./impl/verapdf.ts";
import { downloadWithProgress } from "../core/download.ts";
import { Confirm } from "cliffy/prompt/mod.ts";
import { isWSL } from "../core/platform.ts";
import { ensureDirSync, existsSync, safeRemoveSync } from "../deno_ral/fs.ts";
import { join } from "../deno_ral/path.ts";
import { expandPath, suggestUserBinPaths } from "../core/path.ts";
import { isWindows } from "../deno_ral/platform.ts";
const kInstallableTools: { [key: string]: InstallableTool } = {
tinytex: tinyTexInstallable,
chromium: chromiumInstallable,
verapdf: verapdfInstallable,
};
export async function allTools(): Promise<{
installed: InstallableTool[];
notInstalled: InstallableTool[];
}> {
const installed: InstallableTool[] = [];
const notInstalled: InstallableTool[] = [];
const tools = installableTools();
for (const name of tools) {
const tool = installableTool(name);
const isInstalled = await tool.installed();
if (isInstalled) {
installed.push(tool);
} else {
notInstalled.push(tool);
}
}
return {
installed,
notInstalled,
};
}
export function installableTools(): string[] {
const tools: string[] = [];
Object.keys(kInstallableTools).forEach((key) => {
const tool = kInstallableTools[key];
tools.push(tool.name.toLowerCase());
});
return tools;
}
export async function printToolInfo(name: string) {
name = name || "";
const tool = installableTool(name);
if (tool) {
const response: Record<string, unknown> = {
name: tool.name,
installed: await tool.installed(),
version: await tool.installedVersion(),
directory: await tool.installDir(),
};
if (tool.binDir) {
response["bin-directory"] = await tool.binDir();
}
if (response.installed && tool.verifyConfiguration) {
response["configuration"] = await tool.verifyConfiguration();
}
Deno.stdout.writeSync(
new TextEncoder().encode(JSON.stringify(response, null, 2) + "\n"),
);
}
}
export function checkToolRequirement(name: string) {
if (name.toLowerCase() === "chromium" && isWSL()) {
const troubleshootUrl =
"https://pptr.dev/next/troubleshooting#running-puppeteer-on-wsl-windows-subsystem-for-linux.";
warning([
`${name} can't be installed fully on WSL with Quarto as system requirements could be missing.`,
`- Please do a manual installation following recommandations at ${troubleshootUrl}`,
"- See https://github.com/quarto-dev/quarto-cli/issues/1822 for more context.",
].join("\n"));
return false;
} else {
return true;
}
}
export async function installTool(name: string, updatePath?: boolean) {
name = name || "";
const tool = installableTool(name);
if (tool) {
if (checkToolRequirement(name)) {
const workingDir = Deno.makeTempDirSync();
try {
const context = installContext(workingDir, updatePath);
context.info(`Installing ${name}`);
const alreadyInstalled = await tool.installed();
if (alreadyInstalled) {
context.error(`Install canceled - ${name} is already installed.`);
Deno.exit(1);
} else {
const platformPrereqs = tool.prereqs.filter((prereq) =>
prereq.os.includes(platformOs)
);
for (const prereq of platformPrereqs) {
const met = await prereq.check(context);
if (!met) {
context.error(prereq.message);
Deno.exit(1);
}
}
const pkgInfo = await tool.preparePackage(context);
await tool.install(pkgInfo, context);
const restartRequired = await tool.afterInstall(context);
context.info("Installation successful");
if (restartRequired) {
context.info(
"To complete this installation, please restart your system.",
);
}
}
} finally {
safeRemoveSync(workingDir, { recursive: true });
}
}
} else {
info(
`Could not install '${name}'- try again with one of the following:`,
);
installableTools().forEach((name) =>
info("quarto install " + name, { indent: 2 })
);
}
}
export async function uninstallTool(name: string, updatePath?: boolean) {
const tool = installableTool(name);
if (tool) {
const installed = await tool.installed();
if (installed) {
const workingDir = Deno.makeTempDirSync();
const context = installContext(workingDir, updatePath);
context.info(`Uninstalling ${name}`);
try {
await tool.uninstall(context);
info(`Uninstallation successful`);
} catch (e) {
logError(e);
} finally {
safeRemoveSync(workingDir, { recursive: true });
}
} else {
info(
`${name} is not installed use 'quarto install ${name} to install it.`,
);
}
}
}
export async function updateTool(name: string) {
const summary = await toolSummary(name);
const tool = installableTool(name);
if (tool && summary && summary.installed) {
const workingDir = Deno.makeTempDirSync();
const context = installContext(workingDir);
try {
context.info(
`Updating ${tool.name} from ${summary.installedVersion} to ${summary.latestRelease.version}`,
);
const pkgInfo = await tool.preparePackage(context);
context.info(`Removing ${summary.installedVersion}`);
await tool.uninstall(context);
context.info(`Installing ${summary.latestRelease.version}`);
await tool.install(pkgInfo, context);
context.info("Finishing update");
const restartRequired = await tool.afterInstall(context);
context.info("Update successful");
if (restartRequired) {
context.info(
"To complete this update, please restart your system.",
);
}
} catch (e) {
logError(e);
} finally {
safeRemoveSync(workingDir, { recursive: true });
}
} else {
info(
`${name} is not installed use 'quarto install ${name.toLowerCase()} to install it.`,
);
}
}
export async function toolSummary(
name: string,
): Promise<ToolSummaryData | undefined> {
const tool = installableTool(name);
if (tool) {
const installed = await tool.installed();
const installedVersion = await tool.installedVersion();
const latestRelease = await tool.latestRelease();
const configuration = tool.verifyConfiguration && installed
? await tool.verifyConfiguration()
: { status: "ok" } as ToolConfigurationState;
return { installed, installedVersion, latestRelease, configuration };
} else {
return undefined;
}
}
export function installableTool(name: string) {
return kInstallableTools[name.toLowerCase()];
}
const installContext = (
workingDir: string,
updatePath?: boolean,
): InstallContext => {
const installMessaging = {
info: (msg: string) => {
info(msg);
},
error: (msg: string) => {
info(msg);
},
confirm: (msg: string, def?: boolean) => {
if (def !== undefined) {
return Confirm.prompt({ message: msg, default: def });
} else {
return Confirm.prompt(msg);
}
},
withSpinner,
};
return {
download: async (
name: string,
url: string,
target: string,
) => {
try {
await downloadWithProgress(url, `Downloading ${name}`, target);
} catch (error) {
if (!(error instanceof Error)) {
throw error;
}
installMessaging.error(
error.message,
);
Deno.exit(1);
}
},
workingDir,
...installMessaging,
props: {},
flags: {
[kUpdatePath]: updatePath,
},
};
};
export async function createToolSymlink(
binaryPath: string,
symlinkName: string,
context: InstallContext,
): Promise<boolean> {
if (isWindows) {
context.info(
`Add the tool's directory to your PATH to use ${symlinkName} from anywhere.`,
);
return false;
}
const binPaths = suggestUserBinPaths();
if (binPaths.length === 0) {
context.info(
`No suitable bin directory found in PATH. Add the tool's directory to your PATH manually.`,
);
return false;
}
for (const binPath of binPaths) {
const expandedBinPath = expandPath(binPath);
ensureDirSync(expandedBinPath);
const symlinkPath = join(expandedBinPath, symlinkName);
try {
if (existsSync(symlinkPath)) {
await Deno.remove(symlinkPath);
}
await Deno.symlink(binaryPath, symlinkPath);
return true;
} catch {
continue;
}
}
context.info(
`Could not create symlink. Add the tool's directory to your PATH manually.`,
);
return false;
}
export async function removeToolSymlink(symlinkName: string): Promise<void> {
if (isWindows) {
return;
}
const binPaths = suggestUserBinPaths();
for (const binPath of binPaths) {
const symlinkPath = join(expandPath(binPath), symlinkName);
try {
const stat = await Deno.lstat(symlinkPath);
if (stat.isSymlink) {
await Deno.remove(symlinkPath);
}
} catch {
}
}
}