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