Path: blob/master/src/packages/next/lib/landing/software-specs.ts
5787 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";78import {9SOFTWARE_ENV_NAMES,10SoftwareEnvNames,11} from "@cocalc/util/consts/software-envs";12import { hours_ago } from "@cocalc/util/relative-time";13import { reuseInFlight } from "@cocalc/util/reuse-in-flight";14import withCustomize from "lib/with-customize";15import { SOFTWARE_FALLBACK, SOFTWARE_URLS } from "./software-data";16import {17ComputeComponents,18ComputeInventory,19EnvData,20LanguageName,21SoftwareSpec,22} from "./types";2324const { readFile } = promises;2526async function makeObject(keys, fn) {27return zipObject(keys, await Promise.all(map(keys, fn)));28}2930type SoftwareEnvironments = { [key in SoftwareEnvNames]: EnvData };3132let SoftwareEnvSpecs: SoftwareEnvironments | null = null;33let SoftwareEnvDownloadedTimestamp: number = 0;3435async function file2json(path: string): Promise<any> {36const data = await readFile(path, "utf8");37return JSON.parse(data);38}3940async function downloadInventoryJson(name: SoftwareEnvNames): Promise<EnvData> {41try {42const raw = await fetch(SOFTWARE_URLS[name]);43if (!raw.ok) {44console.log(`Problem downloading: ${raw.status}: ${raw.statusText}`);45} else {46const data = await raw.json();47console.log(`Downloaded software inventory ${name} successfully`);48return data;49}50} catch (err) {51console.log(`Problem downloading: ${err}`);52}53return SOFTWARE_FALLBACK[name] as EnvData;54}5556// load the current version of the software specs – if there is a problem, use the locally stored files as fallback.57async function fetchInventory(): Promise<SoftwareEnvironments> {58// for development, set the env variable to directory, where this files are59const localSpec = process.env.COCALC_SOFTWARE_ENVIRONMENTS;60if (localSpec != null) {61// read compute-inventory.json and compute-components.json from the local filesystem62console.log(`Reading inventory information from directory ${localSpec}`);63return await makeObject(64SOFTWARE_ENV_NAMES,65async (name) =>66await file2json(`${localSpec}/software-inventory-${name}.json`),67);68}69try {70// download the files for the newest information from the server71const ret = await makeObject(72SOFTWARE_ENV_NAMES,73async (name) => await downloadInventoryJson(name),74);75return ret;76} catch (err) {77console.error(`Problem fetching software inventory: ${err}`);78return SOFTWARE_FALLBACK;79}80}8182const fetchSoftwareSpec = reuseInFlight(async function () {83SoftwareEnvSpecs = await fetchInventory();84SoftwareEnvDownloadedTimestamp = Date.now();85return SoftwareEnvSpecs;86});8788/**89* get a cached copy of the software specs90*/91async function getSoftwareInfo(name: SoftwareEnvNames): Promise<EnvData> {92// if SoftwareEnvSpecs is not set or not older than one hour, fetch it93if (SoftwareEnvSpecs != null) {94if (SoftwareEnvDownloadedTimestamp > hours_ago(1).getTime()) {95// fresh enough, just return it96return SoftwareEnvSpecs[name];97} else {98// we asynchroneously fetch to refresh, but return the data immediately to the client99fetchSoftwareSpec();100return SoftwareEnvSpecs[name];101}102} else {103const specs = await fetchSoftwareSpec();104return specs[name];105}106}107108async function getSoftwareInfoLang(109name: SoftwareEnvNames,110lang: LanguageName,111): Promise<{112inventory: ComputeInventory[LanguageName];113components: ComputeComponents[LanguageName];114timestamp: string;115}> {116const { inventory, data, timestamp } = await getSoftwareInfo(name);117return { inventory: inventory[lang], components: data[lang], timestamp };118}119120// during startup, we fetch getSoftwareSpec() once to warm up the cache…121(async function () {122fetchSoftwareSpec(); // not blocking123})();124125// cached processed software specs126let SPEC: Record<SoftwareEnvNames, Readonly<SoftwareSpec> | null> = {} as any;127128async function getSoftwareSpec(name: SoftwareEnvNames): Promise<SoftwareSpec> {129const cached = SPEC[name];130if (cached != null) return cached;131const nextSpec: Partial<SoftwareSpec> = {};132const { inventory } = await getSoftwareInfo(name);133for (const cmd in inventory.language_exes) {134const info = inventory.language_exes[cmd];135if (nextSpec[info.lang] == null) {136nextSpec[info.lang] = {};137}138// use the full command as key to avoid basename collisions139nextSpec[info.lang][cmd] = {140cmd,141name: info.name,142doc: info.doc,143url: info.url,144path: info.path,145};146}147SPEC[name] = nextSpec as SoftwareSpec;148return nextSpec as SoftwareSpec;149}150151/**152* This determines the order of columns when there is more than on executable for a language.153*/154function getLanguageExecutables({ lang, inventory }): string[] {155if (inventory == null) return [];156return sortBy(keys(inventory[lang]), (x: string) => {157if (lang === "python") {158if (x.endsWith("python3")) return ["0", x];159if (x.indexOf("sage") >= 0) return ["2", x];160if (x.endsWith("python2")) return ["3", x];161return ["1", x]; // anaconda envs and others162} else {163return x.toLowerCase();164}165});166}167168// this is for the server side getServerSideProps function169export async function withCustomizedAndSoftwareSpec(170context,171lang: LanguageName | "executables",172) {173const { name } = context.params;174175// if name is not in SOFTWARE_ENV_NAMES, return {notFound : true}176if (!SOFTWARE_ENV_NAMES.includes(name)) {177return { notFound: true };178}179180const [customize, spec] = await Promise.all([181withCustomize({ context }),182getSoftwareSpec(name),183]);184185customize.props.name = name;186187if (lang === "executables") {188// this is instant because specs are already in the cache189const softwareInfo = await getSoftwareInfo(name);190const { inventory, timestamp } = softwareInfo;191customize.props.executablesSpec = inventory.executables;192customize.props.timestamp = timestamp;193return customize;194} else {195customize.props.spec = spec[lang];196// this is instant because specs are already in the cache197const { inventory, components, timestamp } = await getSoftwareInfoLang(198name,199lang,200);201customize.props.inventory = inventory;202customize.props.components = components;203customize.props.timestamp = timestamp;204}205206// at this point, lang != "executables"207// we gather the list of interpreters (executables) for the given language208const { inventory } = await getSoftwareInfo(name);209customize.props.execInfo = {};210for (const cmd of getLanguageExecutables({ inventory, lang })) {211const path = inventory.language_exes[cmd]?.path ?? cmd;212customize.props.execInfo[path] = inventory.executables?.[path] ?? null;213}214215return customize;216}217218219