acooliodude / portage-hx

Push x4 Browse files
ACoolioDude committed 1d ago parent cbc41de commit 37a342436c85158df9c6de3a63729b7b9861dcb2
Showing 7 changed files with 600 additions and 27 deletions. UnifiedSplit
+166 −4 src/Main.hx
@@ -40,6 +40,8 @@ class Main {
40 static var showRepo = false; 40 static var showRepo = false;
41 static var update = false; 41 static var update = false;
42 static var deep = false; 42 static var deep = false;
43 + static var unmergeMode = false;
44 + static var depclean = false;
43 static var quiet = false; 45 static var quiet = false;
44 static var syncFlag = false; 46 static var syncFlag = false;
45 static var syncOnly: Null<String> = null; 47 static var syncOnly: Null<String> = null;
@@ -206,9 +208,12 @@ class Main {
206 208
207 static function installedSatisfies(a: Atom): Bool { 209 static function installedSatisfies(a: Atom): Bool {
208 for (i in vdb.installed(a.cat, a.pkg)) { 210 for (i in vdb.installed(a.cat, a.pkg)) {
209 - var info = Slots.installedSlot(vdb.root, a.cat, a.pkg + "-" + i.ver.raw); 211 + var pkgVer = a.pkg + "-" + i.ver.raw;
210 - if (AtomParser.matches(a, i.ver) && AtomParser.slotMatches(a, info.slot, info.subslot)) return true; 212 + var info = Slots.installedSlot(vdb.root, a.cat, pkgVer);
211 - if (AtomParser.matches(a, i.ver)) return true; 213 + if (!AtomParser.matches(a, i.ver)) continue;
214 + if (!AtomParser.slotMatches(a, info.slot, info.subslot)) continue;
215 + if (!AtomParser.useMatches(a, Slots.installedUse(vdb.root, a.cat, pkgVer))) continue;
216 + return true;
212 } 217 }
213 return false; 218 return false;
214 } 219 }
@@ -264,7 +269,10 @@ class Main {
264 269
265 static function hostSatisfies(a: Atom): Bool { 270 static function hostSatisfies(a: Atom): Bool {
266 for (i in hostVdb.installed(a.cat, a.pkg)) { 271 for (i in hostVdb.installed(a.cat, a.pkg)) {
267 - if (AtomParser.matches(a, i.ver)) return true; 272 + var pkgVer = a.pkg + "-" + i.ver.raw;
273 + if (!AtomParser.matches(a, i.ver)) continue;
274 + if (!AtomParser.useMatches(a, Slots.installedUse(hostVdb.root, a.cat, pkgVer))) continue;
275 + return true;
268 } 276 }
269 return false; 277 return false;
270 } 278 }
@@ -331,6 +339,7 @@ class Main {
331 if (!AtomParser.matches(a, v)) continue; 339 if (!AtomParser.matches(a, v)) continue;
332 var si = r.tree.slotInfo(r.tree.ebuildPath(a.cat, a.pkg, v)); 340 var si = r.tree.slotInfo(r.tree.ebuildPath(a.cat, a.pkg, v));
333 if (!AtomParser.slotMatches(a, si.slot, si.subslot)) continue; 341 if (!AtomParser.slotMatches(a, si.slot, si.subslot)) continue;
342 + if (a.useDeps.length > 0 && !AtomParser.useMatches(a, UseFlags.forPackage(a.cat, a.pkg, r.tree.iuse(r.tree.ebuildPath(a.cat, a.pkg, v))))) continue;
334 var exact = AtomParser.parse("=" + a.cat + "/" + a.pkg + "-" + v.raw); 343 var exact = AtomParser.parse("=" + a.cat + "/" + a.pkg + "-" + v.raw);
335 if (exact == null) continue; 344 if (exact == null) continue;
336 var pick = repos.best(exact); 345 var pick = repos.best(exact);
@@ -775,6 +784,142 @@ class Main {
775 return out; 784 return out;
776 } 785 }
777 786
787 + static function ebuildFile(cat: String, pkgVer: String): Null<String> {
788 + var pn = Unmerge.pnFromDir(pkgVer);
789 + for (r in repos.list) {
790 + var path = r.tree.root + "/" + cat + "/" + pn + "/" + pkgVer + ".ebuild";
791 + if (FileSystem.exists(path)) return path;
792 + }
793 + return null;
794 + }
795 +
796 + static function removeInstalled(cat: String, pkgVer: String): Void {
797 + var ebuild = ebuildFile(cat, pkgVer);
798 + Sys.putEnv("ROOT", targetRoot);
799 +
800 + if (ebuild == null) {
801 + Sys.println(c("1;33", ">>> " + cat + "/" + pkgVer + ": no ebuild in any repo, pkg_prerm and pkg_postrm skipped"));
802 + } else if (Sys.command("ebuild", [ebuild, "prerm"]) != 0) {
803 + Sys.println(c("1;31", ">>> pkg_prerm failed for " + cat + "/" + pkgVer + "; not removed"));
804 + Sys.exit(1);
805 + }
806 +
807 + Unmerge.remove(targetRoot, cat, pkgVer);
808 +
809 + if (ebuild != null && Sys.command("ebuild", [ebuild, "postrm"]) != 0) {
810 + Sys.println(c("1;33", ">>> pkg_postrm failed for " + cat + "/" + pkgVer));
811 + }
812 + }
813 +
814 + static function runUnmerge(specs: Array<String>, pretend: Bool, ask: Bool): Void {
815 + if (specs.length == 0) {
816 + Sys.println("usage: emerge-hx -C [-p] [--root DIR] <atom> [atom...]");
817 + Sys.exit(1);
818 + }
819 +
820 + var targets = [];
821 + for (x in specs) {
822 + if (StringTools.startsWith(x, "@")) {
823 + Sys.println("sets can't be unmerged: " + x);
824 + Sys.exit(1);
825 + }
826 + var found = Unmerge.find(targetRoot, x);
827 + if (found.length == 0) {
828 + Sys.println('no installed package matches "' + x + '"');
829 + Sys.exit(1);
830 + }
831 + for (f in found) targets.push(f);
832 + }
833 +
834 + var blocked = false;
835 + for (t in targets) {
836 + for (dep in Unmerge.dependents(targetRoot, t.cat, t.pkg)) {
837 + var removing = false;
838 + for (other in targets) if (other.cat + "/" + other.pkg == dep) removing = true;
839 + if (removing) continue;
840 + Sys.println(c("1;31", "!!! " + dep + " depends on " + t.label + "; unmerge " + dep + " first"));
841 + blocked = true;
842 + }
843 + }
844 + if (blocked) Sys.exit(1);
845 +
846 + Sys.println(c("1;32", "These packages would be unmerged:"));
847 + Sys.println("");
848 + for (t in targets) Sys.println(c("1;37", " " + t.label));
849 + Sys.println("");
850 +
851 + if (pretend) return;
852 +
853 + if (!isRoot()) {
854 + Sys.println("emerge-hx: superuser access is required to unmerge");
855 + Sys.exit(1);
856 + }
857 +
858 + {
859 + Sys.print("\nWould you like to unmerge these packages? [Yes/No] ");
860 + var answer = "";
861 + try {
862 + answer = Sys.stdin().readLine().toLowerCase();
863 + } catch (e: Dynamic) {
864 + answer = "";
865 + }
866 + if (answer != "y" && answer != "yes") {
867 + Sys.println("Aborted.");
868 + return;
869 + }
870 + }
871 +
872 + for (n in 0...targets.length) {
873 + var t = targets[n];
874 + var pos = "(" + (n + 1) + " of " + targets.length + ") ";
875 + Sys.println(c("1;32", ">>> Unmerging " + pos) + c("1;37", t.label));
876 + removeInstalled(t.cat, t.pkgVer);
877 + Sys.println(c("1;32", ">>> Completed " + pos) + c("1;37", t.label));
878 + }
879 + Sys.println(c("1;32", "Done."));
880 + }
881 +
882 + static function runDepclean(pretend: Bool): Void {
883 + var orphans = Unmerge.depcleanCandidates(targetRoot);
884 + if (orphans.length == 0) {
885 + Sys.println(c("1;32", "Nothing to clean."));
886 + return;
887 + }
888 +
889 + Sys.println(c("1;32", "These packages are not needed by the world file or the system profile:"));
890 + Sys.println("");
891 + for (o in orphans) Sys.println(c("1;37", " " + o.label));
892 + Sys.println("");
893 +
894 + if (pretend) return;
895 +
896 + if (!isRoot()) {
897 + Sys.println("emerge-hx: superuser access is required to clean");
898 + Sys.exit(1);
899 + }
900 +
901 + Sys.print("\nWould you like to unmerge these packages? [Yes/No] ");
902 + var answer = "";
903 + try {
904 + answer = Sys.stdin().readLine().toLowerCase();
905 + } catch (e: Dynamic) {
906 + answer = "";
907 + }
908 + if (answer != "y" && answer != "yes") {
909 + Sys.println("Aborted.");
910 + return;
911 + }
912 +
913 + for (n in 0...orphans.length) {
914 + var o = orphans[n];
915 + var pos = "(" + (n + 1) + " of " + orphans.length + ") ";
916 + Sys.println(c("1;32", ">>> Unmerging " + pos) + c("1;37", o.label));
917 + removeInstalled(o.cat, o.pkgVer);
918 + Sys.println(c("1;32", ">>> Completed " + pos) + c("1;37", o.label));
919 + }
920 + Sys.println(c("1;32", "Done."));
921 + }
922 +
778 static function fullLabel(e: Entry): String { 923 static function fullLabel(e: Entry): String {
779 var base = e.cat + "/" + e.pkg + "-" + e.ver.raw; 924 var base = e.cat + "/" + e.pkg + "-" + e.ver.raw;
780 return showRepo ? base + "::" + e.repo : base; 925 return showRepo ? base + "::" + e.repo : base;
@@ -890,6 +1035,8 @@ class Main {
890 case "--verbose" | "-v": verbose = true; 1035 case "--verbose" | "-v": verbose = true;
891 case "--update" | "-u": update = true; 1036 case "--update" | "-u": update = true;
892 case "--deep" | "-D": deep = true; 1037 case "--deep" | "-D": deep = true;
1038 + case "--unmerge" | "-C": unmergeMode = true;
1039 + case "--depclean" | "-d": depclean = true;
893 case "--quiet" | "-q": quiet = true; 1040 case "--quiet" | "-q": quiet = true;
894 case "--sync": 1041 case "--sync":
895 syncFlag = true; 1042 syncFlag = true;
@@ -927,6 +1074,21 @@ class Main {
927 return; 1074 return;
928 } 1075 }
929 1076
1077 + if (depclean && atoms.length > 0) {
1078 + Sys.println("--depclean takes no package arguments; use -C to unmerge specific packages");
1079 + Sys.exit(1);
1080 + }
1081 +
1082 + if (depclean) {
1083 + runDepclean(pretend);
1084 + return;
1085 + }
1086 +
1087 + if (unmergeMode) {
1088 + runUnmerge(atoms, pretend, ask);
1089 + return;
1090 + }
1091 +
930 var namedAtoms: Array<String> = [for (x in atoms) if (!StringTools.startsWith(x, "@")) x]; 1092 var namedAtoms: Array<String> = [for (x in atoms) if (!StringTools.startsWith(x, "@")) x];
931 atoms = expandSets(atoms); 1093 atoms = expandSets(atoms);
932 1094
+1 −0 src/import.hx
@@ -10,6 +10,7 @@ import portage.Search;
10 import portage.Slots; 10 import portage.Slots;
11 import portage.Sync; 11 import portage.Sync;
12 import portage.Tree; 12 import portage.Tree;
13 +import portage.Unmerge;
13 import portage.UseFlags; 14 import portage.UseFlags;
14 import portage.VDB; 15 import portage.VDB;
15 import portage.Version; 16 import portage.Version;
+91 −16 src/portage/Atom.hx
@@ -6,7 +6,10 @@ typedef Atom = {
6 pkg: String, 6 pkg: String,
7 ver: Null<Version>, 7 ver: Null<Version>,
8 slot: Null<String>, 8 slot: Null<String>,
9 - glob: Bool 9 + slotName: Null<String>,
10 + subslot: Null<String>,
11 + glob: Bool,
12 + useDeps: Array<String>
10 } 13 }
11 14
12 class AtomParser { 15 class AtomParser {
@@ -15,6 +18,7 @@ class AtomParser {
15 static final OPS = ["", "=", ">=", "<=", ">", "<", "~"]; 18 static final OPS = ["", "=", ">=", "<=", ">", "<", "~"];
16 19
17 public static function parse(s: String): Null<Atom> { 20 public static function parse(s: String): Null<Atom> {
21 + var useDeps = parseUseDeps(s);
18 var stripped = USE_DEP.replace(s, ""); 22 var stripped = USE_DEP.replace(s, "");
19 if (!RE.match(stripped)) return null; 23 if (!RE.match(stripped)) return null;
20 24
@@ -36,31 +40,96 @@ class AtomParser {
36 } 40 }
37 if (op != "" && ver == null) return null; 41 if (op != "" && ver == null) return null;
38 42
43 + var rawSlot: Null<String> = RE.matched(5);
44 + var slotName: Null<String> = null;
45 + var subslot: Null<String> = null;
46 + if (rawSlot != null) {
47 + var s = StringTools.endsWith(rawSlot, "=") ? rawSlot.substr(0, rawSlot.length - 1) : rawSlot;
48 + if (s != "" && s != "*") {
49 + var parts = s.split("/");
50 + slotName = parts[0];
51 + if (parts.length > 1 && parts[1] != "") subslot = parts[1];
52 + }
53 + }
54 +
39 return { 55 return {
40 op: op, 56 op: op,
41 cat: RE.matched(2), 57 cat: RE.matched(2),
42 pkg: RE.matched(3), 58 pkg: RE.matched(3),
43 ver: ver, 59 ver: ver,
44 - slot: RE.matched(5), 60 + slot: rawSlot,
45 - glob: glob 61 + slotName: slotName,
62 + subslot: subslot,
63 + glob: glob,
64 + useDeps: useDeps
46 }; 65 };
47 } 66 }
48 67
49 - public static function slotMatches(a: Atom, slot: String, subslot: String): Bool { 68 + public static function resolveConditional(a: Atom, parent: Map<String, Bool>): Atom {
50 - if (a.slot == null || a.slot == "*") return true; 69 + if (a.useDeps.length == 0) return a;
51 - var req = a.slot; 70 +
52 - if (StringTools.endsWith(req, "=")) { 71 + var out: Array<String> = [];
53 - req = req.substr(0, req.length - 1); 72 + for (t in a.useDeps) {
73 + if (StringTools.startsWith(t, "-")) {
74 + out.push(t);
75 + continue;
76 + }
77 +
78 + var name = t;
79 + var inverse = StringTools.startsWith(name, "!");
80 + if (inverse) name = name.substr(1);
81 +
82 + var isCond = StringTools.endsWith(name, "?");
83 + var isEq = StringTools.endsWith(name, "=");
84 + if (!isCond && !isEq) {
85 + out.push(t);
86 + continue;
87 + }
88 + name = name.substr(0, name.length - 1).split("(")[0];
89 +
90 + var parentOn = parent.get(name) == true;
91 + var required: Null<Bool> = null;
92 + if (isCond) {
93 + var holds = inverse ? !parentOn : parentOn;
94 + if (holds) required = true;
95 + } else {
96 + required = inverse ? !parentOn : parentOn;
97 + }
98 +
99 + if (required == null) continue;
100 + out.push(required ? name : "-" + name);
54 } 101 }
55 - var slash = req.indexOf("/"); 102 +
56 - if (slash >= 0) { 103 + return {
57 - var rSlot = req.substr(0, slash); 104 + op: a.op,
58 - var rSub = req.substr(slash + 1); 105 + cat: a.cat,
59 - if (rSlot != "" && rSlot != slot) return false; 106 + pkg: a.pkg,
60 - if (rSub != "" && rSub != subslot) return false; 107 + ver: a.ver,
61 - return true; 108 + slot: a.slot,
109 + slotName: a.slotName,
110 + subslot: a.subslot,
111 + glob: a.glob,
112 + useDeps: out
113 + };
114 + }
115 +
116 + static function parseUseDeps(s: String): Array<String> {
117 + if (!USE_DEP.match(s)) return [];
118 + var inner = USE_DEP.matched(0);
119 + inner = inner.substr(1, inner.length - 2);
120 + return [for (t in inner.split(",")) if (StringTools.trim(t) != "") StringTools.trim(t)];
121 + }
122 +
123 + public static function useMatches(a: Atom, flags: Map<String, Bool>): Bool {
124 + for (t in a.useDeps) {
125 + var neg = StringTools.startsWith(t, "-");
126 + var name = neg ? t.substr(1) : t;
127 + if (name.indexOf("?") >= 0 || name.indexOf("=") >= 0 || StringTools.startsWith(name, "!")) continue;
128 +
129 + var flag = name.split("(")[0];
130 + var on = flags.get(flag) == true;
131 + if (on == neg) return false;
62 } 132 }
63 - if (req != "" && req != slot) return false;
64 return true; 133 return true;
65 } 134 }
66 135
@@ -88,4 +157,10 @@ class AtomParser {
88 default: false; 157 default: false;
89 }; 158 };
90 } 159 }
160 +
161 + public static function slotMatches(a: Atom, slot: String, subslot: String): Bool {
162 + if (a.slotName != null && a.slotName != slot) return false;
163 + if (a.subslot != null && a.subslot != subslot) return false;
164 + return true;
165 + }
91 } 166 }
+4 −6 src/portage/Dependencies.hx
@@ -3,9 +3,9 @@ package portage;
3 enum Dep { 3 enum Dep {
4 Leaf(a: Atom); 4 Leaf(a: Atom);
5 AllOf(kids: Array<Dep>); 5 AllOf(kids: Array<Dep>);
6 - AnyOf(alts: Array<Dep>); 6 + AnyOf(alts: Array<Dep>); // || ( ... ): pick one alternative
7 - Cond(flag: String, negate: Bool, kids: Array<Dep>); 7 + Cond(flag: String, negate: Bool, kids: Array<Dep>); // flag? ( ... ) / !flag? ( ... )
8 - Block(a: Atom, strong: Bool); 8 + Block(a: Atom, strong: Bool); // !atom (weak) / !!atom (strong): must not be installed
9 } 9 }
10 10
11 typedef Chooser = Array<Array<Atom>> -> Int; 11 typedef Chooser = Array<Array<Atom>> -> Int;
@@ -82,7 +82,6 @@ class Dependencies {
82 static function collectBlockers(d: Dep, use: Map<String, Bool>, out: Array<{atom: Atom, strong: Bool}>): Void { 82 static function collectBlockers(d: Dep, use: Map<String, Bool>, out: Array<{atom: Atom, strong: Bool}>): Void {
83 switch d { 83 switch d {
84 case Leaf(_): 84 case Leaf(_):
85 -
86 case Block(a, strong): 85 case Block(a, strong):
87 out.push({atom: a, strong: strong}); 86 out.push({atom: a, strong: strong});
88 case AllOf(kids): 87 case AllOf(kids):
@@ -104,11 +103,10 @@ class Dependencies {
104 static function evalOne(d: Dep, use: Map<String, Bool>, pick: Chooser, out: Array<Atom>): Void { 103 static function evalOne(d: Dep, use: Map<String, Bool>, pick: Chooser, out: Array<Atom>): Void {
105 switch d { 104 switch d {
106 case Leaf(a): 105 case Leaf(a):
107 - out.push(a); 106 + out.push(AtomParser.resolveConditional(a, use));
108 case AllOf(kids): 107 case AllOf(kids):
109 for (k in kids) evalOne(k, use, pick, out); 108 for (k in kids) evalOne(k, use, pick, out);
110 case Block(_, _): 109 case Block(_, _):
111 -
112 case Cond(flag, negate, kids): 110 case Cond(flag, negate, kids):
113 var on = use.get(flag) == true; 111 var on = use.get(flag) == true;
114 if (on != negate) for (k in kids) evalOne(k, use, pick, out); 112 if (on != negate) for (k in kids) evalOne(k, use, pick, out);
+9 −1 src/portage/Slots.hx
@@ -28,7 +28,7 @@ class Slots {
28 var dir = catDir + "/" + pkgVer; 28 var dir = catDir + "/" + pkgVer;
29 if (!FileSystem.isDirectory(dir)) continue; 29 if (!FileSystem.isDirectory(dir)) continue;
30 30
31 - var name = read(dir + "/PN"); 31 + var name = Unmerge.pnFromDir(pkgVer);
32 if (name == "") continue; 32 if (name == "") continue;
33 var depLabel = catName + "/" + name; 33 var depLabel = catName + "/" + name;
34 if (depLabel == label) continue; 34 if (depLabel == label) continue;
@@ -49,6 +49,14 @@ class Slots {
49 return false; 49 return false;
50 } 50 }
51 51
52 + public static function installedUse(root: String, cat: String, pkgVer: String): Map<String, Bool> {
53 + var m = new Map<String, Bool>();
54 + for (t in ~/[ \t\r\n]+/g.split(read(root + "/var/db/pkg/" + cat + "/" + pkgVer + "/USE"))) {
55 + if (t != "") m.set(t, true);
56 + }
57 + return m;
58 + }
59 +
52 static function read(file: String): String { 60 static function read(file: String): String {
53 if (!FileSystem.exists(file)) return ""; 61 if (!FileSystem.exists(file)) return "";
54 return StringTools.trim(File.getContent(file)); 62 return StringTools.trim(File.getContent(file));
+1 −0 src/portage/Tree.hx
@@ -38,6 +38,7 @@ class Tree {
38 var path = ebuildPath(a.cat, a.pkg, v); 38 var path = ebuildPath(a.cat, a.pkg, v);
39 var slot = slotInfo(path); 39 var slot = slotInfo(path);
40 if (!AtomParser.slotMatches(a, slot.slot, slot.subslot)) continue; 40 if (!AtomParser.slotMatches(a, slot.slot, slot.subslot)) continue;
41 + if (a.useDeps.length > 0 && !AtomParser.useMatches(a, UseFlags.forPackage(a.cat, a.pkg, iuse(path)))) continue;
41 42
42 if (Mask.isMasked(a.cat, a.pkg, v)) continue; 43 if (Mask.isMasked(a.cat, a.pkg, v)) continue;
43 44
+328 −0 src/portage/Unmerge.hx new file mode 100644
@@ -0,0 +1,328 @@
1 +package portage;
2 +
3 +import sys.FileSystem;
4 +import sys.io.File;
5 +import sys.io.Process;
6 +
7 +typedef InstalledPkg = {
8 + cat: String,
9 + pkg: String,
10 + pkgVer: String,
11 + label: String
12 +}
13 +
14 +class Unmerge {
15 + static final DEP_FILES = ["DEPEND", "RDEPEND", "PDEPEND", "BDEPEND"];
16 +
17 + public static function find(root: String, spec: String): Array<InstalledPkg> {
18 + var out: Array<InstalledPkg> = [];
19 + var base = root + "/var/db/pkg";
20 + if (!FileSystem.exists(base)) return out;
21 +
22 + var atom = spec.indexOf("/") >= 0 ? AtomParser.parse(spec) : null;
23 + if (spec.indexOf("/") >= 0 && atom == null) return out;
24 +
25 + for (cat in FileSystem.readDirectory(base)) {
26 + var catDir = base + "/" + cat;
27 + if (!FileSystem.isDirectory(catDir)) continue;
28 +
29 + for (pv in FileSystem.readDirectory(catDir)) {
30 + var dir = catDir + "/" + pv;
31 + if (!FileSystem.isDirectory(dir)) continue;
32 +
33 + var pn = pnFromDir(pv);
34 + if (pn == "" || !StringTools.startsWith(pv, pn + "-")) continue;
35 + var ver = Version.parse(pv.substr(pn.length + 1));
36 + if (ver == null) continue;
37 +
38 + if (atom != null) {
39 + if (atom.cat != cat || atom.pkg != pn) continue;
40 + var info = Slots.installedSlot(root, cat, pv);
41 + if (!AtomParser.matches(atom, ver)) continue;
42 + if (!AtomParser.slotMatches(atom, info.slot, info.subslot)) continue;
43 + if (!AtomParser.useMatches(atom, Slots.installedUse(root, cat, pv))) continue;
44 + } else if (pn != spec) {
45 + continue;
46 + }
47 +
48 + out.push({cat: cat, pkg: pn, pkgVer: pv, label: cat + "/" + pv});
49 + }
50 + }
51 + return out;
52 + }
53 +
54 + public static function dependents(root: String, cat: String, pkg: String): Array<String> {
55 + var label = cat + "/" + pkg;
56 + var out: Array<String> = [];
57 + var base = root + "/var/db/pkg";
58 + if (!FileSystem.exists(base)) return out;
59 +
60 + for (catName in FileSystem.readDirectory(base)) {
61 + var catDir = base + "/" + catName;
62 + if (!FileSystem.isDirectory(catDir)) continue;
63 +
64 + for (pv in FileSystem.readDirectory(catDir)) {
65 + var dir = catDir + "/" + pv;
66 + if (!FileSystem.isDirectory(dir)) continue;
67 +
68 + var name = pnFromDir(pv);
69 + if (name == "" || catName + "/" + name == label) continue;
70 +
71 + for (file in DEP_FILES) {
72 + if (mentions(read(dir + "/" + file), label)) {
73 + var depLabel = catName + "/" + name;
74 + if (out.indexOf(depLabel) < 0) out.push(depLabel);
75 + break;
76 + }
77 + }
78 + }
79 + }
80 + return out;
81 + }
82 +
83 + static function mentions(deps: String, label: String): Bool {
84 + for (t in ~/[ \t\r\n]+/g.split(deps)) {
85 + var s = t;
86 + while (s != "" && "<>=~!".indexOf(s.charAt(0)) >= 0) s = s.substr(1);
87 + if (s == label || StringTools.startsWith(s, label + ":") || StringTools.startsWith(s, label + "[")) return true;
88 + if (StringTools.startsWith(s, label + "-")) {
89 + var next = s.charAt(label.length + 1);
90 + if (next != "" && next >= "0" && next <= "9") return true;
91 + }
92 + }
93 + return false;
94 + }
95 +
96 + public static function remove(root: String, cat: String, pkgVer: String): Void {
97 + var base = StringTools.endsWith(root, "/") ? root.substr(0, root.length - 1) : root;
98 + var dir = base + "/var/db/pkg/" + cat + "/" + pkgVer;
99 +
100 + var dirs: Array<String> = [];
101 + for (line in lines(dir + "/CONTENTS")) {
102 + var words = line.split(" ");
103 + if (words.length < 2) continue;
104 + var path = base + words[1];
105 +
106 + if (words[0] == "dir") {
107 + dirs.push(path);
108 + } else if (isModifiedConfig(base, words)) {
109 + Sys.println(" keeping modified config file " + words[1]);
110 + } else {
111 + try {
112 + FileSystem.deleteFile(path);
113 + } catch (e: Dynamic) {}
114 + }
115 + }
116 +
117 + dirs.sort(function(x, y) return y.length - x.length);
118 + for (d in dirs) {
119 + try {
120 + if (FileSystem.exists(d) && FileSystem.isDirectory(d) && FileSystem.readDirectory(d).length == 0) {
121 + FileSystem.deleteDirectory(d);
122 + }
123 + } catch (e: Dynamic) {}
124 + }
125 +
126 + if (FileSystem.exists(dir) && FileSystem.isDirectory(dir)) {
127 + for (f in FileSystem.readDirectory(dir)) {
128 + try {
129 + FileSystem.deleteFile(dir + "/" + f);
130 + } catch (e: Dynamic) {}
131 + }
132 + FileSystem.deleteDirectory(dir);
133 + }
134 + }
135 +
136 + public static function allInstalled(root: String): Array<InstalledPkg> {
137 + var out: Array<InstalledPkg> = [];
138 + var base = root + "/var/db/pkg";
139 + if (!FileSystem.exists(base)) return out;
140 +
141 + for (cat in FileSystem.readDirectory(base)) {
142 + var catDir = base + "/" + cat;
143 + if (!FileSystem.isDirectory(catDir)) continue;
144 + for (pv in FileSystem.readDirectory(catDir)) {
145 + var dir = catDir + "/" + pv;
146 + if (!FileSystem.isDirectory(dir)) continue;
147 + var pn = pnFromDir(pv);
148 + if (pn == "" || !StringTools.startsWith(pv, pn + "-")) continue;
149 + out.push({cat: cat, pkg: pn, pkgVer: pv, label: cat + "/" + pv});
150 + }
151 + }
152 + return out;
153 + }
154 +
155 + public static function depcleanCandidates(root: String): Array<InstalledPkg> {
156 + var installed = allInstalled(root);
157 + var byName = new Map<String, Array<InstalledPkg>>();
158 + for (p in installed) {
159 + var key = p.cat + "/" + p.pkg;
160 + if (!byName.exists(key)) byName.set(key, []);
161 + byName.get(key).push(p);
162 + }
163 +
164 + var queue: Array<String> = systemNames().concat(worldNames(root));
165 + var keep = new Map<String, Bool>();
166 + while (queue.length > 0) {
167 + var name: String = queue.shift();
168 + if (keep.exists(name)) continue;
169 + keep.set(name, true);
170 + if (!byName.exists(name)) continue;
171 +
172 + for (p in byName.get(name)) {
173 + var dir = root + "/var/db/pkg/" + p.cat + "/" + p.pkgVer;
174 + for (file in DEP_FILES) {
175 + for (t in ~/[ \t\r\n]+/g.split(read(dir + "/" + file))) {
176 + var n = nameOf(t);
177 + if (n != "") queue.push(n);
178 + }
179 + }
180 + }
181 + }
182 +
183 + var out: Array<InstalledPkg> = [];
184 + for (p in installed) {
185 + if (!keep.exists(p.cat + "/" + p.pkg)) out.push(p);
186 + }
187 + return out;
188 + }
189 +
190 + static function nameOf(token: String): String {
191 + var s = token;
192 + while (s != "" && "<>=~!".indexOf(s.charAt(0)) >= 0) s = s.substr(1);
193 +
194 + var cut = s.length;
195 + var colon = s.indexOf(":");
196 + if (colon >= 0 && colon < cut) cut = colon;
197 + var bracket = s.indexOf("[");
198 + if (bracket >= 0 && bracket < cut) cut = bracket;
199 + s = s.substr(0, cut);
200 +
201 + var slash = s.indexOf("/");
202 + if (slash <= 0 || slash == s.length - 1) return "";
203 + var cat = s.substr(0, slash);
204 + var rest = s.substr(slash + 1);
205 +
206 + for (i in 0...rest.length) {
207 + if (rest.charAt(i) != "-" || i + 1 >= rest.length) continue;
208 + var next = rest.charAt(i + 1);
209 + if (next >= "0" && next <= "9" && Version.parse(rest.substr(i + 1)) != null) {
210 + return cat + "/" + rest.substr(0, i);
211 + }
212 + }
213 + return cat + "/" + rest;
214 + }
215 +
216 + static function worldNames(root: String): Array<String> {
217 + var base = StringTools.endsWith(root, "/") ? root.substr(0, root.length - 1) : root;
218 + var out: Array<String> = [];
219 + for (line in lines(base + "/var/lib/portage/world")) {
220 + var n = nameOf(line);
221 + if (n != "" && out.indexOf(n) < 0) out.push(n);
222 + }
223 + return out;
224 + }
225 +
226 + static function systemNames(): Array<String> {
227 + var out: Array<String> = [];
228 + for (dir in profileChain()) {
229 + for (line in lines(dir + "/packages")) {
230 + if (!StringTools.startsWith(line, "*")) continue;
231 + var n = nameOf(line.substr(1));
232 + if (n != "" && out.indexOf(n) < 0) out.push(n);
233 + }
234 + }
235 + return out;
236 + }
237 +
238 + static function profileChain(): Array<String> {
239 + var out: Array<String> = [];
240 + var link = realpath("/etc/portage/make.profile");
241 + if (link != null) collectProfile(link, out, 0);
242 + return out;
243 + }
244 +
245 + static function collectProfile(dir: String, out: Array<String>, depth: Int): Void {
246 + if (depth > 20) return;
247 + for (line in lines(dir + "/parent")) {
248 + var p = dir + "/" + line;
249 + if (FileSystem.exists(p) && FileSystem.isDirectory(p)) collectProfile(p, out, depth + 1);
250 + }
251 + out.push(dir);
252 + }
253 +
254 + static function realpath(p: String): Null<String> {
255 + if (!FileSystem.exists(p)) return null;
256 + try {
257 + var proc = new Process("readlink", ["-f", p]);
258 + var out = StringTools.trim(proc.stdout.readAll().toString());
259 + proc.close();
260 + return out == "" ? null : out;
261 + } catch (e: Dynamic) {
262 + return null;
263 + }
264 + }
265 +
266 + static function configProtect(): Array<String> {
267 + var value = Sys.getEnv("CONFIG_PROTECT");
268 + if (value == null && FileSystem.exists("/etc/portage/make.conf")) {
269 + var text = "\n" + File.getContent("/etc/portage/make.conf");
270 + var re = new EReg("\nCONFIG_PROTECT=[\"']?([^\"'\n]*)[\"']?", "");
271 + var rest = text;
272 + while (re.match(rest)) {
273 + value = re.matched(1);
274 + rest = re.matchedRight();
275 + }
276 + }
277 + if (value == null) value = "/etc /usr/share/config";
278 + return [for (w in ~/[ \t\r\n]+/g.split(value)) if (w != "") w];
279 + }
280 +
281 + static function isProtected(path: String): Bool {
282 + for (prefix in configProtect()) {
283 + if (path == prefix || StringTools.startsWith(path, prefix + "/")) return true;
284 + }
285 + return false;
286 + }
287 +
288 + static function md5Of(path: String): String {
289 + if (!FileSystem.exists(path) || FileSystem.isDirectory(path)) return "";
290 + try {
291 + var p = new Process("md5sum", [path]);
292 + var out = StringTools.trim(p.stdout.readAll().toString());
293 + p.close();
294 + var parts = out.split(" ");
295 + return parts.length > 0 ? parts[0] : "";
296 + } catch (e: Dynamic) {
297 + return "";
298 + }
299 + }
300 +
301 + static function isModifiedConfig(base: String, words: Array<String>): Bool {
302 + if (words.length < 3 || words[0] != "obj") return false;
303 + if (!isProtected(words[1])) return false;
304 + var current = md5Of(base + words[1]);
305 + return current != "" && current != words[2];
306 + }
307 +
308 + public static function pnFromDir(pv: String): String {
309 + for (i in 0...pv.length) {
310 + if (pv.charAt(i) != "-" || i + 1 >= pv.length) continue;
311 + var next = pv.charAt(i + 1);
312 + if (next >= "0" && next <= "9" && Version.parse(pv.substr(i + 1)) != null) {
313 + return pv.substr(0, i);
314 + }
315 + }
316 + return "";
317 + }
318 +
319 + static function read(file: String): String {
320 + if (!FileSystem.exists(file)) return "";
321 + return StringTools.trim(File.getContent(file));
322 + }
323 +
324 + static function lines(file: String): Array<String> {
325 + if (!FileSystem.exists(file)) return [];
326 + return [for (l in File.getContent(file).split("\n")) if (StringTools.trim(l) != "") StringTools.trim(l)];
327 + }
328 +}