acooliodude / portage-hx

Branch: main
Switch branches/tags
main
ACoolioDude Push x4 37a3424 1d ago
1189 lines · 42.7 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 slot: String,
12 deps: Array<String>,
13 subslot: String,
14 use: String
15 }
16
17 typedef Status = {
18 tag: String,
19 old: Null<String>
20 }
21
22 typedef Candidate = {
23 ver: Version,
24 name: String,
25 tree: Tree
26 }
27
28 typedef Suggestion = {
29 label: String,
30 score: Int
31 }
32
33 class Main {
34 static var repos: Repository;
35 static var seen = new Map<String, Bool>();
36 static var plan: Array<Entry> = [];
37 static var useColor = Sys.getEnv("NO_COLOR") == null;
38 static var trace = Sys.getEnv("EMERGE_TRACE") != null;
39 static var targetRoot = "/";
40 static var showRepo = false;
41 static var update = false;
42 static var deep = false;
43 static var unmergeMode = false;
44 static var depclean = false;
45 static var quiet = false;
46 static var syncFlag = false;
47 static var syncOnly: Null<String> = null;
48 static var explainAtom: Null<String> = null;
49 static var vdb: VDB;
50 static var hostVdb: VDB;
51 static var chain: Array<String> = [];
52
53 static final PROGRESS_LABEL = "Calculating dependencies ";
54 static final SPIN_FRAMES = ["|", "/", "-", "\\"];
55 static final FRAME_SECONDS = 0.08;
56 static final MIN_SHOW_SECONDS = 0.6;
57 static var spinIndex = 0;
58 static var progressStart = 0.0;
59 static var lastFrame = 0.0;
60
61 static final NAME_RE = ~/^[a-zA-Z0-9][a-zA-Z0-9_+.-]*$/;
62 static final NON_CATEGORY_DIRS = ["profiles", "eclass", "metadata", "licenses", "scripts", "packages", "distfiles"];
63 static final MAX_SUGGESTIONS = 10;
64 static var indexCache: Null<Array<String>> = null;
65
66 static function c(code: String, s: String): String {
67 return useColor ? '\x1B[${code}m$s\x1B[0m' : s;
68 }
69
70 static function log(msg: String): Void {
71 if (!trace) return;
72 Sys.stderr().writeString(msg + "\n");
73 Sys.stderr().flush();
74 }
75
76 static function startProgress(): Void {
77 progressStart = haxe.Timer.stamp();
78 drawFrame();
79 }
80
81 static function drawFrame(): Void {
82 spinIndex = (spinIndex + 1) % SPIN_FRAMES.length;
83 Sys.print("\r" + PROGRESS_LABEL + SPIN_FRAMES[spinIndex]);
84 Sys.stdout().flush();
85 lastFrame = haxe.Timer.stamp();
86 }
87
88 static function spin(): Void {
89 if (haxe.Timer.stamp() - lastFrame < FRAME_SECONDS) return;
90 drawFrame();
91 }
92
93 static function finishProgress(): Void {
94 while (haxe.Timer.stamp() - progressStart < MIN_SHOW_SECONDS) {
95 drawFrame();
96 Sys.sleep(FRAME_SECONDS);
97 }
98 Sys.print("\r" + PROGRESS_LABEL + "... done!\n");
99 Sys.stdout().flush();
100 }
101
102 static function clearProgress(): Void {
103 Sys.print("\r" + StringTools.lpad("", " ", 40) + "\r");
104 Sys.stdout().flush();
105 }
106
107 static function warn(msg: String): Void {
108 clearProgress();
109 Sys.println("warning: " + msg);
110 }
111
112 static function isRoot(): Bool {
113 try {
114 var p = new Process("id", ["-u"]);
115 var out = StringTools.trim(p.stdout.readAll().toString());
116 p.close();
117 return out == "0";
118 } catch (e: Dynamic) {
119 return false;
120 }
121 }
122
123 static function packageIndex(): Array<String> {
124 if (indexCache != null) return indexCache;
125
126 var seenLabel = new Map<String, Bool>();
127 var out: Array<String> = [];
128 for (r in repos.list) {
129 if (!FileSystem.exists(r.tree.root)) continue;
130 for (cat in FileSystem.readDirectory(r.tree.root)) {
131 if (StringTools.startsWith(cat, ".") || NON_CATEGORY_DIRS.indexOf(cat) >= 0) continue;
132 var catDir = r.tree.root + "/" + cat;
133 if (!FileSystem.isDirectory(catDir)) continue;
134 for (pkg in FileSystem.readDirectory(catDir)) {
135 if (StringTools.startsWith(pkg, ".")) continue;
136 if (!FileSystem.isDirectory(catDir + "/" + pkg)) continue;
137 var label = cat + "/" + pkg;
138 if (seenLabel.exists(label)) continue;
139 seenLabel.set(label, true);
140 out.push(label);
141 }
142 }
143 }
144 out.sort(function(a, b) return a < b ? -1 : (a > b ? 1 : 0));
145 indexCache = out;
146 return out;
147 }
148
149 static function nameOf(label: String): String {
150 return label.substr(label.lastIndexOf("/") + 1);
151 }
152
153 static function levenshtein(a: String, b: String): Int {
154 var prev = [for (j in 0...b.length + 1) j];
155 for (i in 1...a.length + 1) {
156 var cur = [i];
157 for (j in 1...b.length + 1) {
158 var cost = a.charAt(i - 1) == b.charAt(j - 1) ? 0 : 1;
159 var del = prev[j] + 1;
160 var ins = cur[j - 1] + 1;
161 var sub = prev[j - 1] + cost;
162 cur.push(Std.int(Math.min(del, Math.min(ins, sub))));
163 }
164 prev = cur;
165 }
166 return prev[b.length];
167 }
168
169 static function resolveName(name: String): String {
170 var exact = [for (label in packageIndex()) if (nameOf(label) == name) label];
171 if (exact.length == 1) return exact[0];
172
173 if (exact.length > 1) {
174 Sys.println('\n"$name" is in more than one category. Use category/name to pick one:');
175 for (l in exact) Sys.println(" " + l);
176 Sys.exit(1);
177 }
178
179 var hits: Array<Suggestion> = [];
180 var limit = Std.int(Math.max(3, name.length / 3));
181 for (label in packageIndex()) {
182 var pkg = nameOf(label);
183 var score: Int;
184 if (pkg.indexOf(name) >= 0 || (pkg.length >= 3 && name.indexOf(pkg) >= 0)) {
185 score = 0;
186 } else {
187 score = levenshtein(name, pkg);
188 }
189 if (score <= limit) hits.push({label: label, score: score});
190 }
191 hits.sort(function(a, b) {
192 if (a.score != b.score) return a.score - b.score;
193 return a.label < b.label ? -1 : (a.label > b.label ? 1 : 0);
194 });
195
196 if (hits.length == 0) {
197 Sys.println('\nNo package named "$name" in any repository.');
198 Sys.exit(1);
199 }
200
201 Sys.println('\nNo package named "$name". Did you mean:');
202 var shown = hits.length < MAX_SUGGESTIONS ? hits.length : MAX_SUGGESTIONS;
203 for (i in 0...shown) Sys.println(" " + hits[i].label);
204 if (hits.length > shown) Sys.println(' ...and ${hits.length - shown} more');
205 Sys.exit(1);
206 return name;
207 }
208
209 static function installedSatisfies(a: Atom): Bool {
210 for (i in vdb.installed(a.cat, a.pkg)) {
211 var pkgVer = a.pkg + "-" + i.ver.raw;
212 var info = Slots.installedSlot(vdb.root, a.cat, pkgVer);
213 if (!AtomParser.matches(a, i.ver)) continue;
214 if (!AtomParser.slotMatches(a, info.slot, info.subslot)) continue;
215 if (!AtomParser.useMatches(a, Slots.installedUse(vdb.root, a.cat, pkgVer))) continue;
216 return true;
217 }
218 return false;
219 }
220
221 static var blockerList: Array<{atom: Atom, strong: Bool, by: String}> = [];
222
223 static function checkBlockers(): Bool {
224 var planned = new Map<String, Bool>();
225 for (e in plan) planned.set(e.cat + "/" + e.pkg, true);
226
227 var ok = true;
228 for (b in blockerList) {
229 var label = b.atom.cat + "/" + b.atom.pkg;
230 if (planned.exists(label)) continue;
231 for (i in vdb.installed(b.atom.cat, b.atom.pkg)) {
232 var info = Slots.installedSlot(vdb.root, b.atom.cat, b.atom.pkg + "-" + i.ver.raw);
233 if (!AtomParser.matches(b.atom, i.ver) || !AtomParser.slotMatches(b.atom, info.slot, info.subslot)) continue;
234
235 ok = false;
236 var kind = b.strong ? "!!" : "!";
237 Sys.println(c("1;31", ">>> blocked: " + b.by + " conflicts with installed " + label + "-" + i.ver.raw + " (" + kind + " blocker)"));
238 }
239 }
240 if (!ok) Sys.println(c("1;31", ">>> Resolve the blocker first. Unmerging installed packages is not supported yet."));
241 return ok;
242 }
243
244 static var reportedCycles = new Map<String, Bool>();
245
246 static function reportCycle(key: String): Void {
247 var start = chain.indexOf(key);
248 var loop = chain.slice(start).concat([key]);
249 var text = loop.join(" -> ");
250 if (reportedCycles.exists(text)) return;
251 reportedCycles.set(text, true);
252 warn("circular dependency: " + text + " (cut here; merge order inside the loop may be wrong)");
253 }
254
255 static function slotOf(repoName: String, path: String): {slot: String, subslot: String} {
256 for (r in repos.list) {
257 if (r.name == repoName) return r.tree.slotInfo(path);
258 }
259 return {slot: "0", subslot: "0"};
260 }
261
262 static function installedSubslot(cat: String, pkg: String, slot: String): Null<String> {
263 for (i in vdb.installed(cat, pkg)) {
264 var info = Slots.installedSlot(vdb.root, cat, pkg + "-" + i.ver.raw);
265 if (info.slot == slot) return info.subslot;
266 }
267 return null;
268 }
269
270 static function hostSatisfies(a: Atom): Bool {
271 for (i in hostVdb.installed(a.cat, a.pkg)) {
272 var pkgVer = a.pkg + "-" + i.ver.raw;
273 if (!AtomParser.matches(a, i.ver)) continue;
274 if (!AtomParser.useMatches(a, Slots.installedUse(hostVdb.root, a.cat, pkgVer))) continue;
275 return true;
276 }
277 return false;
278 }
279
280 static function rejectedFor(a: Atom): Array<String> {
281 var out: Array<String> = [];
282 for (r in repos.list) {
283 for (v in r.tree.versions(a.cat, a.pkg)) {
284 if (!AtomParser.matches(a, v)) continue;
285 var path = r.tree.ebuildPath(a.cat, a.pkg, v);
286 var si = r.tree.slotInfo(path);
287 if (!AtomParser.slotMatches(a, si.slot, si.subslot)) continue;
288
289 var licenses = LicenseCheck.unacceptedFor(a.cat, a.pkg, v, r.tree.variable(path, "LICENSE"), UseFlags.forPackage(a.cat, a.pkg, r.tree.iuse(path)));
290 var masked = Mask.isMasked(a.cat, a.pkg, v);
291 var missingKeyword = !Keywords.acceptsWith(r.tree.variable(path, "KEYWORDS"), Mask.extraKeywords(a.cat, a.pkg, v));
292 if (licenses.length == 0 && !masked && !missingKeyword) continue;
293
294 var parts: Array<String> = [];
295 if (licenses.length > 0) parts.push(licenses.join(" ") + " license(s)");
296 if (masked) parts.push("package.mask");
297 if (missingKeyword) parts.push("missing keyword");
298 var reason = (licenses.length > 0 || masked) ? "masked by: " + parts.join(", ") : parts.join(", ");
299 out.push(a.cat + "/" + a.pkg + "-" + v.raw + "::" + r.name + " (" + reason + ")");
300 }
301 }
302 return out;
303 }
304
305 static function noMatchError(a: Atom, label: String): String {
306 var rejected = rejectedFor(a);
307 if (rejected.length == 0) return 'no ebuild matches "$label"';
308
309 var lines: Array<String> = [
310 '!!! All ebuilds that could satisfy "' + label + '" have been masked.',
311 "!!! One of the following masked packages is required to complete your request:"
312 ];
313 for (r in rejected) lines.push("- " + r);
314 lines.push("");
315 lines.push("For more information, see the MASKED PACKAGES section in the emerge");
316 lines.push("man page or refer to the Gentoo Handbook.");
317 return lines.join("\n");
318 }
319
320 static var seenLog: Array<String> = [];
321 static final MAX_ALTERNATIVES = 6;
322
323 static function rollback(planMark: Int, blockMark: Int, chainMark: Int, seenMark: Int): Void {
324 while (plan.length > planMark) plan.pop();
325 while (blockerList.length > blockMark) blockerList.pop();
326 while (chain.length > chainMark) chain.pop();
327 while (seenLog.length > seenMark) {
328 var k = seenLog.pop();
329 if (k != null) seen.remove(k);
330 }
331 }
332
333 static function candidatesFor(a: Atom): Array<Candidate> {
334 var out: Array<Candidate> = [];
335 var taken = new Map<String, Bool>();
336 for (r in repos.list) {
337 for (v in r.tree.versions(a.cat, a.pkg)) {
338 if (taken.exists(v.raw)) continue;
339 if (!AtomParser.matches(a, v)) continue;
340 var si = r.tree.slotInfo(r.tree.ebuildPath(a.cat, a.pkg, v));
341 if (!AtomParser.slotMatches(a, si.slot, si.subslot)) continue;
342 if (a.useDeps.length > 0 && !AtomParser.useMatches(a, UseFlags.forPackage(a.cat, a.pkg, r.tree.iuse(r.tree.ebuildPath(a.cat, a.pkg, v))))) continue;
343 var exact = AtomParser.parse("=" + a.cat + "/" + a.pkg + "-" + v.raw);
344 if (exact == null) continue;
345 var pick = repos.best(exact);
346 if (pick == null || pick.ver.compare(v) != 0) continue;
347 taken.set(v.raw, true);
348 out.push({ver: v, name: r.name, tree: r.tree});
349 }
350 }
351 out.sort(function(x, y) return y.ver.compare(x.ver));
352 return out;
353 }
354
355 static function resolve(a: Atom, label: String, top: Bool) {
356 var planMark = plan.length;
357 var blockMark = blockerList.length;
358 var chainMark = chain.length;
359 var seenMark = seenLog.length;
360 try {
361 resolveWith(a, label, top, null);
362 } catch (first: Dynamic) {
363 rollback(planMark, blockMark, chainMark, seenMark);
364 var primary = repos.best(a);
365 var tried = 0;
366 for (c in candidatesFor(a)) {
367 if (tried >= MAX_ALTERNATIVES) break;
368 if (primary != null && c.ver.compare(primary.ver) == 0 && c.name == primary.repo.name) continue;
369 tried++;
370 try {
371 resolveWith(a, label, top, c);
372 return;
373 } catch (ignored: Dynamic) {
374 rollback(planMark, blockMark, chainMark, seenMark);
375 }
376 }
377 throw first;
378 }
379 }
380
381 static function resolveWith(a: Atom, label: String, top: Bool, forced: Null<Candidate>) {
382 if (installedSatisfies(a) && (top ? (update && !upgradeAvailable(a)) : !(update && deep && upgradeAvailable(a)))) {
383 log("installed, skipping " + label);
384 return;
385 }
386
387 spin();
388
389 var hit: Dynamic = null;
390 if (forced != null) hit = {ver: forced.ver, repo: {name: forced.name, tree: forced.tree}};
391 else hit = repos.best(a);
392 var inst = vdb.installed(a.cat, a.pkg);
393
394 var targetVer: Version = null;
395 var targetRepo: String = null;
396 var targetPath: String = null;
397
398 if (hit != null) {
399 targetVer = hit.ver;
400 targetRepo = hit.repo.name;
401 targetPath = hit.repo.tree.ebuildPath(a.cat, a.pkg, hit.ver);
402 }
403
404 if (inst.length > 0) {
405 var topInst = inst[0];
406 for (i in inst) {
407 if (i.ver.compare(topInst.ver) > 0) topInst = i;
408 }
409
410 if (hit == null || topInst.ver.compare(hit.ver) >= 0) {
411 if (AtomParser.matches(a, topInst.ver)) {
412 var matchedRepo: Dynamic = null;
413 for (r in repos.list) {
414 if (r.name == topInst.repo) {
415 matchedRepo = r;
416 break;
417 }
418 }
419 if (matchedRepo == null && repos.list.length > 0) matchedRepo = repos.list[0];
420
421 if (matchedRepo != null) {
422 var p = matchedRepo.tree.ebuildPath(a.cat, a.pkg, topInst.ver);
423 if (FileSystem.exists(p)) {
424 targetVer = topInst.ver;
425 targetRepo = matchedRepo.name;
426 targetPath = p;
427 }
428 }
429 }
430 }
431 }
432
433 if (targetVer == null) {
434 if (hit == null) throw noMatchError(a, label);
435 targetVer = hit.ver;
436 targetRepo = hit.repo.name;
437 targetPath = hit.repo.tree.ebuildPath(a.cat, a.pkg, hit.ver);
438 }
439
440 var key = a.cat + "/" + a.pkg + "-" + targetVer.raw;
441 if (seen.exists(key)) {
442 if (chain.indexOf(key) >= 0) reportCycle(key);
443 return;
444 }
445 seen.set(key, true);
446 seenLog.push(key);
447
448 if (chain.length > 150) {
449 throw "dependency chain too deep: " + chain.slice(-10).join(" -> ") + " -> " + key;
450 }
451 chain.push(key);
452 log("resolve " + key);
453
454 var activeRepo = null;
455 for (r in repos.list) {
456 if (r.name == targetRepo) {
457 activeRepo = r;
458 break;
459 }
460 }
461 var tree = activeRepo != null ? activeRepo.tree : repos.list[0].tree;
462 var flags = UseFlags.forPackage(a.cat, a.pkg, tree.iuse(targetPath));
463
464 var requiredSpec = tree.variable(targetPath, "REQUIRED_USE");
465 var unmetUse = RequiredUse.unmet(requiredSpec, flags);
466 if (unmetUse.length > 0) {
467 var where = a.cat + "/" + a.pkg + "-" + targetVer.raw + "::" + targetRepo + " USE=\"" + useLine(tree.iuse(targetPath), flags) + "\"";
468 throw "The ebuild selected to satisfy \"" + label + "\" has unmet requirements.\n"
469 + " - " + where + "\n"
470 + " The following REQUIRED_USE flag constraints are unsatisfied:\n"
471 + unmetUse.map(function(u) return " " + u).join("\n") + "\n\n"
472 + " The above constraints are a subset of the following complete expression:\n"
473 + " " + RequiredUse.pretty(requiredSpec);
474 }
475
476 var runtime = Dependencies.eval(Dependencies.parse(tree.runtimeSpec(targetPath)), flags, chooser);
477
478 var toResolve = runtime.copy();
479 var build = Dependencies.eval(Dependencies.parse(tree.buildSpec(targetPath)), flags, chooser);
480 for (d in build) {
481 if (targetRoot == "/") {
482 toResolve.push(d);
483 } else if (!hostSatisfies(d)) {
484 warn('build dependency not installed on host: ${d.cat}/${d.pkg}');
485 }
486 }
487
488 var depLabels: Array<String> = [];
489 for (d in toResolve) {
490 var dl = d.cat + "/" + d.pkg;
491 depLabels.push(dl);
492 resolve(d, dl, false);
493 }
494
495 chain.pop();
496 var si = slotOf(targetRepo, targetPath);
497 plan.push({cat: a.cat, pkg: a.pkg, ver: targetVer, path: targetPath, repo: targetRepo, deps: depLabels, slot: si.slot, subslot: si.subslot, use: useLine(tree.iuse(targetPath), flags)});
498
499 var by = a.cat + "/" + a.pkg + "-" + targetVer.raw;
500 var blocked = Dependencies.blockers(Dependencies.parse(tree.runtimeSpec(targetPath)), flags)
501 .concat(Dependencies.blockers(Dependencies.parse(tree.buildSpec(targetPath)), flags));
502 for (b in blocked) blockerList.push({atom: b.atom, strong: b.strong, by: by});
503
504 for (d in Dependencies.eval(Dependencies.parse(tree.variable(targetPath, "PDEPEND")), flags, chooser)) {
505 resolve(d, d.cat + "/" + d.pkg, false);
506 }
507 }
508
509 static function upgradeAvailable(a: Atom): Bool {
510 var hit = repos.best(a);
511 if (hit == null) return false;
512 for (i in vdb.installed(a.cat, a.pkg)) {
513 if (i.ver.compare(hit.ver) >= 0) return false;
514 }
515 return true;
516 }
517
518 static function syncRepos(only: Null<String>): Bool {
519 Sys.println(c("1;32", only == null ? ">>> Syncing repositories ..." : ">>> Syncing " + only + " ..."));
520 var ok = Sync.run(only);
521 if (!ok) {
522 Sys.println(c("1;31", ">>> Sync finished with errors"));
523 return false;
524 }
525 Sys.println(c("1;32", ">>> Sync complete."));
526 return true;
527 }
528
529 static function rebuildSubslotDependents(): Void {
530 for (e in plan.copy()) {
531 var old = installedSubslot(e.cat, e.pkg, e.slot);
532 if (old == null || old == e.subslot) continue;
533
534 for (dep in Slots.dependentsWithSubslotOp(vdb.root, e.cat, e.pkg)) {
535 var a = AtomParser.parse(dep);
536 if (a == null) continue;
537 clearProgress();
538 Sys.println(c("1;33", ">>> " + dep + " uses " + e.cat + "/" + e.pkg + ":= and must be rebuilt (subslot " + old + " -> " + e.subslot + ")"));
539 try {
540 resolve(a, dep, true);
541 } catch (x: Dynamic) {
542 Sys.println(c("1;31", ">>> could not plan rebuild of " + dep + ": " + x));
543 }
544 }
545 }
546 }
547
548 static function chooser(alts: Array<Array<Atom>>): Int {
549 var firstResolvable = -1;
550 for (i in 0...alts.length) {
551 var resolvable = alts[i].length > 0;
552 var installed = alts[i].length > 0;
553 for (at in alts[i]) {
554 if (repos.best(at) == null) {
555 resolvable = false;
556 break;
557 }
558 if (!installedSatisfies(at)) installed = false;
559 }
560 if (!resolvable) continue;
561 if (installed) return i;
562 if (firstResolvable < 0) firstResolvable = i;
563 }
564 return firstResolvable < 0 ? 0 : firstResolvable;
565 }
566
567 static function status(vdb: VDB, e: Entry): Status {
568 var exactInst = [for (i in vdb.installed(e.cat, e.pkg))
569 if (Slots.installedSlot(vdb.root, e.cat, e.pkg + "-" + i.ver.raw).slot == e.slot) i];
570
571 if (exactInst.length > 0) {
572 var top = exactInst[0];
573 for (i in exactInst) {
574 if (i.ver.compare(top.ver) > 0) top = i;
575 }
576
577 var cmp = e.ver.compare(top.ver);
578 var oldStr = top.ver.raw + "::" + top.repo;
579 if (cmp == 0) return {tag: "R", old: null};
580 if (cmp > 0) return {tag: "U", old: oldStr};
581 return {tag: "UD", old: oldStr};
582 }
583
584 var allInst = vdb.installed(e.cat, e.pkg);
585 if (allInst.length == 0) return {tag: "N", old: null};
586
587 var topAll = allInst[0];
588 for (i in allInst) {
589 if (i.ver.compare(topAll.ver) > 0) topAll = i;
590 }
591 return {tag: "NS", old: topAll.ver.raw};
592 }
593
594 static function isTesting(e: Entry): Bool {
595 var accept = Keywords.active();
596 var kw = "";
597 for (r in repos.list) {
598 if (r.name == e.repo) {
599 kw = r.tree.variable(e.path, "KEYWORDS");
600 break;
601 }
602 }
603
604 var testing = false;
605 for (t in ~/[ \t\r\n]+/g.split(kw)) {
606 if (t == "") continue;
607 if (accept.indexOf(t) < 0) continue;
608 if (StringTools.startsWith(t, "~")) testing = true;
609 else return false;
610 }
611 return testing;
612 }
613
614 static function useLine(iuse: Array<String>, flags: Map<String, Bool>): String {
615 var parts: Array<String> = [];
616 for (tok in iuse) {
617 var name = StringTools.startsWith(tok, "+") || StringTools.startsWith(tok, "-") ? tok.substr(1) : tok;
618 if (name == "") continue;
619 parts.push(flags.get(name) == true ? name : "-" + name);
620 }
621 return parts.join(" ");
622 }
623
624 static function showPlan(vdb: VDB, verbose: Bool) {
625 if (plan.length == 0) {
626 Sys.println(c("1;32", "Nothing to merge; all requested packages are installed."));
627 return;
628 }
629
630 Sys.println(c("1;32", "These are the packages that would be merged, in order:"));
631 Sys.println("");
632
633 var counts = new Map<String, Int>();
634 for (e in plan) {
635 var st = status(vdb, e);
636 counts.set(st.tag, (counts.exists(st.tag) ? counts.get(st.tag) : 0) + 1);
637
638 var tagCol = switch st.tag {
639 case "N" | "NS": "32";
640 case "U": "34";
641 case "UD": "35";
642 default: "33";
643 };
644
645 var line = c("1;32", "[ebuild ")
646 + c(tagCol, " " + StringTools.rpad(st.tag, " ", 2) + (isTesting(e) ? "+" : " ") + " ")
647 + c("1;32", "]")
648 + " " + c("1;37", e.cat + "/" + e.pkg + "-" + e.ver.raw)
649 + (showRepo ? c("0;36", "::" + e.repo) : "");
650
651 if (st.old != null) {
652 line += " " + c("0;36", "[" + st.old + "]");
653 }
654
655 if (verbose) {
656 if (e.use != "") line += " " + c("0;33", "USE=\"" + e.use + "\"");
657 var pkgSize = Std.int(getDownloadSize(e) / 1024);
658 if (pkgSize > 0) {
659 line += " " + formatNum(pkgSize) + " KiB";
660 } else {
661 line += " 0 KiB";
662 }
663 }
664
665 Sys.println(line);
666 }
667
668 Sys.println("");
669 var parts: Array<String> = [];
670 if (counts.exists("U")) parts.push(counts.get("U") + " upgrade" + (counts.get("U") == 1 ? "" : "s"));
671 if (counts.exists("UD")) parts.push(counts.get("UD") + " downgrade" + (counts.get("UD") == 1 ? "" : "s"));
672 if (counts.exists("N")) parts.push(counts.get("N") + " new");
673 if (counts.exists("NS")) parts.push(counts.get("NS") + " in new slot");
674 if (counts.exists("R")) parts.push(counts.get("R") + " reinstall");
675
676 var totalSize = 0;
677 for (e in plan) {
678 totalSize += getDownloadSize(e);
679 }
680 var sizeKiB = Std.int(totalSize / 1024);
681
682 var plural = plan.length == 1 ? "" : "s";
683 var summary = "Total: " + plan.length + " package" + plural + " (" + parts.join(", ") + ")";
684 if (sizeKiB > 0) {
685 summary += ", Size of downloads: " + formatNum(sizeKiB) + " KiB";
686 }
687 if (verbose) Sys.println(summary);
688 }
689
690 static function explain(spec: String): Void {
691 var a = AtomParser.parse(spec);
692 if (a == null) {
693 Sys.println("invalid atom: " + spec);
694 return;
695 }
696
697 var hit = repos.best(a);
698 var chosen: Null<Version> = hit == null ? null : hit.ver;
699
700 Sys.println("ACCEPT_KEYWORDS: " + Keywords.active().join(" "));
701 for (r in repos.list) {
702 var vs = r.tree.versions(a.cat, a.pkg);
703 vs.sort(function(x, y) return y.compare(x));
704 for (v in vs) {
705 var kw = r.tree.variable(r.tree.ebuildPath(a.cat, a.pkg, v), "KEYWORDS");
706 var masked = Mask.isMasked(a.cat, a.pkg, v);
707 var kwOk = Keywords.acceptsWith(kw, Mask.extraKeywords(a.cat, a.pkg, v));
708 var picked = chosen != null && chosen.compare(v) == 0;
709 Sys.println(" " + a.cat + "/" + a.pkg + "-" + v.raw + "::" + r.name
710 + " KEYWORDS=\"" + kw + "\""
711 + " masked=" + masked
712 + " keywords-ok=" + kwOk
713 + (picked ? " <- picked" : ""));
714 }
715 }
716 }
717
718 static function expandSets(list: Array<String>): Array<String> {
719 var out: Array<String> = [];
720 for (arg in list) {
721 if (arg == "@world") {
722 for (a in readWorld()) out.push(a);
723 } else {
724 out.push(arg);
725 }
726 }
727 return out;
728 }
729
730 static function worldLine(spec: String): Null<String> {
731 var a = AtomParser.parse(spec);
732 if (a == null) return null;
733
734 var line = a.cat + "/" + a.pkg;
735 var slot = a.slot;
736 if (slot != null) {
737 var name = slot.split("/")[0];
738 if (name != "" && name != "*" && name != "=") line += ":" + name;
739 }
740 return line;
741 }
742
743 static function updateWorld(specs: Array<String>): Void {
744 var base = StringTools.endsWith(targetRoot, "/") ? targetRoot.substr(0, targetRoot.length - 1) : targetRoot;
745 var dir = base + "/var/lib/portage";
746 var path = dir + "/world";
747
748 var lines: Array<String> = [];
749 if (FileSystem.exists(path)) {
750 for (raw in File.getContent(path).split("\n")) {
751 var t = StringTools.trim(raw);
752 if (t != "" && lines.indexOf(t) < 0) lines.push(t);
753 }
754 }
755
756 for (spec in specs) {
757 var line = worldLine(spec);
758 if (line != null && lines.indexOf(line) < 0) lines.push(line);
759 }
760
761 lines.sort(function(x, y) return x < y ? -1 : (x > y ? 1 : 0));
762 if (!FileSystem.exists(dir)) FileSystem.createDirectory(dir);
763 File.saveContent(path, lines.join("\n") + "\n");
764 }
765
766 static function readWorld(): Array<String> {
767 var base = StringTools.endsWith(targetRoot, "/") ? targetRoot.substr(0, targetRoot.length - 1) : targetRoot;
768 var path = base + "/var/lib/portage/world";
769 if (!FileSystem.exists(path)) {
770 Sys.println('no world file at ' + path);
771 Sys.exit(1);
772 }
773
774 var out: Array<String> = [];
775 for (raw in File.getContent(path).split("\n")) {
776 var line = StringTools.trim(raw);
777 if (line == "" || StringTools.startsWith(line, "#")) continue;
778 if (AtomParser.parse(line) == null) {
779 Sys.println('warning: skipping world entry "' + line + '"');
780 continue;
781 }
782 out.push(line);
783 }
784 return out;
785 }
786
787 static function ebuildFile(cat: String, pkgVer: String): Null<String> {
788 var pn = Unmerge.pnFromDir(pkgVer);
789 for (r in repos.list) {
790 var path = r.tree.root + "/" + cat + "/" + pn + "/" + pkgVer + ".ebuild";
791 if (FileSystem.exists(path)) return path;
792 }
793 return null;
794 }
795
796 static function removeInstalled(cat: String, pkgVer: String): Void {
797 var ebuild = ebuildFile(cat, pkgVer);
798 Sys.putEnv("ROOT", targetRoot);
799
800 if (ebuild == null) {
801 Sys.println(c("1;33", ">>> " + cat + "/" + pkgVer + ": no ebuild in any repo, pkg_prerm and pkg_postrm skipped"));
802 } else if (Sys.command("ebuild", [ebuild, "prerm"]) != 0) {
803 Sys.println(c("1;31", ">>> pkg_prerm failed for " + cat + "/" + pkgVer + "; not removed"));
804 Sys.exit(1);
805 }
806
807 Unmerge.remove(targetRoot, cat, pkgVer);
808
809 if (ebuild != null && Sys.command("ebuild", [ebuild, "postrm"]) != 0) {
810 Sys.println(c("1;33", ">>> pkg_postrm failed for " + cat + "/" + pkgVer));
811 }
812 }
813
814 static function runUnmerge(specs: Array<String>, pretend: Bool, ask: Bool): Void {
815 if (specs.length == 0) {
816 Sys.println("usage: emerge-hx -C [-p] [--root DIR] <atom> [atom...]");
817 Sys.exit(1);
818 }
819
820 var targets = [];
821 for (x in specs) {
822 if (StringTools.startsWith(x, "@")) {
823 Sys.println("sets can't be unmerged: " + x);
824 Sys.exit(1);
825 }
826 var found = Unmerge.find(targetRoot, x);
827 if (found.length == 0) {
828 Sys.println('no installed package matches "' + x + '"');
829 Sys.exit(1);
830 }
831 for (f in found) targets.push(f);
832 }
833
834 var blocked = false;
835 for (t in targets) {
836 for (dep in Unmerge.dependents(targetRoot, t.cat, t.pkg)) {
837 var removing = false;
838 for (other in targets) if (other.cat + "/" + other.pkg == dep) removing = true;
839 if (removing) continue;
840 Sys.println(c("1;31", "!!! " + dep + " depends on " + t.label + "; unmerge " + dep + " first"));
841 blocked = true;
842 }
843 }
844 if (blocked) Sys.exit(1);
845
846 Sys.println(c("1;32", "These packages would be unmerged:"));
847 Sys.println("");
848 for (t in targets) Sys.println(c("1;37", " " + t.label));
849 Sys.println("");
850
851 if (pretend) return;
852
853 if (!isRoot()) {
854 Sys.println("emerge-hx: superuser access is required to unmerge");
855 Sys.exit(1);
856 }
857
858 {
859 Sys.print("\nWould you like to unmerge these packages? [Yes/No] ");
860 var answer = "";
861 try {
862 answer = Sys.stdin().readLine().toLowerCase();
863 } catch (e: Dynamic) {
864 answer = "";
865 }
866 if (answer != "y" && answer != "yes") {
867 Sys.println("Aborted.");
868 return;
869 }
870 }
871
872 for (n in 0...targets.length) {
873 var t = targets[n];
874 var pos = "(" + (n + 1) + " of " + targets.length + ") ";
875 Sys.println(c("1;32", ">>> Unmerging " + pos) + c("1;37", t.label));
876 removeInstalled(t.cat, t.pkgVer);
877 Sys.println(c("1;32", ">>> Completed " + pos) + c("1;37", t.label));
878 }
879 Sys.println(c("1;32", "Done."));
880 }
881
882 static function runDepclean(pretend: Bool): Void {
883 var orphans = Unmerge.depcleanCandidates(targetRoot);
884 if (orphans.length == 0) {
885 Sys.println(c("1;32", "Nothing to clean."));
886 return;
887 }
888
889 Sys.println(c("1;32", "These packages are not needed by the world file or the system profile:"));
890 Sys.println("");
891 for (o in orphans) Sys.println(c("1;37", " " + o.label));
892 Sys.println("");
893
894 if (pretend) return;
895
896 if (!isRoot()) {
897 Sys.println("emerge-hx: superuser access is required to clean");
898 Sys.exit(1);
899 }
900
901 Sys.print("\nWould you like to unmerge these packages? [Yes/No] ");
902 var answer = "";
903 try {
904 answer = Sys.stdin().readLine().toLowerCase();
905 } catch (e: Dynamic) {
906 answer = "";
907 }
908 if (answer != "y" && answer != "yes") {
909 Sys.println("Aborted.");
910 return;
911 }
912
913 for (n in 0...orphans.length) {
914 var o = orphans[n];
915 var pos = "(" + (n + 1) + " of " + orphans.length + ") ";
916 Sys.println(c("1;32", ">>> Unmerging " + pos) + c("1;37", o.label));
917 removeInstalled(o.cat, o.pkgVer);
918 Sys.println(c("1;32", ">>> Completed " + pos) + c("1;37", o.label));
919 }
920 Sys.println(c("1;32", "Done."));
921 }
922
923 static function fullLabel(e: Entry): String {
924 var base = e.cat + "/" + e.pkg + "-" + e.ver.raw;
925 return showRepo ? base + "::" + e.repo : base;
926 }
927
928 static function expandShortFlags(args: Array<String>): Array<String> {
929 var bundled = new EReg("^-[a-zA-Z]{2,}$", "");
930 var out: Array<String> = [];
931 for (arg in args) {
932 if (bundled.match(arg)) {
933 for (i in 1...arg.length) out.push("-" + arg.charAt(i));
934 } else {
935 out.push(arg);
936 }
937 }
938 return out;
939 }
940
941 static function getDownloadSize(e: Entry): Int {
942 var dir = haxe.io.Path.directory(e.path);
943 var manifestPath = dir + "/Manifest";
944 if (!FileSystem.exists(manifestPath)) return 0;
945
946 var baseVer = e.ver.raw;
947 var dashR = baseVer.lastIndexOf("-r");
948 if (dashR > 0) {
949 baseVer = baseVer.substr(0, dashR);
950 }
951
952 var totalSize = 0;
953 try {
954 var content = File.getContent(manifestPath);
955 for (line in content.split("\n")) {
956 var trimmed = StringTools.trim(line);
957 if (StringTools.startsWith(trimmed, "DIST ")) {
958 var parts = ~/[ \t]+/g.split(trimmed);
959 if (parts.length >= 3) {
960 var filename = parts[1];
961 if (StringTools.contains(filename, baseVer)) {
962 var sz = Std.parseInt(parts[2]);
963 if (sz != null) {
964 totalSize += sz;
965 }
966 }
967 }
968 }
969 }
970 } catch (err: Dynamic) {}
971 return totalSize;
972 }
973
974 static function formatNum(n: Int): String {
975 var s = Std.string(n);
976 var out = "";
977 var len = s.length;
978 for (i in 0...len) {
979 if (i > 0 && (len - i) % 3 == 0) out += ",";
980 out += s.charAt(i);
981 }
982 return out;
983 }
984
985 static function mergeOne(e: Entry, count: Int, total: Int): Bool {
986 var pos = "(" + count + " of " + total + ") ";
987
988 Sys.println("");
989 Sys.println(c("1;32", ">>> Emerging " + pos) + c("1;37", fullLabel(e)));
990
991 var buildCmd = "ebuild " + e.path + " clean unpack prepare configure compile install";
992 if (quiet) buildCmd += " > /dev/null 2>&1";
993 var build = Sys.command("sh", ["-c", buildCmd]);
994 if (build != 0) {
995 Sys.println(c("1;31", ">>> Failed " + pos + fullLabel(e) + " (build exit " + build + ")"));
996 return false;
997 }
998
999 Sys.println(c("1;32", ">>> Installing " + pos) + c("1;37", fullLabel(e)));
1000
1001 var mergeCmd = "ebuild " + e.path + " qmerge";
1002 if (quiet) mergeCmd += " > /dev/null 2>&1";
1003 var merge = Sys.command("sh", ["-c", mergeCmd]);
1004 if (merge != 0) {
1005 Sys.println(c("1;31", ">>> Failed " + pos + fullLabel(e) + " (merge exit " + merge + ")"));
1006 return false;
1007 }
1008
1009 Sys.println(c("1;32", ">>> Completed " + pos) + c("1;37", fullLabel(e)));
1010 return true;
1011 }
1012
1013 static function main() {
1014 repos = Repository.load();
1015
1016 var pretend = false;
1017 var ask = false;
1018 var atoms: Array<String> = [];
1019 var searchQuery: Null<String> = null;
1020 var searchDesc = false;
1021 var verbose = false;
1022
1023 var args = expandShortFlags(Sys.args());
1024 var idx = 0;
1025 while (idx < args.length) {
1026 var arg = args[idx++];
1027 if (StringTools.startsWith(arg, "--sync=")) {
1028 syncFlag = true;
1029 syncOnly = arg.substr("--sync=".length);
1030 continue;
1031 }
1032 switch arg {
1033 case "--pretend" | "-p": pretend = true;
1034 case "--ask" | "-a": ask = true;
1035 case "--verbose" | "-v": verbose = true;
1036 case "--update" | "-u": update = true;
1037 case "--deep" | "-D": deep = true;
1038 case "--unmerge" | "-C": unmergeMode = true;
1039 case "--depclean" | "-d": depclean = true;
1040 case "--quiet" | "-q": quiet = true;
1041 case "--sync":
1042 syncFlag = true;
1043 if (idx < args.length && Sync.repoNames().indexOf(args[idx]) >= 0) {
1044 syncOnly = args[idx++];
1045 }
1046 case "--explain":
1047 if (idx < args.length) explainAtom = args[idx++];
1048 case "--search" | "-s":
1049 if (idx >= args.length) {
1050 Sys.println("--search needs a query");
1051 Sys.exit(1);
1052 }
1053 searchQuery = args[idx++];
1054 case "--desc" | "-S": searchDesc = true;
1055 case "--root":
1056 if (idx >= args.length) {
1057 Sys.println("--root needs a directory");
1058 Sys.exit(1);
1059 }
1060 targetRoot = args[idx++];
1061 default: atoms.push(arg);
1062 }
1063 }
1064
1065 showRepo = verbose;
1066
1067 if (searchQuery != null) {
1068 Search.run(repos, searchQuery, searchDesc);
1069 return;
1070 }
1071
1072 if (explainAtom != null) {
1073 explain(explainAtom);
1074 return;
1075 }
1076
1077 if (depclean && atoms.length > 0) {
1078 Sys.println("--depclean takes no package arguments; use -C to unmerge specific packages");
1079 Sys.exit(1);
1080 }
1081
1082 if (depclean) {
1083 runDepclean(pretend);
1084 return;
1085 }
1086
1087 if (unmergeMode) {
1088 runUnmerge(atoms, pretend, ask);
1089 return;
1090 }
1091
1092 var namedAtoms: Array<String> = [for (x in atoms) if (!StringTools.startsWith(x, "@")) x];
1093 atoms = expandSets(atoms);
1094
1095 if (syncFlag) {
1096 if (!isRoot()) {
1097 Sys.println("emerge-hx: superuser access is required for --sync");
1098 Sys.exit(1);
1099 }
1100 if (!syncRepos(syncOnly)) Sys.exit(1);
1101 indexCache = null;
1102 repos = Repository.load();
1103 if (atoms.length == 0) return;
1104 }
1105
1106 if (atoms.length == 0) {
1107 Sys.println("usage: emerge-hx [--sync[=REPO]] [-u] [--pretend | --ask] [--root DIR] <atom> [atom...]");
1108 Sys.println(" emerge-hx --search <query> [--desc]");
1109 Sys.exit(1);
1110 }
1111
1112 if (!pretend && !isRoot()) {
1113 if (!ask) {
1114 Sys.println("emerge-hx: superuser access is required");
1115 Sys.exit(1);
1116 }
1117 Sys.println("This action requires superuser access...");
1118 Sys.print("Would you like to add --pretend to options? [Yes/No] ");
1119 var choice = "";
1120 try {
1121 choice = Sys.stdin().readLine().toLowerCase();
1122 } catch (e: Dynamic) {
1123 choice = "";
1124 }
1125 if (choice != "y" && choice != "yes") {
1126 Sys.println("Aborted.");
1127 Sys.exit(1);
1128 }
1129 pretend = true;
1130 }
1131
1132 vdb = new VDB(targetRoot);
1133 hostVdb = new VDB("/");
1134
1135 startProgress();
1136
1137 for (arg in atoms) {
1138 var spec = NAME_RE.match(arg) ? resolveName(arg) : arg;
1139
1140 var a = AtomParser.parse(spec);
1141 if (a == null) {
1142 clearProgress();
1143 Sys.println('invalid atom: $arg');
1144 Sys.exit(1);
1145 }
1146 try {
1147 resolve(a, spec, true);
1148 } catch (e: Dynamic) {
1149 clearProgress();
1150 var message = Std.string(e);
1151 Sys.println(StringTools.startsWith(message, "!!!") ? message : "error: " + message);
1152 Sys.exit(1);
1153 }
1154 }
1155
1156 rebuildSubslotDependents();
1157
1158 finishProgress();
1159 Sys.println("");
1160
1161 if (!checkBlockers()) Sys.exit(1);
1162
1163 showPlan(vdb, verbose);
1164
1165 if (pretend || plan.length == 0) return;
1166
1167 if (ask) {
1168 Sys.print("\nWould you like to merge these packages? [Yes/No] ");
1169 var answer = "";
1170 try {
1171 answer = Sys.stdin().readLine();
1172 } catch (e: Dynamic) {
1173 answer = "";
1174 }
1175 answer = answer.toLowerCase();
1176 if (answer != "y" && answer != "yes") {
1177 Sys.println("Aborted.");
1178 return;
1179 }
1180 }
1181
1182 Sys.putEnv("ROOT", targetRoot);
1183 var total = plan.length;
1184 for (n in 0...total) {
1185 if (!mergeOne(plan[n], n + 1, total)) Sys.exit(1);
1186 }
1187 updateWorld([for (x in namedAtoms) NAME_RE.match(x) ? resolveName(x) : x]);
1188 }
1189 }