acooliodude / portage-hx

Branch: d412a4e
Switch branches/tags
main
ACoolioDude Initial commit d412a4e 2d ago
662 lines · 22 KB Raw Blame History
1 import sys.FileSystem;
2 import sys.io.File;
3 import sys.io.Process;
4
5 typedef Entry = {
6 cat: String,
7 pkg: String,
8 ver: Version,
9 path: String,
10 repo: String,
11 deps: Array<String>
12 }
13
14 typedef Status = {
15 tag: String,
16 old: Null<String>
17 }
18
19 typedef Suggestion = {
20 label: String,
21 score: Int
22 }
23
24 class Main {
25 static var repos: Repository;
26 static var seen = new Map<String, Bool>();
27 static var plan: Array<Entry> = [];
28 static var useColor = Sys.getEnv("NO_COLOR") == null;
29 static var trace = Sys.getEnv("EMERGE_TRACE") != null;
30 static var targetRoot = "/";
31 static var showRepo = false;
32 static var update = false;
33 static var syncFlag = false;
34 static var syncOnly: Null<String> = null;
35 static var explainAtom: Null<String> = null;
36 static var vdb: VDB;
37 static var hostVdb: VDB;
38 static var chain: Array<String> = [];
39
40 static final PROGRESS_LABEL = "Calculating dependencies ";
41 static final SPIN_FRAMES = ["|", "/", "-", "\\"];
42 static final FRAME_SECONDS = 0.08;
43 static final MIN_SHOW_SECONDS = 0.6;
44 static var spinIndex = 0;
45 static var progressStart = 0.0;
46 static var lastFrame = 0.0;
47
48 static final NAME_RE = ~/^[a-zA-Z0-9][a-zA-Z0-9_+.-]*$/;
49 static final NON_CATEGORY_DIRS = ["profiles", "eclass", "metadata", "licenses", "scripts", "packages", "distfiles"];
50 static final MAX_SUGGESTIONS = 10;
51 static var indexCache: Null<Array<String>> = null;
52
53 static function c(code: String, s: String): String {
54 return useColor ? '\x1B[${code}m$s\x1B[0m' : s;
55 }
56
57 static function log(msg: String): Void {
58 if (!trace) return;
59 Sys.stderr().writeString(msg + "\n");
60 Sys.stderr().flush();
61 }
62
63 static function startProgress(): Void {
64 progressStart = haxe.Timer.stamp();
65 drawFrame();
66 }
67
68 static function drawFrame(): Void {
69 spinIndex = (spinIndex + 1) % SPIN_FRAMES.length;
70 Sys.print("\r" + PROGRESS_LABEL + SPIN_FRAMES[spinIndex]);
71 Sys.stdout().flush();
72 lastFrame = haxe.Timer.stamp();
73 }
74
75 static function spin(): Void {
76 if (haxe.Timer.stamp() - lastFrame < FRAME_SECONDS) return;
77 drawFrame();
78 }
79
80 static function finishProgress(): Void {
81 while (haxe.Timer.stamp() - progressStart < MIN_SHOW_SECONDS) {
82 drawFrame();
83 Sys.sleep(FRAME_SECONDS);
84 }
85 Sys.print("\r" + PROGRESS_LABEL + "... done!\n");
86 Sys.stdout().flush();
87 }
88
89 static function clearProgress(): Void {
90 Sys.print("\r" + StringTools.lpad("", " ", 40) + "\r");
91 Sys.stdout().flush();
92 }
93
94 static function warn(msg: String): Void {
95 clearProgress();
96 Sys.println("warning: " + msg);
97 }
98
99 static function isRoot(): Bool {
100 try {
101 var p = new Process("id", ["-u"]);
102 var out = StringTools.trim(p.stdout.readAll().toString());
103 p.close();
104 return out == "0";
105 } catch (e: Dynamic) {
106 return false;
107 }
108 }
109
110 static function packageIndex(): Array<String> {
111 if (indexCache != null) return indexCache;
112
113 var seenLabel = new Map<String, Bool>();
114 var out: Array<String> = [];
115 for (r in repos.list) {
116 if (!FileSystem.exists(r.tree.root)) continue;
117 for (cat in FileSystem.readDirectory(r.tree.root)) {
118 if (StringTools.startsWith(cat, ".") || NON_CATEGORY_DIRS.indexOf(cat) >= 0) continue;
119 var catDir = r.tree.root + "/" + cat;
120 if (!FileSystem.isDirectory(catDir)) continue;
121 for (pkg in FileSystem.readDirectory(catDir)) {
122 if (StringTools.startsWith(pkg, ".")) continue;
123 if (!FileSystem.isDirectory(catDir + "/" + pkg)) continue;
124 var label = cat + "/" + pkg;
125 if (seenLabel.exists(label)) continue;
126 seenLabel.set(label, true);
127 out.push(label);
128 }
129 }
130 }
131 out.sort(function(a, b) return a < b ? -1 : (a > b ? 1 : 0));
132 indexCache = out;
133 return out;
134 }
135
136 static function nameOf(label: String): String {
137 return label.substr(label.lastIndexOf("/") + 1);
138 }
139
140 static function levenshtein(a: String, b: String): Int {
141 var prev = [for (j in 0...b.length + 1) j];
142 for (i in 1...a.length + 1) {
143 var cur = [i];
144 for (j in 1...b.length + 1) {
145 var cost = a.charAt(i - 1) == b.charAt(j - 1) ? 0 : 1;
146 var del = prev[j] + 1;
147 var ins = cur[j - 1] + 1;
148 var sub = prev[j - 1] + cost;
149 cur.push(Std.int(Math.min(del, Math.min(ins, sub))));
150 }
151 prev = cur;
152 }
153 return prev[b.length];
154 }
155
156 static function resolveName(name: String): String {
157 var exact = [for (label in packageIndex()) if (nameOf(label) == name) label];
158 if (exact.length == 1) return exact[0];
159
160 if (exact.length > 1) {
161 Sys.println('\n"$name" is in more than one category. Use category/name to pick one:');
162 for (l in exact) Sys.println(" " + l);
163 Sys.exit(1);
164 }
165
166 var hits: Array<Suggestion> = [];
167 var limit = Std.int(Math.max(3, name.length / 3));
168 for (label in packageIndex()) {
169 var pkg = nameOf(label);
170 var score: Int;
171 if (pkg.indexOf(name) >= 0 || (pkg.length >= 3 && name.indexOf(pkg) >= 0)) {
172 score = 0;
173 } else {
174 score = levenshtein(name, pkg);
175 }
176 if (score <= limit) hits.push({label: label, score: score});
177 }
178 hits.sort(function(a, b) {
179 if (a.score != b.score) return a.score - b.score;
180 return a.label < b.label ? -1 : (a.label > b.label ? 1 : 0);
181 });
182
183 if (hits.length == 0) {
184 Sys.println('\nNo package named "$name" in any repository.');
185 Sys.exit(1);
186 }
187
188 Sys.println('\nNo package named "$name". Did you mean:');
189 var shown = hits.length < MAX_SUGGESTIONS ? hits.length : MAX_SUGGESTIONS;
190 for (i in 0...shown) Sys.println(" " + hits[i].label);
191 if (hits.length > shown) Sys.println(' ...and ${hits.length - shown} more');
192 Sys.exit(1);
193 return name;
194 }
195
196 static function installedSatisfies(a: Atom): Bool {
197 for (i in vdb.installed(a.cat, a.pkg)) {
198 if (AtomParser.matches(a, i.ver)) return true;
199 }
200 return false;
201 }
202
203 static function hostSatisfies(a: Atom): Bool {
204 for (i in hostVdb.installed(a.cat, a.pkg)) {
205 if (AtomParser.matches(a, i.ver)) return true;
206 }
207 return false;
208 }
209
210 static function resolve(a: Atom, label: String, top: Bool) {
211 if (installedSatisfies(a) && (update ? !upgradeAvailable(a) : !top)) {
212 log("installed, skipping " + label);
213 return;
214 }
215
216 spin();
217
218 var hit = repos.best(a);
219 var inst = vdb.installed(a.cat, a.pkg);
220
221 var targetVer: Version = null;
222 var targetRepo: String = null;
223 var targetPath: String = null;
224
225 if (hit != null) {
226 targetVer = hit.ver;
227 targetRepo = hit.repo.name;
228 targetPath = hit.repo.tree.ebuildPath(a.cat, a.pkg, hit.ver);
229 }
230
231 if (inst.length > 0) {
232 var topInst = inst[0];
233 for (i in inst) {
234 if (i.ver.compare(topInst.ver) > 0) topInst = i;
235 }
236
237 if (hit == null || topInst.ver.compare(hit.ver) >= 0) {
238 if (AtomParser.matches(a, topInst.ver)) {
239 var matchedRepo = null;
240 for (r in repos.list) {
241 if (r.name == topInst.repo) {
242 matchedRepo = r;
243 break;
244 }
245 }
246 if (matchedRepo == null && repos.list.length > 0) matchedRepo = repos.list[0];
247
248 if (matchedRepo != null) {
249 var p = matchedRepo.tree.ebuildPath(a.cat, a.pkg, topInst.ver);
250 if (FileSystem.exists(p)) {
251 targetVer = topInst.ver;
252 targetRepo = matchedRepo.name;
253 targetPath = p;
254 }
255 }
256 }
257 }
258 }
259
260 if (targetVer == null) {
261 if (hit == null) throw 'no ebuild matches "$label"';
262 targetVer = hit.ver;
263 targetRepo = hit.repo.name;
264 targetPath = hit.repo.tree.ebuildPath(a.cat, a.pkg, hit.ver);
265 }
266
267 var key = a.cat + "/" + a.pkg + "-" + targetVer.raw;
268 if (seen.exists(key)) return;
269 seen.set(key, true);
270
271 if (chain.length > 150) {
272 throw "dependency chain too deep: " + chain.slice(-10).join(" -> ") + " -> " + key;
273 }
274 chain.push(key);
275 log("resolve " + key);
276
277 var activeRepo = null;
278 for (r in repos.list) {
279 if (r.name == targetRepo) {
280 activeRepo = r;
281 break;
282 }
283 }
284 var tree = activeRepo != null ? activeRepo.tree : repos.list[0].tree;
285 var flags = UseFlags.forPackage(a.cat, a.pkg, tree.iuse(targetPath));
286
287 var runtime = Dependencies.eval(Dependencies.parse(tree.runtimeSpec(targetPath)), flags, chooser);
288
289 var toResolve = runtime.copy();
290 var build = Dependencies.eval(Dependencies.parse(tree.buildSpec(targetPath)), flags, chooser);
291 for (d in build) {
292 if (targetRoot == "/") {
293 toResolve.push(d);
294 } else if (!hostSatisfies(d)) {
295 warn('build dependency not installed on host: ${d.cat}/${d.pkg}');
296 }
297 }
298
299 var depLabels: Array<String> = [];
300 for (d in toResolve) {
301 var dl = d.cat + "/" + d.pkg;
302 depLabels.push(dl);
303 resolve(d, dl, false);
304 }
305
306 chain.pop();
307 plan.push({cat: a.cat, pkg: a.pkg, ver: targetVer, path: targetPath, repo: targetRepo, deps: depLabels});
308 }
309
310 static function upgradeAvailable(a: Atom): Bool {
311 var hit = repos.best(a);
312 if (hit == null) return false;
313 for (i in vdb.installed(a.cat, a.pkg)) {
314 if (i.ver.compare(hit.ver) >= 0) return false;
315 }
316 return true;
317 }
318
319 static function syncRepos(only: Null<String>): Bool {
320 Sys.println(c("1;32", only == null ? ">>> Syncing repositories ..." : ">>> Syncing " + only + " ..."));
321 var ok = Sync.run(only);
322 if (!ok) {
323 Sys.println(c("1;31", ">>> Sync finished with errors"));
324 return false;
325 }
326 Sys.println(c("1;32", ">>> Sync complete."));
327 return true;
328 }
329
330 static function chooser(alts: Array<Array<Atom>>): Int {
331 var firstResolvable = -1;
332 for (i in 0...alts.length) {
333 var resolvable = alts[i].length > 0;
334 var installed = alts[i].length > 0;
335 for (at in alts[i]) {
336 if (repos.best(at) == null) {
337 resolvable = false;
338 break;
339 }
340 if (vdb.installed(at.cat, at.pkg).length == 0) installed = false;
341 }
342 if (!resolvable) continue;
343 if (installed) return i;
344 if (firstResolvable < 0) firstResolvable = i;
345 }
346 return firstResolvable < 0 ? 0 : firstResolvable;
347 }
348
349 static function status(vdb: VDB, e: Entry): Status {
350 var inst = vdb.installed(e.cat, e.pkg);
351 if (inst.length == 0) return {tag: "N", old: null};
352
353 var top = inst[0];
354 for (i in inst) {
355 if (i.ver.compare(top.ver) > 0) top = i;
356 }
357
358 var cmp = e.ver.compare(top.ver);
359 var oldStr = top.ver.raw + "::" + top.repo;
360 if (cmp == 0) return {tag: "R", old: null};
361 if (cmp > 0) return {tag: "U", old: oldStr};
362 return {tag: "UD", old: oldStr};
363 }
364
365 static function showPlan(vdb: VDB, verbose: Bool) {
366 if (plan.length == 0) {
367 Sys.println(c("1;32", "Nothing to merge; all requested packages are installed."));
368 return;
369 }
370
371 Sys.println(c("1;32", "These are the packages that would be merged, in order:"));
372 Sys.println("");
373
374 var counts = new Map<String, Int>();
375 for (e in plan) {
376 var st = status(vdb, e);
377 counts.set(st.tag, (counts.exists(st.tag) ? counts.get(st.tag) : 0) + 1);
378
379 var tagCol = switch st.tag {
380 case "N": "32";
381 case "U": "34";
382 case "UD": "35";
383 default: "33";
384 };
385
386 var line = c("1;32", "[ebuild ")
387 + c(tagCol, " " + StringTools.rpad(st.tag, " ", 4) + " ")
388 + c("1;32", "]")
389 + " " + c("1;37", e.cat + "/" + e.pkg + "-" + e.ver.raw)
390 + (showRepo ? c("0;36", "::" + e.repo) : "");
391
392 if (st.old != null) {
393 line += " " + c("0;36", "[" + st.old + "]");
394 }
395 Sys.println(line);
396 }
397
398 Sys.println("");
399 var parts: Array<String> = [];
400 if (counts.exists("U")) parts.push(counts.get("U") + " upgrade");
401 if (counts.exists("UD")) parts.push(counts.get("UD") + " downgrade");
402 if (counts.exists("N")) parts.push(counts.get("N") + " new");
403 if (counts.exists("R")) parts.push(counts.get("R") + " reinstall");
404
405 var plural = plan.length == 1 ? "" : "s";
406 Sys.println("Total: " + plan.length + " package" + plural + " (" + parts.join(", ") + ")");
407 }
408
409 static function explain(spec: String): Void {
410 var a = AtomParser.parse(spec);
411 if (a == null) {
412 Sys.println("invalid atom: " + spec);
413 return;
414 }
415
416 var hit = repos.best(a);
417 var chosen: Null<Version> = hit == null ? null : hit.ver;
418
419 Sys.println("ACCEPT_KEYWORDS: " + Keywords.active().join(" "));
420 for (r in repos.list) {
421 var vs = r.tree.versions(a.cat, a.pkg);
422 vs.sort(function(x, y) return y.compare(x));
423 for (v in vs) {
424 var kw = r.tree.variable(r.tree.ebuildPath(a.cat, a.pkg, v), "KEYWORDS");
425 var masked = Mask.isMasked(a.cat, a.pkg, v);
426 var kwOk = Keywords.acceptsWith(kw, Mask.extraKeywords(a.cat, a.pkg, v));
427 var picked = chosen != null && chosen.compare(v) == 0;
428 Sys.println(" " + a.cat + "/" + a.pkg + "-" + v.raw + "::" + r.name
429 + " KEYWORDS=\"" + kw + "\""
430 + " masked=" + masked
431 + " keywords-ok=" + kwOk
432 + (picked ? " <- picked" : ""));
433 }
434 }
435 }
436
437 static function expandSets(list: Array<String>): Array<String> {
438 var out: Array<String> = [];
439 for (arg in list) {
440 if (arg == "@world") {
441 for (a in readWorld()) out.push(a);
442 } else {
443 out.push(arg);
444 }
445 }
446 return out;
447 }
448
449 static function readWorld(): Array<String> {
450 var base = StringTools.endsWith(targetRoot, "/") ? targetRoot.substr(0, targetRoot.length - 1) : targetRoot;
451 var path = base + "/var/lib/portage/world";
452 if (!FileSystem.exists(path)) {
453 Sys.println('no world file at ' + path);
454 Sys.exit(1);
455 }
456
457 var out: Array<String> = [];
458 for (raw in File.getContent(path).split("\n")) {
459 var line = StringTools.trim(raw);
460 if (line == "" || StringTools.startsWith(line, "#")) continue;
461 if (AtomParser.parse(line) == null) {
462 Sys.println('warning: skipping world entry "' + line + '"');
463 continue;
464 }
465 out.push(line);
466 }
467 return out;
468 }
469
470 static function fullLabel(e: Entry): String {
471 var base = e.cat + "/" + e.pkg + "-" + e.ver.raw;
472 return showRepo ? base + "::" + e.repo : base;
473 }
474
475 static function expandShortFlags(args: Array<String>): Array<String> {
476 var bundled = new EReg("^-[a-zA-Z]{2,}$", "");
477 var out: Array<String> = [];
478 for (arg in args) {
479 if (bundled.match(arg)) {
480 for (i in 1...arg.length) out.push("-" + arg.charAt(i));
481 } else {
482 out.push(arg);
483 }
484 }
485 return out;
486 }
487
488 static function mergeOne(e: Entry, count: Int, total: Int): Bool {
489 var pos = "(" + count + " of " + total + ") ";
490
491 Sys.println("");
492 Sys.println(c("1;32", ">>> Emerging " + pos) + c("1;37", fullLabel(e)));
493
494 var build = Sys.command("ebuild", [e.path, "clean", "unpack", "prepare", "configure", "compile", "install"]);
495 if (build != 0) {
496 Sys.println(c("1;31", ">>> Failed " + pos + fullLabel(e) + " (build exit " + build + ")"));
497 return false;
498 }
499
500 Sys.println(c("1;32", ">>> Installing " + pos) + c("1;37", fullLabel(e)));
501
502 var merge = Sys.command("ebuild", [e.path, "qmerge"]);
503 if (merge != 0) {
504 Sys.println(c("1;31", ">>> Failed " + pos + fullLabel(e) + " (merge exit " + merge + ")"));
505 return false;
506 }
507
508 Sys.println(c("1;32", ">>> Completed " + pos) + c("1;37", fullLabel(e)));
509 return true;
510 }
511
512 static function main() {
513 repos = Repository.load();
514
515 var pretend = false;
516 var ask = false;
517 var atoms: Array<String> = [];
518 var searchQuery: Null<String> = null;
519 var searchDesc = false;
520 var verbose = false;
521
522 var args = expandShortFlags(Sys.args());
523 var idx = 0;
524 while (idx < args.length) {
525 var arg = args[idx++];
526 if (StringTools.startsWith(arg, "--sync=")) {
527 syncFlag = true;
528 syncOnly = arg.substr("--sync=".length);
529 continue;
530 }
531 switch arg {
532 case "--pretend" | "-p": pretend = true;
533 case "--ask" | "-a": ask = true;
534 case "--verbose" | "-v": verbose = true;
535 case "--update" | "-u": update = true;
536 case "--sync":
537 syncFlag = true;
538 if (idx < args.length && Sync.repoNames().indexOf(args[idx]) >= 0) {
539 syncOnly = args[idx++];
540 }
541 case "--explain":
542 if (idx < args.length) explainAtom = args[idx++];
543 case "--search" | "-s":
544 if (idx >= args.length) {
545 Sys.println("--search needs a query");
546 Sys.exit(1);
547 }
548 searchQuery = args[idx++];
549 case "--desc" | "-S": searchDesc = true;
550 case "--root":
551 if (idx >= args.length) {
552 Sys.println("--root needs a directory");
553 Sys.exit(1);
554 }
555 targetRoot = args[idx++];
556 default: atoms.push(arg);
557 }
558 }
559
560 showRepo = verbose;
561
562 if (searchQuery != null) {
563 Search.run(repos, searchQuery, searchDesc);
564 return;
565 }
566
567 if (explainAtom != null) {
568 explain(explainAtom);
569 return;
570 }
571
572 atoms = expandSets(atoms);
573
574 if (syncFlag) {
575 if (!isRoot()) {
576 Sys.println("emerge-hx: superuser access is required for --sync");
577 Sys.exit(1);
578 }
579 if (!syncRepos(syncOnly)) Sys.exit(1);
580 indexCache = null;
581 repos = Repository.load();
582 if (atoms.length == 0) return;
583 }
584
585 if (atoms.length == 0) {
586 Sys.println("usage: emerge-hx [--sync[=REPO]] [-u] [--pretend | --ask] [--root DIR] <atom> [atom...]");
587 Sys.println(" emerge-hx --search <query> [--desc]");
588 Sys.exit(1);
589 }
590
591 if (!pretend && !isRoot()) {
592 if (!ask) {
593 Sys.println("emerge-hx: superuser access is required");
594 Sys.exit(1);
595 }
596 Sys.println("This action requires superuser access...");
597 Sys.print("Would you like to add --pretend to options? [Yes/No] ");
598 var choice = "";
599 try {
600 choice = Sys.stdin().readLine().toLowerCase();
601 } catch (e: Dynamic) {
602 choice = "";
603 }
604 if (choice != "y" && choice != "yes") {
605 Sys.println("Aborted.");
606 Sys.exit(1);
607 }
608 pretend = true;
609 }
610
611 vdb = new VDB(targetRoot);
612 hostVdb = new VDB("/");
613
614 startProgress();
615
616 for (arg in atoms) {
617 var spec = NAME_RE.match(arg) ? resolveName(arg) : arg;
618
619 var a = AtomParser.parse(spec);
620 if (a == null) {
621 clearProgress();
622 Sys.println('invalid atom: $arg');
623 Sys.exit(1);
624 }
625 try {
626 resolve(a, spec, true);
627 } catch (e: Dynamic) {
628 clearProgress();
629 Sys.println('error: $e');
630 Sys.exit(1);
631 }
632 }
633
634 finishProgress();
635 Sys.println("");
636
637 showPlan(vdb, verbose);
638
639 if (pretend || plan.length == 0) return;
640
641 if (ask) {
642 Sys.print("\nWould you like to merge these packages? [Yes/No] ");
643 var answer = "";
644 try {
645 answer = Sys.stdin().readLine();
646 } catch (e: Dynamic) {
647 answer = "";
648 }
649 answer = answer.toLowerCase();
650 if (answer != "y" && answer != "yes") {
651 Sys.println("Aborted.");
652 return;
653 }
654 }
655
656 Sys.putEnv("ROOT", targetRoot);
657 var total = plan.length;
658 for (n in 0...total) {
659 if (!mergeOne(plan[n], n + 1, total)) Sys.exit(1);
660 }
661 }
662 }