UNPKG

@cocalc/project

Version:
299 lines 10.9 kB
"use strict"; /* * This file is part of CoCalc: Copyright © 2020 Sagemath, Inc. * License: AGPLv3 s.t. "Commons Clause" – see LICENSE.md for details */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.get_configuration = void 0; /* * This derives the configuration and capabilities of the current project. * It is used in the UI to only show/run those elements, which should work. * The corresponding file in the webapp is @cocalc/frontend/project_configuration.ts */ const apps_1 = require("@cocalc/frontend/frame-editors/x11-editor/apps"); const project_configuration_1 = require("@cocalc/frontend/project_configuration"); const code_formatter_1 = require("@cocalc/util/code-formatter"); const misc_1 = require("@cocalc/util/misc"); const child_process_1 = require("child_process"); const fs_1 = require("fs"); const promises_1 = require("fs/promises"); const util_1 = require("util"); const which_1 = __importDefault(require("which")); const exec = (0, util_1.promisify)(child_process_1.exec); // we prefix the environment PATH by default bin paths pointing into it in order to pick up locally installed binaries. // they can't be set as defaults for projects since this could break it from starting up function construct_path() { const env = process.env; // we can safely assume that PATH is defined const entries = env.PATH.split(":"); const home = env.HOME ?? "/home/user"; entries.unshift(`${home}/.local/bin`); entries.unshift(`${home}/bin`); return entries.join(":"); } const PATH = construct_path(); // test if the given utiltiy "name" exists (executable in the PATH) async function have(name) { return new Promise((resolve) => { (0, which_1.default)(name, { path: PATH }, function (error, path) { resolve(error == null && path != null); }); }); } // we cache this as long as the project runs const conf = {}; // check for all X11 apps. // UI will only show buttons for existing executables. async function x11_apps() { const status = []; const KEYS = Object.keys(apps_1.APPS); for (const key of KEYS) { const app = apps_1.APPS[key]; status.push(have(app.command != null ? app.command : key)); } const results = await Promise.all(status); const ret = {}; KEYS.map((name, idx) => (ret[name] = results[idx])); return ret; } // determines if X11 support exists at all async function get_x11() { return await have("xpra"); } // Quarto document formatter (on top of pandoc) async function get_quarto() { return await have("quarto"); } // do we have "sage"? which version? async function get_sage_info() { // TODO probably also check if smc_sagews is working? or the sage server? // without sage, sagews files are disabled const exists = await have("sage"); let version = undefined; if (exists) { // We need the version of sage (--version runs quickly) try { const env = (0, misc_1.copy)(process.env); env.PATH = PATH; const info = (await exec("sage --version", { env })).stdout.trim(); const m = info.match(/version ([\d+.]+[\d+])/); if (m != null) { const v = m[1]; if (v != null && v.length > 1) { version = v.split(".").map((x) => parseInt(x)); // console.log(`Sage version info: ${info} -> ${version}`, env); } } } catch (err) { // TODO: do something better than silently ignoring errors. This console.log // isn't going to be seen by the user. console.log("Problem fetching sage version info -- ignoring", err); } } return { exists, version }; } // this checks the level of jupyter support. none (false), or classical, lab, ... async function get_jupyter() { if (await have("jupyter")) { return { lab: await have("jupyter-lab"), notebook: await have("jupyter-notebook"), kernelspec: await have("jupyter-kernelspec"), }; } else { return false; } } // to support latex, we need a couple of executables available // TODO dumb down the UI to also work with less tools (e.g. without synctex) async function get_latex(hashsums) { const prereq = ["pdflatex", "latexmk", "synctex"]; const have_prereq = (await Promise.all(prereq.map(have))).every((p) => p); // TODO webapp only uses sha1sum. use a fallback if not available. return hashsums.sha1sum && have_prereq; } // plain text editors (md, tex, ...) use aspell → disable calling aspell if not available. async function get_spellcheck() { return await have("aspell"); } // without sshd we cannot copy to this project. that's vital for courses. async function get_sshd() { return await have("/usr/sbin/sshd"); } // we check if we can use headless chrome to do html to pdf conversion, // which uses either google-chrome or chromium-browser. Note that there // is no good headless pdf support using firefox. // (TODO: I don't think this is used in our code in practice, and instead not // having one of these at runtime would just result in a error message // to the user mentioning it is missing.) async function get_html2pdf() { return (await have("chromium-browser")) || (await have("google-chrome")); } // do we have pandoc, e.g. used for docx2md async function get_pandoc() { return await have("pandoc"); } // this is for rnw RMarkdown files. // This just tests R, which provides knitr out of the box? async function get_rmd() { return await have("R"); } // jq is used to e.g. pre-process ipynb files async function get_jq() { return await have("jq"); } // code-server is VS Code's Sever version, which we use to provide a web-based editor. async function get_vscode() { return await have("code-server"); } // julia executable, for the programming language, and we also assume that "Pluto" package is installed async function get_julia() { return await have("julia"); } // check if we can read that json file. // if it exists, show the corresponding button in "Files". async function get_library() { return new Promise((resolve) => { (0, fs_1.access)(project_configuration_1.LIBRARY_INDEX_FILE, fs_1.constants.R_OK, (err) => { resolve(err ? false : true); }); }); } // formatting code, e.g. python, javascript, etc. // we check this here, because the frontend should offer these choices if available. // in some cases like python, there could be multiple ways (yapf, yapf3, black, autopep8, ...) async function get_formatting() { const status = []; const tools = new Array(...new Set(Object.keys(code_formatter_1.syntax2tool).map((k) => code_formatter_1.syntax2tool[k]))); tools.push("yapf3", "black", "autopep8"); const tidy = have("tidy"); const ret = {}; for (const tool of tools) { if (tool === "formatR") { // TODO special case. must check for package "formatR" in "R" -- for now just test for R status.push((async () => (ret[tool] = await have("R")))()); } else if (tool == "bib-biber") { // another special case status.push((async () => (ret[tool] = await have("biber")))()); } else if (tool === "xml-tidy") { // tidy, already covered } else { status.push((async () => (ret[tool] = await have(tool)))()); } } // this populates all "await have" in ret[...] await Promise.all(status); ret["tidy"] = await tidy; // just for testing // ret['yapf'] = false; // prettier always available, because it is a js library dependency ret["prettier"] = true; return ret; } // this could be used by the webapp to fall back to other hashsums async function get_hashsums() { return { sha1sum: await have("sha1sum"), sha256sum: await have("sha256sum"), md5sum: await have("md5sum"), }; } async function get_homeDirectory() { // realpath is necessary, because in some circumstances the home dir is a symlink const home = process.env.HOME; if (home == null) { return null; } else { return await (0, promises_1.realpath)(home); } } // assemble capabilities object async function capabilities() { const sage_info_future = get_sage_info(); const hashsums = await get_hashsums(); const [formatting, latex, jupyter, spellcheck, html2pdf, pandoc, sshd, library, x11, rmd, qmd, vscode, julia, homeDirectory,] = await Promise.all([ get_formatting(), get_latex(hashsums), get_jupyter(), get_spellcheck(), get_html2pdf(), get_pandoc(), get_sshd(), get_library(), get_x11(), get_rmd(), get_quarto(), get_vscode(), get_julia(), get_homeDirectory(), ]); const caps = { jupyter, formatting, hashsums, latex, sage: false, sage_version: undefined, x11, rmd, qmd, jq: await get_jq(), spellcheck, library, sshd, html2pdf, pandoc, vscode, julia, homeDirectory, }; const sage = await sage_info_future; caps.sage = sage.exists; if (caps.sage) { caps.sage_version = sage.version; } return caps; } // this is the entry point for the API call // "main": everything that's needed throughout the project // "x11": additional checks which are queried when an X11 editor opens up // TODO similarly, query available "shells" to use for the corresponding code editor button async function get_configuration(aspect, no_cache = false) { const cached = conf[aspect]; if (cached != null && !no_cache) return cached; const t0 = new Date().getTime(); const new_conf = (async function () { switch (aspect) { case "main": return { timestamp: new Date(), capabilities: await capabilities(), }; case "x11": return { timestamp: new Date(), capabilities: await x11_apps(), }; } })(); new_conf.timing_s = (new Date().getTime() - t0) / 1000; conf[aspect] = await new_conf; return new_conf; } exports.get_configuration = get_configuration; // testing: uncomment, and run $ ts-node configuration.ts // (async () => { // console.log(await x11_apps()); // console.log(await capabilities()); // })(); //# sourceMappingURL=configuration.js.map