Compare commits

..

1 Commits

Author SHA1 Message Date
e98ac9bb85 feat: prepare initial fortress host 2026-04-10 16:06:13 +02:00
51 changed files with 804 additions and 1810 deletions

View File

@@ -1,8 +1,6 @@
# nix fmt & statix # nix fmt & statix
f011c8d71ba09bd94ab04b8d771858b90a03fbf9 f011c8d71ba09bd94ab04b8d771858b90a03fbf9
3aff25b4486a143cd6282f8845c16216598e1c7e 3aff25b4486a143cd6282f8845c16216598e1c7e
2204b12fadf27886058e6945806ce93a547f5278
77236af5896524218605badcd3cdfc2267b213da
# host rename # host rename
cfe4c43887a41e52be4e6472474c0fc3788f86e8 cfe4c43887a41e52be4e6472474c0fc3788f86e8

View File

@@ -1,73 +0,0 @@
{
nixos =
{
config,
lib,
inputs,
...
}:
let
cfg = config.features.bootloader;
in
{
imports = [ inputs.lanzaboote.nixosModules.lanzaboote ];
options.features.bootloader = {
enable = lib.mkEnableOption "bootloader";
mode = lib.mkOption {
type = lib.types.enum [
"systemd-boot"
"lanzaboote"
];
default = "systemd-boot";
};
plymouth.enable = lib.mkEnableOption "plymouth boot splash";
};
config = lib.mkIf cfg.enable (
lib.mkMerge [
{
boot.loader.efi.canTouchEfiVariables = true;
# request the largest framebuffer uefi offers; plymouth inherits it
boot.loader.systemd-boot.consoleMode = "max";
}
(lib.mkIf (cfg.mode == "systemd-boot") {
boot.loader.systemd-boot.enable = true;
})
(lib.mkIf (cfg.mode == "lanzaboote") {
boot.loader.systemd-boot.enable = lib.mkForce false;
boot.lanzaboote = {
enable = true;
pkiBundle = "/var/lib/sbctl";
};
})
(lib.mkIf cfg.plymouth.enable {
# plymouth needs systemd-initrd to render the luks prompt cleanly
boot.initrd.systemd.enable = true;
# host is responsible for early-KMS so plymouth lands on the gpu driver,
# not simpledrm (e.g. hardware.amdgpu.initrd.enable on amd hosts)
boot.plymouth.enable = true;
stylix.targets.plymouth.logoAnimated = false;
boot.kernelParams = [
"quiet"
"splash"
"loglevel=3"
"rd.systemd.show_status=false"
"rd.udev.log_level=3"
"udev.log_priority=3"
"plymouth.force-scale=1"
];
boot.consoleLogLevel = 0;
boot.initrd.verbose = false;
})
]
);
};
}

22
features/calibre.nix Normal file
View File

@@ -0,0 +1,22 @@
{
nixos =
{ pkgs, ... }:
{
environment.systemPackages = [ pkgs.calibre ];
# udev rules for kindle and mtp device access
# NOTE:(@janezicmatej) uses services.udev.packages instead of extraRules
# because extraRules writes to 99-local.rules which is too late for uaccess
# see https://github.com/NixOS/nixpkgs/issues/308681
services.udev.packages = [
pkgs.libmtp
(pkgs.writeTextFile {
name = "kindle-udev-rules";
text = ''
ACTION!="remove", SUBSYSTEM=="usb", ATTRS{idVendor}=="1949", TAG+="uaccess"
'';
destination = "/etc/udev/rules.d/70-kindle.rules";
})
];
};
}

View File

@@ -1,28 +1,10 @@
{ {
nixos =
{ lib, ... }:
{
options.features.claude.enable = lib.mkEnableOption "claude";
};
home = home =
{ pkgs, ... }:
{ {
pkgs, home.packages = [
lib, pkgs.claude-code
inputs, pkgs.mcp-nixos
osConfig, ];
...
}:
let
cfg = osConfig.features.claude;
packages = inputs.self.outputs.packages.${pkgs.stdenv.hostPlatform.system};
in
{
config = lib.mkIf cfg.enable {
home.packages = [
packages.claude-code
pkgs.mcp-nixos
];
};
}; };
} }

View File

@@ -0,0 +1,45 @@
{
nixos =
{ pkgs, inputs, ... }:
{
imports = [ inputs.stylix.nixosModules.stylix ];
# audio
services.pipewire = {
enable = true;
pulse.enable = true;
};
security.polkit.enable = true;
services.dbus.enable = true;
services.playerctld.enable = true;
xdg.portal = {
enable = true;
xdgOpenUsePortal = true;
extraPortals = [
pkgs.xdg-desktop-portal-wlr
pkgs.xdg-desktop-portal-gtk
];
};
fonts.packages = with pkgs; [
font-awesome
nerd-fonts.jetbrains-mono
];
# theming
stylix = {
enable = true;
polarity = "dark";
image = "${inputs.assets}/wallpaper.png";
base16Scheme = "${pkgs.base16-schemes}/share/themes/gruvbox-material-dark-medium.yaml";
};
};
home =
{ inputs, ... }:
{
home.file.".assets".source = inputs.assets;
};
}

View File

@@ -1,273 +1,85 @@
{ {
nixos = nixos =
{ pkgs, inputs, ... }:
{ {
config, imports = [ inputs.stylix.nixosModules.stylix ];
lib,
pkgs,
inputs,
...
}:
let
cfg = config.features.desktop;
in
{
options.features.desktop = {
enable = lib.mkEnableOption "desktop environment";
audio.enable = lib.mkOption { # audio
type = lib.types.bool; services.pipewire = {
default = true; enable = true;
}; pulse.enable = true;
bluetooth.enable = lib.mkOption {
type = lib.types.bool;
default = false;
};
apps.enable = lib.mkOption {
type = lib.types.bool;
default = true;
};
theme = {
wallpaper = lib.mkOption {
type = lib.types.path;
default = "${inputs.assets}/wallpaper.png";
};
scheme = lib.mkOption {
type = lib.types.str;
default = "gruvbox-material-dark-medium";
};
polarity = lib.mkOption {
type = lib.types.enum [
"dark"
"light"
];
default = "dark";
};
};
internalCA.enable = lib.mkOption {
type = lib.types.bool;
default = true;
};
}; };
config = lib.mkIf cfg.enable ( # bluetooth
lib.mkMerge [ hardware.bluetooth.enable = true;
# base desktop services.blueman.enable = true;
{
security.polkit.enable = true;
services.dbus.enable = true;
services.playerctld.enable = true;
xdg.portal = { security.polkit.enable = true;
enable = true; services.dbus.enable = true;
extraPortals = with pkgs; [ services.playerctld.enable = true;
xdg-desktop-portal-wlr
xdg-desktop-portal-gtk
];
};
# honor persist_mode so electron apps don't re-prompt for screencast every login xdg.portal = {
systemd.user.services.xdg-desktop-portal-wlr.environment.XDPW_PERSIST_MODE = "permanent"; enable = true;
xdgOpenUsePortal = true;
extraPortals = [
pkgs.xdg-desktop-portal-wlr
pkgs.xdg-desktop-portal-gtk
];
};
# enable ozone/wayland for electron apps so idle detection works fonts.packages = with pkgs; [
environment.sessionVariables.NIXOS_OZONE_WL = "1"; font-awesome
nerd-fonts.jetbrains-mono
];
fonts.packages = with pkgs; [ # theming
font-awesome stylix = {
nerd-fonts.jetbrains-mono enable = true;
]; polarity = "dark";
image = "${inputs.assets}/wallpaper.png";
base16Scheme = "${pkgs.base16-schemes}/share/themes/gruvbox-material-dark-medium.yaml";
};
stylix = { programs.thunderbird.enable = true;
enable = true; programs._1password.enable = true;
inherit (cfg.theme) polarity; programs._1password-gui.enable = true;
image = cfg.theme.wallpaper;
base16Scheme = "${pkgs.base16-schemes}/share/themes/${cfg.theme.scheme}.yaml";
};
}
# audio environment.systemPackages = with pkgs; [
(lib.mkIf cfg.audio.enable { easyeffects
services.pipewire = { ghostty
enable = true; google-chrome
pulse.enable = true; zathura
}; pavucontrol
environment.systemPackages = with pkgs; [ bolt-launcher
pavucontrol libnotify
easyeffects bibata-cursors
]; vesktop
}) rocketchat-desktop
telegram-desktop
slack
jellyfin-media-player
cider-2
mpv
ffmpeg
wf-recorder
wl-mirror
protonmail-bridge
ledger-live-desktop
];
# bluetooth # internal CA
(lib.mkIf cfg.bluetooth.enable { security.pki.certificateFiles = [
hardware.bluetooth.enable = true; inputs.self.outputs.packages.${pkgs.stdenv.hostPlatform.system}.ca-matheo-si
services.blueman.enable = true; ];
})
# apps xdg.mime.defaultApplications = {
(lib.mkIf cfg.apps.enable { "application/pdf" = "org.pwmt.zathura.desktop";
programs.thunderbird.enable = true; };
environment.systemPackages = with pkgs; [
ghostty
google-chrome
zathura
calibre
bolt-launcher
libnotify
bibata-cursors
discord
rocketchat-desktop
telegram-desktop
slack
jellyfin-media-player
cider-2
mpv
ffmpeg
wf-recorder
wl-mirror
protonmail-bridge
ledger-live-desktop
imv
yazi
nemo
file-roller
libreoffice-still
];
# kindle udev rules for calibre
features.udev.kindle.enable = lib.mkDefault true;
})
# internal CA
(lib.mkIf cfg.internalCA.enable {
security.pki.certificateFiles = [
inputs.self.outputs.packages.${pkgs.stdenv.hostPlatform.system}.ca-matheo-si
];
})
]
);
}; };
home = home =
{ inputs, ... }:
{ {
lib, home.file.".assets".source = inputs.assets;
inputs,
osConfig,
...
}:
let
cfg = osConfig.features.desktop;
in
{
config = lib.mkIf cfg.enable (
lib.mkMerge [
{
home.file.".assets".source = inputs.assets;
}
(lib.mkIf cfg.apps.enable {
# TODO:(@janezicmatej) consider moving nvim desktop entry to neovim feature
xdg.desktopEntries.nvim = {
name = "Neovim";
exec = "ghostty -e nvim %F";
terminal = false;
mimeType = [
"text/plain"
"application/json"
"text/markdown"
];
};
xdg.mimeApps = {
enable = true;
defaultApplications = {
# text
"text/plain" = "nvim.desktop";
"application/json" = "nvim.desktop";
"text/markdown" = "nvim.desktop";
# web
"text/html" = "google-chrome.desktop";
"application/xhtml+xml" = "google-chrome.desktop";
"x-scheme-handler/http" = "google-chrome.desktop";
"x-scheme-handler/https" = "google-chrome.desktop";
"x-scheme-handler/ftp" = "google-chrome.desktop";
"x-scheme-handler/about" = "google-chrome.desktop";
"x-scheme-handler/unknown" = "google-chrome.desktop";
# mail and calendar
"x-scheme-handler/mailto" = "thunderbird.desktop";
"message/rfc822" = "thunderbird.desktop";
"text/calendar" = "thunderbird.desktop";
# documents
"application/pdf" = "org.pwmt.zathura.desktop";
"application/postscript" = "org.pwmt.zathura.desktop";
"image/vnd.djvu" = "org.pwmt.zathura.desktop";
"application/epub+zip" = "org.pwmt.zathura.desktop";
# office
"application/msword" = "libreoffice-writer.desktop";
"application/vnd.ms-excel" = "libreoffice-calc.desktop";
"application/vnd.ms-powerpoint" = "libreoffice-impress.desktop";
"application/vnd.openxmlformats-officedocument.wordprocessingml.document" =
"libreoffice-writer.desktop";
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" = "libreoffice-calc.desktop";
"application/vnd.openxmlformats-officedocument.presentationml.presentation" =
"libreoffice-impress.desktop";
"application/vnd.oasis.opendocument.text" = "libreoffice-writer.desktop";
"application/vnd.oasis.opendocument.spreadsheet" = "libreoffice-calc.desktop";
"application/vnd.oasis.opendocument.presentation" = "libreoffice-impress.desktop";
"text/csv" = "libreoffice-calc.desktop";
# images
"image/png" = "imv-dir.desktop";
"image/jpeg" = "imv-dir.desktop";
"image/gif" = "imv-dir.desktop";
"image/webp" = "imv-dir.desktop";
"image/tiff" = "imv-dir.desktop";
"image/bmp" = "imv-dir.desktop";
"image/svg+xml" = "google-chrome.desktop";
# video
"video/mp4" = "mpv.desktop";
"video/x-matroska" = "mpv.desktop";
"video/webm" = "mpv.desktop";
"video/quicktime" = "mpv.desktop";
"video/x-msvideo" = "mpv.desktop";
# audio
"audio/mpeg" = "mpv.desktop";
"audio/flac" = "mpv.desktop";
"audio/ogg" = "mpv.desktop";
"audio/wav" = "mpv.desktop";
"audio/aac" = "mpv.desktop";
# archives
"application/zip" = "org.gnome.FileRoller.desktop";
"application/x-tar" = "org.gnome.FileRoller.desktop";
"application/gzip" = "org.gnome.FileRoller.desktop";
"application/x-rar-compressed" = "org.gnome.FileRoller.desktop";
"application/x-7z-compressed" = "org.gnome.FileRoller.desktop";
"application/x-bzip2" = "org.gnome.FileRoller.desktop";
"application/x-xz" = "org.gnome.FileRoller.desktop";
# file manager
"inode/directory" = "nemo.desktop";
# app deep links
"x-scheme-handler/tg" = "org.telegram.desktop.desktop";
"x-scheme-handler/discord" = "discord.desktop";
"x-scheme-handler/slack" = "slack.desktop";
};
};
})
]
);
}; };
} }

View File

@@ -1,36 +1,25 @@
{ {
nixos =
{ lib, ... }:
{
options.features.dev.enable = lib.mkEnableOption "development tools";
};
home = home =
{ { pkgs, inputs, ... }:
pkgs,
lib,
inputs,
osConfig,
...
}:
let let
cfg = osConfig.features.dev;
packages = inputs.self.outputs.packages.${pkgs.stdenv.hostPlatform.system}; packages = inputs.self.outputs.packages.${pkgs.stdenv.hostPlatform.system};
in in
{ {
config = lib.mkIf cfg.enable { home.packages = [
home.packages = [ pkgs.git
pkgs.python3 packages.git-linearize
pkgs.osc packages.ggman
pkgs.google-cloud-sdk pkgs.python3
pkgs.google-cloud-sql-proxy pkgs.osc
pkgs.google-cloud-sdk
pkgs.google-cloud-sql-proxy
packages.ahab
pkgs.just
pkgs.presenterm
];
packages.ahab
pkgs.just
pkgs.presenterm
pkgs.qemu
];
};
}; };
} }

View File

@@ -1,45 +1,26 @@
{ {
nixos = nixos =
{ inputs, ... }:
{ {
config, nix.registry.dev = {
lib, from = {
inputs, type = "indirect";
... id = "dev";
}: };
let to = {
cfg = config.features.direnv; type = "path";
in path = inputs.self.outPath;
{
options.features.direnv.enable = lib.mkEnableOption "direnv";
config = lib.mkIf cfg.enable {
nix.registry.dev = {
from = {
type = "indirect";
id = "dev";
};
to = {
type = "path";
path = inputs.self.outPath;
};
}; };
}; };
}; };
home = home = _: {
{ lib, osConfig, ... }: programs.direnv = {
let enable = true;
cfg = osConfig.features.direnv; nix-direnv.enable = true;
in config.global.hide_env_diff = true;
{
config = lib.mkIf cfg.enable {
programs.direnv = {
enable = true;
nix-direnv.enable = true;
config.global.hide_env_diff = true;
};
xdg.configFile."direnv/lib/use_dev.sh".source = ./use_dev.sh;
};
}; };
xdg.configFile."direnv/lib/use_dev.sh".source = ./use_dev.sh;
};
} }

View File

@@ -1,24 +1,12 @@
{ {
nixos = nixos =
{ user, ... }:
{ {
config, virtualisation.docker = {
lib, enable = true;
user, logDriver = "json-file";
...
}:
let
cfg = config.features.docker;
in
{
options.features.docker.enable = lib.mkEnableOption "docker";
config = lib.mkIf cfg.enable {
virtualisation.docker = {
enable = true;
logDriver = "json-file";
};
users.users.${user}.extraGroups = [ "docker" ];
}; };
users.users.${user}.extraGroups = [ "docker" ];
}; };
} }

View File

@@ -1,61 +1,42 @@
{ {
nixos = nixos =
{ config, userKeys, ... }:
{ {
config, sops.secrets.filedrop-authorized-keys = {
lib, sopsFile = ../secrets/floo.yaml;
userKeys, mode = "0444";
...
}:
let
cfg = config.features.filedrop;
in
{
options.features.filedrop = {
enable = lib.mkEnableOption "filedrop sftp service";
sopsFile = lib.mkOption {
type = lib.types.path;
};
}; };
config = lib.mkIf cfg.enable { users.groups.filedrop = {
sops.secrets.filedrop-authorized-keys = { members = [ "matej" ];
inherit (cfg) sopsFile;
mode = "0444";
};
users.groups.filedrop = {
members = [ "matej" ];
};
users.users.filedrop = {
isSystemUser = true;
group = "filedrop";
home = "/home/filedrop";
shell = "/run/current-system/sw/bin/nologin";
openssh.authorizedKeys.keys = userKeys.sshAuthorizedKeys;
};
# chroot dir must be root-owned; incoming is writable by filedrop
systemd.tmpfiles.rules = [
"d /home/filedrop 0755 root root -"
"d /home/filedrop/incoming 2775 filedrop filedrop -"
"a+ /home/filedrop/incoming - - - - group:filedrop:rwx"
"a+ /home/filedrop/incoming - - - - default:group:filedrop:rwx"
"a+ /home/filedrop/incoming - - - - default:mask::rwx"
"L /home/matej/filedrop - - - - /home/filedrop/incoming"
];
# relaxed umask so default acl takes full effect
services.openssh.extraConfig = ''
Match User filedrop
ForceCommand internal-sftp -u 0002
ChrootDirectory /home/filedrop
AuthorizedKeysFile /etc/ssh/authorized_keys.d/filedrop %h/.ssh/authorized_keys ${config.sops.secrets.filedrop-authorized-keys.path}
AllowTcpForwarding no
X11Forwarding no
'';
}; };
users.users.filedrop = {
isSystemUser = true;
group = "filedrop";
home = "/home/filedrop";
shell = "/run/current-system/sw/bin/nologin";
openssh.authorizedKeys.keys = userKeys.sshAuthorizedKeys;
};
# chroot dir must be root-owned; incoming is writable by filedrop
systemd.tmpfiles.rules = [
"d /home/filedrop 0755 root root -"
"d /home/filedrop/incoming 2775 filedrop filedrop -"
"a+ /home/filedrop/incoming - - - - group:filedrop:rwx"
"a+ /home/filedrop/incoming - - - - default:group:filedrop:rwx"
"a+ /home/filedrop/incoming - - - - default:mask::rwx"
"L /home/matej/filedrop - - - - /home/filedrop/incoming"
];
# relaxed umask so default acl takes full effect
services.openssh.extraConfig = ''
Match User filedrop
ForceCommand internal-sftp -u 0002
ChrootDirectory /home/filedrop
AuthorizedKeysFile /etc/ssh/authorized_keys.d/filedrop %h/.ssh/authorized_keys ${config.sops.secrets.filedrop-authorized-keys.path}
AllowTcpForwarding no
X11Forwarding no
'';
}; };
} }

View File

@@ -1,26 +1,14 @@
{ {
nixos = nixos =
{ pkgs, ... }:
{ {
config, programs.steam = {
lib, enable = true;
pkgs, remotePlay.openFirewall = true;
... dedicatedServer.openFirewall = true;
}: localNetworkGameTransfers.openFirewall = true;
let
cfg = config.features.gaming;
in
{
options.features.gaming.enable = lib.mkEnableOption "gaming";
config = lib.mkIf cfg.enable {
programs.steam = {
enable = true;
remotePlay.openFirewall = true;
dedicatedServer.openFirewall = true;
localNetworkGameTransfers.openFirewall = true;
};
environment.systemPackages = [ pkgs.prismlauncher ];
}; };
environment.systemPackages = [ pkgs.prismlauncher ];
}; };
} }

View File

@@ -1,29 +0,0 @@
{
nixos =
{ lib, ... }:
{
options.features.git.enable = lib.mkEnableOption "git";
};
home =
{
pkgs,
lib,
inputs,
osConfig,
...
}:
let
cfg = osConfig.features.git;
packages = inputs.self.outputs.packages.${pkgs.stdenv.hostPlatform.system};
in
{
config = lib.mkIf cfg.enable {
home.packages = [
pkgs.git
packages.git-linearize
packages.ggman
];
};
};
}

View File

@@ -1,43 +1,9 @@
{ {
nixos = nixos = _: {
{ programs.gnupg.agent = {
config, enable = true;
lib, enableSSHSupport = true;
pkgs, enableExtraSocket = true;
...
}:
let
cfg = config.features.gnupg;
in
{
options.features.gnupg = {
enable = lib.mkEnableOption "gnupg";
yubikey.enable = lib.mkOption {
type = lib.types.bool;
default = false;
};
};
config = lib.mkIf cfg.enable (
lib.mkMerge [
{
programs.gnupg.agent = {
enable = true;
enableSSHSupport = true;
enableExtraSocket = true;
};
}
(lib.mkIf cfg.yubikey.enable {
environment.systemPackages = with pkgs; [
yubikey-personalization
yubikey-manager
];
services.pcscd.enable = true;
})
]
);
}; };
};
} }

28
features/greeter.nix Normal file
View File

@@ -0,0 +1,28 @@
{
nixos =
{ lib, inputs, ... }:
{
programs.regreet = {
enable = true;
# single output to avoid stretching across monitors
cageArgs = [
"-s"
"-m"
"last"
];
font = {
name = lib.mkForce "JetBrainsMono Nerd Font";
size = lib.mkForce 14;
};
settings = {
background = {
path = lib.mkForce "${inputs.assets}/wallpaper.png";
fit = lib.mkForce "Cover";
};
GTK = {
application_prefer_dark_theme = lib.mkForce true;
};
};
};
};
}

View File

@@ -1,14 +1,12 @@
{ {
nixos = nixos =
{ {
config,
lib,
pkgs, pkgs,
config,
inputs, inputs,
... ...
}: }:
let let
cfg = config.features.harmonia;
hosts = [ hosts = [
"fw16" "fw16"
"tower" "tower"
@@ -19,34 +17,35 @@
flakeRef = inputs.self.outPath; flakeRef = inputs.self.outPath;
in in
{ {
options.features.harmonia.enable = lib.mkEnableOption "harmonia"; services.harmonia.cache = {
enable = true;
signKeyPaths = [ config.sops.secrets.nix-signing-key.path ];
};
config = lib.mkIf cfg.enable { networking.firewall.interfaces."tailscale0".allowedTCPPorts = [ 5000 ];
services.harmonia.cache = {
enable = true; systemd.services.cache-builder = {
signKeyPaths = [ config.sops.secrets.nix-signing-key.path ]; description = "Build all host closures for binary cache";
serviceConfig = {
Type = "oneshot";
ExecStart = "${pkgs.bash}/bin/bash ${./cache-builder.sh}";
}; };
environment = {
networking.firewall.interfaces."tailscale0".allowedTCPPorts = [ 5000 ]; FLAKE_REF = flakeRef;
HOSTS = builtins.concatStringsSep " " hosts;
systemd.services.cache-builder = { GC_ROOT_DIR = "/nix/var/nix/gcroots/cache-builder";
description = "Build all host closures for binary cache";
serviceConfig = {
Type = "oneshot";
ExecStart = "${pkgs.bash}/bin/bash ${./cache-builder.sh}";
};
environment = {
FLAKE_REF = flakeRef;
HOSTS = builtins.concatStringsSep " " hosts;
GC_ROOT_DIR = "/nix/var/nix/gcroots/cache-builder";
};
path = [ config.nix.package ];
}; };
path = [ config.nix.package ];
};
# restart cache-builder after every nixos switch (non-blocking) systemd.timers.cache-builder = {
system.activationScripts.cache-builder = lib.stringAfter [ "specialfs" ] '' description = "Periodically build all host closures";
${config.systemd.package}/bin/systemctl restart --no-block cache-builder.service || true wantedBy = [ "timers.target" ];
''; timerConfig = {
OnUnitActiveSec = "15min";
OnBootSec = "5min";
Persistent = true;
};
}; };
}; };
} }

View File

@@ -2,7 +2,6 @@
nixos = nixos =
{ lib, config, ... }: { lib, config, ... }:
let let
cfg = config.features.initrd-ssh;
keyDir = "/etc/secrets/initrd"; keyDir = "/etc/secrets/initrd";
mkIpString = mkIpString =
@@ -16,52 +15,46 @@
"${address}::${gateway}:${netmask}::${interface}:none"; "${address}::${gateway}:${netmask}::${interface}:none";
in in
{ {
options.features.initrd-ssh = { options = {
enable = lib.mkEnableOption "initrd ssh"; initrd-ssh = {
ip = {
enable = lib.mkEnableOption "static IP for initrd (otherwise DHCP)";
ip = { address = lib.mkOption {
enable = lib.mkEnableOption "static IP for initrd (otherwise DHCP)"; type = lib.types.str;
};
address = lib.mkOption { gateway = lib.mkOption {
type = lib.types.str; type = lib.types.str;
};
netmask = lib.mkOption {
type = lib.types.str;
default = "255.255.255.0";
};
interface = lib.mkOption {
type = lib.types.str;
};
}; };
gateway = lib.mkOption { authorizedKeys = lib.mkOption {
type = lib.types.str; type = lib.types.listOf lib.types.str;
default = [ ];
}; };
netmask = lib.mkOption { networkModule = lib.mkOption {
type = lib.types.str;
default = "255.255.255.0";
};
interface = lib.mkOption {
type = lib.types.str; type = lib.types.str;
}; };
}; };
authorizedKeys = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
};
networkModule = lib.mkOption {
type = lib.types.str;
};
}; };
config = lib.mkIf cfg.enable { config = {
boot.initrd.availableKernelModules = [ cfg.networkModule ]; boot.initrd.kernelModules = [ config.initrd-ssh.networkModule ];
boot.initrd.kernelModules = [ cfg.networkModule ]; boot.kernelParams = lib.mkIf config.initrd-ssh.ip.enable [
boot.kernelParams = lib.mkIf cfg.ip.enable [ "ip=${mkIpString config.initrd-ssh.ip}"
"ip=${mkIpString cfg.ip}"
]; ];
boot.initrd.systemd.enable = true;
# remote unlock may take a while; don't let device units give up
boot.initrd.systemd.settings.Manager.DefaultDeviceTimeoutSec = "infinity";
boot.initrd.network = { boot.initrd.network = {
enable = true; enable = true;
ssh = { ssh = {
@@ -71,20 +64,12 @@
"${keyDir}/ssh_host_rsa_key" "${keyDir}/ssh_host_rsa_key"
"${keyDir}/ssh_host_ed25519_key" "${keyDir}/ssh_host_ed25519_key"
]; ];
inherit (cfg) authorizedKeys; inherit (config.initrd-ssh) authorizedKeys;
}; };
postCommands = ''
echo 'cryptsetup-askpass' >> /root/.profile
'';
}; };
# systemd-networkd retries DHCP indefinitely, unlike udhcpc
boot.initrd.systemd.network.networks = lib.mkIf (!cfg.ip.enable) {
"10-initrd" = {
matchConfig.Driver = cfg.networkModule;
networkConfig.DHCP = "yes";
};
};
# forward LUKS password prompt to the SSH session
boot.initrd.systemd.users.root.shell = "/bin/systemd-tty-ask-password-agent";
}; };
}; };
} }

View File

@@ -1,30 +1,25 @@
{ {
nixos = nixos =
{ lib, config, ... }: { lib, config, ... }:
let
cfg = config.features.localisation;
in
{ {
options.features.localisation = { options = {
enable = lib.mkEnableOption "localisation"; localisation = {
timeZone = lib.mkOption {
type = lib.types.str;
};
timeZone = lib.mkOption { defaultLocale = lib.mkOption {
type = lib.types.str; type = lib.types.str;
default = "Europe/Ljubljana"; };
};
defaultLocale = lib.mkOption {
type = lib.types.str;
default = "en_US.UTF-8";
}; };
}; };
config = lib.mkIf cfg.enable { config = {
time.timeZone = cfg.timeZone; time.timeZone = config.localisation.timeZone;
i18n.defaultLocale = cfg.defaultLocale; i18n.defaultLocale = config.localisation.defaultLocale;
# NOTE:(@janezicmatej) some apps (e.g. java) need TZ env var explicitly # NOTE:(@janezicmatej) some apps (e.g. java) need TZ env var explicitly
environment.variables.TZ = cfg.timeZone; environment.variables.TZ = config.localisation.timeZone;
}; };
}; };
} }

View File

@@ -1,17 +1,4 @@
{ {
nixos =
{ lib, ... }:
{
options.features.neovim = {
enable = lib.mkEnableOption "neovim";
dotfiles = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
};
};
};
home = home =
{ {
config, config,
@@ -19,64 +6,64 @@
lib, lib,
pkgs, pkgs,
inputs, inputs,
osConfig,
... ...
}: }:
let
cfg = osConfig.features.neovim;
in
{ {
config = lib.mkIf cfg.enable ( options = {
lib.mkMerge [ neovim.dotfiles = lib.mkOption {
(lib.optionalAttrs (options ? stylix) { type = lib.types.nullOr lib.types.path;
# disable stylix neovim target when stylix is present default = null;
stylix.targets.neovim.enable = false; };
}) };
{
xdg.configFile."nvim" = lib.mkIf (cfg.dotfiles != null) {
source = cfg.dotfiles;
};
programs.neovim = { config = lib.mkMerge [
enable = true; (lib.optionalAttrs (options ? stylix) {
sideloadInitLua = true; # disable stylix neovim target when stylix is present (loaded by desktop feature)
vimAlias = true; stylix.targets.neovim.enable = false;
defaultEditor = true; })
package = inputs.neovim-nightly-overlay.packages.${pkgs.stdenv.hostPlatform.system}.default; {
xdg.configFile."nvim" = lib.mkIf (config.neovim.dotfiles != null) {
source = config.neovim.dotfiles;
};
extraPackages = with pkgs; [ programs.neovim = {
gcc enable = true;
luajit vimAlias = true;
nodejs_22 defaultEditor = true;
tree-sitter package = inputs.neovim-nightly-overlay.packages.${pkgs.stdenv.hostPlatform.system}.default;
gnumake
osc
fd extraPackages = with pkgs; [
ripgrep gcc
bat luajit
delta nodejs_22
tree-sitter
gnumake
osc
pyright fd
typescript-language-server ripgrep
lua-language-server bat
gopls delta
nil
nixd
nixpkgs-fmt pyright
stylua typescript-language-server
]; lua-language-server
gopls
nil
nixd
extraWrapperArgs = [ nixpkgs-fmt
"--suffix" stylua
"LD_LIBRARY_PATH" ];
":"
"${lib.makeLibraryPath [ pkgs.stdenv.cc.cc.lib ]}" extraWrapperArgs = [
]; "--suffix"
}; "LD_LIBRARY_PATH"
} ":"
] "${lib.makeLibraryPath [ pkgs.stdenv.cc.cc.lib ]}"
); ];
};
}
];
}; };
} }

View File

@@ -1,18 +1,9 @@
{ {
nixos = nixos = _: {
{ config, lib, ... }: networking.networkmanager.enable = true;
let networking.nameservers = [
cfg = config.features.networkmanager; "1.1.1.1"
in "8.8.8.8"
{ ];
options.features.networkmanager.enable = lib.mkEnableOption "networkmanager"; };
config = lib.mkIf cfg.enable {
networking.networkmanager.enable = true;
networking.nameservers = [
"1.1.1.1"
"8.8.8.8"
];
};
};
} }

View File

@@ -1,14 +1,5 @@
{ {
nixos = nixos = _: {
{ config, lib, ... }: programs.nix-ld.enable = true;
let };
cfg = config.features.nix-ld;
in
{
options.features.nix-ld.enable = lib.mkEnableOption "nix-ld";
config = lib.mkIf cfg.enable {
programs.nix-ld.enable = true;
};
};
} }

View File

@@ -1,71 +0,0 @@
{
nixos =
{ config, lib, ... }:
let
cfg = config.features.nix-settings;
in
{
options.features.nix-settings = {
enable = lib.mkEnableOption "nix settings";
towerCache.enable = lib.mkOption {
type = lib.types.bool;
default = true;
};
gc = {
dates = lib.mkOption {
type = lib.types.str;
default = "monthly";
};
olderThan = lib.mkOption {
type = lib.types.str;
default = "30d";
};
};
optimise.dates = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ "monthly" ];
};
};
config = lib.mkIf cfg.enable {
nix = {
settings = {
experimental-features = [
"nix-command"
"flakes"
];
download-buffer-size = 2 * 1024 * 1024 * 1024;
download-attempts = 3;
fallback = true;
warn-dirty = false;
substituters = [
"https://cache.nixos.org"
"https://nix-community.cachix.org?priority=45"
]
++ lib.optional cfg.towerCache.enable "http://tower:5000?priority=50";
trusted-public-keys = [
"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
"nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs="
]
++ lib.optional cfg.towerCache.enable "matej.nix-1:TdbemLVYblvAxqJcwb3mVKmmr3cfzXbMcZHE5ILnZDE=";
};
gc = {
automatic = true;
inherit (cfg.gc) dates;
options = "--delete-older-than ${cfg.gc.olderThan}";
};
optimise = {
automatic = true;
inherit (cfg.optimise) dates;
};
};
};
};
}

View File

@@ -1,23 +0,0 @@
{
nixos =
{
config,
lib,
user,
...
}:
let
cfg = config.features.onepassword;
in
{
options.features.onepassword.enable = lib.mkEnableOption "1password";
config = lib.mkIf cfg.enable {
programs._1password.enable = true;
programs._1password-gui = {
enable = true;
polkitPolicyOwners = [ user ];
};
};
};
}

View File

@@ -1,23 +1,18 @@
{ {
nixos = nixos =
{ lib, config, ... }: { lib, config, ... }:
let
cfg = config.features.openssh;
in
{ {
options.features.openssh = { options = {
enable = lib.mkEnableOption "openssh"; openssh.port = lib.mkOption {
port = lib.mkOption {
type = lib.types.port; type = lib.types.port;
default = 22; default = 22;
}; };
}; };
config = lib.mkIf cfg.enable { config = {
services.openssh = { services.openssh = {
enable = true; enable = true;
ports = [ cfg.port ]; ports = [ config.openssh.port ];
settings = { settings = {
PasswordAuthentication = false; PasswordAuthentication = false;
AllowUsers = null; AllowUsers = null;

View File

@@ -1,57 +0,0 @@
{
nixos =
{ config, lib, ... }:
let
cfg = config.features.power;
in
{
options.features.power = {
enable = lib.mkEnableOption "laptop power management";
resumeDevice = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
};
lidSwitch = lib.mkOption {
type = lib.types.str;
default = "suspend-then-hibernate";
};
powerKey = lib.mkOption {
type = lib.types.str;
default = "suspend-then-hibernate";
};
idleAction = lib.mkOption {
type = lib.types.str;
default = "suspend-then-hibernate";
};
idleActionSec = lib.mkOption {
type = lib.types.str;
default = "15min";
};
hibernateDelaySec = lib.mkOption {
type = lib.types.str;
default = "30min";
};
};
config = lib.mkIf cfg.enable {
boot.resumeDevice = lib.mkIf (cfg.resumeDevice != null) cfg.resumeDevice;
services.logind.settings.Login = {
HandleLidSwitch = cfg.lidSwitch;
HandlePowerKey = cfg.powerKey;
IdleAction = cfg.idleAction;
IdleActionSec = cfg.idleActionSec;
};
systemd.sleep.settings.Sleep = {
HibernateDelaySec = cfg.hibernateDelaySec;
};
};
};
}

View File

@@ -1,19 +1,10 @@
{ {
nixos = nixos = _: {
{ config, lib, ... }: services.printing.enable = true;
let services.avahi = {
cfg = config.features.printing; enable = true;
in nssmdns4 = true;
{ openFirewall = true;
options.features.printing.enable = lib.mkEnableOption "printing";
config = lib.mkIf cfg.enable {
services.printing.enable = true;
services.avahi = {
enable = true;
nssmdns4 = true;
openFirewall = true;
};
};
}; };
};
} }

View File

@@ -1,25 +1,13 @@
{ {
nixos = nixos =
{ config, user, ... }:
{ {
config, sops.secrets.user-password = {
lib, sopsFile = ../secrets/common.yaml;
user, neededForUsers = true;
...
}:
let
cfg = config.features.remote-base;
in
{
options.features.remote-base.enable = lib.mkEnableOption "remote-base";
config = lib.mkIf cfg.enable {
sops.secrets.user-password = {
sopsFile = ../secrets/common.yaml;
neededForUsers = true;
};
users.mutableUsers = false;
users.users.${user}.hashedPasswordFile = config.sops.secrets.user-password.path;
}; };
users.mutableUsers = false;
users.users.${user}.hashedPasswordFile = config.sops.secrets.user-password.path;
}; };
} }

View File

@@ -0,0 +1,16 @@
{
nixos = _: {
programs.zsh.enable = true;
environment.etc."zshenv".text = ''
export ZDOTDIR=$HOME/.config/zsh
'';
};
home =
{ pkgs, ... }:
{
home.packages = with pkgs; [
starship
];
};
}

View File

@@ -1,33 +1,25 @@
{ {
nixos = nixos = _: {
{ lib, ... }: programs.zsh.enable = true;
{ environment.etc."zshenv".text = ''
options.features.shell.enable = lib.mkEnableOption "shell extras"; export ZDOTDIR=$HOME/.config/zsh
}; '';
};
home = home =
{ pkgs, ... }:
{ {
pkgs, home.packages = with pkgs; [
lib, starship
osConfig, fzf
... htop
}: jc
let jq
cfg = osConfig.features.shell; openssl
in pv
{ ripgrep
config = lib.mkIf cfg.enable { fd
home.packages = with pkgs; [ tmux
fzf ];
htop
jc
jq
openssl
pv
ripgrep
fd
tmux
];
};
}; };
} }

View File

@@ -1,100 +1,38 @@
{ {
nixos = nixos =
{ pkgs, ... }:
{ {
config, programs.sway = {
lib, enable = true;
pkgs, package = pkgs.swayfx;
... wrapperFeatures.gtk = true;
}: extraSessionCommands = ''
let # fix for java awt apps not rendering
cfg = config.features.sway; export _JAVA_AWT_WM_NONREPARENTING=1
desktopCfg = config.features.desktop; '';
in
{
options.features.sway = {
enable = lib.mkEnableOption "sway window manager";
greeter.enable = lib.mkOption {
type = lib.types.bool;
default = true;
};
}; };
config = lib.mkIf cfg.enable ( environment.systemPackages = with pkgs; [
lib.mkMerge [ waybar
{ mako
# soft dependency wob
features.desktop.enable = lib.mkDefault true; playerctl
brightnessctl
# hard dependency foot
assertions = [ grim
{ pulseaudio
assertion = desktopCfg.enable; swayidle
message = "features.sway requires features.desktop"; swaylock-effects
} jq
]; slurp
wl-clipboard
programs.sway = { pamixer
enable = true; wlsunset
package = pkgs.swayfx; satty
wrapperFeatures.gtk = true; wayland-pipewire-idle-inhibit
extraSessionCommands = '' fuzzel
# fix for java awt apps not rendering cliphist
export _JAVA_AWT_WM_NONREPARENTING=1 zenity
# propagate XDG_DATA_DIRS to dbus/systemd for d-bus activated apps ];
dbus-update-activation-environment --systemd XDG_DATA_DIRS
'';
};
environment.systemPackages = with pkgs; [
waybar
mako
wob
playerctl
brightnessctl
foot
grim
pulseaudio
swayidle
swaylock-effects
jq
slurp
wl-clipboard
pamixer
wlsunset
satty
wayland-pipewire-idle-inhibit
fuzzel
cliphist
zenity
];
}
# greeter
(lib.mkIf cfg.greeter.enable {
programs.regreet = {
enable = true;
cageArgs = [
"-s"
"-m"
"last"
];
font = {
name = lib.mkForce "JetBrainsMono Nerd Font";
size = lib.mkForce 14;
};
settings = {
background = {
path = lib.mkForce (toString desktopCfg.theme.wallpaper);
fit = lib.mkForce "Cover";
};
GTK = {
application_prefer_dark_theme = lib.mkForce true;
};
};
};
})
]
);
}; };
} }

View File

@@ -1,17 +1,8 @@
{ {
nixos = nixos = _: {
{ config, lib, ... }: services.tailscale = {
let enable = true;
cfg = config.features.tailscale; useRoutingFeatures = "both";
in
{
options.features.tailscale.enable = lib.mkEnableOption "tailscale";
config = lib.mkIf cfg.enable {
services.tailscale = {
enable = true;
useRoutingFeatures = "both";
};
};
}; };
};
} }

View File

@@ -1,59 +0,0 @@
{
nixos =
{
config,
lib,
pkgs,
...
}:
let
cfg = config.features.udev;
in
{
options.features.udev = {
enable = lib.mkEnableOption "custom udev rules";
kindle.enable = lib.mkOption {
type = lib.types.bool;
default = false;
};
ledger.enable = lib.mkOption {
type = lib.types.bool;
default = false;
};
keyboard-zsa.enable = lib.mkOption {
type = lib.types.bool;
default = false;
};
};
config = lib.mkIf cfg.enable (
lib.mkMerge [
(lib.mkIf cfg.kindle.enable {
# NOTE:(@janezicmatej) uses services.udev.packages instead of extraRules
# because extraRules writes to 99-local.rules which is too late for uaccess
services.udev.packages = [
pkgs.libmtp
(pkgs.writeTextFile {
name = "kindle-udev-rules";
text = ''
ACTION!="remove", SUBSYSTEM=="usb", ATTRS{idVendor}=="1949", TAG+="uaccess"
'';
destination = "/etc/udev/rules.d/70-kindle.rules";
})
];
})
(lib.mkIf cfg.ledger.enable {
hardware.ledger.enable = true;
})
(lib.mkIf cfg.keyboard-zsa.enable {
hardware.keyboard.zsa.enable = true;
})
]
);
};
}

View File

@@ -9,20 +9,25 @@ in
sshAuthorizedKeys = sshKeys; sshAuthorizedKeys = sshKeys;
}; };
nixos = _: { nixos =
users.users.matej = { { pkgs, ... }:
uid = 1000; {
isNormalUser = true; programs.zsh.enable = true;
home = "/home/matej";
extraGroups = [ "wheel" ];
openssh.authorizedKeys.keys = sshKeys;
};
users.groups.matej = { users.users.matej = {
gid = 1000; uid = 1000;
members = [ "matej" ]; isNormalUser = true;
home = "/home/matej";
shell = pkgs.zsh;
extraGroups = [ "wheel" ];
openssh.authorizedKeys.keys = sshKeys;
};
users.groups.matej = {
gid = 1000;
members = [ "matej" ];
};
}; };
};
home = _: { home = _: {
home.stateVersion = "26.05"; home.stateVersion = "26.05";

View File

@@ -0,0 +1,72 @@
{
nixos =
{
pkgs,
lib,
config,
...
}:
let
inherit (config.vm-9p-automount) user;
inherit (config.users.users.${user}) home group;
in
{
options = {
vm-9p-automount = {
user = lib.mkOption {
type = lib.types.str;
};
prefix = lib.mkOption {
type = lib.types.str;
default = "m_";
};
basePath = lib.mkOption {
type = lib.types.str;
default = "${home}/mnt";
};
};
};
config = {
systemd.services.vm-9p-automount = {
description = "Auto-discover and mount 9p shares";
after = [
"local-fs.target"
"nss-user-lookup.target"
"systemd-modules-load.service"
];
wants = [ "systemd-modules-load.service" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = pkgs.writeShellScript "vm-9p-automount" ''
BASE="${config.vm-9p-automount.basePath}"
PREFIX="${config.vm-9p-automount.prefix}"
mkdir -p "$BASE"
chown ${user}:${group} "$BASE"
for tagfile in $(find /sys/devices -name mount_tag 2>/dev/null); do
[ -f "$tagfile" ] || continue
tag=$(tr -d '\0' < "$tagfile")
case "$tag" in
"$PREFIX"*) ;;
*) continue ;;
esac
name="''${tag#"$PREFIX"}"
target="$BASE/$name"
mkdir -p "$target"
${pkgs.util-linux}/bin/mount -t 9p "$tag" "$target" \
-o trans=virtio,version=9p2000.L || continue
done
'';
};
};
};
};
}

View File

@@ -6,107 +6,42 @@
config, config,
... ...
}: }:
let
cfg = config.features.vm-guest;
autoUser = cfg.automount.user;
autoHome = config.users.users.${autoUser}.home;
autoGroup = config.users.users.${autoUser}.group;
in
{ {
options.features.vm-guest = { options = {
enable = lib.mkEnableOption "qemu vm guest"; vm-guest.headless = lib.mkOption {
headless = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
default = false; default = false;
}; };
automount = {
enable = lib.mkEnableOption "9p share automount";
user = lib.mkOption {
type = lib.types.str;
};
prefix = lib.mkOption {
type = lib.types.str;
default = "m_";
};
basePath = lib.mkOption {
type = lib.types.str;
default = "${autoHome}/mnt";
};
};
}; };
config = lib.mkIf cfg.enable ( config = {
lib.mkMerge [ services.qemuGuest.enable = true;
{ services.spice-vdagentd.enable = lib.mkIf (!config.vm-guest.headless) true;
services.spice-vdagentd.enable = lib.mkIf (!cfg.headless) true;
boot.kernelParams = lib.mkIf cfg.headless [ "console=ttyS0,115200" ]; boot.kernelParams = lib.mkIf config.vm-guest.headless [ "console=ttyS0,115200" ];
# 9p autoloads on first mount boot.initrd.availableKernelModules = [
boot.initrd.availableKernelModules = [ "9p"
"9p" "9pnet_virtio"
"9pnet_virtio" ];
]; boot.kernelModules = [
"9p"
"9pnet_virtio"
];
networking = { networking = {
useDHCP = true; useDHCP = true;
firewall.allowedTCPPorts = [ 22 ]; firewall.allowedTCPPorts = [ 22 ];
}; };
security.sudo.wheelNeedsPassword = false; security.sudo.wheelNeedsPassword = false;
environment.systemPackages = with pkgs; [ environment.systemPackages = with pkgs; [
curl curl
wget wget
htop htop
]; sshfs
} ];
};
(lib.mkIf cfg.automount.enable {
systemd.services.vm-9p-automount = {
description = "Auto-discover and mount 9p shares";
after = [
"local-fs.target"
"nss-user-lookup.target"
"systemd-modules-load.service"
];
wants = [ "systemd-modules-load.service" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = pkgs.writeShellScript "vm-9p-automount" ''
BASE="${cfg.automount.basePath}"
PREFIX="${cfg.automount.prefix}"
mkdir -p "$BASE"
chown ${autoUser}:${autoGroup} "$BASE"
for tagfile in $(find /sys/devices -name mount_tag 2>/dev/null); do
[ -f "$tagfile" ] || continue
tag=$(tr -d '\0' < "$tagfile")
case "$tag" in
"$PREFIX"*) ;;
*) continue ;;
esac
name="''${tag#"$PREFIX"}"
target="$BASE/$name"
mkdir -p "$target"
${pkgs.util-linux}/bin/mount -t 9p "$tag" "$target" \
-o trans=virtio,version=9p2000.L || continue
done
'';
};
};
})
]
);
}; };
} }

12
features/yubikey.nix Normal file
View File

@@ -0,0 +1,12 @@
{
nixos =
{ pkgs, ... }:
{
environment.systemPackages = with pkgs; [
yubikey-personalization
yubikey-manager
];
services.pcscd.enable = true;
};
}

View File

@@ -1,55 +0,0 @@
{
nixos =
{
config,
lib,
pkgs,
user,
...
}:
let
cfg = config.features.zsh;
in
{
options.features.zsh = {
enable = lib.mkEnableOption "zsh";
loginShell.enable = lib.mkOption {
type = lib.types.bool;
default = true;
};
};
config = lib.mkIf cfg.enable (
lib.mkMerge [
{
programs.zsh.enable = true;
environment.etc."zshenv".text = ''
export ZDOTDIR=$HOME/.config/zsh
'';
}
(lib.mkIf cfg.loginShell.enable {
users.users.${user}.shell = pkgs.zsh;
})
]
);
};
home =
{
pkgs,
lib,
osConfig,
...
}:
let
cfg = osConfig.features.zsh;
in
{
config = lib.mkIf cfg.enable {
home.packages = [ pkgs.starship ];
};
};
}

60
flake.lock generated
View File

@@ -106,11 +106,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1776613567, "lastModified": 1773889306,
"narHash": "sha256-gC9Cp5ibBmGD5awCA9z7xy6MW6iJufhazTYJOiGlCUI=", "narHash": "sha256-PAqwnsBSI9SVC2QugvQ3xeYCB0otOwCacB1ueQj2tgw=",
"owner": "nix-community", "owner": "nix-community",
"repo": "disko", "repo": "disko",
"rev": "32f4236bfc141ae930b5ba2fb604f561fed5219d", "rev": "5ad85c82cc52264f4beddc934ba57f3789f28347",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -273,11 +273,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1776777932, "lastModified": 1775556024,
"narHash": "sha256-0R3Yow/NzSeVGUke5tL7CCkqmss4Vmi6BbV6idHzq/8=", "narHash": "sha256-j1u/859OVS54rGlsvFqJdwKPEnFYCI+4pyfTiSfv1Xc=",
"owner": "nix-community", "owner": "nix-community",
"repo": "home-manager", "repo": "home-manager",
"rev": "5d5640599a0050b994330328b9fd45709c909720", "rev": "4bdfeff1d9b7473e6e58f73f5809576e8a69e406",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -317,11 +317,11 @@
"nixpkgs": "nixpkgs" "nixpkgs": "nixpkgs"
}, },
"locked": { "locked": {
"lastModified": 1776729909, "lastModified": 1775520277,
"narHash": "sha256-wGu/N42PJqrj8ju9GoXdppg4rwaKzZqdAjsgxJbCvfY=", "narHash": "sha256-dUI8/Mc6CfA+EmfrYByt+oNIEvdRLtSSDVS54O0YpBM=",
"owner": "nix-community", "owner": "nix-community",
"repo": "neovim-nightly-overlay", "repo": "neovim-nightly-overlay",
"rev": "ff21a18bde28b4c8ca0bc1f9a5b7186a1b89a3d1", "rev": "b63186e453b0ad340760648293aa1ccf75528c46",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -333,11 +333,11 @@
"neovim-src": { "neovim-src": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1776727374, "lastModified": 1775514165,
"narHash": "sha256-iP5SviNXW5W+ay4ZmwjDFsfQjfM+fYlUxRlLPHjpwWI=", "narHash": "sha256-mAUtXA5BfCH5SRInzfJdaHY9egHrVLED0DsQzgBW4Fk=",
"owner": "neovim", "owner": "neovim",
"repo": "neovim", "repo": "neovim",
"rev": "901b3f0c394a53961781ebeee682e64ad690a242", "rev": "b36eafd5dafae561763e5dc2ba73b3f0b74c63fe",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -364,11 +364,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1776329215, "lastModified": 1775464765,
"narHash": "sha256-a8BYi3mzoJ/AcJP8UldOx8emoPRLeWqALZWu4ZvjPXw=", "narHash": "sha256-nex6TL2x1/sVHCyDWcvl1t/dbTedb9bAGC4DLf/pmYk=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "b86751bc4085f48661017fa226dee99fab6c651b", "rev": "83e29f2b8791f6dec20804382fcd9a666d744c07",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -395,11 +395,11 @@
}, },
"nixpkgs-master": { "nixpkgs-master": {
"locked": { "locked": {
"lastModified": 1776807375, "lastModified": 1775560693,
"narHash": "sha256-LDnHG0T54OEHyRydmGUlAND8ham0KrRNWjgoS+6GUd4=", "narHash": "sha256-V1y4hd0R0XYG5GMpdj0W9h6976r6kZV5vcmKysclmfE=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "553ecb1686a2edb75dee44c9f72e1674e6adc26a", "rev": "987687fd632e29817fc9c9d96eddba7d264510c0",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -411,11 +411,11 @@
}, },
"nixpkgs-stable": { "nixpkgs-stable": {
"locked": { "locked": {
"lastModified": 1776560675, "lastModified": 1775305101,
"narHash": "sha256-p68udKWWh7+V4ZPpcMDq0gTHWNZJnr4JPI+kHPPE40o=", "narHash": "sha256-/74n1oQPtKG52Yw41cbToxspxHbYz6O3vi+XEw16Qe8=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "e07580dae39738e46609eaab8b154de2488133ce", "rev": "36a601196c4ebf49e035270e10b2d103fe39076b",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -427,11 +427,11 @@
}, },
"nixpkgs_2": { "nixpkgs_2": {
"locked": { "locked": {
"lastModified": 1776548001, "lastModified": 1775423009,
"narHash": "sha256-ZSK0NL4a1BwVbbTBoSnWgbJy9HeZFXLYQizjb2DPF24=", "narHash": "sha256-vPKLpjhIVWdDrfiUM8atW6YkIggCEKdSAlJPzzhkQlw=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "b12141ef619e0a9c1c84dc8c684040326f27cdcc", "rev": "68d8aa3d661f0e6bd5862291b5bb263b2a6595c9",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -550,11 +550,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1776771786, "lastModified": 1775365543,
"narHash": "sha256-DRFGPfFV6hbrfO9a1PH1FkCi7qR5FgjSqsQGGvk1rdI=", "narHash": "sha256-f50qrK0WwZ9z5EdaMGWOTtALgSF7yb7XwuE7LjCuDmw=",
"owner": "Mic92", "owner": "Mic92",
"repo": "sops-nix", "repo": "sops-nix",
"rev": "bef289e2248991f7afeb95965c82fbcd8ff72598", "rev": "a4ee2de76efb759fe8d4868c33dec9937897916f",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -583,11 +583,11 @@
"tinted-zed": "tinted-zed" "tinted-zed": "tinted-zed"
}, },
"locked": { "locked": {
"lastModified": 1776170745, "lastModified": 1775429060,
"narHash": "sha256-Tl1aZVP5EIlT+k0+iAKH018GLHJpLz3hhJ0LNQOWxCc=", "narHash": "sha256-wbFF5cRxQOCzL/wHOKYm21t5AHPH2Lfp0mVPCOAvEoc=",
"owner": "danth", "owner": "danth",
"repo": "stylix", "repo": "stylix",
"rev": "e3861617645a43c9bbefde1aa6ac54dd0a44bfa9", "rev": "d27951a6539951d87f75cf0a7cda8a3a24016019",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@@ -15,29 +15,25 @@ in
system = "x86_64-linux"; system = "x86_64-linux";
user = "matej"; user = "matej";
features = [ features = [
"bootloader"
"claude"
"desktop"
"dev"
"direnv"
"docker"
"gaming"
"git"
"gnupg"
"localisation"
"neovim"
"networkmanager"
"nix-ld"
"nix-settings"
"onepassword"
"openssh" "openssh"
"power" "localisation"
"printing" "gnupg"
"shell" "shell"
"desktop"
"sway" "sway"
"greeter"
"printing"
"networkmanager"
"docker"
"tailscale" "tailscale"
"udev" "nix-ld"
"zsh" "yubikey"
"calibre"
"gaming"
"direnv"
"neovim"
"dev"
"claude"
]; ];
}; };
@@ -45,30 +41,27 @@ in
system = "x86_64-linux"; system = "x86_64-linux";
user = "matej"; user = "matej";
features = [ features = [
"bootloader"
"claude"
"desktop"
"dev"
"direnv"
"docker"
"gaming"
"git"
"gnupg"
"harmonia"
"initrd-ssh"
"localisation"
"neovim"
"networkmanager"
"nix-ld"
"nix-settings"
"onepassword"
"openssh" "openssh"
"printing" "localisation"
"gnupg"
"shell" "shell"
"desktop"
"sway" "sway"
"greeter"
"printing"
"networkmanager"
"docker"
"tailscale" "tailscale"
"udev" "nix-ld"
"zsh" "yubikey"
"calibre"
"gaming"
"initrd-ssh"
"direnv"
"neovim"
"dev"
"claude"
"harmonia"
]; ];
}; };
@@ -77,9 +70,7 @@ in
system = "x86_64-linux"; system = "x86_64-linux";
user = "matej"; user = "matej";
features = [ features = [
"nix-settings"
"openssh" "openssh"
"zsh"
]; ];
}; };
@@ -87,14 +78,11 @@ in
system = "x86_64-linux"; system = "x86_64-linux";
user = "matej"; user = "matej";
features = [ features = [
"bootloader"
"localisation"
"nix-settings"
"openssh" "openssh"
"remote-base" "localisation"
"shell" "shell"
"tailscale" "tailscale"
"zsh" "remote-base"
]; ];
}; };
@@ -103,14 +91,12 @@ in
system = "x86_64-linux"; system = "x86_64-linux";
user = "matej"; user = "matej";
features = [ features = [
"filedrop"
"localisation"
"nix-settings"
"openssh" "openssh"
"remote-base" "localisation"
"shell" "shell"
"tailscale" "tailscale"
"zsh" "remote-base"
"filedrop"
]; ];
}; };
@@ -118,15 +104,14 @@ in
system = "x86_64-linux"; system = "x86_64-linux";
user = "matej"; user = "matej";
features = [ features = [
"bootloader"
"desktop"
"gnupg"
"localisation" "localisation"
"networkmanager" "gnupg"
"nix-settings" "shell-minimal"
"desktop-minimal"
"sway" "sway"
"udev" "greeter"
"zsh" "networkmanager"
"yubikey"
]; ];
}; };
@@ -134,18 +119,16 @@ in
system = "x86_64-linux"; system = "x86_64-linux";
user = "matej"; user = "matej";
features = [ features = [
"claude"
"dev"
"docker"
"git"
"gnupg"
"localisation"
"neovim"
"nix-settings"
"openssh" "openssh"
"localisation"
"gnupg"
"shell" "shell"
"vm-guest" "vm-guest"
"zsh" "vm-9p-automount"
"docker"
"neovim"
"claude"
"dev"
]; ];
}; };
}; };

View File

@@ -1,5 +1,19 @@
_: { inputs, ... }:
{ {
flake.overlays.default = _: _: { }; flake.overlays.default =
_: prev:
let
pkgs-stable = import inputs.nixpkgs-stable {
inherit (prev.stdenv.hostPlatform) system;
inherit (prev) config;
};
pkgs-master = import inputs.nixpkgs-master {
inherit (prev.stdenv.hostPlatform) system;
inherit (prev) config;
};
in
{
inherit (pkgs-master) claude-code;
};
} }

View File

@@ -2,6 +2,9 @@
{ {
imports = [ inputs.disko.nixosModules.disko ]; imports = [ inputs.disko.nixosModules.disko ];
boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = true;
disko.devices.disk.main = { disko.devices.disk.main = {
type = "disk"; type = "disk";
device = "/dev/nvme0n1"; device = "/dev/nvme0n1";
@@ -29,5 +32,10 @@
}; };
}; };
localisation = {
timeZone = "Europe/Ljubljana";
defaultLocale = "en_US.UTF-8";
};
system.stateVersion = "25.11"; system.stateVersion = "25.11";
} }

View File

@@ -5,7 +5,6 @@
... ...
}: }:
{ {
features.nix-settings.towerCache.enable = false;
# no hardware firmware needed in a VM # no hardware firmware needed in a VM
hardware.enableRedistributableFirmware = lib.mkForce false; hardware.enableRedistributableFirmware = lib.mkForce false;
hardware.wirelessRegulatoryDatabase = lib.mkForce false; hardware.wirelessRegulatoryDatabase = lib.mkForce false;
@@ -13,93 +12,37 @@
documentation.enable = false; documentation.enable = false;
environment.defaultPackages = [ ]; environment.defaultPackages = [ ];
# qcow2, no channel copy; post-processed with parallel zstd on qcow2 v3 # compressed qcow2, no channel copy
# (~half the size of zlib v2, faster decompress)
image.modules.qemu = image.modules.qemu =
{ config, modulesPath, ... }: { config, modulesPath, ... }:
{ {
system.build.image = lib.mkForce ( system.build.image = lib.mkForce (
let import (modulesPath + "/../lib/make-disk-image.nix") {
rawImage = import (modulesPath + "/../lib/make-disk-image.nix") { inherit lib config pkgs;
inherit lib config pkgs; inherit (config.virtualisation) diskSize;
inherit (config.virtualisation) diskSize;
inherit (config.image) baseName;
format = "qcow2";
copyChannel = false;
partitionTableType = "legacy";
};
inherit (config.image) baseName; inherit (config.image) baseName;
in format = "qcow2-compressed";
pkgs.runCommand baseName { nativeBuildInputs = [ pkgs.qemu-utils ]; } '' copyChannel = false;
mkdir -p $out partitionTableType = "legacy";
# qemu-img caps -m at 16 }
cores="''${NIX_BUILD_CORES:-4}"
[ "$cores" -gt 0 ] || cores=4
[ "$cores" -gt 16 ] && cores=16
qemu-img convert \
-f qcow2 \
-O qcow2 \
-c \
-o compression_type=zstd \
-m "$cores" \
${rawImage}/${baseName}.qcow2 \
$out/${baseName}.qcow2
''
); );
}; };
# auto-login on serial console vm-guest.headless = true;
services.getty.autologinUser = "matej";
# enable zsh in home-manager so starship init gets wired up vm-9p-automount.user = "matej";
home-manager.users.matej.programs.zsh = {
enable = true; localisation = {
dotDir = "/home/matej/.config/zsh"; timeZone = "UTC";
shellAliases.dsp = "claude --dangerously-skip-permissions"; defaultLocale = "en_US.UTF-8";
}; };
home-manager.users.matej.programs.starship = { home-manager.users.matej = {
enable = true; neovim.dotfiles = inputs.nvim;
settings = {
add_newline = false;
format = "$username$hostname$directory$character";
hostname = {
ssh_only = false;
style = "bold blue";
format = "[@$hostname]($style)";
};
username = {
show_always = true;
style_user = "bold blue";
format = "[$user]($style)";
};
directory.format = " [$path]($style) ";
character = {
success_symbol = "[>](bold green)";
error_symbol = "[>](bold red)";
};
};
}; };
features.vm-guest.headless = true;
features.vm-guest.automount = {
enable = true;
user = "matej";
};
features.neovim.dotfiles = inputs.nvim;
# ensure .config exists with correct ownership before automount # ensure .config exists with correct ownership before automount
systemd.tmpfiles.rules = [ "d /home/matej/.config 0700 matej users -" ]; systemd.tmpfiles.rules = [ "d /home/matej/.config 0755 matej users -" ];
# TODO:(@janezicmatej) replace ssh with virtio-console (hvc0) when qemu 11.0 lands
# https://www.mail-archive.com/qemu-devel@nongnu.org/msg1162844.html
# accept any ssh key (ephemeral localhost-only vm)
services.openssh.settings.AuthorizedKeysCommand =
let
acceptKey = pkgs.writeShellScript "ephvm-accept-key" ''echo "$1 $2"'';
in
"${acceptKey} %t %k";
services.openssh.settings.AuthorizedKeysCommandUser = "nobody";
# writable claude config via 9p # writable claude config via 9p
fileSystems."/home/matej/.config/claude" = { fileSystems."/home/matej/.config/claude" = {

View File

@@ -2,8 +2,6 @@
{ {
imports = [ inputs.disko.nixosModules.disko ]; imports = [ inputs.disko.nixosModules.disko ];
features.filedrop.sopsFile = ../../secrets/floo.yaml;
boot.loader.grub.enable = true; boot.loader.grub.enable = true;
disko.devices.disk.main = { disko.devices.disk.main = {
@@ -28,5 +26,10 @@
}; };
}; };
localisation = {
timeZone = "Europe/Ljubljana";
defaultLocale = "en_US.UTF-8";
};
system.stateVersion = "25.11"; system.stateVersion = "25.11";
} }

View File

@@ -10,15 +10,18 @@
inputs.nixos-hardware.nixosModules.framework-16-amd-ai-300-series inputs.nixos-hardware.nixosModules.framework-16-amd-ai-300-series
]; ];
features.desktop = { localisation = {
apps.enable = false; timeZone = "Europe/Ljubljana";
internalCA.enable = false; defaultLocale = "en_US.UTF-8";
}; };
features.gnupg.yubikey.enable = true;
boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = true;
# TODO:(@janezicmatej) replace device path with actual SSD device
disko.devices.disk.main = { disko.devices.disk.main = {
type = "disk"; type = "disk";
device = "/dev/sda"; device = "/dev/nvme1n1";
content = { content = {
type = "gpt"; type = "gpt";
partitions = { partitions = {

View File

@@ -1,30 +0,0 @@
{
config,
lib,
pkgs,
modulesPath,
...
}:
{
imports = [
(modulesPath + "/installer/scan/not-detected.nix")
];
hardware.firmware = [ pkgs.linux-firmware ];
boot.initrd.availableKernelModules = [
"nvme"
"xhci_pci"
"thunderbolt"
"usbhid"
"uas"
"sd_mod"
];
boot.initrd.kernelModules = [ "dm-snapshot" ];
boot.kernelModules = [ "kvm-amd" ];
boot.extraModulePackages = [ ];
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
hardware.cpu.amd.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
}

View File

@@ -1,7 +1,8 @@
{ {
lib, lib,
options, pkgs,
inputs, inputs,
options,
... ...
}: }:
@@ -10,23 +11,37 @@
inputs.nixos-hardware.nixosModules.framework-16-amd-ai-300-series inputs.nixos-hardware.nixosModules.framework-16-amd-ai-300-series
]; ];
features.bootloader.plymouth.enable = true; localisation = {
features.desktop.bluetooth.enable = true; timeZone = "Europe/Ljubljana";
features.gnupg.yubikey.enable = true; defaultLocale = "en_US.UTF-8";
features.udev = {
ledger.enable = true;
keyboard-zsa.enable = true;
}; };
features.power.resumeDevice = "/dev/disk/by-uuid/ff4750e7-3a9f-42c2-bb68-c458a6560540";
boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = true;
boot.kernelParams = [ "pcie_aspm.policy=powersupersave" ]; boot.kernelParams = [ "pcie_aspm.policy=powersupersave" ];
boot.resumeDevice = "/dev/disk/by-uuid/ff4750e7-3a9f-42c2-bb68-c458a6560540";
services.logind.settings.Login = {
HandleLidSwitch = "suspend-then-hibernate";
HandlePowerKey = "suspend-then-hibernate";
IdleAction = "suspend-then-hibernate";
IdleActionSec = "15min";
};
systemd.sleep.settings.Sleep = {
HibernateDelaySec = "30min";
};
programs.nix-ld.libraries = options.programs.nix-ld.libraries.default; programs.nix-ld.libraries = options.programs.nix-ld.libraries.default;
services.gnome.gnome-keyring.enable = true; services.gnome.gnome-keyring.enable = true;
services.teamviewer.enable = true; services.teamviewer.enable = true;
services.hardware.bolt.enable = true; services.hardware.bolt.enable = true;
hardware.keyboard.zsa.enable = true;
hardware.ledger.enable = true;
hardware.bluetooth.powerOnBoot = true; hardware.bluetooth.powerOnBoot = true;
hardware.inputmodule.enable = true; hardware.inputmodule.enable = true;

View File

@@ -1,13 +1,15 @@
{ lib, userKeys, ... }: { lib, ... }:
{ {
features.nix-settings.towerCache.enable = false;
image.modules.iso-installer = { image.modules.iso-installer = {
isoImage.squashfsCompression = "zstd -Xcompression-level 6"; isoImage.squashfsCompression = "zstd -Xcompression-level 6";
}; };
# live iso: passwordless login and sudo # live iso: passwordless login and sudo
users.users.matej.initialHashedPassword = ""; users.users.matej.initialHashedPassword = "";
users.users.root.openssh.authorizedKeys.keys = userKeys.sshAuthorizedKeys; users.users.root.openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICQGLdINKzs+sEy62Pefng0bcedgU396+OryFgeH99/c janezicmatej"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDk00+Km03epQXQs+xEwwH3zcurACzkEH+kDOPBw6RQe openpgp:0xB095D449"
];
services.openssh.settings.PermitRootLogin = lib.mkForce "prohibit-password"; services.openssh.settings.PermitRootLogin = lib.mkForce "prohibit-password";
security.sudo.wheelNeedsPassword = false; security.sudo.wheelNeedsPassword = false;

View File

@@ -1,33 +1,38 @@
{ {
config, config,
lib,
inputs,
userKeys, userKeys,
... ...
}: }:
{ {
features.nix-settings.towerCache.enable = false; imports = [
features.bootloader = { inputs.lanzaboote.nixosModules.lanzaboote
mode = "lanzaboote"; ];
plymouth.enable = true;
};
features.desktop.bluetooth.enable = true;
features.gnupg.yubikey.enable = true;
features.udev = {
ledger.enable = true;
keyboard-zsa.enable = true;
};
features.initrd-ssh = {
networkModule = "r8169";
authorizedKeys = userKeys.sshAuthorizedKeys;
};
# nix store signing # nix store signing
sops.secrets.nix-signing-key.sopsFile = ../../secrets/tower.yaml; sops.secrets.nix-signing-key.sopsFile = ../../secrets/tower.yaml;
nix.settings.secret-key-files = [ config.sops.secrets.nix-signing-key.path ]; nix.settings.secret-key-files = [ config.sops.secrets.nix-signing-key.path ];
localisation = {
timeZone = "Europe/Ljubljana";
defaultLocale = "en_US.UTF-8";
};
initrd-ssh = {
networkModule = "r8169";
authorizedKeys = userKeys.sshAuthorizedKeys;
};
# lanzaboote secure boot
boot.kernelParams = [ "btusb.reset=1" ]; boot.kernelParams = [ "btusb.reset=1" ];
# early kms so plymouth lands on amdgpu, not simpledrm boot.loader.efi.canTouchEfiVariables = true;
hardware.amdgpu.initrd.enable = true; boot.loader.systemd-boot.enable = lib.mkForce false;
boot.lanzaboote = {
enable = true;
pkiBundle = "/var/lib/sbctl";
};
services.udisks2.enable = true; services.udisks2.enable = true;

View File

@@ -27,19 +27,7 @@ let
hostConfig = ../hosts/${name}/configuration.nix; hostConfig = ../hosts/${name}/configuration.nix;
hostHWConfig = ../hosts/${name}/hardware-configuration.nix; hostHWConfig = ../hosts/${name}/hardware-configuration.nix;
# auto-discover all features, excluding user-* and default.nix # load feature with path check
featureDir = builtins.readDir ../features;
allFeatureNames = lib.pipe featureDir [
(lib.filterAttrs (
n: t:
(t == "regular" && lib.hasSuffix ".nix" n && n != "default.nix" && !lib.hasPrefix "user-" n)
|| (t == "directory" && builtins.pathExists ../features/${n}/default.nix)
))
builtins.attrNames
(map (n: lib.removeSuffix ".nix" n))
];
# load all features unconditionally
loadFeature = loadFeature =
f: f:
assert assert
@@ -47,7 +35,7 @@ let
|| throw "feature '${f}' not found at ${toString (featurePath f)}"; || throw "feature '${f}' not found at ${toString (featurePath f)}";
import (featurePath f); import (featurePath f);
loadedFeatures = map loadFeature allFeatureNames; loadedFeatures = map loadFeature features;
# load user feature with path check # load user feature with path check
userFeature = userFeature =
@@ -67,27 +55,17 @@ let
# collect nixos and home modules from all features # collect nixos and home modules from all features
nixosMods = map (f: f.nixos) (builtins.filter (f: f ? nixos) allFeatures); nixosMods = map (f: f.nixos) (builtins.filter (f: f ? nixos) allFeatures);
homeMods = map (f: f.home) (builtins.filter (f: f ? home) allFeatures); homeMods = map (f: f.home) (builtins.filter (f: f ? home) allFeatures);
# translate features list to enable flags
featureEnableModule =
{ lib, ... }:
{
config.features = lib.genAttrs features (_: {
enable = true;
});
};
in in
nixpkgs.lib.nixosSystem { nixpkgs.lib.nixosSystem {
inherit system; inherit system;
modules = [ modules = [
../nix.nix
inputs.sops-nix.nixosModules.sops inputs.sops-nix.nixosModules.sops
inputs.stylix.nixosModules.stylix
{ nixpkgs.overlays = overlays; } { nixpkgs.overlays = overlays; }
{ nixpkgs.config.allowUnfree = true; } { nixpkgs.config.allowUnfree = true; }
{ networking.hostName = name; } { networking.hostName = name; }
featureEnableModule
hostConfig hostConfig
] ]
++ lib.optional (builtins.pathExists hostHWConfig) hostHWConfig ++ lib.optional (builtins.pathExists hostHWConfig) hostHWConfig

View File

@@ -1,91 +0,0 @@
{ pkgs, ... }:
let
inherit (pkgs) stdenv lib;
version = "2.1.116";
# upstream ships platform-native binaries as separate npm packages under
# @anthropic-ai/claude-code-<platform>; the wrapper package is just a
# postinstall shim that copies the matching one into place
sources = {
"x86_64-linux" = {
slug = "linux-x64";
hash = "sha256-QEjJ4CRk35TubDNW02Dzcu+EMRLLndJUXJeP3BFT3b8=";
};
"aarch64-linux" = {
slug = "linux-arm64";
hash = "sha256-/Hqp8GQx8Hub8K4w0Fnx/AksksY61vRC44XxrJVwF5w=";
};
"x86_64-darwin" = {
slug = "darwin-x64";
hash = "sha256-O3J/ew2fWbUQePs6tHEhK0Q9E3Mx/BDSL7b7NL3FRc8=";
};
"aarch64-darwin" = {
slug = "darwin-arm64";
hash = "sha256-O41sf7b05SJfXVjszMeTp838mja+PgZ+aEKykLsHeNo=";
};
};
source =
sources.${stdenv.hostPlatform.system}
or (throw "claude-code: unsupported system ${stdenv.hostPlatform.system}");
in
stdenv.mkDerivation {
pname = "claude-code";
inherit version;
src = pkgs.fetchzip {
url = "https://registry.npmjs.org/@anthropic-ai/claude-code-${source.slug}/-/claude-code-${source.slug}-${version}.tgz";
inherit (source) hash;
};
nativeBuildInputs = [
pkgs.makeWrapper
]
++ lib.optionals stdenv.hostPlatform.isLinux [ pkgs.patchelf ];
dontBuild = true;
dontConfigure = true;
dontStrip = true;
installPhase = ''
runHook preInstall
install -Dm755 claude $out/bin/claude
runHook postInstall
'';
# NOTE:(@janezicmatej) upstream is a bun single-file-executable; the
# embedded script payload sits at the tail of the ELF, so autoPatchelfHook's
# section-layout changes corrupt it — only the interpreter can be rewritten
postFixup =
lib.optionalString stdenv.hostPlatform.isLinux ''
patchelf --set-interpreter ${stdenv.cc.bintools.dynamicLinker} $out/bin/claude
''
+ ''
wrapProgram $out/bin/claude \
--set DISABLE_AUTOUPDATER 1 \
--set-default FORCE_AUTOUPDATE_PLUGINS 1 \
--set DISABLE_INSTALLATION_CHECKS 1 \
--unset DEV \
--prefix PATH : ${
lib.makeBinPath (
[
pkgs.procps
]
++ lib.optionals stdenv.hostPlatform.isLinux [
pkgs.bubblewrap
pkgs.socat
]
)
}
'';
meta = {
description = "Agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster";
homepage = "https://github.com/anthropics/claude-code";
downloadPage = "https://www.npmjs.com/package/@anthropic-ai/claude-code";
license = lib.licenses.unfree;
mainProgram = "claude";
platforms = lib.attrNames sources;
};
}

View File

@@ -1,53 +0,0 @@
#!/usr/bin/env nix-shell
#!nix-shell -i bash -p curl jq nix
# shellcheck shell=bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PKG_FILE="$SCRIPT_DIR/package.nix"
# keep in sync with the `sources` attrset in package.nix
PLATFORMS=(linux-x64 linux-arm64 darwin-x64 darwin-arm64)
prefetch() {
local url="$1"
nix --extra-experimental-features 'nix-command flakes' \
store prefetch-file --unpack --json "$url" 2>/dev/null | jq -r '.hash'
}
main() {
echo "fetching latest version from npm..."
local latest current
latest=$(curl -sf "https://registry.npmjs.org/@anthropic-ai/claude-code/latest" | jq -r '.version')
current=$(grep 'version = ' "$PKG_FILE" | head -1 | sed 's/.*"\(.*\)".*/\1/')
if [[ "$current" == "$latest" ]]; then
echo "claude-code already at $latest"
return 0
fi
echo "updating claude-code: $current -> $latest"
sed -i "s|version = \"$current\"|version = \"$latest\"|" "$PKG_FILE"
local slug url new_hash old_hash
for slug in "${PLATFORMS[@]}"; do
url="https://registry.npmjs.org/@anthropic-ai/claude-code-${slug}/-/claude-code-${slug}-${latest}.tgz"
echo " prefetching $slug..."
new_hash=$(prefetch "$url")
old_hash=$(awk -v slug="$slug" '
$0 ~ "slug = \"" slug "\";" { found=1; next }
found && /hash = "sha256-/ {
match($0, /sha256-[A-Za-z0-9+\/]+=*/)
print substr($0, RSTART, RLENGTH)
exit
}
' "$PKG_FILE")
sed -i "s|$old_hash|$new_hash|" "$PKG_FILE"
echo " $new_hash"
done
echo "claude-code updated to $latest"
}
main "$@"

View File

@@ -27,44 +27,23 @@ info() {
# globals for cleanup trap # globals for cleanup trap
CLEANUP_OVERLAY="" CLEANUP_OVERLAY=""
CLEANUP_TMPDIR=""
QEMU_PID=""
VM_READY=false
cleanup() { cleanup() {
[ -n "$QEMU_PID" ] && kill "$QEMU_PID" 2>/dev/null && wait "$QEMU_PID" 2>/dev/null
[ -n "$CLEANUP_OVERLAY" ] && rm -rf "$CLEANUP_OVERLAY" [ -n "$CLEANUP_OVERLAY" ] && rm -rf "$CLEANUP_OVERLAY"
# preserve tmpdir on abnormal exit so the qemu log survives for inspection
if [ -n "$CLEANUP_TMPDIR" ]; then
if [ "$VM_READY" = true ]; then
rm -rf "$CLEANUP_TMPDIR"
else
echo "qemu log preserved: $CLEANUP_TMPDIR/qemu.log" >&2
fi
fi
return 0 return 0
} }
trap cleanup EXIT trap cleanup EXIT
# returns 0 once the guest's sshd is speaking (first bytes are "SSH-")
awaiting_ssh_banner() {
local port="$1"
local banner
banner=$(timeout 2 bash -c "exec 3<>/dev/tcp/localhost/$port; head -c 4 <&3" 2>/dev/null) || return 1
[ "$banner" = "SSH-" ]
}
usage() { usage() {
cat <<EOF cat <<EOF
Usage: ephvm-run.sh [options] Usage: ephvm-run.sh [options]
Options: Options:
--mount <path> Mount host directory into VM (repeatable) --mount <path> Mount host directory into VM (repeatable)
--no-claude Skip mounting claude config dir --claude Mount claude config dir (requires CLAUDE_CONFIG_DIR)
--disk-size <size> Resize guest disk (e.g. 50G) --disk-size <size> Resize guest disk (e.g. 50G)
--memory <size> VM memory (default: 4G) --memory <size> VM memory (default: 8G)
--cpus <n> VM CPUs (default: 2) --cpus <n> VM CPUs (default: 4)
--ssh-port <port> Use specific SSH port (default: auto) --ssh-port <port> SSH port forward (default: 2222)
--serial Attach to serial console instead of SSH
-h, --help Show usage -h, --help Show usage
EOF EOF
exit "${1:-0}" exit "${1:-0}"
@@ -73,9 +52,7 @@ EOF
main() { main() {
setup_colors setup_colors
[ "$EUID" -eq 0 ] && die "ephvm-run.sh must not run as root" local ssh_port=2222 memory=8G cpus=4 claude=false disk_size=""
local ssh_port="" memory=4G cpus=2 claude=true disk_size="" serial=false
local -a mounts=() local -a mounts=()
while [ $# -gt 0 ]; do while [ $# -gt 0 ]; do
@@ -84,8 +61,8 @@ main() {
mounts+=("$2") mounts+=("$2")
shift 2 shift 2
;; ;;
--no-claude) --claude)
claude=false claude=true
shift shift
;; ;;
--disk-size) --disk-size)
@@ -104,10 +81,6 @@ main() {
ssh_port="$2" ssh_port="$2"
shift 2 shift 2
;; ;;
--serial)
serial=true
shift
;;
-h | --help) usage ;; -h | --help) usage ;;
*) *)
echo "${red}error:${reset} unknown option: $1" >&2 echo "${red}error:${reset} unknown option: $1" >&2
@@ -118,9 +91,7 @@ main() {
info "building ephvm image..." info "building ephvm image..."
local image_dir image local image_dir image
[ -n "${EPHVM_FLAKE:-}" ] || die "EPHVM_FLAKE must be set to the flake directory" image_dir=$(nix build --no-link --print-out-paths .#nixosConfigurations.ephvm.config.system.build.images.qemu)
local flake="$EPHVM_FLAKE"
image_dir=$(nix build --no-link --print-out-paths "${flake}#nixosConfigurations.ephvm.config.system.build.images.qemu")
image=$(find "$image_dir" -name '*.qcow2' -print -quit) image=$(find "$image_dir" -name '*.qcow2' -print -quit)
[ -n "$image" ] || die "no qcow2 image found in $image_dir" [ -n "$image" ] || die "no qcow2 image found in $image_dir"
@@ -130,49 +101,31 @@ main() {
CLEANUP_OVERLAY=$(mktemp -d) CLEANUP_OVERLAY=$(mktemp -d)
local overlay="$CLEANUP_OVERLAY/overlay.qcow2" local overlay="$CLEANUP_OVERLAY/overlay.qcow2"
qemu-img create -f qcow2 -b "$(realpath "$image")" -F qcow2 "$overlay" "$disk_size" qemu-img create -f qcow2 -b "$(realpath "$image")" -F qcow2 "$overlay" "$disk_size"
drive_arg="if=none,id=hd0,file=$overlay,format=qcow2,cache=writeback,aio=threads,discard=unmap,detect-zeroes=unmap" drive_arg="file=$overlay,format=qcow2"
else else
drive_arg="if=none,id=hd0,file=$image,format=qcow2,snapshot=on,cache=writeback,aio=threads,discard=unmap,detect-zeroes=unmap" drive_arg="file=$image,format=qcow2,snapshot=on"
fi fi
command -v qemu-system-x86_64 &>/dev/null || die "qemu-system-x86_64 not found" local accel="tcg"
[ -r /dev/kvm ] || die "/dev/kvm not readable; kvm is required" [ -r /dev/kvm ] && accel="kvm"
# auto-allocate ssh port unless serial mode
if [ "$serial" = false ] && [ -z "$ssh_port" ]; then
ssh_port=10022
while ss -tln | grep -q ":${ssh_port}\b"; do
ssh_port=$((ssh_port + 1))
done
fi
local nic_arg="user,model=virtio-net-pci"
if [ -n "$ssh_port" ]; then
nic_arg="user,model=virtio-net-pci,hostfwd=tcp:127.0.0.1:${ssh_port}-:22"
fi
local -a qemu_args=( local -a qemu_args=(
qemu-system-x86_64 qemu-system-x86_64
-accel kvm -accel "$accel"
-cpu host
-m "$memory" -m "$memory"
-smp "$cpus" -smp "$cpus"
-drive "$drive_arg" -drive "$drive_arg"
-device "virtio-blk-pci,drive=hd0" -nic "user,hostfwd=tcp::${ssh_port}-:22"
-device virtio-rng-pci
-nic "$nic_arg"
-nographic -nographic
-sandbox "on,obsolete=deny,elevateprivileges=deny,spawn=deny,resourcecontrol=deny"
) )
if [ "$accel" != "tcg" ]; then
qemu_args+=(-cpu host)
fi
local fs_id=0 mount_path name tag local fs_id=0 mount_path name tag
for mount_path in "${mounts[@]}"; do for mount_path in "${mounts[@]}"; do
[ -e "$mount_path" ] || die "--mount path does not exist: $mount_path"
mount_path=$(realpath "$mount_path") mount_path=$(realpath "$mount_path")
# qemu parses -virtfs as csv, a comma in the path would inject options
case "$mount_path" in
*,*) die "--mount path may not contain commas: $mount_path" ;;
esac
name=$(basename "$mount_path") name=$(basename "$mount_path")
tag="m_${name:0:29}" tag="m_${name:0:29}"
qemu_args+=( qemu_args+=(
@@ -182,13 +135,10 @@ main() {
done done
if [ "$claude" = true ]; then if [ "$claude" = true ]; then
[ -n "${CLAUDE_CONFIG_DIR:-}" ] || die "CLAUDE_CONFIG_DIR must be set (use --no-claude to skip)" [ -n "${CLAUDE_CONFIG_DIR:-}" ] || die "--claude requires CLAUDE_CONFIG_DIR to be set"
mkdir -p "$CLAUDE_CONFIG_DIR" mkdir -p "$CLAUDE_CONFIG_DIR"
local claude_dir local claude_dir
claude_dir=$(realpath "$CLAUDE_CONFIG_DIR") claude_dir=$(realpath "$CLAUDE_CONFIG_DIR")
case "$claude_dir" in
*,*) die "claude config dir may not contain commas: $claude_dir" ;;
esac
qemu_args+=( qemu_args+=(
-virtfs "local,path=$claude_dir,mount_tag=claude,security_model=none,id=fs${fs_id}" -virtfs "local,path=$claude_dir,mount_tag=claude,security_model=none,id=fs${fs_id}"
@@ -197,42 +147,10 @@ main() {
fi fi
info "---" info "---"
[ -n "$ssh_port" ] && info "SSH: ssh -p $ssh_port matej@localhost" info "Accel: $accel | SSH: ssh -p $ssh_port matej@localhost"
info "---" info "---"
if [ "$serial" = true ]; then exec "${qemu_args[@]}"
exec "${qemu_args[@]}"
fi
CLEANUP_TMPDIR=$(mktemp -d)
local qemu_log="$CLEANUP_TMPDIR/qemu.log"
# start qemu in background and auto-ssh
"${qemu_args[@]}" &>"$qemu_log" &
QEMU_PID=$!
# throwaway ssh key (vm accepts any key via AuthorizedKeysCommand)
local ssh_key="$CLEANUP_TMPDIR/id_ed25519"
ssh-keygen -t ed25519 -f "$ssh_key" -N "" -q
info "waiting for vm (port $ssh_port)..."
local attempts=0
# poll for the real SSH banner, not TCP accept: qemu's user-mode nic
# accepts host-side the moment qemu starts, well before guest sshd is up
while ! awaiting_ssh_banner "$ssh_port"; do
attempts=$((attempts + 1))
[ $attempts -gt 120 ] && die "vm did not become ready in 60s"
kill -0 "$QEMU_PID" 2>/dev/null || die "qemu exited unexpectedly"
sleep 0.5
done
VM_READY=true
ssh -p "$ssh_port" -t \
-i "$ssh_key" \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
-o LogLevel=ERROR \
matej@localhost
} }
main "$@" main "$@"