bitspire/flake.nix
Padreug e98fd67653 feat(deploy): set the timezone per machine, sintra to Europe/Paris
Every machine inherited America/Guatemala from the shared base config,
which is right for douro and tejo and wrong for sintra in France. It read
six hours behind, so its journal timestamps had to be converted by hand
against anything on the relay.

The clock is not cosmetic here. system.autoUpgrade's `dates = "04:00"` is
local time, so the zone decides when the nightly rebuild restarts the app
and the Fujitsu dispenser runs its audible init routine. On sintra that
was firing at 10:17 in the morning, in the room, rather than at 4am.

timeZoneForModel mirrors fiatCodeForModel and lists only the exceptions.
The base config keeps America/Guatemala as the fleet default, now under
mkDefault so a per-model value wins without mkForce. Verified by eval:
sintra resolves to Europe/Paris, douro, tejo and batm3 are unchanged.

batm3 is deliberately left alone. It is USD and in the field, and I do
not know where.
2026-09-24 12:26:55 +02:00

542 lines
26 KiB
Nix

{
description = "bitSpire - Nostr-Native Lightning ATM";
inputs = {
# Stable NixOS for the ATM OS base
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
# Unstable for Electron, Node.js, pnpm (latest versions)
nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
flake-utils.url = "github:numtide/flake-utils";
rust-overlay = {
url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs-unstable";
};
devenv = {
url = "github:cachix/devenv";
inputs.nixpkgs.follows = "nixpkgs-unstable";
};
# Determinate Nix — ensures douro uses same nix version as dev machines
# so cachix binary cache hits match (nix 2.33 hashes == nix 2.33 hashes).
# The bare `?3` semver pin (≥3.0.0) was resolving to 3.16.3, which
# ships a regressed nix-functional-tests:local-overlay-store /
# stale-file-handle that FAILs when the determinate-nix derivation
# has to be built from source (no binary cache hit) — this blocked
# disk-image-sintra builds locally. `?3.15` (semver ≥3.15.0) skips
# past the regression; flake.lock currently resolves it to 3.20.0,
# which builds and ships nix 2.34.6.
determinate.url = "https://flakehub.com/f/DeterminateSystems/determinate/3.15";
# ATM TUI — operator management tool
atm-tui = {
url = "git+ssh://forgejo@git.atitlan.io/aiolabs/atm-tui.git";
inputs.nixpkgs.follows = "nixpkgs-unstable";
};
};
outputs = { self, nixpkgs, nixpkgs-unstable, flake-utils, rust-overlay, devenv, determinate, atm-tui }:
let
system = "x86_64-linux";
pkgs = import nixpkgs {
inherit system;
config.allowUnfree = true;
};
pkgs-unstable = import nixpkgs-unstable {
inherit system;
config.allowUnfree = true;
overlays = [ (import rust-overlay) ];
};
# Pure ATM app builder (no --impure needed)
mkAtmApp = import ./nix/mkAtmApp.nix {
inherit pkgs pkgs-unstable;
src = self;
};
# Fiat code per machine model
fiatCodeForModel = {
douro = "GTQ";
tejo = "GTQ";
sintra = "EUR";
batm3 = "USD";
};
# Timezone per machine model. Wall-clock time on an ATM is not cosmetic:
# it is what the journal is read in when someone is standing at the
# machine, and system.autoUpgrade's `dates = "04:00"` is local, so this
# decides when the nightly rebuild restarts the app and the dispenser
# runs its audible init routine. A machine on the wrong zone does that
# in the middle of its own business hours.
#
# Unlisted models fall back to the fleet default in configuration.nix.
timeZoneForModel = {
sintra = "Europe/Paris";
};
lib = nixpkgs.lib;
# Helper to create a live USB NixOS config for a specific machine model
mkLiveConfig = machineModel:
let
atm-app = mkAtmApp {
model = machineModel;
fiatCode = fiatCodeForModel.${machineModel} or "USD";
};
in
nixpkgs.lib.nixosSystem {
inherit system;
specialArgs = {
inherit pkgs-unstable nixpkgs machineModel atm-app;
};
modules = [
./deploy/nixos/live.nix
determinate.nixosModules.default
{
environment.systemPackages = [
atm-tui.packages.${system}.default
(pkgs.writeShellScriptBin "fund-atm" ''
exec ${pkgs-unstable.nodejs}/bin/node ${atm-app}/dist-electron/fund-atm.bundle.cjs "$@"
'')
];
environment.variables.ATM_DB_PATH = "/var/lib/bitspire/state.db";
}
];
};
# Helper to create a disk-installed NixOS config for a specific machine model.
# Unlike live configs (squashfs + tmpfs), installed configs use ext4 root
# and support `nixos-rebuild switch` for in-place updates.
mkInstalledConfig = machineModel: hardwareModule:
let
atm-app = mkAtmApp {
model = machineModel;
fiatCode = fiatCodeForModel.${machineModel} or "USD";
};
fiatCode = fiatCodeForModel.${machineModel} or "USD";
in
nixpkgs.lib.nixosSystem {
inherit system;
specialArgs = {
inherit pkgs-unstable atm-app;
};
modules = [
hardwareModule
./deploy/nixos/configuration.nix
./deploy/nixos/bitspire-atm.nix
determinate.nixosModules.default
({ config, lib, pkgs, ... }: {
services.bitspire = {
enable = true;
appDir = "${atm-app}";
};
# Per-machine wall clock; see timeZoneForModel above.
time.timeZone = lib.mkIf (timeZoneForModel ? ${machineModel})
timeZoneForModel.${machineModel};
# Operator TUI and CLI tools
environment.systemPackages = [
atm-tui.packages.${system}.default
(pkgs.writeShellScriptBin "fund-atm" ''
exec ${pkgs-unstable.nodejs}/bin/node ${atm-app}/dist-electron/fund-atm.bundle.cjs "$@"
'')
];
environment.variables.ATM_DB_PATH = "/var/lib/bitspire/state.db";
# Electron sandbox needs unprivileged user namespaces
boot.kernel.sysctl."kernel.unprivileged_userns_clone" = 1;
# Passwordless sudo for remote nixos-rebuild switch
security.sudo.wheelNeedsPassword = false;
# Allow bitspire user to use nix commands + pull from aiolabs binary cache.
# max-jobs = 1: prefer substitution from the cache, but allow ONE
# local build slot for tiny activation-time stitch derivations
# (boot.json, system-units, X-Restart-Triggers, etc.) that are
# inherently per-machine and can never be pre-cached. Heavy
# nixpkgs compiles (rustc, kernel, electron) are still
# effectively cache-only — they're upstream-cached, so a cache
# miss on them stays vanishingly rare in practice.
# max-jobs = 0 was tried first and silently bricked nightly
# auto-upgrades for 6+ days on an uncacheable trivial derivation
# (systemd-boot's boot.json).
nix.settings = {
max-jobs = 1;
# Hard ceiling on any local build's wall-clock time. Activation-
# time stitch derivations (boot.json, system-units, etc.) finish
# in well under a second; anything that doesn't return in 60s
# is by definition a heavy compile that has no business running
# on ATM hardware (kernel, electron, rustc). Kill it fast so
# the upgrade fails loudly instead of silently wedging the box
# for an hour. Time-bounds the max-jobs=1 escape hatch.
timeout = 60;
trusted-users = [ "root" "bitspire" ];
substituters = [ "https://cache.nixos.org" "https://aiolabs.cachix.org" ];
trusted-public-keys = [
"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
"aiolabs.cachix.org-1:PrAjsGU9PE77tFKP2+iO+mgR88c4xv3utM9JmpTblUQ="
];
};
# Auto-upgrade: pulls latest flake and runs nixos-rebuild switch.
# NOTE: bitSpire machines pull from the `aiolabs/bitspire` repo —
# the post-migration home of this code. This branch (dev) pins the
# upgrade source to ?ref=dev so any ATM flashed from `dev` stays on
# `dev`. Without the explicit ?ref=dev, nix would resolve the repo's
# default branch and could silently change a dev-deployed Sintra at
# 04:00. The legacy `aiolabs/lamassu-next` repo still feeds the
# not-yet-converted production ATMs (batm3, douro) from its own
# branches; it is retired once those machines migrate to bitspire.
# To update manually: sudo nixos-rebuild switch --flake git+ssh://forgejo@git.atitlan.io/aiolabs/bitspire.git?ref=dev#<model>-installed
system.autoUpgrade = {
enable = true;
flake = "git+ssh://forgejo@git.atitlan.io/aiolabs/bitspire.git?ref=dev#${machineModel}-installed";
flags = [ "--refresh" ];
dates = "04:00"; # daily at 4am
allowReboot = false;
};
# Minimal env template (aiolabs/bitspire#70 remnant hygiene).
# Seed ONLY image-baked, non-maskable values. Everything else the
# ATM needs comes from the pairing SEED (relay, lnbits_npub, bunker)
# or from LNbits over the transport (operator pubkey, fee config) —
# so we must NOT pre-seed those keys. A present-but-empty
# VITE_RELAY_URL / VITE_LNBITS_SERVER_PUBKEY / VITE_OPERATOR_PUBKEYS
# is a masking hazard: env WINS over the seed, and this activation
# only writes when .env is ABSENT, so any value written at first
# boot is frozen for the life of the disk. Leaving the keys out
# entirely lets the seed/transport be the sole source.
#
# VITE_RELAY_URL / VITE_LNBITS_SERVER_PUBKEY are emitted ONLY when
# the operator deliberately pins them via the Nix options (non-empty
# default ""), which is an explicit override that wins over the seed.
system.activationScripts.bitspire-env = ''
mkdir -p /var/lib/bitspire
if [ ! -f /var/lib/bitspire/.env ]; then
cp ${pkgs.writeText "bitspire-env-default" (''
VITE_LAMASSU_MACHINE_MODEL=${machineModel}
VITE_LAMASSU_FIAT_CODE=${fiatCode}
VITE_SPIRE_SEED=
ELECTRON_FORCE_PROD=1
DISPLAY=:0
'' + pkgs.lib.optionalString (config.services.bitspire.relayUrl != "") ''
VITE_RELAY_URL=${config.services.bitspire.relayUrl}
'' + pkgs.lib.optionalString (config.services.bitspire.lnbitsServerPubkey != "") ''
VITE_LNBITS_SERVER_PUBKEY=${config.services.bitspire.lnbitsServerPubkey}
'')} /var/lib/bitspire/.env
chmod 600 /var/lib/bitspire/.env
chown bitspire:bitspire /var/lib/bitspire/.env
fi
'';
# Override systemd service for Electron runtime
systemd.services.bitspire = {
serviceConfig = {
EnvironmentFile = lib.mkForce "/var/lib/bitspire/.env";
Environment = "LD_LIBRARY_PATH=${pkgs.stdenv.cc.cc.lib}/lib";
ExecStart = lib.mkForce "${pkgs-unstable.electron}/bin/electron --no-sandbox --disable-gpu-sandbox --disable-gpu --disable-software-rasterizer --enable-logging ${atm-app}";
MemoryMax = lib.mkForce "1G";
NoNewPrivileges = lib.mkForce false;
ProtectSystem = lib.mkForce false;
ProtectHome = lib.mkForce false;
PrivateTmp = lib.mkForce false;
DevicePolicy = lib.mkForce "auto";
DeviceAllow = lib.mkForce [ "char-* rw" ];
};
};
# Reset eDP display output after X starts
systemd.services.display-reset = {
description = "Reset eDP display output";
after = [ "display-manager.service" ];
requires = [ "display-manager.service" ];
wantedBy = [ "graphical.target" ];
before = [ "bitspire.service" ];
serviceConfig = {
Type = "oneshot";
User = "bitspire";
Environment = "DISPLAY=:0";
ExecStart = "${pkgs.bash}/bin/bash -c '${pkgs.xorg.xrandr}/bin/xrandr --output eDP-1 --off; sleep 1; ${pkgs.xorg.xrandr}/bin/xrandr --output eDP-1 --auto'";
};
};
# Swap file — ATMs have ~2GB RAM; prevents hard-freeze under memory pressure
swapDevices = [{ device = "/var/swapfile"; size = 1024; }];
# Clean /tmp on boot to prevent stale build artifacts filling disk
boot.tmp.cleanOnBoot = true;
# SSH with password for initial provisioning
services.openssh.settings.PasswordAuthentication = lib.mkForce true;
})
];
};
in
{
# ── NixOS Configurations (top-level, not per-system) ──────────
nixosConfigurations = {
# Ergonomic names: `nixos-rebuild switch --flake .#douro`
douro = mkLiveConfig "douro";
tejo = mkLiveConfig "tejo";
sintra = mkLiveConfig "sintra";
batm3 = mkLiveConfig "batm3";
# Backwards-compat aliases. Renamed lamassu-live-* → bitSpire-live-*
# for the brand transition; both styles available until callers
# (CI / scripts / docs) catch up. Drop the lamassu-* names once
# nothing references them.
bitSpire-live-douro = mkLiveConfig "douro";
bitSpire-live-tejo = mkLiveConfig "tejo";
bitSpire-live-sintra = mkLiveConfig "sintra";
bitSpire-live = mkLiveConfig "douro";
lamassu-live-douro = mkLiveConfig "douro";
lamassu-live-tejo = mkLiveConfig "tejo";
lamassu-live-sintra = mkLiveConfig "sintra";
lamassu-live = mkLiveConfig "douro";
# Installed-to-disk configs (proper GPT + systemd-boot, supports nixos-rebuild)
douro-installed = mkInstalledConfig "douro" ./deploy/nixos/hardware/douro.nix;
tejo-installed = mkInstalledConfig "tejo" ./deploy/nixos/hardware/upboard.nix;
# Sintra shares Aaeon UP Board hardware with tejo (same validator
# at ttyJ5, dispenser at ttyJ7 layout) — reuse the same hw module.
sintra-installed = mkInstalledConfig "sintra" ./deploy/nixos/hardware/upboard.nix;
batm3-installed = mkInstalledConfig "batm3" ./deploy/nixos/hardware/batm3.nix;
# USB-bootable variant of batm3-installed. This is the config the
# flashed USB stick actually runs — distinct fs labels so stage-1 can't
# latch the internal drive, nofail /boot, no growPartition, autoUpgrade
# off. Exposed as a named config (not just inline in the disk-image
# target) so its system closure can be built here and deployed in-place
# with `nix copy` + `switch-to-configuration` — updating the app on a
# running stick WITHOUT reflashing (preserves pairing + /var/lib state).
# disk-image-batm3-usb builds its filesystem image from this same config.
batm3-usb = self.nixosConfigurations.batm3-installed.extendModules {
modules = [
({ lib, ... }: {
fileSystems."/".device = lib.mkForce "/dev/disk/by-label/nixos-usb";
fileSystems."/boot".device = lib.mkForce "/dev/disk/by-label/ESP-USB";
# /boot must NOT be a hard boot dependency on the USB image. The
# firmware already loaded the bootloader before Linux; without
# nofail, a slow/late ESP-USB enumeration (BOT is slower than UAS)
# blows past systemd's 90s device-timeout into emergency mode with
# root locked — a dead end. nofail + short timeout lets the
# already-mounted root carry the boot; /boot mounts if/when it shows.
fileSystems."/boot".options = [ "nofail" "x-systemd.device-timeout=10s" ];
# NO growPartition/autoResize: sfdisk rewriting the partition table
# on first boot is the single most bus-stressing write, and flaky
# USB bridges drop off the bus mid-rewrite (sfdisk wedges in D-state
# and ESP-USB vanishes with the device). Persistent state is a few
# MB and the image ships ~2GB free. The internal-SATA disk-image-
# batm3 keeps growPartition (a real AHCI SSD won't drop the bus).
system.autoUpgrade.enable = lib.mkForce false;
})
];
};
};
# ── Standalone NixOS module ───────────────────────────────────
nixosModules.default = import ./deploy/nixos/bitspire-atm.nix;
nixosModules.bitspire = import ./deploy/nixos/bitspire-atm.nix;
# ── Packages (x86_64-linux only for ATM hardware) ─────────────
packages.${system} = {
# Pure ATM app derivations
atm-app-douro = mkAtmApp { model = "douro"; fiatCode = "GTQ"; };
atm-app-tejo = mkAtmApp { model = "tejo"; fiatCode = "GTQ"; };
atm-app-sintra = mkAtmApp { model = "sintra"; fiatCode = "EUR"; };
atm-app-batm3 = mkAtmApp { model = "batm3"; fiatCode = "USD"; };
# ISO images
iso-douro = self.nixosConfigurations.douro.config.system.build.isoImage;
iso-tejo = self.nixosConfigurations.tejo.config.system.build.isoImage;
iso-sintra = self.nixosConfigurations.sintra.config.system.build.isoImage;
iso-batm3 = self.nixosConfigurations.batm3.config.system.build.isoImage;
# Raw disk images (dd-able to mSATA/eMMC, proper GPT + ESP)
disk-image-douro = import (nixpkgs + "/nixos/lib/make-disk-image.nix") {
inherit pkgs lib;
config = self.nixosConfigurations.douro-installed.config;
format = "raw";
partitionTableType = "efi";
diskSize = "auto";
};
disk-image-sintra = import (nixpkgs + "/nixos/lib/make-disk-image.nix") {
inherit pkgs lib;
config = self.nixosConfigurations.sintra-installed.config;
format = "raw";
partitionTableType = "efi";
diskSize = "auto";
};
# BATM3 (OptiPlex 9030 AIO board-swap) installed image, dd-able to
# its SATA drive. Unlike the douro/sintra images, this one grows
# itself: growPartition expands the root partition to fill whatever
# drive it lands on (16GB today) at first boot and autoResize
# stretches the ext4 to match — no manual parted/resize2fs step
# after flashing, and all the drive's headroom is available to the
# nix store from day one (cf. #55). Image-only override: once
# grown, subsequent nixos-rebuilds against plain batm3-installed
# are unaffected.
disk-image-batm3 =
let
cfg = self.nixosConfigurations.batm3-installed.extendModules {
modules = [
{
boot.growPartition = true;
fileSystems."/".autoResize = true;
}
];
};
in
import (nixpkgs + "/nixos/lib/make-disk-image.nix") {
inherit pkgs lib;
config = cfg.config;
format = "raw";
partitionTableType = "efi";
diskSize = "auto";
};
# USB-bootable Sintra image with DISTINCT partition labels
# (nixos-usb / ESP-USB) so the stick can be booted on a Sintra whose
# eMMC already holds a nixos/ESP-labelled install without a by-label
# collision — stage-1 would otherwise race between the two roots and
# likely mount the eMMC. Auto-upgrade is disabled: this is a portable
# test / hand-off image, not a managed fleet member, and disabling it
# also removes the scheduled bootloader writes that could otherwise
# land on the eMMC's ESP.
disk-image-sintra-usb =
let
cfg = self.nixosConfigurations.sintra-installed.extendModules {
modules = [
({ lib, ... }: {
fileSystems."/".device = lib.mkForce "/dev/disk/by-label/nixos-usb";
fileSystems."/boot".device = lib.mkForce "/dev/disk/by-label/ESP-USB";
system.autoUpgrade.enable = lib.mkForce false;
# The Sintra's Aaeon firmware USB-boots in Legacy/BIOS mode — it
# boots the live ISO via its isolinux (BIOS) El Torito image, not
# the UEFI ESP. systemd-boot is UEFI-only, so a dd'd systemd-boot
# image isn't recognised as bootable. Switch THIS USB image to
# GRUB with BOTH BIOS (MBR + bios_grub partition, via the "hybrid"
# table below) and UEFI (removable /EFI/BOOT/BOOTX64.EFI) — mirroring
# the live ISO's dual boot — so it boots on Legacy and UEFI alike.
# Scoped to the USB image; the eMMC install keeps systemd-boot.
boot.loader.systemd-boot.enable = lib.mkForce false;
boot.loader.efi.canTouchEfiVariables = lib.mkForce false;
boot.loader.grub = {
enable = lib.mkForce true;
efiSupport = true;
efiInstallAsRemovable = true;
# make-disk-image's build VM exposes the image as /dev/vda;
# GRUB installs its BIOS stage to that disk's MBR.
devices = lib.mkForce [ "/dev/vda" ];
};
})
];
};
baseImage = import (nixpkgs + "/nixos/lib/make-disk-image.nix") {
inherit pkgs lib;
config = cfg.config;
format = "raw";
# hybrid = GPT + bios_grub partition + ESP → BIOS + UEFI bootable.
partitionTableType = "hybrid";
diskSize = "auto";
label = "nixos-usb"; # ext4 root label (make-disk-image -L)
};
in
pkgs.runCommand "nixos-disk-image-sintra-usb"
{ nativeBuildInputs = [ pkgs.parted pkgs.mtools ]; }
''
mkdir -p $out
cp --sparse=always ${baseImage}/nixos.img $out/nixos.img
chmod +w $out/nixos.img
# make-disk-image hardcodes the ESP FAT label to "ESP"; relabel the
# volume to ESP-USB so /boot (by-label/ESP-USB) doesn't collide with
# the eMMC's ESP. Volume label only — bootloader files are untouched,
# and UEFI loads /EFI/BOOT/BOOTX64.EFI regardless of the label.
espStart=$(parted -sm "$out/nixos.img" unit B print | awk -F: '$1==1 {gsub("B","",$2); print $2}')
echo "ESP partition starts at byte $espStart — relabelling to ESP-USB"
export MTOOLS_SKIP_CHECK=1
mlabel -i "$out/nixos.img@@$espStart" ::ESP-USB
printf 'verify ESP label: '; mlabel -i "$out/nixos.img@@$espStart" -s :: || true
'';
# USB-bootable BATM3 TEST image with DISTINCT partition labels
# (nixos-usb / ESP-USB). The plain disk-image-batm3 reuses the generic
# nixos/ESP labels, so a USB stick carrying it, booted on a batm3 whose
# internal SATA drive ALREADY holds a nixos/ESP-labelled install, makes
# stage-1's by-label/nixos resolve to the internal drive (larger fs,
# journal recovers) instead of the stick — the stage-2 init path baked
# into the USB's boot entry isn't on that root, so stage 1 aborts.
# Distinct labels make stage-1 pick the stick unambiguously WITHOUT
# touching the internal drive. Unlike disk-image-sintra-usb this keeps
# systemd-boot: the batm3 firmware UEFI-USB-boots fine via the ESP's
# /EFI/BOOT/BOOTX64.EFI removable fallback, so no GRUB/hybrid-table
# change is needed — only the label disambiguation here plus the
# usb_storage/uas initrd modules (in batm3.nix). Does NOT grow to fill
# the stick (see the growPartition note below — sfdisk on first boot
# wedges flaky USB bridges); auto-upgrade off (test image, not a managed
# fleet member — also stops scheduled bootloader writes landing on the
# internal drive's ESP).
disk-image-batm3-usb =
let
# Filesystem image of the batm3-usb config (defined in
# nixosConfigurations). Same config that in-place deploys target, so
# a reflash and a `switch-to-configuration` converge on one system.
baseImage = import (nixpkgs + "/nixos/lib/make-disk-image.nix") {
inherit pkgs lib;
config = self.nixosConfigurations.batm3-usb.config;
format = "raw";
partitionTableType = "efi";
diskSize = "auto";
label = "nixos-usb"; # ext4 root label (make-disk-image -L)
};
in
pkgs.runCommand "nixos-disk-image-batm3-usb"
{ nativeBuildInputs = [ pkgs.parted pkgs.mtools ]; }
''
mkdir -p $out
cp --sparse=always ${baseImage}/nixos.img $out/nixos.img
chmod +w $out/nixos.img
# make-disk-image hardcodes the ESP FAT label to "ESP"; relabel the
# volume to ESP-USB so /boot (by-label/ESP-USB) can't resolve to an
# internal drive's ESP. Volume label only — bootloader files are
# untouched, and UEFI loads /EFI/BOOT/BOOTX64.EFI regardless.
espStart=$(parted -sm "$out/nixos.img" unit B print | awk -F: '$1==1 {gsub("B","",$2); print $2}')
echo "ESP partition starts at byte $espStart — relabelling to ESP-USB"
export MTOOLS_SKIP_CHECK=1
mlabel -i "$out/nixos.img@@$espStart" ::ESP-USB
printf 'verify ESP label: '; mlabel -i "$out/nixos.img@@$espStart" -s :: || true
'';
# Backwards compat
iso = self.nixosConfigurations.douro.config.system.build.isoImage;
};
}
//
# ── Dev shells (per-system via flake-utils) ───────────────────
flake-utils.lib.eachDefaultSystem (sys:
let
dev-pkgs = import nixpkgs-unstable {
system = sys;
overlays = [ (import rust-overlay) ];
};
in
{
devShells.default = devenv.lib.mkShell {
pkgs = dev-pkgs;
modules = [ ./devenv.nix ];
};
}
);
}