acooliodude / portage-hx

Initial commit Browse files
ACoolioDude committed 2d ago commit d412a4e4a55388d38efab43efecc35169c170ddc
Showing 16 changed files with 1838 additions and 0 deletions. UnifiedSplit
+1 −0 .gitignore new file mode 100644
@@ -0,0 +1 @@
1 +bin
\ No newline at end of file
+3 −0 README.md new file mode 100644
@@ -0,0 +1,3 @@
1 +# portage-hx
2 +
3 + - A rewrite of Portage in Haxe (W.I.P)
+5 −0 build.hxml new file mode 100644
@@ -0,0 +1,5 @@
1 +-cp src
2 +-main Main
3 +-lib hxcpp
4 +--cpp bin/cpp
5 +--cmd mv bin/cpp/Main bin/cpp/emerge-hx
+662 −0 src/Main.hx new file mode 100644 Large diff collapsed, click to expand
@@ -0,0 +1,662 @@
1 +import sys.FileSystem;
2 +import sys.io.File;
3 +import sys.io.Process;
4 +
5 +typedef Entry = {
6 + cat: String,
7 + pkg: String,
8 + ver: Version,
9 + path: String,
10 + repo: String,
11 + deps: Array<String>
12 +}
13 +
14 +typedef Status = {
15 + tag: String,
16 + old: Null<String>
17 +}
18 +
19 +typedef Suggestion = {
20 + label: String,
21 + score: Int
22 +}
23 +
24 +class Main {
25 + static var repos: Repository;
26 + static var seen = new Map<String, Bool>();
27 + static var plan: Array<Entry> = [];
28 + static var useColor = Sys.getEnv("NO_COLOR") == null;
29 + static var trace = Sys.getEnv("EMERGE_TRACE") != null;
30 + static var targetRoot = "/";
31 + static var showRepo = false;
32 + static var update = false;
33 + static var syncFlag = false;
34 + static var syncOnly: Null<String> = null;
35 + static var explainAtom: Null<String> = null;
36 + static var vdb: VDB;
37 + static var hostVdb: VDB;
38 + static var chain: Array<String> = [];
39 +
40 + static final PROGRESS_LABEL = "Calculating dependencies ";
41 + static final SPIN_FRAMES = ["|", "/", "-", "\\"];
42 + static final FRAME_SECONDS = 0.08;
43 + static final MIN_SHOW_SECONDS = 0.6;
44 + static var spinIndex = 0;
45 + static var progressStart = 0.0;
46 + static var lastFrame = 0.0;
47 +
48 + static final NAME_RE = ~/^[a-zA-Z0-9][a-zA-Z0-9_+.-]*$/;
49 + static final NON_CATEGORY_DIRS = ["profiles", "eclass", "metadata", "licenses", "scripts", "packages", "distfiles"];
50 + static final MAX_SUGGESTIONS = 10;
51 + static var indexCache: Null<Array<String>> = null;
52 +
53 + static function c(code: String, s: String): String {
54 + return useColor ? '\x1B[${code}m$s\x1B[0m' : s;
55 + }
56 +
57 + static function log(msg: String): Void {
58 + if (!trace) return;
59 + Sys.stderr().writeString(msg + "\n");
60 + Sys.stderr().flush();
61 + }
62 +
63 + static function startProgress(): Void {
64 + progressStart = haxe.Timer.stamp();
65 + drawFrame();
66 + }
67 +
68 + static function drawFrame(): Void {
69 + spinIndex = (spinIndex + 1) % SPIN_FRAMES.length;
70 + Sys.print("\r" + PROGRESS_LABEL + SPIN_FRAMES[spinIndex]);
71 + Sys.stdout().flush();
72 + lastFrame = haxe.Timer.stamp();
73 + }
74 +
75 + static function spin(): Void {
76 + if (haxe.Timer.stamp() - lastFrame < FRAME_SECONDS) return;
77 + drawFrame();
78 + }
79 +
80 + static function finishProgress(): Void {
81 + while (haxe.Timer.stamp() - progressStart < MIN_SHOW_SECONDS) {
82 + drawFrame();
83 + Sys.sleep(FRAME_SECONDS);
84 + }
85 + Sys.print("\r" + PROGRESS_LABEL + "... done!\n");
86 + Sys.stdout().flush();
87 + }
88 +
89 + static function clearProgress(): Void {
90 + Sys.print("\r" + StringTools.lpad("", " ", 40) + "\r");
91 + Sys.stdout().flush();
92 + }
93 +
94 + static function warn(msg: String): Void {
95 + clearProgress();
96 + Sys.println("warning: " + msg);
97 + }
98 +
99 + static function isRoot(): Bool {
100 + try {
101 + var p = new Process("id", ["-u"]);
102 + var out = StringTools.trim(p.stdout.readAll().toString());
103 + p.close();
104 + return out == "0";
105 + } catch (e: Dynamic) {
106 + return false;
107 + }
108 + }
109 +
110 + static function packageIndex(): Array<String> {
111 + if (indexCache != null) return indexCache;
112 +
113 + var seenLabel = new Map<String, Bool>();
114 + var out: Array<String> = [];
115 + for (r in repos.list) {
116 + if (!FileSystem.exists(r.tree.root)) continue;
117 + for (cat in FileSystem.readDirectory(r.tree.root)) {
118 + if (StringTools.startsWith(cat, ".") || NON_CATEGORY_DIRS.indexOf(cat) >= 0) continue;
119 + var catDir = r.tree.root + "/" + cat;
120 + if (!FileSystem.isDirectory(catDir)) continue;
121 + for (pkg in FileSystem.readDirectory(catDir)) {
122 + if (StringTools.startsWith(pkg, ".")) continue;
123 + if (!FileSystem.isDirectory(catDir + "/" + pkg)) continue;
124 + var label = cat + "/" + pkg;
125 + if (seenLabel.exists(label)) continue;
126 + seenLabel.set(label, true);
127 + out.push(label);
128 + }
129 + }
130 + }
131 + out.sort(function(a, b) return a < b ? -1 : (a > b ? 1 : 0));
132 + indexCache = out;
133 + return out;
134 + }
135 +
136 + static function nameOf(label: String): String {
137 + return label.substr(label.lastIndexOf("/") + 1);
138 + }
139 +
140 + static function levenshtein(a: String, b: String): Int {
141 + var prev = [for (j in 0...b.length + 1) j];
142 + for (i in 1...a.length + 1) {
143 + var cur = [i];
144 + for (j in 1...b.length + 1) {
145 + var cost = a.charAt(i - 1) == b.charAt(j - 1) ? 0 : 1;
146 + var del = prev[j] + 1;
147 + var ins = cur[j - 1] + 1;
148 + var sub = prev[j - 1] + cost;
149 + cur.push(Std.int(Math.min(del, Math.min(ins, sub))));
150 + }
151 + prev = cur;
152 + }
153 + return prev[b.length];
154 + }
155 +
156 + static function resolveName(name: String): String {
157 + var exact = [for (label in packageIndex()) if (nameOf(label) == name) label];
158 + if (exact.length == 1) return exact[0];
159 +
160 + if (exact.length > 1) {
161 + Sys.println('\n"$name" is in more than one category. Use category/name to pick one:');
162 + for (l in exact) Sys.println(" " + l);
163 + Sys.exit(1);
164 + }
165 +
166 + var hits: Array<Suggestion> = [];
167 + var limit = Std.int(Math.max(3, name.length / 3));
168 + for (label in packageIndex()) {
169 + var pkg = nameOf(label);
170 + var score: Int;
171 + if (pkg.indexOf(name) >= 0 || (pkg.length >= 3 && name.indexOf(pkg) >= 0)) {
172 + score = 0;
173 + } else {
174 + score = levenshtein(name, pkg);
175 + }
176 + if (score <= limit) hits.push({label: label, score: score});
177 + }
178 + hits.sort(function(a, b) {
179 + if (a.score != b.score) return a.score - b.score;
180 + return a.label < b.label ? -1 : (a.label > b.label ? 1 : 0);
181 + });
182 +
183 + if (hits.length == 0) {
184 + Sys.println('\nNo package named "$name" in any repository.');
185 + Sys.exit(1);
186 + }
187 +
188 + Sys.println('\nNo package named "$name". Did you mean:');
189 + var shown = hits.length < MAX_SUGGESTIONS ? hits.length : MAX_SUGGESTIONS;
190 + for (i in 0...shown) Sys.println(" " + hits[i].label);
191 + if (hits.length > shown) Sys.println(' ...and ${hits.length - shown} more');
192 + Sys.exit(1);
193 + return name;
194 + }
195 +
196 + static function installedSatisfies(a: Atom): Bool {
197 + for (i in vdb.installed(a.cat, a.pkg)) {
198 + if (AtomParser.matches(a, i.ver)) return true;
199 + }
200 + return false;
201 + }
202 +
203 + static function hostSatisfies(a: Atom): Bool {
204 + for (i in hostVdb.installed(a.cat, a.pkg)) {
205 + if (AtomParser.matches(a, i.ver)) return true;
206 + }
207 + return false;
208 + }
209 +
210 + static function resolve(a: Atom, label: String, top: Bool) {
211 + if (installedSatisfies(a) && (update ? !upgradeAvailable(a) : !top)) {
212 + log("installed, skipping " + label);
213 + return;
214 + }
215 +
216 + spin();
217 +
218 + var hit = repos.best(a);
219 + var inst = vdb.installed(a.cat, a.pkg);
220 +
221 + var targetVer: Version = null;
222 + var targetRepo: String = null;
223 + var targetPath: String = null;
224 +
225 + if (hit != null) {
226 + targetVer = hit.ver;
227 + targetRepo = hit.repo.name;
228 + targetPath = hit.repo.tree.ebuildPath(a.cat, a.pkg, hit.ver);
229 + }
230 +
231 + if (inst.length > 0) {
232 + var topInst = inst[0];
233 + for (i in inst) {
234 + if (i.ver.compare(topInst.ver) > 0) topInst = i;
235 + }
236 +
237 + if (hit == null || topInst.ver.compare(hit.ver) >= 0) {
238 + if (AtomParser.matches(a, topInst.ver)) {
239 + var matchedRepo = null;
240 + for (r in repos.list) {
241 + if (r.name == topInst.repo) {
242 + matchedRepo = r;
243 + break;
244 + }
245 + }
246 + if (matchedRepo == null && repos.list.length > 0) matchedRepo = repos.list[0];
247 +
248 + if (matchedRepo != null) {
249 + var p = matchedRepo.tree.ebuildPath(a.cat, a.pkg, topInst.ver);
250 + if (FileSystem.exists(p)) {
251 + targetVer = topInst.ver;
252 + targetRepo = matchedRepo.name;
253 + targetPath = p;
254 + }
255 + }
256 + }
257 + }
258 + }
259 +
260 + if (targetVer == null) {
261 + if (hit == null) throw 'no ebuild matches "$label"';
262 + targetVer = hit.ver;
263 + targetRepo = hit.repo.name;
264 + targetPath = hit.repo.tree.ebuildPath(a.cat, a.pkg, hit.ver);
265 + }
266 +
267 + var key = a.cat + "/" + a.pkg + "-" + targetVer.raw;
268 + if (seen.exists(key)) return;
269 + seen.set(key, true);
270 +
271 + if (chain.length > 150) {
272 + throw "dependency chain too deep: " + chain.slice(-10).join(" -> ") + " -> " + key;
273 + }
274 + chain.push(key);
275 + log("resolve " + key);
276 +
277 + var activeRepo = null;
278 + for (r in repos.list) {
279 + if (r.name == targetRepo) {
280 + activeRepo = r;
281 + break;
282 + }
283 + }
284 + var tree = activeRepo != null ? activeRepo.tree : repos.list[0].tree;
285 + var flags = UseFlags.forPackage(a.cat, a.pkg, tree.iuse(targetPath));
286 +
287 + var runtime = Dependencies.eval(Dependencies.parse(tree.runtimeSpec(targetPath)), flags, chooser);
288 +
289 + var toResolve = runtime.copy();
290 + var build = Dependencies.eval(Dependencies.parse(tree.buildSpec(targetPath)), flags, chooser);
291 + for (d in build) {
292 + if (targetRoot == "/") {
293 + toResolve.push(d);
294 + } else if (!hostSatisfies(d)) {
295 + warn('build dependency not installed on host: ${d.cat}/${d.pkg}');
296 + }
297 + }
298 +
299 + var depLabels: Array<String> = [];
300 + for (d in toResolve) {
301 + var dl = d.cat + "/" + d.pkg;
302 + depLabels.push(dl);
303 + resolve(d, dl, false);
304 + }
305 +
306 + chain.pop();
307 + plan.push({cat: a.cat, pkg: a.pkg, ver: targetVer, path: targetPath, repo: targetRepo, deps: depLabels});
308 + }
309 +
310 + static function upgradeAvailable(a: Atom): Bool {
311 + var hit = repos.best(a);
312 + if (hit == null) return false;
313 + for (i in vdb.installed(a.cat, a.pkg)) {
314 + if (i.ver.compare(hit.ver) >= 0) return false;
315 + }
316 + return true;
317 + }
318 +
319 + static function syncRepos(only: Null<String>): Bool {
320 + Sys.println(c("1;32", only == null ? ">>> Syncing repositories ..." : ">>> Syncing " + only + " ..."));
321 + var ok = Sync.run(only);
322 + if (!ok) {
323 + Sys.println(c("1;31", ">>> Sync finished with errors"));
324 + return false;
325 + }
326 + Sys.println(c("1;32", ">>> Sync complete."));
327 + return true;
328 + }
329 +
330 + static function chooser(alts: Array<Array<Atom>>): Int {
331 + var firstResolvable = -1;
332 + for (i in 0...alts.length) {
333 + var resolvable = alts[i].length > 0;
334 + var installed = alts[i].length > 0;
335 + for (at in alts[i]) {
336 + if (repos.best(at) == null) {
337 + resolvable = false;
338 + break;
339 + }
340 + if (vdb.installed(at.cat, at.pkg).length == 0) installed = false;
341 + }
342 + if (!resolvable) continue;
343 + if (installed) return i;
344 + if (firstResolvable < 0) firstResolvable = i;
345 + }
346 + return firstResolvable < 0 ? 0 : firstResolvable;
347 + }
348 +
349 + static function status(vdb: VDB, e: Entry): Status {
350 + var inst = vdb.installed(e.cat, e.pkg);
351 + if (inst.length == 0) return {tag: "N", old: null};
352 +
353 + var top = inst[0];
354 + for (i in inst) {
355 + if (i.ver.compare(top.ver) > 0) top = i;
356 + }
357 +
358 + var cmp = e.ver.compare(top.ver);
359 + var oldStr = top.ver.raw + "::" + top.repo;
360 + if (cmp == 0) return {tag: "R", old: null};
361 + if (cmp > 0) return {tag: "U", old: oldStr};
362 + return {tag: "UD", old: oldStr};
363 + }
364 +
365 + static function showPlan(vdb: VDB, verbose: Bool) {
366 + if (plan.length == 0) {
367 + Sys.println(c("1;32", "Nothing to merge; all requested packages are installed."));
368 + return;
369 + }
370 +
371 + Sys.println(c("1;32", "These are the packages that would be merged, in order:"));
372 + Sys.println("");
373 +
374 + var counts = new Map<String, Int>();
375 + for (e in plan) {
376 + var st = status(vdb, e);
377 + counts.set(st.tag, (counts.exists(st.tag) ? counts.get(st.tag) : 0) + 1);
378 +
379 + var tagCol = switch st.tag {
380 + case "N": "32";
381 + case "U": "34";
382 + case "UD": "35";
383 + default: "33";
384 + };
385 +
386 + var line = c("1;32", "[ebuild ")
387 + + c(tagCol, " " + StringTools.rpad(st.tag, " ", 4) + " ")
388 + + c("1;32", "]")
389 + + " " + c("1;37", e.cat + "/" + e.pkg + "-" + e.ver.raw)
390 + + (showRepo ? c("0;36", "::" + e.repo) : "");
391 +
392 + if (st.old != null) {
393 + line += " " + c("0;36", "[" + st.old + "]");
394 + }
395 + Sys.println(line);
396 + }
397 +
398 + Sys.println("");
399 + var parts: Array<String> = [];
400 + if (counts.exists("U")) parts.push(counts.get("U") + " upgrade");
401 + if (counts.exists("UD")) parts.push(counts.get("UD") + " downgrade");
402 + if (counts.exists("N")) parts.push(counts.get("N") + " new");
403 + if (counts.exists("R")) parts.push(counts.get("R") + " reinstall");
404 +
405 + var plural = plan.length == 1 ? "" : "s";
406 + Sys.println("Total: " + plan.length + " package" + plural + " (" + parts.join(", ") + ")");
407 + }
408 +
409 + static function explain(spec: String): Void {
410 + var a = AtomParser.parse(spec);
411 + if (a == null) {
412 + Sys.println("invalid atom: " + spec);
413 + return;
414 + }
415 +
416 + var hit = repos.best(a);
417 + var chosen: Null<Version> = hit == null ? null : hit.ver;
418 +
419 + Sys.println("ACCEPT_KEYWORDS: " + Keywords.active().join(" "));
420 + for (r in repos.list) {
421 + var vs = r.tree.versions(a.cat, a.pkg);
422 + vs.sort(function(x, y) return y.compare(x));
423 + for (v in vs) {
424 + var kw = r.tree.variable(r.tree.ebuildPath(a.cat, a.pkg, v), "KEYWORDS");
425 + var masked = Mask.isMasked(a.cat, a.pkg, v);
426 + var kwOk = Keywords.acceptsWith(kw, Mask.extraKeywords(a.cat, a.pkg, v));
427 + var picked = chosen != null && chosen.compare(v) == 0;
428 + Sys.println(" " + a.cat + "/" + a.pkg + "-" + v.raw + "::" + r.name
429 + + " KEYWORDS=\"" + kw + "\""
430 + + " masked=" + masked
431 + + " keywords-ok=" + kwOk
432 + + (picked ? " <- picked" : ""));
433 + }
434 + }
435 + }
436 +
437 + static function expandSets(list: Array<String>): Array<String> {
438 + var out: Array<String> = [];
439 + for (arg in list) {
440 + if (arg == "@world") {
441 + for (a in readWorld()) out.push(a);
442 + } else {
443 + out.push(arg);
444 + }
445 + }
446 + return out;
447 + }
448 +
449 + static function readWorld(): Array<String> {
450 + var base = StringTools.endsWith(targetRoot, "/") ? targetRoot.substr(0, targetRoot.length - 1) : targetRoot;
451 + var path = base + "/var/lib/portage/world";
452 + if (!FileSystem.exists(path)) {
453 + Sys.println('no world file at ' + path);
454 + Sys.exit(1);
455 + }
456 +
457 + var out: Array<String> = [];
458 + for (raw in File.getContent(path).split("\n")) {
459 + var line = StringTools.trim(raw);
460 + if (line == "" || StringTools.startsWith(line, "#")) continue;
461 + if (AtomParser.parse(line) == null) {
462 + Sys.println('warning: skipping world entry "' + line + '"');
463 + continue;
464 + }
465 + out.push(line);
466 + }
467 + return out;
468 + }
469 +
470 + static function fullLabel(e: Entry): String {
471 + var base = e.cat + "/" + e.pkg + "-" + e.ver.raw;
472 + return showRepo ? base + "::" + e.repo : base;
473 + }
474 +
475 + static function expandShortFlags(args: Array<String>): Array<String> {
476 + var bundled = new EReg("^-[a-zA-Z]{2,}$", "");
477 + var out: Array<String> = [];
478 + for (arg in args) {
479 + if (bundled.match(arg)) {
480 + for (i in 1...arg.length) out.push("-" + arg.charAt(i));
481 + } else {
482 + out.push(arg);
483 + }
484 + }
485 + return out;
486 + }
487 +
488 + static function mergeOne(e: Entry, count: Int, total: Int): Bool {
489 + var pos = "(" + count + " of " + total + ") ";
490 +
491 + Sys.println("");
492 + Sys.println(c("1;32", ">>> Emerging " + pos) + c("1;37", fullLabel(e)));
493 +
494 + var build = Sys.command("ebuild", [e.path, "clean", "unpack", "prepare", "configure", "compile", "install"]);
495 + if (build != 0) {
496 + Sys.println(c("1;31", ">>> Failed " + pos + fullLabel(e) + " (build exit " + build + ")"));
497 + return false;
498 + }
499 +
500 + Sys.println(c("1;32", ">>> Installing " + pos) + c("1;37", fullLabel(e)));
501 +
502 + var merge = Sys.command("ebuild", [e.path, "qmerge"]);
503 + if (merge != 0) {
504 + Sys.println(c("1;31", ">>> Failed " + pos + fullLabel(e) + " (merge exit " + merge + ")"));
505 + return false;
506 + }
507 +
508 + Sys.println(c("1;32", ">>> Completed " + pos) + c("1;37", fullLabel(e)));
509 + return true;
510 + }
511 +
512 + static function main() {
513 + repos = Repository.load();
514 +
515 + var pretend = false;
516 + var ask = false;
517 + var atoms: Array<String> = [];
518 + var searchQuery: Null<String> = null;
519 + var searchDesc = false;
520 + var verbose = false;
521 +
522 + var args = expandShortFlags(Sys.args());
523 + var idx = 0;
524 + while (idx < args.length) {
525 + var arg = args[idx++];
526 + if (StringTools.startsWith(arg, "--sync=")) {
527 + syncFlag = true;
528 + syncOnly = arg.substr("--sync=".length);
529 + continue;
530 + }
531 + switch arg {
532 + case "--pretend" | "-p": pretend = true;
533 + case "--ask" | "-a": ask = true;
534 + case "--verbose" | "-v": verbose = true;
535 + case "--update" | "-u": update = true;
536 + case "--sync":
537 + syncFlag = true;
538 + if (idx < args.length && Sync.repoNames().indexOf(args[idx]) >= 0) {
539 + syncOnly = args[idx++];
540 + }
541 + case "--explain":
542 + if (idx < args.length) explainAtom = args[idx++];
543 + case "--search" | "-s":
544 + if (idx >= args.length) {
545 + Sys.println("--search needs a query");
546 + Sys.exit(1);
547 + }
548 + searchQuery = args[idx++];
549 + case "--desc" | "-S": searchDesc = true;
550 + case "--root":
551 + if (idx >= args.length) {
552 + Sys.println("--root needs a directory");
553 + Sys.exit(1);
554 + }
555 + targetRoot = args[idx++];
556 + default: atoms.push(arg);
557 + }
558 + }
559 +
560 + showRepo = verbose;
561 +
562 + if (searchQuery != null) {
563 + Search.run(repos, searchQuery, searchDesc);
564 + return;
565 + }
566 +
567 + if (explainAtom != null) {
568 + explain(explainAtom);
569 + return;
570 + }
571 +
572 + atoms = expandSets(atoms);
573 +
574 + if (syncFlag) {
575 + if (!isRoot()) {
576 + Sys.println("emerge-hx: superuser access is required for --sync");
577 + Sys.exit(1);
578 + }
579 + if (!syncRepos(syncOnly)) Sys.exit(1);
580 + indexCache = null;
581 + repos = Repository.load();
582 + if (atoms.length == 0) return;
583 + }
584 +
585 + if (atoms.length == 0) {
586 + Sys.println("usage: emerge-hx [--sync[=REPO]] [-u] [--pretend | --ask] [--root DIR] <atom> [atom...]");
587 + Sys.println(" emerge-hx --search <query> [--desc]");
588 + Sys.exit(1);
589 + }
590 +
591 + if (!pretend && !isRoot()) {
592 + if (!ask) {
593 + Sys.println("emerge-hx: superuser access is required");
594 + Sys.exit(1);
595 + }
596 + Sys.println("This action requires superuser access...");
597 + Sys.print("Would you like to add --pretend to options? [Yes/No] ");
598 + var choice = "";
599 + try {
600 + choice = Sys.stdin().readLine().toLowerCase();
601 + } catch (e: Dynamic) {
602 + choice = "";
603 + }
604 + if (choice != "y" && choice != "yes") {
605 + Sys.println("Aborted.");
606 + Sys.exit(1);
607 + }
608 + pretend = true;
609 + }
610 +
611 + vdb = new VDB(targetRoot);
612 + hostVdb = new VDB("/");
613 +
614 + startProgress();
615 +
616 + for (arg in atoms) {
617 + var spec = NAME_RE.match(arg) ? resolveName(arg) : arg;
618 +
619 + var a = AtomParser.parse(spec);
620 + if (a == null) {
621 + clearProgress();
622 + Sys.println('invalid atom: $arg');
623 + Sys.exit(1);
624 + }
625 + try {
626 + resolve(a, spec, true);
627 + } catch (e: Dynamic) {
628 + clearProgress();
629 + Sys.println('error: $e');
630 + Sys.exit(1);
631 + }
632 + }
633 +
634 + finishProgress();
635 + Sys.println("");
636 +
637 + showPlan(vdb, verbose);
638 +
639 + if (pretend || plan.length == 0) return;
640 +
641 + if (ask) {
642 + Sys.print("\nWould you like to merge these packages? [Yes/No] ");
643 + var answer = "";
644 + try {
645 + answer = Sys.stdin().readLine();
646 + } catch (e: Dynamic) {
647 + answer = "";
648 + }
649 + answer = answer.toLowerCase();
650 + if (answer != "y" && answer != "yes") {
651 + Sys.println("Aborted.");
652 + return;
653 + }
654 + }
655 +
656 + Sys.putEnv("ROOT", targetRoot);
657 + var total = plan.length;
658 + for (n in 0...total) {
659 + if (!mergeOne(plan[n], n + 1, total)) Sys.exit(1);
660 + }
661 + }
662 +}
+12 −0 src/import.hx new file mode 100644
@@ -0,0 +1,12 @@
1 +import portage.Atom;
2 +import portage.Atom.AtomParser;
3 +import portage.Dependencies;
4 +import portage.Keywords;
5 +import portage.Mask;
6 +import portage.Repository;
7 +import portage.Search;
8 +import portage.Sync;
9 +import portage.Tree;
10 +import portage.UseFlags;
11 +import portage.VDB;
12 +import portage.Version;
+73 −0 src/portage/Atom.hx new file mode 100644
@@ -0,0 +1,73 @@
1 +package portage;
2 +
3 +typedef Atom = {
4 + op: String,
5 + cat: String,
6 + pkg: String,
7 + ver: Null<Version>,
8 + slot: Null<String>,
9 + glob: Bool
10 +}
11 +
12 +class AtomParser {
13 + static final USE_DEP = ~/\[[^\]]*\]$/;
14 + static final RE = ~/^([<>=~]{0,2})([a-zA-Z0-9][a-zA-Z0-9_+.-]*)\/([a-zA-Z0-9_+][a-zA-Z0-9_+-]*?)(?:-([0-9][^:\[]*))?(?::([^\[]+))?(?:\[[^\]]*\])?$/;
15 + static final OPS = ["", "=", ">=", "<=", ">", "<", "~"];
16 +
17 + public static function parse(s: String): Null<Atom> {
18 + var stripped = USE_DEP.replace(s, "");
19 + if (!RE.match(stripped)) return null;
20 +
21 + var op = RE.matched(1);
22 + if (OPS.indexOf(op) < 0) return null;
23 +
24 + var verStr: Null<String> = RE.matched(4);
25 + var glob = false;
26 + if (verStr != null && StringTools.endsWith(verStr, "*")) {
27 + if (op != "=") return null;
28 + glob = true;
29 + verStr = verStr.substr(0, verStr.length - 1);
30 + }
31 +
32 + var ver: Null<Version> = null;
33 + if (verStr != null) {
34 + ver = Version.parse(verStr);
35 + if (ver == null) return null;
36 + }
37 + if (op != "" && ver == null) return null;
38 +
39 + return {
40 + op: op,
41 + cat: RE.matched(2),
42 + pkg: RE.matched(3),
43 + ver: ver,
44 + slot: RE.matched(5),
45 + glob: glob
46 + };
47 + }
48 +
49 + public static function matches(a: Atom, v: Version): Bool {
50 + if (a.ver == null) return true;
51 + var target = a.ver;
52 +
53 + if (a.glob) {
54 + return StringTools.startsWith(v.raw, target.raw);
55 + }
56 +
57 + if (a.op == "~") {
58 + var lhs = new Version(v.components, v.letter, v.suffixes, 0, v.raw);
59 + var rhs = new Version(target.components, target.letter, target.suffixes, 0, target.raw);
60 + return lhs.compare(rhs) == 0;
61 + }
62 +
63 + var c = v.compare(target);
64 + return switch a.op {
65 + case "" | "=": c == 0;
66 + case ">=": c >= 0;
67 + case "<=": c <= 0;
68 + case ">": c > 0;
69 + case "<": c < 0;
70 + default: false;
71 + };
72 + }
73 +}
+105 −0 src/portage/Dependencies.hx new file mode 100644
@@ -0,0 +1,105 @@
1 +package portage;
2 +
3 +enum Dep {
4 + Leaf(a: Atom);
5 + AllOf(kids: Array<Dep>);
6 + AnyOf(alts: Array<Dep>);
7 + Cond(flag: String, negate: Bool, kids: Array<Dep>);
8 +}
9 +
10 +typedef Chooser = Array<Array<Atom>> -> Int;
11 +
12 +class Dependencies {
13 + static var trace = Sys.getEnv("EMERGE_TRACE") != null;
14 + static final USE_BRACKETS = ~/\[[^\]]*\]/g;
15 +
16 + final toks: Array<String>;
17 + var pos = 0;
18 +
19 + function new(s: String) {
20 + var noUse = USE_BRACKETS.replace(s, "");
21 + var spaced = StringTools.replace(StringTools.replace(noUse, "(", " ( "), ")", " ) ");
22 + toks = [for (t in ~/[ \t\r\n]+/g.split(spaced)) if (t != "") t];
23 + }
24 +
25 + public static function parse(s: String): Array<Dep> {
26 + return new Dependencies(s).items(false);
27 + }
28 +
29 + function items(inGroup: Bool): Array<Dep> {
30 + var out: Array<Dep> = [];
31 + while (pos < toks.length) {
32 + var t = toks[pos++];
33 + if (trace) {
34 + Sys.stderr().writeString(" tok[" + (pos - 1) + "] " + t + "\n");
35 + Sys.stderr().flush();
36 + }
37 +
38 + if (t == ")") {
39 + if (inGroup) return out;
40 + continue;
41 + }
42 + if (t == "(") {
43 + out.push(AllOf(items(true)));
44 + } else if (t == "||") {
45 + out.push(AnyOf(group()));
46 + } else if (StringTools.endsWith(t, "?")) {
47 + var neg = StringTools.startsWith(t, "!");
48 + var start = neg ? 1 : 0;
49 + var flag = t.substr(start, t.length - start - 1);
50 + out.push(Cond(flag, neg, group()));
51 + } else if (StringTools.startsWith(t, "!")) {
52 +
53 + } else {
54 + var a = AtomParser.parse(t);
55 + if (a == null) {
56 + if (t.indexOf("/") >= 0) Sys.println('warning: skipping unparseable dependency "$t"');
57 + continue;
58 + }
59 + out.push(Leaf(a));
60 + }
61 + }
62 + return out;
63 + }
64 +
65 + function group(): Array<Dep> {
66 + if (pos < toks.length && toks[pos] == "(") {
67 + pos++;
68 + return items(true);
69 + }
70 + throw 'expected "(" in dependency string';
71 + }
72 +
73 + public static function eval(deps: Array<Dep>, use: Map<String, Bool>, pick: Chooser): Array<Atom> {
74 + var out: Array<Atom> = [];
75 + for (d in deps) evalOne(d, use, pick, out);
76 + return out;
77 + }
78 +
79 + static function evalOne(d: Dep, use: Map<String, Bool>, pick: Chooser, out: Array<Atom>): Void {
80 + switch d {
81 + case Leaf(a):
82 + out.push(a);
83 + case AllOf(kids):
84 + for (k in kids) evalOne(k, use, pick, out);
85 + case Cond(flag, negate, kids):
86 + var on = use.get(flag) == true;
87 + if (on != negate) for (k in kids) evalOne(k, use, pick, out);
88 + case AnyOf(alts):
89 + if (alts.length == 0) return;
90 + var options: Array<Array<Atom>> = [];
91 + for (alt in alts) {
92 + var acc: Array<Atom> = [];
93 + evalOne(alt, use, pick, acc);
94 + options.push(acc);
95 + }
96 + if (trace) {
97 + Sys.stderr().writeString(" || group with " + options.length + " alternatives\n");
98 + Sys.stderr().flush();
99 + }
100 + var i = pick(options);
101 + if (i < 0 || i >= options.length) i = 0;
102 + for (x in options[i]) out.push(x);
103 + }
104 + }
105 +}
+78 −0 src/portage/Keywords.hx new file mode 100644
@@ -0,0 +1,78 @@
1 +package portage;
2 +
3 +import sys.FileSystem;
4 +import sys.io.File;
5 +import sys.io.Process;
6 +
7 +class Keywords {
8 + static var tokens: Null<Array<String>> = null;
9 +
10 + static function load(): Array<String> {
11 + if (tokens != null) return tokens;
12 +
13 + var value = Sys.getEnv("ACCEPT_KEYWORDS");
14 + if (value == null) value = fromFile("/etc/portage/make.conf");
15 + if (value == null) value = fromFile("/etc/make.conf");
16 +
17 + var list = value == null ? [] : split(value);
18 + if (list.length == 0) list = [defaultArch()];
19 + tokens = list;
20 + return tokens;
21 + }
22 +
23 + static function fromFile(path: String): Null<String> {
24 + if (!FileSystem.exists(path)) return null;
25 +
26 + var text = "\n" + File.getContent(path);
27 + var re = new EReg("\nACCEPT_KEYWORDS=[\"']?([^\"'\n]*)[\"']?", "");
28 + var found: Null<String> = null;
29 + var rest = text;
30 + while (re.match(rest)) {
31 + found = re.matched(1);
32 + rest = re.matchedRight();
33 + }
34 + return found;
35 + }
36 +
37 + static function defaultArch(): String {
38 + try {
39 + var p = new Process("uname", ["-m"]);
40 + var m = StringTools.trim(p.stdout.readAll().toString());
41 + p.close();
42 + return switch m {
43 + case "x86_64": "amd64";
44 + case "aarch64": "arm64";
45 + case "i686" | "i386": "x86";
46 + default: "amd64";
47 + };
48 + } catch (e: Dynamic) {
49 + return "amd64";
50 + }
51 + }
52 +
53 + public static function active(): Array<String> {
54 + return load();
55 + }
56 +
57 + static function split(s: String): Array<String> {
58 + return [for (t in ~/[ \t\r\n]+/g.split(s)) if (t != "") t];
59 + }
60 +
61 + public static function accepts(keywordString: String): Bool {
62 + return acceptsWith(keywordString, []);
63 + }
64 +
65 + public static function acceptsWith(keywordString: String, extra: Array<String>): Bool {
66 + var keywords = split(keywordString);
67 + var accept = load().concat(extra);
68 + if (accept.indexOf("**") >= 0) return true;
69 + if (keywords.length == 0) return false;
70 +
71 + for (k in keywords) {
72 + if (StringTools.startsWith(k, "-")) continue;
73 + if (accept.indexOf(k) >= 0) return true;
74 + if (!StringTools.startsWith(k, "~") && accept.indexOf("~" + k) >= 0) return true;
75 + }
76 + return false;
77 + }
78 +}
+132 −0 src/portage/Mask.hx new file mode 100644
@@ -0,0 +1,132 @@
1 +package portage;
2 +
3 +import sys.FileSystem;
4 +import sys.io.File;
5 +import sys.io.Process;
6 +
7 +typedef KeywordRule = {
8 + atom: Atom,
9 + keywords: Array<String>
10 +}
11 +
12 +class Mask {
13 + static var loaded = false;
14 + static var masks: Array<Atom> = [];
15 + static var unmasks: Array<Atom> = [];
16 + static var keywordRules: Array<KeywordRule> = [];
17 +
18 + static function load(): Void {
19 + if (loaded) return;
20 + loaded = true;
21 +
22 + for (dir in profileDirs()) {
23 + for (line in linesOf(dir + "/package.mask")) {
24 + if (StringTools.startsWith(line, "-")) {
25 + var a = AtomParser.parse(line.substr(1));
26 + if (a != null) unmasks.push(a);
27 + } else {
28 + var a = AtomParser.parse(line);
29 + if (a != null) masks.push(a);
30 + }
31 + }
32 + }
33 +
34 + for (line in configLines("/etc/portage/package.mask")) {
35 + var a = AtomParser.parse(line);
36 + if (a != null) masks.push(a);
37 + }
38 +
39 + for (line in configLines("/etc/portage/package.unmask")) {
40 + var a = AtomParser.parse(line);
41 + if (a != null) unmasks.push(a);
42 + }
43 +
44 + for (line in configLines("/etc/portage/package.accept_keywords")) {
45 + var words = split(line);
46 + if (words.length < 2) continue;
47 + var a = AtomParser.parse(words[0]);
48 + if (a == null) continue;
49 + keywordRules.push({atom: a, keywords: words.slice(1)});
50 + }
51 + }
52 +
53 + static function split(s: String): Array<String> {
54 + return [for (t in ~/[ \t\r\n]+/g.split(s)) if (t != "") t];
55 + }
56 +
57 + static function matchesAny(list: Array<Atom>, cat: String, pkg: String, v: Version): Bool {
58 + for (a in list) {
59 + if (a.cat == cat && a.pkg == pkg && AtomParser.matches(a, v)) return true;
60 + }
61 + return false;
62 + }
63 +
64 + public static function isMasked(cat: String, pkg: String, v: Version): Bool {
65 + load();
66 + return matchesAny(masks, cat, pkg, v) && !matchesAny(unmasks, cat, pkg, v);
67 + }
68 +
69 + public static function extraKeywords(cat: String, pkg: String, v: Version): Array<String> {
70 + load();
71 + var out: Array<String> = [];
72 + for (r in keywordRules) {
73 + if (r.atom.cat == cat && r.atom.pkg == pkg && AtomParser.matches(r.atom, v)) {
74 + for (k in r.keywords) out.push(k);
75 + }
76 + }
77 + return out;
78 + }
79 +
80 + static function linesOf(path: String): Array<String> {
81 + var out: Array<String> = [];
82 + if (!FileSystem.exists(path) || FileSystem.isDirectory(path)) return out;
83 + for (raw in File.getContent(path).split("\n")) {
84 + var line = StringTools.trim(raw);
85 + if (line == "" || StringTools.startsWith(line, "#")) continue;
86 + out.push(line);
87 + }
88 + return out;
89 + }
90 +
91 + static function configLines(path: String): Array<String> {
92 + if (!FileSystem.exists(path)) return [];
93 + if (!FileSystem.isDirectory(path)) return linesOf(path);
94 +
95 + var names = FileSystem.readDirectory(path);
96 + names.sort(function(a, b) return a < b ? -1 : (a > b ? 1 : 0));
97 + var out: Array<String> = [];
98 + for (n in names) {
99 + for (line in linesOf(path + "/" + n)) out.push(line);
100 + }
101 + return out;
102 + }
103 +
104 + static function profileDirs(): Array<String> {
105 + var out: Array<String> = [];
106 + var link = realpath("/etc/portage/make.profile");
107 + if (link != null) collectProfile(link, out, 0);
108 + return out;
109 + }
110 +
111 + static function collectProfile(dir: String, out: Array<String>, depth: Int): Void {
112 + if (depth > 20) return;
113 + var parentFile = dir + "/parent";
114 + for (line in linesOf(parentFile)) {
115 + var p = dir + "/" + line;
116 + if (FileSystem.exists(p) && FileSystem.isDirectory(p)) collectProfile(p, out, depth + 1);
117 + }
118 + out.push(dir);
119 + }
120 +
121 + static function realpath(p: String): Null<String> {
122 + if (!FileSystem.exists(p)) return null;
123 + try {
124 + var proc = new Process("readlink", ["-f", p]);
125 + var out = StringTools.trim(proc.stdout.readAll().toString());
126 + proc.close();
127 + return out == "" ? null : out;
128 + } catch (e: Dynamic) {
129 + return null;
130 + }
131 + }
132 +}
+108 −0 src/portage/Repository.hx new file mode 100644
@@ -0,0 +1,108 @@
1 +package portage;
2 +
3 +import haxe.io.Path;
4 +import sys.FileSystem;
5 +import sys.io.File;
6 +
7 +typedef RepoInfo = {
8 + name: String,
9 + tree: Tree
10 +}
11 +
12 +typedef Found = {
13 + repo: RepoInfo,
14 + ver: Version
15 +}
16 +
17 +typedef RepoConf = {
18 + name: String,
19 + location: String
20 +}
21 +
22 +class Repository {
23 + public final list: Array<RepoInfo>;
24 +
25 + public function new(list: Array<RepoInfo>) {
26 + this.list = list;
27 + }
28 +
29 + public static function load(): Repository {
30 + var over = Sys.getEnv("EMERGE_REPO");
31 + if (over != null) {
32 + return new Repository([{name: Path.withoutDirectory(over), tree: new Tree(over)}]);
33 + }
34 +
35 + var seen = new Map<String, Bool>();
36 + var list: Array<RepoInfo> = [];
37 + for (f in confFiles()) {
38 + for (conf in parseRepos(f)) {
39 + if (seen.exists(conf.name)) continue;
40 + if (!FileSystem.exists(conf.location)) continue;
41 + seen.set(conf.name, true);
42 + list.push({name: conf.name, tree: new Tree(conf.location)});
43 + }
44 + }
45 +
46 + if (list.length == 0) {
47 + list.push({name: "gentoo", tree: new Tree("/var/db/repos/gentoo")});
48 + }
49 + return new Repository(list);
50 + }
51 +
52 + public function best(a: Atom): Null<Found> {
53 + var found: Null<Found> = null;
54 + for (r in list) {
55 + var v = r.tree.best(a);
56 + if (v == null) continue;
57 + if (found == null || v.compare(found.ver) > 0) {
58 + found = {repo: r, ver: v};
59 + }
60 + }
61 + return found;
62 + }
63 +
64 + static function confFiles(): Array<String> {
65 + var out: Array<String> = [];
66 + for (p in ["/etc/portage/repos.conf", "/usr/share/portage/config/repos.conf"]) {
67 + if (!FileSystem.exists(p)) continue;
68 + if (FileSystem.isDirectory(p)) {
69 + var names = FileSystem.readDirectory(p);
70 + names.sort(function(a, b) return a < b ? -1 : (a > b ? 1 : 0));
71 + for (n in names) out.push(p + "/" + n);
72 + } else {
73 + out.push(p);
74 + }
75 + }
76 + return out;
77 + }
78 +
79 + static function parseRepos(file: String): Array<RepoConf> {
80 + var out: Array<RepoConf> = [];
81 + var name: Null<String> = null;
82 + var location: Null<String> = null;
83 +
84 + for (raw in File.getContent(file).split("\n")) {
85 + var line = StringTools.trim(raw);
86 + if (line == "" || StringTools.startsWith(line, "#")) continue;
87 +
88 + if (StringTools.startsWith(line, "[") && StringTools.endsWith(line, "]")) {
89 + if (name != null && name != "DEFAULT" && location != null) {
90 + out.push({name: name, location: location});
91 + }
92 + name = line.substr(1, line.length - 2);
93 + location = null;
94 + continue;
95 + }
96 +
97 + var eq = line.indexOf("=");
98 + if (eq < 0 || name == null) continue;
99 + var key = StringTools.trim(line.substr(0, eq));
100 + if (key == "location") location = StringTools.trim(line.substr(eq + 1));
101 + }
102 +
103 + if (name != null && name != "DEFAULT" && location != null) {
104 + out.push({name: name, location: location});
105 + }
106 + return out;
107 + }
108 +}
\ No newline at end of file
+77 −0 src/portage/Search.hx new file mode 100644
@@ -0,0 +1,77 @@
1 +package portage;
2 +
3 +import sys.FileSystem;
4 +import sys.io.File;
5 +
6 +class Search {
7 + static final SKIP_DIRS = ["profiles", "eclass", "metadata", "licenses", "scripts", "packages", "distfiles"];
8 +
9 + public static function run(repos: Repository, query: String, searchDesc: Bool): Void {
10 + var q = query.toLowerCase();
11 + var hits = 0;
12 + var seen = new Map<String, Bool>();
13 +
14 + for (r in repos.list) {
15 + var tree = r.tree;
16 + if (!FileSystem.exists(tree.root)) continue;
17 +
18 + var cats = FileSystem.readDirectory(tree.root);
19 + cats.sort(function(a, b) return a < b ? -1 : (a > b ? 1 : 0));
20 +
21 + for (cat in cats) {
22 + if (StringTools.startsWith(cat, ".") || SKIP_DIRS.indexOf(cat) >= 0) continue;
23 + var catDir = tree.root + "/" + cat;
24 + if (!FileSystem.isDirectory(catDir)) continue;
25 +
26 + var pkgs = FileSystem.readDirectory(catDir);
27 + pkgs.sort(function(a, b) return a < b ? -1 : (a > b ? 1 : 0));
28 +
29 + for (pkg in pkgs) {
30 + if (StringTools.startsWith(pkg, ".")) continue;
31 + if (!FileSystem.isDirectory(catDir + "/" + pkg)) continue;
32 +
33 + var label = cat + "/" + pkg;
34 + if (seen.exists(label)) continue;
35 +
36 + var nameHit = label.toLowerCase().indexOf(q) >= 0;
37 + var latest = latestVersion(tree, cat, pkg);
38 + if (latest == null) continue;
39 +
40 + var desc = describe(tree, cat, pkg, latest);
41 + var descHit = searchDesc && desc != null && desc.toLowerCase().indexOf(q) >= 0;
42 + if (!nameHit && !descHit) continue;
43 +
44 + seen.set(label, true);
45 + hits++;
46 + Sys.println("* " + label + "::" + r.name);
47 + Sys.println(" Latest version available: " + latest.raw);
48 + if (desc != null) Sys.println(" Description: " + desc);
49 + Sys.println("");
50 + }
51 + }
52 + }
53 +
54 + if (hits == 0) Sys.println('No packages matching "$query".');
55 + else Sys.println(hits + " package(s) found.");
56 + }
57 +
58 + static function latestVersion(tree: Tree, cat: String, pkg: String): Null<Version> {
59 + var best: Null<Version> = null;
60 + var liveBest: Null<Version> = null;
61 + for (v in tree.versions(cat, pkg)) {
62 + if (StringTools.contains(v.raw, "9999")) {
63 + if (liveBest == null || v.compare(liveBest) > 0) liveBest = v;
64 + continue;
65 + }
66 + if (best == null || v.compare(best) > 0) best = v;
67 + }
68 + return best != null ? best : liveBest;
69 + }
70 +
71 + static final DESC_RE = ~/\nDESCRIPTION="([^"]*)"/;
72 +
73 + static function describe(tree: Tree, cat: String, pkg: String, v: Version): Null<String> {
74 + var text = "\n" + File.getContent(tree.ebuildPath(cat, pkg, v));
75 + return DESC_RE.match(text) ? DESC_RE.matched(1) : null;
76 + }
77 +}
\ No newline at end of file
+137 −0 src/portage/Sync.hx new file mode 100644
@@ -0,0 +1,137 @@
1 +package portage;
2 +
3 +import haxe.io.Path;
4 +import sys.FileSystem;
5 +import sys.io.File;
6 +
7 +typedef SyncRepo = {
8 + name: String,
9 + location: String,
10 + type: String,
11 + uri: String
12 +}
13 +
14 +class Sync {
15 + public static function repoNames(): Array<String> {
16 + var out: Array<String> = [];
17 + for (f in confFiles()) {
18 + for (r in parseRepos(f)) {
19 + if (out.indexOf(r.name) < 0) out.push(r.name);
20 + }
21 + }
22 + return out;
23 + }
24 +
25 + public static function run(only: Null<String>): Bool {
26 + if (only != null && repoNames().indexOf(only) < 0) {
27 + Sys.println('no repo named "' + only + '" in repos.conf. Known repos: ' + repoNames().join(", "));
28 + return false;
29 + }
30 +
31 + var ok = true;
32 + var seen = new Map<String, Bool>();
33 + for (f in confFiles()) {
34 + for (r in parseRepos(f)) {
35 + if (seen.exists(r.name)) continue;
36 + seen.set(r.name, true);
37 + if (only != null && r.name != only) continue;
38 +
39 + if (r.location == "" || r.uri == "" || r.type == "") {
40 + Sys.println(">>> " + r.name + ": no sync-type/sync-uri/location in repos.conf, skipped");
41 + continue;
42 + }
43 + if (!syncOne(r)) ok = false;
44 + }
45 + }
46 + return ok;
47 + }
48 +
49 + static function syncOne(r: SyncRepo): Bool {
50 + Sys.println(">>> Syncing " + r.name + " (" + r.type + ") from " + r.uri);
51 + return switch r.type {
52 + case "git": syncGit(r);
53 + case "rsync": syncRsync(r);
54 + default:
55 + Sys.println(">>> sync-type " + r.type + " is not supported natively yet; skipped");
56 + true;
57 + };
58 + }
59 +
60 + static function syncGit(r: SyncRepo): Bool {
61 + if (FileSystem.exists(r.location + "/.git")) {
62 + return exec("git", ["-C", r.location, "pull", "--ff-only"]);
63 + }
64 + if (FileSystem.exists(r.location) && FileSystem.readDirectory(r.location).length > 0) {
65 + Sys.println(">>> " + r.location + " exists and is not a git checkout; not cloning into it");
66 + return false;
67 + }
68 + FileSystem.createDirectory(Path.directory(r.location));
69 + return exec("git", ["clone", "--depth", "1", r.uri, r.location]);
70 + }
71 +
72 + static function syncRsync(r: SyncRepo): Bool {
73 + FileSystem.createDirectory(r.location);
74 + return exec("rsync", [
75 + "-rlpt", "--delete", "--timeout=180", "--safe-links",
76 + "--exclude=/distfiles", "--exclude=/packages", "--exclude=/local",
77 + withSlash(r.uri), withSlash(r.location)
78 + ]);
79 + }
80 +
81 + static function withSlash(s: String): String {
82 + return StringTools.endsWith(s, "/") ? s : s + "/";
83 + }
84 +
85 + static function exec(cmd: String, args: Array<String>): Bool {
86 + try {
87 + return Sys.command(cmd, args) == 0;
88 + } catch (e: Dynamic) {
89 + Sys.println(">>> " + cmd + " not found. Install it to sync this repository.");
90 + return false;
91 + }
92 + }
93 +
94 + static function confFiles(): Array<String> {
95 + var out: Array<String> = [];
96 + for (p in ["/etc/portage/repos.conf", "/usr/share/portage/config/repos.conf"]) {
97 + if (!FileSystem.exists(p)) continue;
98 + if (FileSystem.isDirectory(p)) {
99 + var names = FileSystem.readDirectory(p);
100 + names.sort(function(a, b) return a < b ? -1 : (a > b ? 1 : 0));
101 + for (n in names) out.push(p + "/" + n);
102 + } else {
103 + out.push(p);
104 + }
105 + }
106 + return out;
107 + }
108 +
109 + static function parseRepos(file: String): Array<SyncRepo> {
110 + var out: Array<SyncRepo> = [];
111 + var cur: Null<SyncRepo> = null;
112 +
113 + for (raw in File.getContent(file).split("\n")) {
114 + var line = StringTools.trim(raw);
115 + if (line == "" || StringTools.startsWith(line, "#")) continue;
116 +
117 + if (StringTools.startsWith(line, "[") && StringTools.endsWith(line, "]")) {
118 + if (cur != null && cur.name != "DEFAULT") out.push(cur);
119 + cur = {name: line.substr(1, line.length - 2), location: "", type: "", uri: ""};
120 + continue;
121 + }
122 +
123 + var eq = line.indexOf("=");
124 + if (eq < 0 || cur == null) continue;
125 + var key = StringTools.trim(line.substr(0, eq));
126 + var val = StringTools.trim(line.substr(eq + 1));
127 + switch key {
128 + case "location": cur.location = val;
129 + case "sync-type": cur.type = val;
130 + case "sync-uri": cur.uri = val;
131 + }
132 + }
133 +
134 + if (cur != null && cur.name != "DEFAULT") out.push(cur);
135 + return out;
136 + }
137 +}
+129 −0 src/portage/Tree.hx new file mode 100644
@@ -0,0 +1,129 @@
1 +package portage;
2 +
3 +import sys.FileSystem;
4 +import sys.io.File;
5 +import haxe.io.Path;
6 +
7 +class Tree {
8 + static inline var EXPAND_BUDGET = 5000;
9 +
10 + public final root: String;
11 + var budget = 0;
12 +
13 + public function new(root: String) {
14 + this.root = root;
15 + }
16 +
17 + public function versions(cat: String, pkg: String): Array<Version> {
18 + var dir = root + "/" + cat + "/" + pkg;
19 + if (!FileSystem.exists(dir) || !FileSystem.isDirectory(dir)) return [];
20 +
21 + var out: Array<Version> = [];
22 + for (f in FileSystem.readDirectory(dir)) {
23 + if (!StringTools.endsWith(f, ".ebuild")) continue;
24 + var base = f.substr(0, f.length - ".ebuild".length);
25 + if (!StringTools.startsWith(base, pkg + "-")) continue;
26 + var v = Version.parse(base.substr(pkg.length + 1));
27 + if (v != null) out.push(v);
28 + }
29 + return out;
30 + }
31 +
32 + public function best(a: Atom): Null<Version> {
33 + var found: Null<Version> = null;
34 + for (v in versions(a.cat, a.pkg)) {
35 + if (StringTools.contains(v.raw, "9999")) continue;
36 + if (!AtomParser.matches(a, v)) continue;
37 + if (Mask.isMasked(a.cat, a.pkg, v)) continue;
38 +
39 + var keywords = variable(ebuildPath(a.cat, a.pkg, v), "KEYWORDS");
40 + if (!Keywords.acceptsWith(keywords, Mask.extraKeywords(a.cat, a.pkg, v))) continue;
41 +
42 + if (found == null || v.compare(found) > 0) found = v;
43 + }
44 + return found;
45 + }
46 +
47 + public function ebuildPath(cat: String, pkg: String, v: Version): String {
48 + return root + "/" + cat + "/" + pkg + "/" + pkg + "-" + v.raw + ".ebuild";
49 + }
50 +
51 + function cachedVars(path: String): Null<Map<String, String>> {
52 + var pkgDir = Path.directory(path);
53 + var cat = Path.withoutDirectory(Path.directory(pkgDir));
54 + var entry = Path.withoutExtension(Path.withoutDirectory(path));
55 + var file = root + "/metadata/md5-cache/" + cat + "/" + entry;
56 + if (!FileSystem.exists(file)) return null;
57 +
58 + var m = new Map<String, String>();
59 + for (line in File.getContent(file).split("\n")) {
60 + var eq = line.indexOf("=");
61 + if (eq <= 0) continue;
62 + m.set(line.substr(0, eq), line.substr(eq + 1));
63 + }
64 + return m;
65 + }
66 +
67 + public function variable(path: String, name: String): String {
68 + var cache = cachedVars(path);
69 + if (cache != null) return cache.exists(name) ? cache.get(name) : "";
70 +
71 + var text = File.getContent(path);
72 + budget = EXPAND_BUDGET;
73 + return expandVars(rawVariable(text, name), text, path, 0);
74 + }
75 +
76 + static function rawVariable(text: String, name: String): String {
77 + var re = new EReg("\n" + name + "=\"([^\"]*)\"", "");
78 + return re.match("\n" + text) ? re.matched(1) : "";
79 + }
80 +
81 + static function builtins(path: String): Map<String, String> {
82 + var m = new Map<String, String>();
83 + var pkg = Path.withoutDirectory(Path.directory(path));
84 + var pf = Path.withoutExtension(Path.withoutDirectory(path));
85 + var pv = pf.substr(pkg.length + 1);
86 + var pr = "r0";
87 + var rev = new EReg("-(r[0-9]+)$", "");
88 + if (rev.match(pv)) {
89 + pr = rev.matched(1);
90 + pv = rev.replace(pv, "");
91 + }
92 + m.set("PN", pkg);
93 + m.set("PV", pv);
94 + m.set("PR", pr);
95 + m.set("P", pkg + "-" + pv);
96 + m.set("PF", pf);
97 + m.set("PVR", pr == "r0" ? pv : pv + "-" + pr);
98 + return m;
99 + }
100 +
101 + function expandVars(s: String, text: String, path: String, depth: Int): String {
102 + if (depth > 8) return s;
103 + if (budget <= 0) return s;
104 + var b = builtins(path);
105 + var re = new EReg("\\$\\{([A-Za-z_][A-Za-z0-9_]*)\\}", "g");
106 + return re.map(s, function(r) {
107 + budget--;
108 + var name = r.matched(1);
109 + if (b.exists(name)) return b.get(name);
110 + return expandVars(rawVariable(text, name), text, path, depth + 1);
111 + });
112 + }
113 +
114 + public function depSpec(path: String): String {
115 + return variable(path, "DEPEND") + " " + variable(path, "RDEPEND") + " " + variable(path, "BDEPEND");
116 + }
117 +
118 + public function runtimeSpec(path: String): String {
119 + return variable(path, "RDEPEND");
120 + }
121 +
122 + public function buildSpec(path: String): String {
123 + return variable(path, "DEPEND") + " " + variable(path, "BDEPEND");
124 + }
125 +
126 + public function iuse(path: String): Array<String> {
127 + return [for (t in ~/[ \t\r\n]+/g.split(variable(path, "IUSE"))) if (t != "") t];
128 + }
129 +}
+166 −0 src/portage/UseFlags.hx new file mode 100644
@@ -0,0 +1,166 @@
1 +package portage;
2 +
3 +import sys.FileSystem;
4 +import sys.io.File;
5 +import sys.io.Process;
6 +
7 +class UseFlags {
8 + static final EXPANDS: Array<{name: String, prefix: String}> = [
9 + {name: "ELIBC", prefix: "elibc_"},
10 + {name: "KERNEL", prefix: "kernel_"},
11 + {name: "PYTHON_TARGETS", prefix: "python_targets_"},
12 + {name: "PYTHON_SINGLE_TARGET", prefix: "python_single_target_"},
13 + {name: "CPU_FLAGS_X86", prefix: "cpu_flags_x86_"},
14 + ];
15 +
16 + static var globalFlags: Null<Map<String, Bool>> = null;
17 + static var packageFlags: Null<Map<String, Array<String>>> = null;
18 +
19 + public static function forPackage(cat: String, pkg: String, iuse: Array<String>): Map<String, Bool> {
20 + var m = global().copy();
21 + for (tok in iuse) {
22 + if (StringTools.startsWith(tok, "+")) {
23 + var f = tok.substr(1);
24 + if (!m.exists(f)) m.set(f, true);
25 + } else if (StringTools.startsWith(tok, "-")) {
26 + var f = tok.substr(1);
27 + if (!m.exists(f)) m.set(f, false);
28 + }
29 + }
30 + if (packageFlags == null) packageFlags = loadPackageUse();
31 + var key = cat + "/" + pkg;
32 + if (packageFlags.exists(key)) applyTokens(m, packageFlags.get(key));
33 + return m;
34 + }
35 +
36 + public static function global(): Map<String, Bool> {
37 + if (globalFlags != null) return globalFlags;
38 +
39 + var m = new Map<String, Bool>();
40 + var vars = new Map<String, String>();
41 +
42 + for (dir in profileDirs()) readVars(dir + "/make.defaults", m, vars);
43 + for (p in ["/etc/portage/make.conf", "/etc/make.conf"]) {
44 + if (FileSystem.exists(p)) {
45 + readVars(p, m, vars);
46 + break;
47 + }
48 + }
49 +
50 + for (e in EXPANDS) {
51 + var val: Null<String> = vars.get(e.name);
52 + if (val == null) continue;
53 + for (t in splitWords(val)) {
54 + if (t.indexOf("$") < 0) m.set(e.prefix + t.toLowerCase(), true);
55 + }
56 + }
57 + var arch: Null<String> = vars.get("ARCH");
58 + if (arch != null) {
59 + for (t in splitWords(arch)) {
60 + if (t.indexOf("$") < 0) m.set(t, true);
61 + }
62 + }
63 +
64 + var envUse = Sys.getEnv("USE");
65 + if (envUse != null) applyTokens(m, splitWords(envUse));
66 +
67 + globalFlags = m;
68 + return m;
69 + }
70 +
71 + static function readVars(file: String, m: Map<String, Bool>, vars: Map<String, String>): Void {
72 + if (!FileSystem.exists(file)) return;
73 + var text = "\n" + File.getContent(file);
74 + for (v in assignments(text, "USE")) applyTokens(m, splitWords(v));
75 + for (name in ["ARCH"].concat([for (e in EXPANDS) e.name])) {
76 + for (v in assignments(text, name)) vars.set(name, v);
77 + }
78 + }
79 +
80 + static function assignments(text: String, name: String): Array<String> {
81 + var out: Array<String> = [];
82 + var re = new EReg("\n" + name + "=\"([^\"]*)\"", "");
83 + var rest = text;
84 + while (re.match(rest)) {
85 + out.push(re.matched(1));
86 + rest = re.matchedRight();
87 + }
88 + return out;
89 + }
90 +
91 + static function profileDirs(): Array<String> {
92 + var out: Array<String> = [];
93 + var link = realpath("/etc/portage/make.profile");
94 + if (link != null) collectProfile(link, out, 0);
95 + return out;
96 + }
97 +
98 + static function collectProfile(dir: String, out: Array<String>, depth: Int): Void {
99 + if (depth > 20) return;
100 + var parentFile = dir + "/parent";
101 + if (FileSystem.exists(parentFile)) {
102 + for (raw in File.getContent(parentFile).split("\n")) {
103 + var line = StringTools.trim(raw);
104 + if (line == "" || StringTools.startsWith(line, "#")) continue;
105 + var p = dir + "/" + line;
106 + if (FileSystem.exists(p) && FileSystem.isDirectory(p)) collectProfile(p, out, depth + 1);
107 + }
108 + }
109 + out.push(dir);
110 + }
111 +
112 + static function realpath(p: String): Null<String> {
113 + if (!FileSystem.exists(p)) return null;
114 + try {
115 + var proc = new Process("readlink", ["-f", p]);
116 + var out = StringTools.trim(proc.stdout.readAll().toString());
117 + proc.close();
118 + return out == "" ? null : out;
119 + } catch (e: Dynamic) {
120 + return null;
121 + }
122 + }
123 +
124 + static function applyTokens(m: Map<String, Bool>, words: Array<String>): Void {
125 + for (t in words) {
126 + if (t == "" || t.indexOf("$") >= 0) continue;
127 + if (t == "-*") {
128 + m.clear();
129 + } else if (StringTools.startsWith(t, "-")) {
130 + m.set(t.substr(1), false);
131 + } else {
132 + m.set(t, true);
133 + }
134 + }
135 + }
136 +
137 + static function loadPackageUse(): Map<String, Array<String>> {
138 + var out = new Map<String, Array<String>>();
139 + var base = "/etc/portage/package.use";
140 + if (!FileSystem.exists(base)) return out;
141 +
142 + var files: Array<String> = FileSystem.isDirectory(base)
143 + ? [for (n in FileSystem.readDirectory(base)) base + "/" + n]
144 + : [base];
145 +
146 + for (f in files) {
147 + if (FileSystem.isDirectory(f)) continue;
148 + for (raw in File.getContent(f).split("\n")) {
149 + var line = StringTools.trim(raw);
150 + if (line == "" || StringTools.startsWith(line, "#")) continue;
151 + var words = splitWords(line);
152 + var a = AtomParser.parse(words[0]);
153 + if (a == null) continue;
154 + var key = a.cat + "/" + a.pkg;
155 + var list: Array<String> = out.exists(key) ? out.get(key) : [];
156 + for (w in words.slice(1)) list.push(w);
157 + out.set(key, list);
158 + }
159 + }
160 + return out;
161 + }
162 +
163 + static function splitWords(s: String): Array<String> {
164 + return [for (t in ~/[ \t\r\n]+/g.split(s)) if (t != "") t];
165 + }
166 +}
\ No newline at end of file
+31 −0 src/portage/VDB.hx new file mode 100644
@@ -0,0 +1,31 @@
1 +package portage;
2 +
3 +import sys.FileSystem;
4 +import sys.io.File;
5 +
6 +class VDB {
7 + public final root: String;
8 +
9 + public function new(root: String) {
10 + this.root = root;
11 + }
12 +
13 + public function installed(cat: String, pkg: String): Array<{ver: Version, repo: String}> {
14 + var dir = root + "/var/db/pkg/" + cat;
15 + if (!FileSystem.exists(dir) || !FileSystem.isDirectory(dir)) return [];
16 +
17 + var out: Array<{ver: Version, repo: String}> = [];
18 + for (entry in FileSystem.readDirectory(dir)) {
19 + if (!StringTools.startsWith(entry, pkg + "-")) continue;
20 + var v = Version.parse(entry.substr(pkg.length + 1));
21 + if (v == null) continue;
22 +
23 + var repoFile = dir + "/" + entry + "/repository";
24 + var repo = FileSystem.exists(repoFile)
25 + ? StringTools.trim(File.getContent(repoFile))
26 + : "?";
27 + out.push({ver: v, repo: repo});
28 + }
29 + return out;
30 + }
31 +}
+119 −0 src/portage/Version.hx new file mode 100644
@@ -0,0 +1,119 @@
1 +package portage;
2 +
3 +enum SuffixKind {
4 + Alpha;
5 + Beta;
6 + Pre;
7 + Rc;
8 + Normal;
9 + P;
10 +}
11 +
12 +typedef Suffix = {
13 + kind: SuffixKind,
14 + num: Int
15 +}
16 +
17 +class Version {
18 + public final components: Array<String>;
19 + public final letter: String;
20 + public final suffixes: Array<Suffix>;
21 + public final revision: Int;
22 + public final raw: String;
23 +
24 + public function new(components, letter, suffixes, revision, raw) {
25 + this.components = components;
26 + this.letter = letter;
27 + this.suffixes = suffixes;
28 + this.revision = revision;
29 + this.raw = raw;
30 + }
31 +
32 + static final VERSION_RE = ~/^([0-9]+(?:\.[0-9]+)*)([a-z]?)((?:_(?:alpha|beta|pre|rc|p)[0-9]*)*)(?:-r([0-9]+))?$/;
33 + static final SUFFIX_RE = ~/^(alpha|beta|pre|rc|p)([0-9]*)$/;
34 +
35 + static function toInt(s: String): Int {
36 + var v = Std.parseInt(s);
37 + return v == null ? 0 : v;
38 + }
39 +
40 + public static function parse(s: String): Null<Version> {
41 + if (!VERSION_RE.match(s)) return null;
42 +
43 + var components = VERSION_RE.matched(1).split(".");
44 + var letter = VERSION_RE.matched(2);
45 +
46 + var suffixes: Array<Suffix> = [];
47 + var sfx = VERSION_RE.matched(3);
48 + if (sfx != "") {
49 + for (part in sfx.substr(1).split("_")) {
50 + if (!SUFFIX_RE.match(part)) return null;
51 + var kind = switch SUFFIX_RE.matched(1) {
52 + case "alpha": Alpha;
53 + case "beta": Beta;
54 + case "pre": Pre;
55 + case "rc": Rc;
56 + default: P;
57 + };
58 + suffixes.push({kind: kind, num: toInt(SUFFIX_RE.matched(2))});
59 + }
60 + }
61 +
62 + var revStr = VERSION_RE.matched(4);
63 + var rev = revStr == null ? 0 : toInt(revStr);
64 + return new Version(components, letter, suffixes, rev, s);
65 + }
66 +
67 + public function compare(other: Version): Int {
68 + var len = Std.int(Math.max(components.length, other.components.length));
69 + for (i in 0...len) {
70 + var a = i < components.length ? components[i] : "0";
71 + var b = i < other.components.length ? other.components[i] : "0";
72 + var c = compareComponent(a, b, i == 0);
73 + if (c != 0) return c;
74 + }
75 +
76 + if (letter != other.letter) return letter < other.letter ? -1 : 1;
77 +
78 + var len2 = Std.int(Math.max(suffixes.length, other.suffixes.length));
79 + for (i in 0...len2) {
80 + var a: Suffix = i < suffixes.length ? suffixes[i] : {kind: Normal, num: 0};
81 + var b: Suffix = i < other.suffixes.length ? other.suffixes[i] : {kind: Normal, num: 0};
82 + var ra = rank(a.kind), rb = rank(b.kind);
83 + if (ra != rb) return ra < rb ? -1 : 1;
84 + if (a.num != b.num) return a.num < b.num ? -1 : 1;
85 + }
86 +
87 + if (revision != other.revision) return revision < other.revision ? -1 : 1;
88 + return 0;
89 + }
90 +
91 + static function compareComponent(a: String, b: String, first: Bool): Int {
92 + var leadingZero = !first && ((a.length > 1 && a.charAt(0) == "0") || (b.length > 1 && b.charAt(0) == "0"));
93 + if (leadingZero) {
94 + var sa = stripTrailingZeros(a), sb = stripTrailingZeros(b);
95 + if (sa == sb) return 0;
96 + return sa < sb ? -1 : 1;
97 + }
98 + var ia = toInt(a), ib = toInt(b);
99 + if (ia == ib) return 0;
100 + return ia < ib ? -1 : 1;
101 + }
102 +
103 + static function stripTrailingZeros(s: String): String {
104 + var end = s.length;
105 + while (end > 0 && s.charAt(end - 1) == "0") end--;
106 + return s.substr(0, end);
107 + }
108 +
109 + static function rank(k: SuffixKind): Int {
110 + return switch k {
111 + case Alpha: -4;
112 + case Beta: -3;
113 + case Pre: -2;
114 + case Rc: -1;
115 + case Normal: 0;
116 + case P: 1;
117 + }
118 + }
119 +}