436 lines
13 KiB
Text
436 lines
13 KiB
Text
#!/usr/bin/env nu
|
|
|
|
const HEADER = "
|
|
----- MEAT ----------------------------------------
|
|
"
|
|
|
|
const FOOTER = "
|
|
---------------------------------------------------
|
|
"
|
|
|
|
const HELP_TEXT = "
|
|
YUM - CONSUME DELICIOUS MEATS
|
|
COOK - ONLY PREPARE MEATS
|
|
POKE - TASTE SUSPICIOUS MEATS
|
|
GUT - CLEAN MEAT STORES
|
|
FRESH - HUNT FRESH MEATS
|
|
LOOK - LOOK FOR FRESHER MEATS
|
|
HUNT - HUNT FOR MEATS IN NIXPKGS
|
|
RITUAL - PERFORM RITUAL THEN CONSUME
|
|
TRADE - SEND MEATS FAR AWAY
|
|
..-A - ..ALL MEATS"
|
|
|
|
def meat-print [text: string] {
|
|
print $"\n \t($text)\n"
|
|
}
|
|
|
|
def with-frame [body: closure] {
|
|
print -n $HEADER
|
|
do $body
|
|
print -n $FOOTER
|
|
}
|
|
|
|
def hn []: nothing -> string {
|
|
^hostname | str trim
|
|
}
|
|
|
|
def system-attr []: nothing -> string {
|
|
$"nixosConfigurations.(hn).config.system.build.toplevel"
|
|
}
|
|
|
|
def nix-conf-attr []: nothing -> string {
|
|
$"nixosConfigurations.(hn).config.environment.etc.\"nix/nix.conf\""
|
|
}
|
|
|
|
def nix-build-nom [out: string, source: string, attr: string, extras: list<string> = []] {
|
|
if ($env.MONITOR? | is-empty) {
|
|
^nix-build --out-link $out $source -A $attr ...$extras
|
|
} else {
|
|
^nix-build --log-format internal-json --out-link $out $source -A $attr ...$extras out+err>| ^$env.MONITOR --json
|
|
}
|
|
# Piping to MONITOR masks nix-build's exit code, so detect failure by the
|
|
# absence of the out-link.
|
|
if not ($out | path exists) {
|
|
error make { msg: "nix-build produced no output" }
|
|
}
|
|
}
|
|
|
|
def differ-step [build: string] {
|
|
if ($env.DIFFER? | is-empty) {
|
|
meat-print " (no DIFFER set; skipping diff)"
|
|
} else {
|
|
^$env.DIFFER /nix/var/nix/profiles/system $build
|
|
}
|
|
}
|
|
|
|
def activate [build_path: string] {
|
|
^sudo $nu.current-exe -c $"nix-env --set -p /nix/var/nix/profiles/system ($build_path); ($build_path)/bin/switch-to-configuration switch"
|
|
}
|
|
|
|
def do-build [extras: list<string> = []] {
|
|
let tmpdir = ^mktemp -d -t "meat-build.XXXXXX" | str trim
|
|
let build = $"($tmpdir)/build"
|
|
warm-pins
|
|
nix-build-nom $build $"($env.MEATS)/entry.nix" (system-attr) $extras
|
|
differ-step $build
|
|
activate $build
|
|
try { rm $build }
|
|
}
|
|
|
|
def cmd-help [] {
|
|
print -n $"($HEADER)($HELP_TEXT)($FOOTER)\n"
|
|
}
|
|
|
|
def cmd-yum [...args: string] {
|
|
with-frame {
|
|
meat-print "CONSUMING DELICIOUS MEATS.."
|
|
try { do-build $args } catch { print "FAILED TO CONSUME MEATS." }
|
|
}
|
|
}
|
|
|
|
def cmd-cook [...args: string] {
|
|
with-frame {
|
|
meat-print "PREPARING DELICIOUS MEATS.."
|
|
warm-pins
|
|
try { ^nix-build --no-out-link $"($env.MEATS)/entry.nix" -A (system-attr) ...$args }
|
|
}
|
|
}
|
|
|
|
def cmd-poke [...args: string] {
|
|
with-frame {
|
|
meat-print "PREPARING SUSPICIOUS MEATS.."
|
|
warm-pins
|
|
try { ^nix-build --no-out-link --show-trace $"($env.MEATS)/entry.nix" -A (system-attr) ...$args }
|
|
}
|
|
}
|
|
|
|
def cmd-gut [...args: string] {
|
|
with-frame {
|
|
meat-print "CLEANING MEAT STORES.."
|
|
try { ^nh clean all ...$args }
|
|
}
|
|
}
|
|
|
|
def cmd-trade [] {
|
|
with-frame {
|
|
meat-print "TRADING FOREIGN MEATS.."
|
|
meat-print "tbd"
|
|
}
|
|
}
|
|
|
|
def pins-path [] { $"($env.MEATS)/pins/pins.toml" }
|
|
def lock-path [] { $"($env.MEATS)/pins/pins.lock.json" }
|
|
|
|
def load-pins [] {
|
|
let p = pins-path
|
|
if not ($p | path exists) { error make { msg: $"no pins file at ($p)" } }
|
|
open --raw $p | from toml
|
|
}
|
|
|
|
def load-lock [] {
|
|
let p = lock-path
|
|
if ($p | path exists) { open --raw $p | from json } else { {} }
|
|
}
|
|
|
|
# Sort keys alphabetically and write atomically.
|
|
def write-lock [lock: record] {
|
|
let p = lock-path
|
|
let sorted = $lock | columns | sort | reduce -f {} { |k, acc| $acc | insert $k ($lock | get $k) }
|
|
let tmp = $"($p).tmp"
|
|
$sorted | to json --indent 2 | save -f $tmp
|
|
^mv $tmp $p
|
|
}
|
|
|
|
# scheme:rest → expansion via {path} template, or pass-through.
|
|
def expand-shorturl [url: string, shorturls: record] {
|
|
let parts = $url | split row ":" -n 2
|
|
if (($parts | length) < 2) { return $url }
|
|
let scheme = $parts | get 0
|
|
if not ($scheme in ($shorturls | columns)) { return $url }
|
|
($shorturls | get $scheme) | str replace --regex '\{path\}' ($parts | get 1)
|
|
}
|
|
|
|
# Decompose to {git_url, ref?} for cheap ls-remote queries (used by `look`).
|
|
def parse-git-target [url: string] {
|
|
if ($url | str starts-with "github:") {
|
|
let body = $url | str substring 7..
|
|
let path_query = $body | split row "?" -n 2
|
|
let segs = ($path_query | get 0) | split row "/"
|
|
let owner = $segs | get 0
|
|
let repo = $segs | get 1
|
|
let ref = if (($segs | length) > 2) { $segs | skip 2 | str join "/" } else { null }
|
|
return { git_url: $"https://github.com/($owner)/($repo).git", ref: $ref }
|
|
}
|
|
if ($url | str starts-with "git+") {
|
|
let stripped = $url | str substring 4..
|
|
let parts = $stripped | split row "?" -n 2
|
|
let base = $parts | get 0
|
|
let ref = if (($parts | length) > 1) {
|
|
($parts | get 1) | split row "&" | each { |kv|
|
|
let kvp = $kv | split row "=" -n 2
|
|
{ k: ($kvp | get 0), v: ($kvp | get 1?) }
|
|
} | where k == "ref" | get -o 0 | get -o v
|
|
} else { null }
|
|
return { git_url: $base, ref: $ref }
|
|
}
|
|
error make { msg: $"unsupported url scheme for ls-remote: ($url)" }
|
|
}
|
|
|
|
# Cheap "is upstream ahead?" check via git ls-remote. Returns the rev string.
|
|
def ls-remote-head [url: string] {
|
|
let tgt = parse-git-target $url
|
|
let target_ref = if ($tgt.ref? | is-not-empty) { $"refs/heads/($tgt.ref)" } else { "HEAD" }
|
|
let r = ^git ls-remote $tgt.git_url $target_ref | complete
|
|
if $r.exit_code != 0 { error make { msg: $"ls-remote failed: ($r.stderr)" } }
|
|
let first = $r.stdout | str trim | lines | get -o 0
|
|
if ($first | is-empty) { error make { msg: "no refs returned" } }
|
|
$first | split row "\t" | get 0
|
|
}
|
|
|
|
# Real prefetch — caches in /nix/store and returns the full locked attrset.
|
|
# Output JSON: { hash: SRI, locked: {...}, original: {...}, storePath: PATH }
|
|
# --refresh bypasses the ref→rev cache (otherwise stale within tarball-ttl).
|
|
def prefetch-pin [url: string] {
|
|
let r = ^nix flake prefetch --refresh --json $url | complete
|
|
if $r.exit_code != 0 { error make { msg: $"prefetch failed for ($url): ($r.stderr)" } }
|
|
let j = $r.stdout | from json
|
|
$j.locked | insert narHash $j.hash
|
|
}
|
|
|
|
# Fetch one locked input into the store, mirroring lib/inputs.nix's
|
|
# `builtins.fetchTree lock.<name>`. Returns the name on failure, else null.
|
|
def warm-pin [name: string, node: record] {
|
|
let tmp = ^mktemp -t "meat-pin.XXXXXX.json" | str trim
|
|
$node | to json | save -f $tmp
|
|
let expr = $"\(builtins.fetchTree \(builtins.fromJSON \(builtins.readFile \"($tmp)\"\)\)\).outPath"
|
|
let r = ^nix eval --impure --raw --expr $expr | complete
|
|
try { rm $tmp }
|
|
if $r.exit_code != 0 { $name } else { null }
|
|
}
|
|
|
|
# Pre-fetch every locked input in parallel so the serial fetchTree calls
|
|
# during evaluation become cache hits. Warming is idempotent and non-fatal:
|
|
# anything that fails here is simply re-fetched by the build itself.
|
|
def warm-pins [] {
|
|
let lock = load-lock
|
|
let names = $lock | columns
|
|
if ($names | is-empty) { return }
|
|
meat-print "GATHERING MEATS.."
|
|
let failed = $names | par-each { |name| warm-pin $name ($lock | get $name) } | compact
|
|
for name in $failed {
|
|
meat-print $" COULDN'T GATHER ($name | str upcase) \(BUILD WILL RETRY\)"
|
|
}
|
|
}
|
|
|
|
def cmd-fresh [...names: string] {
|
|
with-frame {
|
|
meat-print "HUNTING FRESH MEATS.."
|
|
let pins = load-pins
|
|
let shorturls = $pins.shorturls? | default {}
|
|
let inputs = $pins.inputs
|
|
let lock = load-lock
|
|
|
|
let requested = if ($names | is-empty) { $inputs | columns } else { $names }
|
|
|
|
# Report unknown names up front and keep only real targets.
|
|
let targets = $requested | each { |name|
|
|
if not ($name in ($inputs | columns)) {
|
|
meat-print $"NO MEAT CALLED ($name | str upcase).."
|
|
null
|
|
} else { $name }
|
|
} | compact
|
|
|
|
# Prefetch every target in parallel — this is the network-bound work.
|
|
# No lock writes happen here; results are collected and applied below.
|
|
let results = $targets | par-each { |name|
|
|
let old_rev = ($lock | get -o $name) | default {} | get -o rev
|
|
try {
|
|
let pin = $inputs | get $name
|
|
let expanded = expand-shorturl $pin.url $shorturls
|
|
let entry = prefetch-pin $expanded
|
|
{ name: $name, ok: true, entry: $entry, old_rev: $old_rev, new_rev: $entry.rev }
|
|
} catch { |e|
|
|
{ name: $name, ok: false, err: $e.msg }
|
|
}
|
|
}
|
|
|
|
# Apply results sequentially in target order so the lock file is never
|
|
# written concurrently and reporting stays deterministic.
|
|
mut lock = $lock
|
|
for name in $targets {
|
|
let r = $results | where name == $name | first
|
|
meat-print $"PROCESSING ($name | str upcase).."
|
|
if not $r.ok {
|
|
meat-print $" NO FIND ($name | str upcase): ($r.err)"
|
|
continue
|
|
}
|
|
if $r.old_rev == $r.new_rev {
|
|
meat-print $" ($name | str upcase) STILL FRESH"
|
|
continue
|
|
}
|
|
$lock = $lock | upsert $name $r.entry
|
|
write-lock $lock
|
|
let from = if ($r.old_rev | is-empty) { "NEW" } else { $r.old_rev | str substring 0..8 }
|
|
meat-print $" ($name | str upcase): ($from) -> ($r.new_rev | str substring 0..8)"
|
|
}
|
|
}
|
|
print ""
|
|
}
|
|
|
|
def cmd-look [] {
|
|
with-frame {
|
|
meat-print "LOOK FOR NEW MEATS.."
|
|
let pins = load-pins
|
|
let shorturls = $pins.shorturls? | default {}
|
|
let inputs = $pins.inputs
|
|
let lock = load-lock
|
|
|
|
let rows = $inputs | transpose name pin
|
|
|
|
# Query every upstream head in parallel; no printing happens here.
|
|
let results = $rows | par-each { |row|
|
|
let old_rev = ($lock | get -o $row.name) | default {} | get -o rev
|
|
try {
|
|
let expanded = expand-shorturl $row.pin.url $shorturls
|
|
let new_rev = ls-remote-head $expanded
|
|
{ name: $row.name, ok: true, stale: ($old_rev != $new_rev), old_rev: $old_rev, new_rev: $new_rev }
|
|
} catch {
|
|
{ name: $row.name, ok: false }
|
|
}
|
|
}
|
|
|
|
# Report in input order so output is deterministic and never races.
|
|
mut stale = []
|
|
for row in $rows {
|
|
let r = $results | where name == $row.name | first
|
|
if not $r.ok {
|
|
meat-print $" NO FIND ($row.name | str upcase).."
|
|
continue
|
|
}
|
|
if $r.stale {
|
|
let from = if ($r.old_rev | is-empty) { "NEW" } else { $r.old_rev | str substring 0..8 }
|
|
meat-print $" ($row.name | str upcase): ($from) -> ($r.new_rev | str substring 0..8)"
|
|
$stale = ($stale | append $row.name)
|
|
}
|
|
}
|
|
if ($stale | is-empty) {
|
|
meat-print "NO MEAT FRESHER"
|
|
}
|
|
}
|
|
}
|
|
|
|
def truncate-desc [s: string, n: int]: nothing -> string {
|
|
if (($s | str length) > $n) {
|
|
($s | str substring 0..$n) + ".."
|
|
} else {
|
|
$s
|
|
}
|
|
}
|
|
|
|
def cmd-hunt [...query: string] {
|
|
with-frame {
|
|
if ($query | is-empty) {
|
|
meat-print "WHAT MEAT YOU SEEK?"
|
|
return
|
|
}
|
|
let q = $query | str join " "
|
|
meat-print $"HUNTING FOR ($q | str upcase).."
|
|
let body = {
|
|
from: 0,
|
|
size: 20,
|
|
sort: [{ _score: "desc", package_attr_name: "desc", package_pversion: "desc" }],
|
|
collapse: { field: "package_attr_name" },
|
|
query: {
|
|
bool: {
|
|
must: [
|
|
{ term: { type: "package" } },
|
|
{ multi_match: {
|
|
type: "cross_fields",
|
|
query: $q,
|
|
fields: ["package_attr_name^9", "package_pname^6", "package_description^1.3", "package_longDescription^1"]
|
|
} }
|
|
]
|
|
}
|
|
}
|
|
}
|
|
try {
|
|
let resp = (
|
|
http post
|
|
--user "aWVSALXpZv"
|
|
--password "X8gPHnzL52wFEekuxsfQ9cSh"
|
|
--content-type "application/json"
|
|
"https://nixos-search-7-1733963800.us-east-1.bonsaisearch.net/nixos-*-unstable-*/_search"
|
|
$body
|
|
)
|
|
let hits = $resp.hits?.hits? | default []
|
|
if ($hits | is-empty) {
|
|
meat-print "NO MEATS FOUND!"
|
|
} else {
|
|
for hit in $hits {
|
|
let src = $hit._source
|
|
let name = $src.package_attr_name? | default "?"
|
|
let ver = $src.package_pversion? | default ""
|
|
let desc = $src.package_description? | default ""
|
|
let ver_str = if ($ver | is-empty) { "" } else { $" \(($ver)\)" }
|
|
print $" \tnixpkgs#($name)($ver_str)"
|
|
if not ($desc | is-empty) {
|
|
print $" \t (truncate-desc $desc 60)"
|
|
}
|
|
}
|
|
}
|
|
} catch { |e|
|
|
meat-print $"HUNT FAILED: ($e.msg)"
|
|
}
|
|
}
|
|
}
|
|
|
|
def cmd-ritual [] {
|
|
with-frame {
|
|
meat-print "PREPARING RITUAL GROUND.."
|
|
let tmpdir = ^mktemp -d -t "meat-chew.XXXXXX" | str trim
|
|
let meats = $env.MEATS
|
|
let nix_conf = $"($tmpdir)/nix.conf"
|
|
let build = $"($tmpdir)/build"
|
|
try {
|
|
warm-pins
|
|
nix-build-nom $nix_conf $"($meats)/entry.nix" (nix-conf-attr)
|
|
meat-print "CONSUMING MEATS.."
|
|
with-env { NIX_USER_CONF_FILES: $nix_conf } {
|
|
nix-build-nom $build $"($meats)/entry.nix" (system-attr)
|
|
}
|
|
^$env.DIFFER /nix/var/nix/profiles/system $build
|
|
activate $build
|
|
try { rm $nix_conf }
|
|
try { rm $build }
|
|
} catch {
|
|
print "FAILED TO CONSUME MEATS."
|
|
}
|
|
}
|
|
}
|
|
|
|
def cmd-shelter [new_closure: string] {
|
|
activate $new_closure
|
|
}
|
|
|
|
def main [...args: string] {
|
|
if ($env.MEATS? | is-empty) {
|
|
meat-print "NO PATH TO RUNESTONE FOUND!"
|
|
cmd-help
|
|
return
|
|
}
|
|
let sub = $args | get 0? | default "" | str downcase
|
|
let rest = $args | skip 1
|
|
match $sub {
|
|
"yum" => { cmd-yum ...$rest }
|
|
"cook" => { cmd-cook ...$rest }
|
|
"poke" => { cmd-poke ...$rest }
|
|
"gut" => { cmd-gut ...$rest }
|
|
"trade" => { cmd-trade }
|
|
"look" => { cmd-look }
|
|
"fresh" => { cmd-fresh ...$rest }
|
|
"hunt" => { cmd-hunt ...$rest }
|
|
"ritual" => { cmd-ritual }
|
|
"shelter" => { cmd-shelter $rest }
|
|
_ => { cmd-help }
|
|
}
|
|
}
|