Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/next/lib/landing/software-specs.ts
5787 views
1
/*
2
* This file is part of CoCalc: Copyright © 2021 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
import { keys, map, sortBy, zipObject } from "lodash";
7
import { promises } from "node:fs";
8
9
import {
10
SOFTWARE_ENV_NAMES,
11
SoftwareEnvNames,
12
} from "@cocalc/util/consts/software-envs";
13
import { hours_ago } from "@cocalc/util/relative-time";
14
import { reuseInFlight } from "@cocalc/util/reuse-in-flight";
15
import withCustomize from "lib/with-customize";
16
import { SOFTWARE_FALLBACK, SOFTWARE_URLS } from "./software-data";
17
import {
18
ComputeComponents,
19
ComputeInventory,
20
EnvData,
21
LanguageName,
22
SoftwareSpec,
23
} from "./types";
24
25
const { readFile } = promises;
26
27
async function makeObject(keys, fn) {
28
return zipObject(keys, await Promise.all(map(keys, fn)));
29
}
30
31
type SoftwareEnvironments = { [key in SoftwareEnvNames]: EnvData };
32
33
let SoftwareEnvSpecs: SoftwareEnvironments | null = null;
34
let SoftwareEnvDownloadedTimestamp: number = 0;
35
36
async function file2json(path: string): Promise<any> {
37
const data = await readFile(path, "utf8");
38
return JSON.parse(data);
39
}
40
41
async function downloadInventoryJson(name: SoftwareEnvNames): Promise<EnvData> {
42
try {
43
const raw = await fetch(SOFTWARE_URLS[name]);
44
if (!raw.ok) {
45
console.log(`Problem downloading: ${raw.status}: ${raw.statusText}`);
46
} else {
47
const data = await raw.json();
48
console.log(`Downloaded software inventory ${name} successfully`);
49
return data;
50
}
51
} catch (err) {
52
console.log(`Problem downloading: ${err}`);
53
}
54
return SOFTWARE_FALLBACK[name] as EnvData;
55
}
56
57
// load the current version of the software specs – if there is a problem, use the locally stored files as fallback.
58
async function fetchInventory(): Promise<SoftwareEnvironments> {
59
// for development, set the env variable to directory, where this files are
60
const localSpec = process.env.COCALC_SOFTWARE_ENVIRONMENTS;
61
if (localSpec != null) {
62
// read compute-inventory.json and compute-components.json from the local filesystem
63
console.log(`Reading inventory information from directory ${localSpec}`);
64
return await makeObject(
65
SOFTWARE_ENV_NAMES,
66
async (name) =>
67
await file2json(`${localSpec}/software-inventory-${name}.json`),
68
);
69
}
70
try {
71
// download the files for the newest information from the server
72
const ret = await makeObject(
73
SOFTWARE_ENV_NAMES,
74
async (name) => await downloadInventoryJson(name),
75
);
76
return ret;
77
} catch (err) {
78
console.error(`Problem fetching software inventory: ${err}`);
79
return SOFTWARE_FALLBACK;
80
}
81
}
82
83
const fetchSoftwareSpec = reuseInFlight(async function () {
84
SoftwareEnvSpecs = await fetchInventory();
85
SoftwareEnvDownloadedTimestamp = Date.now();
86
return SoftwareEnvSpecs;
87
});
88
89
/**
90
* get a cached copy of the software specs
91
*/
92
async function getSoftwareInfo(name: SoftwareEnvNames): Promise<EnvData> {
93
// if SoftwareEnvSpecs is not set or not older than one hour, fetch it
94
if (SoftwareEnvSpecs != null) {
95
if (SoftwareEnvDownloadedTimestamp > hours_ago(1).getTime()) {
96
// fresh enough, just return it
97
return SoftwareEnvSpecs[name];
98
} else {
99
// we asynchroneously fetch to refresh, but return the data immediately to the client
100
fetchSoftwareSpec();
101
return SoftwareEnvSpecs[name];
102
}
103
} else {
104
const specs = await fetchSoftwareSpec();
105
return specs[name];
106
}
107
}
108
109
async function getSoftwareInfoLang(
110
name: SoftwareEnvNames,
111
lang: LanguageName,
112
): Promise<{
113
inventory: ComputeInventory[LanguageName];
114
components: ComputeComponents[LanguageName];
115
timestamp: string;
116
}> {
117
const { inventory, data, timestamp } = await getSoftwareInfo(name);
118
return { inventory: inventory[lang], components: data[lang], timestamp };
119
}
120
121
// during startup, we fetch getSoftwareSpec() once to warm up the cache…
122
(async function () {
123
fetchSoftwareSpec(); // not blocking
124
})();
125
126
// cached processed software specs
127
let SPEC: Record<SoftwareEnvNames, Readonly<SoftwareSpec> | null> = {} as any;
128
129
async function getSoftwareSpec(name: SoftwareEnvNames): Promise<SoftwareSpec> {
130
const cached = SPEC[name];
131
if (cached != null) return cached;
132
const nextSpec: Partial<SoftwareSpec> = {};
133
const { inventory } = await getSoftwareInfo(name);
134
for (const cmd in inventory.language_exes) {
135
const info = inventory.language_exes[cmd];
136
if (nextSpec[info.lang] == null) {
137
nextSpec[info.lang] = {};
138
}
139
// use the full command as key to avoid basename collisions
140
nextSpec[info.lang][cmd] = {
141
cmd,
142
name: info.name,
143
doc: info.doc,
144
url: info.url,
145
path: info.path,
146
};
147
}
148
SPEC[name] = nextSpec as SoftwareSpec;
149
return nextSpec as SoftwareSpec;
150
}
151
152
/**
153
* This determines the order of columns when there is more than on executable for a language.
154
*/
155
function getLanguageExecutables({ lang, inventory }): string[] {
156
if (inventory == null) return [];
157
return sortBy(keys(inventory[lang]), (x: string) => {
158
if (lang === "python") {
159
if (x.endsWith("python3")) return ["0", x];
160
if (x.indexOf("sage") >= 0) return ["2", x];
161
if (x.endsWith("python2")) return ["3", x];
162
return ["1", x]; // anaconda envs and others
163
} else {
164
return x.toLowerCase();
165
}
166
});
167
}
168
169
// this is for the server side getServerSideProps function
170
export async function withCustomizedAndSoftwareSpec(
171
context,
172
lang: LanguageName | "executables",
173
) {
174
const { name } = context.params;
175
176
// if name is not in SOFTWARE_ENV_NAMES, return {notFound : true}
177
if (!SOFTWARE_ENV_NAMES.includes(name)) {
178
return { notFound: true };
179
}
180
181
const [customize, spec] = await Promise.all([
182
withCustomize({ context }),
183
getSoftwareSpec(name),
184
]);
185
186
customize.props.name = name;
187
188
if (lang === "executables") {
189
// this is instant because specs are already in the cache
190
const softwareInfo = await getSoftwareInfo(name);
191
const { inventory, timestamp } = softwareInfo;
192
customize.props.executablesSpec = inventory.executables;
193
customize.props.timestamp = timestamp;
194
return customize;
195
} else {
196
customize.props.spec = spec[lang];
197
// this is instant because specs are already in the cache
198
const { inventory, components, timestamp } = await getSoftwareInfoLang(
199
name,
200
lang,
201
);
202
customize.props.inventory = inventory;
203
customize.props.components = components;
204
customize.props.timestamp = timestamp;
205
}
206
207
// at this point, lang != "executables"
208
// we gather the list of interpreters (executables) for the given language
209
const { inventory } = await getSoftwareInfo(name);
210
customize.props.execInfo = {};
211
for (const cmd of getLanguageExecutables({ inventory, lang })) {
212
const path = inventory.language_exes[cmd]?.path ?? cmd;
213
customize.props.execInfo[path] = inventory.executables?.[path] ?? null;
214
}
215
216
return customize;
217
}
218
219