{ lib, }: let inherit (builtins) isAttrs isList isString hasAttr getAttr attrNames replaceStrings ; inherit (lib) mapAttrs recursiveUpdate; # Deep-merge attrsets (right-biased). deepMerge = a: b: recursiveUpdate a b; # Merge component sources: base.sources overlaid by overrides (component-wise deep merge). mergeSources = baseSources: overrides: baseSources // mapAttrs ( name: ov: if hasAttr name baseSources then deepMerge (getAttr name baseSources) ov else ov ) overrides; # Apply a single variant overlay (variables + sources). applyVariantOnce = selected: variant: let vVars = variant.variables or { }; vSrcs = variant.sources or { }; in { variables = selected.variables // vVars; sources = mergeSources selected.sources vSrcs; }; # Apply platform-specific overrides if present for the given system. applyPlatforms = selected: variant: system: if system == null || !(variant ? platforms) || !(hasAttr system variant.platforms) then selected else let p = variant.platforms.${system}; pVars = p.variables or { }; pSrcs = p.sources or { }; in { variables = selected.variables // pVars; sources = mergeSources selected.sources pSrcs; }; # Resolve variant chain via inherits (ancestor first), then apply platforms. resolveVariant = spec: baseSelected: variantName: system: if variantName == null || !(spec ? variants) || !(hasAttr variantName spec.variants) then baseSelected else let v = spec.variants.${variantName}; parentSelected = if v ? inherits then resolveVariant spec baseSelected v.inherits system else baseSelected; withVariant = applyVariantOnce parentSelected v; in applyPlatforms withVariant v system; # Render ${var} substitutions in any string within attrs/lists. renderValue = value: vars: if isString value then let keys = attrNames vars; patterns = map (k: "\${" + k + "}") keys; replacements = map (k: toString (getAttr k vars)) keys; in replaceStrings patterns replacements value else if isAttrs value then mapAttrs (_: v: renderValue v vars) value else if isList value then map (v: renderValue v vars) value else value; # Decide fetcher for URL type based on optional extra.unpack hint. useFetchZip = comp: comp ? extra && comp.extra ? unpack && comp.extra.unpack == "zip"; # Build a single src from a rendered component spec, using the given pkgs for fetchers. mkSrcFromRendered' = pkgs': comp: let fetcher = comp.fetcher or "none"; in if fetcher == "github" then pkgs'.fetchFromGitHub ( { inherit (comp) owner repo hash; # Allow tag as rev (ignore null/empty tag) rev = if comp ? tag && comp.tag != null && comp.tag != "" then comp.tag else comp.rev; fetchSubmodules = comp.submodules or false; } // lib.optionalAttrs (comp ? name) { inherit (comp) name; } ) else if fetcher == "git" then pkgs'.fetchgit { inherit (comp) url rev hash; fetchSubmodules = comp.submodules or false; } else if fetcher == "url" then let url = comp.url or comp.urlTemplate; in if useFetchZip comp then pkgs'.fetchzip ( { inherit (comp) hash; inherit url; } // lib.optionalAttrs (comp ? extra && comp.extra ? stripRoot) { inherit (comp.extra) stripRoot; } ) else pkgs'.fetchurl { inherit (comp) hash; inherit url; } else if fetcher == "pypi" then pkgs'.python3Packages.fetchPypi { inherit (comp) version hash; pname = comp.name; } else # fetcher == "none": pass-through (e.g., linux version/hash consumed by custom logic) comp; in rec { /* Select a variant from a loaded version.json specification. Usage: let selected = versioning.selectVariant spec variantName system; - spec: attrset from lib.importJSON ./version.json - variantName: string or null (when null, uses spec.defaultVariant if present) - system: string like "x86_64-linux" or null (to apply platforms overrides) */ selectVariant = spec: variantName: system: let chosen = if variantName != null then variantName else (spec.defaultVariant or null); baseSelected = { variables = spec.variables or { }; sources = spec.sources or { }; }; in resolveVariant spec baseSelected chosen system; /* Render ${var} template substitutions across any value using provided variables. Strings, attrsets, and lists are traversed. */ render = value: variables: renderValue value variables; /* Render a component with variables and then build its src (or pass-through for fetcher "none"). Prefer using mkAllSources, which handles rendering for all components. pkgs: the nixpkgs instance to use for fetchers (must match the target system). */ mkSrc = pkgs': comp: variables: let rendered = renderValue comp variables; in mkSrcFromRendered' pkgs' rendered; /* Produce an attrset of all sources for a selected spec: mkAllSources pkgs selected Where: pkgs: the nixpkgs instance to use for fetchers (must match the target system). selected = selectVariant spec variantName system Returns: { componentName = src | renderedComp (for "none"); ... } */ mkAllSources = pkgs': selected: mapAttrs ( _name: comp: if comp ? fetcher && comp.fetcher == "none" then renderValue comp selected.variables else mkSrc pkgs' (renderValue comp selected.variables) selected.variables ) selected.sources; # Expose deepMerge for convenience (right-biased). inherit deepMerge; }