delegate pin management to tack
This commit is contained in:
parent
070c3f0a7f
commit
9b2604da81
8 changed files with 402 additions and 274 deletions
302
.tack/default.nix
Normal file
302
.tack/default.nix
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
# SPDX-License-Identifier: EUPL-1.2
|
||||
# tack-managed resolver. delete this line to take ownership; tack will leave it alone afterwards.
|
||||
|
||||
let
|
||||
inherit (builtins)
|
||||
attrNames
|
||||
attrValues
|
||||
concatMap
|
||||
elemAt
|
||||
filter
|
||||
foldl'
|
||||
fromJSON
|
||||
head
|
||||
intersectAttrs
|
||||
isList
|
||||
isString
|
||||
listToAttrs
|
||||
mapAttrs
|
||||
match
|
||||
pathExists
|
||||
readFile
|
||||
substring
|
||||
tail
|
||||
trace
|
||||
;
|
||||
|
||||
call =
|
||||
{
|
||||
overrides ? { },
|
||||
}:
|
||||
let
|
||||
pins = fromTOML (readFile ./pins.toml);
|
||||
lock = fromJSON (readFile ./pins.lock.json);
|
||||
all_follow_raw = pins.all_follow or { };
|
||||
|
||||
# flatten `target = [aliases]` rows alongside `alias = "target"` rows
|
||||
all_follow = foldl' (
|
||||
acc: key:
|
||||
let
|
||||
val = all_follow_raw.${key};
|
||||
in
|
||||
if isList val then
|
||||
acc
|
||||
// {
|
||||
${key} = key;
|
||||
}
|
||||
// listToAttrs (
|
||||
map (a: {
|
||||
name = a;
|
||||
value = key;
|
||||
}) val
|
||||
)
|
||||
else if isString val then
|
||||
acc // { ${key} = val; }
|
||||
else
|
||||
acc
|
||||
) { } (attrNames all_follow_raw);
|
||||
|
||||
# a path node's stored path is absolute, or relative to this resolver dir
|
||||
fetchPin =
|
||||
name:
|
||||
let
|
||||
node = lock.${name};
|
||||
in
|
||||
if (node.type or "") == "path" && substring 0 1 node.path != "/" then
|
||||
fetchTree (node // { path = ./. + ("/" + node.path); })
|
||||
else
|
||||
fetchTree node;
|
||||
|
||||
fetchFixed =
|
||||
name: entry:
|
||||
let
|
||||
raw = derivation {
|
||||
inherit name;
|
||||
inherit (entry) url;
|
||||
builder = "builtin:fetchurl";
|
||||
system = "builtin";
|
||||
outputHash = entry.sha256;
|
||||
outputHashAlgo = "sha256";
|
||||
outputHashMode = "flat";
|
||||
};
|
||||
unpacked = derivation {
|
||||
inherit name;
|
||||
builder = "builtin:unpack-channel";
|
||||
system = "builtin";
|
||||
src = raw;
|
||||
channelName = name;
|
||||
};
|
||||
in
|
||||
if (entry.unpack or "file") == "tarball" then unpacked.outPath + "/" + name else raw.outPath;
|
||||
|
||||
resolveSpec = upLock: spec: if isList spec then walkPath upLock upLock.root spec else spec;
|
||||
|
||||
walkPath =
|
||||
upLock: nodeName: path:
|
||||
if path == [ ] then
|
||||
nodeName
|
||||
else
|
||||
walkPath upLock (resolveSpec upLock upLock.nodes.${nodeName}.inputs.${head path}) (tail path);
|
||||
|
||||
followsFor =
|
||||
pin:
|
||||
let
|
||||
rules = removeAttrs all_follow (pin.exclude_follow or [ ]);
|
||||
in
|
||||
{
|
||||
level = (pin.follows or { }) // rules;
|
||||
deep = rules;
|
||||
};
|
||||
|
||||
resolveFollows = mapAttrs (
|
||||
_: target: self.${target} or (throw "tack: follows target '${target}' is not a pin")
|
||||
);
|
||||
|
||||
# follows key is `flake:name`, `tack:name`, or bare `name`
|
||||
# project onto one side, rekeyed to bare names
|
||||
followsForSide =
|
||||
side: follows:
|
||||
listToAttrs (
|
||||
concatMap (
|
||||
key:
|
||||
let
|
||||
m = match "(flake|tack):(.*)" key;
|
||||
in
|
||||
if m == null then
|
||||
[
|
||||
{
|
||||
name = key;
|
||||
value = follows.${key};
|
||||
}
|
||||
]
|
||||
else if head m == side then
|
||||
[
|
||||
{
|
||||
name = elemAt m 1;
|
||||
value = follows.${key};
|
||||
}
|
||||
]
|
||||
else
|
||||
[ ]
|
||||
) (attrNames follows)
|
||||
);
|
||||
|
||||
mkCallerInputs =
|
||||
upLock: nodeName: rawInputs: levelFollows: deepFollows:
|
||||
let
|
||||
resolved = resolveFollows levelFollows;
|
||||
in
|
||||
mapAttrs (
|
||||
n: _decl:
|
||||
resolved.${n} or (
|
||||
if upLock != null then
|
||||
let
|
||||
ref =
|
||||
(upLock.nodes.${nodeName}.inputs or { }).${n}
|
||||
or (throw "tack: input '${n}' declared but not in flake.lock node '${nodeName}'");
|
||||
childName = resolveSpec upLock ref;
|
||||
childNode = upLock.nodes.${childName};
|
||||
childSrc = fetchTree childNode.locked;
|
||||
in
|
||||
if childNode.flake or true then evalTransitive upLock childName childSrc deepFollows else childSrc
|
||||
else
|
||||
throw "tack: no flake.lock; cannot resolve input '${n}'"
|
||||
)
|
||||
) rawInputs;
|
||||
|
||||
mkFlakeResult =
|
||||
sourceInfo: flakeDir: callerInputs: outputs:
|
||||
outputs
|
||||
// sourceInfo
|
||||
// {
|
||||
outPath = flakeDir;
|
||||
inputs = callerInputs;
|
||||
inherit outputs sourceInfo;
|
||||
_type = "flake";
|
||||
};
|
||||
|
||||
evalFlake =
|
||||
sourceInfo: flakeDir: upLock: nodeName: levelFollows: deepFollows:
|
||||
let
|
||||
raw = import (flakeDir + "/flake.nix");
|
||||
|
||||
tackPinsPath = flakeDir + "/.tack/pins.toml";
|
||||
hasTack = pathExists tackPinsPath;
|
||||
upPins = if hasTack then fromTOML (readFile tackPinsPath) else { };
|
||||
|
||||
# project follows onto each side, keep only names that side has
|
||||
# bare follow reaches both; `flake:`/`tack:` reaches just one
|
||||
tackOverrides = resolveFollows (
|
||||
intersectAttrs (upPins.inputs or { }) (followsForSide "tack" levelFollows)
|
||||
);
|
||||
flakeLevel = intersectAttrs (raw.inputs or { }) (followsForSide "flake" levelFollows);
|
||||
|
||||
# deep follows pass down raw, so each descendant re-projects per side
|
||||
callerInputs = mkCallerInputs upLock nodeName (raw.inputs or { }) flakeLevel deepFollows;
|
||||
|
||||
# upstream declares its outputs forward tackOverrides; a closed `{ self }:`
|
||||
# would throw on the extra kwarg, so forward only when declared
|
||||
supportsOverrides = (upPins.tack or { }).recomposable or false;
|
||||
|
||||
extraArgs = if supportsOverrides && tackOverrides != { } then { inherit tackOverrides; } else { };
|
||||
|
||||
outputs = raw.outputs (callerInputs // extraArgs // { self = result; });
|
||||
|
||||
result =
|
||||
let
|
||||
base = mkFlakeResult sourceInfo flakeDir callerInputs outputs;
|
||||
in
|
||||
if hasTack && tackOverrides != { } && !supportsOverrides then
|
||||
trace "tack: ${flakeDir}: not marked recomposable (set [tack] recomposable = true); overrides will not reach upstream" base
|
||||
else
|
||||
base;
|
||||
in
|
||||
result;
|
||||
|
||||
evalTransitive =
|
||||
upLock: nodeName: sourceInfo: follows:
|
||||
evalFlake sourceInfo sourceInfo.outPath upLock nodeName follows follows;
|
||||
|
||||
evalTopFlake =
|
||||
sourceInfo: pin:
|
||||
let
|
||||
flakeDir = sourceInfo.outPath + (if pin ? dir then "/" + pin.dir else "");
|
||||
upLockPath = flakeDir + "/flake.lock";
|
||||
upLock = if pathExists upLockPath then fromJSON (readFile upLockPath) else null;
|
||||
rootNode = if upLock != null then upLock.root else null;
|
||||
f = followsFor pin;
|
||||
in
|
||||
evalFlake sourceInfo flakeDir upLock rootNode f.level f.deep;
|
||||
|
||||
evalFetch =
|
||||
sourceInfo: pin: subdir:
|
||||
let
|
||||
path = sourceInfo.outPath + subdir;
|
||||
tackPinsPath = path + "/.tack/pins.toml";
|
||||
hasTack = pathExists tackPinsPath;
|
||||
upPins = if hasTack then fromTOML (readFile tackPinsPath) else { };
|
||||
f = followsFor pin;
|
||||
# a fetch drill-in is tack-only
|
||||
tackOverrides = resolveFollows (
|
||||
intersectAttrs (upPins.inputs or { }) (followsForSide "tack" f.level)
|
||||
);
|
||||
in
|
||||
# a fetch pin is a source tree (path); hand back resolved inputs only when
|
||||
# there are overrides to push into the upstream's .tack
|
||||
if hasTack && tackOverrides != { } then
|
||||
let
|
||||
upstream = import (path + "/.tack");
|
||||
in
|
||||
# old resolvers return a plain attrset, not a callable functor
|
||||
if upstream ? __functor then
|
||||
(upstream { overrides = tackOverrides; }) // { outPath = path; }
|
||||
else
|
||||
trace "tack: ${path}: upstream .tack predates override support; overrides will not reach it" path
|
||||
else
|
||||
path;
|
||||
|
||||
loadPin =
|
||||
name: pin:
|
||||
let
|
||||
pinType = pin.type or (if pin.flake or true then "flake" else "fetch");
|
||||
subdir = if pin ? dir then "/" + pin.dir else "";
|
||||
in
|
||||
if pinType == "fixed" then
|
||||
fetchFixed name lock.${name}
|
||||
else
|
||||
let
|
||||
sourceInfo = fetchPin name;
|
||||
in
|
||||
if pinType == "flake" then evalTopFlake sourceInfo pin else evalFetch sourceInfo pin subdir;
|
||||
|
||||
declared = pins.inputs or { };
|
||||
|
||||
# undeclared lock entries are auto-dedup synthetics only when referenced as
|
||||
# [all_follow] targets; stale locks from hand-edits are ignored (tack rm to clean)
|
||||
autoTargets = listToAttrs (
|
||||
map (target: {
|
||||
name = target;
|
||||
value = true;
|
||||
}) (attrValues all_follow)
|
||||
);
|
||||
autoNames = filter (n: !(declared ? ${n}) && autoTargets ? ${n}) (attrNames lock);
|
||||
autoPin =
|
||||
name:
|
||||
let
|
||||
sourceInfo = fetchPin name;
|
||||
in
|
||||
if pathExists (sourceInfo.outPath + "/flake.nix") then evalTopFlake sourceInfo { } else sourceInfo;
|
||||
|
||||
self =
|
||||
(mapAttrs loadPin declared)
|
||||
// listToAttrs (
|
||||
map (name: {
|
||||
inherit name;
|
||||
value = autoPin name;
|
||||
}) autoNames
|
||||
)
|
||||
// overrides;
|
||||
in
|
||||
self // { __functor = _: call; };
|
||||
in
|
||||
call { }
|
||||
32
.tack/pins.lock.json
Normal file
32
.tack/pins.lock.json
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"nix-systems": {
|
||||
"type": "github",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default-linux",
|
||||
"rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68",
|
||||
"narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=",
|
||||
"lastModified": 1689347949
|
||||
},
|
||||
"nixpkgs": {
|
||||
"type": "tarball",
|
||||
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-26.11pre1011620.cbb5cf358f50/nixexprs.tar.xz",
|
||||
"narHash": "sha256-d86UGSjwACWRttincQzeiAEZYVPtP3kODhr9hYZD4e8=",
|
||||
"lastModified": 1780787167
|
||||
},
|
||||
"tack": {
|
||||
"type": "github",
|
||||
"owner": "manic-systems",
|
||||
"repo": "tack",
|
||||
"rev": "0bc8dcb07dbd85d5d8d889c89accc9b228a6e4cc",
|
||||
"narHash": "sha256-/GrnjZK71UdLFtugaXkShE8dWpvGxJyOHpj073v+pXU=",
|
||||
"lastModified": 1780970897
|
||||
},
|
||||
"unf": {
|
||||
"type": "git",
|
||||
"url": "https://git.atagen.co/atagen/unf",
|
||||
"ref": "HEAD",
|
||||
"rev": "4f5ab30ee524f09126b09f42f249c0aba756459d",
|
||||
"narHash": "sha256-ztzeJVOQDhQtE0z9DwtSWL+OyoWKZrAD4gzRRvUP9ug=",
|
||||
"lastModified": 1779596435
|
||||
}
|
||||
}
|
||||
41
.tack/pins.toml
Normal file
41
.tack/pins.toml
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# tack pins
|
||||
# edit by hand or with tack add / rm / alias
|
||||
# nix reads pins.lock.json
|
||||
# this file drives tack update
|
||||
|
||||
# shorturl schemes
|
||||
# scheme rest expands rest into {path}
|
||||
[shorturls]
|
||||
# gh = "github:{path}"
|
||||
|
||||
# all_follow maps input names to the top-level pin they follow
|
||||
# opt out per-input with exclude_follow
|
||||
[all_follow]
|
||||
# nixpkgs = "nixpkgs"
|
||||
# nixpkgs = ["nixpkgs-stable", "nixpkgs-unstable"]
|
||||
|
||||
[tack]
|
||||
recomposable = true
|
||||
|
||||
# inputs.<name> fields
|
||||
# url required, shorturl ok, ?rev=<sha> pins an exact commit
|
||||
# type flake (default), fetch, or fixed
|
||||
# flake legacy false means type = "fetch"
|
||||
# follows { child = "pin" } override map
|
||||
# exclude_follow all_follow names to skip
|
||||
# dir subdir holding flake.nix
|
||||
# submodules fetch git submodules
|
||||
# unpack fixed-only tarball or file
|
||||
[inputs]
|
||||
|
||||
[inputs.nixpkgs]
|
||||
url = "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz"
|
||||
|
||||
[inputs.nix-systems]
|
||||
url = "github:nix-systems/default-linux"
|
||||
|
||||
[inputs.unf]
|
||||
url = "git+https://git.atagen.co/atagen/unf"
|
||||
|
||||
[inputs.tack]
|
||||
url = "github:manic-systems/tack/next"
|
||||
105
flake.lock
generated
105
flake.lock
generated
|
|
@ -1,109 +1,6 @@
|
|||
{
|
||||
"nodes": {
|
||||
"ndg": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1779030999,
|
||||
"narHash": "sha256-PLR0pNxIN3JPs/rSVnXTIPXzkPLaGAdt/wjPq7+k1PE=",
|
||||
"owner": "feel-co",
|
||||
"repo": "ndg",
|
||||
"rev": "b363612b524436520c85100192ec2bdab9a675c0",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "feel-co",
|
||||
"repo": "ndg",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nix-systems": {
|
||||
"locked": {
|
||||
"lastModified": 1689347949,
|
||||
"narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default-linux",
|
||||
"rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default-linux",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1778869304,
|
||||
"narHash": "sha256-30sZNZoA1cqF5JNO9fVX+wgiQYjB7HJqqJ4ztCDeBZE=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "d233902339c02a9c334e7e593de68855ad26c4cb",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1775036866,
|
||||
"narHash": "sha256-ByAX1LkhCwZ94+KnFAmnJSMAvui7kgCxjHgUHsWAbfI=",
|
||||
"rev": "6201e203d09599479a3b3450ed24fa81537ebc4e",
|
||||
"type": "tarball",
|
||||
"url": "https://releases.nixos.org/nixos/unstable/nixos-26.05pre972949.6201e203d095/nixexprs.tar.xz?lastModified=1775036866&rev=6201e203d09599479a3b3450ed24fa81537ebc4e"
|
||||
},
|
||||
"original": {
|
||||
"type": "tarball",
|
||||
"url": "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz"
|
||||
}
|
||||
},
|
||||
"nixpkgs_3": {
|
||||
"locked": {
|
||||
"lastModified": 1778869304,
|
||||
"narHash": "sha256-30sZNZoA1cqF5JNO9fVX+wgiQYjB7HJqqJ4ztCDeBZE=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "d233902339c02a9c334e7e593de68855ad26c4cb",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nix-systems": "nix-systems",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"unf": "unf"
|
||||
}
|
||||
},
|
||||
"unf": {
|
||||
"inputs": {
|
||||
"ndg": "ndg",
|
||||
"nixpkgs": "nixpkgs_3"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1760178630,
|
||||
"narHash": "sha256-oxRMTQtzIO1yFRhY++Ss8+ea1cTH40bD/+FAE+m5NFk=",
|
||||
"ref": "refs/heads/main",
|
||||
"rev": "8a6aa536039f1b207888b1369c5cabf0b131e07b",
|
||||
"revCount": 5,
|
||||
"type": "git",
|
||||
"url": "https://git.atagen.co/atagen/unf"
|
||||
},
|
||||
"original": {
|
||||
"type": "git",
|
||||
"url": "https://git.atagen.co/atagen/unf"
|
||||
}
|
||||
}
|
||||
"root": {}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
|
|
|
|||
21
flake.nix
21
flake.nix
|
|
@ -1,20 +1,22 @@
|
|||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
|
||||
nix-systems.url = "github:nix-systems/default-linux";
|
||||
unf.url = "git+https://git.atagen.co/atagen/unf";
|
||||
};
|
||||
|
||||
outputs =
|
||||
inputs:
|
||||
with inputs;
|
||||
{ self, ... }@args:
|
||||
let
|
||||
inputs = import ./.tack { overrides = args.tackOverrides or { }; };
|
||||
inherit (inputs)
|
||||
nixpkgs
|
||||
nix-systems
|
||||
tack
|
||||
unf
|
||||
;
|
||||
version = builtins.toString self.lastModified;
|
||||
forEachSystem =
|
||||
function:
|
||||
nixpkgs.lib.genAttrs (import nix-systems) (
|
||||
system: function nixpkgs.legacyPackages.${system} system
|
||||
);
|
||||
tackFor = system: tack.packages.${system}.default;
|
||||
in
|
||||
{
|
||||
devShells = forEachSystem (
|
||||
|
|
@ -28,9 +30,10 @@
|
|||
);
|
||||
|
||||
packages = forEachSystem (
|
||||
pkgs: _: {
|
||||
pkgs: system: {
|
||||
default = pkgs.callPackage ./nix/default.nix {
|
||||
inherit version;
|
||||
tack = tackFor system;
|
||||
};
|
||||
docs = pkgs.callPackage unf.lib.pak-chooie {
|
||||
inherit self;
|
||||
|
|
@ -55,9 +58,11 @@
|
|||
in
|
||||
{
|
||||
imports = [ ./nix/module.nix ];
|
||||
programs.meat.tack = lib.mkDefault (tackFor pkgs.stdenv.hostPlatform.system);
|
||||
programs.meat.package = self.packages.${pkgs.stdenv.hostPlatform.system}.default.override {
|
||||
differ = cfg.differ;
|
||||
monitor = cfg.monitor;
|
||||
tack = cfg.tack;
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
167
meat.nu
Normal file → Executable file
167
meat.nu
Normal file → Executable file
|
|
@ -121,86 +121,14 @@ def cmd-trade [] {
|
|||
}
|
||||
}
|
||||
|
||||
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 lock-path [] { $"($env.MEATS)/.tack/pins.lock.json" }
|
||||
|
||||
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
|
||||
# Fetch one locked input into the store, mirroring the tack resolver'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
|
||||
|
|
@ -228,97 +156,14 @@ def warm-pins [] {
|
|||
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)"
|
||||
}
|
||||
do { cd $env.MEATS; ^$env.TACK update ...$names }
|
||||
}
|
||||
print ""
|
||||
}
|
||||
|
||||
def cmd-look [] {
|
||||
def cmd-look [...names: string] {
|
||||
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"
|
||||
}
|
||||
do { cd $env.MEATS; ^$env.TACK look ...$names }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -429,7 +274,7 @@ def main [...args: string] {
|
|||
"poke" => { cmd-poke ...$rest }
|
||||
"gut" => { cmd-gut ...$rest }
|
||||
"trade" => { cmd-trade }
|
||||
"look" => { cmd-look }
|
||||
"look" => { cmd-look ...$rest }
|
||||
"fresh" => { cmd-fresh ...$rest }
|
||||
"hunt" => { cmd-hunt ...$rest }
|
||||
"ritual" => { cmd-ritual }
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
nushell,
|
||||
makeBinaryWrapper,
|
||||
version,
|
||||
tack,
|
||||
differ ? pkgs.dix,
|
||||
monitor ? pkgs.nix-output-monitor,
|
||||
...
|
||||
|
|
@ -26,7 +27,8 @@ stdenvNoCC.mkDerivation {
|
|||
makeBinaryWrapper ${nushell}/bin/nu $out/bin/meat \
|
||||
--add-flags "$out/share/meat/meat.nu" \
|
||||
--set DIFFER ${lib.getExe differ} \
|
||||
--set MONITOR ${lib.getExe monitor}
|
||||
--set MONITOR ${lib.getExe monitor} \
|
||||
--set TACK ${lib.getExe tack}
|
||||
runHook postInstall
|
||||
'';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,10 @@ in
|
|||
description = "nix monitoring tool to use";
|
||||
default = pkgs.nix-output-monitor;
|
||||
};
|
||||
tack = mkOption {
|
||||
type = types.package;
|
||||
description = "tack pin manager";
|
||||
};
|
||||
};
|
||||
config = lib.mkIf cfg.enable {
|
||||
environment.sessionVariables.MEATS = cfg.flake;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue