Monorepo for Tangled
tangled.org
1{
2 pkgs,
3 lib,
4 options,
5 ...
6} @ args: let
7 configPath = /run/spindle/user-config/config.json;
8 userConfig =
9 args.userConfig
10 or (
11 if builtins.pathExists configPath
12 then lib.importJSON configPath
13 else {}
14 );
15
16 registry = userConfig.registry or {};
17
18 # registry targets may be structured attrs or flake ref strings; strings are
19 # parsed by nix itself in getFlake. flakeRefToString rejects unforced attr
20 # values, hence the toJSON round-trip
21 toRefString = target:
22 if builtins.isAttrs target
23 then builtins.flakeRefToString (builtins.fromJSON (builtins.toJSON target))
24 else target;
25
26 # user registry entries shadow the system registry (which pins nixpkgs)
27 getFlake = ref: builtins.getFlake (toRefString (registry.${ref} or ref));
28
29 # "flakeref#attr" or a bare attr looked up in nixpkgs. nixpkgs refs use the
30 # already-evaluated pkgs directly instead of re-evaluating via getFlake,
31 # unless the user remapped nixpkgs in their registry
32 resolvePackage = ref: let
33 parts = lib.splitString "#" ref;
34 hasAttr = lib.length parts > 1;
35 flakeRef =
36 if hasAttr
37 then lib.head parts
38 else "nixpkgs";
39 pkgName =
40 if hasAttr
41 then lib.elemAt parts 1
42 else ref;
43 system = pkgs.stdenv.hostPlatform.system;
44 flake = getFlake flakeRef;
45 notFound = throw "Package ${pkgName} not found in ${flakeRef}";
46 in
47 if flakeRef == "nixpkgs" && !(registry ? nixpkgs)
48 then pkgs.${pkgName} or notFound
49 else flake.legacyPackages.${system}.${pkgName} or flake.packages.${system}.${pkgName} or notFound;
50
51 # strings are resolved as package references only where the option type
52 # actually expects packages; everything else passes through untouched
53 resolveForType = type: v:
54 if type.name == "package" && builtins.isString v
55 then resolvePackage v
56 # path-typed options (e.g. services.udev.packages) accept derivations via
57 # coercion; "#" disambiguates flake refs from actual paths, which are
58 # always absolute
59 else if type.name == "path" && builtins.isString v && lib.hasInfix "#" v && !lib.hasPrefix "/" v
60 then resolvePackage v
61 else if type.name == "nullOr" && v != null
62 then resolveForType type.nestedTypes.elemType v
63 else if type.name == "listOf" && builtins.isList v
64 then map (resolveForType type.nestedTypes.elemType) v
65 else if (type.name == "attrsOf" || type.name == "lazyAttrsOf") && builtins.isAttrs v
66 then builtins.mapAttrs (_: resolveForType type.nestedTypes.elemType) v
67 else if type.name == "submodule" && builtins.isAttrs v
68 then resolveOptions (type.getSubOptions []) v
69 else v;
70
71 resolveOptions = opts: builtins.mapAttrs (name: resolveValue (opts.${name} or null));
72
73 resolveValue = opt: v:
74 if !builtins.isAttrs opt
75 then v
76 else if lib.isOption opt
77 then resolveForType opt.type v
78 else if builtins.isAttrs v
79 then resolveOptions opt v
80 else v;
81
82 # `foo = true` is shorthand for `foo.enable = true`, but only when an
83 # enable option actually exists under foo
84 hasEnableOption = opt:
85 builtins.isAttrs opt
86 && (
87 if lib.isOption opt
88 then (opt.type.getSubOptions opt.loc) ? enable
89 else opt ? enable && lib.isOption opt.enable
90 );
91
92 normalize = opts: name: v: let
93 opt = opts.${name} or null;
94 in
95 if builtins.isBool v && hasEnableOption opt
96 then {enable = v;}
97 else resolveValue opt v;
98
99 # dependencies go into a devshell so we can make use of stdenv setup hooks
100 # (e.g. for pkg-config and such)
101 dependencies = userConfig.dependencies or [];
102 spindleDevShell = pkgs.mkShellNoCC {
103 name = "spindle-deps";
104 packages = map resolvePackage dependencies;
105 };
106in {
107 nix.registry = builtins.mapAttrs (name: _:
108 lib.mkForce {
109 to = {
110 type = "path";
111 path = (getFlake name).outPath;
112 };
113 })
114 registry;
115 # put the devshell into the resulting image env.
116 # we do this instead of using a `.nix` file because it lets us skip eval time.
117 environment.etc = lib.mkIf (dependencies != []) {
118 "spindle/devshell.drv".source = spindleDevShell.drvPath;
119 "spindle/devshell-inputs".source = spindleDevShell.inputDerivation;
120 };
121 services = builtins.mapAttrs (normalize (options.services or {})) (userConfig.services or {});
122 virtualisation = builtins.mapAttrs (normalize (options.virtualisation or {})) (userConfig.virtualisation or {});
123}