acooliodude / portage-hx

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