-
-
Notifications
You must be signed in to change notification settings - Fork 45
Expand file tree
/
Copy pathgenerate-db.js
More file actions
executable file
·281 lines (249 loc) · 9.62 KB
/
generate-db.js
File metadata and controls
executable file
·281 lines (249 loc) · 9.62 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
#!/usr/bin/env node
import { glob, readFile, writeFile } from "node:fs/promises";
import { gunzip } from "node:zlib";
import { join } from "node:path";
import { promisify } from "node:util";
const gunzipAsync = promisify(gunzip);
const { TERMUX_SCRIPTDIR, TERMUX_PREFIX, TERMUX_ARCH } = process.env;
if (!TERMUX_SCRIPTDIR) {
throw new Error("TERMUX_SCRIPTDIR environment variable is not defined");
}
if (!TERMUX_PREFIX) {
throw new Error("TERMUX_PREFIX environment variable is not defined");
}
if (!TERMUX_ARCH) {
throw new Error("TERMUX_ARCH environment variable is not defined");
}
const binPrefix = TERMUX_PREFIX.substring(1) + "/bin/";
const repos = JSON.parse(await readFile(join(TERMUX_SCRIPTDIR, "repo.json")));
/**
* Parses an alternative file and returns an array of alternative entries.
*
* The parsing isn't strict, and doesn't report errors. It may unintentionally throw errors if the file is malformed.
*
* Each entry contains:
* - `name`: The name of the alternative.
* - `link`: The link to the alternative.
* - `alternative`: The alternative path.
* - `dependents`: An array of dependents, each with `link`, `name`, and `path`. This is the list of slaves of the alternative. If there is no dependents, it'll be an empty array for consistency
* - `priority`: The priority of the alternative.
*
* Note that both the name and path do not start with TERMUX_PREFIX, but instead start with the relative path from TERMUX_PREFIX.
*/
async function parseAlternativeFile(filePath) {
const content = await readFile(filePath, "utf8");
let name = undefined;
let link = undefined;
let alternative = undefined;
let dependents = undefined;
let priority = undefined;
let parsingDependents = false;
const alternatives = [];
for (let line of content.split("\n")) {
// Remove trailing comment
// Comment starts with a '#' and can be at the end of the line as well
let match = line.match(/\s*#.*/);
line = line.substring(0, match === null ? line.length : match.index);
if (line.startsWith("Name: ")) {
if (parsingDependents) {
parsingDependents = false;
}
// We already had a alternative entry, so push what we have parsed so far as an alternative entry
if (name !== undefined) {
alternatives.push({
name: name,
link: link,
alternative: alternative,
dependents: dependents === undefined ? [] : dependents,
priority: parseInt(priority),
});
name = undefined;
link = undefined;
alternative = undefined;
dependents = undefined;
priority = undefined;
}
name = line.substring("Name: ".length).trim();
}
if (line.startsWith("Link: ")) {
parsingDependents = false;
link = line.substring("Link: ".length).trim();
}
if (line.startsWith("Alternative: ")) {
parsingDependents = false;
alternative = line.substring("Alternative: ".length).trim();
}
if (line.startsWith("Priority: ")) {
parsingDependents = false;
priority = line.substring("Priority: ".length).trim();
}
if (line.startsWith("Dependents:")) {
parsingDependents = true;
}
// Parse the dependents entry here
if (parsingDependents) {
line = line.trim();
// We have not parsed any dependents yet, so we initialize the dependents array
if (dependents === undefined) {
dependents = [];
}
// We trim the line to remove the leading indentation
// The line should be in the format: "->link name path"
// "->" is the leading indent
// We use the regex \s+ to split the line into parts since the there can
// be multiple spaces used for separating the parts for enhancing readibility
const [dependentLink, dependentName, dependentPath] = line
.trim()
.split(/\s+/);
if (dependentLink && dependentName && dependentPath) {
dependents.push({
link: dependentLink,
name: dependentName,
path: dependentPath,
});
}
}
}
// After parsing the entire file, if we have a name, this means this is the
// final entry. So push it as well
if (name !== undefined) {
alternatives.push({
name: name,
link: link,
alternative: alternative,
dependents: dependents === undefined ? [] : dependents,
priority: priority,
});
}
return alternatives;
}
async function fetchURL(url) {
if (url.startsWith("file:///")) {
return await readFile(url.substring('file://'.length));
}
const response = await fetch(url);
if (!response.ok) {
throw new Error(`${url} returned ${response.status}`);
}
return await response.arrayBuffer();
}
async function processRepo(repo, repoPath, arch) {
// Fetch the Contents.gz file for the given architecture from the apt mirror
const url = `${repo.url}/dists/${repo.distribution}/Contents-${arch}.gz`;
const response = await fetchURL(url);
// Since we are using a gzip file, we need to decompress it
const data = await gunzipAsync(await response);
// Convert to string and split by new lines
// Each line is of the format:
// "path/to/file package"
//
// Where `path/to/file` is the path to the file in the package, and `package`
// is the name of the package that provides this file.
const lines = data.toString().split("\n");
// Stores mappings of binary names to package names
// The key is the binary name, and the value is an array of package names
// that provide this binary
const binMap = new Map();
// Stores mappings of file paths to package names
// This is needed to resolve the package names for binaries that are setup
// using the alternatives system
const fileMap = new Map();
// Populate the fileMap
lines.forEach((line) => {
const [path, packageName] = line.split(" ");
fileMap.set(path, packageName);
});
// Now filter the entries from Contents.gz that have binaries, and store them
// in binMap
lines
.filter((line) => line.startsWith(binPrefix))
.forEach((line) => {
const [pathToBinary, packageNames] = line.split(" ");
const binary = pathToBinary.substring(pathToBinary.lastIndexOf("/") + 1);
const packages = packageNames.split(",");
packages.forEach((packageName) => {
if (!binMap.has(packageName)) {
binMap.set(packageName, []);
}
binMap.get(packageName).push(binary);
});
});
// Now go through all the *.alternatives files in the repository and parse
// them to find the alternatives and their dependents
repoPath = join(TERMUX_SCRIPTDIR, repoPath);
for await (const file of glob(`${repoPath}/*/*.alternatives`, {
nodir: true,
})) {
const alternatives = await parseAlternativeFile(file);
alternatives.forEach((alternativeEntry) => {
let packageName = file.substring(repoPath.length + 1);
packageName = packageName.substring(0, packageName.indexOf("/"));
if (alternativeEntry.link.startsWith("bin/")) {
const path = alternativeEntry.alternative;
const binary = alternativeEntry.link.substring(
alternativeEntry.link.lastIndexOf("/") + 1,
);
const packageName = fileMap.get(join(TERMUX_PREFIX.substring(1), path));
// This could happen when there exists alternative file but the package itself doesn't exist for some of the architecture.
// This is needed for the openjdk-25 package.
// Keep the warning as it might be also due to some other inconsistency in the alternatives file as well. So having it in the logs should be nice
if (packageName === undefined) {
console.warn(
`WARNING: Package name not found for path: ${path}. Skipping...`,
);
return;
}
if (!binMap.has(packageName)) {
binMap.set(packageName, []);
}
binMap.get(packageName).push(binary);
alternativeEntry.dependents.forEach(({ link, name: _, path }) => {
if (link.startsWith("bin/")) {
const depPackageName = fileMap.get(
join(TERMUX_PREFIX.substring(1), path),
);
const depBinary = link.substring(link.lastIndexOf("/") + 1);
if (!binMap.has(depPackageName)) {
binMap.set(depPackageName, []);
}
binMap.get(depPackageName).push(depBinary);
}
// Register the link in the fileMap for the package
// This is used by vim.alternatives where bin/vim is a link with alternative libexec/vim/vim
// and bin/editor is a link with alternative bin/vim
fileMap.set(join(TERMUX_PREFIX.substring(1), link), packageName);
if (!binMap.has(packageName)) {
binMap.set(packageName, []);
}
binMap.get(packageName).push(binary);
});
}
// Register the link in the fileMap for the package
// This is used by vim.alternatives where bin/vim is a link with alternative libexec/vim/vim
// and bin/editor is a link with alternative bin/vim
fileMap.set(
join(TERMUX_PREFIX.substring(1), alternativeEntry.link),
packageName,
);
});
}
const headerFile = `commands-${arch}-${repo.name}.h`;
const header = Array.from(binMap.keys())
.sort()
.map((packageName) => {
const binaries = binMap
.get(packageName)
.sort()
.map((bin) => `" ${bin}",`);
return `"${packageName}",\n${binaries.join("\n")}`;
})
.join("\n");
await writeFile(headerFile, header);
}
const promises = [];
for (const path in repos) {
if (path === "pkg_format") continue;
const repo = repos[path];
promises.push(processRepo(repo, path, TERMUX_ARCH));
}
await Promise.all(promises);