Initial commit
Browse files
+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 | +} |