diff --git a/.tack/default.nix b/.tack/default.nix new file mode 100644 index 0000000..16c00f6 --- /dev/null +++ b/.tack/default.nix @@ -0,0 +1,292 @@ +# 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 + 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); + + fetchPin = name: fetchTree lock.${name}; + + 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 { } diff --git a/.tack/pins.lock.json b/.tack/pins.lock.json new file mode 100644 index 0000000..370c7ac --- /dev/null +++ b/.tack/pins.lock.json @@ -0,0 +1,24 @@ +{ + "fenix": { + "type": "github", + "owner": "nix-community", + "repo": "fenix", + "rev": "26ddad7ab014ac49bf02fc52e76f801963d684ae", + "narHash": "sha256-7JhNLs5O+FgXmU8FFP9DMug5xv3y4no+n4Kn94pkFGU=", + "lastModified": 1780485330 + }, + "nixpkgs": { + "type": "tarball", + "url": "https://releases.nixos.org/nixpkgs/nixpkgs-26.11pre1008784.4df1b885d76a/nixexprs.tar.xz", + "narHash": "sha256-bWVU1JP9hCYZzQjMLdMzr/FINF+UvpZGvCJcnNY616k=", + "lastModified": 1780362082 + }, + "systems": { + "type": "github", + "owner": "nix-systems", + "repo": "default-linux", + "rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68", + "narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=", + "lastModified": 1689347949 + } +} diff --git a/.tack/pins.toml b/.tack/pins.toml new file mode 100644 index 0000000..2e0a80b --- /dev/null +++ b/.tack/pins.toml @@ -0,0 +1,39 @@ +# 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"] + +# inputs. fields +# url required, shorturl ok, ?rev= 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.systems] +url = "github:nix-systems/default-linux" +type = "fetch" + +[inputs.nixpkgs] +url = "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz" + +[inputs.fenix] +url = "github:nix-community/fenix" + +[inputs.fenix.follows] +nixpkgs = "nixpkgs" diff --git a/flake.lock b/flake.lock index d6af19a..5999137 100644 --- a/flake.lock +++ b/flake.lock @@ -1,81 +1,6 @@ { "nodes": { - "fenix": { - "inputs": { - "nixpkgs": [ - "nixpkgs" - ], - "rust-analyzer-src": "rust-analyzer-src" - }, - "locked": { - "lastModified": 1780397302, - "narHash": "sha256-SQmrkj17xBdvc8lVMbsY7pIJCjKftgNh42R1keP3dLQ=", - "owner": "nix-community", - "repo": "fenix", - "rev": "f6670530f53e69cc284f7aef818eb0f08fe81905", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "fenix", - "type": "github" - } - }, - "nixpkgs": { - "locked": { - "lastModified": 1779536132, - "narHash": "sha256-q+fF42iv/geEbHfgSzy3tS0FF/EyD6XTZ98E6yxiBO8=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "3d8f0f3f72a6cd4d93d0ad13203f2ea1cb7e1456", - "type": "github" - }, - "original": { - "owner": "nixos", - "ref": "nixpkgs-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "root": { - "inputs": { - "fenix": "fenix", - "nixpkgs": "nixpkgs", - "systems": "systems" - } - }, - "rust-analyzer-src": { - "flake": false, - "locked": { - "lastModified": 1780337665, - "narHash": "sha256-RS3F+/6dtBIwd5+47GVen3jAk62/KumFZ4q3RVYafDk=", - "owner": "rust-lang", - "repo": "rust-analyzer", - "rev": "55af1774e9e840d3b28d12fcfaa72d2e2caa8ac9", - "type": "github" - }, - "original": { - "owner": "rust-lang", - "ref": "nightly", - "repo": "rust-analyzer", - "type": "github" - } - }, - "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" - } - } + "root": {} }, "root": "root", "version": 7 diff --git a/flake.nix b/flake.nix index 316b704..6fb0748 100644 --- a/flake.nix +++ b/flake.nix @@ -1,17 +1,9 @@ { - inputs.nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; - inputs.fenix.url = "github:nix-community/fenix"; - inputs.fenix.inputs.nixpkgs.follows = "nixpkgs"; - inputs.systems.url = "github:nix-systems/default-linux"; - + # nixpkgs, systems, and fenix are pinned with tack (see ./.tack), not flake inputs outputs = - { - self, - nixpkgs, - systems, - fenix, - }: + { self }: let + inherit (import ./.tack) nixpkgs systems fenix; forAllSystems = function: nixpkgs.lib.genAttrs (import systems) (system: function nixpkgs.legacyPackages.${system} system);