Path: blob/master/src/packages/next/lib/landing/software-specs.ts
1450 views
/*1* This file is part of CoCalc: Copyright © 2021 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import { keys, map, sortBy, zipObject } from "lodash";6import { promises } from "node:fs";7import { basename } from "node:path";89import {10SOFTWARE_ENV_NAMES,11SoftwareEnvNames,12} from "@cocalc/util/consts/software-envs";13import { hours_ago } from "@cocalc/util/relative-time";14import { reuseInFlight } from "@cocalc/util/reuse-in-flight";15import withCustomize from "lib/with-customize";16import { SOFTWARE_FALLBACK, SOFTWARE_URLS } from "./software-data";17import {18ComputeComponents,19ComputeInventory,20EnvData,21LanguageName,22SoftwareSpec,23} from "./types";2425const { readFile } = promises;2627async function makeObject(keys, fn) {28return zipObject(keys, await Promise.all(map(keys, fn)));29}3031type SoftwareEnvironments = { [key in SoftwareEnvNames]: EnvData };3233let SoftwareEnvSpecs: SoftwareEnvironments | null = null;34let SoftwareEnvDownloadedTimestamp: number = 0;3536async function file2json(path: string): Promise<any> {37const data = await readFile(path, "utf8");38return JSON.parse(data);39}4041async function downloadInventoryJson(name: SoftwareEnvNames): Promise<EnvData> {42try {43const raw = await fetch(SOFTWARE_URLS[name]);44if (!raw.ok) {45console.log(`Problem downloading: ${raw.status}: ${raw.statusText}`);46} else {47const data = await raw.json();48console.log(`Downloaded software inventory ${name} successfully`);49return data;50}51} catch (err) {52console.log(`Problem downloading: ${err}`);53}54return SOFTWARE_FALLBACK[name] as EnvData;55}5657// load the current version of the software specs – if there is a problem, use the locally stored files as fallback.58async function fetchInventory(): Promise<SoftwareEnvironments> {59// for development, set the env variable to directory, where this files are60const localSpec = process.env.COCALC_SOFTWARE_ENVIRONMENTS;61if (localSpec != null) {62// read compute-inventory.json and compute-components.json from the local filesystem63console.log(`Reading inventory information from directory ${localSpec}`);64return await makeObject(65SOFTWARE_ENV_NAMES,66async (name) =>67await file2json(`${localSpec}/software-inventory-${name}.json`),68);69}70try {71// download the files for the newest information from the server72const ret = await makeObject(73SOFTWARE_ENV_NAMES,74async (name) => await downloadInventoryJson(name),75);76return ret;77} catch (err) {78console.error(`Problem fetching software inventory: ${err}`);79return SOFTWARE_FALLBACK;80}81}8283const fetchSoftwareSpec = reuseInFlight(async function () {84SoftwareEnvSpecs = await fetchInventory();85SoftwareEnvDownloadedTimestamp = Date.now();86return SoftwareEnvSpecs;87});8889/**90* get a cached copy of the software specs91*/92async function getSoftwareInfo(name: SoftwareEnvNames): Promise<EnvData> {93// if SoftwareEnvSpecs is not set or not older than one hour, fetch it94if (SoftwareEnvSpecs != null) {95if (SoftwareEnvDownloadedTimestamp > hours_ago(1).getTime()) {96// fresh enough, just return it97return SoftwareEnvSpecs[name];98} else {99// we asynchroneously fetch to refresh, but return the data immediately to the client100fetchSoftwareSpec();101return SoftwareEnvSpecs[name];102}103} else {104const specs = await fetchSoftwareSpec();105return specs[name];106}107}108109async function getSoftwareInfoLang(110name: SoftwareEnvNames,111lang: LanguageName,112): Promise<{113inventory: ComputeInventory[LanguageName];114components: ComputeComponents[LanguageName];115timestamp: string;116}> {117const { inventory, data, timestamp } = await getSoftwareInfo(name);118return { inventory: inventory[lang], components: data[lang], timestamp };119}120121// during startup, we fetch getSoftwareSpec() once to warm up the cache…122(async function () {123fetchSoftwareSpec(); // not blocking124})();125126// cached processed software specs127let SPEC: Record<SoftwareEnvNames, Readonly<SoftwareSpec> | null> = {} as any;128129async function getSoftwareSpec(name: SoftwareEnvNames): Promise<SoftwareSpec> {130const cached = SPEC[name];131if (cached != null) return cached;132const nextSpec: Partial<SoftwareSpec> = {};133const { inventory } = await getSoftwareInfo(name);134for (const cmd in inventory.language_exes) {135const info = inventory.language_exes[cmd];136if (nextSpec[info.lang] == null) {137nextSpec[info.lang] = {};138}139// the basename of the cmd path140const base = cmd.indexOf(" ") > 0 ? cmd : basename(cmd);141nextSpec[info.lang][base] = {142cmd,143name: info.name,144doc: info.doc,145url: info.url,146path: info.path,147};148}149SPEC[name] = nextSpec as SoftwareSpec;150return nextSpec as SoftwareSpec;151}152153/**154* This determines the order of columns when there is more than on executable for a language.155*/156function getLanguageExecutables({ lang, inventory }): string[] {157if (inventory == null) return [];158return sortBy(keys(inventory[lang]), (x: string) => {159if (lang === "python") {160if (x.endsWith("python3")) return ["0", x];161if (x.indexOf("sage") >= 0) return ["2", x];162if (x.endsWith("python2")) return ["3", x];163return ["1", x]; // anaconda envs and others164} else {165return x.toLowerCase();166}167});168}169170// this is for the server side getServerSideProps function171export async function withCustomizedAndSoftwareSpec(172context,173lang: LanguageName | "executables",174) {175const { name } = context.params;176177// if name is not in SOFTWARE_ENV_NAMES, return {notFound : true}178if (!SOFTWARE_ENV_NAMES.includes(name)) {179return { notFound: true };180}181182const [customize, spec] = await Promise.all([183withCustomize({ context }),184getSoftwareSpec(name),185]);186187customize.props.name = name;188189if (lang === "executables") {190// this is instant because specs are already in the cache191const softwareInfo = await getSoftwareInfo(name);192const { inventory, timestamp } = softwareInfo;193customize.props.executablesSpec = inventory.executables;194customize.props.timestamp = timestamp;195return customize;196} else {197customize.props.spec = spec[lang];198// this is instant because specs are already in the cache199const { inventory, components, timestamp } = await getSoftwareInfoLang(200name,201lang,202);203customize.props.inventory = inventory;204customize.props.components = components;205customize.props.timestamp = timestamp;206}207208// at this point, lang != "executables"209// we gather the list of interpreters (executables) for the given language210const { inventory } = await getSoftwareInfo(name);211customize.props.execInfo = {};212for (const cmd of getLanguageExecutables({ inventory, lang })) {213const path = inventory.language_exes[cmd]?.path ?? cmd;214customize.props.execInfo[path] = inventory.executables?.[path] ?? null;215}216217return customize;218}219220221