acooliodude / portage-hx

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