merge: update Natu workflow
This commit is contained in:
@@ -4,7 +4,7 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.guests.actualbudget;
|
cfg = config.guests.actualbudget; # NOLINT(let_order)
|
||||||
dataDir = "/data";
|
dataDir = "/data";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.guests.copyparty;
|
cfg = config.guests.copyparty; # NOLINT(let_order)
|
||||||
|
|
||||||
volumeType = lib.types.submodule {
|
volumeType = lib.types.submodule {
|
||||||
options = {
|
options = {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.guests.homepage;
|
cfg = config.guests.homepage; # NOLINT(let_order)
|
||||||
environmentFile = "/run/secrets/homepage.env";
|
environmentFile = "/run/secrets/homepage.env";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.guests.palworld;
|
cfg = config.guests.palworld; # NOLINT(let_order)
|
||||||
steamDir = "/data/steamcmd";
|
steamDir = "/data/steamcmd";
|
||||||
installDir = "/data/server";
|
installDir = "/data/server";
|
||||||
stateDir = "/data/state";
|
stateDir = "/data/state";
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.guests.valheim;
|
cfg = config.guests.valheim; # NOLINT(let_order)
|
||||||
steamDir = "/data/steamcmd";
|
steamDir = "/data/steamcmd";
|
||||||
installDir = "/data/server";
|
installDir = "/data/server";
|
||||||
stateDir = "/data/state";
|
stateDir = "/data/state";
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
}:
|
}:
|
||||||
# Alacritty as the desktop terminal.
|
# Alacritty as the desktop terminal.
|
||||||
let
|
let
|
||||||
cfg = config.modules.desktop.apps.alacritty;
|
cfg = config.modules.desktop.apps.alacritty; # NOLINT(let_order)
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,9 +7,9 @@
|
|||||||
}:
|
}:
|
||||||
# Firefox as the desktop browser: stock mainline, hardened and de-monetized by policy.
|
# Firefox as the desktop browser: stock mainline, hardened and de-monetized by policy.
|
||||||
let
|
let
|
||||||
cfg = config.modules.desktop.apps.firefox;
|
cfg = config.modules.desktop.apps.firefox; # NOLINT(let_order)
|
||||||
user = config.user.name;
|
|
||||||
firefoxAddons = inputs.firefox-addons.packages.${pkgs.stdenv.hostPlatform.system};
|
firefoxAddons = inputs.firefox-addons.packages.${pkgs.stdenv.hostPlatform.system};
|
||||||
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.desktop.apps.firefox = {
|
options.modules.desktop.apps.firefox = {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
}:
|
}:
|
||||||
# Ghostty as the desktop terminal.
|
# Ghostty as the desktop terminal.
|
||||||
let
|
let
|
||||||
cfg = config.modules.desktop.apps.ghostty;
|
cfg = config.modules.desktop.apps.ghostty; # NOLINT(let_order)
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
}:
|
}:
|
||||||
# Obsidian as the desktop note-taking vault.
|
# Obsidian as the desktop note-taking vault.
|
||||||
let
|
let
|
||||||
cfg = config.modules.desktop.apps.obsidian;
|
cfg = config.modules.desktop.apps.obsidian; # NOLINT(let_order)
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
}:
|
}:
|
||||||
# r2modman, a Thunderstore mod manager.
|
# r2modman, a Thunderstore mod manager.
|
||||||
let
|
let
|
||||||
cfg = config.modules.desktop.apps.r2modman;
|
cfg = config.modules.desktop.apps.r2modman; # NOLINT(let_order)
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
}:
|
}:
|
||||||
# Signal desktop messenger.
|
# Signal desktop messenger.
|
||||||
let
|
let
|
||||||
cfg = config.modules.desktop.apps.signal;
|
cfg = config.modules.desktop.apps.signal; # NOLINT(let_order)
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
}:
|
}:
|
||||||
# Steam game launcher and runtime integration.
|
# Steam game launcher and runtime integration.
|
||||||
let
|
let
|
||||||
cfg = config.modules.desktop.apps.steam;
|
cfg = config.modules.desktop.apps.steam; # NOLINT(let_order)
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.desktop.apps.steam.enable = lib.mkEnableOption "Steam game launcher";
|
options.modules.desktop.apps.steam.enable = lib.mkEnableOption "Steam game launcher";
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
}:
|
}:
|
||||||
# Vesktop, a Discord client with Vencord built in.
|
# Vesktop, a Discord client with Vencord built in.
|
||||||
let
|
let
|
||||||
cfg = config.modules.desktop.apps.vencord;
|
cfg = config.modules.desktop.apps.vencord; # NOLINT(let_order)
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{ config, lib, ... }:
|
{ config, lib, ... }:
|
||||||
# The desktop aggregator: one flag brings up the whole graphical session.
|
# The desktop aggregator: one flag brings up the whole graphical session.
|
||||||
let
|
let
|
||||||
cfg = config.modules.desktop;
|
cfg = config.modules.desktop; # NOLINT(let_order)
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.desktop.enable = lib.mkEnableOption "the keyboard-driven Hyprland desktop";
|
options.modules.desktop.enable = lib.mkEnableOption "the keyboard-driven Hyprland desktop";
|
||||||
|
|||||||
@@ -6,8 +6,7 @@
|
|||||||
}:
|
}:
|
||||||
# The Hyprland compositor.
|
# The Hyprland compositor.
|
||||||
let
|
let
|
||||||
cfg = config.modules.desktop.session.hyprland;
|
cfg = config.modules.desktop.session.hyprland; # NOLINT(let_order)
|
||||||
user = config.user.name;
|
|
||||||
cursor = config.stylix.cursor;
|
cursor = config.stylix.cursor;
|
||||||
|
|
||||||
# Numbered-workspace switch and move for 1..9, the operator's i3 muscle memory.
|
# Numbered-workspace switch and move for 1..9, the operator's i3 muscle memory.
|
||||||
@@ -16,6 +15,8 @@ let
|
|||||||
"$mod SHIFT, ${toString n}, movetoworkspace, ${toString n}"
|
"$mod SHIFT, ${toString n}, movetoworkspace, ${toString n}"
|
||||||
]) (lib.range 1 9);
|
]) (lib.range 1 9);
|
||||||
|
|
||||||
|
user = config.user.name;
|
||||||
|
|
||||||
# Flip the tiling strategy between the two built-in layouts, since neither a
|
# Flip the tiling strategy between the two built-in layouts, since neither a
|
||||||
# dispatcher nor a keyword toggles it on its own.
|
# dispatcher nor a keyword toggles it on its own.
|
||||||
toggleLayout = pkgs.writeShellScript "hypr-toggle-layout" ''
|
toggleLayout = pkgs.writeShellScript "hypr-toggle-layout" ''
|
||||||
|
|||||||
@@ -6,15 +6,16 @@
|
|||||||
}:
|
}:
|
||||||
# hypridle: idle-triggered locking and display power management.
|
# hypridle: idle-triggered locking and display power management.
|
||||||
let
|
let
|
||||||
cfg = config.modules.desktop.session.hyprland.hypridle;
|
cfg = config.modules.desktop.session.hyprland.hypridle; # NOLINT(let_order)
|
||||||
|
|
||||||
|
# The guard drops the call when a locker is already up, so no idle trigger
|
||||||
|
# stacks a second hyprlock over the first.
|
||||||
|
lockCmd = "${pkgs.procps}/bin/pidof hyprlock || ${hyprlock}";
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
|
|
||||||
hyprctl = "${config.programs.hyprland.package}/bin/hyprctl";
|
hyprctl = "${config.programs.hyprland.package}/bin/hyprctl";
|
||||||
hyprlock = "${config.home-manager.users.${user}.programs.hyprlock.package}/bin/hyprlock";
|
hyprlock = "${config.home-manager.users.${user}.programs.hyprlock.package}/bin/hyprlock";
|
||||||
|
|
||||||
# The guard drops the call when a locker is already up, so no idle trigger
|
|
||||||
# stacks a second hyprlock over the first.
|
|
||||||
lockCmd = "${pkgs.procps}/bin/pidof hyprlock || ${hyprlock}";
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.desktop.session.hyprland.hypridle = {
|
options.modules.desktop.session.hyprland.hypridle = {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
}:
|
}:
|
||||||
# The lock screen: a hyprlock session-lock surface the compositor owns.
|
# The lock screen: a hyprlock session-lock surface the compositor owns.
|
||||||
let
|
let
|
||||||
cfg = config.modules.desktop.session.hyprland.hyprlock;
|
cfg = config.modules.desktop.session.hyprland.hyprlock; # NOLINT(let_order)
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
|
|
||||||
# The hyprlock this module installs, used by the lock keybind below.
|
# The hyprlock this module installs, used by the lock keybind below.
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
# Text login: greetd running the tuigreet greeter, which starts the session
|
# Text login: greetd running the tuigreet greeter, which starts the session
|
||||||
# through the universal Wayland session manager.
|
# through the universal Wayland session manager.
|
||||||
let
|
let
|
||||||
cfg = config.modules.desktop.session.login;
|
cfg = config.modules.desktop.session.login; # NOLINT(let_order)
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.desktop.session.login.enable =
|
options.modules.desktop.session.login.enable =
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
}:
|
}:
|
||||||
# mako as the notification daemon: toasts, a do-not-disturb mode, and history recall.
|
# mako as the notification daemon: toasts, a do-not-disturb mode, and history recall.
|
||||||
let
|
let
|
||||||
cfg = config.modules.desktop.session.mako;
|
cfg = config.modules.desktop.session.mako; # NOLINT(let_order)
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
|
|
||||||
makoctl = "${pkgs.mako}/bin/makoctl";
|
makoctl = "${pkgs.mako}/bin/makoctl";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{ config, lib, ... }:
|
{ config, lib, ... }:
|
||||||
# XDG desktop portal routing for in-app screen sharing and file dialogs.
|
# XDG desktop portal routing for in-app screen sharing and file dialogs.
|
||||||
let
|
let
|
||||||
cfg = config.modules.desktop.session.portals;
|
cfg = config.modules.desktop.session.portals; # NOLINT(let_order)
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.desktop.session.portals.enable =
|
options.modules.desktop.session.portals.enable =
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
}:
|
}:
|
||||||
# rofi as the desktop launcher and dmenu-style menu frontend.
|
# rofi as the desktop launcher and dmenu-style menu frontend.
|
||||||
let
|
let
|
||||||
cfg = config.modules.desktop.session.rofi;
|
cfg = config.modules.desktop.session.rofi; # NOLINT(let_order)
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
|
|
||||||
# A session menu built on the same themed rofi, so the power actions look
|
# A session menu built on the same themed rofi, so the power actions look
|
||||||
|
|||||||
@@ -6,13 +6,14 @@
|
|||||||
}:
|
}:
|
||||||
# Waybar: a top status bar reading system state at a glance.
|
# Waybar: a top status bar reading system state at a glance.
|
||||||
let
|
let
|
||||||
cfg = config.modules.desktop.session.waybar;
|
cfg = config.modules.desktop.session.waybar; # NOLINT(let_order)
|
||||||
vpnClient = config.modules.network.vpn-client;
|
vpnClient = config.modules.network.vpn-client;
|
||||||
user = config.user.name;
|
|
||||||
|
|
||||||
# A Nerd Font Private-Use-Area codepoint as a literal character, decoded
|
# A Nerd Font Private-Use-Area codepoint as a literal character, decoded
|
||||||
# through JSON so the glyph survives as bytes rather than an editor paste.
|
# through JSON so the glyph survives as bytes rather than an editor paste.
|
||||||
g = code: builtins.fromJSON ''"\u${code}"'';
|
g = code: builtins.fromJSON ''"\u${code}"'';
|
||||||
|
user = config.user.name;
|
||||||
|
|
||||||
sleepOffIcon = builtins.fromJSON ''"\udb81\udcb3"'';
|
sleepOffIcon = builtins.fromJSON ''"\udb81\udcb3"'';
|
||||||
|
|
||||||
# Renders and toggles mako's do-not-disturb mode.
|
# Renders and toggles mako's do-not-disturb mode.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{ config, lib, ... }:
|
{ config, lib, ... }:
|
||||||
# PipeWire as the desktop audio server.
|
# PipeWire as the desktop audio server.
|
||||||
let
|
let
|
||||||
cfg = config.modules.desktop.utilities.audio;
|
cfg = config.modules.desktop.utilities.audio; # NOLINT(let_order)
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.desktop.utilities.audio.enable = lib.mkEnableOption "the PipeWire audio server";
|
options.modules.desktop.utilities.audio.enable = lib.mkEnableOption "the PipeWire audio server";
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
}:
|
}:
|
||||||
# Clipboard history: cliphist records every copy, picked back through rofi.
|
# Clipboard history: cliphist records every copy, picked back through rofi.
|
||||||
let
|
let
|
||||||
cfg = config.modules.desktop.utilities.clipboard;
|
cfg = config.modules.desktop.utilities.clipboard; # NOLINT(let_order)
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
|
|
||||||
cliphist = "${pkgs.cliphist}/bin/cliphist";
|
cliphist = "${pkgs.cliphist}/bin/cliphist";
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
}:
|
}:
|
||||||
# SwayOSD: a transient on-screen popup for volume and brightness changes.
|
# SwayOSD: a transient on-screen popup for volume and brightness changes.
|
||||||
let
|
let
|
||||||
cfg = config.modules.desktop.utilities.osd;
|
cfg = config.modules.desktop.utilities.osd; # NOLINT(let_order)
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
}:
|
}:
|
||||||
# Keybound screen recording: pick a region, then toggle a video-only capture.
|
# Keybound screen recording: pick a region, then toggle a video-only capture.
|
||||||
let
|
let
|
||||||
cfg = config.modules.desktop.utilities.recording;
|
cfg = config.modules.desktop.utilities.recording; # NOLINT(let_order)
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
|
|
||||||
slurp = "${pkgs.slurp}/bin/slurp";
|
slurp = "${pkgs.slurp}/bin/slurp";
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
# Keyboard-driven screenshots that open in satty for annotation, then land in
|
# Keyboard-driven screenshots that open in satty for annotation, then land in
|
||||||
# both the clipboard and a dated file.
|
# both the clipboard and a dated file.
|
||||||
let
|
let
|
||||||
cfg = config.modules.desktop.utilities.screenshot;
|
cfg = config.modules.desktop.utilities.screenshot; # NOLINT(let_order)
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
|
|
||||||
grimblast = "${pkgs.grimblast}/bin/grimblast";
|
grimblast = "${pkgs.grimblast}/bin/grimblast";
|
||||||
|
|||||||
@@ -6,8 +6,7 @@
|
|||||||
}:
|
}:
|
||||||
# Nord theming for the graphical layer, driven from one base16 scheme by Stylix.
|
# Nord theming for the graphical layer, driven from one base16 scheme by Stylix.
|
||||||
let
|
let
|
||||||
cfg = config.modules.desktop.utilities.theming;
|
cfg = config.modules.desktop.utilities.theming; # NOLINT(let_order)
|
||||||
user = config.user.name;
|
|
||||||
|
|
||||||
# A single static Nord wallpaper, drawn as a vertical gradient across the
|
# A single static Nord wallpaper, drawn as a vertical gradient across the
|
||||||
# Polar Night shades so it needs no committed binary and no network fetch.
|
# Polar Night shades so it needs no committed binary and no network fetch.
|
||||||
@@ -16,6 +15,7 @@ let
|
|||||||
-size 2560x1440 gradient:'#2E3440'-'#3B4252' \
|
-size 2560x1440 gradient:'#2E3440'-'#3B4252' \
|
||||||
"$out"
|
"$out"
|
||||||
'';
|
'';
|
||||||
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.desktop.utilities.theming.enable =
|
options.modules.desktop.utilities.theming.enable =
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# Declares the XDG user directories, so a tool that queries them lands in a
|
# Declares the XDG user directories, so a tool that queries them lands in a
|
||||||
# well-known folder a host can relocate from one place.
|
# well-known folder a host can relocate from one place.
|
||||||
let
|
let
|
||||||
cfg = config.modules.desktop.utilities.userdirs;
|
cfg = config.modules.desktop.utilities.userdirs; # NOLINT(let_order)
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{ config, lib, ... }:
|
{ config, lib, ... }:
|
||||||
let
|
let
|
||||||
cfg = config.modules.guest;
|
cfg = config.modules.guest; # NOLINT(let_order)
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.guest.enable = lib.mkEnableOption "nested-guest base configuration";
|
options.modules.guest.enable = lib.mkEnableOption "nested-guest base configuration";
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.modules.host;
|
cfg = config.modules.host; # NOLINT(let_order)
|
||||||
inherit (config) user;
|
inherit (config) user;
|
||||||
hostSecrets = ../secrets + "/${config.networking.hostName}.yaml";
|
hostSecrets = ../secrets + "/${config.networking.hostName}.yaml";
|
||||||
passwordSecret = "${user.name}-password";
|
passwordSecret = "${user.name}-password";
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
}:
|
}:
|
||||||
# Tools for developing this monorepo.
|
# Tools for developing this monorepo.
|
||||||
let
|
let
|
||||||
cfg = config.modules.monorepo;
|
cfg = config.modules.monorepo; # NOLINT(let_order)
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.monorepo.enable = lib.mkEnableOption "tools for developing this monorepo";
|
options.modules.monorepo.enable = lib.mkEnableOption "tools for developing this monorepo";
|
||||||
|
|||||||
@@ -6,33 +6,7 @@
|
|||||||
}:
|
}:
|
||||||
# SSH on this machine, in both directions.
|
# SSH on this machine, in both directions.
|
||||||
let
|
let
|
||||||
cfg = config.modules.network.ssh;
|
cfg = config.modules.network.ssh; # NOLINT(let_order)
|
||||||
user = config.user.name;
|
|
||||||
|
|
||||||
inherit (lib)
|
|
||||||
concatLists
|
|
||||||
concatStringsSep
|
|
||||||
elem
|
|
||||||
filter
|
|
||||||
genAttrs
|
|
||||||
hasAttr
|
|
||||||
imap0
|
|
||||||
listToAttrs
|
|
||||||
mapAttrs
|
|
||||||
mapAttrsToList
|
|
||||||
mkIf
|
|
||||||
mkMerge
|
|
||||||
mkOption
|
|
||||||
nameValuePair
|
|
||||||
optional
|
|
||||||
optionalAttrs
|
|
||||||
types
|
|
||||||
unique
|
|
||||||
;
|
|
||||||
|
|
||||||
hostKeySecret = type: "ssh-host-${type}-key";
|
|
||||||
userKeySecret = "ssh-user-ed25519-key";
|
|
||||||
|
|
||||||
targetType = types.submodule (
|
targetType = types.submodule (
|
||||||
{ name, ... }:
|
{ name, ... }:
|
||||||
{
|
{
|
||||||
@@ -90,54 +64,18 @@ let
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
keyWithoutComment = key: concatStringsSep " " (lib.take 2 (lib.splitString " " key));
|
|
||||||
|
|
||||||
targetNamesIn = names: filter (name: hasAttr name cfg.targets) names;
|
|
||||||
|
|
||||||
workstationNames = targetNamesIn cfg.hosts.workstations;
|
|
||||||
serverNames = targetNamesIn cfg.hosts.servers;
|
|
||||||
|
|
||||||
clientKeysFor = names: filter (key: key != null) (map (name: cfg.targets.${name}.clientKey) names);
|
|
||||||
|
|
||||||
currentHost = config.networking.hostName;
|
|
||||||
isServer = elem currentHost cfg.hosts.servers;
|
|
||||||
isWorkstation = elem currentHost cfg.hosts.workstations;
|
|
||||||
|
|
||||||
defaultAuthorizedKeys = lib.flatten (
|
defaultAuthorizedKeys = lib.flatten (
|
||||||
clientKeysFor workstationNames ++ optional isServer (clientKeysFor serverNames)
|
clientKeysFor workstationNames ++ optional isServer (clientKeysFor serverNames)
|
||||||
);
|
);
|
||||||
|
|
||||||
outboundTargetNames =
|
clientKeysFor = names: filter (key: key != null) (map (name: cfg.targets.${name}.clientKey) names);
|
||||||
let
|
|
||||||
groupTargets =
|
|
||||||
if isServer then
|
|
||||||
[ "gitea" ] ++ cfg.hosts.servers
|
|
||||||
else if isWorkstation then
|
|
||||||
[ "gitea" ] ++ cfg.hosts.servers ++ cfg.hosts.workstations
|
|
||||||
else
|
|
||||||
[ "gitea" ];
|
|
||||||
in
|
|
||||||
filter (name: name != currentHost) (targetNamesIn groupTargets);
|
|
||||||
|
|
||||||
sshSettingsFor =
|
workstationNames = targetNamesIn cfg.hosts.workstations;
|
||||||
name:
|
targetNamesIn = names: filter (name: hasAttr name cfg.targets) names;
|
||||||
let
|
|
||||||
target = cfg.targets.${name};
|
|
||||||
in
|
|
||||||
{
|
|
||||||
header = "Host ${concatStringsSep " " target.aliases}";
|
|
||||||
HostName = target.hostName;
|
|
||||||
User = target.user;
|
|
||||||
}
|
|
||||||
// optionalAttrs (target.port != 22) { Port = target.port; };
|
|
||||||
|
|
||||||
knownHostNamesFor =
|
isServer = elem currentHost cfg.hosts.servers;
|
||||||
target:
|
currentHost = config.networking.hostName;
|
||||||
let
|
serverNames = targetNamesIn cfg.hosts.servers;
|
||||||
names = unique (target.aliases ++ [ target.hostName ]);
|
|
||||||
withPort = name: if target.port == 22 then name else "[${name}]:${toString target.port}";
|
|
||||||
in
|
|
||||||
map withPort names;
|
|
||||||
|
|
||||||
knownHosts = listToAttrs (
|
knownHosts = listToAttrs (
|
||||||
concatLists (
|
concatLists (
|
||||||
@@ -153,6 +91,68 @@ let
|
|||||||
) cfg.targets
|
) cfg.targets
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
knownHostNamesFor =
|
||||||
|
target:
|
||||||
|
let
|
||||||
|
withPort = name: if target.port == 22 then name else "[${name}]:${toString target.port}";
|
||||||
|
names = unique (target.aliases ++ [ target.hostName ]);
|
||||||
|
in
|
||||||
|
map withPort names;
|
||||||
|
|
||||||
|
keyWithoutComment = key: concatStringsSep " " (lib.take 2 (lib.splitString " " key));
|
||||||
|
|
||||||
|
sshSettingsFor =
|
||||||
|
name:
|
||||||
|
let
|
||||||
|
target = cfg.targets.${name};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
header = "Host ${concatStringsSep " " target.aliases}";
|
||||||
|
HostName = target.hostName;
|
||||||
|
User = target.user;
|
||||||
|
}
|
||||||
|
// optionalAttrs (target.port != 22) { Port = target.port; };
|
||||||
|
|
||||||
|
outboundTargetNames =
|
||||||
|
let
|
||||||
|
groupTargets =
|
||||||
|
if isServer then
|
||||||
|
[ "gitea" ] ++ cfg.hosts.servers
|
||||||
|
else if isWorkstation then
|
||||||
|
[ "gitea" ] ++ cfg.hosts.servers ++ cfg.hosts.workstations
|
||||||
|
else
|
||||||
|
[ "gitea" ];
|
||||||
|
in
|
||||||
|
filter (name: name != currentHost) (targetNamesIn groupTargets);
|
||||||
|
|
||||||
|
isWorkstation = elem currentHost cfg.hosts.workstations;
|
||||||
|
|
||||||
|
hostKeySecret = type: "ssh-host-${type}-key";
|
||||||
|
user = config.user.name;
|
||||||
|
|
||||||
|
inherit (lib)
|
||||||
|
concatLists
|
||||||
|
concatStringsSep
|
||||||
|
elem
|
||||||
|
filter
|
||||||
|
genAttrs
|
||||||
|
hasAttr
|
||||||
|
imap0
|
||||||
|
listToAttrs
|
||||||
|
mapAttrs
|
||||||
|
mapAttrsToList
|
||||||
|
mkIf
|
||||||
|
mkMerge
|
||||||
|
mkOption
|
||||||
|
nameValuePair
|
||||||
|
optional
|
||||||
|
optionalAttrs
|
||||||
|
types
|
||||||
|
unique
|
||||||
|
;
|
||||||
|
|
||||||
|
userKeySecret = "ssh-user-ed25519-key";
|
||||||
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.network.ssh = {
|
options.modules.network.ssh = {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
}:
|
}:
|
||||||
# The host networking foundation: per-VLAN bridges over a tagged trunk.
|
# The host networking foundation: per-VLAN bridges over a tagged trunk.
|
||||||
let
|
let
|
||||||
cfg = config.modules.network.vlans;
|
cfg = config.modules.network.vlans; # NOLINT(let_order)
|
||||||
|
|
||||||
inherit (util) bridgeName;
|
inherit (util) bridgeName;
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.modules.network.vpn-client;
|
cfg = config.modules.network.vpn-client; # NOLINT(let_order)
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.network.vpn-client = {
|
options.modules.network.vpn-client = {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.modules.services.actualbudget;
|
cfg = config.modules.services.actualbudget; # NOLINT(let_order)
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.services.actualbudget = {
|
options.modules.services.actualbudget = {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.modules.services.copyparty;
|
cfg = config.modules.services.copyparty; # NOLINT(let_order)
|
||||||
|
|
||||||
volumeType = lib.types.submodule {
|
volumeType = lib.types.submodule {
|
||||||
options = {
|
options = {
|
||||||
@@ -38,6 +38,31 @@ let
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
configScript = pkgs.writeShellScript "copyparty-config" ''
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
umask 077
|
||||||
|
password="$(${pkgs.coreutils}/bin/tr -d '\n' < "$CREDENTIALS_DIRECTORY/password")"
|
||||||
|
|
||||||
|
cat > /run/copyparty/copyparty.conf <<EOF
|
||||||
|
[global]
|
||||||
|
p: ${toString cfg.port}
|
||||||
|
usernames
|
||||||
|
http-only
|
||||||
|
no-crt
|
||||||
|
no-thumb
|
||||||
|
no-snap
|
||||||
|
no-rescan
|
||||||
|
ses-db: ${cfg.stateDir}/sessions.db
|
||||||
|
chpw-db: ${cfg.stateDir}/chpw.json
|
||||||
|
shr-db: ${cfg.stateDir}/shares.db
|
||||||
|
${renderedReverseProxy}
|
||||||
|
[accounts]
|
||||||
|
${cfg.accountName}: $password
|
||||||
|
|
||||||
|
${renderedVolumes}
|
||||||
|
EOF
|
||||||
|
'';
|
||||||
renderAccess = permission: accounts: " ${permission}: ${lib.concatStringsSep ", " accounts}";
|
renderAccess = permission: accounts: " ${permission}: ${lib.concatStringsSep ", " accounts}";
|
||||||
|
|
||||||
hasFlag =
|
hasFlag =
|
||||||
@@ -72,31 +97,6 @@ let
|
|||||||
|
|
||||||
renderedVolumes = lib.concatStringsSep "\n" (lib.mapAttrsToList renderVolume cfg.volumes);
|
renderedVolumes = lib.concatStringsSep "\n" (lib.mapAttrsToList renderVolume cfg.volumes);
|
||||||
|
|
||||||
configScript = pkgs.writeShellScript "copyparty-config" ''
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
umask 077
|
|
||||||
password="$(${pkgs.coreutils}/bin/tr -d '\n' < "$CREDENTIALS_DIRECTORY/password")"
|
|
||||||
|
|
||||||
cat > /run/copyparty/copyparty.conf <<EOF
|
|
||||||
[global]
|
|
||||||
p: ${toString cfg.port}
|
|
||||||
usernames
|
|
||||||
http-only
|
|
||||||
no-crt
|
|
||||||
no-thumb
|
|
||||||
no-snap
|
|
||||||
no-rescan
|
|
||||||
ses-db: ${cfg.stateDir}/sessions.db
|
|
||||||
chpw-db: ${cfg.stateDir}/chpw.json
|
|
||||||
shr-db: ${cfg.stateDir}/shares.db
|
|
||||||
${renderedReverseProxy}
|
|
||||||
[accounts]
|
|
||||||
${cfg.accountName}: $password
|
|
||||||
|
|
||||||
${renderedVolumes}
|
|
||||||
EOF
|
|
||||||
'';
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.services.copyparty = {
|
options.modules.services.copyparty = {
|
||||||
|
|||||||
@@ -5,11 +5,44 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.modules.services.homepage;
|
cfg = config.modules.services.homepage; # NOLINT(let_order)
|
||||||
|
|
||||||
mkRegistryPath =
|
derivedAllowedHosts = lib.unique (
|
||||||
registryName: host: entry:
|
lib.optional (selfRoute != null) selfRoute.domain
|
||||||
"registry.${registryName}.${host}.${entry}";
|
++ lib.optionals (selfAddress != null) [
|
||||||
|
selfAddress
|
||||||
|
"${selfAddress}:${toString cfg.listenPort}"
|
||||||
|
]
|
||||||
|
);
|
||||||
|
selfRoute = selfEntry.route or null;
|
||||||
|
|
||||||
|
selfEntry = registry.join cfg.hostName cfg.entryName;
|
||||||
|
selfAddress = selfEntry.address or null;
|
||||||
|
homepageServices = map mkTab (lib.filter (tab: lib.hasAttr tab entriesByTab) orderedTabs);
|
||||||
|
|
||||||
|
mkTab =
|
||||||
|
tab:
|
||||||
|
let
|
||||||
|
sections = entriesByTab.${tab};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
${tab} = map (section: { ${section} = sections.${section}; }) (orderedSections tab sections);
|
||||||
|
};
|
||||||
|
|
||||||
|
entriesByTab = lib.foldl' insertEntry { } sortedEntries;
|
||||||
|
|
||||||
|
insertEntry =
|
||||||
|
acc: item:
|
||||||
|
acc
|
||||||
|
// {
|
||||||
|
${item.homepage.tab} = (acc.${item.homepage.tab} or { }) // {
|
||||||
|
${item.homepage.section} = (acc.${item.homepage.tab}.${item.homepage.section} or [ ]) ++ [
|
||||||
|
{ ${item.homepage.name} = mkServiceAttrs item; }
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
sortedEntries = lib.sort (left: right: left.homepage.name < right.homepage.name) homepageEntries;
|
||||||
|
|
||||||
homepageEntries = lib.concatLists (
|
homepageEntries = lib.concatLists (
|
||||||
lib.mapAttrsToList (
|
lib.mapAttrsToList (
|
||||||
@@ -21,6 +54,18 @@ let
|
|||||||
) registry.homepage
|
) registry.homepage
|
||||||
);
|
);
|
||||||
|
|
||||||
|
orderedTabs = tabOrder ++ lib.filter (tab: !lib.elem tab tabOrder) (lib.attrNames entriesByTab);
|
||||||
|
|
||||||
|
tabOrder = [
|
||||||
|
"Apps"
|
||||||
|
"Download"
|
||||||
|
"System"
|
||||||
|
];
|
||||||
|
|
||||||
|
mkRegistryPath =
|
||||||
|
registryName: host: entry:
|
||||||
|
"registry.${registryName}.${host}.${entry}";
|
||||||
|
|
||||||
mkHref =
|
mkHref =
|
||||||
item:
|
item:
|
||||||
if item.homepage.href != null then
|
if item.homepage.href != null then
|
||||||
@@ -42,29 +87,6 @@ let
|
|||||||
inherit (item.homepage) description icon;
|
inherit (item.homepage) description icon;
|
||||||
};
|
};
|
||||||
|
|
||||||
sortedEntries = lib.sort (left: right: left.homepage.name < right.homepage.name) homepageEntries;
|
|
||||||
|
|
||||||
insertEntry =
|
|
||||||
acc: item:
|
|
||||||
acc
|
|
||||||
// {
|
|
||||||
${item.homepage.tab} = (acc.${item.homepage.tab} or { }) // {
|
|
||||||
${item.homepage.section} = (acc.${item.homepage.tab}.${item.homepage.section} or [ ]) ++ [
|
|
||||||
{ ${item.homepage.name} = mkServiceAttrs item; }
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
entriesByTab = lib.foldl' insertEntry { } sortedEntries;
|
|
||||||
|
|
||||||
tabOrder = [
|
|
||||||
"Apps"
|
|
||||||
"Download"
|
|
||||||
"System"
|
|
||||||
];
|
|
||||||
|
|
||||||
orderedTabs = tabOrder ++ lib.filter (tab: !lib.elem tab tabOrder) (lib.attrNames entriesByTab);
|
|
||||||
|
|
||||||
sectionOrderByTab = {
|
sectionOrderByTab = {
|
||||||
Apps = [
|
Apps = [
|
||||||
"Media"
|
"Media"
|
||||||
@@ -91,28 +113,6 @@ let
|
|||||||
in
|
in
|
||||||
sectionOrder ++ lib.filter (section: !lib.elem section sectionOrder) (lib.attrNames sections);
|
sectionOrder ++ lib.filter (section: !lib.elem section sectionOrder) (lib.attrNames sections);
|
||||||
|
|
||||||
mkTab =
|
|
||||||
tab:
|
|
||||||
let
|
|
||||||
sections = entriesByTab.${tab};
|
|
||||||
in
|
|
||||||
{
|
|
||||||
${tab} = map (section: { ${section} = sections.${section}; }) (orderedSections tab sections);
|
|
||||||
};
|
|
||||||
|
|
||||||
homepageServices = map mkTab (lib.filter (tab: lib.hasAttr tab entriesByTab) orderedTabs);
|
|
||||||
|
|
||||||
selfEntry = registry.join cfg.hostName cfg.entryName;
|
|
||||||
selfAddress = selfEntry.address or null;
|
|
||||||
selfRoute = selfEntry.route or null;
|
|
||||||
|
|
||||||
derivedAllowedHosts = lib.unique (
|
|
||||||
lib.optional (selfRoute != null) selfRoute.domain
|
|
||||||
++ lib.optionals (selfAddress != null) [
|
|
||||||
selfAddress
|
|
||||||
"${selfAddress}:${toString cfg.listenPort}"
|
|
||||||
]
|
|
||||||
);
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.services.homepage = {
|
options.modules.services.homepage = {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.modules.services.navidrome;
|
cfg = config.modules.services.navidrome; # NOLINT(let_order)
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.services.navidrome = {
|
options.modules.services.navidrome = {
|
||||||
|
|||||||
@@ -5,63 +5,45 @@
|
|||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
cfg = config.modules.services.reverse-proxy;
|
cfg = config.modules.services.reverse-proxy; # NOLINT(let_order)
|
||||||
|
|
||||||
mkRegistryPath =
|
generatedUdpStreams = lib.concatLists (
|
||||||
registryName: host: entry:
|
lib.mapAttrsToList (
|
||||||
"registry.${registryName}.${host}.${entry}";
|
host: routes:
|
||||||
|
lib.concatLists (
|
||||||
|
lib.mapAttrsToList (entry: route: map (mkUdpStream host entry) route.udp.ports) routes
|
||||||
|
)
|
||||||
|
) registry.route
|
||||||
|
);
|
||||||
|
|
||||||
mkBackend = address: port: "http://${address}:${toString port}";
|
mkUdpStream =
|
||||||
|
host: entry: port:
|
||||||
mkVirtualHost =
|
|
||||||
host: entry: mode:
|
|
||||||
let
|
let
|
||||||
compositeEntry = registry.join host entry;
|
|
||||||
inherit (compositeEntry) route;
|
|
||||||
address =
|
address =
|
||||||
compositeEntry.address
|
compositeEntry.address
|
||||||
or (throw "no matching entry in registry.address for ${mkRegistryPath "route" host entry}");
|
or (throw "no matching entry in registry.address for ${mkRegistryPath "route" host entry}");
|
||||||
isHttp = mode == "http";
|
compositeEntry = registry.join host entry;
|
||||||
port = if isHttp then route.http.port else route.port;
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
inherit host entry;
|
inherit
|
||||||
registryName = "route";
|
host
|
||||||
name = route.domain;
|
entry
|
||||||
value = {
|
port
|
||||||
forceSSL = !isHttp;
|
address
|
||||||
enableACME = !isHttp;
|
;
|
||||||
locations."/".proxyPass = mkBackend address port;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
generatedHttpsVirtualHostList = lib.concatLists (
|
generatedStreamConfig = lib.concatMapStringsSep "\n" (stream: ''
|
||||||
lib.mapAttrsToList (
|
server {
|
||||||
host: routes:
|
listen ${toString stream.port} udp;
|
||||||
lib.mapAttrsToList (entry: _route: mkVirtualHost host entry "https") (
|
proxy_pass ${stream.address}:${toString stream.port};
|
||||||
lib.filterAttrs (_entry: route: route.port != null) routes
|
|
||||||
)
|
|
||||||
) registry.route
|
|
||||||
);
|
|
||||||
|
|
||||||
generatedHttpVirtualHostList = lib.concatLists (
|
|
||||||
lib.mapAttrsToList (
|
|
||||||
host: routes:
|
|
||||||
lib.mapAttrsToList (entry: _route: mkVirtualHost host entry "http") (
|
|
||||||
lib.filterAttrs (_entry: route: route.http.port != null) routes
|
|
||||||
)
|
|
||||||
) registry.route
|
|
||||||
);
|
|
||||||
|
|
||||||
generatedVirtualHostList = generatedHttpsVirtualHostList ++ generatedHttpVirtualHostList;
|
|
||||||
|
|
||||||
virtualHostsByDomain = lib.foldl' (
|
|
||||||
acc: virtualHost:
|
|
||||||
acc
|
|
||||||
// {
|
|
||||||
${virtualHost.name} = (acc.${virtualHost.name} or [ ]) ++ [ virtualHost ];
|
|
||||||
}
|
}
|
||||||
) { } generatedVirtualHostList;
|
'') generatedUdpStreams;
|
||||||
|
generatedVirtualHosts =
|
||||||
|
if duplicateDomainMessages != [ ] then
|
||||||
|
throw (builtins.head duplicateDomainMessages)
|
||||||
|
else
|
||||||
|
lib.listToAttrs generatedVirtualHostList;
|
||||||
|
|
||||||
duplicateDomainMessages = lib.concatLists (
|
duplicateDomainMessages = lib.concatLists (
|
||||||
lib.mapAttrsToList (
|
lib.mapAttrsToList (
|
||||||
@@ -79,44 +61,62 @@ let
|
|||||||
) virtualHostsByDomain
|
) virtualHostsByDomain
|
||||||
);
|
);
|
||||||
|
|
||||||
generatedVirtualHosts =
|
virtualHostsByDomain = lib.foldl' (
|
||||||
if duplicateDomainMessages != [ ] then
|
acc: virtualHost:
|
||||||
throw (builtins.head duplicateDomainMessages)
|
acc
|
||||||
else
|
// {
|
||||||
lib.listToAttrs generatedVirtualHostList;
|
${virtualHost.name} = (acc.${virtualHost.name} or [ ]) ++ [ virtualHost ];
|
||||||
|
}
|
||||||
|
) { } generatedVirtualHostList;
|
||||||
|
|
||||||
mkUdpStream =
|
generatedVirtualHostList = generatedHttpsVirtualHostList ++ generatedHttpVirtualHostList;
|
||||||
host: entry: port:
|
|
||||||
let
|
|
||||||
compositeEntry = registry.join host entry;
|
|
||||||
address =
|
|
||||||
compositeEntry.address
|
|
||||||
or (throw "no matching entry in registry.address for ${mkRegistryPath "route" host entry}");
|
|
||||||
in
|
|
||||||
{
|
|
||||||
inherit
|
|
||||||
host
|
|
||||||
entry
|
|
||||||
port
|
|
||||||
address
|
|
||||||
;
|
|
||||||
};
|
|
||||||
|
|
||||||
generatedUdpStreams = lib.concatLists (
|
generatedHttpsVirtualHostList = lib.concatLists (
|
||||||
lib.mapAttrsToList (
|
lib.mapAttrsToList (
|
||||||
host: routes:
|
host: routes:
|
||||||
lib.concatLists (
|
lib.mapAttrsToList (entry: _route: mkVirtualHost host entry "https") (
|
||||||
lib.mapAttrsToList (entry: route: map (mkUdpStream host entry) route.udp.ports) routes
|
lib.filterAttrs (_entry: route: route.port != null) routes
|
||||||
)
|
)
|
||||||
) registry.route
|
) registry.route
|
||||||
);
|
);
|
||||||
|
|
||||||
generatedStreamConfig = lib.concatMapStringsSep "\n" (stream: ''
|
mkVirtualHost =
|
||||||
server {
|
host: entry: mode:
|
||||||
listen ${toString stream.port} udp;
|
let
|
||||||
proxy_pass ${stream.address}:${toString stream.port};
|
isHttp = mode == "http";
|
||||||
}
|
address =
|
||||||
'') generatedUdpStreams;
|
compositeEntry.address
|
||||||
|
or (throw "no matching entry in registry.address for ${mkRegistryPath "route" host entry}");
|
||||||
|
compositeEntry = registry.join host entry;
|
||||||
|
inherit (compositeEntry) route;
|
||||||
|
port = if isHttp then route.http.port else route.port;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
inherit host entry;
|
||||||
|
registryName = "route";
|
||||||
|
name = route.domain;
|
||||||
|
value = {
|
||||||
|
forceSSL = !isHttp;
|
||||||
|
enableACME = !isHttp;
|
||||||
|
locations."/".proxyPass = mkBackend address port;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
mkBackend = address: port: "http://${address}:${toString port}";
|
||||||
|
|
||||||
|
generatedHttpVirtualHostList = lib.concatLists (
|
||||||
|
lib.mapAttrsToList (
|
||||||
|
host: routes:
|
||||||
|
lib.mapAttrsToList (entry: _route: mkVirtualHost host entry "http") (
|
||||||
|
lib.filterAttrs (_entry: route: route.http.port != null) routes
|
||||||
|
)
|
||||||
|
) registry.route
|
||||||
|
);
|
||||||
|
|
||||||
|
mkRegistryPath =
|
||||||
|
registryName: host: entry:
|
||||||
|
"registry.${registryName}.${host}.${entry}";
|
||||||
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.services.reverse-proxy = {
|
options.modules.services.reverse-proxy = {
|
||||||
|
|||||||
@@ -80,10 +80,10 @@
|
|||||||
|
|
||||||
(lib.mkIf config.modules.services.steamcmd.enable (
|
(lib.mkIf config.modules.services.steamcmd.enable (
|
||||||
let
|
let
|
||||||
service = config.modules.services.steamcmd;
|
|
||||||
user = "steamcmd";
|
|
||||||
group = "storage";
|
group = "storage";
|
||||||
|
|
||||||
|
service = config.modules.services.steamcmd;
|
||||||
|
user = "steamcmd";
|
||||||
prepareScript = pkgs.writeShellScript "steamcmd-${service.serviceName}-prepare" ''
|
prepareScript = pkgs.writeShellScript "steamcmd-${service.serviceName}-prepare" ''
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
}:
|
}:
|
||||||
# Bazel for the primary user's shell environment.
|
# Bazel for the primary user's shell environment.
|
||||||
let
|
let
|
||||||
cfg = config.modules.shell.bazel;
|
cfg = config.modules.shell.bazel; # NOLINT(let_order)
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
}:
|
}:
|
||||||
# direnv for the primary user, configured through home-manager.
|
# direnv for the primary user, configured through home-manager.
|
||||||
let
|
let
|
||||||
cfg = config.modules.shell.direnv;
|
cfg = config.modules.shell.direnv; # NOLINT(let_order)
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,14 +6,14 @@
|
|||||||
}:
|
}:
|
||||||
# fish for the primary user, configured through home-manager.
|
# fish for the primary user, configured through home-manager.
|
||||||
let
|
let
|
||||||
cfg = config.modules.shell.fish;
|
cfg = config.modules.shell.fish; # NOLINT(let_order)
|
||||||
user = config.user.name;
|
|
||||||
shortcuts = lib.attrByPath [ config.networking.hostName ] { } config.registry.shell-shortcut;
|
|
||||||
envVars = lib.attrByPath [ config.networking.hostName ] { } config.registry.env-var;
|
|
||||||
commandsOf = shortcuts: lib.mapAttrs (_name: shortcut: shortcut.command) shortcuts;
|
commandsOf = shortcuts: lib.mapAttrs (_name: shortcut: shortcut.command) shortcuts;
|
||||||
|
shortcuts = lib.attrByPath [ config.networking.hostName ] { } config.registry.shell-shortcut;
|
||||||
exportEnvVar = name: value: ''
|
exportEnvVar = name: value: ''
|
||||||
set -gx ${name} ${lib.escapeShellArg value}
|
set -gx ${name} ${lib.escapeShellArg value}
|
||||||
'';
|
'';
|
||||||
|
envVars = lib.attrByPath [ config.networking.hostName ] { } config.registry.env-var;
|
||||||
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.shell.fish = {
|
options.modules.shell.fish = {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
}:
|
}:
|
||||||
# git for the primary user, configured through home-manager.
|
# git for the primary user, configured through home-manager.
|
||||||
let
|
let
|
||||||
cfg = config.modules.shell.git;
|
cfg = config.modules.shell.git; # NOLINT(let_order)
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
}:
|
}:
|
||||||
# gitea-axi for the primary user, installed through its own home-manager module.
|
# gitea-axi for the primary user, installed through its own home-manager module.
|
||||||
let
|
let
|
||||||
cfg = config.modules.shell.gitea-axi;
|
cfg = config.modules.shell.gitea-axi; # NOLINT(let_order)
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
}:
|
}:
|
||||||
# Herdr, a terminal multiplexer for coding agents.
|
# Herdr, a terminal multiplexer for coding agents.
|
||||||
let
|
let
|
||||||
cfg = config.modules.shell.herdr;
|
cfg = config.modules.shell.herdr; # NOLINT(let_order)
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
}:
|
}:
|
||||||
# Neovim for the primary user, configured declaratively through nixvim.
|
# Neovim for the primary user, configured declaratively through nixvim.
|
||||||
let
|
let
|
||||||
cfg = config.modules.shell.nvim;
|
cfg = config.modules.shell.nvim; # NOLINT(let_order)
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
}:
|
}:
|
||||||
# Oh My Pi, installed alongside upstream Pi as the `omp` command.
|
# Oh My Pi, installed alongside upstream Pi as the `omp` command.
|
||||||
let
|
let
|
||||||
cfg = config.modules.shell.omp;
|
cfg = config.modules.shell.omp; # NOLINT(let_order)
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,13 +8,13 @@
|
|||||||
# home-manager, which ships the package and manages global settings.
|
# home-manager, which ships the package and manages global settings.
|
||||||
# The login credential is left unmanaged, so it survives rebuilds.
|
# The login credential is left unmanaged, so it survives rebuilds.
|
||||||
let
|
let
|
||||||
cfg = config.modules.shell.pi;
|
cfg = config.modules.shell.pi; # NOLINT(let_order)
|
||||||
user = config.user.name;
|
|
||||||
piDir = "${config.users.users.${user}.home}/.pi/agent";
|
|
||||||
vendoredPi = pkgs.pi-coding-agent.overrideAttrs {
|
vendoredPi = pkgs.pi-coding-agent.overrideAttrs {
|
||||||
src = ../../../../../third_party/pi;
|
src = ../../../../../third_party/pi;
|
||||||
patches = [ ];
|
patches = [ ];
|
||||||
};
|
};
|
||||||
|
user = config.user.name;
|
||||||
|
piDir = "${config.users.users.${user}.home}/.pi/agent";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.shell.pi = {
|
options.modules.shell.pi = {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
}:
|
}:
|
||||||
# Python tools for the primary user's shell environment.
|
# Python tools for the primary user's shell environment.
|
||||||
let
|
let
|
||||||
cfg = config.modules.shell.python;
|
cfg = config.modules.shell.python; # NOLINT(let_order)
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,8 +6,7 @@
|
|||||||
}:
|
}:
|
||||||
# Rust tools for the primary user's shell environment.
|
# Rust tools for the primary user's shell environment.
|
||||||
let
|
let
|
||||||
cfg = config.modules.shell.rust;
|
cfg = config.modules.shell.rust; # NOLINT(let_order)
|
||||||
user = config.user.name;
|
|
||||||
toolchain = pkgs.symlinkJoin {
|
toolchain = pkgs.symlinkJoin {
|
||||||
name = "rust-toolchain";
|
name = "rust-toolchain";
|
||||||
paths = with pkgs; [
|
paths = with pkgs; [
|
||||||
@@ -18,6 +17,7 @@ let
|
|||||||
rustfmt
|
rustfmt
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.shell.rust.enable =
|
options.modules.shell.rust.enable =
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
# tmux for the primary user, configured through home-manager.
|
# tmux for the primary user, configured through home-manager.
|
||||||
# Settings without a home-manager option are read from ./extra.conf.
|
# Settings without a home-manager option are read from ./extra.conf.
|
||||||
let
|
let
|
||||||
cfg = config.modules.shell.tmux;
|
cfg = config.modules.shell.tmux; # NOLINT(let_order)
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
}:
|
}:
|
||||||
# Yazi, a terminal file manager.
|
# Yazi, a terminal file manager.
|
||||||
let
|
let
|
||||||
cfg = config.modules.shell.yazi;
|
cfg = config.modules.shell.yazi; # NOLINT(let_order)
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
}:
|
}:
|
||||||
# The host-level ZFS pool import: durable service state a host mounts, never rebuilds.
|
# The host-level ZFS pool import: durable service state a host mounts, never rebuilds.
|
||||||
let
|
let
|
||||||
cfg = config.modules.zfs;
|
cfg = config.modules.zfs; # NOLINT(let_order)
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.modules.zfs = {
|
options.modules.zfs = {
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
{ lib, ... }:
|
{ lib, ... }:
|
||||||
let
|
let
|
||||||
inherit (lib)
|
duplicateUids =
|
||||||
filterAttrs
|
entries:
|
||||||
groupBy
|
filterAttrs (_uid: serviceNames: builtins.length serviceNames > 1) (serviceNamesByUid entries);
|
||||||
mapAttrs
|
|
||||||
unique
|
|
||||||
;
|
|
||||||
|
|
||||||
serviceNamesByUid =
|
serviceNamesByUid =
|
||||||
entries:
|
entries:
|
||||||
@@ -13,10 +10,6 @@ let
|
|||||||
groupBy (entry: toString entry.value) entries
|
groupBy (entry: toString entry.value) entries
|
||||||
);
|
);
|
||||||
|
|
||||||
duplicateUids =
|
|
||||||
entries:
|
|
||||||
filterAttrs (_uid: serviceNames: builtins.length serviceNames > 1) (serviceNamesByUid entries);
|
|
||||||
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
# Next stable UID: 20005
|
# Next stable UID: 20005
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
util,
|
util,
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
|
files = util.collectNixFiles root;
|
||||||
mkGuest =
|
mkGuest =
|
||||||
name:
|
name:
|
||||||
{ config, lib, ... }:
|
{ config, lib, ... }:
|
||||||
@@ -167,22 +168,22 @@ let
|
|||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
assertions =
|
assertions =
|
||||||
let
|
let
|
||||||
hasDirectorySettings = mount: mount.owner != null || mount.group != null || mount.mode != null;
|
|
||||||
hasCompleteDirectorySettings =
|
|
||||||
mount: mount.owner != null && mount.group != null && mount.mode != null;
|
|
||||||
hasOneSource = mount: (mount.directory != null) != (mount.file != null);
|
|
||||||
mountsWithInvalidSource = lib.attrNames (
|
mountsWithInvalidSource = lib.attrNames (
|
||||||
lib.filterAttrs (_: mount: !hasOneSource mount) cfg.mounts
|
lib.filterAttrs (_: mount: !hasOneSource mount) cfg.mounts
|
||||||
);
|
);
|
||||||
|
hasOneSource = mount: (mount.directory != null) != (mount.file != null);
|
||||||
fileMountsWithHostSettings = lib.attrNames (
|
fileMountsWithHostSettings = lib.attrNames (
|
||||||
lib.filterAttrs (_: mount: mount.file != null && hasDirectorySettings mount) cfg.mounts
|
lib.filterAttrs (_: mount: mount.file != null && hasDirectorySettings mount) cfg.mounts
|
||||||
);
|
);
|
||||||
|
hasDirectorySettings = mount: mount.owner != null || mount.group != null || mount.mode != null;
|
||||||
directoryMountsWithPartialHostSettings = lib.attrNames (
|
directoryMountsWithPartialHostSettings = lib.attrNames (
|
||||||
lib.filterAttrs (
|
lib.filterAttrs (
|
||||||
_: mount:
|
_: mount:
|
||||||
mount.directory != null && hasDirectorySettings mount && !hasCompleteDirectorySettings mount
|
mount.directory != null && hasDirectorySettings mount && !hasCompleteDirectorySettings mount
|
||||||
) cfg.mounts
|
) cfg.mounts
|
||||||
);
|
);
|
||||||
|
hasCompleteDirectorySettings =
|
||||||
|
mount: mount.owner != null && mount.group != null && mount.mode != null;
|
||||||
in
|
in
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
@@ -275,11 +276,10 @@ let
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
files = util.collectNixFiles root;
|
names = map (file: lib.removeSuffix ".nix" file) (lib.attrNames guestFiles);
|
||||||
guestFiles = lib.filterAttrs (file: type: type == "regular" && lib.hasSuffix ".nix" file) (
|
guestFiles = lib.filterAttrs (file: type: type == "regular" && lib.hasSuffix ".nix" file) (
|
||||||
builtins.readDir root
|
builtins.readDir root
|
||||||
);
|
);
|
||||||
names = map (file: lib.removeSuffix ".nix" file) (lib.attrNames guestFiles);
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
imports = files ++ map mkGuest names;
|
imports = files ++ map mkGuest names;
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
registryModule,
|
registryModule,
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
|
names = lib.attrNames directories;
|
||||||
|
directories = lib.filterAttrs (_name: type: type == "directory") (builtins.readDir root);
|
||||||
mkHost =
|
mkHost =
|
||||||
{
|
{
|
||||||
root,
|
root,
|
||||||
@@ -36,7 +38,5 @@ let
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
directories = lib.filterAttrs (_name: type: type == "directory") (builtins.readDir root);
|
|
||||||
names = lib.attrNames directories;
|
|
||||||
in
|
in
|
||||||
lib.genAttrs names (name: mkHost { inherit root name; })
|
lib.genAttrs names (name: mkHost { inherit root name; })
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ pub enum Expr {
|
|||||||
Ident(Ident),
|
Ident(Ident),
|
||||||
Select(Select),
|
Select(Select),
|
||||||
Let {
|
Let {
|
||||||
|
range: SourceRange,
|
||||||
bindings: Vec<Binding>,
|
bindings: Vec<Binding>,
|
||||||
body: Box<Expr>,
|
body: Box<Expr>,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
mod ast;
|
mod ast;
|
||||||
mod file_store;
|
mod file_store;
|
||||||
mod parser;
|
mod parser;
|
||||||
@@ -23,7 +25,45 @@ impl Report {
|
|||||||
|
|
||||||
impl std::fmt::Display for Report {
|
impl std::fmt::Display for Report {
|
||||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let mut reported_let_order_files = HashSet::new();
|
||||||
|
|
||||||
for diagnostic in &self.diagnostics {
|
for diagnostic in &self.diagnostics {
|
||||||
|
if diagnostic.finding.rule_id == "let_order" {
|
||||||
|
if !reported_let_order_files.insert(diagnostic.file_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let file = self.files.get(diagnostic.file_id);
|
||||||
|
writeln!(
|
||||||
|
formatter,
|
||||||
|
"{} [let_order] Let bindings must follow dependency order",
|
||||||
|
file.path.display()
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mut findings = self
|
||||||
|
.diagnostics
|
||||||
|
.iter()
|
||||||
|
.filter(|candidate| {
|
||||||
|
candidate.file_id == diagnostic.file_id
|
||||||
|
&& candidate.finding.rule_id == "let_order"
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
findings.sort_by_key(|finding| finding.finding.range.start);
|
||||||
|
|
||||||
|
for finding in findings {
|
||||||
|
let (start, _) = line_range(&file.source, finding.finding.range);
|
||||||
|
let expected_order =
|
||||||
|
finding.finding.expected_order.as_ref().expect("let order");
|
||||||
|
writeln!(
|
||||||
|
formatter,
|
||||||
|
" let at line {start}: {}",
|
||||||
|
expected_order.join(", ")
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let file = self.files.get(diagnostic.file_id);
|
let file = self.files.get(diagnostic.file_id);
|
||||||
let (start, end) = line_range(&file.source, diagnostic.finding.range);
|
let (start, end) = line_range(&file.source, diagnostic.finding.range);
|
||||||
let location = if start == end {
|
let location = if start == end {
|
||||||
@@ -71,7 +111,7 @@ pub fn future_sight(files: Vec<SourceFile>) -> status::Result<Report> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let graph = ScopeGraph::from_file(&parsed);
|
let graph = ScopeGraph::from_file(&parsed);
|
||||||
diagnostics.extend(rules::run_rules(&parsed, &graph));
|
diagnostics.extend(rules::run_rules(&parsed, &graph, &file.source));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Report {
|
Ok(Report {
|
||||||
|
|||||||
@@ -50,7 +50,30 @@ fn natu_reports_out_of_order_let_bindings() -> googletest::Result<()> {
|
|||||||
.iter()
|
.iter()
|
||||||
.filter(|diagnostic| diagnostic.finding.rule_id == "let_order")
|
.filter(|diagnostic| diagnostic.finding.rule_id == "let_order")
|
||||||
.count();
|
.count();
|
||||||
expect_that!(let_order_count, eq(4));
|
expect_that!(let_order_count, eq(2));
|
||||||
|
expect_that!(
|
||||||
|
report.to_string(),
|
||||||
|
eq(concat!(
|
||||||
|
"testdata/lint_findings.nix:11 [manual_inherit] Use `inherit` for this assignment\n",
|
||||||
|
"testdata/lint_findings.nix:12 [manual_inherit_from] Use `inherit` for this selected attribute\n",
|
||||||
|
"testdata/lint_findings.nix:13 [unquoted_uri] URI literals should be quoted\n",
|
||||||
|
"testdata/lint_findings.nix [let_order] Let bindings must follow dependency order\n",
|
||||||
|
" let at line 1: first, second, nested\n",
|
||||||
|
" let at line 4: nestedSecond, nestedFirst\n",
|
||||||
|
))
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gtest]
|
||||||
|
fn natu_preserves_suppressed_bindings() -> googletest::Result<()> {
|
||||||
|
let report = natu::future_sight(vec![SourceFile {
|
||||||
|
path: PathBuf::from("testdata/nolint.nix"),
|
||||||
|
source: "{ config, ... }: let\n cfg = config.example; # NOLINT(let_order)\n value = cfg;\nin value\n".into(),
|
||||||
|
}])
|
||||||
|
.or_fail()?;
|
||||||
|
|
||||||
|
expect_that!(report.is_clean(), is_true());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ fn convert_expr(expr: rnix::ast::Expr) -> ast::Expr {
|
|||||||
match expr {
|
match expr {
|
||||||
rnix::ast::Expr::Ident(ident) => ast::Expr::Ident(ident.into()),
|
rnix::ast::Expr::Ident(ident) => ast::Expr::Ident(ident.into()),
|
||||||
rnix::ast::Expr::LetIn(let_in) => ast::Expr::Let {
|
rnix::ast::Expr::LetIn(let_in) => ast::Expr::Let {
|
||||||
|
range: let_in.syntax().text_range().into(),
|
||||||
bindings: let_in
|
bindings: let_in
|
||||||
.attrpath_values()
|
.attrpath_values()
|
||||||
.filter_map(convert_binding_ast)
|
.filter_map(convert_binding_ast)
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ pub struct Finding {
|
|||||||
pub range: ast::SourceRange,
|
pub range: ast::SourceRange,
|
||||||
pub message: &'static str,
|
pub message: &'static str,
|
||||||
pub binding: Option<String>,
|
pub binding: Option<String>,
|
||||||
|
pub expected_order: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for Diagnostic {
|
impl std::fmt::Display for Diagnostic {
|
||||||
@@ -58,7 +59,7 @@ pub trait TokenRule {
|
|||||||
fn check(token: &Self::Token) -> Option<&'static str>;
|
fn check(token: &Self::Token) -> Option<&'static str>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run_rules(file: &ast::ParsedFile, graph: &ScopeGraph) -> Vec<Diagnostic> {
|
pub fn run_rules(file: &ast::ParsedFile, graph: &ScopeGraph, source: &str) -> Vec<Diagnostic> {
|
||||||
let node_diagnostics = file
|
let node_diagnostics = file
|
||||||
.nodes
|
.nodes
|
||||||
.iter()
|
.iter()
|
||||||
@@ -75,16 +76,21 @@ pub fn run_rules(file: &ast::ParsedFile, graph: &ScopeGraph) -> Vec<Diagnostic>
|
|||||||
.flat_map(|token| [run_token_rule::<UnquotedUri>(token)])
|
.flat_map(|token| [run_token_rule::<UnquotedUri>(token)])
|
||||||
.flatten();
|
.flatten();
|
||||||
|
|
||||||
let let_order_diagnostics = let_order::check(graph).into_iter().map(|finding| Finding {
|
let let_order_diagnostics =
|
||||||
rule_id: "let_order",
|
let_order::check(graph, source)
|
||||||
range: finding.range,
|
.into_iter()
|
||||||
message: "Let bindings must follow dependency order",
|
.map(|finding| Finding {
|
||||||
binding: Some(finding.binding),
|
rule_id: "let_order",
|
||||||
});
|
range: finding.range,
|
||||||
|
message: "Let bindings must follow dependency order",
|
||||||
|
binding: None,
|
||||||
|
expected_order: Some(finding.expected_order),
|
||||||
|
});
|
||||||
|
|
||||||
node_diagnostics
|
node_diagnostics
|
||||||
.chain(token_diagnostics)
|
.chain(token_diagnostics)
|
||||||
.chain(let_order_diagnostics)
|
.chain(let_order_diagnostics)
|
||||||
|
.filter(|finding| !is_suppressed(source, finding.range, finding.rule_id))
|
||||||
.map(|finding| Diagnostic {
|
.map(|finding| Diagnostic {
|
||||||
file_id: file.file_id,
|
file_id: file.file_id,
|
||||||
finding,
|
finding,
|
||||||
@@ -92,6 +98,24 @@ pub fn run_rules(file: &ast::ParsedFile, graph: &ScopeGraph) -> Vec<Diagnostic>
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_suppressed(source: &str, range: ast::SourceRange, rule_id: &str) -> bool {
|
||||||
|
let line = source[range.start..].lines().next().unwrap_or_default();
|
||||||
|
let Some(comment) = line.split_once('#').map(|(_, comment)| comment.trim()) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if comment == "NOLINT" {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
comment
|
||||||
|
.strip_prefix("NOLINT(")
|
||||||
|
.and_then(|rules| rules.strip_suffix(')'))
|
||||||
|
.is_some_and(|rules| {
|
||||||
|
rules
|
||||||
|
.split(',')
|
||||||
|
.any(|candidate| candidate.trim() == rule_id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn run_node_rule<R>(node: &ast::Node) -> Option<Finding>
|
fn run_node_rule<R>(node: &ast::Node) -> Option<Finding>
|
||||||
where
|
where
|
||||||
R: NodeRule,
|
R: NodeRule,
|
||||||
@@ -104,6 +128,7 @@ where
|
|||||||
range: node.range,
|
range: node.range,
|
||||||
message,
|
message,
|
||||||
binding: None,
|
binding: None,
|
||||||
|
expected_order: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,5 +144,6 @@ where
|
|||||||
range: token.range,
|
range: token.range,
|
||||||
message,
|
message,
|
||||||
binding: None,
|
binding: None,
|
||||||
|
expected_order: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,43 +1,59 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
ast,
|
ast,
|
||||||
|
rules::is_suppressed,
|
||||||
scope_graph::{LetScope, NodeId, ScopeGraph},
|
scope_graph::{LetScope, NodeId, ScopeGraph},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Finding {
|
pub struct Finding {
|
||||||
pub range: ast::SourceRange,
|
pub range: ast::SourceRange,
|
||||||
pub binding: String,
|
pub expected_order: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn check(graph: &ScopeGraph) -> Vec<Finding> {
|
pub fn check(graph: &ScopeGraph, source: &str) -> Vec<Finding> {
|
||||||
graph
|
graph
|
||||||
.let_scopes()
|
.let_scopes()
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|let_scope| {
|
.flat_map(|let_scope| {
|
||||||
let expected = expected_order(graph, let_scope);
|
let expected = expected_order(graph, let_scope)
|
||||||
let positions = expected
|
.into_iter()
|
||||||
.iter()
|
.filter(|declaration| !binding_suppressed(let_scope, *declaration, source))
|
||||||
.enumerate()
|
|
||||||
.map(|(position, declaration)| (*declaration, position))
|
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
let actual = let_scope
|
||||||
let_scope
|
|
||||||
.bindings
|
.bindings
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.filter(|binding| !is_suppressed(source, binding.range, "let_order"))
|
||||||
.filter_map(move |(position, binding)| {
|
.map(|binding| binding.declaration)
|
||||||
positions
|
.collect::<Vec<_>>();
|
||||||
.iter()
|
|
||||||
.find(|(declaration, _)| *declaration == binding.declaration)
|
(!actual.starts_with(&expected)).then(|| Finding {
|
||||||
.filter(|(_, expected_position)| position != *expected_position)
|
range: let_scope.range,
|
||||||
.map(|_| Finding {
|
expected_order: expected
|
||||||
range: binding.range,
|
.iter()
|
||||||
binding: binding.name.clone(),
|
.map(|declaration| binding_name(let_scope, *declaration).to_owned())
|
||||||
})
|
.collect(),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn binding_suppressed(let_scope: &LetScope, declaration: NodeId, source: &str) -> bool {
|
||||||
|
let binding = let_scope
|
||||||
|
.bindings
|
||||||
|
.iter()
|
||||||
|
.find(|binding| binding.declaration == declaration)
|
||||||
|
.expect("let declaration");
|
||||||
|
is_suppressed(source, binding.range, "let_order")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn binding_name(let_scope: &LetScope, declaration: NodeId) -> &str {
|
||||||
|
&let_scope
|
||||||
|
.bindings
|
||||||
|
.iter()
|
||||||
|
.find(|binding| binding.declaration == declaration)
|
||||||
|
.expect("let declaration")
|
||||||
|
.name
|
||||||
|
}
|
||||||
|
|
||||||
fn expected_order(graph: &ScopeGraph, let_scope: &LetScope) -> Vec<NodeId> {
|
fn expected_order(graph: &ScopeGraph, let_scope: &LetScope) -> Vec<NodeId> {
|
||||||
let mut order = Vec::new();
|
let mut order = Vec::new();
|
||||||
visit_references(graph, &let_scope.entry_references, let_scope, &mut order);
|
visit_references(graph, &let_scope.entry_references, let_scope, &mut order);
|
||||||
|
|||||||
@@ -26,7 +26,11 @@ impl ScopeGraph {
|
|||||||
fn build_expr(&mut self, expr: &ast::Expr, scope: NodeId) -> Vec<NodeId> {
|
fn build_expr(&mut self, expr: &ast::Expr, scope: NodeId) -> Vec<NodeId> {
|
||||||
match expr {
|
match expr {
|
||||||
ast::Expr::Ident(ident) => vec![self.add_reference(&ident.name, scope)],
|
ast::Expr::Ident(ident) => vec![self.add_reference(&ident.name, scope)],
|
||||||
ast::Expr::Let { bindings, body } => self.build_let(bindings, body, scope),
|
ast::Expr::Let {
|
||||||
|
range,
|
||||||
|
bindings,
|
||||||
|
body,
|
||||||
|
} => self.build_let(*range, bindings, body, scope),
|
||||||
ast::Expr::AttrSet(bindings) => bindings
|
ast::Expr::AttrSet(bindings) => bindings
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|binding| self.build_expr(&binding.value, scope))
|
.flat_map(|binding| self.build_expr(&binding.value, scope))
|
||||||
@@ -48,6 +52,7 @@ impl ScopeGraph {
|
|||||||
|
|
||||||
fn build_let(
|
fn build_let(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
range: ast::SourceRange,
|
||||||
bindings: &[ast::Binding],
|
bindings: &[ast::Binding],
|
||||||
body: &ast::Expr,
|
body: &ast::Expr,
|
||||||
parent: NodeId,
|
parent: NodeId,
|
||||||
@@ -74,6 +79,7 @@ impl ScopeGraph {
|
|||||||
.chain(entry_references.iter().copied())
|
.chain(entry_references.iter().copied())
|
||||||
.collect();
|
.collect();
|
||||||
self.lets.push(LetScope {
|
self.lets.push(LetScope {
|
||||||
|
range,
|
||||||
bindings: graph_bindings,
|
bindings: graph_bindings,
|
||||||
entry_references,
|
entry_references,
|
||||||
});
|
});
|
||||||
@@ -181,6 +187,7 @@ struct Edge {
|
|||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) struct LetScope {
|
pub(crate) struct LetScope {
|
||||||
|
pub(crate) range: ast::SourceRange,
|
||||||
pub(crate) bindings: Vec<Binding>,
|
pub(crate) bindings: Vec<Binding>,
|
||||||
pub(crate) entry_references: Vec<NodeId>,
|
pub(crate) entry_references: Vec<NodeId>,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user