# FLAKE FEEDBACK: # - if flake inputs are meant to be human-readable, a human should be able to easily track them down given the URL. # - this is not the case with registry URLs, like `nixpkgs/nixos-22.11`. # - this is marginally the case with schemes like `github:nixos/nixpkgs`. # - given the *existing* `git+https://` scheme, i propose expressing github URLs similarly: # - `github+https://github.com/nixos/nixpkgs/tree/nixos-22.11` # - this would allow for the same optimizations as today's `github:nixos/nixpkgs`, but without obscuring the source. # a code reader could view the source being referenced simply by clicking the https:// portion of that URI. # - need some way to apply local patches to inputs. # # # DEVELOPMENT DOCS: # - Flake docs: # - Flake RFC: # - Discussion: # - # # # COMMON OPERATIONS: # - update a specific flake input: # - `nix flake lock --update-input nixpkgs` { outputs = { self, }@inputs: let inherit (builtins) attrNames elem listToAttrs map mapAttrs; # redefine some nixpkgs `lib` functions to avoid the infinite recursion # of if we tried to use patched `nixpkgs.lib` as part of the patching process. mapAttrs' = f: set: listToAttrs (map (attr: f attr set.${attr}) (attrNames set)); optionalAttrs = cond: attrs: if cond then attrs else {}; # mapAttrs but without the `name` argument mapAttrValues = f: mapAttrs (_: f); nixpkgs' = import ./pkgs/additional/nixpkgs; nixpkgsUnpatched = nixpkgs' { doPatch = false; localSystem = "x86_64-linux"; }; nixpkgsCompiledBy = { system, variant ? "master" }: (nixpkgs' { inherit variant system; }).legacyPackages."${system}"; evalHost = { name, local, target, variant ? null, nixpkgs ? nixpkgs' { localSystem = local; system = target;} }: nixpkgs.nixos ( [ (optionalAttrs (variant == "light") { sane.maxBuildCost = 2; }) (optionalAttrs (variant == "min") { sane.maxBuildCost = 0; }) (import ./hosts/instantiate.nix { hostName = name; }) (import ./modules) (nixpkgs.appendOverlays [ self.overlays.pkgs ]).sops-nix.nixosModules.sops { nixpkgs.overlays = [ self.overlays.sane-all ]; } ] ); in { nixosConfigurations = let hosts = { servo = { name = "servo"; local = "x86_64-linux"; target = "x86_64-linux"; }; desko = { name = "desko"; local = "x86_64-linux"; target = "x86_64-linux"; }; desko-light = { name = "desko"; local = "x86_64-linux"; target = "x86_64-linux"; variant = "light"; }; lappy = { name = "lappy"; local = "x86_64-linux"; target = "x86_64-linux"; }; lappy-light = { name = "lappy"; local = "x86_64-linux"; target = "x86_64-linux"; variant = "light"; }; lappy-min = { name = "lappy"; local = "x86_64-linux"; target = "x86_64-linux"; variant = "min"; }; moby = { name = "moby"; local = "x86_64-linux"; target = "aarch64-linux"; }; moby-light = { name = "moby"; local = "x86_64-linux"; target = "aarch64-linux"; variant = "light"; }; moby-min = { name = "moby"; local = "x86_64-linux"; target = "aarch64-linux"; variant = "min"; }; # crappy is technically armv7a, and armv7l uses only a _subset_ of the available ISA. # but it's not as widely cached. crappy = { name = "crappy"; local = "x86_64-linux"; target = "armv7l-linux"; }; crappy-min = { name = "crappy"; local = "x86_64-linux"; target = "armv7l-linux"; variant = "min"; }; crappy-7a = { name = "crappy"; local = "x86_64-linux"; target = "armv7a-linux"; variant = "min"; }; rescue = { name = "rescue"; local = "x86_64-linux"; target = "x86_64-linux"; }; }; hostsNext = mapAttrs' (h: v: { name = "${h}-next"; value = v // { nixpkgs = nixpkgs' { localSystem = v.local; system = v.target; variant = "staging-next"; }; }; }) hosts; hostsStaging = mapAttrs' (h: v: { name = "${h}-staging"; value = v // { nixpkgs = nixpkgs' { localSystem = v.local; system = v.target; variant = "staging"; }; }; }) hosts; in mapAttrValues evalHost ( hosts // hostsNext // hostsStaging ); # unofficial output # this produces a EFI-bootable .img file (GPT with a /boot partition and a system (/ or /nix) partition). # after building this: # - flash it to a bootable medium (SD card, flash drive, HDD) # - resize the root partition (use cfdisk) # - mount the part # - chown root:nixbld /nix/store # - chown root:root -R /nix/store/* # - chown root:root -R /persist # if using impermanence # - populate any important things (persist/, home/colin/.ssh, etc) # - boot # - if fs wasn't resized automatically, then `sudo btrfs filesystem resize max /` # - checkout this flake into /etc/nixos AND UPDATE THE FS UUIDS. # - `nixos-rebuild --flake './#' switch` imgs = mapAttrValues (host: host.config.system.build.img) self.nixosConfigurations; # unofficial output hostConfigs = mapAttrValues (host: host.config) self.nixosConfigurations; hostSystems = mapAttrValues (host: host.config.system.build.toplevel) self.nixosConfigurations; hostPkgs = mapAttrValues (host: host.config.system.build.pkgs) self.nixosConfigurations; hostPrograms = mapAttrValues (host: mapAttrValues (p: p.package) host.config.sane.programs) self.nixosConfigurations; overlays = { # N.B.: `nix flake check` requires every overlay to take `final: prev:` at defn site, # hence the weird redundancy. default = final: prev: self.overlays.pkgs final prev; sane-all = final: prev: import ./overlays/all.nix final prev; pkgs = final: prev: import ./overlays/pkgs.nix final prev; preferences = final: prev: import ./overlays/preferences.nix final prev; }; # this includes both our native packages and all the nixpkgs packages. legacyPackages = let allPkgsFor = variant: additionalOverlays: system: (nixpkgs' { inherit system variant; localSystem = "x86_64-linux"; }) .appendOverlays ( [ self.overlays.pkgs ] ++ additionalOverlays ); allPkgsFor' = system: allPkgsFor "master" [(self: super: { # build `pkgsNext.FOO` to build the package FOO from nixpkgs staging-next branch pkgsNext = allPkgsFor "staging-next" [] system; pkgsStaging = allPkgsFor "staging" [] system; })] system ; in { x86_64-linux = allPkgsFor' "x86_64-linux"; aarch64-linux = allPkgsFor' "aarch64-linux"; }; # extract only our own packages from the full set. # because of `nix flake check`, we flatten the package set and only surface x86_64-linux packages. packages = mapAttrs (system: passthruPkgs: passthruPkgs.lib.filterAttrs (name: pkg: # keep only packages which will pass `nix flake check`, i.e. keep only: # - derivations (not package sets) # - packages that build for the given platform (! elem name [ "feeds" "pythonPackagesExtensions" ]) && (passthruPkgs.lib.meta.availableOn passthruPkgs.stdenv.hostPlatform pkg) ) ( import ./pkgs { pkgs = passthruPkgs; } ) ) # self.legacyPackages; { x86_64-linux = nixpkgs' { localSystem = "x86_64-linux"; }; } ; apps."x86_64-linux" = let pkgs = self.legacyPackages."x86_64-linux"; sanePkgs = import ./pkgs { inherit pkgs; }; deployScript = host: addr: action: pkgs.writeShellScript "deploy-${host}" '' set -e host="${host}" addr="${addr}" action="${if action != null then action else ""}" runOnTarget() { # run the command ($@) on the machine we're deploying to. # if that's a remote machine, then do it via ssh, else local shell. if [ -n "$addr" ]; then ssh "$addr" "$@" else "$@" fi } nix build ".#nixosConfigurations.$host.config.system.build.toplevel" --out-link "./build/result-$host" "$@" storePath="$(readlink ./build/result-$host)" # mimic `nixos-rebuild --target-host`, in effect: # - nix-copy-closure ... # - nix-env --set ... # - switch-to-configuration # avoid the actual `nixos-rebuild` for a few reasons: # - fewer nix evals # - more introspectability and debuggability # - sandbox friendliness (especially: `git` doesn't have to be run as root) if [ -n "$addr" ]; then sudo nix store sign -r -k /run/secrets/nix_signing_key "$storePath" # add more `-v` for more verbosity (up to 5). # builders-use-substitutes false: optimizes so that the remote machine doesn't try to get paths from its substituters. # we already have all paths here, and the remote substitution is slow to check and SERIOUSLY flaky on moby in particular. nix copy -vv --option builders-use-substitutes false --to "ssh-ng://$addr" "$storePath" fi if [ -n "$action" ]; then runOnTarget sudo nix-env -p /nix/var/nix/profiles/system --set "$storePath" runOnTarget sudo "$storePath/bin/switch-to-configuration" "$action" fi ''; deployApp = host: addr: action: { type = "app"; program = ''${deployScript host addr action}''; }; # pkg updating. # a cleaner alternative lives here: # mkUpdater :: [ String ] -> { type = "app"; program = path; } mkUpdater = attrPath: { type = "app"; program = let pkg = pkgs.lib.getAttrFromPath attrPath sanePkgs; strAttrPath = pkgs.lib.concatStringsSep "." attrPath; commandArgv = pkg.updateScript.command or pkg.updateScript; command = pkgs.lib.escapeShellArgs commandArgv; in builtins.toString (pkgs.writeShellScript "update-${strAttrPath}" '' set -x env UPDATE_NIX_NAME=${pkg.name} UPDATE_NIX_PNAME=${pkg.pname} UPDATE_NIX_OLD_VERSION=${pkg.version} UPDATE_NIX_ATTR_PATH=${strAttrPath} ${command} ''); }; mkUpdatersNoAliases = opts: basePath: pkgs.lib.concatMapAttrs (name: pkg: if pkg.recurseForDerivations or false then { "${name}" = mkUpdaters opts (basePath ++ [ name ]); } else if pkg.updateScript or null != null then { "${name}" = mkUpdater (basePath ++ [ name ]); } else {} ) (pkgs.lib.getAttrFromPath basePath sanePkgs); mkUpdaters = { ignore ? [], flakePrefix ? [] }@opts: basePath: let updaters = mkUpdatersNoAliases opts basePath; invokeUpdater = name: pkg: let fullPath = basePath ++ [ name ]; doUpdateByDefault = !builtins.elem fullPath ignore; # in case `name` has a `.` in it, we have to quote it escapedPath = builtins.map (p: ''"${p}"'') fullPath; updatePath = builtins.concatStringsSep "." (flakePrefix ++ escapedPath); in pkgs.lib.optionalString doUpdateByDefault ( pkgs.lib.escapeShellArgs [ "nix" "run" ".#${updatePath}" ] ); in { type = "app"; # top-level app just invokes the updater of everything one layer below it program = builtins.toString (pkgs.writeShellScript (builtins.concatStringsSep "-" (flakePrefix ++ basePath)) (builtins.concatStringsSep "\n" (pkgs.lib.mapAttrsToList invokeUpdater updaters) ) ); } // updaters; in { help = { type = "app"; program = let helpMsg = builtins.toFile "nixos-config-help-message" '' commands: - `nix run '.#help'` - show this message - `nix run '.#update.pkgs'` - updates every package - `nix run '.#update.feeds'` - updates metadata for all feeds - `nix run '.#init-feed' ` - `nix run '.#deploy.{desko,lappy,moby,servo}[-light|-test]' [nix args ...]` - build and deploy the host - `nix run '.#preDeploy.{desko,lappy,moby,servo}[-light]' [nix args ...]` - copy closures to a host, but don't activate it - or `nix run '.#preDeploy'` to target all hosts - `nix run '.#check'` - make sure all systems build; NUR evaluates - `nix run '.#bench'` - benchmark the eval time of common targets this flake provides specific build targets of interest: - `nix build '.#imgs.rescue'` ''; in builtins.toString (pkgs.writeShellScript "nixos-config-help" '' cat ${helpMsg} echo "" echo "complete flake structure:" nix flake show --option allow-import-from-derivation true ''); }; # wrangle some names to get package updaters which refer back into the flake, but also conditionally ignore certain paths (e.g. sane.feeds). # TODO: better design update = rec { _impl.pkgs.sane = mkUpdaters { flakePrefix = [ "update" "_impl" "pkgs" ]; ignore = [ [ "sane" "feeds" ] ]; } [ "sane" ]; pkgs = _impl.pkgs.sane; _impl.feeds.sane.feeds = mkUpdaters { flakePrefix = [ "update" "_impl" "feeds" ]; } [ "sane" "feeds" ]; feeds = _impl.feeds.sane.feeds; }; init-feed = { type = "app"; program = "${pkgs.feeds.init-feed}"; }; deploy = { desko = deployApp "desko" "desko" "switch"; desko-light = deployApp "desko-light" "desko" "switch"; lappy = deployApp "lappy" "lappy" "switch"; lappy-light = deployApp "lappy-light" "lappy" "switch"; lappy-min = deployApp "lappy-min" "lappy" "switch"; moby = deployApp "moby" "moby" "switch"; moby-light = deployApp "moby-light" "moby" "switch"; moby-min = deployApp "moby-min" "moby" "switch"; moby-test = deployApp "moby" "moby" "test"; servo = deployApp "servo" "servo" "switch"; # like `nixos-rebuild --flake . switch` self = deployApp "$(hostname)" "" "switch"; self-light = deployApp "$(hostname)-light" "" "switch"; self-min = deployApp "$(hostname)-min" "" "switch"; type = "app"; program = builtins.toString (pkgs.writeShellScript "deploy-all" '' nix run '.#deploy.lappy' nix run '.#deploy.moby' nix run '.#deploy.desko' nix run '.#deploy.servo' ''); }; preDeploy = { # build the host and copy the runtime closure to that host, but don't activate it. desko = deployApp "desko" "desko" null; desko-light = deployApp "desko-light" "desko" null; lappy = deployApp "lappy" "lappy" null; lappy-light = deployApp "lappy-light" "lappy" null; lappy-min = deployApp "lappy-min" "lappy" null; moby = deployApp "moby" "moby" null; moby-light = deployApp "moby-light" "moby" null; moby-min = deployApp "moby-min" "moby" null; servo = deployApp "servo" "servo" null; type = "app"; program = builtins.toString (pkgs.writeShellScript "predeploy-all" '' # copy the -min/-light variants first; this might be run while waiting on a full build. or the full build failed. nix run '.#preDeploy.moby-min' -- "$@" nix run '.#preDeploy.lappy-min' -- "$@" nix run '.#preDeploy.moby-light' -- "$@" nix run '.#preDeploy.lappy-light' -- "$@" nix run '.#preDeploy.desko-light' -- "$@" nix run '.#preDeploy.lappy' -- "$@" nix run '.#preDeploy.servo' -- "$@" nix run '.#preDeploy.moby' -- "$@" nix run '.#preDeploy.desko' -- "$@" ''); }; sync = { type = "app"; program = builtins.toString (pkgs.writeShellScript "sync-all" '' RC_lappy=$(nix run '.#sync.lappy' -- "$@") RC_moby=$(nix run '.#sync.moby' -- "$@") RC_desko=$(nix run '.#sync.desko' -- "$@") echo "lappy: $RC_lappy" echo "moby: $RC_moby" echo "desko: $RC_desko" ''); }; sync.desko = { # copy music from servo to desko # can run this from any device that has ssh access to desko and servo type = "app"; program = builtins.toString (pkgs.writeShellScript "sync-to-desko" '' sudo mount /mnt/desko/home ${pkgs.sane-scripts.sync-music}/bin/sane-sync-music --compat /mnt/servo/media/Music /mnt/desko/home/Music "$@" ''); }; sync.lappy = { # copy music from servo to lappy # can run this from any device that has ssh access to lappy and servo type = "app"; program = builtins.toString (pkgs.writeShellScript "sync-to-lappy" '' sudo mount /mnt/lappy/home ${pkgs.sane-scripts.sync-music}/bin/sane-sync-music --compress --compat /mnt/servo/media/Music /mnt/lappy/home/Music "$@" ''); }; sync.moby = { # copy music from servo to moby # can run this from any device that has ssh access to moby and servo type = "app"; program = builtins.toString (pkgs.writeShellScript "sync-to-moby" '' sudo mount /mnt/moby/home sudo mount /mnt/desko/home sudo mount /mnt/servo/media/Books # copy photos/screenshots from moby to desko: ${pkgs.rsync}/bin/rsync -arv --exclude servo-macros /mnt/moby/home/Pictures/ /mnt/desko/home/Pictures/moby/ # copy books from servo to moby; delete old/untracked ones, but keep KOreader state files (sdr) ${pkgs.rsync}/bin/rsync -arv --delete --exclude unprocessed --exclude '*.sdr' /mnt/servo/media/Books/ /mnt/moby/home/Books/local/servo/ # N.B.: limited by network/disk -> reduce job count to improve pause/resume behavior ${pkgs.sane-scripts.sync-music}/bin/sane-sync-music --compress --compat --jobs 4 /mnt/servo/media/Music /mnt/moby/home/Music "$@" ''); }; check = { type = "app"; program = builtins.toString (pkgs.writeShellScript "check-all" '' nix run '.#check.nur' RC0=$? nix run '.#check.hostConfigs' RC1=$? nix run '.#check.rescue' RC2=$? echo "nur: $RC0" echo "hostConfigs: $RC1" echo "rescue: $RC2" exit $(($RC0 | $RC1 | $RC2)) ''); }; check.nur = { # `nix run '.#check-nur'` # validates that my repo can be included in the Nix User Repository type = "app"; program = builtins.toString (pkgs.writeShellScript "check-nur" '' cd ${./.}/integrations/nur NIX_PATH= NIXPKGS_ALLOW_UNSUPPORTED_SYSTEM=1 nix-env -f . -qa \* --meta --xml \ --allowed-uris https://static.rust-lang.org \ --option restrict-eval true \ --option allow-import-from-derivation true \ --drv-path --show-trace \ -I nixpkgs=${nixpkgsUnpatched} \ -I nixpkgs-overlays=${./.}/hosts/common/nix/overlay \ -I ../../ \ | tee # tee to prevent interactive mode ''); }; check.hostConfigs = { type = "app"; program = let checkHost = host: let shellHost = pkgs.lib.replaceStrings [ "-" ] [ "_" ] host; in '' nix build -v '.#nixosConfigurations.${host}.config.system.build.toplevel' --out-link ./build/result-${host} -j2 "$@" RC_${shellHost}=$? ''; in builtins.toString (pkgs.writeShellScript "check-host-configs" '' # build minimally-usable hosts first, then their full image. # this gives me a minimal image i can deploy or copy over, early. ${checkHost "lappy-min"} ${checkHost "moby-min"} ${checkHost "desko-light"} ${checkHost "moby-light"} ${checkHost "lappy-light"} ${checkHost "desko"} ${checkHost "lappy"} ${checkHost "servo"} ${checkHost "moby"} ${checkHost "rescue"} # still want to build the -light variants first so as to avoid multiple simultaneous webkitgtk builds ${checkHost "desko-light-next"} ${checkHost "moby-light-next"} ${checkHost "desko-next"} ${checkHost "lappy-next"} ${checkHost "servo-next"} ${checkHost "moby-next"} ${checkHost "rescue-next"} echo "desko: $RC_desko" echo "lappy: $RC_lappy" echo "servo: $RC_servo" echo "moby: $RC_moby" echo "rescue: $RC_rescue" echo "desko-next: $RC_desko_next" echo "lappy-next: $RC_lappy_next" echo "servo-next: $RC_servo_next" echo "moby-next: $RC_moby_next" echo "rescue-next: $RC_rescue_next" # i don't really care if the -next hosts fail. i build them mostly to keep the cache fresh/ready exit $(($RC_desko | $RC_lappy | $RC_servo | $RC_moby | $RC_rescue)) '' ); }; check.rescue = { type = "app"; program = builtins.toString (pkgs.writeShellScript "check-rescue" '' nix build -v '.#imgs.rescue' --out-link ./build/result-rescue-img -j2 ''); }; bench = { type = "app"; program = builtins.toString (pkgs.writeShellScript "bench" '' doBench() { attrPath="$1" shift echo -n "benchmarking eval of '$attrPath'... " /run/current-system/sw/bin/time -f "%e sec" -o /dev/stdout \ nix eval --no-eval-cache --quiet --raw ".#$attrPath" --apply 'result: if result != null then "" else "unexpected null"' $@ 2> /dev/null } if [ -n "$1" ]; then doBench "$@" else doBench hostConfigs doBench hostConfigs.lappy doBench hostConfigs.lappy.sane.programs doBench hostConfigs.lappy.sane.users.colin doBench hostConfigs.lappy.sane.fs doBench hostConfigs.lappy.environment.systemPackages fi ''); }; }; templates = { env.python-data = { # initialize with: # - `nix flake init -t '/home/colin/dev/nixos/#env.python-data'` # then enter with: # - `nix develop` path = ./templates/env/python-data; description = "python environment for data processing"; }; pkgs.make = { # initialize with: # - `nix flake init -t '/home/colin/dev/nixos/#pkgs.make'` path = ./templates/pkgs/make; description = "default Makefile-based derivation"; }; pkgs.python = { # initialize with: # - `nix flake init -t '/home/colin/dev/nixos/#pkgs.python'` path = ./templates/pkgs/python; description = "python package"; }; pkgs.rust-inline = { # initialize with: # - `nix flake init -t '/home/colin/dev/nixos/#pkgs.rust-inline'` path = ./templates/pkgs/rust-inline; description = "rust package and development environment (inline rust sources)"; }; pkgs.rust = { # initialize with: # - `nix flake init -t '/home/colin/dev/nixos/#pkgs.rust'` path = ./templates/pkgs/rust; description = "rust package fit to ship in nixpkgs"; }; }; }; }