diff options
Diffstat (limited to 'users/sterni')
117 files changed, 8461 insertions, 0 deletions
diff --git a/users/sterni/OWNERS b/users/sterni/OWNERS new file mode 100644 index 000000000000..6434d4ca30f6 --- /dev/null +++ b/users/sterni/OWNERS @@ -0,0 +1,3 @@ +set noparent + +sterni diff --git a/users/sterni/clhs-lookup/README.md b/users/sterni/clhs-lookup/README.md new file mode 100644 index 000000000000..1f42ff43a210 --- /dev/null +++ b/users/sterni/clhs-lookup/README.md @@ -0,0 +1,13 @@ +# clhs-lookup + +Simple cli to lookup symbols' documentation in a local copy of the +Common Lisp HyperSpec. + +## usage + +``` +clhs-lookup [--print] symbol [symbol [...]] + + --print Print documentation paths to stdout instead of + opening them with $BROWSER (defaults to xdg-open). +``` diff --git a/users/sterni/clhs-lookup/clhs-lookup.lisp b/users/sterni/clhs-lookup/clhs-lookup.lisp new file mode 100644 index 000000000000..0e61dd901f93 --- /dev/null +++ b/users/sterni/clhs-lookup/clhs-lookup.lisp @@ -0,0 +1,46 @@ +(in-package :clhs-lookup) +(declaim (optimize (safety 3))) + +(defun find-symbols-paths (syms clhs) + "Find pathnames to HyperSpec files describing the listed + symbol names (as strings). Paths are returned in the order + of the symbols given with missing entries removed." + (check-type syms list) + (check-type clhs pathname) + (let* ((data-dir (merge-pathnames "HyperSpec/Data/" clhs)) + (data (merge-pathnames "Map_Sym.txt" data-dir)) + (found (make-hash-table :test #'equal)) + (syms (mapcar #'string-upcase syms))) + (with-open-file (s data :direction :input) + (loop + with missing = syms + for symbol-line = (read-line s nil :eof) + for path-line = (read-line s nil :eof) + until (or (eq symbol-line :eof) + (eq path-line :eof) + (null missing)) + for pos = (position symbol-line missing :test #'equal) + when pos + do (progn + (delete symbol-line missing) + (setf (gethash symbol-line found) path-line))) + ; TODO(sterni): get rid of Data/../ in path + (mapcar + (lambda (x) (merge-pathnames x data-dir)) + (remove nil + (mapcar (lambda (x) (gethash x found)) syms)))))) + +(defun main () + (let* ((browser (or (uiop:getenvp "BROWSER") "xdg-open")) + (args (uiop:command-line-arguments)) + (prin (member "--print" args :test #'equal)) + (syms (remove-if (lambda (x) (eq (char x 0) #\-)) args)) + (paths (find-symbols-paths syms *clhs-path*))) + (if (null paths) + (uiop:quit 1) + (dolist (p paths) + (if prin + (format t "~A~%" p) + (uiop:launch-program + (format nil "~A ~A" browser p) + :force-shell t)))))) diff --git a/users/sterni/clhs-lookup/default.nix b/users/sterni/clhs-lookup/default.nix new file mode 100644 index 000000000000..1cde38e8ce3b --- /dev/null +++ b/users/sterni/clhs-lookup/default.nix @@ -0,0 +1,39 @@ +{ pkgs, depot, ... }: + +let + inherit (pkgs) fetchzip writeText; + inherit (depot.nix) buildLisp; + inherit (builtins) replaceStrings; + + clhsVersion = "7-0"; + + clhs = fetchzip { + name = "HyperSpec-${replaceStrings [ "-" ] [ "." ] clhsVersion}"; + url = "ftp://ftp.lispworks.com/pub/software_tools/reference/HyperSpec-${clhsVersion}.tar.gz"; + sha256 = "1zsi35245m5sfb862ibzy0pzlph48wvlggnqanymhgqkpa1v20ak"; + stripRoot = false; + }; + + clhs-path = writeText "clhs-path.lisp" '' + (in-package :clhs-lookup.clhs-path) + (defparameter *clhs-path* (pathname "${clhs}/")) + ''; + + clhs-lookup = buildLisp.program { + name = "clhs-lookup"; + + deps = [ + { + default = buildLisp.bundled "asdf"; + sbcl = buildLisp.bundled "uiop"; + } + ]; + + srcs = [ + ./packages.lisp + clhs-path + ./clhs-lookup.lisp + ]; + }; +in +clhs-lookup diff --git a/users/sterni/clhs-lookup/packages.lisp b/users/sterni/clhs-lookup/packages.lisp new file mode 100644 index 000000000000..d059b96ce9f0 --- /dev/null +++ b/users/sterni/clhs-lookup/packages.lisp @@ -0,0 +1,10 @@ +(defpackage :clhs-lookup.clhs-path + (:use :cl) + (:export :*clhs-path*)) + +(defpackage clhs-lookup + (:use :cl :uiop) + (:import-from :clhs-lookup.clhs-path :*clhs-path*) + (:export :main + :find-symbols-paths)) + diff --git a/users/sterni/dot-time-man-pages/OWNERS b/users/sterni/dot-time-man-pages/OWNERS new file mode 100644 index 000000000000..b9bc074a8020 --- /dev/null +++ b/users/sterni/dot-time-man-pages/OWNERS @@ -0,0 +1 @@ +edef diff --git a/users/sterni/dot-time-man-pages/default.nix b/users/sterni/dot-time-man-pages/default.nix new file mode 100644 index 000000000000..c449cde613f9 --- /dev/null +++ b/users/sterni/dot-time-man-pages/default.nix @@ -0,0 +1,70 @@ +{ depot, lib, ... }: + +let + # TODO(sterni): find a better place for this: is dot time //fun? + + # get the email address of a depot user from //ops/users + findEmail = user: + let + res = builtins.filter ({ username, ... }: username == user) depot.ops.users; + len = builtins.length res; + in + if len == 1 + then (builtins.head res).email + else builtins.throw "findEmail: got ${toString len} results instead of 1"; + + # dot-time(7) man page, ported from dotti.me + dot-time = rec { + name = "dot-time"; + section = 7; + content = '' + .Dd $Mdocdate$ + .Dt ${lib.toUpper name} ${toString section} + .Os + .Sh NAME + .Nm ${name} + .Nd a universal convention for conveying time + .Sh DESCRIPTION + For those of us who travel often or coordinate across many timezones, + working with local time is frequently impractical. + ISO8601, in all its wisdom, allows for time zone designators, + but still represents the hours and minutes as local time, + thus making it inconvenient for quickly comparing timestamps from + different locations. + .Pp + Dot time instead uses UTC for all date, hour, and minute indications, + and while it allows for time zone designators, they are optional + information that can be dropped without changing the indicated time. + It uses an alternate hour separator to make it easy to distinguish from + regular ISO8601. + When a time zone designator is provided, one can easily obtain + the matching local time by adding the UTC offset to the UTC time. + .Sh EXAMPLES + These timestamps all represent the same point in time. + .TS + allbox tab(|); + lb | lb | lb + l | l | l. + dot time|ISO8601|RFC3339 + 2019-06-19T22·13-04|2019-06-19T18:13-04|2019-06-19T18:13:00-04:00 + 2019-06-19T22·13+00|2019-06-19T22:13+00|2019-06-19T22:13:00Z + 2019-06-19T22·13+02|2019-06-20T00:13+02|2019-06-20T00:13:00+02:00 + .TE + .Sh SEE ALSO + .Lk https://dotti.me dotti.me + .Sh AUTHORS + .An -nosplit + .Sy dot time + has been proposed and documented by + .An edef Aq Mt ${findEmail "edef"} + and ported to + .Xr mdoc 7 + by + .An sterni Aq Mt ${findEmail "sterni"} . + ''; + }; + +in +depot.nix.buildManPages "dot-time" { } [ + dot-time +] diff --git a/users/sterni/emacs/default.nix b/users/sterni/emacs/default.nix new file mode 100644 index 000000000000..9d057fef6355 --- /dev/null +++ b/users/sterni/emacs/default.nix @@ -0,0 +1,110 @@ +{ depot, pkgs, lib, ... }: + +let + inherit (pkgs.stdenv.hostPlatform) is64bit; + + # Wrap chktex(1) with the flags we want because the chktex flycheck checker + # ignores tex-chktex-extra-flags and has no other way to set flags. I did + # not want to mess around with chktexrc because that seems to involve copying + # around a lot of rules (that would need to be updated?). + # + # Warning 8 is about correct dash length. This is really annoying because it'll + # light up everywhere if you use typographically correct dashes in German text. + chktexLessWarnings = pkgs.writeShellScript "chktex-less-warnings" '' + exec chktex -n8 "$@" + ''; + + emacs = (pkgs.emacsPackagesFor pkgs.emacs29-pgtk).withPackages (epkgs: [ + epkgs.bqn-mode + #epkgs.elpaPackages.ada-mode + epkgs.elpaPackages.rainbow-mode + epkgs.elpaPackages.undo-tree + epkgs.elpaPackages.which-key + epkgs.melpaPackages.adoc-mode + epkgs.melpaPackages.cmake-mode + epkgs.melpaPackages.deft + epkgs.melpaPackages.direnv + epkgs.melpaPackages.dockerfile-mode + epkgs.melpaPackages.editorconfig + epkgs.melpaPackages.elfeed + epkgs.melpaPackages.evil + epkgs.melpaPackages.evil-collection + epkgs.melpaPackages.flycheck + epkgs.melpaPackages.haskell-mode + epkgs.melpaPackages.hl-todo + epkgs.melpaPackages.jq-mode + epkgs.melpaPackages.lsp-haskell + epkgs.melpaPackages.lsp-mode + epkgs.melpaPackages.lsp-ui + epkgs.melpaPackages.magit + epkgs.melpaPackages.markdown-mode + epkgs.melpaPackages.meson-mode + epkgs.melpaPackages.nix-mode + epkgs.melpaPackages.org-clock-csv + epkgs.melpaPackages.paredit + epkgs.melpaPackages.rainbow-delimiters + epkgs.melpaPackages.sly + epkgs.melpaPackages.use-package + epkgs.melpaPackages.yaml-mode + epkgs.rust-mode + epkgs.tvlPackages.tvl + epkgs.urweb-mode + ] ++ lib.optionals is64bit [ + epkgs.melpaPackages.languagetool + ]); + + configDirectory = pkgs.symlinkJoin { + name = "emacs.d"; + paths = [ + ./. + (pkgs.writeTextFile { + name = "injected-emacs.d"; + destination = "/nix-inject.el"; + text = + # Java doesn't seem to be available for non 64bit platforms in nixpkgs + # CBQN doesn't seem to support i686 at least + lib.optionalString is64bit '' + ;; bqn-mode + (setq bqn-interpreter-path "${pkgs.cbqn}/bin/BQN") + + ;; languagetool + (setq languagetool-java-bin "${pkgs.jre}/bin/java" + languagetool-console-command "${pkgs.languagetool}/share/languagetool-commandline.jar" + languagetool-server-command "${pkgs.languagetool}/share/languagetool-server.jar") + '' + '' + + ;; use bash instead of fish from SHELL for some things, as it plays + ;; nicer with TERM=dumb, as I don't need/want vterm anyways. + ;; We want it to source /etc/profile for some extra setup that + ;; kicks in if TERM=dumb, meaning we can't use dash/sh mode. + (setq shell-file-name "${pkgs.bash}/bin/bash" + explicit-bash-args '("-l")) + + ;; chktex wrapper that disables warnings I don't want + (setq flycheck-tex-chktex-executable "${chktexLessWarnings}") + (setq tex-chktex-program "${chktexLessWarnings}") + + (provide 'nix-inject) + ''; + }) + ]; + postBuild = '' + rm "$out/default.nix" + ''; + }; +in + +# sadly we can't give an init-file via the command line +(pkgs.writeShellScriptBin "emacs" '' + exec ${emacs}/bin/emacs \ + --no-init-file \ + --directory ${configDirectory} \ + --eval "(require 'init)" \ + "$@" +'').overrideAttrs (super: { + buildCommand = '' + ${super.buildCommand} + + ln -s "${emacs}/bin/emacsclient" "$out/bin/emacsclient" + ''; +}) diff --git a/users/sterni/emacs/init.el b/users/sterni/emacs/init.el new file mode 100644 index 000000000000..4cb741f62d41 --- /dev/null +++ b/users/sterni/emacs/init.el @@ -0,0 +1,357 @@ +;; set up package infrastructure + +(require 'use-package) +(package-initialize) + +;; Set default font and fallback font via set-fontset-font +(let ((mono-font "Bitstream Vera Sans Mono-12") + (emoji-font "Noto Color Emoji-12")) + (setq default-frame-alist `((font . ,mono-font))) + (set-frame-font mono-font t t) + (set-fontset-font t nil emoji-font)) + +(setq inhibit-startup-message t + display-time-24hr-format t + select-enable-clipboard t) + +;; Reload files +(global-auto-revert-mode 1) + +;; Indent +(setq-default indent-tabs-mode nil) +(setq tab-width 2 + css-indent-offset tab-width) + +;; UTF-8 +(setq locale-coding-system 'utf-8) +(set-terminal-coding-system 'utf-8) +(set-keyboard-coding-system 'utf-8) +(set-selection-coding-system 'utf-8) +(prefer-coding-system 'utf-8) + +;; Disable unnecessary GUI elements +(scroll-bar-mode 0) +(menu-bar-mode 0) +(tool-bar-mode 0) + +(add-hook 'after-make-frame-functions + (lambda (frame) (scroll-bar-mode 0))) + +;; don't center on cursor when scrolling +(setq scroll-conservatively 1) + +;; type less +(defalias 'yes-or-no-p 'y-or-n-p) + +;; Extra settings when graphical session +(when window-system + (setq frame-title-format '(buffer-file-name "%f" ("%b"))) + (mouse-wheel-mode t) + (blink-cursor-mode -1)) + +;; /tmp is a tmpfs, but we may want to recover from power loss +(custom-set-variables + `(temporary-file-directory ,(concat (getenv "HOME") "/.emacs/tmp"))) + +(setq auto-save-file-name-transforms + `((".*" ,temporary-file-directory t))) +(setq backup-directory-alist + `((".*" . ,temporary-file-directory))) +(setq undo-tree-history-directory-alist + `((".*" . ,temporary-file-directory))) +(setq backup-by-copying t) +(setq create-lockfiles nil) + +;; save history +(savehist-mode) +(setq savehist-additional-variables '(search-ring regexp-search-ring magit-cl-history)) + +;; buffers + +;; performance migitations +(global-so-long-mode) + +;; unique component should come first for better completion +(setq uniquify-buffer-name-style 'forward) + +;; completions +(ido-mode 1) +(setq ido-enable-flex-matching t) +(ido-everywhere) +(fido-mode) + +;; Display column numbers +(column-number-mode t) +(setq-default fill-column 80) +(setq display-fill-column-indicator-column t) +(add-hook 'prog-mode-hook #'display-fill-column-indicator-mode) + +;; whitespace +(setq whitespace-style '(face trailing tabs) + whitespace-line-column fill-column) +(add-hook 'prog-mode-hook #'whitespace-mode) +(setq-default indicate-empty-lines t) +(setq-default indicate-buffer-boundaries 'left) +(setq sentence-end-double-space nil) + +;;; Configure built in modes + +;; Perl +(setq perl-indent-level 2) +(setq perl-continued-statement-offset 0) +(setq perl-continued-brace-offset 0) + +;; org mode + +(setq org-clock-persist 'history) +(org-clock-persistence-insinuate) + +(let ((org-folder (concat (getenv "HOME") "/files/sync/org"))) + (setq org-agenda-files (directory-files-recursively org-folder "\\.org$") + org-default-notes-file (concat org-folder "/inbox.org") + initial-buffer-choice org-default-notes-file + org-refile-targets '((org-agenda-files . (:maxlevel . 2))))) + +;; latex + +(defun latex-word-count () + "Calls texcount on the file the current buffer points to and displays the result." + (interactive) + (save-buffer) + (let* ((file (buffer-file-name)) ; needs to happen outside with-current-buffer + (word-count + (with-output-to-string + (with-current-buffer standard-output + (call-process "texcount" nil t nil "-brief" "-utf8" file))))) + (message (string-trim-right word-count)))) + +;; ediff +;; doesn't create new window for ediff controls which I always open accidentally +(setq ediff-window-setup-function 'ediff-setup-windows-plain) + +;; man +(setq Man-notify-method 'pushy) ; display man page in current window + +;; shell + +; default, but allows ';' as prompt +(setq shell-prompt-pattern "^[^#$%>;\n]*[#$%>;] *") + +;; projects (see also evil config) + +(defun project-magit () + "Run magit in the current project dir" + (interactive) + (magit (project-root (project-current t)))) + +(define-key project-prefix-map (kbd "G") 'project-magit) + +(setq project-switch-commands + '((project-find-file "Find file") + (project-find-regexp "Find regexp") + (project-dired "Dired") + (project-shell "Shell") + (project-magit "Magit"))) + +;;; Configure packages +(use-package undo-tree + :config + (global-undo-tree-mode) + (setq undo-tree-auto-save-history t)) + +(use-package magit + :after evil + :config + ; reset (buffer-local) fill-column value to emacs' default + ; gerrit doesn't like 80 column commit messages… + (add-hook 'git-commit-mode-hook (lambda () (setq fill-column 72))) + (evil-define-key 'normal 'global (kbd "<leader>gr") 'magit-status)) +(use-package tvl + :after magit + :custom tvl-depot-path (concat (getenv "HOME") "/src/depot")) + +(setq ediff-split-window-function 'split-window-horizontally) + +(use-package evil + :init + (setq evil-want-integration t) + (setq evil-want-keybinding nil) + (setq evil-shift-width 2) + (setq evil-split-window-below t) + (setq evil-split-window-right t) + (setq evil-undo-system 'undo-tree) + :config + (evil-mode 1) + (evil-set-leader 'normal ",") ;; TODO(sterni): space would be nice, but… + (evil-set-leader 'visual ",") + ;; buffer management + (evil-define-key 'normal 'global (kbd "<leader>bk") 'kill-buffer) + (evil-define-key 'normal 'global (kbd "<leader>bb") 'switch-to-buffer) + (evil-define-key 'normal 'global (kbd "<leader>bo") 'switch-to-buffer-other-window) + (evil-define-key 'normal 'global (kbd "<leader>bl") 'list-buffers) + (evil-define-key 'normal 'global (kbd "<leader>br") 'revert-buffer) + ;; window management: C-w hjkl is annoying in neo + (define-key evil-window-map (kbd "<left>") 'evil-window-left) + (define-key evil-window-map (kbd "<right>") 'evil-window-right) + (define-key evil-window-map (kbd "<up>") 'evil-window-up) + (define-key evil-window-map (kbd "<down>") 'evil-window-down) + ;; projects + (evil-define-key 'normal 'global (kbd "<leader>pf") 'project-find-file) + (evil-define-key 'normal 'global (kbd "<leader>pg") 'project-find-regexp) + (evil-define-key 'normal 'global (kbd "<leader>pd") 'project-dired) + (evil-define-key 'normal 'global (kbd "<leader>ps") 'project-shell) + (evil-define-key 'normal 'global (kbd "<leader>pR") 'project-query-replace-regexp) + (evil-define-key 'normal 'global (kbd "<leader>pK") 'project-kill-buffers) + (evil-define-key 'normal 'global (kbd "<leader>pp") 'project-switch-project) + ;; emacs + (evil-define-key 'visual 'global (kbd "<leader>ee") 'eval-region) + (evil-define-key 'normal 'global (kbd "<leader>ee") 'eval-last-sexp) + (evil-define-key 'normal 'global (kbd "<leader>ep") 'eval-print-last-sexp) + (evil-define-key 'normal 'global (kbd "<leader>eh") 'help) + (evil-define-key 'normal 'global (kbd "<leader>em") 'man) + (evil-define-key '(normal visual) 'global (kbd "<leader>eu") 'browse-url-at-point) + (evil-define-key '(normal visual) 'global (kbd "<leader>ef") 'ffap) + ;; modify what is displayed + (evil-define-key 'normal 'global (kbd "<leader>dw") + (lambda () + (interactive) + (whitespace-mode 'toggle) + (display-fill-column-indicator-mode 'toggle))) + ;; org-mode + (evil-define-key 'normal 'global (kbd "<leader>oa") 'org-agenda) + (evil-define-key 'normal 'global (kbd "<leader>oc") 'org-capture)) + +(use-package evil-collection + :after evil + :config + (evil-collection-init)) + +;; parens +(use-package rainbow-delimiters + :hook ((prog-mode . rainbow-delimiters-mode))) + +(setq show-paren-delay 0) +(show-paren-mode) + +(use-package paredit + :hook ((emacs-lisp-mode . paredit-mode) + (lisp-mode . paredit-mode) + (ielm-mode . paredit-mode) + (lisp-interaction-mode . paredit-mode))) + +(use-package which-key :config (which-key-mode t)) + +(use-package nix-mode :mode "\\.nix\\'") +(use-package nix-drv-mode :mode "\\.drv\\'") + +(use-package direnv + :config (direnv-mode)) + +(use-package editorconfig + :config (editorconfig-mode 1)) + +(use-package haskell-mode) +(use-package flycheck + :init (global-flycheck-mode) + :custom flycheck-keymap-prefix (kbd "<leader>!")) +(use-package lsp-mode + :hook ((haskell-mode . lsp-deferred)) + :commands (lsp lsp-deferred) + :custom + lsp-modeline-code-actions-segments '() ; using lsp-ui-sideline instead + :config + (evil-define-key 'normal 'global + (kbd "<leader>lwr") 'lsp-workspace-restart + (kbd "<leader>lwq") 'lsp-workspace-shutdown + (kbd "<leader>la=") 'lsp-format-buffer + (kbd "<leader>lar") 'lsp-rename + (kbd "<leader>laa") 'lsp-execute-code-action)) +(use-package lsp-ui + :after lsp-mode + :custom + lsp-ui-doc-enable t + lsp-ui-doc-border "DimGray" + lsp-ui-doc-delay 0.5 + :config + (set-face-background 'lsp-ui-doc-background "WhiteSmoke") + (set-face-foreground 'lsp-ui-sideline-code-action "SaddleBrown") + (setq lsp-ui-sideline-code-actions-prefix "🔨 " + lsp-ui-sideline-show-diagnostics nil + lsp-ui-sideline-show-code-actions t) ; is :custom, but won't take effect? + (evil-define-key 'normal lsp-ui-mode-map + ;; TODO(sterni): emulate using xref for non-lsp? + (kbd "<leader>lgr") 'lsp-ui-peek-find-references + (kbd "<leader>lgd") 'lsp-ui-peek-find-definitions + (kbd "<leader>lc") 'lsp-ui-flycheck-list)) +(use-package lsp-haskell + :after lsp-mode + :custom + lsp-haskell-formatting-provider "ormolu") + +(use-package urweb-mode) +(use-package bqn-mode + :mode "\\.bqn\\'" + :custom bqn-mode-map-prefix "C-s-") ; probably rather using C-\ +(use-package yaml-mode) +(use-package dockerfile-mode) +(use-package jq-mode + :config (add-to-list 'auto-mode-alist '("\\.jq\\'" . jq-mode))) +(use-package rust-mode) +(use-package sly + :after evil + :hook ((sly-mrepl-mode . (lambda () + (enable-paredit-mode) + (rainbow-delimiters-mode-enable)))) + :config + (evil-define-key '(normal insert) sly-mrepl-mode-map (kbd "C-r") 'isearch-backward)) + +; TODO(sterni): https://github.com/NixOS/nixpkgs/pull/173893/files +; (use-package ada-mode) + +(use-package rainbow-mode) +(use-package hl-todo + :hook ((prog-mode . hl-todo-mode)) + :config + (setq hl-todo-keyword-faces + '(("TODO" . "#FF0000") + ("FIXME" . "#FF0000") + ("HACK" . "#7f7f7f") + ("XXX" . "#aa0000")))) + +(use-package markdown-mode + :commands (markdown-mode gfm-mode) + :mode (("\\.md\\'" . markdown-mode))) +(use-package adoc-mode + :mode (("\\.adoc\\'" . adoc-mode))) +(use-package languagetool + :after evil + :custom + languagetool-java-arguments '("-Dfile.encoding=UTF-8") + languagetool-default-language "en-GB" + languagetool-mother-tongue "de-DE" + :config + (evil-define-key 'normal 'global (kbd "<leader>mll") 'languagetool-check) + (evil-define-key 'normal 'global (kbd "<leader>mlc") 'languagetool-correct-at-point) + (evil-define-key 'normal 'global (kbd "<leader>mls") 'languagetool-set-language) + (evil-define-key 'normal 'global (kbd "<leader>mlr") 'languagetool-clear-suggestions) + ;; Fill background of issues instead of just underlining to make it easier to read + (set-face-background 'languagetool-issue-default "yellow") + (set-face-background 'languagetool-issue-misspelling "red")) + +(use-package deft + :config + ;; This is based on (car deft-extensions), but unfortunately the variable is + ;; not re-bound in the hook defined by defcustom, so it is always "txt". + (setq deft-default-extension "org") + (evil-define-key 'normal 'global (kbd "<leader>mn") 'deft) + :custom + deft-directory (expand-file-name "~/files/sync/org/notes") + deft-extensions '("org" "md" "txt" "tex")) + +(unless (server-running-p) + (server-start)) + +(require 'subscriptions) ; elfeed config +(require 'nix-inject) + +(provide 'init) diff --git a/users/sterni/emacs/subscriptions.el b/users/sterni/emacs/subscriptions.el new file mode 100644 index 000000000000..ba63da3063fd --- /dev/null +++ b/users/sterni/emacs/subscriptions.el @@ -0,0 +1,71 @@ +;;; elfeed +(use-package elfeed + :after evil + :config + ;; elfeed bindings for evil + (evil-define-key 'normal 'global (kbd "<leader>mf") 'elfeed) + (evil-define-key '(normal visual) elfeed-search-mode-map + (kbd "o") 'elfeed-search-browse-url + (kbd "r") 'elfeed-search-untag-all-unread + (kbd "u") 'elfeed-search-tag-all-unread + (kbd "ff") 'elfeed-search-fetch + (kbd "fc") 'elfeed-db-compact) + ;; elfeed subscriptions + (setq elfeed-feeds + (append + ;; immutable subscriptions tracked in git + '(("https://repology.org/maintainer/sternenseemann%40systemli.org/feed-for-repo/nix_unstable/atom" dashboard releases) + ("https://www.stackage.org/feed" dashboard releases) + ("http://hundimbuero.blogspot.com/feeds/posts/default?alt=rss" blog cool-and-nice) + ("https://text.causal.agency/feed.atom" blog) + ("http://xsteadfastx.org/feed/" blog cool-and-nice) + ("https://tvl.fyi/feed.atom" blog cool-and-nice) + ("https://hannes.robur.coop/atom" blog) + ("https://stevelosh.com/rss.xml" blog) + ("https://blog.benjojo.co.uk/rss.xml" blog) + ("https://leahneukirchen.org/blog/index.atom" blog cool-and-nice) + ("https://leahneukirchen.org/trivium/index.atom" blog links cool-and-nice) + ("https://firefly.nu/feeds/all.atom.xml" blog cool-and-nice) + ("https://tazj.in/feed.atom" blog cool-and-nice) + ("https://alyssa.is/feed.xml" blog cool-and-nice) + ("https://eta.st/feed.xml" blog cool-and-nice) + ("https://spectrum-os.org/git/www/atom/bibliography.html" links blog) + ("https://vulns.xyz/feed.xml" blog) + ("https://www.german-foreign-policy.com/?type=9818" news) + ("https://niedzejkob.p4.team/rss.xml" blog) + ("https://grahamc.com/feed/" blog) + ("http://blog.nullspace.io/feed.xml" blog) + ("https://blog.kingcons.io/rss.xml" blog) + ("https://www.imperialviolet.org/iv-rss.xml" blog) + ("https://22gato.tumblr.com/rss" pictures cool-and-nice) + ("https://theprofoundprogrammer.com/rss" blog) + ("http://shitopenlabsays.tumblr.com/rss" openlab) + ("https://kristaps.bsd.lv/lowdown/atom.xml" releases) + ("http://0pointer.net/blog/index.atom" blog) + ("https://emacsninja.com/feed.atom" blog) + ("https://emacshorrors.com/feed.atom" blog) + ("http://therealmntmn.tumblr.com/rss" blog) + ("http://blog.duangle.com/feeds/posts/default" blog) + ("http://ccc.de/de/rss/updates.xml" news) + ("http://ffaaaffaffaffaa.tumblr.com/rss" pictures) + ("http://fotografiona.tumblr.com/rss" pictures) + ("http://guteaussicht.org/rss" pictures) + ("http://konvergenzfehler.de/feed/" blog) + ("https://markuscisler.com/feed.xml" blog) + ("http://www.plomlompom.de/PlomRogue/plomwiki.php?action=Blog_Atom" blog) + ("http://www.whvrt.de/rss" pictures) + ("https://echtsuppe.wordpress.com/feed/" blog defunct) + ("https://mgsloan.com/feed.xml" blog) + ("http://beza1e1.tuxen.de/blog_en.atom" blog) + ("https://anchor.fm/s/94bb000/podcast/rss" podcast)) + ;; http://www.wollenzin.de/feed/ ;_; + + ;; add more feeds from an untracked file in $HOME + (let ((file (concat (getenv "HOME") + "/.config/emacs-custom/mutable-subscriptions.el"))) + (when (file-exists-p file) + (read (with-temp-buffer + (insert-file-contents file) + (buffer-string)))))))) + +(provide 'subscriptions) diff --git a/users/sterni/exercises/aoc/.gitignore b/users/sterni/exercises/aoc/.gitignore new file mode 100644 index 000000000000..b9720d74516d --- /dev/null +++ b/users/sterni/exercises/aoc/.gitignore @@ -0,0 +1,2 @@ +/*/input +/*/*/input \ No newline at end of file diff --git a/users/sterni/exercises/aoc/2021/default.nix b/users/sterni/exercises/aoc/2021/default.nix new file mode 100644 index 000000000000..d3ed563ec6f8 --- /dev/null +++ b/users/sterni/exercises/aoc/2021/default.nix @@ -0,0 +1,10 @@ +{ depot ? import ../../../../.. { } +, pkgs ? depot.third_party.nixpkgs +, ... +}: + +pkgs.mkShell { + nativeBuildInputs = [ + pkgs.cbqn + ]; +} diff --git a/users/sterni/exercises/aoc/2021/solutions.bqn b/users/sterni/exercises/aoc/2021/solutions.bqn new file mode 100755 index 000000000000..755c9440460a --- /dev/null +++ b/users/sterni/exercises/aoc/2021/solutions.bqn @@ -0,0 +1,484 @@ +#!/usr/bin/env BQN + +⟨Xor⟩ ← •Import "../lib.bqn" + +# +# Utilities +# + +⟨IsAsciiNum,ReadInt,ReadDec,SplitOn,_fix⟩ ← •Import •path∾"/../lib.bqn" + +ReadInput ← {•file.Lines ∾ •path‿"/input/day"‿(•Fmt 𝕩)} + +# +# 2021-12-01 +# + +# part 1 + +day1ExampleInput ← 199‿200‿208‿210‿200‿207‿240‿269‿260‿263 +day1Input ← ReadDec¨ReadInput 1 + +# NB: Because distance from the ground is never smaller than zero, it's +# no problem that nudge inserts a zero at the end of the right list +PositiveDeltaCount ← +´∘(⊢<«)+˝˘∘↕ + +! 7 = 1 PositiveDeltaCount day1ExampleInput + +•Out "Day 1.1: "∾•Fmt 1 PositiveDeltaCount day1Input + +# part 2 + +! 5 = 3 PositiveDeltaCount day1ExampleInput + +•Out "Day 1.2: "∾•Fmt 3 PositiveDeltaCount day1Input + +# +# 2021-12-02 +# + +# part 1 + +day2ExampleInput ← ⟨ + "forward 5", + "down 5", + "forward 8", + "up 3", + "down 8", + "forward 2", +⟩ + +day2Input ← ReadInput 2 + +ParseSubmarineCommand ← (((↕2)⊸((((-1)⊸⋆)∘(2⊸|))×(=⟜(⌊∘(÷⟜2))))∘("duf"⊸⊐)∘⊑)×ReadDec∘(IsAsciiNum/⊢)) + +SubmarineDestProduct ← {×´+´ParseSubmarineCommand¨𝕩} + +! 150 = SubmarineDestProduct day2ExampleInput + +•Out "Day 2.1: "∾•Fmt SubmarineDestProduct day2Input + +# part 2 + +SubmarineAimedDestProduct ← { + ×´+´((×´)∘(1⊸↓)≍(1⊸⊑))¨ (<0‿0‿0) (⊢∾((⊑∘⌽⊣)+(⊑⊢)))` ParseSubmarineCommand¨𝕩 +} + +! 900 = SubmarineAimedDestProduct day2ExampleInput + +•Out "Day 2.2: "∾•Fmt SubmarineAimedDestProduct day2Input + +# +# 2021-12-03 +# + +BinTable ← '0'-˜> + +day3ExampleInput ← BinTable ⟨ + "00100", + "11110", + "10110", + "10111", + "10101", + "01111", + "00111", + "11100", + "10000", + "11001", + "00010", + "01010", +⟩ + +day3Input ← BinTable ReadInput 3 + +DeBinList ← ((2⊸×)+⊣)´⌽ +_tableAggr ← {((÷⟜2)∘(/⟜⥊)´∘⌽∘≢𝔽(+˝))𝕩} +GammaRate ← < _tableAggr + +! 22 = DeBinList GammaRate day3ExampleInput +! 9 = DeBinList ¬GammaRate day3ExampleInput + +•Out "Day 3.1: "∾•Fmt (¬×○DeBinList⊢) GammaRate day3Input + +_lifeSupportRating ← { + # Need to rename the arguments, otherwise the ternary expr becomes a function + bitPos ← 𝕨 + Cmp ← 𝔽 + + crit ← Cmp _tableAggr 𝕩 + matchPos ← bitPos ⊑˘ crit ((⥊˜⟜≢)=⊢) 𝕩 + match ← matchPos/𝕩 + {1=≠match?⊏match;(bitPos+1) Cmp _lifeSupportRating match} +} + +OxygenGeneratorRating ← DeBinList 0 ≤_lifeSupportRating ⊢ +CO2ScrubberRating ← DebinList 0 >_lifeSupportRating ⊢ + +! 23 = OxygenGeneratorRating day3ExampleInput +! 10 = CO2ScrubberRating day3ExampleInput + +•Out "Day 3.2: "∾•Fmt (OxygenGeneratorRating×CO2ScrubberRating) day3Input + +# +# 2021-12-04 +# + +day4Numbers ← ReadDec¨ ',' SplitOn ⊑ReadInput 4 +day4Boards ← ReadDec¨>˘(' '⊸SplitOn¨)> (<⟨⟩) SplitOn 2↓ReadInput 4 + +BoardWins ← {C ← ∨´∘(∧´˘) ⋄ (C∨C∘⍉)𝕩} + +_CallNumber ← {(𝕗∊⥊𝕩) (∨⍟(¬∘BoardWins∘⊢))˘ 𝕨} + +BoardWinScores ← { + 𝕩 (0⊸</×) (⊢-») (+´)∘(BoardWins˘/(+´⥊)˘∘(𝕨⊸×⟜¬))¨ (<0⥊˜≢𝕨) (𝕨 _CallNumber)`𝕩 +} + +day4WinScores ← day4Boards BoardWinScores day4Numbers + +•Out "Day 4.1: "∾•Fmt ⊑day4WinScores +•Out "Day 4.2: "∾•Fmt ⊑⌽day4WinScores + +# +# 2021-12-06 +# + +day6ExampleInput ← ⟨3,4,3,1,2⟩ +day6Input ← ReadDec¨ ',' SplitOn ⊑ReadInput 6 + +LanternfishPopulation ← {+´ (1⊸⌽+(⊑×((6⊸=)∘↕∘≠)))⍟𝕨 9↑≠¨⊔ 𝕩} + +! 26 = 18 LanternfishPopulation day6ExampleInput +! 5934 = 80 LanternfishPopulation day6ExampleInput + +•Out "Day 6.1: "∾•Fmt 80 LanternfishPopulation day6Input +•Out "Day 6.2: "∾•Fmt 256 LanternfishPopulation day6Input + +# +# 2021-12-07 +# + +# part 1 + +day7ExampleInput ← ⟨16,1,2,0,4,2,7,1,2,14⟩ +day7Input ← ReadDec¨ ',' SplitOn ⊑ReadInput 7 + +PossiblePositions ← (⌊´+⟜(↕1⊸+)⌈´) +FuelConsumption ← +˝∘|∘(-⌜) +_lowestFuelPossible ← {⌊´∘(𝔽⟜PossiblePositions)˜ 𝕩} + +! 37 = FuelConsumption _lowestFuelPossible day7ExampleInput + +•Out "Day 7.1: "∾•Fmt FuelConsumption _lowestFuelPossible day7Input + +# part 2 + +TriNum ← 1⊸+×÷⟜2 + +FuelConsumption2 ← +˝∘(TriNum¨)∘|∘(-⌜) + +! 168 = FuelConsumption2 _lowestFuelPossible day7ExampleInput + +•Out "Day 7.2: "∾•Fmt FuelConsumption2 _lowestFuelPossible day7Input + +# +# 2021-12-09 +# + +# part 1 + +ParseHeightMap ← ((≠≍(≠⊑))⥊∾)∘-⟜'0' + +day9ExampleInput ← ParseHeightMap ⟨ + "2199943210", + "3987894921", + "9856789892", + "8767896789", + "9899965678" +⟩ +day9Input ← ParseHeightMap ReadInput 9 + +Rotate ← (⍉⌽)∘⊢⍟⊣ # counter clockwise +LowPoints ← {∧´𝕩⊸(⊣<((-⊢) Rotate ∞⊸»˘∘Rotate˜))¨ ↕4} + +RiskLevelSum ← (+´⥊)∘(1⊸+×LowPoints) + +! 15 = RiskLevelSum day9ExampleInput + +•Out "Day 9.1: "∾•Fmt RiskLevelSum day9Input + +# part 2 + +NumberBasins ← ((1⊸+⊒⌾⥊)×⊢)∘LowPoints +Basins ← {𝕩⊸((<⟜9⊣)∧(«⌈»⌈«˘⌈»˘⌈⊢)∘⊢) _fix NumberBasins 𝕩} +LargestBasinsProduct ← {×´ 3↑ ∨ 1↓ ≠¨ ⊔⥊Basins 𝕩} + +! 1134 = LargestBasinsProduct day9ExampleInput + +•Out "Day 9.2: "∾•Fmt LargestBasinsProduct day9Input + +# +# 2021-12-10 +# + +day10ExampleInput ← ⟨ + "[({(<(())[]>[[{[]{<()<>>", + "[(()[<>])]({[<{<<[]>>(", + "{([(<{}[<>[]}>{[]{[(<()>", + "(((({<>}<{<{<>}{[]{[]{}", + "[[<[([]))<([[{}[[()]]]", + "[{[{({}]{}}([{[{{{}}([]", + "{<[[]]>}<{[{[{[]{()[[[]", + "[<(<(<(<{}))><([]([]()", + "<{([([[(<>()){}]>(<<{{", + "<{([{{}}[<[[[<>{}]]]>[]]", +⟩ +day10Input ← ReadInput 10 + +# part 1 + +opp ← "([{<" +clp ← ")]}>" +SwapParen ← (opp∾⌽clp)⊸((⊑⊐)⊑(⌽⊣)) + +ParenStacks ← ((<⟨⟩)⊸(((⊑∊)⟜clp⊢)◶(∾˜⟜SwapParen)‿(1⊸↓⊣)`)) +LegalParens ← ((1⊸↑)¨∘»∘ParenStacks ((∊⟜opp⊢)∨(≡⟜⋈)¨) ⊢) + +_ScoreFor_ ← {𝕗⊸(𝕘⊸⊐⊏⊣) 𝕩} + +SyntaxScore ← +´∘(0‿3‿57‿1197‿25137 _ScoreFor_ (" "∾clp))∘∾∘(1⊸↑∘(¬∘LegalParens/⊢)¨) + +! 26397 = SyntaxScore day10ExampleInput +•Out "Day 10.1: "∾•Fmt SyntaxScore day10Input + +# part 2 + +AutocompleteScore ← { + Score ← (5⊸×⊸+)˜´∘⌽∘((1+↕4) _ScoreFor_ clp) + # TODO(sterni): we compute ParenStacks twice here + ((⌊÷⟜2)∘≠⊑⊢) ∧ Score∘(⊑⌽)∘ParenStacks¨ (∧´∘LegalParens¨/⊢) 𝕩 +} + +! 288957 = AutocompleteScore day10ExampleInput +•Out "Day 10.2: "∾•Fmt AutocompleteScore day10Input + +# +# 2021-12-11 +# + +day11Input ← '0'-˜> ReadInput 11 +day11ExampleInput ← >⟨ + ⟨5,4,8,3,1,4,3,2,2,3,⟩, + ⟨2,7,4,5,8,5,4,7,1,1,⟩, + ⟨5,2,6,4,5,5,6,1,7,3,⟩, + ⟨6,1,4,1,3,3,6,1,4,6,⟩, + ⟨6,3,5,7,3,8,5,4,7,8,⟩, + ⟨4,1,6,7,5,2,4,6,4,5,⟩, + ⟨2,1,7,6,8,4,1,7,2,1,⟩, + ⟨6,8,8,2,8,8,1,1,3,4,⟩, + ⟨4,8,4,6,8,4,8,5,5,4,⟩, + ⟨5,2,8,3,7,5,1,5,2,6,⟩, +⟩ + +# part 1 + +OctopusFlash ← { + ((⥊⟜0)∘≢𝕊⊢) 𝕩; + flashing ← (¬𝕨)∧9<𝕩 + energy ← ((«˘»)+(»˘«)+(»˘»)+(«˘«)+(»˘)+(«˘)+«+») flashing + ((𝕨∨flashing)⊸𝕊)⍟(0<+´⥊flashing) energy+𝕩 +} + +OctopusStep ← ((9⊸≥)×⊢)∘OctopusFlash∘(1⊸+) +OctopusFlashCount ← {+´⥊0=>(OctopusStep⊣)`(1+𝕨)⥊<𝕩} + +! 1656 = 100 OctopusFlashCount day11ExampleInput +•Out "Day 11.1: "∾•Fmt 100 OctopusFlashCount day11Input + +# part 2 + +_iterCountUntil_ ← { + 0 𝕊 𝕩; + 𝔾◶⟨((𝕨+1)⊸𝕊)∘𝔽, 𝕨˙⟩ 𝕩 +} + +OctopusAllFlashing ← OctopusStep _iterCountUntil_ (∧´∘⥊∘(0⊸=)) + +! 195 = OctopusAllFlashing day11ExampleInput + +•Out "Day 11.2: "∾•Fmt OctopusAllFlashing day11Input + +# +# 2021-12-13 +# + +SplitFoldingInstructions ← ("fold along"⊸(⊣≡≠⊸↑)¨⊔⊢)∘(0⊸(≠⟜≠¨/⊢)) +day13ExampleInput ← SplitFoldingInstructions ⟨ + "6,10", + "0,14", + "9,10", + "0,3", + "10,4", + "4,11", + "6,0", + "6,12", + "4,1", + "0,13", + "10,12", + "3,4", + "3,0", + "8,4", + "1,10", + "2,14", + "8,10", + "9,0", + "", + "fold along y=7", + "fold along x=5", +⟩ +day13Input ← SplitFoldingInstructions ReadInput 13 + +ParseDots ← ReadDec¨∘(','⊸SplitOn)¨ +ParseFolds ← (⊑∘'y'⊸∊≍ReadDec∘(IsAsciiNum/⊢))¨ +day13ExampleDots ← ParseDots ⊑ day13ExampleInput + +# part 1 + +# 𝕨=0 => x, 𝕨=1 => y +# 𝕩 is coordinate to fold around +# 𝕗 is input dot list (see ParseDots) +_Fold ← {⍷∘((𝕩⊸(((2⊸×⊣)-⊢)⌊⊢)∘⊑≍1⊸⊑)¨⌾(⌽¨⍟𝕨)) 𝕗} + +! 17 = ≠ 1 day13ExampleDots _Fold 7 + +day13Dots ← ParseDots ⊑ day13Input +day13Folds ← ParseFolds 1 ⊑ day13Input + +•Out "Day 13.1: "∾•Fmt ≠ (day13Dots _Fold)´ ⊑day13Folds + +# part 2 + +PerformAllFolds ← {𝕩 {(𝕨 _Fold)´𝕩}˜´ ⌽𝕨} +DotMatrix ← { + ⟨width, height⟩ ← 1+⌈˝∘‿2⥊∾𝕩 + {𝕩? '█';' '}¨ height‿width⥊≠¨⊔((⊣+(width⊸×)∘⊢)´)¨ 𝕩 +} + +•Out "Day 13.2:" +•Out •Fmt DotMatrix day13Folds PerformAllFolds day13Dots + +# +# 2021-12-14 +# + +day14Polymer ← ⊑ReadInput 14 +day14Mapping ← 2↓ReadInput 14 + +lp ← (2⊸↑)¨ day14Mapping +le ← ⍷∾lp + +# returns array as long as 𝕨 detailing how many times the element +# at any given index occurs in 𝕩. +Counts ← ((≠⊣)↑(/⁼)∘⊐) + +deltaPairs ← { + addedPairs ← ((-1)⊸⊑¨day14Mapping) (⌽⌾(0⊸⊑))∘(∾¨)¨ lp + removedPairs ← ⋈¨ (2⊸↑)¨ lp + addedPairs (-○(lp⊸Counts))¨ removedPairs +} + +pairCount ← lp Counts ⥊∘(⋈˘) 2↕day14Polymer + +PairInsert ← {𝕩 +´ 𝕩רdeltaPairs} + +pairElementCount ← (le⊸Counts)¨lp + +ElementRarityDiff ← { + ((-1)⊸⊑-⊑)∧ ⌈2÷˜ +´ pairElementCount×PairInsert⍟𝕩 pairCount +} + +•Out "Day 14.1: "∾•Fmt ElementRarityDiff 10 +•Out "Day 14.2: "∾•Fmt ElementRarityDiff 40 + +# +# 2021-12-15 +# + +day15ExampleInput ← >⟨ + 1‿1‿6‿3‿7‿5‿1‿7‿4‿2 + 1‿3‿8‿1‿3‿7‿3‿6‿7‿2 + 2‿1‿3‿6‿5‿1‿1‿3‿2‿8 + 3‿6‿9‿4‿9‿3‿1‿5‿6‿9 + 7‿4‿6‿3‿4‿1‿7‿1‿1‿1 + 1‿3‿1‿9‿1‿2‿8‿1‿3‿7 + 1‿3‿5‿9‿9‿1‿2‿4‿2‿1 + 3‿1‿2‿5‿4‿2‿1‿6‿3‿9 + 1‿2‿9‿3‿1‿3‿8‿5‿2‿1 + 2‿3‿1‿1‿9‿4‿4‿5‿8‿1 +⟩ +day15Input ← '0'-˜ ((≠⋈≠∘⊑)⥊∾)ReadInput 15 + +LowestRiskLevel ← { + start ← 0˙⌾⊑ (⥊⟜∞) ≢𝕩 + ir ← (1⊑≢𝕩)⥊∞ + Step ← {𝕩 ⌊ 𝕨 + (ir⊸«⌊ir⊸»⌊∞⊸«˘⌊∞⊸»˘) 𝕩} + ⊑⌽⥊ 𝕩⊸Step _fix start +} + +! 40 = LowestRiskLevel day15ExampleInput + +•Out "Day 15.1: "∾•Fmt LowestRiskLevel day15Input + +FiveByFiveMap ← {(9⊸|)⌾(-⟜1) ∾(<𝕩)+ +⌜˜↕5} + +! 315 = LowestRiskLevel FiveByFiveMap day15ExampleInput + +•Out "Day 15.2: "∾•Fmt LowestRiskLevel FiveByFiveMap day15Input + +# +# 2021-12-20 +# + +ParsePic ← (⋈⟜0)∘('#'⊸=)∘> + +day20ExampleAlgo ← '#'="..#.#..#####.#.#.#.###.##.....###.##.#..###.####..#####..#....#..#..##..###..######.###...####..#..#####..##..#.#####...##.#.#..#.##..#.#......#.###.######.###.####...#.##.##..#..#..#####.....#.#....###..#.##......#.....#..#..#..##..#...##.######.####.####.#.#...#.......#..#.#.#...####.##.#......#..#...##.#.##..#...##.#.##..###.#......#.#.......#.#.#.####.###.##...#.....####.#..#..#.##.#....##..#.####....##...##..#...#......#.#.......#.......##..####..#...#.#.#...##..#.#..###..#####........#..####......#..#" +day20ExamplePic ← ParsePic ⟨"#..#.", "#....", "##..#", "..#..", "..###"⟩ +day20Input ← ReadInput 20 +day20Algo ← '#'=⊑day20Input +day20Pic ← ParsePic 2↓day20Input + +GrowAxis ← {(⊢ (-1)⊸⌽∘∾ (⥊⟜𝕨)∘(2˙⌾⊑)∘≢) 𝕩} +Grow ← {𝕨 GrowAxis 𝕨 GrowAxis˘ 𝕩} + +Enhance ← { + inf ← 1⊑𝕩 + npic ← ((⊑⟜𝕨)∘DebinList∘⥊)˘˘ 3‿3↕ (inf⊸Grow)⍟2 ⊑𝕩 + ninf ← 𝕨⊑˜511×inf + npic⋈ninf +} +_EnhancedPixelCount ← {+´⥊⊑ (𝕨⊸Enhance)⍟𝕗 𝕩} + +! 35 = day20ExampleAlgo 2 _EnhancedPixelCount day20ExamplePic +! 3351 = day20ExampleAlgo 50 _EnhancedPixelCount day20ExamplePic + +•Out "Day 20.1: "∾•Fmt day20Algo 2 _EnhancedPixelCount day20Pic +•Out "Day 20.2: "∾•Fmt day20algo 50 _EnhancedPixelCount day20Pic + +# +# 2021-12-25 +# + +day25Input ← ".>v" ⊐ > ReadInput 25 +day25ExampleInput ← ".>v"⊐∘‿10⥊"v...>>.vv>.vv>>.vv..>>.>v>...v>>v>>.>.v.v>v.vv.v..>.>>..v....vv..>.>v.v.v..>>v.v....v..v.>" + +MoveHerd ← {(𝕩∧𝕩≠𝕨)+𝕨× (𝕨=𝕩) (Xor⟜(1⊸⌽)∨⊢) (0=𝕩)∧(-1)⌽𝕨=𝕩} + +_fixCount ← { + 1 𝕊 𝕩; + 𝕩 ≡◶⟨(𝕨+1)⊸𝕊, 𝕨˙⟩ 𝔽 𝕩 +} + +MoveAllHerds ← (2⊸MoveHerd)∘(1⊸MoveHerd˘) + +! 58 = MoveAllHerds _fixCount day25ExampleInput +•Out "Day 25.1: "∾•Fmt MoveAllHerds _fixCount day25Input diff --git a/users/sterni/exercises/aoc/2022/.skip-subtree b/users/sterni/exercises/aoc/2022/.skip-subtree new file mode 100644 index 000000000000..39d1894495b3 --- /dev/null +++ b/users/sterni/exercises/aoc/2022/.skip-subtree @@ -0,0 +1 @@ +nix solutions don't use readTree and the rest is non-nix diff --git a/users/sterni/exercises/aoc/2022/01/1.bqn b/users/sterni/exercises/aoc/2022/01/1.bqn new file mode 100644 index 000000000000..022b476aa9a9 --- /dev/null +++ b/users/sterni/exercises/aoc/2022/01/1.bqn @@ -0,0 +1,7 @@ +lib ← •Import •path∾"/../../lib.bqn" +input ← lib.ReadDec¨¨ (<"") lib.SplitOn •FLines •path∾"/input" + +a‿·‿b ← +`3↑∨+´¨ input + +•Out "day01.1: "∾•Fmt a +•Out "day01.2: "∾•Fmt b diff --git a/users/sterni/exercises/aoc/2022/01/1.k b/users/sterni/exercises/aoc/2022/01/1.k new file mode 100644 index 000000000000..42d64dfb6cc1 --- /dev/null +++ b/users/sterni/exercises/aoc/2022/01/1.k @@ -0,0 +1 @@ +(+\e@3#>e:(+/.'1_)'(&0=#'i)_i:0:"input")_1 diff --git a/users/sterni/exercises/aoc/2022/02/2.bqn b/users/sterni/exercises/aoc/2022/02/2.bqn new file mode 100644 index 000000000000..65e3c817bbd2 --- /dev/null +++ b/users/sterni/exercises/aoc/2022/02/2.bqn @@ -0,0 +1,7 @@ +lib ← •Import •path∾"/../../lib.bqn" +i ← 3|"ABCXYZ"⊸⊐¨ ' ' ⊑¨∘lib.SplitOn¨ •FLines •path∾"/input" +S1 ← {1+𝕩+3×3|1+𝕩-𝕨} +S2 ← {𝕨 S1 3|𝕨+𝕩-1} + +•Out "day02.1: "∾•Fmt +´S1´¨i +•Out "day02.2: "∾•Fmt +´S2´¨i diff --git a/users/sterni/exercises/aoc/2022/02/2.k b/users/sterni/exercises/aoc/2022/02/2.k new file mode 100644 index 000000000000..9b6d10058d77 --- /dev/null +++ b/users/sterni/exercises/aoc/2022/02/2.k @@ -0,0 +1 @@ ++/{{y+1+3*3!1+y-x}[x]'y,3!x+y-1}.'3!"ABCXYZ"?(0:"input")_'1 diff --git a/users/sterni/exercises/aoc/2022/03/3.bqn b/users/sterni/exercises/aoc/2022/03/3.bqn new file mode 100644 index 000000000000..642fccd45034 --- /dev/null +++ b/users/sterni/exercises/aoc/2022/03/3.bqn @@ -0,0 +1,8 @@ +i ← •FLines •path∾"/input" +c ← ∾(↕26)⊸+¨"aA" +P ← {1+⊑c⊐⊑𝕩} +S ← (∊/⊣) +G ← ((⊣/(↕÷˜⟜≠))⊔⊢) + +•Out "day03.1: "∾•Fmt +´(P S˝)¨2‿∘⊸⥊¨i +•Out "day03.2: "∾•Fmt +´3(P S´)¨∘G i diff --git a/users/sterni/exercises/aoc/2022/03/3.k b/users/sterni/exercises/aoc/2022/03/3.k new file mode 100644 index 000000000000..3e31f5f32ce2 --- /dev/null +++ b/users/sterni/exercises/aoc/2022/03/3.k @@ -0,0 +1 @@ ++/'(58*r<1)+r:-96+(,/{?y@&~^x?y}/')'((2 0N#)';0N 3#)@\:0:"input" diff --git a/users/sterni/exercises/aoc/2022/04/4.bqn b/users/sterni/exercises/aoc/2022/04/4.bqn new file mode 100644 index 000000000000..0b8f1b4500a9 --- /dev/null +++ b/users/sterni/exercises/aoc/2022/04/4.bqn @@ -0,0 +1,11 @@ +⟨SplitOn, ReadDec⟩ ← •Import "../../lib.bqn" + +Sections ← { + a‿b ← ReadDec¨ (<'-') SplitOn 𝕩 + ↕⌾(-⟜a) 1+b +} +i ← ∘‿2⥊Sections¨ ∾(<',') SplitOn¨ •FLines "input" +Is ← ∊´∘((⍋≠¨)⊏⊢) + +•Out "day04.1: "∾•Fmt +´(∧´Is)˘ i +•Out "day04.2: "∾•Fmt +´(∨´Is)˘ i diff --git a/users/sterni/exercises/aoc/2022/05/5.bqn b/users/sterni/exercises/aoc/2022/05/5.bqn new file mode 100644 index 000000000000..15b0dfc805b5 --- /dev/null +++ b/users/sterni/exercises/aoc/2022/05/5.bqn @@ -0,0 +1,18 @@ +⟨ReadDec, SplitOn, IsAsciiNum⟩ ← •Import "../../lib.bqn" +rs‿rc ← (<"") SplitOn •FLines "../05/input" + +stacks ← { + count ← '0'-˜⊑⌽' ' (≠/⊢) ⊑⌽rs + ' ' (≠/⊢)¨<˘ (count×4) ((»∘(0⊸=)∘(4⊸|)∘↕⊣)/↑) ⍉> (-1)↓rs +} + +cmds ← {0‿1‿1-˜ ReadDec¨ ((∧´IsAsciiNum)¨/⊢) (<' ') SplitOn 𝕩}¨ rc + +_ApplyCmd ← { + s Fn _self c‿f‿t : + m‿k ← 2↑ c ((≤⟜(↕≠))⊔⊢) f⊑s + (Fn m)⊸∾⌾(t⊸⊑) k˙⌾(f⊸⊑) s +} + +•Out "day05.1: "∾⊑¨stacks ⌽_ApplyCmd˜´ ⌽ cmds +•Out "day05.2: "∾⊑¨stacks ⊢_ApplyCmd˜´ ⌽ cmds diff --git a/users/sterni/exercises/aoc/2022/06/6.bqn b/users/sterni/exercises/aoc/2022/06/6.bqn new file mode 100644 index 000000000000..041a2e9100d3 --- /dev/null +++ b/users/sterni/exercises/aoc/2022/06/6.bqn @@ -0,0 +1,4 @@ +i ← ⊑•FLines "input" +FirstMarker ← {𝕩+⊑/(∧´∘¬⊒)˘𝕩↕i} +•Out "day06.1: "∾•Fmt FirstMarker 4 +•Out "day06.2: "∾•Fmt FirstMarker 14 diff --git a/users/sterni/exercises/aoc/2022/06/6.k b/users/sterni/exercises/aoc/2022/06/6.k new file mode 100644 index 000000000000..3dc0de0a3e2d --- /dev/null +++ b/users/sterni/exercises/aoc/2022/06/6.k @@ -0,0 +1 @@ +4 14{x+*&x=#'?'x':y}\:1:"input" diff --git a/users/sterni/exercises/aoc/2022/07/7.bqn b/users/sterni/exercises/aoc/2022/07/7.bqn new file mode 100644 index 000000000000..2fc387f3406d --- /dev/null +++ b/users/sterni/exercises/aoc/2022/07/7.bqn @@ -0,0 +1,24 @@ +lib ← •Import "../../lib.bqn" +cmds ← 1↓ '$' ((+`= ⟜(⊑¨))⊔⊢) •FLines "input" +paths ← (<⟨⟩) { + 𝕨 𝕊 "$ ls": 𝕨; + 𝕨 𝕊 "$ cd /": ⟨⟩; + 𝕨 𝕊 "$ cd ..": (-1)↓𝕨; + 𝕨 𝕊 𝕩: 𝕨∾<5↓𝕩 # "$ cd …" +}` ⊑¨cmds +ParseLs ← { + dirs‿files ← 2↑((lib.IsAsciiNum∘⊑∘⊑)¨⊔⊢) ((<' ')⊸lib.SplitOn)¨ 1↓𝕩 + (1⊑¨dirs)⋈(lib.ReadDec 0⊸⊑)¨files +} +dirlists ← ParseLs⌾(1⊸⊑)¨⥊⋈˘(("$ cd"⊸≢⟜(4⊸↑)∘⊑¨)∘(1⊸⊏)˘/⊢) (⍒≠¨paths)⊏⍉paths≍cmds +DirSize ← {⊑𝕨 (⊑∘(1⊸⊑¨∘⊣⊐⊢)⊑⊣) <𝕩} +DirName ← ∾'/'⊸∾¨ +dirsizes ← ⊑¨ ⟨⟩ { + szs 𝕊 ⟨dir, subdirs‿files⟩: + Canon ← DirName dir⊸∾⟜⋈ + sz ← +´files∾szs⊸DirSize∘Canon¨ subdirs + szs∾<sz⋈DirName dir +}˜´ ⌽dirlists + +•Out "day07.1: "∾•Fmt +´ 100000 (≥/⊢) dirsizes +•Out "day07.2: "∾•Fmt (30000000-70000000-⌈´dirsizes) ⌊´∘(≤/⊢) dirsizes diff --git a/users/sterni/exercises/aoc/2022/08/8.bqn b/users/sterni/exercises/aoc/2022/08/8.bqn new file mode 100644 index 000000000000..91a16d9573c9 --- /dev/null +++ b/users/sterni/exercises/aoc/2022/08/8.bqn @@ -0,0 +1,15 @@ +i ← >'0'-˜•FLines "input" +Visible ← { + _vis ← {(⌈`∘(¯1⊸»˘⌾⍉)<⊢)⌾𝕏 𝕗} + ∨´𝕩 _vis¨ ⟨⊢,⌽,⍉,⌽⍉⟩ +} + +•Out "day08.1: "∾•Fmt +´⥊Visible i + +ViewingDistances ← { + DirView ← {≠1(»⟜(∧`(⊑𝕩)⊸>)/⊢) 1↓𝕩} + _spliceDir ← {! =´≢𝕗 ⋄ 𝕏⁼(⊢↓(⊏⟜(𝕏𝕗))∘⊣)´¨ ⋈⌜˜↕≠𝕗} + ×´ DirView¨¨ 𝕩 _spliceDir¨ ⟨⊢, ⌽˘, ⍉, ⌽˘⍉⟩ +} + +•Out "day08.2: "∾•Fmt ⌈´⥊ViewingDistances i diff --git a/users/sterni/exercises/aoc/2022/09/9.bqn b/users/sterni/exercises/aoc/2022/09/9.bqn new file mode 100644 index 000000000000..fff38b591311 --- /dev/null +++ b/users/sterni/exercises/aoc/2022/09/9.bqn @@ -0,0 +1,17 @@ +⟨SplitOn,ReadDec⟩ ← •Import "../../lib.bqn" +i ← ReadDec⌾(1⊸⊑)¨ (<' ')⊸SplitOn¨ •FLines "input" + +UnitDelta ← (⊢÷(|+0⊸=)) +ExpandStep ← { + 𝕊 "L"‿l: 𝕊 (-l)‿0; + 𝕊 "R"‿r: 𝕊 r‿0; + 𝕊 "U"‿u: 𝕊 0‿u; + 𝕊 "D"‿d: 𝕊 0‿(-d); + 𝕊 delta: ((⌈´|)⥊<∘UnitDelta) delta +} + +Step ← {knots 𝕊 delta: {h 𝕊 t: (UnitDelta h-t) +⍟(1<⌈´|h-t) t}` (delta⊸+)⌾⊑ knots} +Visited ← {+´0=⊒(¯1⊸⊑)¨(<𝕨⥊<0‿0) Step` ∾ExpandStep¨ 𝕩} + +•Out "day09.1: "∾•Fmt 2 Visited i +•Out "day09.2: "∾•Fmt 10 Visited i diff --git a/users/sterni/exercises/aoc/2022/10/10.bqn b/users/sterni/exercises/aoc/2022/10/10.bqn new file mode 100644 index 000000000000..04e3d6a8e563 --- /dev/null +++ b/users/sterni/exercises/aoc/2022/10/10.bqn @@ -0,0 +1,25 @@ +⟨SplitOn,ReadDec⟩ ← •Import "../../lib.bqn" +# Instead of implementing the VM described in the problem, translate the +# program to instructions with equivalent timing for a similar VM that +# only needs 1 cycle for every instruction. +is ← ∾{"noop": <"noop"; 𝕩: (<"noop")∾<ReadDec⌾(1⊸⊑) (<' ') SplitOn 𝕩}¨ •FLines "input" + +Op ← {x 𝕊 "noop": x;x 𝕊 "addx"‿i: x+i} +Draw ← {𝕊 c‿x‿pic: pic∨(↕240)((c-1)⊸=∘⊣∧∊)(⌊⌾(÷⟜40)c)+¯1+x+↕3} +_vm ← { + is _self s: (⊑s)≥≠is? s; + is _self prev‿sum‿x‿pic: + cycle ← prev+1 + is _self ⟨ + cycle, + sum+x×cycle×⊑cycle∊20‿60‿100‿140‿180‿220, + x Op (¯1+cycle)⊑is, + Draw cycle‿x‿pic + ⟩ +} + +·‿sum‿·‿pic ← is _vm 1‿0‿1‿(240⥊0) + +•Out "day10.1: "∾•Fmt sum +•Out "day10.2:" +•Show ".#" ⊏˜ ∘‿40⥊pic diff --git a/users/sterni/exercises/aoc/2022/11/11.bqn b/users/sterni/exercises/aoc/2022/11/11.bqn new file mode 100644 index 000000000000..12b9b5097a59 --- /dev/null +++ b/users/sterni/exercises/aoc/2022/11/11.bqn @@ -0,0 +1,41 @@ +# needs export BQNLIBS=/path/to/mlochbaum/bqn-libs +⟨ReadDec,ImportBqnLibs⟩ ← •Import "../../lib.bqn" +⟨Split⟩ ← ImportBqnLibs "strings.bqn" +MakeOp ← { + 𝕊 a‿"+"‿b: 𝕊 a‿+‿b; + 𝕊 a‿"*"‿b: 𝕊 a‿×‿b; + 𝕊 a‿op‿b: + is‿xs ← (<"old") (≡¨⊔⊢) a‿b + {op´ (𝕩⋆≠xs) ∾ReadDec¨ is} +} +ParseMonkey ← { + ·‿items‿op‿if‿then‿else: + { + initial ⇐ ReadDec¨ ", " Split 18↓items + op ⇐ MakeOp " " Split 19↓op + if ⇐ ReadDec 21↓if + then ⇐ ReadDec 29↓then + else ⇐ ReadDec 30↓else + } +} +monkeys ← ParseMonkey¨ 1↓' '((+`(≠⟜⊑)¨)⊔⊢)0(≠⟜≠¨/⊢)•FLines "input" +items ← {𝕩.initial}¨ monkeys +lim ← ×´{𝕩.if}¨ monkeys + +Sim ← { + div 𝕊 len: + Turn ← { + items 𝕊 turnidx: + i ← (≠monkeys)|turnidx + m ← i⊑monkeys + + worry ← lim|⌊div÷˜ m.Op¨ i⊑items + else‿then ← 2↑0 (=⟜(m.if⊸|)⊔⊢) worry + + ⟨then, else⟩⊸(∾˜¨)⌾(m.then‿m.else⊸⊏) ⟨⟩˙⌾(i⊸⊑) items + } + ×´2↑∨+˝(<items) ((≠⊑)⊸(>((↕⊣)=|)¨)×(≠¨˘)∘>∘(⊣»Turn`)) ↕len×≠items +} + +•Out "day11.1: "∾•Fmt 3 Sim 20 +•Out "day11.2: "∾•Fmt 1 Sim 10000 diff --git a/users/sterni/exercises/aoc/2022/12/12.bqn b/users/sterni/exercises/aoc/2022/12/12.bqn new file mode 100644 index 000000000000..cf42f6f899ca --- /dev/null +++ b/users/sterni/exercises/aoc/2022/12/12.bqn @@ -0,0 +1,16 @@ +⟨ImportBqnLibs,_fix⟩ ← •Import "../../lib.bqn" +⟨ReplaceAll⟩ ← ImportBqnLibs "strings.bqn" +i ← >•FLines "input" + +elevation ← 'a'-˜⟨"S","E"⟩‿⟨"a","z"⟩ ReplaceAll⌾⥊ i +starts ← (⊏⟜∞‿0)¨⟨'S'=i,0=elevation⟩ +end ← 'E'=i + +Step ← { + 𝕊 steps: + Go ← {𝕏⁼((⊢∾¨↕∘≢)(≤⟜(∞⊸»˘∘+⟜1))˜𝕏elevation)⊑>((⥊⟜∞)∘≢⊸⋈)˜∞⊸»˘1+𝕏steps} + steps⌊´Go¨⟨⊢,⌽˘,⍉,⍉⌽⟩ +} +Shortest ← {⊑end/⊸⊏○⥊Step _fix 𝕩} + +•Out¨ "day12.1: "‿"day12.2: "∾¨ •Fmt∘Shortest¨ starts diff --git a/users/sterni/exercises/aoc/2022/13/13.bqn b/users/sterni/exercises/aoc/2022/13/13.bqn new file mode 100644 index 000000000000..0242cc5093a1 --- /dev/null +++ b/users/sterni/exercises/aoc/2022/13/13.bqn @@ -0,0 +1,14 @@ +lib ← •Import "../../lib.bqn" +str ← lib.ImportBqnLibs "strings.bqn" +i ← >⟨"[","]"⟩‿⟨"⟨","⟩"⟩⊸(•BQN str.ReplaceAll)¨¨0((⟨⟩⊸≡¨¯1˙⍟⊣¨(+`(=⟜≠)¨))⊔⊢)•FLines "input" + +Ord ← { + i1 𝕊 i2: 1‿1≡•Type¨ i1‿i2? ¯1‿1‿0⊑˜i1(=+≤)i2; + i1 𝕊 l2: 1‿0≡•Type¨ i1‿l2? l2 Ord˜ ⋈i1; + l1 𝕊 i2: 0‿1≡•Type¨ l1‿i2? l1 Ord ⋈i2; + l1 𝕊 l2: 0‿0≡•Type¨ l1‿l2? + ⊑1↑0(≠/⊢)l1 Ord¨○((l1⌈○≠l2)⊸(↑⌾(+⟜1))) l2 +} + +•Out "day13.1: "∾•Fmt +´1+/(1⊸=Ord´)˘i +•Out "day13.2: "∾•Fmt ×´1‿2++´˘¯1=⟨⟨2⟩⟩‿⟨⟨6⟩⟩Ord⌜⥊i diff --git a/users/sterni/exercises/aoc/2022/15/15.bqn b/users/sterni/exercises/aoc/2022/15/15.bqn new file mode 100644 index 000000000000..e47355856bf8 --- /dev/null +++ b/users/sterni/exercises/aoc/2022/15/15.bqn @@ -0,0 +1,18 @@ +lib ← •Import "../../lib.bqn" + +F ← ¬∘('-'⊸=∨lib.IsAsciiNum) +i ← ⌽˘˘∘‿2‿2⥊lib.ReadDec¨>(0⊸<⟜≠¨/⊢)∘((F ¯1˙⍟⊣¨(+`F))⊔⊢)¨ •FLines "input" + +ssp ← 4000000 + +sds ← (⊏˘∾˘(+´˘(|(-˝))˘)) i + +# _fix is needed to deal with e.g. ⟨0‿15, 5‿8, 12‿23⟩ +MergeRanges ← ((⊑∾⊑∘⌽)∘∧∘∾)¨∘(+`∘((<∞‿∞)⊸»{<´1‿2⊏𝕨∾𝕩}¨⊢)⊔⊢) lib._fix + +Range ← {cky 𝕊 y‿x‿d: x+¯1‿1×d-|cky-y} +RangesY ← {<˘∧𝕩(⊣Range˘({cky 𝕊 y‿·‿d: d≥|y-cky}˘/⊢))sds} +OutRangeY ← {(1<≠)◶⟨0˙,𝕩⊸+∘(ssp⊸×⟜(+⟜1))∘(1⊸⊑)∘∾⟩ MergeRanges ssp⌊0⌈RangesY 𝕩} + +•Out "day15.1: "∾•Fmt +´-˜´¨MergeRanges RangesY 2÷˜ssp +•Out "day15.2: "∾•Fmt +´OutRangeY¨↕ssp diff --git a/users/sterni/exercises/aoc/2022/16/16.k b/users/sterni/exercises/aoc/2022/16/16.k new file mode 100644 index 000000000000..40d5ace60e84 --- /dev/null +++ b/users/sterni/exercises/aoc/2022/16/16.k @@ -0,0 +1,21 @@ +/ parsing +(f;r;t):+{x[1 4],,9_x^'","}'" "\' 0:"input" +(f;t):`s$''(f;t) +r:f!{`I$x[&(x<58)&47<x]}' r +g:f!t + +/ total flow scoring +tf: {+/+\{x,(30-#x)#0}((r.)'x)*`XX=':x} + +/ valves to open +vto: f^(=r).0; + +/ paths to keep after each step +best: {x[(1000&#x)#>tf'x]} + +p:{[n;ps] + ms:ps[&~fin:(#vto)={#?x[&`X=':x]}'ps]; + rt: best[ps[&fin],(ms[w],'(,*|)'ms[w:&{(0|/vto=l)&~|/0&':x=l:*|x}'ms]),,/{x,/:,'g[*|x]}' ms]; + $[n>1;o[n-1;rt];rt]} + +*tf'p[29;,,`AA] diff --git a/users/sterni/exercises/aoc/2022/17/17.bqn b/users/sterni/exercises/aoc/2022/17/17.bqn new file mode 100644 index 000000000000..21b94221aabd --- /dev/null +++ b/users/sterni/exercises/aoc/2022/17/17.bqn @@ -0,0 +1,51 @@ +jets ← '>'= "<>" (∊˜/⊢) •FChars "input" +pieces ← >¨⟨1‿1‿1‿1⟩‿⟨0‿1‿0,1‿1‿1,0‿1‿0⟩‿⟨0‿0‿1,0‿0‿1,1‿1‿1⟩‿⟨⋈1,⋈1,⋈1,⋈1⟩‿⟨1‿1,1‿1⟩ +w ← 7 +initial ← 0‿w⥊0 + +# Warning: mutated global! +ji ← 0 +_try ← {(⊢ 𝕩˙⍟(≠○(+´∘⥊∘∨⟜𝕨)) 𝔽) 𝕩} +Fall ← { + pushed ← 𝕨 ((ji⊑jets)◶«‿»)˘ _try 𝕩 + ji ↩ (≠jets)|ji+1 + fallen ← 𝕨 » _try pushed + 𝕨 𝕊⍟(pushed≢fallen) fallen +} +Height ← ≠∘(∨´˘/⊢) +ThrowPiece ← { + piece ← 𝕩 (|˜⟜≠⊑⊢) pieces + chamber ← (((3+≠piece)⊸+∘⊑∘(1⊸↑)∘⌽∘(1⊸+)∘/∨´˘)↑⊢)⌾⌽𝕨 + falling ← (≠chamber)↑(»⍟2 w⊸↑)˘piece + chamber (⊣∨Fall) falling +} + +•Out "day17.1: "∾•Fmt Height initial ThrowPiece˜´ ⌽↕2022 + +# https://mlochbaum.github.io/BQN/doc/control.html#while +While ← {𝕩{𝔽⍟𝔾∘𝔽_𝕣_𝔾∘𝔽⍟𝔾𝕩}𝕨@}´ +{ + target ← 1000000000000 + ji ↩ 0 ⋄ i ← 0 ⋄ res ← @ + + chamber ← initial + cycles ← ⟨≠pieces,≠jets⟩⥊<⟨⟩ + + While {𝕤⋄res=@}‿{𝕤 + chamber ↩ chamber ThrowPiece i + i +↩ 1 + + t ← i|˜≠pieces + cycles ↩ { + new ← 𝕩∾<i⋈Height chamber + res ↩ {𝕊 𝕩: + ⟨pl,hl⟩‿· ← chk ← ¯2↑new + pd‿hd ← -´⌽chk + @˙⍟(0≠pd|target-pl) hl+hd×pd÷˜target-pl + }⍟(1<≠new) @ + new + }⌾(t‿ji⊸⊑) cycles + } + + •Out "day17.2: "∾•Fmt res +} diff --git a/users/sterni/exercises/aoc/2022/18/18.bqn b/users/sterni/exercises/aoc/2022/18/18.bqn new file mode 100644 index 000000000000..76ec569fed41 --- /dev/null +++ b/users/sterni/exercises/aoc/2022/18/18.bqn @@ -0,0 +1,14 @@ +lib ← •Import "../../lib.bqn" + +i ← (lib.ReadDec¨(<',')⊸lib.SplitOn)¨ •FLines "input" +dim ← 1+⌈´i +cubes ← i∊˜↕dim + +views ← ⟨0‿1‿2, 1‿2‿0, 2‿0‿1⟩ +Exposed ← {(6×+´⥊𝕩)-2×+´views{+´⥊(∧˝˘)2↕𝕨⍉𝕩}¨<𝕩} +Interior ← {(¬𝕩)∧´views{((lib.Xor`∘((∊∧⊢)∨»∘(∊⌾⌽∧⊢)))⎉1)⌾(𝕨⊸⍉)𝕩}¨<𝕩} +Displace ← {⌈´(⥊⊢‿⌽⋈⌜views){F‿a 𝕊 𝕩:((-∘¬∘(»((0⊸=⊣)∧>)⊢)⌈⊢)⎉1)⌾(F a⊸⍉)𝕩}¨<𝕩} +Exterior ← (⊢-○Exposed ¯1⊸=∘(Displace lib._fix)∘(-∘Interior+⊢)) + +•Out "day18.1: "∾•Fmt Exposed cubes +•Out "day18.2: "∾•Fmt Exterior cubes diff --git a/users/sterni/exercises/aoc/2022/20/20.bqn b/users/sterni/exercises/aoc/2022/20/20.bqn new file mode 100644 index 000000000000..8d4c905e8749 --- /dev/null +++ b/users/sterni/exercises/aoc/2022/20/20.bqn @@ -0,0 +1,13 @@ +⟨ReadDec⟩ ← •Import "../../lib.bqn" +enc ← ReadDec¨ •FLines "input" + +CoordSum ← +´∘(1000‿2000‿3000⊸((⊢≠⊸|+⟜(⊑∘(/=⟜0)∘⊢))⊏⊢)) +Mix ← { + M ← {m 𝕊 i: + l ← ≠m + i {n ← (l-1)|(𝕩⊑m)+⊑/𝕩=𝕨 ⋄ (n⊸↑(∾⟜𝕩)⊸∾n⊸↓) 𝕩(≠/⊢)𝕨}˜´ ⌽↕l + } + CoordSum ((⊢M⍟𝕨↕∘≠)⊏⊢) 𝕩 +} +•Out "day20.1: "∾•Fmt 1 Mix enc +•Out "day20.2: "∾•Fmt 10 Mix 811589153×enc diff --git a/users/sterni/exercises/aoc/2022/21/21.bqn b/users/sterni/exercises/aoc/2022/21/21.bqn new file mode 100644 index 000000000000..2f91f55d445e --- /dev/null +++ b/users/sterni/exercises/aoc/2022/21/21.bqn @@ -0,0 +1,25 @@ +⟨ImportBqnLibs, IsAsciiNum, ReadDec⟩ ← •Import "../../lib.bqn" +⟨ReplaceAll, Split⟩ ← ImportBqnLibs "strings.bqn" + +i ← ": "⊸Split¨ •FLines "input" +ReplaceInts ← { + 𝕊 𝕩: 𝕊´ 2↑(¬∘(∧´IsAsciiNum∘⊑∘⌽)¨⊔⊢) 𝕩; + # TODO: Efficient replace on tokens + is 𝕊 es: (((•Fmt⍟(0⊸≠•Type))¨⌾(1⊸⊑) <˘⍉>is)⊸ReplaceAll⌾(1⊸⊑))¨ es +} + +c ← 0 +CanEval ← (IsAsciiNum∨∊⟜"+-/* ") +Eval ← { + a‿s‿b ← " " Split 𝕩 + f ← ⊑+‿-‿×‿÷⊏˜"+-*/"⊐s + a F○ReadDec b +} +EvalExprs ← { + p‿e ← 2↑((∧´CanEval∘⊑∘⌽)¨⊔⊢) 𝕩 + ev ← (Eval⌾(⊑⌽))¨ e + c +↩1 + (⊑(⊑¨ev)∊˜<"root")◶⟨EvalExprs∘(ReplaceInts⟜p),1⊸⊑⊑⟩ ev +} + +•Show EvalExprs ReplaceInts i diff --git a/users/sterni/exercises/aoc/2022/25/25.bqn b/users/sterni/exercises/aoc/2022/25/25.bqn new file mode 100644 index 000000000000..921099141f09 --- /dev/null +++ b/users/sterni/exercises/aoc/2022/25/25.bqn @@ -0,0 +1,4 @@ +c ← "=-012" +F ← +´∘(⊢×(5⊸⋆)∘⌽∘↕∘≠)∘(-⟜2)∘(c⊸⊐) +T ← {c⊏˜5|2+𝕩 {(⌊5÷˜𝕨+2) (𝕊⟜(𝕨⊸∾))⍟(0<𝕨) 𝕩} ⟨⟩} +•Out "day25.1: "∾T +´F¨ •FLines "input" diff --git a/users/sterni/exercises/aoc/2022/25/25.k b/users/sterni/exercises/aoc/2022/25/25.k new file mode 100644 index 000000000000..df956f002f2e --- /dev/null +++ b/users/sterni/exercises/aoc/2022/25/25.k @@ -0,0 +1 @@ +c@2+{(1_o,0)+x+-5*o:2<x}/5\+/{5/x-2}'(c:"=-012")?0:"input" diff --git a/users/sterni/exercises/aoc/2022/README.md b/users/sterni/exercises/aoc/2022/README.md new file mode 100644 index 000000000000..65d51dd21fe7 --- /dev/null +++ b/users/sterni/exercises/aoc/2022/README.md @@ -0,0 +1,8 @@ +# sterni's [Advent of Code 2022](adventofcode.com/2022) + +I'm trying to do it in BQN again to redeem myself for my unfinished [AoC 2021](../2021), +but will allow myself falling back to another language if I get stuck, so I actually +complete this one. +~~I also plan to write additional solutions in Nix (when I have the time) in order to +throw `//tvix/eval` against some new problems.~~ +We'll see how it goes, as my December promises to be quite busy. diff --git a/users/sterni/exercises/aoc/2022/default.nix b/users/sterni/exercises/aoc/2022/default.nix new file mode 100644 index 000000000000..01134d130697 --- /dev/null +++ b/users/sterni/exercises/aoc/2022/default.nix @@ -0,0 +1,53 @@ +{ depot, pkgs, lib, ... }: + +let + inherit (pkgs.buildPackages) cbqn ngn-k; + + # input files are not checked in + meta.ci.skip = true; + + BQNLIBS = pkgs.fetchFromGitHub { + owner = "mlochbaum"; + repo = "bqn-libs"; + rev = "d56d8ea0b8c294fac7274678d9ab112553a03f42"; + sha256 = "1c1bkqj62v8m13jgaa32ridy0fk5iqysq5b2qwxbqxhky5zwnk9h"; + }; +in + +depot.nix.readTree.drvTargets { + shell = pkgs.mkShell { + name = "aoc-2022-shell"; + packages = [ + cbqn + ngn-k + ]; + + inherit BQNLIBS; + }; + + bqn = pkgs.runCommand "bqn-aoc-2022" + { + nativeBuildInputs = [ + cbqn + ]; + + aoc = builtins.path { + name = "bqn-aoc-2022"; + path = ./../.; + # Need lib.bqn from ../ and all inputs as well as bqn files from ./* + filter = path: type: + lib.hasSuffix ".bqn" path || ( + lib.hasPrefix (toString ./.) path + && ( + type == "directory" + || lib.hasSuffix "/input" path + ) + ); + }; + + inherit meta BQNLIBS; + } + '' + find "$aoc/2022" -name '*.bqn' -exec BQN {} \; | tee "$out" + ''; +} diff --git a/users/sterni/exercises/aoc/lib.bqn b/users/sterni/exercises/aoc/lib.bqn new file mode 100644 index 000000000000..e870a5dfa426 --- /dev/null +++ b/users/sterni/exercises/aoc/lib.bqn @@ -0,0 +1,18 @@ +IsAsciiNum ⇐ ('0'⊸≤∧≤⟜'9') +IsAlpha ⇐ (('a'⊸≤∧≤⟜'z')∨('A'⊸≤∧≤⟜'Z')) + +# based on leah2's function +ReadInt ⇐ { + 𝕨 𝕊 𝕩: '-'=⊑𝕩? -𝕨 𝕊 1↓𝕩; + 𝕨 𝕊 𝕩: (𝕨⊸×+⊣)´∘⌽-⟜'0'𝕩 +} +ReadDec ⇐ 10⊸ReadInt + +SplitOn ⇐ ((⊢ (-1˙)⍟⊣¨ +`∘(1⊸»<⊢))∘(≡¨)⊔⊢) +SplitAt ← ((⊣≤↕∘≠∘⊢)⊔⊢) + +_fix ⇐ {𝕩 𝕊∘⊢⍟≢ 𝔽 𝕩} + +ImportBqnLibs ⇐ {•Import 𝕩∾˜"/"∾˜¯1↓1⊑•SH "printenv"‿"BQNLIBS"} + +Xor ⇐ (¬⊸∧∨∧⟜¬) diff --git a/users/sterni/external/flipdot-gschichtler.nix b/users/sterni/external/flipdot-gschichtler.nix new file mode 100644 index 000000000000..58f4fe1e7c8f --- /dev/null +++ b/users/sterni/external/flipdot-gschichtler.nix @@ -0,0 +1,9 @@ +{ pkgs, depot, ... }: + +import depot.users.sterni.external.sources.flipdot-gschichtler { inherit pkgs; } // { + # all targets we care about for depot + meta.ci.targets = [ + "bahnhofshalle" + "warteraum" + ]; +} diff --git a/users/sterni/external/likely-music.nix b/users/sterni/external/likely-music.nix new file mode 100644 index 000000000000..cfb6d120bdd9 --- /dev/null +++ b/users/sterni/external/likely-music.nix @@ -0,0 +1,11 @@ +{ depot, pkgs, ... }: + +import depot.users.sterni.external.sources.likely-music + { + inherit pkgs; + inherit (depot.third_party) napalm; + } // { + meta.ci.targets = [ + "likely-music" + ]; +} diff --git a/users/sterni/external/sources.json b/users/sterni/external/sources.json new file mode 100644 index 000000000000..5233aed23e67 --- /dev/null +++ b/users/sterni/external/sources.json @@ -0,0 +1,26 @@ +{ + "flipdot-gschichtler": { + "branch": "master", + "description": "send text to the flipdots, orderly queued", + "homepage": "https://flipdot.openlab-augsburg.de", + "owner": "openlab-aux", + "repo": "flipdot-gschichtler", + "rev": "93683a7fff04e167963b70a8906f982567646501", + "sha256": "134kgmlv63vzdvc3lr0rys55klmzip7qpfnyzssahihp4mjyyq16", + "type": "tarball", + "url": "https://github.com/openlab-aux/flipdot-gschichtler/archive/93683a7fff04e167963b70a8906f982567646501.tar.gz", + "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz" + }, + "likely-music": { + "branch": "master", + "description": "experimental application for probabilistic music composition", + "homepage": "", + "owner": "sternenseemann", + "repo": "likely-music", + "rev": "c9bef141d846c493a045385ab8146aa28fc8ef33", + "sha256": "1wqgxx8wk7lrvyn9h66gga2wf7dcq7si8wq1w5gfhjnwnsrnvs6y", + "type": "tarball", + "url": "https://github.com/sternenseemann/likely-music/archive/c9bef141d846c493a045385ab8146aa28fc8ef33.tar.gz", + "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz" + } +} diff --git a/users/sterni/external/sources.nix b/users/sterni/external/sources.nix new file mode 100644 index 000000000000..cdcde6da5c54 --- /dev/null +++ b/users/sterni/external/sources.nix @@ -0,0 +1,197 @@ +# This file has been generated by Niv. +_: +let + + # + # The fetchers. fetch_<type> fetches specs of type <type>. + # + + fetch_file = pkgs: name: spec: + let + name' = sanitizeName name + "-src"; + in + if spec.builtin or true then + builtins_fetchurl { inherit (spec) url sha256; name = name'; } + else + pkgs.fetchurl { inherit (spec) url sha256; name = name'; }; + + fetch_tarball = pkgs: name: spec: + let + name' = sanitizeName name + "-src"; + in + if spec.builtin or true then + builtins_fetchTarball { name = name'; inherit (spec) url sha256; } + else + pkgs.fetchzip { name = name'; inherit (spec) url sha256; }; + + fetch_git = name: spec: + let + ref = + if spec ? ref then spec.ref else + if spec ? branch then "refs/heads/${spec.branch}" else + if spec ? tag then "refs/tags/${spec.tag}" else + abort "In git source '${name}': Please specify `ref`, `tag` or `branch`!"; + submodules = if spec ? submodules then spec.submodules else false; + submoduleArg = + let + nixSupportsSubmodules = builtins.compareVersions builtins.nixVersion "2.4" >= 0; + emptyArgWithWarning = + if submodules == true + then + builtins.trace + ( + "The niv input \"${name}\" uses submodules " + + "but your nix's (${builtins.nixVersion}) builtins.fetchGit " + + "does not support them" + ) + { } + else { }; + in + if nixSupportsSubmodules + then { inherit submodules; } + else emptyArgWithWarning; + in + builtins.fetchGit + ({ url = spec.repo; inherit (spec) rev; inherit ref; } // submoduleArg); + + fetch_local = spec: spec.path; + + fetch_builtin-tarball = name: throw + ''[${name}] The niv type "builtin-tarball" is deprecated. You should instead use `builtin = true`. + $ niv modify ${name} -a type=tarball -a builtin=true''; + + fetch_builtin-url = name: throw + ''[${name}] The niv type "builtin-url" will soon be deprecated. You should instead use `builtin = true`. + $ niv modify ${name} -a type=file -a builtin=true''; + + # + # Various helpers + # + + # https://github.com/NixOS/nixpkgs/pull/83241/files#diff-c6f540a4f3bfa4b0e8b6bafd4cd54e8bR695 + sanitizeName = name: + ( + concatMapStrings (s: if builtins.isList s then "-" else s) + ( + builtins.split "[^[:alnum:]+._?=-]+" + ((x: builtins.elemAt (builtins.match "\\.*(.*)" x) 0) name) + ) + ); + + # The set of packages used when specs are fetched using non-builtins. + mkPkgs = sources: system: + let + sourcesNixpkgs = + import (builtins_fetchTarball { inherit (sources.nixpkgs) url sha256; }) { inherit system; }; + hasNixpkgsPath = builtins.any (x: x.prefix == "nixpkgs") builtins.nixPath; + hasThisAsNixpkgsPath = <nixpkgs> == ./.; + in + if builtins.hasAttr "nixpkgs" sources + then sourcesNixpkgs + else if hasNixpkgsPath && ! hasThisAsNixpkgsPath then + import <nixpkgs> { } + else + abort + '' + Please specify either <nixpkgs> (through -I or NIX_PATH=nixpkgs=...) or + add a package called "nixpkgs" to your sources.json. + ''; + + # The actual fetching function. + fetch = pkgs: name: spec: + + if ! builtins.hasAttr "type" spec then + abort "ERROR: niv spec ${name} does not have a 'type' attribute" + else if spec.type == "file" then fetch_file pkgs name spec + else if spec.type == "tarball" then fetch_tarball pkgs name spec + else if spec.type == "git" then fetch_git name spec + else if spec.type == "local" then fetch_local spec + else if spec.type == "builtin-tarball" then fetch_builtin-tarball name + else if spec.type == "builtin-url" then fetch_builtin-url name + else + abort "ERROR: niv spec ${name} has unknown type ${builtins.toJSON spec.type}"; + + # If the environment variable NIV_OVERRIDE_${name} is set, then use + # the path directly as opposed to the fetched source. + replace = name: drv: + let + saneName = stringAsChars (c: if isNull (builtins.match "[a-zA-Z0-9]" c) then "_" else c) name; + ersatz = builtins.getEnv "NIV_OVERRIDE_${saneName}"; + in + if ersatz == "" then drv else + # this turns the string into an actual Nix path (for both absolute and + # relative paths) + if builtins.substring 0 1 ersatz == "/" then /. + ersatz else /. + builtins.getEnv "PWD" + "/${ersatz}"; + + # Ports of functions for older nix versions + + # a Nix version of mapAttrs if the built-in doesn't exist + mapAttrs = builtins.mapAttrs or ( + f: set: with builtins; + listToAttrs (map (attr: { name = attr; value = f attr set.${attr}; }) (attrNames set)) + ); + + # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/lists.nix#L295 + range = first: last: if first > last then [ ] else builtins.genList (n: first + n) (last - first + 1); + + # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L257 + stringToCharacters = s: map (p: builtins.substring p 1 s) (range 0 (builtins.stringLength s - 1)); + + # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L269 + stringAsChars = f: s: concatStrings (map f (stringToCharacters s)); + concatMapStrings = f: list: concatStrings (map f list); + concatStrings = builtins.concatStringsSep ""; + + # https://github.com/NixOS/nixpkgs/blob/8a9f58a375c401b96da862d969f66429def1d118/lib/attrsets.nix#L331 + optionalAttrs = cond: as: if cond then as else { }; + + # fetchTarball version that is compatible between all the versions of Nix + builtins_fetchTarball = { url, name ? null, sha256 }@attrs: + let + inherit (builtins) lessThan nixVersion fetchTarball; + in + if lessThan nixVersion "1.12" then + fetchTarball ({ inherit url; } // (optionalAttrs (!isNull name) { inherit name; })) + else + fetchTarball attrs; + + # fetchurl version that is compatible between all the versions of Nix + builtins_fetchurl = { url, name ? null, sha256 }@attrs: + let + inherit (builtins) lessThan nixVersion fetchurl; + in + if lessThan nixVersion "1.12" then + fetchurl ({ inherit url; } // (optionalAttrs (!isNull name) { inherit name; })) + else + fetchurl attrs; + + # Create the final "sources" from the config + mkSources = config: + mapAttrs + ( + name: spec: + if builtins.hasAttr "outPath" spec + then + abort + "The values in sources.json should not have an 'outPath' attribute" + else + spec // { outPath = replace name (fetch config.pkgs name spec); } + ) + config.sources; + + # The "config" used by the fetchers + mkConfig = + { sourcesFile ? if builtins.pathExists ./sources.json then ./sources.json else null + , sources ? if isNull sourcesFile then { } else builtins.fromJSON (builtins.readFile sourcesFile) + , system ? builtins.currentSystem + , pkgs ? mkPkgs sources system + }: rec { + # The sources, i.e. the attribute set of spec name to spec + inherit sources; + + # The "pkgs" (evaluated nixpkgs) to use for e.g. non-builtin fetchers + inherit pkgs; + }; + +in +mkSources (mkConfig { }) // { __functor = _: settings: mkSources (mkConfig settings); } diff --git a/users/sterni/htmlman/README.md b/users/sterni/htmlman/README.md new file mode 100644 index 000000000000..258233d4c4d2 --- /dev/null +++ b/users/sterni/htmlman/README.md @@ -0,0 +1,36 @@ +# htmlman + +static site generator for man pages intended for +rendering man page documentation viewable using +a web browser. + +## usage + +If you have a nix expression, `doc.nix`, like this: + +```nix +{ depot, ... }: + +depot.users.sterni.htmlman { + title = "foo project"; + pages = [ + { + name = "foo"; + section = 1; + } + { + name = "foo"; + section = 3; + path = ../devman/foo.3; + } + ]; + manDir = ../man; +} +``` + +You can run the following to directly deploy the resulting +documentation output to a specific target directory: + +```sh +nix-build -A deploy doc.nix && ./result target_directory +``` diff --git a/users/sterni/htmlman/default.nix b/users/sterni/htmlman/default.nix new file mode 100644 index 000000000000..6bf21ce2dbfd --- /dev/null +++ b/users/sterni/htmlman/default.nix @@ -0,0 +1,268 @@ +{ depot, lib, pkgs, ... }: + +let + inherit (depot.nix) + getBins + runExecline + yants + ; + + inherit (depot.tools) + cheddar + ; + + inherit (pkgs) + mandoc + coreutils + fetchurl + writers + ; + + bins = getBins cheddar [ "cheddar" ] + // getBins mandoc [ "mandoc" ] + // getBins coreutils [ "cat" "mv" "mkdir" ] + ; + + normalizeDrv = fetchurl { + url = "https://necolas.github.io/normalize.css/8.0.1/normalize.css"; + sha256 = "04jmvybwh2ks4dlnfa70sb3a3z3ig4cv0ya9rizjvm140xq1h22q"; + }; + + execlineStdoutInto = target: line: [ + "redirfd" + "-w" + "1" + target + ] ++ line; + + # I will not write a pure nix markdown renderer + # I will not write a pure nix markdown renderer + # I will not write a pure nix markdown renderer + # I will not write a pure nix markdown renderer + # I will not write a pure nix markdown renderer + markdown = md: + let + html = runExecline.local "rendered-markdown" + { + stdin = md; + } + ([ + "importas" + "-iu" + "out" + "out" + ] ++ execlineStdoutInto "$out" [ + bins.cheddar + "--about-filter" + "description.md" + ]); + in + builtins.readFile html; + + indexTemplate = { title, description, pages ? [ ] }: '' + <!doctype html> + <html> + <head> + <meta charset="utf-8"> + <title>${title}</title> + <link rel="stylesheet" type="text/css" href="style.css"/> + </head> + <body> + <div class="index-text"> + <h1>${title}</h1> + ${markdown description} + <h2>man pages</h2> + <ul> + ${lib.concatMapStrings ({ name, section, ... }: '' + <li><a href="${name}.${toString section}.html">${name}(${toString section})</a></li> + '') pages} + </ul> + </div> + </body> + </html> + ''; + + defaultStyle = import ./defaultStyle.nix { }; + + # This deploy script automatically copies the build result into + # a TARGET directory and marks it as writeable optionally. + # It is exposed as the deploy attribute of the result of + # htmlman, so an htmlman expression can be used like this: + # nix-build -A deploy htmlman.nix && ./result target_dir + deployScript = title: drv: writers.writeDash "deploy-${title}" '' + usage() { + printf 'Usage: %s [-w] TARGET\n\n' "$0" + printf 'Deploy htmlman documentation to TARGET directory.\n\n' + printf ' -h Display this help message\n' + printf ' -w Make TARGET directory writeable\n' + } + + if test "$#" -lt 1; then + usage + exit 100 + fi + + writeable=false + + while test "$#" -gt 0; do + case "$1" in + -h) + usage + exit 0 + ;; + -w) + writeable=true + ;; + -*) + usage + exit 100 + ;; + *) + if test -z "$target"; then + target="$1" + else + echo "Too many arguments" + exit 100 + fi + ;; + esac + + shift + done + + if test -z "$target"; then + echo "Missing TARGET" + usage + exit 100 + fi + + set -ex + + mkdir -p "$target" + cp -RTL --reflink=auto "${drv}" "$target" + + if $writeable; then + chmod -R +w "$target" + fi + ''; + + htmlman = + { title + # title of the index page + , description ? "" + # description which is displayed after + # the main heading on the index page + , pages ? [ ] + # man pages of the following structure: + # { + # name : string; + # section : int; + # path : either path string; + # } + # path is optional, if it is not given, + # the man page source must be located at + # "${manDir}/${name}.${toString section}" + , manDir ? null + # directory in which man page sources are located + , style ? defaultStyle + # CSS to use as a string + , normalizeCss ? true + # whether to include normalize.css before the custom CSS + , linkXr ? "all" + # How to handle cross references in the html output: + # + # * none: don't convert cross references into hyperlinks + # * all: link all cross references as if they were + # rendered into $out by htmlman + # * inManDir: link to all man pages which have their source + # in `manDir` and use the format string defined + # in linkXrFallback for all other cross references. + , linkXrFallback ? "https://manpages.debian.org/unstable/%N.%S.en.html" + # fallback link to use if linkXr == "inManDir" and the man + # page is not in ${manDir}. Placeholders %N (name of page) + # and %S (section of page) can be used. See mandoc(1) for + # more information. + }: + + let + linkXrEnum = yants.enum "linkXr" [ "all" "inManDir" "none" ]; + + index = indexTemplate { + inherit title description pages; + }; + + resolvePath = { path ? null, name, section }: + if path != null + then path + else "${manDir}/${name}.${toString section}"; + + mandocOpts = lib.concatStringsSep "," ([ + "style=style.css" + ] ++ linkXrEnum.match linkXr { + all = [ "man=./%N.%S.html" ]; + inManDir = [ "man=./%N.%S.html;${linkXrFallback}" ]; + none = [ ]; + }); + + html = + runExecline.local "htmlman-${title}" + { + derivationArgs = { + inherit index style; + passAsFile = [ "index" "style" ]; + }; + } + ([ + "multisubstitute" + [ + "importas" + "-iu" + "out" + "out" + "importas" + "-iu" + "index" + "indexPath" + "importas" + "-iu" + "style" + "stylePath" + ] + "if" + [ bins.mkdir "-p" "$out" ] + "if" + [ bins.mv "$index" "\${out}/index.html" ] + "if" + (execlineStdoutInto "\${out}/style.css" [ + "if" + ([ + bins.cat + ] ++ lib.optional normalizeCss normalizeDrv + ++ [ + "$style" + ]) + ]) + # let mandoc check for available man pages + "execline-cd" + "${manDir}" + ] ++ lib.concatMap + ({ name, section, ... }@p: + execlineStdoutInto "\${out}/${name}.${toString section}.html" [ + "if" + [ + bins.mandoc + "-mdoc" + "-T" + "html" + "-O" + mandocOpts + (resolvePath p) + ] + ]) + pages); + in + html // { + deploy = deployScript title html; + }; +in +htmlman diff --git a/users/sterni/htmlman/defaultStyle.nix b/users/sterni/htmlman/defaultStyle.nix new file mode 100644 index 000000000000..a44b5ef06934 --- /dev/null +++ b/users/sterni/htmlman/defaultStyle.nix @@ -0,0 +1,49 @@ +{ ... }: + +'' + body { + font-size: 1em; + line-height: 1.5; + font-family: serif; + background-color: #efefef; + } + + h1, h2, h3, h4, h5, h6 { + font-family: sans-serif; + font-size: 1em; + margin: 5px 0; + } + + h1 { + margin-top: 0; + } + + a:link, a:visited { + color: #3e7eff; + } + + h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { + text-decoration: none; + } + + .manual-text, .index-text { + padding: 20px; + max-width: 800px; + background-color: white; + margin: 0 auto; + } + + table.head, table.foot { + display: none; + } + + .Nd { + display: inline; + } + + /* use same as cheddar for man pages */ + pre { + padding: 16px; + background-color: #f6f8fa; + } +'' diff --git a/users/sterni/keys.nix b/users/sterni/keys.nix new file mode 100644 index 000000000000..0a422bc0d136 --- /dev/null +++ b/users/sterni/keys.nix @@ -0,0 +1,8 @@ +{ ... }: + +{ + all = [ + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJk+KvgvI2oJTppMASNUfMcMkA2G5ZNt+HnWDzaXKLlo lukas@wolfgang" + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIZTrefeqOlXDz7nnDWw820+29vLgn6R3o4N1G3lRWrr lukas@ludwig" + ]; +} diff --git a/users/sterni/lv/gopher/default.nix b/users/sterni/lv/gopher/default.nix new file mode 100644 index 000000000000..f8f42c82d56f --- /dev/null +++ b/users/sterni/lv/gopher/default.nix @@ -0,0 +1,8 @@ +{ depot, ... }: + +depot.users.sterni.nix.build.buildGopherHole { + name = "gopher-sterni.lv"; + dir = [ + "🚧 closed for construction 🚧" + ]; +} diff --git a/users/sterni/machines/.skip-subtree b/users/sterni/machines/.skip-subtree new file mode 100644 index 000000000000..a79762853edd --- /dev/null +++ b/users/sterni/machines/.skip-subtree @@ -0,0 +1 @@ +Subdirectories are manually reexposed by default.nix as the contain NixOS modules diff --git a/users/sterni/machines/default.nix b/users/sterni/machines/default.nix new file mode 100644 index 000000000000..291d9756c7e3 --- /dev/null +++ b/users/sterni/machines/default.nix @@ -0,0 +1,81 @@ +{ depot, lib, pkgs, ... }: + +let + bins = depot.nix.getBins pkgs.nq [ "fq" "nq" ]; + + machines = lib.mapAttrs + (name: _: + depot.ops.nixos.nixosFor (import (./. + ("/" + name))) + ) + (lib.filterAttrs (_: type: type == "directory") (builtins.readDir ./.)); + + # TODO(sterni): share code with rebuild-system + localDeployScriptFor = { system, config, ... }: + pkgs.writeShellScript "local-deploy-${system.name}" '' + set -eu + if [[ "$(hostname)" != "${config.networking.hostName}" ]]; then + echo "$0: unexpected hostname: $(hostname). Are you deploying on the right machine?" + exit 1 + fi + nix-env -p /nix/var/nix/profiles/system --set "${system}" + "${system}/bin/switch-to-configuration" switch + ''; + + # Builds the system on the remote machine + deployScriptFor = { system, ... }@machine: + pkgs.writeShellScript "remote-deploy-${system.name}" '' + set -eu + + if [ $# != 1 ]; then + printf 'usage: %s [USER@]HOST' "$0" + exit 100 + fi + + readonly TARGET_HOST="$1" + readonly DEPLOY_DRV="${ + builtins.unsafeDiscardOutputDependency ( + # Wrapper script around localDeployScriptFor that merely starts the + # local deploy script using and nq and then waits using fq. This means + # we can't Ctrl-C the deploy and it won't be terminated by a lost + # connection. + pkgs.writeShellScript "queue-deploy-${system.name}" '' + readonly STATE_DIR="''${XDG_STATE_HOME:-$HOME/.local/state}/sterni-deploy" + mkdir -p "$STATE_DIR" + + export NQDIR="$STATE_DIR" + + "${bins.nq}" "${localDeployScriptFor machine}" + "${bins.fq}" + '' + ).drvPath + }" + + nix-copy-closure -s --gzip --to "$TARGET_HOST" "$DEPLOY_DRV" + + readonly DEPLOY_OUT="$(ssh "$TARGET_HOST" "nix-store -r '$DEPLOY_DRV'")" + + ssh "$TARGET_HOST" "$DEPLOY_OUT" + ''; + +in + +depot.nix.readTree.drvTargets ( + # this somehow becomes necessarily ugly with nixpkgs-fmt + machines // { inherit deployScriptFor; } // + + lib.mapAttrs' + (name: _: { + name = "${name}System"; + value = machines.${name}.system; + }) + machines + + // + + lib.mapAttrs' + (name: _: { + name = "${name}Deploy"; + value = deployScriptFor machines.${name}; + }) + machines +) diff --git a/users/sterni/machines/edwin/default.nix b/users/sterni/machines/edwin/default.nix new file mode 100644 index 000000000000..68f20787a9bf --- /dev/null +++ b/users/sterni/machines/edwin/default.nix @@ -0,0 +1,19 @@ +{ config, lib, pkgs, depot, ... }: + +{ + imports = [ + # Third party modules we use + "${depot.third_party.agenix.src}/modules/age.nix" + # Basic settings + ../../modules/common.nix + # These modules touch things related to booting (filesystems, initrd network…) + ./hardware.nix + ./network.nix + # These modules configure services, websites etc. + (depot.path.origSrc + "/ops/modules/btrfs-auto-scrub.nix") + ]; + + config = { + system.stateVersion = "20.09"; + }; +} diff --git a/users/sterni/machines/edwin/hardware.nix b/users/sterni/machines/edwin/hardware.nix new file mode 100644 index 000000000000..0e33de753aeb --- /dev/null +++ b/users/sterni/machines/edwin/hardware.nix @@ -0,0 +1,63 @@ +{ config, lib, pkgs, depot, ... }: + +{ + config = { + boot = { + loader.grub = { + enable = true; + # TODO(sterni): use /dev/disk/by-id ? + devices = [ + "/dev/sda" + "/dev/sdb" + ]; + }; + + kernelModules = [ + "kvm-intel" + ]; + + initrd.availableKernelModules = [ + "ahci" + "sd_mod" + "btrfs" + "realtek" + "r8169" + ]; + }; + + boot.initrd.luks.devices = { + "crypt1".device = "/dev/disk/by-uuid/02ac34ee-be10-401b-90c2-1c6aa54c4d5f"; + "crypt2".device = "/dev/disk/by-uuid/7ce07191-e704-4aed-a60f-dfa3ce386b26"; + "crypt-swap1".device = "/dev/disk/by-uuid/fec7155c-6a65-4f25-b271-43763e4c31eb"; + "crypt-swap2".device = "/dev/disk/by-uuid/7b0a03fc-51de-4578-9811-94b00df09d88"; + }; + + fileSystems = { + "/" = { + device = "/dev/disk/by-label/root"; + fsType = "btrfs"; + }; + + "/boot" = { + device = "/dev/disk/by-label/boot"; + fsType = "btrfs"; + }; + }; + + swapDevices = [ + { device = "/dev/disk/by-label/swap1"; } + { device = "/dev/disk/by-label/swap2"; } + ]; + + powerManagement.cpuFreqGovernor = "performance"; + hardware = { + enableRedistributableFirmware = true; + cpu.intel.updateMicrocode = true; + }; + + nix.settings = { + max-jobs = 2; + cores = 4; + }; + }; +} diff --git a/users/sterni/machines/edwin/network.nix b/users/sterni/machines/edwin/network.nix new file mode 100644 index 000000000000..1e3d4e76f078 --- /dev/null +++ b/users/sterni/machines/edwin/network.nix @@ -0,0 +1,62 @@ +{ config, pkgs, lib, depot, ... }: + +let + ipv6 = "2a01:4f8:151:54d0::/64"; + + ipv4 = "176.9.107.207"; + gatewayv4 = "176.9.107.193"; + netmaskv4 = "255.255.255.224"; +in + +{ + config = { + boot = { + kernelParams = [ + "ip=${ipv4}::${gatewayv4}:${netmaskv4}::eth0:none" + ]; + + initrd.network = { + enable = true; + ssh = { + enable = true; + authorizedKeys = depot.users.sterni.keys.all; + hostKeys = [ + "/etc/nixos/unlock_rsa_key_openssh" + "/etc/nixos/unlock_ed25519_key_openssh" + ]; + }; + postCommands = '' + echo 'cryptsetup-askpass' >> /root/.profile + ''; + }; + }; + + networking = { + usePredictableInterfaceNames = false; + useDHCP = false; + interfaces."eth0".useDHCP = false; + + hostName = "edwin"; + + firewall = { + enable = true; + allowPing = true; + allowedTCPPorts = [ 22 80 443 ]; + }; + }; + + systemd.network = { + enable = true; + networks."eth0".extraConfig = '' + [Match] + Name = eth0 + + [Network] + Address = ${ipv6} + Gateway = fe80::1 + Address = ${ipv4}/27 + Gateway = ${gatewayv4} + ''; + }; + }; +} diff --git a/users/sterni/machines/ingeborg/default.nix b/users/sterni/machines/ingeborg/default.nix new file mode 100644 index 000000000000..2d026ae05bc7 --- /dev/null +++ b/users/sterni/machines/ingeborg/default.nix @@ -0,0 +1,33 @@ +{ config, lib, pkgs, depot, ... }: + +{ + imports = [ + # Third party modules + "${depot.third_party.agenix.src}/modules/age.nix" + # Basic settings + ../../modules/common.nix + # These modules touch things related to booting (filesystems, initrd network…) + ./hardware.nix + ./network.nix + # (More or less) pluggable service configuration + (depot.path.origSrc + "/ops/modules/btrfs-auto-scrub.nix") + ./monitoring.nix + ./minecraft.nix + ./http/sterni.lv.nix + ./http/code.sterni.lv.nix + ./http/flipdot.openlab-augsburg.de.nix + ./tv.nix + ./quassel.nix + + # Inactive: + # ./http/likely-music.sterni.lv.nix + # ./gopher.nix + + # TODO(sterni): fail2ban + # TODO(sterni): automatic backups for full recovery + ]; + + config = { + system.stateVersion = "24.05"; + }; +} diff --git a/users/sterni/machines/ingeborg/gopher.nix b/users/sterni/machines/ingeborg/gopher.nix new file mode 100644 index 000000000000..57275e13a55a --- /dev/null +++ b/users/sterni/machines/ingeborg/gopher.nix @@ -0,0 +1,19 @@ +{ depot, ... }: + +{ + config = { + services.spacecookie = { + enable = true; + openFirewall = true; + settings = { + hostname = "sterni.lv"; + root = depot.users.sterni.lv.gopher; + log = { + enable = true; + hide-ips = true; + hide-time = true; + }; + }; + }; + }; +} diff --git a/users/sterni/machines/ingeborg/hardware.nix b/users/sterni/machines/ingeborg/hardware.nix new file mode 100644 index 000000000000..982598131eb6 --- /dev/null +++ b/users/sterni/machines/ingeborg/hardware.nix @@ -0,0 +1,76 @@ +{ config, lib, pkgs, depot, ... }: + +{ + # Booting / Kernel + boot = { + loader.grub = { + enable = true; + devices = [ + "/dev/disk/by-id/wwn-0x5000c500a4859731" + "/dev/disk/by-id/wwn-0x5000c500a485c1b5" + ]; + }; + + initrd = { + availableKernelModules = [ + "ahci" + "btrfs" + "sd_mod" + "xhci_pci" + "e1000e" + ]; + kernelModules = [ + "dm-snapshot" + ]; + }; + + swraid = { + enable = true; + mdadmConf = '' + ARRAY /dev/md/boot-raid metadata=1.2 name=nixos:boot-raid UUID=13007b9d:ab7a1129:c45ec40f:3c9f2111 + ARRAY /dev/md/encrypted-container-raid metadata=1.2 name=nixos:encrypted-container-raid UUID=38dfa683:a6d30690:32a5de6f:fb7980fe + ''; + }; + + kernelModules = [ + "kvm-intel" + ]; + }; + + # Filesystems + services.lvm.enable = true; + + boot.initrd.luks.devices."container" = { + device = "/dev/md/encrypted-container-raid"; + preLVM = true; + }; + + fileSystems = { + "/" = { + device = "/dev/mainvg/root"; + fsType = "btrfs"; + }; + + "/boot" = { + device = "/dev/disk/by-label/boot"; + fsType = "ext4"; + }; + }; + + swapDevices = [ + { device = "/dev/mainvg/swap"; } + ]; + + # CPU + hardware = { + cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware; + enableRedistributableFirmware = true; + }; + + nix.settings = { + max-jobs = 2; + cores = 4; + }; + + powerManagement.cpuFreqGovernor = "performance"; +} diff --git a/users/sterni/machines/ingeborg/http/code.sterni.lv.nix b/users/sterni/machines/ingeborg/http/code.sterni.lv.nix new file mode 100644 index 000000000000..fd4975ed1d59 --- /dev/null +++ b/users/sterni/machines/ingeborg/http/code.sterni.lv.nix @@ -0,0 +1,263 @@ +{ depot, pkgs, lib, config, ... }: + +let + virtualHost = "code.sterni.lv"; + + repoSections = [ + { + section = "active"; + repos = { + spacecookie = { + description = "gopher server (and library for Haskell)"; + upstream = "https://github.com/sternenseemann/spacecookie.git"; + }; + "mirror/depot" = { + description = "monorepo for the virus lounge"; + upstream = "https://code.tvl.fyi/depot.git"; + cgit.defbranch = "canon"; + }; + "mirror/flipdot-gschichtler" = { + description = "message queue system for OpenLab's flipdot display"; + upstream = "https://github.com/openlab-aux/flipdot-gschichtler.git"; + }; + "mirror/nixpkgs" = { + description = "Nix packages collection"; + upstream = "https://github.com/nixos/nixpkgs.git"; + cgit.enable-commit-graph = "0"; # too slow + }; + "mirror/vuizvui" = { + description = "Nix(OS) expressions used by the OpenLab and its members"; + upstream = "https://github.com/openlab-aux/vuizvui.git"; + }; + }; + } + { + section = "poc"; + repos = { + emoji-generic = { + description = "generic emoji library for Haskell"; + upstream = "https://github.com/sternenseemann/emoji-generic.git"; + }; + grav2ty = { + description = "“realistic” 2d space game"; + upstream = "https://github.com/sternenseemann/grav2ty.git"; + }; + haskell-dot-time = { + description = "UTC-centric time library for haskell with dot time support"; + cgit.defbranch = "main"; + }; + buchstabensuppe = { + description = "toy font rendering for low pixelcount, high contrast displays"; + upstream = "https://github.com/sternenseemann/buchstabensuppe.git"; + cgit.defbranch = "main"; + }; + "mirror/saneterm" = { + description = "modern line-oriented terminal emulator without support for TUIs"; + upstream = "https://git.8pit.net/saneterm.git"; + }; + }; + } + { + # TODO(sterni): resisort, klammeraffe, cl-ca, ponify, tinyrl + section = "archive"; + repos = { + gopher-proxy = { + description = "Gopher over HTTP proxy"; + upstream = "https://github.com/sternenseemann/gopher-proxy.git"; + }; + likely-music = { + description = "experimental application for probabilistic music composition"; + upstream = "https://github.com/sternenseemann/likely-music.git"; + }; + logbook = { + description = "file format for keeping a personal log"; + upstream = "https://github.com/sternenseemann/logbook.git"; + }; + sternenblog = { + description = "file based cgi blog software"; + upstream = "https://github.com/sternenseemann/sternenblog.git"; + }; + }; + } + ]; + + repoPath = name: repo: repo.path or "/srv/git/${name}.git"; + + cgitRepoEntry = name: repo: + lib.concatStringsSep "\n" ( + [ + "repo.url=${name}" + "repo.path=${repoPath name repo}" + ] + ++ lib.optional (repo ? description) "repo.desc=${repo.description}" + ++ lib.mapAttrsToList (n: v: "repo.${n}=${v}") repo.cgit or { } + ); + + cgitHead = pkgs.writeText "cgit-head.html" '' + <style> + #summary { + max-width: 80em; + } + + #summary * { + max-width: 100%; + } + </style> + ''; + + cgitConfig = pkgs.writeText "cgitrc" '' + virtual-root=/ + + enable-http-clone=1 + clone-url=https://${virtualHost}/$CGIT_REPO_URL + + enable-blame=1 + enable-log-filecount=1 + enable-log-linecount=1 + enable-index-owner=0 + enable-blame=1 + enable-commit-graph=1 + + root-title=code.sterni.lv + css=/cgit.css + head-include=${cgitHead} + + mimetype-file=${pkgs.mime-types}/etc/mime.types + + about-filter=${depot.tools.cheddar.about-filter}/bin/cheddar-about + source-filter=${depot.tools.cheddar}/bin/cheddar + readme=:README.md + readme=:readme.md + + section-sort=0 + ${ + lib.concatMapStringsSep "\n" (section: + '' + section=${section.section} + + '' + + builtins.concatStringsSep "\n\n" (lib.mapAttrsToList cgitRepoEntry section.repos) + ) repoSections + } + ''; + + /* Merge a list of attrs, but fail when the same attribute occurs twice. + + Type: [ attrs ] -> attrs + */ + mergeManyDistinctAttrs = lib.foldAttrs + ( + val: nul: + if nul == null then val else throw "Every attribute name may occur only once" + ) + null; + + flatRepos = mergeManyDistinctAttrs + (builtins.map (section: section.repos) repoSections); + + reposToMirror = lib.filterAttrs (_: repo: repo ? upstream) flatRepos; + + # User and group name used for running the mirror scripts + mirroredReposOwner = "git"; + + # Make repo name suitable for systemd unit/timer + unitName = name: "mirror-${lib.strings.sanitizeDerivationName name}"; +in + +{ + imports = [ + ./nginx.nix + ./fcgiwrap.nix + ]; + + config = { + services.nginx.virtualHosts."${virtualHost}" = { + enableACME = true; + forceSSL = true; + root = "${pkgs.cgit-pink}/cgit/"; + extraConfig = '' + try_files $uri @cgit; + + location @cgit { + include ${pkgs.nginx}/conf/fastcgi_params; + fastcgi_param SCRIPT_FILENAME ${pkgs.cgit-pink}/cgit/cgit.cgi; + fastcgi_param PATH_INFO $uri; + fastcgi_param QUERY_STRING $args; + fastcgi_param HTTP_HOST $server_name; + fastcgi_param CGIT_CONFIG ${cgitConfig}; + fastcgi_pass unix:${toString config.services.fcgiwrap.socketAddress}; + } + ''; + }; + + users = { + users.${mirroredReposOwner} = { + group = mirroredReposOwner; + isSystemUser = true; + }; + + groups.${mirroredReposOwner} = { }; + }; + + + systemd.timers = lib.mapAttrs' + ( + name: repo: + { + name = unitName name; + value = { + description = "regularly update mirror git repository ${name}"; + wantedBy = [ "timers.target" ]; + enable = true; + timerConfig = { + # Fire every 6h and distribute the workload over next 6h randomly + OnCalendar = "*-*-* 00/6:00:00"; + RandomizedDelaySec = "6h"; + Persistent = true; + }; + }; + } + ) + reposToMirror; + + systemd.services = lib.mapAttrs' + ( + name: repo: + { + name = unitName name; + value = { + description = "mirror git repository ${name}"; + requires = [ "network-online.target" ]; + after = [ "network-online.target" ]; + + script = + let + path = repoPath name repo; + in + '' + set -euo pipefail + + export PATH="${lib.makeBinPath [ pkgs.coreutils pkgs.git ]}" + + if test ! -d "${path}"; then + mkdir -p "$(dirname "${path}")" + git clone --mirror "${repo.upstream}" "${path}" + exit 0 + fi + + cd "${path}" + + git fetch "${repo.upstream}" '+refs/*:refs/*' --prune + ''; + + serviceConfig = { + Type = "oneshot"; + User = mirroredReposOwner; + Group = mirroredReposOwner; + }; + }; + } + ) + reposToMirror; + }; +} diff --git a/users/sterni/machines/ingeborg/http/fcgiwrap.nix b/users/sterni/machines/ingeborg/http/fcgiwrap.nix new file mode 100644 index 000000000000..19696d85d413 --- /dev/null +++ b/users/sterni/machines/ingeborg/http/fcgiwrap.nix @@ -0,0 +1,15 @@ +{ ... }: + +{ + imports = [ + ./nginx.nix + ]; + + config.services.fcgiwrap = { + enable = true; + socketType = "unix"; + socketAddress = "/run/fcgiwrap.sock"; + user = "http"; + group = "http"; + }; +} diff --git a/users/sterni/machines/ingeborg/http/flipdot.openlab-augsburg.de.nix b/users/sterni/machines/ingeborg/http/flipdot.openlab-augsburg.de.nix new file mode 100644 index 000000000000..c86956a0a473 --- /dev/null +++ b/users/sterni/machines/ingeborg/http/flipdot.openlab-augsburg.de.nix @@ -0,0 +1,36 @@ +{ depot, lib, config, ... }: + +let + inherit (depot.users.sterni.external.flipdot-gschichtler) + bahnhofshalle + warteraum + nixosModule + ; +in + +{ + imports = [ + nixosModule + ./nginx.nix + ]; + + config = { + age.secrets = lib.genAttrs [ + "warteraum-salt" + "warteraum-tokens" + ] + (name: { + file = depot.users.sterni.secrets."${name}.age"; + }); + + services.flipdot-gschichtler = { + enable = true; + virtualHost = "flipdot.openlab-augsburg.de"; + packages = { + inherit bahnhofshalle warteraum; + }; + saltFile = config.age.secretsDir + "/warteraum-salt"; + tokensFile = config.age.secretsDir + "/warteraum-tokens"; + }; + }; +} diff --git a/users/sterni/machines/ingeborg/http/likely-music.sterni.lv.nix b/users/sterni/machines/ingeborg/http/likely-music.sterni.lv.nix new file mode 100644 index 000000000000..8da03ac5e6ec --- /dev/null +++ b/users/sterni/machines/ingeborg/http/likely-music.sterni.lv.nix @@ -0,0 +1,23 @@ +{ depot, ... }: + +let + inherit (depot.users.sterni.external.likely-music) + nixosModule + likely-music + ; +in + +{ + imports = [ + ./nginx.nix + nixosModule + ]; + + config = { + services.likely-music = { + enable = true; + virtualHost = "likely-music.sterni.lv"; + package = likely-music; + }; + }; +} diff --git a/users/sterni/machines/ingeborg/http/nginx.nix b/users/sterni/machines/ingeborg/http/nginx.nix new file mode 100644 index 000000000000..d551b8391d18 --- /dev/null +++ b/users/sterni/machines/ingeborg/http/nginx.nix @@ -0,0 +1,30 @@ +{ ... }: + +{ + config = { + users = { + users.http = { + isSystemUser = true; + group = "http"; + }; + + groups.http = { }; + }; + + services.nginx = { + enable = true; + recommendedTlsSettings = true; + recommendedGzipSettings = true; + recommendedProxySettings = true; + + user = "http"; + group = "http"; + + appendHttpConfig = '' + charset utf-8; + ''; + }; + + networking.firewall.allowedTCPPorts = [ 80 443 ]; + }; +} diff --git a/users/sterni/machines/ingeborg/http/sterni.lv.nix b/users/sterni/machines/ingeborg/http/sterni.lv.nix new file mode 100644 index 000000000000..50c1bac293e2 --- /dev/null +++ b/users/sterni/machines/ingeborg/http/sterni.lv.nix @@ -0,0 +1,34 @@ +{ pkgs, depot, ... }: + +let + inherit (depot.users.sterni.nix.html) + __findFile + withDoctype + ; +in + +{ + imports = [ + ./nginx.nix + ]; + + config = { + services.nginx.virtualHosts."sterni.lv" = { + enableACME = true; + forceSSL = true; + root = pkgs.writeTextFile { + name = "sterni.lv-http-root"; + destination = "/index.html"; + text = withDoctype (<html> { } [ + (<head> { } [ + (<meta> { charset = "utf-8"; } null) + (<title> { } "no thoughts") + ]) + (<body> { } "🦩") + ]); + }; + # TODO(sterni): tmp.sterni.lv + locations."/tmp/".root = toString /srv/http; + }; + }; +} diff --git a/users/sterni/machines/ingeborg/irccat.nix b/users/sterni/machines/ingeborg/irccat.nix new file mode 100644 index 000000000000..0c40f15e33a4 --- /dev/null +++ b/users/sterni/machines/ingeborg/irccat.nix @@ -0,0 +1,23 @@ +{ depot, config, pkgs, lib, ... }: + +{ + imports = [ + (depot.path.origSrc + "/ops/modules/irccat.nix") + ]; + + config = { + services.depot.irccat = { + enable = true; + secretsFile = builtins.toFile "empty.json" "{}"; # TODO(sterni): register + config = { + tcp.listen = ":4722"; # ircc + irc = { + server = "irc.hackint.org:6697"; + tls = true; + nick = config.networking.hostName; + realname = "irccat"; + }; + }; + }; + }; +} diff --git a/users/sterni/machines/ingeborg/minecraft.nix b/users/sterni/machines/ingeborg/minecraft.nix new file mode 100644 index 000000000000..383fee8ca04c --- /dev/null +++ b/users/sterni/machines/ingeborg/minecraft.nix @@ -0,0 +1,125 @@ +{ pkgs, depot, config, ... }: + +let + carpet = pkgs.fetchurl { + url = "https://github.com/gnembon/fabric-carpet/releases/download/1.4.128/fabric-carpet-1.20.3-1.4.128+v231205.jar"; + sha256 = "1jh2pb9pjwyfv1ianzykmja21nqlv175a8rg926xg3w4hhhwzrfq"; + }; + + carpet-extra = pkgs.fetchurl { + url = "https://github.com/gnembon/carpet-extra/releases/download/1.4.128/carpet-extra-1.20.3-1.4.128.jar"; + sha256 = "0gxwm5ayr0y5dri0kxlnrrgy9pyaim34rl6km1j42fkyvc4r8p6x"; + }; + + userGroup = "minecraft"; + + makeJvmOpts = megs: [ + "-Xms${toString megs}M" + "-Xmx${toString megs}M" + ]; + + whitelist = { + spreadwasser = "242a66eb-2df2-4585-9a28-ac763ad0d0f9"; + sternenseemann = "d8e48069-1905-4886-a5da-a4ee917ee254"; + }; + + rconPasswordFile = config.age.secretsDir + "/minecraft-rcon"; + + baseProperties = { + white-list = true; + allow-flight = true; + difficulty = "hard"; + function-permission-level = 4; + snooper-enabled = false; + view-distance = 12; + sync-chunk-writes = "false"; # the single biggest performance fix + max-tick-time = 6000000; # TODO(sterni): disable watchdog via carpet + enforce-secure-profile = false; + }; +in + +{ + imports = [ + ../../modules/minecraft-fabric.nix + ../../modules/backup-minecraft-fabric.nix + ]; + + config = { + environment.systemPackages = [ + pkgs.mcrcon + pkgs.jre + ]; + + users = { + users."${userGroup}" = { + isNormalUser = true; + openssh.authorizedKeys.keys = depot.users.sterni.keys.all; + shell = "${pkgs.fish}/bin/fish"; + }; + + groups."${userGroup}" = { }; + }; + + age.secrets = { + minecraft-rcon.file = depot.users.sterni.secrets."minecraft-rcon.age"; + }; + + services.backup-minecraft-fabric-servers = { + enable = true; + repository = "/srv/backup/from-local/minecraft"; + }; + + services.minecraft-fabric-server = { + creative = { + enable = false; # not actively used + version = "1.20.4"; + mods = [ + carpet + carpet-extra + ]; + world = config.users.users.${userGroup}.home + "/worlds/creative"; + + jvmOpts = makeJvmOpts 2048; + user = userGroup; + group = userGroup; + + inherit whitelist rconPasswordFile; + ops = whitelist; + + serverProperties = baseProperties // { + server-port = 25566; + "rcon.port" = 25576; + gamemode = "creative"; + enable-command-block = true; + motd = "storage design server"; + spawn-protection = 2; + }; + }; + + carpet = { + enable = true; + version = "1.20.4"; + mods = [ + carpet + carpet-extra + ]; + world = config.users.users.${userGroup}.home + "/worlds/carpet"; + + jvmOpts = makeJvmOpts 4096; + user = userGroup; + group = userGroup; + + inherit whitelist rconPasswordFile; + ops = whitelist; + + serverProperties = baseProperties // { + server-port = 25565; + "rcon.port" = 25575; + motd = "ich tu fleissig hustlen nenn mich bob der baumeister"; + + level-seed = 7240251176989694927; # for posterity + }; + }; + }; + }; +} diff --git a/users/sterni/machines/ingeborg/monitoring.nix b/users/sterni/machines/ingeborg/monitoring.nix new file mode 100644 index 000000000000..6244bc5e88ce --- /dev/null +++ b/users/sterni/machines/ingeborg/monitoring.nix @@ -0,0 +1,152 @@ +{ pkgs, lib, config, ... }: + +let + ircChannel = "#sterni.lv"; + irccatPort = + builtins.replaceStrings [ ":" ] [ "" ] + config.services.depot.irccat.config.tcp.listen; + + mkIrcMessager = + { name + , msgExpr + }: + pkgs.writeShellScript name '' + set -euo pipefail + printf '%s %s\n' ${lib.escapeShellArg ircChannel} ${msgExpr} | \ + ${lib.getBin pkgs.netcat-openbsd}/bin/nc -N localhost ${irccatPort} + ''; + + netdataPort = 19999; +in + +{ + imports = [ + ./irccat.nix + ]; + + config = { + services.depot.irccat.config.irc.channels = [ + ircChannel + ]; + + # Since we have irccat we can wire up mdadm --monitor + boot.swraid.mdadmConf = '' + PROGRAM ${ + mkIrcMessager { + name = "mdmonitor-to-irc"; + # prog EVENT MD_DEVICE COMPONENT_DEVICE + msgExpr = ''"mdmonitor: $1($2''${3:+, $3})"''; + } + } + ''; + + # TODO(sterni): irc notifications (?) + services = { + smartd = { + enable = true; + autodetect = true; + # Short self test every day 03:00 + # Long self test every tuesday 05:00 + defaults.autodetected = "-a -o on -s (S/../.././03|L/../../2/05)"; + extraOptions = [ + "-A" + "/var/log/smartd/" + ]; + }; + + netdata = { + enable = true; + config = { + logs = { + access = "syslog"; + error = "syslog"; + debug = "syslog"; + health = "syslog"; + collector = "syslog"; + }; + web = { + "default port" = toString netdataPort; + "bind to" = "localhost:${toString netdataPort}"; + }; + health = { + "script to execute on alarm" = pkgs.writeShellScript "simple-alarm-notify" '' + set -euo pipefail + + # This humongous list is copied over from netdata's alarm-notify.sh + roles="''${1}" # the roles that should be notified for this event + args_host="''${2}" # the host generated this event + unique_id="''${3}" # the unique id of this event + alarm_id="''${4}" # the unique id of the alarm that generated this event + event_id="''${5}" # the incremental id of the event, for this alarm id + when="''${6}" # the timestamp this event occurred + name="''${7}" # the name of the alarm, as given in netdata health.d entries + chart="''${8}" # the name of the chart (type.id) + status="''${9}" # the current status : REMOVED, UNINITIALIZED, UNDEFINED, CLEAR, WARNING, CRITICAL + old_status="''${10}" # the previous status: REMOVED, UNINITIALIZED, UNDEFINED, CLEAR, WARNING, CRITICAL + value="''${11}" # the current value of the alarm + old_value="''${12}" # the previous value of the alarm + src="''${13}" # the line number and file the alarm has been configured + duration="''${14}" # the duration in seconds of the previous alarm state + non_clear_duration="''${15}" # the total duration in seconds this is/was non-clear + units="''${16}" # the units of the value + info="''${17}" # a short description of the alarm + value_string="''${18}" # friendly value (with units) + # shellcheck disable=SC2034 + # variable is unused, but https://github.com/netdata/netdata/pull/5164#discussion_r255572947 + old_value_string="''${19}" # friendly old value (with units), previously named "old_value_string" + calc_expression="''${20}" # contains the expression that was evaluated to trigger the alarm + calc_param_values="''${21}" # the values of the parameters in the expression, at the time of the evaluation + total_warnings="''${22}" # Total number of alarms in WARNING state + total_critical="''${23}" # Total number of alarms in CRITICAL state + total_warn_alarms="''${24}" # List of alarms in warning state + total_crit_alarms="''${25}" # List of alarms in critical state + classification="''${26}" # The class field from .conf files + edit_command_line="''${27}" # The command to edit the alarm, with the line number + child_machine_guid="''${28}" # the machine_guid of the child + transition_id="''${29}" # the transition_id of the alert + summary="''${30}" # the summary text field of the alert + + # Verify that they haven't extended the arg list + ARG_COUNT_EXPECTED=30 + + if [[ "$#" != "$ARG_COUNT_EXPECTED" ]]; then + echo "$0: WARNING: unexpected number of arguments: $#. Did netdata add more?" >&2 + fi + + MSG="netdata: $status ''${name//_/ } ($chart): ''${summary//_/ } = $value_string" + + # Filter rules by chart name. This is necessary, since the "enabled alarms" + # filter only allows for filtering alarm types, not specific alarms + # belonging to that alarm. + case "$chart" in + # netdata prefers the automatically assigned names (dm-<n>, md<n>, + # sd<c>) over ids for alerts, so this configuration assumes that + # we have two physical disks which we kind of assert using the + # grub configuration (it is more difficult with the soft raid + # config). + # ${assert builtins.length config.boot.loader.grub.devices == 2; ""} + disk_util.sda | disk_util.sdb | disk_backlog.sda | disk_backlog.sdb) + + ;; + disk_util.* | disk_backlog.*) + echo "$0: INFO: DISCARDING message: $MSG" >&2 + exit 0 + ;; + *) + ;; + esac + + echo "$0: INFO: sending message: $MSG" >&2 + ${ + mkIrcMessager { + name = "trivial-send-to-irc"; + msgExpr = "\"$1\""; + } + } "$MSG" + ''; + }; + }; + }; + }; + }; +} diff --git a/users/sterni/machines/ingeborg/network.nix b/users/sterni/machines/ingeborg/network.nix new file mode 100644 index 000000000000..fceb530d55d8 --- /dev/null +++ b/users/sterni/machines/ingeborg/network.nix @@ -0,0 +1,62 @@ +{ config, pkgs, lib, depot, ... }: + +let + ipv6 = "2a01:4f9:2a:1bc6::/64"; + + ipv4 = "95.216.27.158"; + gatewayv4 = "95.216.27.129"; + netmaskv4 = "255.255.255.192"; +in + +{ + config = { + boot = { + kernelParams = [ + "ip=${ipv4}::${gatewayv4}:${netmaskv4}::eth0:none" + ]; + + initrd.network = { + enable = true; + ssh = { + enable = true; + authorizedKeys = depot.users.sterni.keys.all; + hostKeys = [ + "/etc/nixos/unlock_rsa_key_openssh" + "/etc/nixos/unlock_ed25519_key_openssh" + ]; + }; + postCommands = '' + echo 'cryptsetup-askpass' >> /root/.profile + ''; + }; + }; + + networking = { + usePredictableInterfaceNames = false; + useDHCP = false; + interfaces."eth0".useDHCP = false; + + hostName = "ingeborg"; + + firewall = { + enable = true; + allowPing = true; + allowedTCPPorts = [ 22 ]; + }; + }; + + systemd.network = { + enable = true; + networks."eth0".extraConfig = '' + [Match] + Name = eth0 + + [Network] + Address = ${ipv6} + Gateway = fe80::1 + Address = ${ipv4}/27 + Gateway = ${gatewayv4} + ''; + }; + }; +} diff --git a/users/sterni/machines/ingeborg/quassel.nix b/users/sterni/machines/ingeborg/quassel.nix new file mode 100644 index 000000000000..cd8dacc91773 --- /dev/null +++ b/users/sterni/machines/ingeborg/quassel.nix @@ -0,0 +1,18 @@ +{ depot, ... }: + +{ + imports = [ + (depot.path.origSrc + "/ops/modules/quassel.nix") + ]; + + config = { + services.depot.quassel = { + enable = true; + acmeHost = "sterni.lv"; + bindAddresses = [ + "0.0.0.0" + "::" + ]; + }; + }; +} diff --git a/users/sterni/machines/ingeborg/tv.nix b/users/sterni/machines/ingeborg/tv.nix new file mode 100644 index 000000000000..016ad256ef07 --- /dev/null +++ b/users/sterni/machines/ingeborg/tv.nix @@ -0,0 +1,13 @@ +{ pkgs, ... }: + +{ + config = { + # TODO(sterni): smb or nfs may be a faster alternative? + services.openssh.allowSFTP = true; + + users.users.tv = { + group = "users"; + isNormalUser = true; + }; + }; +} diff --git a/users/sterni/mblog/.gitignore b/users/sterni/mblog/.gitignore new file mode 100644 index 000000000000..ae957fcad0dc --- /dev/null +++ b/users/sterni/mblog/.gitignore @@ -0,0 +1,5 @@ +# local test data +test-msg + +# sly C-c C-k +*.fasl diff --git a/users/sterni/mblog/LICENSE b/users/sterni/mblog/LICENSE new file mode 100644 index 000000000000..f288702d2fa1 --- /dev/null +++ b/users/sterni/mblog/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + <program> Copyright (C) <year> <name of author> + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +<https://www.gnu.org/licenses/>. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +<https://www.gnu.org/licenses/why-not-lgpl.html>. diff --git a/users/sterni/mblog/cli.lisp b/users/sterni/mblog/cli.lisp new file mode 100644 index 000000000000..555f8def535f --- /dev/null +++ b/users/sterni/mblog/cli.lisp @@ -0,0 +1,75 @@ +;; SPDX-License-Identifier: GPL-3.0-only +;; SPDX-FileCopyrightText: Copyright (C) 2022-2023 by sterni + +(in-package :cli) +(declaim (optimize (safety 3))) + +;; TODO(sterni): nicer messages for various errors signaled? + +(defun partition-by (f seq) + "Split SEQ into two lists, returned as multiple values. The first list + contains all elements for which F returns T, the second one the remaining + elements." + (loop for x in seq + if (funcall f x) + collecting x into yes + else + collecting x into no + finally (return (values yes no)))) + +(defparameter +help+ '(("mnote-html" . "FILE [FILE [ ... ]]") + ("mblog" . "MAILDIR OUT"))) + +(defun mnote-html (name flags &rest args) + "Convert all note mime messages given as ARGS to HTML fragments." + (declare (ignore name flags)) + (loop for arg in args + do (note:apple-note-html-fragment + (note:make-apple-note (mime:mime-message (pathname arg))) + *standard-output*))) + +(defun mblog (name flags maildir outdir) + "Read a MAILDIR and build an mblog in OUTDIR " + (declare (ignore name flags)) + (build-mblog (pathname maildir) (pathname outdir))) + +(defun display-help (name flags &rest args) + "Print help message for current executable." + (declare (ignore args flags)) + (format *error-output* "Usage: ~A ~A~%" + name + (or (cdr (assoc name +help+ :test #'string=)) + (concatenate 'string "Unknown executable: " name)))) + +(defun usage-error (name flags &rest args) + "Print help and exit with a non-zero exit code." + (format *error-output* "~A: usage error~%" name) + (display-help name args flags) + (uiop:quit 100)) + +(defun main () + "Dispatch to correct main function based on arguments and UIOP:ARGV0." + (config:init-from-env) + (multiple-value-bind (flags args) + (partition-by (lambda (x) (starts-with #\- x)) + (uiop:command-line-arguments)) + + (let ((prog-name (pathname-name (pathname (uiop:argv0)))) + (help-requested-p (find-if (lambda (x) + (member x '("-h" "--help" "--usage") + :test #'string=)) + args))) + (apply + (if help-requested-p + #'display-help + (cond + ((and (string= prog-name "mnote-html") + (null flags)) + #'mnote-html) + ((and (string= prog-name "mblog") + (null flags) + (= 2 (length args))) + #'mblog) + (t #'usage-error))) + (append (list prog-name flags) + args))))) diff --git a/users/sterni/mblog/config.lisp b/users/sterni/mblog/config.lisp new file mode 100644 index 000000000000..0d4cbfe8ae20 --- /dev/null +++ b/users/sterni/mblog/config.lisp @@ -0,0 +1,31 @@ +;; SPDX-License-Identifier: GPL-3.0-only +;; SPDX-FileCopyrightText: Copyright (C) 2023 by sterni + +(in-package :config) + +(eval-when (:compile-toplevel :load-toplevel) + (defun plist-to-alist (lst) + (loop for (name . (default . (parser . nil))) on lst by #'cdddr + collect (cons name (list default parser)))) + + (defun symbol-to-env-var-name (symbol) + (concatenate 'string + "MBLOG_" + (string-upcase + (remove #\* (substitute #\_ #\- (string symbol))))))) + +(defmacro define-configuration-variables (&rest args) + (let ((vars (plist-to-alist args)) + (val-var-sym (gensym))) + `(progn + ,@(loop for (name . (default nil)) in vars + collect `(defvar ,name ,default)) + + (defun init-from-env () + ,@(loop for (name . (nil parser)) in vars + collect + `(when-let ((,val-var-sym (getenv ,(symbol-to-env-var-name name)))) + (setf ,name (funcall ,parser ,val-var-sym)))))))) + +(define-configuration-variables + *general-buffer-size* (min 4096 qbase64:+max-bytes-length+) #'parse-integer) diff --git a/users/sterni/mblog/default.nix b/users/sterni/mblog/default.nix new file mode 100644 index 000000000000..6ad8a10ce378 --- /dev/null +++ b/users/sterni/mblog/default.nix @@ -0,0 +1,47 @@ +# SPDX-License-Identifier: GPL-3.0-only +# SPDX-FileCopyrightText: Copyright (C) 2022-2023 by sterni +{ depot, pkgs, ... }: + +(depot.nix.buildLisp.program { + name = "mblog"; + + srcs = [ + ./packages.lisp + ./config.lisp + ./maildir.lisp + ./transformer.lisp + ./note.lisp + ./mblog.lisp + ./cli.lisp + ]; + + deps = [ + { + sbcl = depot.nix.buildLisp.bundled "uiop"; + default = depot.nix.buildLisp.bundled "asdf"; + } + depot.lisp.klatre + depot.third_party.lisp.alexandria + depot.third_party.lisp.closure-html + depot.third_party.lisp.cl-date-time-parser + depot.third_party.lisp.cl-who + depot.third_party.lisp.local-time + depot.third_party.lisp.mime4cl + ]; + + main = "cli:main"; + + # due to sclf + brokenOn = [ + "ccl" + "ecl" + ]; +}).overrideAttrs (super: { + # The built binary dispatches based on argv[0]. Building two executables would + # waste a lot of space. + buildCommand = '' + ${super.buildCommand} + + ln -s "$out/bin/mblog" "$out/bin/mnote-html" + ''; +}) diff --git a/users/sterni/mblog/maildir.lisp b/users/sterni/mblog/maildir.lisp new file mode 100644 index 000000000000..42f18c619d18 --- /dev/null +++ b/users/sterni/mblog/maildir.lisp @@ -0,0 +1,20 @@ +;; SPDX-License-Identifier: GPL-3.0-only +;; SPDX-FileCopyrightText: Copyright (C) 2022 by sterni + +(in-package :maildir) +(declaim (optimize (safety 3))) + +(defun list (dir) + "Returns a list of pathnames to messages in a maildir. The messages are + returned in no guaranteed order. Note that this function doesn't fully + implement the behavior prescribed by maildir(5): It only looks at `cur` + and `new` and won't clean up `tmp` nor move files from `new` to `cur`, + since it is strictly read-only." + (flet ((subdir-contents (subdir) + (directory + (merge-pathnames + (make-pathname :directory `(:relative ,subdir) + :name :wild :type :wild) + dir)))) + (mapcan #'subdir-contents '("cur" "new")))) + diff --git a/users/sterni/mblog/mblog.lisp b/users/sterni/mblog/mblog.lisp new file mode 100644 index 000000000000..7823bde20343 --- /dev/null +++ b/users/sterni/mblog/mblog.lisp @@ -0,0 +1,147 @@ +;; SPDX-License-Identifier: GPL-3.0-only +;; SPDX-FileCopyrightText: Copyright (C) 2022-2023 by sterni +;; SPDX-FileCopyrightText: Copyright (C) 2006-2010 by Walter C. Pelissero + +(in-package :mblog) + +;; util + +;; Taken from SCLF, written by Walter C. Pelissero +(defun pathname-as-directory (pathname) + "Converts PATHNAME to directory form and return it." + (setf pathname (pathname pathname)) + (if (pathname-name pathname) + (make-pathname :directory (append (or (pathname-directory pathname) + '(:relative)) + (list (file-namestring pathname))) + :name nil + :type nil + :defaults pathname) + pathname)) + +(defmacro with-overwrite-file ((&rest args) &body body) + "Like WITH-OPEN-FILE, but creates/supersedes the given file for writing." + `(with-open-file (,@args :direction :output + :if-exists :supersede + :if-does-not-exist :create) + ,@body)) + +;; CSS + +(defvar *style* " +header, main { + width: 100%; + max-width: 800px; +} + +main img { + max-width: 100%; +} + +a:link, a:visited { + color: blue; +} +") + +;; Templating + +(eval-when (:compile-toplevel :load-toplevel) + (setf (who:html-mode) :html5)) + +(defmacro render-page ((stream title &key root) &body body) + "Surround BODY with standard mblog document skeleton and render it to STREAM + using CL-WHO. If :ROOT is T, assume that the page is the top level index page. + Otherwise it is assumed to be one level below the index page." + `(who:with-html-output (,stream nil :prologue t) + (:html + (:head + (:meta :charset "utf-8") + (:meta :viewport "width=device-width") + (:title (who:esc ,title)) + (:link :rel "stylesheet" + :type "text/css" + :href ,(concatenate 'string (if root "" "../") "style.css"))) + (:body + (:header + (:nav + (:a :href ,(who:escape-string (if root "" "..")) "index"))) + (:main ,@body))))) + +;; Build Logic + +(defun build-note-page (note note-dir) + "Convert NOTE to HTML and write it to index.html in NOTE-DIR alongside any + extra attachments NOTE contains." + (with-overwrite-file (html-stream (merge-pathnames "index.html" note-dir)) + (render-page (html-stream (apple-note-subject note)) + (:article + (apple-note-html-fragment note html-stream)))) + + (mime:do-parts (part note) + (unless (string= (mime:mime-id part) + (mime:mime-id (note:apple-note-text-part note))) + (let ((attachment-in (mime:mime-body-stream part)) + (attachment-dst (merge-pathnames + (mime:mime-part-file-name part) + note-dir))) + + (format *error-output* "Writing attachment ~A~%" attachment-dst) + + (with-overwrite-file (attachment-out attachment-dst + :element-type + (stream-element-type attachment-in)) + (redirect-stream attachment-in attachment-out + :buffer-size *general-buffer-size*))))) + + (values)) + +(defun build-index-page (notes-list destination) + "Write an overview page linking all notes in NOTE-LIST in the given order to + DESTINATION. The notes are assumed to be in a sibling directory named like the + each note's UUID." + (with-overwrite-file (listing-stream destination) + (render-page (listing-stream "mblog" :root t) + (:h1 "mblog") + (:table + (dolist (note notes-list) + (who:htm + (:tr + (:td (:a :href (who:escape-string (apple-note-uuid note)) + (who:esc (apple-note-subject note)))) + (:td (who:esc + (klatre:format-dottime + (universal-to-timestamp (apple-note-time note))))))))))) + (values)) + +(defun build-mblog (notes-dir html-dir) + "Take MIME messages from maildir NOTES-DIR and build a complete mblog in HTML-DIR." + (setf notes-dir (pathname-as-directory notes-dir)) + (setf html-dir (pathname-as-directory html-dir)) + + ;; TODO(sterni): avoid rewriting if nothing was updated + ;; TODO(sterni): clean up deleted things + ;; TODO(sterni): atom feed + + (let ((all-notes '())) + (dolist (message-path (maildir:list notes-dir)) + (let* ((note (make-apple-note (mime:mime-message message-path))) + (note-dir (merge-pathnames (make-pathname + :directory + `(:relative ,(apple-note-uuid note))) + html-dir))) + + (format *error-output* "Writing note message ~A to ~A~%" + message-path note-dir) + (ensure-directories-exist note-dir) + (build-note-page note note-dir) + (push note all-notes))) + + ;; reverse sort the entries by time for the index page + (setf all-notes (sort all-notes #'> :key #'apple-note-time)) + + (build-index-page all-notes (merge-pathnames "index.html" html-dir)) + + (with-overwrite-file (css-stream (merge-pathnames "style.css" html-dir)) + (write-string *style* css-stream)) + + (values))) diff --git a/users/sterni/mblog/note.lisp b/users/sterni/mblog/note.lisp new file mode 100644 index 000000000000..f056aaa72d54 --- /dev/null +++ b/users/sterni/mblog/note.lisp @@ -0,0 +1,118 @@ +;; SPDX-License-Identifier: GPL-3.0-only +;; SPDX-FileCopyrightText: Copyright (C) 2022-2023 by sterni + +(in-package :note) +(declaim (optimize (safety 3))) + +;;; util + +(defun html-escape-stream (in out) + "Escape characters read from stream IN and write them to + stream OUT escaped using WHO:ESCAPE-STRING-MINIMAL." + (let ((buf (make-string config:*general-buffer-size*))) + (loop for len = (read-sequence buf in) + while (> len 0) + do (write-string (who:escape-string-minimal (subseq buf 0 len)) out)))) + +(defun cid-header-value (cid) + "Takes a Content-ID as present in Apple Notes' <object> tags and properly + surrounds them with angle brackets for a MIME header" + (concatenate 'string "<" cid ">")) + +(defun find-mime-message-date (message) + (when-let ((date-string (car (mime:mime-message-header-values "Date" message)))) + (date-time-parser:parse-date-time date-string))) + +;;; main implementation + +(defun apple-note-mime-subtype-p (x) + (member x '("plain" "html") :test #'string-equal)) + +(deftype apple-note-mime-subtype () + '(satisfies apple-note-mime-subtype-p)) + +(defclass apple-note (mime:mime-message) + ((text-part + :type mime:mime-text + :initarg :text-part + :reader apple-note-text-part) + (subject + :type string + :initarg :subject + :reader apple-note-subject) + (uuid + :type string + :initarg :uuid + :reader apple-note-uuid) + (time + :type integer + :initarg :time + :reader apple-note-time) + (mime-subtype + :type apple-note-mime-subtype + :initarg :mime-subtype + :reader apple-note-mime-subtype)) + (:documentation + "Representation of a Note created using Apple's Notes via the IMAP backend")) + +(defun apple-note-p (msg) + "Checks X-Uniform-Type-Identifier of a MIME:MIME-MESSAGE + to determine if a given mime message claims to be an Apple Note." + (when-let (uniform-id (car (mime:mime-message-header-values + "X-Uniform-Type-Identifier" + msg))) + (string-equal uniform-id "com.apple.mail-note"))) + +(defun make-apple-note (msg) + (check-type msg mime-message) + + (unless (apple-note-p msg) + (error "Passed message is not an Apple Note according to headers")) + + (let ((text-part (mime:find-mime-text-part msg)) + (subject (car (mime:mime-message-header-values "Subject" msg :decode t))) + (uuid (when-let ((val (car (mime:mime-message-header-values + "X-Universally-Unique-Identifier" + msg)))) + (string-downcase val))) + (time (find-mime-message-date msg))) + ;; The idea here is that we don't need to check a lot manually, instead + ;; the type annotation are going to do this for us (with sufficient safety?) + (change-class msg 'apple-note + :text-part text-part + :subject subject + :uuid uuid + :time time + :mime-subtype (mime:mime-subtype text-part)))) + +(defgeneric apple-note-html-fragment (note out) + (:documentation + "Takes an APPLE-NOTE and writes its text content as HTML to + the OUT stream. The <object> tags are resolved to <img> which + refer to the respective attachment's filename as a relative path, + but extraction of the attachments must be done separately. The + surrounding <html> and <body> tags are stripped and <head> + discarded completely, so only a fragment which can be included + in custom templates remains.")) + +(defmethod apple-note-html-fragment ((note apple-note) (out stream)) + (let ((text (apple-note-text-part note))) + (cond + ;; notemap creates text/plain notes we need to handle properly. + ;; Additionally we *could* check X-Mailer which notemap sets + ((string-equal (apple-note-mime-subtype note) "plain") + (html-escape-stream (mime:mime-body-stream text) out)) + ;; Notes.app creates text/html parts + ((string-equal (apple-note-mime-subtype note) "html") + (closure-html:parse + (mime:mime-body-stream text) + (make-instance + 'apple-note-transformer + :cid-lookup + (lambda (cid) + (when-let* ((part (mime:find-mime-part-by-id note (cid-header-value cid))) + (file (mime:mime-part-file-name part))) + file)) + :next-handler + (closure-html:make-character-stream-sink out)))) + (t (error "Internal error: unexpected MIME subtype"))))) diff --git a/users/sterni/mblog/packages.lisp b/users/sterni/mblog/packages.lisp new file mode 100644 index 000000000000..d6e33955d31c --- /dev/null +++ b/users/sterni/mblog/packages.lisp @@ -0,0 +1,64 @@ +;; SPDX-License-Identifier: GPL-3.0-only +;; SPDX-FileCopyrightText: Copyright (C) 2022-2023 by sterni + +(defpackage :maildir + (:use :common-lisp) + (:shadow :list) + (:export :list) + (:documentation + "Very incomplete package for dealing with maildir(5).")) + +(defpackage :config + (:use + :common-lisp) + (:import-from :uiop :getenv) + (:import-from :alexandria :when-let) + (:export + :init-from-env + :*general-buffer-size*)) + +(defpackage :note + (:use + :common-lisp + :closure-html + :cl-date-time-parser + :mime4cl + :config) + (:import-from + :alexandria + :when-let* + :when-let + :starts-with-subseq + :ends-with-subseq) + (:import-from :who :escape-string-minimal) + (:export + :apple-note + :apple-note-uuid + :apple-note-subject + :apple-note-time + :apple-note-text-part + :make-apple-note + :apple-note-html-fragment)) + +(defpackage :mblog + (:use + :common-lisp + :klatre + :who + :maildir + :note + :config) + (:export :build-mblog) + (:import-from :local-time :universal-to-timestamp) + (:import-from :mime4cl :redirect-stream) + (:shadowing-import-from :common-lisp :list)) + +(defpackage :cli + (:use + :common-lisp + :uiop + :note + :config + :mblog) + (:import-from :alexandria :starts-with) + (:export :main)) diff --git a/users/sterni/mblog/transformer.lisp b/users/sterni/mblog/transformer.lisp new file mode 100644 index 000000000000..c499eafbec4f --- /dev/null +++ b/users/sterni/mblog/transformer.lisp @@ -0,0 +1,130 @@ +;; SPDX-License-Identifier: GPL-3.0-only +;; SPDX-FileCopyrightText: Copyright (C) 2022 by sterni + +(in-package :note) +(declaim (optimize (safety 3))) + +;; Throw away these tags and all of their children +(defparameter +discard-tags-with-children+ '("HEAD")) +;; Only “strip” these tags and leave their content as is +(defparameter +discard-tags-only+ '("BODY" "HTML")) + +;; This is basically the same as cxml's PROXY-HANDLER. +;; Couldn't be bothered to make a BROADCAST-HANDLER because I +;; only need to pass through to one handler. It accepts every +;; event and passes it on to NEXT-HANDLER. This is useful for +;; subclassing mostly where an event can be modified or passed +;; on as is via CALL-NEXT-METHOD. +(defclass hax-proxy-handler (hax:default-handler) + ((next-handler + :initarg :next-handler + :accessor proxy-next-handler))) + +;; Define the trivial handlers which just call themselves for NEXT-HANDLER +(macrolet ((def-proxy-handler (name (&rest args)) + `(defmethod ,name ((h hax-proxy-handler) ,@args) + (,name (proxy-next-handler h) ,@args)))) + (def-proxy-handler hax:start-document (name p-id s-id)) + (def-proxy-handler hax:end-document ()) + (def-proxy-handler hax:start-element (name attrs)) + (def-proxy-handler hax:end-element (name)) + (def-proxy-handler hax:characters (data)) + (def-proxy-handler hax:unescaped (data)) + (def-proxy-handler hax:comment (data))) + +(defclass apple-note-transformer (hax-proxy-handler) + ((cid-lookup + :initarg :cid-lookup + :initform (lambda (cid) nil) + :accessor transformer-cid-lookup) + (discard-until + :initarg :discard-until + :initform nil + :accessor transformer-discard-until) + (depth + :initarg :depth + :initform 0 + :accessor transformer-depth)) + (:documentation + "HAX handler that strips unnecessary tags from the HTML of a com.apple.mail-note + and resolves references to attachments to IMG tags.")) + +;; Define the “boring” handlers which just call the next method (i. e. the next +;; handler) unless discard-until is not nil in which case the event is dropped. +(macrolet ((def-filter-handler (name (&rest args)) + `(defmethod ,name ((h apple-note-transformer) ,@args) + (when (not (transformer-discard-until h)) + (call-next-method))))) + (def-filter-handler hax:start-document (name p-id s-id)) + (def-filter-handler hax:end-document ()) + (def-filter-handler hax:characters (data)) + (def-filter-handler hax:unescaped (data)) + (def-filter-handler hax:comment (data))) + +(defun parse-content-id (attrlist) + (when-let (data (find-if (lambda (x) + (string-equal (hax:attribute-name x) "DATA")) + attrlist)) + (multiple-value-bind (starts-with-cid-p suffix) + (starts-with-subseq "cid:" (hax:attribute-value data) + :return-suffix t :test #'char=) + (if starts-with-cid-p suffix data)))) + +(defmethod hax:start-element ((handler apple-note-transformer) name attrs) + (with-accessors ((discard-until transformer-discard-until) + (next-handler proxy-next-handler) + (cid-lookup transformer-cid-lookup) + (depth transformer-depth)) + handler + + (cond + ;; If we are discarding, any started element is dropped, + ;; since the end-condition only is reached via END-ELEMENT. + (discard-until nil) + ;; If we are not discarding any outer elements, we can set + ;; up a new discard condition if we encounter an appropriate + ;; element. + ((member name +discard-tags-with-children+ :test #'string-equal) + (setf discard-until (cons name depth))) + ;; Only drop this event, must be mirrored in END-ELEMENT to + ;; avoid invalidly nested HTML. + ((member name +discard-tags-only+ :test #'string-equal) nil) + ;; If we encounter an object tag, we drop it and its contents, + ;; but only after inspecting its attributes and emitting new + ;; events representing an img tag which includes the respective + ;; attachment via its filename. + ((string-equal name "OBJECT") + (progn + (setf discard-until (cons "OBJECT" depth)) + ;; TODO(sterni): check type and only resolve images, raise error + ;; otherwise. We should only encounter images anyways, since + ;; other types are only supported for iCloud which doesn't seem + ;; to use IMAP for sync these days. + (when-let* ((cid (parse-content-id attrs)) + (file (apply cid-lookup (list cid))) + (src (hax:make-attribute "SRC" file))) + (hax:start-element next-handler "IMG" (list src)) + (hax:end-element next-handler "IMG")))) + ;; In all other cases, we use HAX-PROXY-HANDLER to pass the event on. + (t (call-next-method))) + (setf depth (1+ depth)))) + +(defmethod hax:end-element ((handler apple-note-transformer) name) + (with-accessors ((discard-until transformer-discard-until) + (depth transformer-depth)) + handler + + (setf depth (1- depth)) + (cond + ;; If we are discarding and encounter the same tag again at the same + ;; depth, we can stop, but still have to discard the current tag. + ((and discard-until + (string-equal (car discard-until) name) + (= (cdr discard-until) depth)) + (setf discard-until nil)) + ;; In all other cases, we drop properly. + (discard-until nil) + ;; Mirrored tag stripping as in START-ELEMENT + ((member name +discard-tags-only+ :test #'string-equal) nil) + ;; In all other cases, we use HAX-PROXY-HANDLER to pass the event on. + (t (call-next-method))))) diff --git a/users/sterni/modules/backup-minecraft-fabric.nix b/users/sterni/modules/backup-minecraft-fabric.nix new file mode 100644 index 000000000000..a80a7f51a9ef --- /dev/null +++ b/users/sterni/modules/backup-minecraft-fabric.nix @@ -0,0 +1,125 @@ +# Companion module to minecraft-fabric.nix which automatically and regularly +# creates backups of all minecraft servers' worlds to a shared borg(1) +# repository. +# +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2023 sterni <sternenseemann@systemli.org> +{ pkgs, depot, config, lib, ... }: + +let + inherit (depot.nix) getBins; + + bins = getBins pkgs.borgbackup [ "borg" ] + // getBins pkgs.mcrcon [ "mcrcon" ]; + + unvaried = ls: builtins.all (l: l == builtins.head ls) ls; + + cfg = config.services.backup-minecraft-fabric-servers; + + instances = lib.filterAttrs (_: i: i.enable) config.services.minecraft-fabric-server; + users = lib.mapAttrsToList (_: i: i.user) instances; + groups = lib.mapAttrsToList (_: i: i.group) instances; + + mkBackupScript = instanceName: instanceCfg: + let + archivePrefix = "minecraft-fabric-${instanceName}-world-${builtins.baseNameOf instanceCfg.world}-"; + in + + pkgs.writeShellScript "backup-minecraft-fabric-${instanceName}" '' + export MCRCON_HOST="localhost" + export MCRCON_PORT="${toString instanceCfg.serverProperties."rcon.port"}" + # Unfortunately, mcrcon can't read the password from a file + export MCRCON_PASS="$(cat "''${CREDENTIALS_DIRECTORY}/${instanceName}-rcon-password")" + + ${bins.mcrcon} save-all + unset MCRCON_PASS + + # Give the server plenty of time to save + sleep 60 + + ${bins.borg} ${lib.escapeShellArgs [ + "create" + "--verbose" "--filter" "AMEU" "--list" + "--stats" "--show-rc" + "--compression" "zlib" + "${cfg.repository}::${archivePrefix}{now}" + instanceCfg.world + ]} + + ${bins.borg} ${lib.escapeShellArgs [ + "prune" + "--list" + "--show-rc" + "--glob-archives" "${archivePrefix}*" + "--keep-hourly" "168" + "--keep-daily" "31" + "--keep-monthly" "6" + "--keep-yearly" "2" + cfg.repository + ]} + + ${bins.borg} compact ${lib.escapeShellArg cfg.repository} + ''; +in + +{ + imports = [ + ./minecraft-fabric.nix + ]; + + options = { + services.backup-minecraft-fabric-servers = { + enable = lib.mkEnableOption "backups of all Minecraft fabric servers"; + + repository = lib.mkOption { + type = lib.types.path; + description = "Path to the borg(1) repository to use for all backups."; + default = "/var/lib/backup/minecraft-fabric"; + }; + }; + }; + + config = lib.mkIf (cfg.enable && builtins.length (builtins.attrNames instances) > 0) { + assertions = [ + { + assertion = unvaried users && unvaried groups; + message = "all instances under services.minecraft-fabric-server must use the same user and group"; + } + ]; + + environment.systemPackages = [ + pkgs.borgbackup + ]; + + systemd = { + services.backup-minecraft-fabric-servers = { + description = "Backup world of all fabric based Minecraft servers"; + wantedBy = [ ]; + after = builtins.map + (name: "minecraft-fabric-${name}.service") + (builtins.attrNames instances); + + script = lib.concatStrings (lib.mapAttrsToList mkBackupScript instances); + + serviceConfig = { + Type = "oneshot"; + User = builtins.head users; + Group = builtins.head groups; + LoadCredential = lib.mapAttrsToList + (instanceName: instanceCfg: "${instanceName}-rcon-password:${instanceCfg.rconPasswordFile}") + instances; + }; + }; + + timers.backup-minecraft-fabric-servers = { + description = "Regularly backup Minecraft fabric servers"; + wantedBy = [ "timers.target" ]; + timerConfig = { + OnCalendar = "*-*-* 00/3:00:00"; + Persistent = true; + RandomizedDelaySec = "1h"; + }; + }; + }; + }; +} diff --git a/users/sterni/modules/common.nix b/users/sterni/modules/common.nix new file mode 100644 index 000000000000..2c513acad343 --- /dev/null +++ b/users/sterni/modules/common.nix @@ -0,0 +1,79 @@ +# This module is common in the weakest sense, i.e. contains common settings to +# all my machines contained in depot—as opposed to common to all my potential +# machines. Consequently, this module is currently very server-centric. +{ pkgs, lib, depot, config, ... }: + +let + me = "lukas"; +in + +{ + config = { + + # More common + + time.timeZone = "Europe/Berlin"; + + nix = { + package = pkgs.nix_2_3; + settings = { + trusted-public-keys = lib.mkAfter [ + "headcounter.org:/7YANMvnQnyvcVB6rgFTdb8p5LG1OTXaO+21CaOSBzg=" + ]; + substituters = lib.mkAfter [ + "https://hydra.build" + ]; + trusted-users = [ me ]; + }; + }; + tvl.cache.enable = true; + + programs.fish.enable = true; + + users = { + users = { + root.openssh.authorizedKeys.keys = depot.users.sterni.keys.all; + ${me} = { + isNormalUser = true; + extraGroups = [ "wheel" "http" "git" ]; + openssh.authorizedKeys.keys = depot.users.sterni.keys.all; + shell = pkgs.fish; + }; + }; + }; + + # Less common + + services = { + journald.extraConfig = '' + SystemMaxUse=10G + ''; + + openssh.enable = true; + }; + + programs = { + mosh.enable = true; + tmux.enable = true; + }; + + environment.systemPackages = [ + pkgs.wget + pkgs.git + pkgs.stow + pkgs.htop + pkgs.foot.terminfo + pkgs.vim + pkgs.smartmontools + ]; + + security.acme = { + defaults.email = builtins.getAttr "email" ( + builtins.head ( + builtins.filter (attrs: attrs.username == "sterni") depot.ops.users + ) + ); + acceptTerms = true; + }; + }; +} diff --git a/users/sterni/modules/default.nix b/users/sterni/modules/default.nix new file mode 100644 index 000000000000..5cc8be3cc6ce --- /dev/null +++ b/users/sterni/modules/default.nix @@ -0,0 +1,2 @@ +# Stop readTree from looking at this directory +_: { } diff --git a/users/sterni/modules/minecraft-fabric.nix b/users/sterni/modules/minecraft-fabric.nix new file mode 100644 index 000000000000..6cc32cd20587 --- /dev/null +++ b/users/sterni/modules/minecraft-fabric.nix @@ -0,0 +1,532 @@ +# Declarative, but low Nix module for a modded minecraft server using the +# fabric mod loader. That is to say, the build of the final server JAR +# is not encapsulated in a derivation. +# +# The module has the following interesting properties: +# +# * The fabric installer is executed on each server startup to assemble the +# patched server.jar. This is unfortunately necessary, as it seems to be +# difficult to do so in a derivation (fabric-installer accesses the network, +# the build doesn't seem to be reproducible). At least this avoids the +# question of the patched jar's redistributability. +# * RCON is used for starting and stopping which should prevent data loss, +# since we can issue a manual save command. +# * The entire runtime directory of the server is assembled from scratch on +# each start, so only blessed state (like the world) and declarative +# configuration (whitelist.json, server.properties, ...) survive. +# * It supports more than one server running on the same machine. +# +# Missing features: +# +# * Support for bans +# * Support for mutable whitelist, ops, … +# * Op levels +# +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2022-2024 sterni <sternenseemann@systemli.org> + +{ lib, pkgs, config, depot, ... }: + +let + # + # Dependencies + # + inherit (depot.nix.utils) storePathName; + inherit (depot.nix) getBins; + + bins = getBins pkgs.mcrcon [ "mcrcon" ] + // getBins pkgs.jre [ "java" ] + // getBins pkgs.diffutils [ "diff" ] + // getBins pkgs.moreutils [ "sponge" ] + // getBins pkgs.extrace [ "pwait" ] + // getBins pkgs.util-linux [ "flock" ]; + + # + # Needed JARs + # + fetchJar = { pname, version, url, sha256, passthru ? { } }: + pkgs.fetchurl { + name = "${pname}-${version}.jar"; + inherit url sha256; + passthru = passthru // { inherit version; }; + }; + + fabricInstallerJar = + fetchJar rec { + pname = "fabric-installer"; + version = "1.0.0"; + url = "https://maven.fabricmc.net/net/fabricmc/fabric-installer/${version}/fabric-installer-${version}.jar"; + sha256 = "0yrlzly1g5a80df27jvrbhxbp10xqxfyk64q0s0j13kz78fmnzkx"; + }; + + # log4j workaround for Minecraft Server >= 1.12 && < 1.17 + log4jFix_112_116 = pkgs.fetchurl { + url = "https://launcher.mojang.com/v1/objects/02937d122c86ce73319ef9975b58896fc1b491d1/log4j2_112-116.xml"; + sha256 = "1paha357xbaffl38ckzgdh4l5iib2ydqbv7jsg67nj31nlalclr9"; + }; + + serverJars = { + # Manually updated list of known minecraft `server.jar`s for now. + # Making this comprehensive isn't that interesting for now, since the module + # is annoying to use outside of depot anyways as it uses //nix. + "1.16.5" = fetchJar { + pname = "server"; + version = "1.16.5"; + url = "https://launcher.mojang.com/v1/objects/1b557e7b033b583cd9f66746b7a9ab1ec1673ced/server.jar"; + sha256 = "19ix6x5ij4jcwqam1dscnqwm0m251gysc2j793wjcrb9sb3jkwsq"; + passthru = { + baseJvmOpts = [ + "-Dlog4j.configurationFile=${log4jFix_112_116}" + ]; + }; + }; + "1.17" = fetchJar { + pname = "server"; + version = "1.17"; + url = "https://launcher.mojang.com/v1/objects/0a269b5f2c5b93b1712d0f5dc43b6182b9ab254e/server.jar"; + sha256 = "0jqz7hpx7zvjj2n5rfrh8jmdj6ziqyp8c9nq4sr4jmkbky6hsfbv"; + passthru.baseJvmOpts = [ + "-Dlog4j2.formatMsgNoLookups=true" + ]; + }; + "1.17.1" = fetchJar { + pname = "server"; + version = "1.17.1"; + url = "https://launcher.mojang.com/v1/objects/a16d67e5807f57fc4e550299cf20226194497dc2/server.jar"; + sha256 = "0pzmzagvrrapjsnd8xg4lqwynwnb5rcqk2n9h2kzba8p2fs13hp8"; + passthru.baseJvmOpts = [ + "-Dlog4j2.formatMsgNoLookups=true" + ]; + }; + "1.18" = fetchJar { + pname = "server"; + version = "1.18"; + url = "https://launcher.mojang.com/v1/objects/3cf24a8694aca6267883b17d934efacc5e44440d/server.jar"; + sha256 = "0vvycjcfq96z7cl5dsrq98k9b7j7l4x0y9nflrcqmcvink7fs5w4"; + passthru.baseJvmOpts = [ + "-Dlog4j2.formatMsgNoLookups=true" + ]; + }; + "1.18.1" = fetchJar { + pname = "server"; + version = "1.18.1"; + url = "https://launcher.mojang.com/v1/objects/125e5adf40c659fd3bce3e66e67a16bb49ecc1b9/server.jar"; + sha256 = "1pyvym6xzjb1siizzj4ma7lpb05qhgxnzps8lmlbk00lv0515kgb"; + }; + "1.18.2" = fetchJar { + pname = "server"; + version = "1.18.2"; + url = "https://launcher.mojang.com/v1/objects/c8f83c5655308435b3dcf03c06d9fe8740a77469/server.jar"; + sha256 = "0hx330bnadixph44sip0h5h986m11qxbdba6lbgwz4da6lg9vgjp"; + }; + "1.19" = fetchJar { + pname = "server"; + version = "1.19"; + url = "https://launcher.mojang.com/v1/objects/e00c4052dac1d59a1188b2aa9d5a87113aaf1122/server.jar"; + sha256 = "1cnjrqr2vn8gppd1y1lcdrc46fd7m1b3zl28zpbw72fgy1bd1vyy"; + }; + "1.19.1" = fetchJar { + pname = "server"; + version = "1.19.1"; + url = "https://piston-data.mojang.com/v1/objects/8399e1211e95faa421c1507b322dbeae86d604df/server.jar"; + sha256 = "0jnlb5z8a7qi6p6bbwnmdl77b8kq83ryfdp58dhx8kg2hf6lbfx8"; + }; + "1.19.2" = fetchJar { + pname = "server"; + version = "1.19.2"; + url = "https://piston-data.mojang.com/v1/objects/f69c284232d7c7580bd89a5a4931c3581eae1378/server.jar"; + sha256 = "15jdxh5zvsgvvk9hnv47swgjfg8fr653g6nx99q1rxpmkq32frxj"; + }; + "1.19.3" = fetchJar { + pname = "server"; + version = "1.19.3"; + url = "https://piston-data.mojang.com/v1/objects/c9df48efed58511cdd0213c56b9013a7b5c9ac1f/server.jar"; + sha256 = "06qykz3nq7qmfw4phs3wvq3nk28clg8s3qrs37856aai8b8kmgaf"; + }; + # Starting with 1.19.4 we could use --pidFile for systemd's PIDFile=, but as + # the service doesn't fork, there seems to be no point. + "1.19.4" = fetchJar { + pname = "server"; + version = "1.19.4"; + url = "https://piston-data.mojang.com/v1/objects/8f3112a1049751cc472ec13e397eade5336ca7ae/server.jar"; + sha256 = "0lrzpqd6zjvqh9g2byicgh66n43z0hwzp863r22ifx2hll6s2955"; + }; + # https://feedback.minecraft.net/hc/en-us/articles/16499677456781-Minecraft-Java-Edition-1-20-Trails-Tales + "1.20" = fetchJar { + name = "server"; + version = "1.20"; + url = "https://piston-data.mojang.com/v1/objects/15c777e2cfe0556eef19aab534b186c0c6f277e1/server.jar"; + sha256 = "0sym07vqrlbhyxxhlpz73ls0jh0g9qcl4plaa1scx0n1rr1cahgz"; + }; + # https://www.minecraft.net/en-us/article/minecraft--java-edition-1-20-1 + "1.20.1" = fetchJar { + pname = "server"; + version = "1.20.1"; + url = "https://piston-data.mojang.com/v1/objects/84194a2f286ef7c14ed7ce0090dba59902951553/server.jar"; + sha256 = "1q3r3c95vkai477r3gsmf2p0pmyl4zfn0qwl8y0y60m1qnfkmxrs"; + }; + # https://www.minecraft.net/en-us/article/minecraft-java-edition-1-20-2 + "1.20.2" = fetchJar { + pname = "server"; + version = "1.20.2"; + url = "https://piston-data.mojang.com/v1/objects/5b868151bd02b41319f54c8d4061b8cae84e665c/server.jar"; + sha256 = "1s7ag1p8v0vyzc6a8mjkd3rcf065hjb4avqa3zj4dbb9hn1y9bhx"; + }; + # https://www.minecraft.net/en-us/article/minecraft-java-edition-1-20-3 + "1.20.3" = fetchJar { + pname = "server"; + version = "1.20.3"; + url = "https://piston-data.mojang.com/v1/objects/4fb536bfd4a83d61cdbaf684b8d311e66e7d4c49/server.jar"; + sha256 = "1blb2cp1nlm0yr7yjhazj33g0hjlgfawx2v7y16h70pijfz8kv9n"; + }; + # https://www.minecraft.net/en-us/article/minecraft-java-edition-1-20-4 + "1.20.4" = fetchJar { + pname = "server"; + version = "1.20.4"; + url = "https://piston-data.mojang.com/v1/objects/8dd1a28015f51b1803213892b50b7b4fc76e594d/server.jar"; + sha256 = "0qykf9a3nacklqsyb30kg9m79nw462la6rf92gsdssdakprscgy0"; + }; + }; + + # + # mods directory for fabric + # + makeModFolder = name: mods: + pkgs.runCommand "${name}-fabric-mod-folder" { } ( + '' + mkdir -p "$out" + '' + lib.concatMapStrings + (mod: '' + test -f "${mod}" || { + printf 'Not a regular file: %s\n' "${mod}" >&2 + exit 1 + } + ln -s "${mod}" "$out/${storePathName mod}" + '') + mods + ); + + # + # Create a server.properties file + # + propertyValue = v: + if builtins.isBool v + then lib.boolToString v + else toString v; + + serverPropertiesFile = name: instanceCfg: + let + serverProperties' = + builtins.removeAttrs instanceCfg.serverProperties [ + "rcon.password" + ] // { + enable-rcon = true; + }; + in + pkgs.writeText "${name}-server.properties" ('' + # created by minecraft-fabric.nix + '' + lib.concatStrings (lib.mapAttrsToList + (key: value: '' + ${key}=${propertyValue value} + '') + serverProperties')); + + # + # Create JSON “state” files + # + writeJson = name: data: pkgs.writeText "${name}.json" (builtins.toJSON data); + + toWhitelist = name: uuid: { inherit name uuid; }; + + whitelistFile = name: instanceCfg: + writeJson "${name}-whitelist" ( + lib.mapAttrsToList toWhitelist instanceCfg.whitelist + ); + + opsFile = name: instanceCfg: + writeJson "${name}-ops" ( + lib.mapAttrsToList + (name: value: + toWhitelist name value // { + level = 4; + bypassesPlayerLimit = true; + } + ) + instanceCfg.ops + ); + + # + # Service start and stop scripts + # + stopScript = name: instanceCfg: + pkgs.writeShellScript "minecraft-fabric-${name}-stop" '' + set -eu + + # Before shutting down, display the diff between prescribed and used + # server.properties file for debugging purposes; filter out credential + actualProperties="''${RUNTIME_DIRECTORY}/server.properties" + sort "$actualProperties" | ${bins.sponge} "$actualProperties" + ( ${bins.diff} -u "${serverPropertiesFile name instanceCfg}" \ + "$actualProperties" \ + || true ) | grep -v rcon.password + + export MCRCON_HOST=localhost + export MCRCON_PORT=${lib.escapeShellArg instanceCfg.serverProperties."rcon.port"} + # Unfortunately, mcrcon can't read the password from a file + export MCRCON_PASS="$(cat "''${CREDENTIALS_DIRECTORY}/rcon-password")" + + # Send stop request + "${bins.mcrcon}" 'say Server is stopping' save-all stop + + # Wait for service to come down (systemd SIGTERMs right after ExecStop) + "${bins.flock}" "''${RUNTIME_DIRECTORY}" true + ''; + + startScript = name: instanceCfg: + let + serverJar = serverJars.${instanceCfg.version} or + (throw "Don't have server.jar for Minecraft Server ${instanceCfg.version}"); + + in + + pkgs.writeShellScript "minecraft-fabric-${name}-start" '' + set -eu + + cd "''${RUNTIME_DIRECTORY}" + + copyFromStore() { + install -m600 "$1" "$2" + } + + # Check if world is available + if test ! -d "${instanceCfg.world}"; then + echo "Could not find world, generating new one" >&2 + mkdir -p "${instanceCfg.world}" + fi + + # Put required files into place + echo eula=true > eula.txt + ln -s "${instanceCfg.world}" "${instanceCfg.level-name or "world"}" + copyFromStore "${serverJar}" server.jar + copyFromStore "${whitelistFile name instanceCfg}" whitelist.json + copyFromStore "${opsFile name instanceCfg}" ops.json + ln -s "${makeModFolder name instanceCfg.mods}" mods + + # Create config and set password from credentials (echo hopefully doesn't leak) + copyFromStore "${serverPropertiesFile name instanceCfg}" server.properties + echo "rcon.password=$(cat "$CREDENTIALS_DIRECTORY/rcon-password")" >> server.properties + + # Build patched jar + "${bins.java}" -jar "${fabricInstallerJar}" \ + server -mcversion "${instanceCfg.version}" + + # Lock is held as long as the server is running, so that we can wait for + # the actual shutdown in the stop script without relying on $MAINPID. + exec "${bins.flock}" "''${RUNTIME_DIRECTORY}" \ + "${bins.java}" \ + ${lib.escapeShellArgs (serverJar.baseJvmOpts or [ ] ++ instanceCfg.jvmOpts)} \ + -jar fabric-server-launch.jar nogui + ''; + + # + # Option types + # + impurePath = lib.types.path // { + name = "impurePath"; + check = x: + lib.types.path.check x + && !(builtins.isPath x) + && !(lib.hasPrefix builtins.storeDir (toString x)); + }; + + + instanceType = lib.types.submodule { + options = { + enable = lib.mkEnableOption "Minecraft server instance with the fabric mod loader"; + + version = lib.mkOption { + type = lib.types.str; + description = "Minecraft Server version to use."; + example = "1.16.5"; + }; + + mods = lib.mkOption { + type = with lib.types; listOf package; + description = "List of fabric mod JARs to load."; + default = [ ]; + }; + + world = lib.mkOption { + type = impurePath; + description = "Path to the Minecraft world folder to use."; + example = "/var/minecraft/world"; + }; + + jvmOpts = lib.mkOption { + type = with lib.types; listOf str; + default = [ ]; + example = [ + "-Xmx2048M" + "-Xms2048M" + ]; + description = '' + Options to pass to + <citerefentry> + <refentrytitle>java</refentrytitle> + <manvolnum>1</manvolnum> + </citerefentry> + in order to tweak the runtime of the JVM. + ''; + }; + + user = lib.mkOption { + type = lib.types.str; + default = "minecraft"; + description = '' + Name of an existing user to run the server as. Needs to have write + access to the specified world. + ''; + }; + + group = lib.mkOption { + type = lib.types.str; + default = "users"; + description = '' + Name of an existing group to run the server under. + ''; + }; + + rconPasswordFile = lib.mkOption { + type = impurePath; + description = '' + File (outised the store) that stores the password to use for Minecraft's + RCON interface. + ''; + example = "/var/secrets/minecraft-rcon"; + }; + + whitelist = lib.mkOption { + type = with lib.types; attrsOf str; + description = '' + Attribute set mapping whitelisted user names to their user ids. + ''; + example = { + sternenseemann = "d8e48069-1905-4886-a5da-a4ee917ee254"; + }; + }; + + ops = lib.mkOption { + type = with lib.types; attrsOf str; + description = '' + Attribute set mapping op-ed user names to their user ids. + Setting permission levels is not possible at the moment, + set to 4 by default. + ''; + example = { + sternenseemann = "d8e48069-1905-4886-a5da-a4ee917ee254"; + }; + }; + + serverProperties = lib.mkOption { + type = lib.types.submodule { + freeformType = lib.types.attrs; + + # Only options the module needs to access are declared explicitly + options = { + server-port = lib.mkOption { + type = lib.types.port; + default = 25565; + description = '' + Port to listen on. + ''; + }; + + "rcon.port" = lib.mkOption { + type = lib.types.port; + default = 25575; + description = '' + Port to use for the RCON control mechanism. + ''; + }; + }; + }; + }; + }; + }; + + cfg = config.services.minecraft-fabric-server; + + serverPorts = lib.mapAttrsToList + (_: instanceCfg: + instanceCfg.serverProperties.server-port + ) + cfg; + + rconPorts = lib.mapAttrsToList + (_: instanceCfg: + instanceCfg.serverProperties."rcon.port" + ) + cfg; +in + +{ + options = { + services.minecraft-fabric-server = lib.mkOption { + type = with lib.types; attrsOf instanceType; + default = { }; + description = "Minecraft server instances with the fabric mod loader"; + }; + }; + + config = { + assertions = [ + { + assertion = builtins.all (instance: !instance.enable) (builtins.attrValues cfg) + || pkgs.config.allowUnfreeRedistributable or false + || pkgs.config.allowUnfree or false; + message = lib.concatStringsSep " " [ + "You need to allow unfree software for minecraft," + "as you'll implicitly agree to Mojang's EULA." + ]; + } + { + assertion = + let + allPorts = serverPorts ++ rconPorts; + in + lib.unique allPorts == allPorts; + message = "All assigned ports need to be unique."; + } + ]; + + systemd.services = lib.mapAttrs' + (name: instanceCfg: + { + name = "minecraft-fabric-${name}"; + value = { + description = "Minecraft server ${name} with the fabric mod loader"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + inherit (instanceCfg) enable; + + serviceConfig = { + Type = "simple"; + User = instanceCfg.user; + Group = instanceCfg.group; + ExecStart = startScript name instanceCfg; + ExecStop = stopScript name instanceCfg; + RuntimeDirectory = "minecraft-fabric-${name}"; + LoadCredential = "rcon-password:${instanceCfg.rconPasswordFile}"; + RestartSec = "40s"; + }; + }; + } + ) + cfg; + + networking.firewall = { + allowedTCPPorts = serverPorts; + allowedUDPPorts = serverPorts; + }; + }; +} diff --git a/users/sterni/nix/build/buildGopherHole/default.nix b/users/sterni/nix/build/buildGopherHole/default.nix new file mode 100644 index 000000000000..eec13a865421 --- /dev/null +++ b/users/sterni/nix/build/buildGopherHole/default.nix @@ -0,0 +1,109 @@ +{ depot, pkgs, lib, ... }: + +let + inherit (pkgs) + runCommand + writeText + ; + + inherit (depot.users.sterni.nix.build) + buildGopherHole + ; + + fileTypes = { + # RFC1436 + text = "0"; + menu = "1"; + cso = "2"; + error = "3"; + binhex = "4"; + dos = "5"; + uuencoded = "6"; + index-server = "7"; + telnet = "8"; + binary = "9"; + mirror = "+"; + gif = "g"; + image = "I"; + tn3270 = "T"; + # non-standard + info = "i"; + html = "h"; + }; + + buildFile = { file, name, fileType ? fileTypes.text }: + runCommand name + { + passthru = { + # respect the file type the file derivation passes + # through. otherwise use explicitly set type or + # default value. + fileType = file.fileType or fileType; + }; + } '' + ln -s ${file} "$out" + ''; + + buildGopherMap = dir: + let + /* strings constitute an info line or an empty line + if their length is zero. sets that contain a menu + value have that added to the gophermap as-is. + + all other entries should be a set which can be built using + buildGopherHole and is linked by their name. The resulting + derivation is expected to passthru a fileType containing the + gopher file type char of themselves. + */ + gopherMapLine = e: + if builtins.isString e + then e + else if e ? menu + then e.menu + else + let + drv = buildGopherHole e; + title = e.title or e.name; + in + "${drv.fileType}${title}\t${drv.name}"; + in + writeText ".gophermap" (lib.concatMapStringsSep "\n" gopherMapLine dir); + + buildDir = + { dir, name, ... }: + + let + # filter all entries out that have to be symlinked: + # sets with the file or dir attribute + drvOnly = builtins.map buildGopherHole (builtins.filter + (x: !(builtins.isString x) && (x ? dir || x ? file)) + dir); + gopherMap = buildGopherMap dir; + in + runCommand name + { + passthru = { + fileType = fileTypes.dir; + }; + } + ('' + mkdir -p "$out" + ln -s "${gopherMap}" "$out/.gophermap" + '' + lib.concatMapStrings + (drv: '' + ln -s "${drv}" "$out/${drv.name}" + '') + drvOnly); +in + +{ + # Dispatch into different file / dir handling code + # which is mutually recursive with this function. + __functor = _: args: + if args ? file then buildFile args + else if args ? dir then buildDir args + else builtins.throw "Unrecognized gopher hole item type: " + + lib.generators.toPretty { } args; + + inherit fileTypes; +} diff --git a/users/sterni/nix/char/all-chars.bin b/users/sterni/nix/char/all-chars.bin new file mode 100644 index 000000000000..017b909e8e8e --- /dev/null +++ b/users/sterni/nix/char/all-chars.bin @@ -0,0 +1,2 @@ + + !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~ \ No newline at end of file diff --git a/users/sterni/nix/char/default.nix b/users/sterni/nix/char/default.nix new file mode 100644 index 000000000000..9c6ce2fb250b --- /dev/null +++ b/users/sterni/nix/char/default.nix @@ -0,0 +1,99 @@ +{ depot, lib, pkgs, ... }: + +let + + inherit (depot.users.sterni.nix.flow) + cond + ; + + inherit (depot.nix) + yants + ; + + inherit (depot.users.sterni.nix) + string + ; + + # A char is the atomic element of a nix string + # which is essentially an array of arbitrary bytes + # as long as they are not a NUL byte. + # + # A char is neither a byte nor a unicode codepoint! + char = yants.restrict "char" (s: builtins.stringLength s == 1) yants.string; + + # integer representation of char + charval = yants.restrict "charval" (i: i >= 1 && i < 256) yants.int; + + allChars = builtins.readFile ./all-chars.bin; + + # Originally I searched a list for this, but came to the + # conclusion that this can never be fast enough in Nix. + # We therefore use a solution similar to infinisil's. + ordMap = builtins.listToAttrs + (lib.imap1 (i: v: { name = v; value = i; }) + (string.toChars allChars)); + + # Note on performance: + # chr and ord have been benchmarked using the following cases: + # + # builtins.map ord (lib.stringToCharacters allChars) + # builtins.map chr (builtins.genList (int.add 1) 255 + # + # The findings are as follows: + # 1. Searching through either strings using recursion is + # unbearably slow in Nix, leading to evaluation times + # of up to 3s for the following very small test case. + # This is why we use the trusty attribute set for ord. + # 2. String indexing is much faster than list indexing which + # is why we use the former for chr. + ord = c: ordMap."${c}"; + + chr = i: string.charAt (i - 1) allChars; + + asciiAlpha = c: + let + v = ord c; + in + (v >= 65 && v <= 90) + || (v >= 97 && v <= 122); + + asciiNum = c: + let + v = ord c; + in + v >= 48 && v <= 57; + + asciiAlphaNum = c: asciiAlpha c || asciiNum c; + +in +{ + inherit + allChars + char + charval + ord + chr + asciiAlpha + asciiNum + asciiAlphaNum + ; + + # originally I generated a nix file containing a list of + # characters, but infinisil uses a better way which I adapt + # which is using builtins.readFile instead of import. + __generateAllChars = pkgs.runCommandCC "generate-all-chars" + { + source = '' + #include <stdio.h> + + int main(void) { + for(int i = 1; i <= 0xff; i++) { + putchar(i); + } + } + ''; + passAsFile = [ "source" ]; + } '' + $CC -o "$out" -x c "$sourcePath" + ''; +} diff --git a/users/sterni/nix/char/tests/default.nix b/users/sterni/nix/char/tests/default.nix new file mode 100644 index 000000000000..cb17b74c578f --- /dev/null +++ b/users/sterni/nix/char/tests/default.nix @@ -0,0 +1,31 @@ +{ depot, ... }: + +let + inherit (depot.nix.runTestsuite) + it + assertEq + runTestsuite + ; + + inherit (depot.users.sterni.nix) + char + string + num + fun + ; + + charList = string.toChars char.allChars; + + testAllCharConversion = it "tests conversion of all chars" [ + (assertEq "char.chr converts to char.allChars" + (builtins.genList (fun.rl char.chr (num.add 1)) 255) + charList) + (assertEq "char.ord converts from char.allChars" + (builtins.genList (num.add 1) 255) + (builtins.map char.ord charList)) + ]; + +in +runTestsuite "char" [ + testAllCharConversion +] diff --git a/users/sterni/nix/float/default.nix b/users/sterni/nix/float/default.nix new file mode 100644 index 000000000000..ecb6465c8842 --- /dev/null +++ b/users/sterni/nix/float/default.nix @@ -0,0 +1,23 @@ +{ depot, ... }: + +let + inherit (depot.users.sterni.nix) + num + ; +in + +rec { + # In C++ Nix, the required builtins have been added in version 2.4 + ceil = builtins.ceil or (throw "Nix implementation is missing builtins.ceil"); + floor = builtins.floor or (throw "Nix implementation is missing builtins.floor"); + + truncate = f: if f >= 0 then floor f else ceil f; + round = f: + let + s = num.sign f; + a = s * f; + in + s * (if a >= floor a + 0.5 then ceil a else floor a); + + intToFloat = i: i * 1.0; +} diff --git a/users/sterni/nix/float/tests/default.nix b/users/sterni/nix/float/tests/default.nix new file mode 100644 index 000000000000..75e2a1bfa091 --- /dev/null +++ b/users/sterni/nix/float/tests/default.nix @@ -0,0 +1,49 @@ +{ depot, lib, ... }: + +let + + inherit (depot.nix.runTestsuite) + runTestsuite + it + assertEq + ; + + inherit (depot.users.sterni.nix) + float + ; + + testsBuiltins = it "tests builtin operations" [ + (assertEq "ceil pos" (float.ceil 1.5) 2) + (assertEq "ceil neg" (float.ceil (-1.5)) (-1)) + (assertEq "floor pos" (float.floor 1.5) 1) + (assertEq "floor neg" (float.floor (-1.5)) (-2)) + ]; + + testsConversionFrom = it "tests integer to float conversion" [ + (assertEq "float.intToFloat is identity for floats" (float.intToFloat 1.3) 1.3) + (assertEq "float.intToFloat converts ints" + (builtins.all + (val: builtins.isFloat val) + (builtins.map float.intToFloat (builtins.genList (i: i - 500) 1000))) + true) + ]; + + exampleFloats = [ 0.5 0.45 0.3 0.1 200 203.457847 204.65547 (-1.5) (-2) (-1.3) (-0.45) ]; + testsConversionTo = it "tests float to integer conversion" [ + (assertEq "round" + (builtins.map float.round exampleFloats) + [ 1 0 0 0 200 203 205 (-2) (-2) (-1) 0 ]) + (assertEq "truncate towards zero" + (builtins.map float.truncate exampleFloats) + [ 0 0 0 0 200 203 204 (-1) (-2) (-1) 0 ]) + ]; +in + +runTestsuite "nix.num" ([ + testsConversionFrom +] + # Skip for e.g. C++ Nix < 2.4 +++ lib.optionals (builtins ? ceil && builtins ? floor) [ + testsConversionTo + testsBuiltins +]) diff --git a/users/sterni/nix/flow/default.nix b/users/sterni/nix/flow/default.nix new file mode 100644 index 000000000000..4bef0abb91e9 --- /dev/null +++ b/users/sterni/nix/flow/default.nix @@ -0,0 +1,83 @@ +{ depot, ... }: + +let + + inherit (depot.nix) + yants + ; + + inherit (depot.users.sterni.nix) + fun + ; + + # we must avoid evaluating any of the sublists + # as they may contain conditions that throw + condition = yants.restrict "condition" + (ls: builtins.length ls == 2) + (yants.list yants.any); + + /* Like the common lisp macro: takes a list + of two elemented lists whose first element + is a boolean. The second element of the + first list that has true as its first + element is returned. + + Type: [ [ bool a ] ] -> a + + Example: + + cond [ + [ (builtins.isString true) 12 ] + [ (3 == 2) 13 ] + [ true 42 ] + ] + + => 42 + */ + cond = conds: switch true conds; + + /* Generic pattern match-ish construct for nix. + Takes a bunch of lists which are of length + two and checks the first element for either + a predicate or a value. The second value of + the first list which either has a value equal + to or a function that evaluates to true for + the given value. + + Type: a -> [ [ (function | a) b ] ] -> b + + Example: + + switch "foo" [ + [ "smol" "SMOL!!!" ] + [ (x: builtins.stringLength x <= 3) "smol-ish" ] + [ (fun.const true) "not smol" ] + ] + + => "smol-ish" + */ + switch = x: conds: + if builtins.length conds == 0 + then builtins.throw "exhausted all conditions" + else + let + c = condition (builtins.head conds); + s = builtins.head c; + b = + if builtins.isFunction s + then s x + else x == s; + in + if b + then builtins.elemAt c 1 + else switch x (builtins.tail conds); + + + +in +{ + inherit + cond + switch + ; +} diff --git a/users/sterni/nix/flow/tests/default.nix b/users/sterni/nix/flow/tests/default.nix new file mode 100644 index 000000000000..9f974a61c7b2 --- /dev/null +++ b/users/sterni/nix/flow/tests/default.nix @@ -0,0 +1,39 @@ +{ depot, ... }: + +let + + inherit (depot.nix.runTestsuite) + runTestsuite + it + assertEq + assertThrows + ; + + inherit (depot.users.sterni.nix.flow) + cond + match + ; + + dontEval = builtins.throw "this should not get evaluated"; + + testCond = it "tests cond" [ + (assertThrows "malformed cond list" + (cond [ [ true 1 2 ] [ false 1 ] ])) + (assertEq "last is true" "last" + (cond [ + [ false dontEval ] + [ false dontEval ] + [ true "last" ] + ])) + (assertEq "first is true" 1 + (cond [ + [ true 1 ] + [ true dontEval ] + [ true dontEval ] + ])) + ]; + +in +runTestsuite "nix.flow" [ + testCond +] diff --git a/users/sterni/nix/fun/default.nix b/users/sterni/nix/fun/default.nix new file mode 100644 index 000000000000..824cebfed244 --- /dev/null +++ b/users/sterni/nix/fun/default.nix @@ -0,0 +1,255 @@ +{ depot, lib, ... }: + +let + + inherit (lib) + id + ; + + # Simple function composition, + # application is right to left. + rl = f1: f2: + (x: f1 (f2 x)); + + # Compose a list of functions, + # application is right to left. + rls = fs: + builtins.foldl' (fOut: f: lr f fOut) id fs; + + # Simple function composition, + # application is left to right. + lr = f1: f2: + (x: f2 (f1 x)); + + # Compose a list of functions, + # application is left to right + lrs = x: fs: + builtins.foldl' (v: f: f v) x fs; + + # Warning: cursed function + # + # Check if a function has an attribute + # set pattern with an ellipsis as its argument. + # + # s/o to puck for discovering that you could use + # builtins.toXML to introspect functions more than + # you should be able to in Nix. + hasEllipsis = f: + builtins.isFunction f && + builtins.match ".*<attrspat ellipsis=\"1\">.*" + (builtins.toXML f) != null; + + /* Return the number of arguments the given function accepts or 0 if the value + is not a function. + + Example: + + argCount argCount + => 1 + + argCount builtins.add + => 2 + + argCount pkgs.stdenv.mkDerivation + => 1 + */ + argCount = f: + let + # N.B. since we are only interested if the result of calling is a function + # as opposed to a normal value or evaluation failure, we never need to + # check success, as value will be false (i.e. not a function) in the + # failure case. + called = builtins.tryEval ( + f (builtins.throw "You should never see this error message") + ); + in + if !(builtins.isFunction f || builtins.isFunction (f.__functor or null)) + then 0 + else 1 + argCount called.value; + + /* Call a given function with a given list of arguments. + + Example: + + apply builtins.sub [ 20 10 ] + => 10 + */ + apply = f: args: + builtins.foldl' (f: x: f x) f args; + + # TODO(sterni): think of a better name for unapply + /* Collect n arguments into a list and pass them to the given function. + Allows calling a function that expects a list by feeding it the list + elements individually as function arguments - the limitation is + that the list must be of constant length. + + This is mainly useful for functions that wrap other, arbitrary functions + in conjunction with argCount and apply, since lists of arguments are + easier to deal with usually. + + Example: + + (unapply 3 lib.id) 1 2 3 + => [ 1 2 3 ] + + (unapply 5 lib.reverse) 1 2 null 4 5 + => [ 5 4 null 2 1 ] + + # unapply and apply compose the identity relation together + + unapply (argCount f) (apply f) + # is equivalent to f (if the function has a constant number of arguments) + + (unapply 2 (apply builtins.sub)) 20 10 + => 10 + */ + unapply = + let + unapply' = acc: n: f: x: + if n == 1 + then f (acc ++ [ x ]) + else unapply' (acc ++ [ x ]) (n - 1) f; + in + unapply' [ ]; + + /* Optimize a tail recursive Nix function by intercepting the recursive + function application and expressing it in terms of builtins.genericClosure + instead. The main benefit of this optimization is that even a naively + written recursive algorithm won't overflow the stack. + + For this to work the following things prerequisites are necessary: + + - The passed function needs to be a fix point for its self reference, + i. e. the argument to tailCallOpt needs to be of the form + `self: # function body that uses self to call itself`. + This is because tailCallOpt needs to manipulate the call to self + which otherwise wouldn't be possible due to Nix's lexical scoping. + + - The passed function may only call itself as a tail call, all other + forms of recursions will fail evaluation. + + This function was mainly written to prove that builtins.genericClosure + can be used to express any (tail) recursive algorithm. It can be used + to avoid stack overflows for deeply recursive, but naively written + functions (in the context of Nix this mainly means using recursion + instead of (ab)using more performant and less limited builtins). + A better alternative to using this function is probably translating + the algorithm to builtins.genericClosure manually. Also note that + using tailCallOpt doesn't mean that the stack won't ever overflow: + Data structures, especially lazy ones, can still cause all the + available stack space to be consumed. + + The optimization also only concerns avoiding stack overflows, + tailCallOpt will make functions slower if anything. + + Type: (F -> F) -> F where F is any tail recursive function. + + Example: + + let + label' = self: acc: n: + if n == 0 + then "This is " + acc + "cursed." + else self (acc + "very ") (n - 1); + + # Equivalent to a naive recursive implementation in Nix + label = (lib.fix label') ""; + + labelOpt = (tailCallOpt label') ""; + in + + label 5 + => "This is very very very very very cursed." + + labelOpt 5 + => "This is very very very very very cursed." + + label 10000 + => error: stack overflow (possible infinite recursion) + + labelOpt 10000 + => "This is very very very very very very very very very… + */ + tailCallOpt = f: + let + argc = argCount (lib.fix f); + + # This function simulates being f for f's self reference. Instead of + # recursing, it will just return the arguments received as a specially + # tagged set, so the recursion step can be performed later. + fakef = unapply argc (args: { + __tailCall = true; + inherit args; + }); + # Pass fakef to f so that it'll be called instead of recursing, ensuring + # only one recursion step is performed at a time. + encodedf = f fakef; + + opt = args: + let + steps = builtins.genericClosure { + # This is how we encode a (tail) call: A set with final == false + # and the list of arguments to pass to be found in args. + startSet = [ + { + key = 0; + final = false; + inherit args; + } + ]; + + operator = + { key, final, ... }@state: + let + # Plumbing to make genericClosure happy + newId = { + key = key + 1; + }; + + # Perform recursion step + call = apply encodedf state.args; + + # If call encodes a new call, return the new encoded call, + # otherwise signal that we're done. + newState = + if builtins.isAttrs call && call.__tailCall or false + then newId // { + final = false; + inherit (call) args; + } else newId // { + final = true; + value = call; + }; + in + + if final + then [ ] # end condition for genericClosure + else [ newState ]; + }; + in + # The returned list contains intermediate steps we ignore. + (builtins.head (builtins.filter (x: x.final) steps)).value; + in + unapply argc opt; +in + +{ + inherit (lib) + fix + flip + const + ; + + inherit + id + rl + rls + lr + lrs + hasEllipsis + argCount + tailCallOpt + apply + unapply + ; +} diff --git a/users/sterni/nix/fun/tests/default.nix b/users/sterni/nix/fun/tests/default.nix new file mode 100644 index 000000000000..6b1e6fcc7b0b --- /dev/null +++ b/users/sterni/nix/fun/tests/default.nix @@ -0,0 +1,82 @@ +{ depot, ... }: + +let + inherit (depot.nix.runTestsuite) + runTestsuite + it + assertEq + ; + + inherit (depot.nix) escapeExecline; + + inherit (depot.users.sterni.nix) + fun + ; + + hasEllipsisTests = it "checks fun.hasEllipsis" [ + (assertEq "Malicious string" false + (fun.hasEllipsis (builtins.toXML ({ foo, ... }: 12)))) + (assertEq "No function" false + (fun.hasEllipsis 23)) + (assertEq "No attribute set pattern" false + (fun.hasEllipsis (a: a + 2))) + (assertEq "No ellipsis" false + (fun.hasEllipsis ({ foo, bar }: foo + bar))) + (assertEq "Ellipsis" true + (fun.hasEllipsis ({ depot, pkgs, ... }: 42))) + ]; + + argCountTests = it "checks fun.argCount" [ + (assertEq "builtins.sub has two arguments" 2 + (fun.argCount builtins.sub)) + (assertEq "fun.argCount has one argument" 1 + (fun.argCount fun.argCount)) + (assertEq "runTestsuite has two arguments" 2 + (fun.argCount runTestsuite)) + ]; + + applyTests = it "checks that fun.apply is equivalent to calling" [ + (assertEq "fun.apply builtins.sub" (builtins.sub 23 42) + (fun.apply builtins.sub [ 23 42 ])) + (assertEq "fun.apply escapeExecline" (escapeExecline [ "foo" [ "bar" ] ]) + (fun.apply escapeExecline [ [ "foo" [ "bar" ] ] ])) + ]; + + unapplyTests = it "checks fun.unapply" [ + (assertEq "fun.unapply 3 accepts 3 args" 3 + (fun.argCount (fun.unapply 3 fun.id))) + (assertEq "fun.unapply 73 accepts 73 args" 73 + (fun.argCount (fun.unapply 73 fun.id))) + (assertEq "fun.unapply 1 accepts 73 args" 1 + (fun.argCount (fun.unapply 1 fun.id))) + (assertEq "fun.unapply collects arguments correctly" + (fun.unapply 5 fun.id 1 2 3 4 5) + [ 1 2 3 4 5 ]) + (assertEq "fun.unapply calls the given function correctly" 1 + (fun.unapply 1 builtins.head 1)) + ]; + + fac' = self: acc: n: if n == 0 then acc else self (n * acc) (n - 1); + + facPlain = fun.fix fac' 1; + facOpt = fun.tailCallOpt fac' 1; + + tailCallOptTests = it "checks fun.tailCallOpt" [ + (assertEq "optimized and unoptimized factorial have the same base case" + (facPlain 0) + (facOpt 0)) + (assertEq "optimized and unoptimized factorial have same value for 1" + (facPlain 1) + (facOpt 1)) + (assertEq "optimized and unoptimized factorial have same value for 100" + (facPlain 100) + (facOpt 100)) + ]; +in +runTestsuite "nix.fun" [ + hasEllipsisTests + argCountTests + applyTests + unapplyTests + tailCallOptTests +] diff --git a/users/sterni/nix/html/README.md b/users/sterni/nix/html/README.md new file mode 100644 index 000000000000..0349e466a166 --- /dev/null +++ b/users/sterni/nix/html/README.md @@ -0,0 +1,148 @@ +# html.nix — _the_ most cursed Nix HTML DSL + +A quick example to show you what it looks like: + +```nix +# Note: this example is for standalone usage out of depot +{ pkgs ? import <nixpkgs> {} }: + +let + # zero dependency, one file implementation + htmlNix = import ./path/to/html.nix { }; + + # make the magic work + inherit (htmlNix) __findFile esc withDoctype; +in + +pkgs.writeText "example.html" (withDoctype (<html> {} [ + (<head> {} [ + (<meta> { charset = "utf-8"; } null) + (<title> {} (esc "hello world")) + ]) + (<body> {} [ + (<h1> {} (esc "hello world")) + (<p> { class = "intro"; } (esc '' + welcome to the land of sillyness! + '')) + (<ul> {} [ + (<li> {} [ + (esc "check out ") + (<a> { href = "https://code.tvl.fyi"; } "depot") + ]) + (<li> {} [ + (esc "find ") + (<a> { href = "https://cl.tvl.fyi/q/hashtag:cursed"; } "cursed things") + ]) + ]) + ]) +])) +``` + +Convince yourself it works: + +```console +$ $BROWSER $(nix-build example.nix) +``` + +Alternatively, in depot: + +```console +$ $BROWSER $(nix-build -A users.sterni.nix.html.tests) +``` + +## Creating tags + +An empty tag is passed `null` as its content argument: + +```nix +<link> { + rel = "stylesheet"; + href = "/main.css"; + type = "text/css"; +} null + +# => "<link href=\"/main.css\" rel=\"stylesheet\" type=\"text/css\"/>" +``` + +Content is expected to be HTML: + +```nix +<div> { class = "foo"; } "<strong>hi</strong>" + +# => "<div class=\"foo\"><strong>hi</strong></div>" +``` + +If it's not, be sure to escape it: + +```nix +<p> {} (esc "A => B") + +# => "<p>A => B</p>" +``` + +Nesting tags works of course: + +```nix +<div> {} (<strong> {} (<em> {} "hi")) + +# => "<div><strong><em>hi</em></strong></div>" +``` + +If the content of a tag is a list, it's concatenated: + +```nix +<h1> {} [ + (esc "The ") + (<strong> {} "Nix") + (esc " ") + (<em> {} "Expression") + (esc " Language") +] + +# => "<h1>The <strong>Nix</strong> <em>Expression</em> Language</h1>" +``` + +More detailed documentation can be found in `nixdoc`-compatible +comments in the source file (`default.nix` in this directory). + +## How does this work? + +*Theoretically* expressions like `<nixpkgs>` are just ordinary paths — +their actual value is determined from `NIX_PATH`. `html.nix` works +because of how this is actually implemented: At [parse time][spath-parsing] +Nix transparently translates an expression like `<foo>` into +`__findFile __nixPath "foo"`: + +``` +nix-repl> <nixpkgs> +/nix/var/nix/profiles/per-user/root/channels/vuizvui/nixpkgs + +nix-repl> __findFile __nixPath "nixpkgs" +/nix/var/nix/profiles/per-user/root/channels/vuizvui/nixpkgs +``` + +This translation doesn't take any scoping issues into account -- +so we can just shadow `__findFile` and make it return anything, +even a function: + +``` +nix-repl> __findFile = nixPath: str: + /**/ if str == "double" then x: x * 2 + else if str == "triple" then x: x * 3 + else throw "what?" + +nix-repl> <double> 2 +4 + +nix-repl> <triple> 3 +9 + +nix-repl> <quadruple> 4 +error: what? +``` + +Exactly this is what we are doing in `html.nix`: +Using `let inherit (htmlNix) __findFile; in` we shadow the builtin `__findFile` +with a function which returns a function rendering a particular HTML tag. + +[spath-parsing]: https://github.com/NixOS/nix/blob/293220bed5a75efc963e33c183787e87e55e28d9/src/libexpr/parser.y#L410-L416 diff --git a/users/sterni/nix/html/default.nix b/users/sterni/nix/html/default.nix new file mode 100644 index 000000000000..d25a7ab8dac0 --- /dev/null +++ b/users/sterni/nix/html/default.nix @@ -0,0 +1,122 @@ +# Copyright © 2021 sterni +# SPDX-License-Identifier: MIT +# +# This file provides a cursed HTML DSL for nix which works by overloading +# the NIX_PATH lookup operation via angle bracket operations, e. g. `<nixpkgs>`. + +{ ... }: + +let + /* Escape everything we have to escape in an HTML document if either + in a normal context or an attribute string (`<>&"'`). + + A shorthand for this function called `esc` is also provided. + + Type: string -> string + + Example: + + escapeMinimal "<hello>" + => "<hello>" + */ + escapeMinimal = builtins.replaceStrings + [ "<" ">" "&" "\"" "'" ] + [ "<" ">" "&" """ "'" ]; + + /* Return a string with a correctly rendered tag of the given name, + with the given attributes which are automatically escaped. + + If the content argument is `null`, the tag will have no children nor a + closing element. If the content argument is a string it is used as the + content as is (unescaped). If the content argument is a list, its + elements are concatenated. + + `renderTag` is only an internal function which is reexposed as `__findFile` + to allow for much neater syntax than calling `renderTag` everywhere: + + ```nix + { depot, ... }: + let + inherit (depot.users.sterni.nix.html) __findFile esc; + in + + <html> {} [ + (<head> {} (<title> {} (esc "hello world"))) + (<body> {} [ + (<h1> {} (esc "hello world")) + (<p> {} (esc "foo bar")) + ]) + ] + + ``` + + As you can see, the need to call a function disappears, instead the + `NIX_PATH` lookup operation via `<foo>` is overloaded, so it becomes + `renderTag "foo"` automatically. + + Since the content argument may contain the result of other `renderTag` + calls, we can't escape it automatically. Instead this must be done manually + using `esc`. + + Type: string -> attrs<string> -> (list<string> | string | null) -> string + + Example: + + <link> { + rel = "stylesheet"; + href = "/css/main.css"; + type = "text/css"; + } null + + renderTag "link" { + rel = "stylesheet"; + href = "/css/main.css"; + type = "text/css"; + } null + + => "<link href=\"/css/main.css\" rel=\"stylesheet\" type=\"text/css\"/>" + + <p> {} [ + "foo " + (<strong> {} "bar") + ] + + renderTag "p" {} "foo <strong>bar</strong>" + => "<p>foo <strong>bar</strong></p>" + */ + renderTag = tag: attrs: content: + let + attrs' = builtins.concatStringsSep "" ( + builtins.map + (n: + " ${escapeMinimal n}=\"${escapeMinimal (toString attrs.${n})}\"" + ) + (builtins.attrNames attrs) + ); + content' = + if builtins.isList content + then builtins.concatStringsSep "" content + else content; + in + if content == null + then "<${tag}${attrs'}/>" + else "<${tag}${attrs'}>${content'}</${tag}>"; + + /* Prepend "<!DOCTYPE html>" to a string. + + Type: string -> string + + Example: + + withDoctype (<body> {} (esc "hello")) + => "<!DOCTYPE html><body>hello</body>" + */ + withDoctype = doc: "<!DOCTYPE html>" + doc; + +in +{ + inherit escapeMinimal renderTag withDoctype; + + __findFile = _: renderTag; + esc = escapeMinimal; +} diff --git a/users/sterni/nix/html/tests/default.nix b/users/sterni/nix/html/tests/default.nix new file mode 100644 index 000000000000..ed520675c55a --- /dev/null +++ b/users/sterni/nix/html/tests/default.nix @@ -0,0 +1,93 @@ +{ depot, pkgs, ... }: + +let + inherit (depot.users.sterni.nix.html) + __findFile + esc + withDoctype + ; + + exampleDocument = withDoctype (<html> { lang = "en"; } [ + (<head> { } [ + (<meta> { charset = "utf-8"; } null) + (<title> { } "html.nix example document") + (<link> + { + rel = "license"; + href = "https://code.tvl.fyi/about/LICENSE"; + type = "text/html"; + } + null) + (<style> { } (esc '' + hgroup h2 { + font-weight: normal; + } + + dd { + margin: 0; + } + '')) + ]) + (<body> { } [ + (<main> { } [ + (<hgroup> { } [ + (<h1> { } (esc "html.nix")) + (<h2> { } [ + (<em> { } "the") + (esc " most cursed HTML DSL ever!") + ]) + ]) + (<dl> { } [ + (<dt> { } [ + (esc "Q: Wait, it's all ") + (<a> + { + href = "https://cl.tvl.fyi/q/hashtag:cursed"; + } + (esc "cursed")) + (esc " nix hacks?") + ]) + (<dd> { } (esc "A: Always has been. 🔫")) + (<dt> { } (esc "Q: Why does this work?")) + (<dd> { } [ + (esc "Because nix ") + (<a> + { + href = "https://github.com/NixOS/nix/blob/293220bed5a75efc963e33c183787e87e55e28d9/src/libexpr/parser.y#L410-L416"; + } + (esc "translates ")) + (<a> + { + href = "https://github.com/NixOS/nix/blob/293220bed5a75efc963e33c183787e87e55e28d9/src/libexpr/lexer.l#L100"; + } + (esc "SPATH tokens")) + (esc " like ") + (<code> { } (esc "<nixpkgs>")) + (esc " into calls to ") + (<code> { } (esc "__findFile")) + (esc " in the ") + (<em> { } (esc "current")) + (esc " scope.") + ]) + ]) + ]) + ]) + ]); +in + +pkgs.runCommand "html.nix.html" +{ + passAsFile = [ "exampleDocument" ]; + inherit exampleDocument; + nativeBuildInputs = [ pkgs.html5validator ]; +} '' + set -x + test "${esc "<> && \" \'"}" = "<> && " '" + + # slow as hell unfortunately + html5validator "$exampleDocumentPath" + + mv "$exampleDocumentPath" "$out" + + set +x +'' diff --git a/users/sterni/nix/int/default.nix b/users/sterni/nix/int/default.nix new file mode 100644 index 000000000000..870744522361 --- /dev/null +++ b/users/sterni/nix/int/default.nix @@ -0,0 +1,114 @@ +{ depot, lib, ... }: + +let + + inherit (depot.users.sterni.nix) + string + num + ; + + inherit (builtins) + bitOr + bitAnd + bitXor + ; + + exp = base: pow: + if pow > 0 + then base * (exp base (pow - 1)) + else if pow < 0 + then 1.0 / exp base (num.abs pow) + else 1; + + bitShiftR = bit: count: + if count == 0 + then bit + else (bitShiftR bit (count - 1)) / 2; + + bitShiftL = bit: count: + if count == 0 + then bit + else 2 * (bitShiftL bit (count - 1)); + + hexdigits = "0123456789ABCDEF"; + + toHex = int: + let + go = i: + if i == 0 + then "" + else go (bitShiftR i 4) + + string.charAt (bitAnd i 15) hexdigits; + sign = lib.optionalString (int < 0) "-"; + in + if int == 0 + then "0" + else "${sign}${go (num.abs int)}"; + + fromHexMap = builtins.listToAttrs + (lib.imap0 (i: c: { name = c; value = i; }) + (lib.stringToCharacters hexdigits)); + + fromHex = literal: + let + negative = string.charAt 0 literal == "-"; + start = if negative then 1 else 0; + len = builtins.stringLength literal; + # reversed list of all digits + digits = builtins.genList + (i: string.charAt (len - 1 - i) literal) + (len - start); + parsed = builtins.foldl' + (v: d: { + val = v.val + (fromHexMap."${d}" * v.mul); + mul = v.mul * 16; + }) + { val = 0; mul = 1; } + digits; + in + if negative + then -parsed.val + else parsed.val; + + # A nix integer is a 64bit signed integer + maxBound = 9223372036854775807; + + # fun fact: -9223372036854775808 is the lower bound + # for a nix integer (as you would expect), but you can't + # use it as an integer literal or you'll be greeted with: + # error: invalid integer '9223372036854775808' + # This is because all int literals when parsing are + # positive, negative "literals" are positive literals + # which are preceded by the arithmetric negation operator. + minBound = -9223372036854775807 - 1; + + odd = x: bitAnd x 1 == 1; + even = x: bitAnd x 1 == 0; + + quot' = builtins.div; # no typecheck + rem = a: b: + assert builtins.isInt a && builtins.isInt b; + let res = quot' a b; in a - (res * b); + quot = a: b: + assert builtins.isInt a && builtins.isInt b; + quot' a b; + +in +{ + inherit + maxBound + minBound + exp + odd + even + quot + rem + bitShiftR + bitShiftL + bitOr + bitAnd + bitXor + toHex + fromHex + ; +} diff --git a/users/sterni/nix/int/tests/default.nix b/users/sterni/nix/int/tests/default.nix new file mode 100644 index 000000000000..80bd05b6b5eb --- /dev/null +++ b/users/sterni/nix/int/tests/default.nix @@ -0,0 +1,455 @@ +{ depot, lib, ... }: + +let + + inherit (depot.nix.runTestsuite) + runTestsuite + it + assertEq + ; + + inherit (depot.users.sterni.nix) + int + string + fun + ; + + testBounds = it "checks minBound and maxBound" [ + (assertEq "maxBound is the maxBound" true + (int.maxBound + 1 < int.maxBound)) + (assertEq "minBound is the minBound" true + (int.minBound - 1 > int.minBound)) + (assertEq "maxBound overflows to minBound" + (int.maxBound + 1) + int.minBound) + (assertEq "minBound overflows to maxBound" + (int.minBound - 1) + int.maxBound) + ]; + + expectedBytes = [ + "00" + "01" + "02" + "03" + "04" + "05" + "06" + "07" + "08" + "09" + "0A" + "0B" + "0C" + "0D" + "0E" + "0F" + "10" + "11" + "12" + "13" + "14" + "15" + "16" + "17" + "18" + "19" + "1A" + "1B" + "1C" + "1D" + "1E" + "1F" + "20" + "21" + "22" + "23" + "24" + "25" + "26" + "27" + "28" + "29" + "2A" + "2B" + "2C" + "2D" + "2E" + "2F" + "30" + "31" + "32" + "33" + "34" + "35" + "36" + "37" + "38" + "39" + "3A" + "3B" + "3C" + "3D" + "3E" + "3F" + "40" + "41" + "42" + "43" + "44" + "45" + "46" + "47" + "48" + "49" + "4A" + "4B" + "4C" + "4D" + "4E" + "4F" + "50" + "51" + "52" + "53" + "54" + "55" + "56" + "57" + "58" + "59" + "5A" + "5B" + "5C" + "5D" + "5E" + "5F" + "60" + "61" + "62" + "63" + "64" + "65" + "66" + "67" + "68" + "69" + "6A" + "6B" + "6C" + "6D" + "6E" + "6F" + "70" + "71" + "72" + "73" + "74" + "75" + "76" + "77" + "78" + "79" + "7A" + "7B" + "7C" + "7D" + "7E" + "7F" + "80" + "81" + "82" + "83" + "84" + "85" + "86" + "87" + "88" + "89" + "8A" + "8B" + "8C" + "8D" + "8E" + "8F" + "90" + "91" + "92" + "93" + "94" + "95" + "96" + "97" + "98" + "99" + "9A" + "9B" + "9C" + "9D" + "9E" + "9F" + "A0" + "A1" + "A2" + "A3" + "A4" + "A5" + "A6" + "A7" + "A8" + "A9" + "AA" + "AB" + "AC" + "AD" + "AE" + "AF" + "B0" + "B1" + "B2" + "B3" + "B4" + "B5" + "B6" + "B7" + "B8" + "B9" + "BA" + "BB" + "BC" + "BD" + "BE" + "BF" + "C0" + "C1" + "C2" + "C3" + "C4" + "C5" + "C6" + "C7" + "C8" + "C9" + "CA" + "CB" + "CC" + "CD" + "CE" + "CF" + "D0" + "D1" + "D2" + "D3" + "D4" + "D5" + "D6" + "D7" + "D8" + "D9" + "DA" + "DB" + "DC" + "DD" + "DE" + "DF" + "E0" + "E1" + "E2" + "E3" + "E4" + "E5" + "E6" + "E7" + "E8" + "E9" + "EA" + "EB" + "EC" + "ED" + "EE" + "EF" + "F0" + "F1" + "F2" + "F3" + "F4" + "F5" + "F6" + "F7" + "F8" + "F9" + "FA" + "FB" + "FC" + "FD" + "FE" + "FF" + ]; + + hexByte = i: string.fit { width = 2; char = "0"; } (int.toHex i); + + hexInts = [ + { left = 0; right = "0"; } + { left = 1; right = "1"; } + { left = 11; right = "B"; } + { left = 123; right = "7B"; } + { left = 9000; right = "2328"; } + { left = 2323; right = "913"; } + { left = 4096; right = "1000"; } + { left = int.maxBound; right = "7FFFFFFFFFFFFFFF"; } + { left = int.minBound; right = "-8000000000000000"; } + ]; + + testHex = it "checks conversion to hex" (lib.flatten [ + (lib.imap0 + (i: hex: [ + (assertEq "hexByte ${toString i} == ${hex}" (hexByte i) hex) + (assertEq "${toString i} == fromHex ${hex}" i (int.fromHex hex)) + ]) + expectedBytes) + (builtins.map + ({ left, right }: [ + (assertEq "toHex ${toString left} == ${right}" (int.toHex left) right) + (assertEq "${toString left} == fromHex ${right}" left (int.fromHex right)) + ]) + hexInts) + ]); + + testBasic = it "checks basic int operations" [ + (assertEq "122 is even" (int.even 122 && !(int.odd 122)) true) + (assertEq "123 is odd" (int.odd 123 && !(int.even 123)) true) + ]; + + expNumbers = [ + { left = -3; right = 0.125; } + { left = -2; right = 0.25; } + { left = -1; right = 0.5; } + { left = 0; right = 1; } + { left = 1; right = 2; } + { left = 2; right = 4; } + { left = 3; right = 8; } + { left = 4; right = 16; } + { left = 5; right = 32; } + { left = 16; right = 65536; } + ]; + + testExp = it "checks exponentiation" + (builtins.map + ({ left, right }: + assertEq + "2 ^ ${toString left} == ${toString right}" + (int.exp 2 left) + right) + expNumbers); + + shifts = [ + { a = 2; b = 5; c = 64; op = "<<"; } + { a = -2; b = 5; c = -64; op = "<<"; } + { a = 123; b = 4; c = 1968; op = "<<"; } + { a = 1; b = 8; c = 256; op = "<<"; } + { a = 256; b = 8; c = 1; op = ">>"; } + { a = 374; b = 2; c = 93; op = ">>"; } + { a = 2; b = 2; c = 0; op = ">>"; } + { a = 99; b = 9; c = 0; op = ">>"; } + ]; + + checkShift = { a, b, c, op }@args: + let + f = string.match op { + "<<" = int.bitShiftL; + ">>" = int.bitShiftR; + }; + in + assertEq "${toString a} ${op} ${toString b} == ${toString c}" (f a b) c; + + checkShiftRDivExp = n: + assertEq "${toString n} >> 5 == ${toString n} / 2 ^ 5" + (int.bitShiftR n 5) + (n / (int.exp 2 5)); + + checkShiftLMulExp = n: + assertEq "${toString n} >> 6 == ${toString n} * 2 ^ 6" + (int.bitShiftL n 5) + (n * (int.exp 2 5)); + + testBit = it "checks bitwise operations" (lib.flatten [ + (builtins.map checkShift shifts) + (builtins.map checkShiftRDivExp [ + 1 + 2 + 3 + 5 + 7 + 23 + 1623 + 238 + 34 + 348 + 2834 + 834 + 348 + ]) + (builtins.map checkShiftLMulExp [ + 1 + 2 + 3 + 5 + 7 + 23 + 384 + 3 + 2 + 5991 + 85109 + 38 + ]) + ]); + + divisions = [ + { a = 2; b = 1; c = 2; rem = 0; } + { a = 2; b = 2; c = 1; rem = 0; } + { a = 20; b = 10; c = 2; rem = 0; } + { a = 12; b = 5; c = 2; rem = 2; } + { a = 23; b = 4; c = 5; rem = 3; } + ]; + + checkQuot = n: { a, b, c, rem }: [ + (assertEq "${n}: quot result" (int.quot a b) c) + (assertEq "${n}: rem result" (int.rem a b) rem) + (assertEq "${n}: quotRem law" ((int.quot a b) * b + (int.rem a b)) a) + ]; + + testQuotRem = it "checks integer quotient and remainder" + (lib.flatten [ + (builtins.map (checkQuot "+a / +b") divisions) + (builtins.map + (fun.rl (checkQuot "-a / +b") (x: x // { + a = -x.a; + c = -x.c; + rem = -x.rem; + })) + divisions) + (builtins.map + (fun.rl (checkQuot "+a / -b") (x: x // { + b = -x.b; + c = -x.c; + })) + divisions) + (builtins.map + (fun.rl (checkQuot "-a / -b") (x: x // { + a = -x.a; + b = -x.b; + rem = -x.rem; + })) + divisions) + ]); + +in +runTestsuite "nix.int" [ + testBounds + testHex + testBasic + testExp + testBit + testQuotRem +] diff --git a/users/sterni/nix/list/default.nix b/users/sterni/nix/list/default.nix new file mode 100644 index 000000000000..568a76d637a1 --- /dev/null +++ b/users/sterni/nix/list/default.nix @@ -0,0 +1,30 @@ +{ ... }: + +{ + /* For a list of length n that consists of lists of length m, + return a list of length m containing lists of length n + so that + + builtins.elemAt (builtins.elemAt orig a) b + == builtins.elemAt (builtins.elemAt transposed b) a + + Essentially, if you think of the nested list as an array with two + dimensions, the two index axes are swapped. + + The length of the inner lists m is determined based on the first element + and assumed to be used for all other lists. Malformed input data may + cause the function to crash or lose data. + + Type: <n>[ <m>[ ] ] -> <m>[ <n>[ ] ] + */ + transpose = list: + let + innerLength = builtins.length (builtins.head list); + outerLength = builtins.length list; + in + builtins.genList + (inner: builtins.genList + (outer: builtins.elemAt (builtins.elemAt list outer) inner) + outerLength) + innerLength; +} diff --git a/users/sterni/nix/misc/default.nix b/users/sterni/nix/misc/default.nix new file mode 100644 index 000000000000..1de9c973ec84 --- /dev/null +++ b/users/sterni/nix/misc/default.nix @@ -0,0 +1,18 @@ +{ ... }: + +let + /* Returns true if it is being evaluated using restrict-eval, false if not. + It's more robust than using `builtins.getEnv` since it isn't fooled by + `env -i`. + + See https://github.com/NixOS/nix/issues/6579 for a description of the + behavior. Precise cause in the evaluator / store implementation is unclear. + + Type: bool + */ + inRestrictedEval = builtins.pathExists (toString ./guinea-pig + "/."); +in + +{ + inherit inRestrictedEval; +} diff --git a/users/sterni/nix/misc/guinea-pig b/users/sterni/nix/misc/guinea-pig new file mode 120000 index 000000000000..73537e478e3f --- /dev/null +++ b/users/sterni/nix/misc/guinea-pig @@ -0,0 +1 @@ +default.nix \ No newline at end of file diff --git a/users/sterni/nix/num/default.nix b/users/sterni/nix/num/default.nix new file mode 100644 index 000000000000..81e2f8377f3b --- /dev/null +++ b/users/sterni/nix/num/default.nix @@ -0,0 +1,17 @@ +{ ... }: + +rec { + inherit (builtins) + mul + div + add + sub + ; + + sign = i: if i < 0 then -1 else 1; + abs = i: if i < 0 then -i else i; + + inRange = a: b: x: x >= a && x <= b; + + sum = builtins.foldl' (a: b: a + b) 0; +} diff --git a/users/sterni/nix/num/tests/default.nix b/users/sterni/nix/num/tests/default.nix new file mode 100644 index 000000000000..ca5f861debe8 --- /dev/null +++ b/users/sterni/nix/num/tests/default.nix @@ -0,0 +1,26 @@ +{ depot, ... }: + +let + + inherit (depot.nix.runTestsuite) + runTestsuite + it + assertEq + ; + + inherit (depot.users.sterni.nix) + num + ; + + testsBasic = it "tests basic operations" [ + (assertEq "abs -4959" (num.abs (-4959)) 4959) + (assertEq "sum" (num.sum [ 123 321 1.5 ]) (123 + 321 + 1.5)) + (assertEq "inRange" + (builtins.map (num.inRange 1.0 5) [ 0 0.5 3 4 4.5 5.5 5 6 ]) + [ false false true true true false true false ]) + ]; +in + +runTestsuite "nix.num" [ + testsBasic +] diff --git a/users/sterni/nix/string/default.nix b/users/sterni/nix/string/default.nix new file mode 100644 index 000000000000..381c8ddff748 --- /dev/null +++ b/users/sterni/nix/string/default.nix @@ -0,0 +1,121 @@ +{ depot, lib, ... }: + +let + + inherit (depot.users.sterni.nix.char) + chr + ord + ; + + inherit (depot.users.sterni.nix) + int + flow + ; + + take = n: s: + builtins.substring 0 n s; + + drop = n: s: + builtins.substring n int.maxBound s; + + charAt = i: s: + let + r = builtins.substring i 1 s; + in + if r == "" then null else r; + + charIndex = char: s: + let + len = builtins.stringLength s; + go = i: + flow.cond [ + [ (i >= len) null ] + [ (charAt i s == char) i ] + [ true (go (i + 1)) ] + ]; + in + go 0; + + toChars = lib.stringToCharacters; + fromChars = lib.concatStrings; + + toBytes = str: + builtins.map ord (toChars str); + + fromBytes = is: lib.concatMapStrings chr is; + + pad = { left ? 0, right ? 0, char ? " " }: s: + let + leftS = fromChars (builtins.genList (_: char) left); + rightS = fromChars (builtins.genList (_: char) right); + in + "${leftS}${s}${rightS}"; + + fit = { char ? " ", width, side ? "left" }: s: + let + diff = width - builtins.stringLength s; + in + if diff <= 0 + then s + else pad { inherit char; "${side}" = diff; } s; + + # pattern matching for strings only + match = val: matcher: matcher."${val}"; + + /* Bare-bones printf implementation. Supported format specifiers: + + * `%%` escapes `%` + * `%s` is substituted by a string + + As expected, the first argument is a format string and the values + for its format specifiers need to provided as the next arguments + in order. + + Type: string -> (printfVal : either string (a -> printfVal)) + */ + printf = formatString: + let + specifierWithArg = token: builtins.elem token [ + "%s" + ]; + isSpecifier = lib.hasPrefix "%"; + + tokens = lib.flatten (builtins.split "(%.)" formatString); + argsNeeded = builtins.length (builtins.filter specifierWithArg tokens); + + format = args: (builtins.foldl' + ({ out ? "", argIndex ? 0 }: token: { + argIndex = argIndex + (if specifierWithArg token then 1 else 0); + out = + if token == "%s" then out + builtins.elemAt args argIndex + else if token == "%%" then out + "%" + else if isSpecifier token then throw "Unsupported format specifier ${token}" + else out + token; + }) + { } + tokens).out; + + accumulateArgs = argCount: args: + if argCount > 0 + then arg: accumulateArgs (argCount - 1) (args ++ [ arg ]) + else format args; + in + accumulateArgs argsNeeded [ ]; + +in +{ + inherit + take + drop + charAt + charIndex + toBytes + fromBytes + toChars + fromChars + pad + fit + match + printf + ; +} diff --git a/users/sterni/nix/string/tests/default.nix b/users/sterni/nix/string/tests/default.nix new file mode 100644 index 000000000000..e9015e95dca4 --- /dev/null +++ b/users/sterni/nix/string/tests/default.nix @@ -0,0 +1,72 @@ +{ depot, ... }: + +let + + inherit (depot.users.sterni.nix) + string + ; + + inherit (depot.nix.runTestsuite) + it + assertEq + runTestsuite + ; + + testTakeDrop = it "tests take and drop" [ + (assertEq "take" + (string.take 5 "five and more") + "five ") + (assertEq "drop" + (string.drop 2 "coin") + "in") + (assertEq "take out of bounds" + (string.take 100 "foo") + "foo") + (assertEq "drop out of bounds" + (string.drop 42 "lol") + "") + ]; + + testIndexing = it "tests string indexing" [ + (assertEq "normal charAt" + (string.charAt 3 "helo") + "o") + (assertEq "out of bounds charAt" + (string.charAt 5 "helo") + null) + ]; + + testFinding = it "tests finding in strings" [ + (assertEq "normal charIndex" + (string.charIndex "d" "abcdefghijkl") + 3) + (assertEq "charIndex no match" + (string.charIndex "w" "zZzZzzzZZZ") + null) + ]; + + dontEval = builtins.throw "this should not get evaluated"; + + testMatch = it "tests match" [ + (assertEq "basic match usage" 42 + (string.match "answer" { + "answer" = 42; + "banana" = dontEval; + "maleur" = dontEval; + })) + ]; + + f = "f"; + testPrintf = it "prints f" [ + (assertEq "basic %s usage" "print ${f}" (string.printf "print %s" f)) + (assertEq "% escaping" "100%" (string.printf "100%%")) + ]; + +in +runTestsuite "nix.string" [ + testTakeDrop + testIndexing + testFinding + testMatch + testPrintf +] diff --git a/users/sterni/nix/url/default.nix b/users/sterni/nix/url/default.nix new file mode 100644 index 000000000000..4a401873a1f2 --- /dev/null +++ b/users/sterni/nix/url/default.nix @@ -0,0 +1,100 @@ +{ depot, lib, ... }: + +let + + inherit (depot.users.sterni.nix) + char + int + string + flow + ; + + reserved = c: builtins.elem c [ + "!" + "#" + "$" + "&" + "'" + "(" + ")" + "*" + "+" + "," + "/" + ":" + ";" + "=" + "?" + "@" + "[" + "]" + ]; + + unreserved = c: char.asciiAlphaNum c + || builtins.elem c [ "-" "_" "." "~" ]; + + percentEncode = c: + if unreserved c + then c + else "%" + (string.fit + { + width = 2; + char = "0"; + side = "left"; + } + (int.toHex (char.ord c))); + + encode = { leaveReserved ? false }: s: + let + chars = lib.stringToCharacters s; + tr = c: + if leaveReserved && reserved c + then c + else percentEncode c; + in + lib.concatStrings (builtins.map tr chars); + + decode = s: + let + tokens = builtins.split "%" s; + decodeStep = + { result ? "" + , inPercent ? false + }: s: + flow.cond [ + [ + (builtins.isList s) + { + inherit result; + inPercent = true; + } + ] + [ + inPercent + { + inPercent = false; + # first two characters came after an % + # the rest is the string until the next % + result = result + + char.chr (int.fromHex (string.take 2 s)) + + (string.drop 2 s); + } + ] + [ + (!inPercent) + { + result = result + s; + } + ] + ]; + + in + (builtins.foldl' decodeStep { } tokens).result; + +in +{ + inherit + encode + decode + ; +} diff --git a/users/sterni/nix/url/tests/default.nix b/users/sterni/nix/url/tests/default.nix new file mode 100644 index 000000000000..4eb6f95ccd07 --- /dev/null +++ b/users/sterni/nix/url/tests/default.nix @@ -0,0 +1,58 @@ +{ depot, ... }: + +let + + inherit (depot.nix.runTestsuite) + it + assertEq + runTestsuite + ; + + inherit (depot.users.sterni.nix) + url + ; + + checkEncoding = args: { left, right }: + assertEq "encode ${builtins.toJSON left} == ${builtins.toJSON right}" + (url.encode args left) + right; + + checkDecoding = { left, right }: + assertEq "${builtins.toJSON left} == decode ${builtins.toJSON right}" + (url.decode left) + right; + + unreserved = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-_~"; + + encodeExpected = [ + { left = "Laguna Beach"; right = "Laguna%20Beach"; } + { left = "👾 Exterminate!"; right = "%F0%9F%91%BE%20Exterminate%21"; } + { left = unreserved; right = unreserved; } + { + left = "`!@#$%^&*()+={}[]:;'\\|<>,?/ \""; + right = "%60%21%40%23%24%25%5E%26%2A%28%29%2B%3D%7B%7D%5B%5D%3A%3B%27%5C%7C%3C%3E%2C%3F%2F%20%22"; + } + ]; + + testEncode = it "checks url.encode" + (builtins.map (checkEncoding { }) encodeExpected); + + testDecode = it "checks url.decode" + (builtins.map checkDecoding encodeExpected); + + testLeaveReserved = it "checks that leaveReserved is like id for valid URLs" + (builtins.map (x: checkEncoding { leaveReserved = true; } { left = x; right = x; }) [ + "ftp://ftp.is.co.za/rfc/rfc1808.txt" + "http://www.ietf.org/rfc/rfc2396.txt" + "ldap://[2001:db8::7]/c=GB?objectClass?one" + "mailto:John.Doe@example.com" + "news:comp.infosystems.www.servers.unix" + "tel:+1-816-555-1212" + "telnet://192.0.2.16:80/" + "urn:oasis:names:specification:docbook:dtd:xml:4.1.2" + ]); +in +runTestsuite "nix.url" [ + testEncode + testLeaveReserved +] diff --git a/users/sterni/nix/utf8/default.nix b/users/sterni/nix/utf8/default.nix new file mode 100644 index 000000000000..e76695f128b2 --- /dev/null +++ b/users/sterni/nix/utf8/default.nix @@ -0,0 +1,326 @@ +{ depot, lib, ... }: + +let + + inherit (depot.users.sterni.nix) + char + flow + fun + num + int + string + util + ; + + /* (Internal) function to determine the amount + bytes left in a UTF-8 byte sequence from the + first byte. + + This function will throw if the given first + byte is ill-formed, but will not detect all + cases of ill-formed-ness. + + Based on table 3-6. from The Unicode Standard, + Version 13.0, section 3.9. + + Type: integer -> integer + */ + byteCount = i: flow.cond [ + [ (int.bitAnd i 128 == 0) 1 ] + [ (int.bitAnd i 224 == 192) 2 ] + [ (int.bitAnd i 240 == 224) 3 ] + [ (int.bitAnd i 248 == 240) 4 ] + [ true (builtins.throw "Ill-formed first byte ${int.toHex i}") ] + ]; + + /* (Internal) function to check if a given byte in + an UTF-8 byte sequence is well-formed. + + Based on table 3-7. from The Unicode Standard, + Version 13.0, section 3.9. + + Type: integer -> integer -> integer -> bool + */ + wellFormedByte = + # first byte's integer value + first: + # byte position as an index starting with 0 + pos: + let + defaultRange = num.inRange 128 191; + + secondBytePredicate = flow.switch first [ + [ (num.inRange 194 223) defaultRange ] # C2..DF + [ 224 (num.inRange 160 191) ] # E0 + [ (num.inRange 225 236) defaultRange ] # E1..EC + [ 237 (num.inRange 128 159) ] # ED + [ (num.inRange 238 239) defaultRange ] # EE..EF + [ 240 (num.inRange 144 191) ] # F0 + [ (num.inRange 241 243) defaultRange ] # F1..F3 + [ 244 (num.inRange 128 143) ] # F4 + [ (fun.const true) null ] + ]; + + firstBytePredicate = byte: assert first == byte; + first < 128 || secondBytePredicate != null; + in + # Either ASCII or in one of the byte ranges of Table 3-6. + if pos == 0 then firstBytePredicate + # return predicate according to Table 3-6. + else if pos == 1 then assert secondBytePredicate != null; secondBytePredicate + # 3rd and 4th byte have only one validity rule + else defaultRange; + + /* Iteration step for decoding an UTF-8 byte sequence. + It decodes incrementally, i. e. it has to be fed + one byte at a time and then returns either a + new state or a final result. + + If the resulting attribute set contains the attribute + result, it is finished and the decoded codepoint is + contained in that attribute. In all other cases, + pass the returned set to step again along with + a new byte. The initial state to pass is the empty + set. + + Extra attributes are always passed through, so you + can pass extra state. Be sure not to use result, + pos, code, first or count. + + This function will throw with a fairly detailed + message if it encounters ill-formed bytes. + + The implementation is based on The Unicode Standard, + Version 13.0, section 3.9, especially table 3-6. + + Type: { ... } -> string -> ({ result :: integer, ... } | { ... }) + + Example: utf8.step {} "f" + => { result = 102; } + */ + step = { pos ? 0, code ? 0, ... }@args: byte: + let + value = char.ord byte; + # first byte is context for well-formed-ness + first = args.first or value; + count = args.count or (byteCount first); + newCode = + if count == 1 + then int.bitAnd 127 first # ascii character + else # multi byte UTF-8 sequence + let + # Calculate the bitmask for extracting the + # codepoint data in the current byte. + # If the codepoint is not ASCII, the bits + # used for codepoint data differ depending + # on the byte position and overall byte + # count. The first byte always ignores + # the (count + 1) most significant bits. + # For all subsequent bytes, the 2 most + # significant bits need to be ignored. + # See also table 3-6. + mask = + if pos == 0 + then int.exp 2 (8 - (count + 1)) - 1 + else 63; + # UTF-8 uses the 6 least significant bits in all + # subsequent bytes after the first one. Therefore + # We can determine the amount we need to shift + # the current value by the amount of bytes left. + offset = (count - (pos + 1)) * 6; + in + code + (int.bitShiftL (int.bitAnd mask value) offset); + illFormedMsg = + "Ill-formed byte ${int.toHex value} at position ${toString pos} in ${toString count} byte UTF-8 sequence"; + in + if !(wellFormedByte first pos value) then builtins.throw illFormedMsg + else if pos + 1 == count + then (builtins.removeAttrs args [ + # allow extra state being passed through + "count" + "code" + "pos" + "first" + ]) // { result = newCode; } + else (builtins.removeAttrs args [ "result" ]) // { + inherit count first; + code = newCode; + pos = pos + 1; + }; + + /* Decode an UTF-8 string into a list of codepoints. + + Throws if the string is ill-formed UTF-8. + + Type: string -> [ integer ] + */ + # TODO(sterni): option to fallback to replacement char instead of failure + decode = s: + let + stringLength = builtins.stringLength s; + iterResult = builtins.genericClosure { + startSet = [ + { + key = "start"; + stringIndex = -1; + state = { }; + codepoint = null; + } + ]; + operator = { state, stringIndex, ... }: + let + # updated values for current iteration step + newIndex = stringIndex + 1; + newState = step state (builtins.substring newIndex 1 s); + in + lib.optional (newIndex < stringLength) { + # unique keys to make genericClosure happy + key = toString newIndex; + # carryover state for the next step + stringIndex = newIndex; + state = newState; + # actual payload for later, steps with value null are filtered out + codepoint = newState.result or null; + }; + }; + in + # extract all steps that yield a code point into a list + builtins.map (v: v.codepoint) ( + builtins.filter + ( + { codepoint, stringIndex, state, ... }: + + let + # error message in case we are missing bytes at the end of input + earlyEndMsg = + if state ? count && state ? pos + then "Missing ${toString (with state; count - pos)} bytes at end of input" + else "Unexpected end of input"; + in + + # filter out all iteration steps without a codepoint value + codepoint != null + # if we are at the iteration step of a non-empty input string, throw + # an error if no codepoint was returned, as it indicates an incomplete + # UTF-8 sequence. + || (stringLength > 0 && stringIndex == stringLength - 1 && throw earlyEndMsg) + + ) + iterResult + ); + + /* Pretty prints a Unicode codepoint in the U+<HEX> notation. + + Type: integer -> string + */ + formatCodepoint = cp: "U+" + string.fit + { + width = 4; + char = "0"; + } + (int.toHex cp); + + encodeCodepoint = cp: + let + # Find the amount of bytes needed to encode the given codepoint. + # Note that this doesn't check if the Unicode codepoint is allowed, + # but rather allows all theoretically UTF-8-encodeable ones. + count = flow.switch cp [ + [ (num.inRange 0 127) 1 ] # 00000000 0xxxxxxx + [ (num.inRange 128 2047) 2 ] # 00000yyy yyxxxxxx + [ (num.inRange 2048 65535) 3 ] # zzzzyyyy yyxxxxxx + [ (num.inRange 65536 1114111) 4 ] # 000uuuuu zzzzyyyy yyxxxxxx, + # capped at U+10FFFF + + [ (fun.const true) (builtins.throw invalidCodepointMsg) ] + ]; + + invalidCodepointMsg = "${formatCodepoint cp} is not a Unicode codepoint"; + + # Extract the bit ranges x, y, z and u from the given codepoint + # according to Table 3-6. from The Unicode Standard, Version 13.0, + # section 3.9. u is split into uh and ul since they are used in + # different bytes in the end. + components = lib.mapAttrs + (_: { mask, offset }: + int.bitAnd (int.bitShiftR cp offset) mask + ) + { + x = { + mask = if count > 1 then 63 else 127; + offset = 0; + }; + y = { + mask = if count > 2 then 63 else 31; + offset = 6; + }; + z = { + mask = 15; + offset = 12; + }; + # u which belongs into the second byte + ul = { + mask = 3; + offset = 16; + }; + # u which belongs into the first byte + uh = { + mask = 7; + offset = 18; + }; + }; + inherit (components) x y z ul uh; + + # Finally construct the byte sequence for the given codepoint. This is + # usually done by using the component and adding a few bits as a prefix + # which depends on the length of the sequence. The longer the sequence, + # the further back each component is pushed. To simplify this, we + # always construct a 4 element list and take the last `count` elements. + # Thanks to laziness the bogus values created by this are never evaluated. + # + # Based on table 3-6. from The Unicode Standard, + # Version 13.0, section 3.9. + bytes = lib.sublist (4 - count) count [ + # 11110uuu + (uh + 240) + # 10uuzzzz or 1110zzzz + (z + (if count > 3 then 128 + int.bitShiftL ul 4 else 224)) + # 10yyyyyy or 110yyyyy + (y + (if count > 2 then 128 else 192)) + # 10xxxxxx or 0xxxxxxx + (x + (if count > 1 then 128 else 0)) + ]; + + firstByte = builtins.head bytes; + + unableToEncodeMessage = "Can't encode ${formatCodepoint cp} as UTF-8"; + + in + string.fromBytes ( + builtins.genList + (i: + let + byte = builtins.elemAt bytes i; + in + if wellFormedByte firstByte i byte + then byte + else builtins.throw unableToEncodeMessage + ) + count + ); + + /* Encode a list of Unicode codepoints into an UTF-8 string. + + Type: [ integer ] -> string + */ + encode = lib.concatMapStrings encodeCodepoint; + +in +{ + inherit + encode + decode + step + formatCodepoint + ; +} diff --git a/users/sterni/nix/utf8/tests/default.nix b/users/sterni/nix/utf8/tests/default.nix new file mode 100644 index 000000000000..40783eab2421 --- /dev/null +++ b/users/sterni/nix/utf8/tests/default.nix @@ -0,0 +1,148 @@ +{ depot, pkgs, lib, ... }: + +let + + inherit (pkgs) + runCommandLocal + ; + + inherit (depot.nix.runTestsuite) + runTestsuite + it + assertEq + assertThrows + assertDoesNotThrow + ; + + inherit (depot.nix.writers) + rustSimple + ; + + inherit (depot.users.sterni.nix) + int + utf8 + string + char + ; + + rustDecoder = rustSimple + { + name = "utf8-decode"; + } '' + use std::io::{self, Read}; + fn main() -> std::io::Result<()> { + let mut buffer = String::new(); + io::stdin().read_to_string(&mut buffer)?; + + print!("[ "); + + for c in buffer.chars() { + print!("{} ", u32::from(c)); + } + + print!("]"); + + Ok(()) + } + ''; + + rustDecode = s: + let + expr = runCommandLocal "${s}-decoded" { } '' + printf '%s' ${lib.escapeShellArg s} | ${rustDecoder} > $out + ''; + in + import expr; + + hexDecode = l: + utf8.decode (string.fromBytes (builtins.map int.fromHex l)); + + hexEncode = l: utf8.encode (builtins.map int.fromHex l); + + testFailures = it "checks UTF-8 decoding failures" ([ + (assertThrows "truncated UTF-8 string throws" (hexDecode [ "F0" "9F" ])) + # examples from The Unicode Standard + (assertThrows "ill-formed: C0 AF" (hexDecode [ "C0" "AF" ])) + (assertThrows "ill-formed: E0 9F 80" (hexDecode [ "E0" "9F" "80" ])) + (assertEq "well-formed: F4 80 83 92" (hexDecode [ "F4" "80" "83" "92" ]) [ 1048786 ]) + (assertThrows "Codepoint out of range: 0xFFFFFF" (hexEncode [ "FFFFFF" ])) + (assertThrows "Codepoint out of range: -0x02" (hexEncode [ "-02" ])) + ] ++ builtins.genList + (i: + let + cp = i + int.fromHex "D800"; + in + assertThrows "Can't encode UTF-16 reserved characters: ${utf8.formatCodepoint cp}" + (utf8.encode [ cp ]) + ) + (int.fromHex "07FF")); + + testAscii = it "checks decoding of ascii strings" + (builtins.map + (s: assertEq "ASCII decoding is equal to UTF-8 decoding for \"${s}\"" + (string.toBytes s) + (utf8.decode s)) [ + "foo bar" + "hello\nworld" + "carriage\r\nreturn" + "1238398494829304 []<><>({})[]!!)" + (string.take 127 char.allChars) + ]); + + randomUnicode = [ + "" # empty string should yield empty list + "🥰👨👨👧👦🐈⬛👩🏽🦰" + # https://kermitproject.org/utf8.html + "ᚠᛇᚻ᛫ᛒᛦᚦ᛫ᚠᚱᚩᚠᚢᚱ᛫ᚠᛁᚱᚪ᛫ᚷᛖᚻᚹᛦᛚᚳᚢᛗ" + "An preost wes on leoden, Laȝamon was ihoten" + "Sîne klâwen durh die wolken sint geslagen," + "Τὴ γλῶσσα μοῦ ἔδωσαν ἑλληνικὴ" + "На берегу пустынных волн" + "ვეპხის ტყაოსანი შოთა რუსთაველი" + "யாமறிந்த மொழிகளிலே தமிழ்மொழி போல் இனிதாவது எங்கும் காணோம், " + "ಬಾ ಇಲ್ಲಿ ಸಂಭವಿಸು " + ]; + + # https://kermitproject.org/utf8.html + glassSentences = [ + "Euro Symbol: €." + "Greek: Μπορώ να φάω σπασμένα γυαλιά χωρίς να πάθω τίποτα." + "Íslenska / Icelandic: Ég get etið gler án þess að meiða mig." + "Polish: Mogę jeść szkło, i mi nie szkodzi." + "Romanian: Pot să mănânc sticlă și ea nu mă rănește." + "Ukrainian: Я можу їсти шкло, й воно мені не пошкодить." + "Armenian: Կրնամ ապակի ուտել և ինծի անհանգիստ չըներ։" + "Georgian: მინას ვჭამ და არა მტკივა." + "Hindi: मैं काँच खा सकता हूँ, मुझे उस से कोई पीडा नहीं होती." + "Hebrew(2): אני יכול לאכול זכוכית וזה לא מזיק לי." + "Yiddish(2): איך קען עסן גלאָז און עס טוט מיר נישט װײ." + "Arabic(2): أنا قادر على أكل الزجاج و هذا لا يؤلمني." + "Japanese: 私はガラスを食べられます。それは私を傷つけません。" + "Thai: ฉันกินกระจกได้ แต่มันไม่ทำให้ฉันเจ็บ " + ]; + + testDecoding = it "checks decoding of UTF-8 strings against Rust's String" + (builtins.map + (s: assertEq "Decoding of “${s}” is correct" (utf8.decode s) (rustDecode s)) + (lib.flatten [ + glassSentences + randomUnicode + ])); + + testDecodingEncoding = it "checks that decoding and then encoding forms an identity" + (builtins.map + (s: assertEq "Decoding and then encoding “${s}” yields itself" + (utf8.encode (utf8.decode s)) + s) + (lib.flatten [ + glassSentences + randomUnicode + ])); + +in +runTestsuite "nix.utf8" [ + testFailures + testAscii + testDecoding + testDecodingEncoding +] diff --git a/users/sterni/nixpkgs-crate-holes/default.nix b/users/sterni/nixpkgs-crate-holes/default.nix new file mode 100644 index 000000000000..1630ecb8f1da --- /dev/null +++ b/users/sterni/nixpkgs-crate-holes/default.nix @@ -0,0 +1,348 @@ +{ depot, pkgs, lib, ... }: + +let + # dependency imports + + inherit (depot.nix) getBins; + inherit (depot.third_party) rustsec-advisory-db; + + bins = getBins pkgs.jq [ + "jq" + ] // getBins pkgs.coreutils [ + "cat" + "printf" + "tee" + "test" + "wc" + ] // getBins pkgs.gnugrep [ + "grep" + ] // getBins pkgs.cargo-audit [ + "cargo-audit" + ] // getBins pkgs.ansi2html [ + "ansi2html" + ] // { + eprintf = depot.tools.eprintf; + }; + + # list of maintainers we may @mention on GitHub + maintainerWhitelist = builtins.attrValues { + inherit (lib.maintainers) + sternenseemann + qyliss + jk + symphorien + erictapen + expipiplus1 + ; + }; + + # buildRustPackage handling + + /* Predicate by which we identify rust packages we are interested in, + i. e. built using `buildRustPackage`. + + Type :: drv -> bool + */ + isRustPackage = v: v ? cargoDeps; + + /* Takes a buildRustPackage derivation and returns a derivation which + builds extracts the `Cargo.lock` of its `cargoDeps` derivation or + `null` if it has none. + + Type: drv -> option<drv> + */ + # TODO(sterni): support cargoVendorDir? + extractCargoLock = drv: + if !(drv ? cargoDeps.outPath) + then null + else + pkgs.runCommand "${drv.name}-Cargo.lock" { } '' + if test -d "${drv.cargoDeps}"; then + cp "${drv.cargoDeps}/Cargo.lock" "$out" + fi + + if test -f "${drv.cargoDeps}"; then + tar -xO \ + --no-wildcards-match-slash --wildcards \ + -f "${drv.cargoDeps}" \ + '*/Cargo.lock' \ + > "$out" + fi + ''; + + # nixpkgs traversal + + # Condition for us to recurse: Either at top-level or recurseForDerivation. + recurseInto = path: x: path == [ ] || + (lib.isAttrs x && (x.recurseForDerivations or false)); + + # Returns the value or false if an eval error occurs. + tryEvalOrFalse = v: (builtins.tryEval v).value; + + /* Traverses nixpkgs as instructed by `recurseInto` and collects + the attribute and lockfile derivation of every rust package it + encounters into a list. + + Type :: attrs + -> list { + attr :: list<str>; + lock :: option<drv>; + maintainers :: list<maintainer>; + } + */ + allLockFiles = + let + go = path: x: + let + isDrv = tryEvalOrFalse (lib.isDerivation x); + doRec = tryEvalOrFalse (recurseInto path x); + isRust = tryEvalOrFalse (isRustPackage x); + in + if doRec then + lib.concatLists + ( + lib.mapAttrsToList (n: go (path ++ [ n ])) x + ) else if isDrv && isRust then [ + { + attr = path; + lock = extractCargoLock x; + maintainers = x.meta.maintainers or [ ]; + } + ] else [ ]; + in + go [ ]; + + # Report generation and formatting + + reportFor = { attr, lock, maintainers ? [ ] }: + let + # naïve attribute path to Nix syntax conversion + strAttr = lib.concatStringsSep "." attr; + strMaintainers = lib.concatMapStringsSep " " (m: "@${m.github}") ( + builtins.filter (x: builtins.elem x maintainerWhitelist) maintainers + ); + in + if lock == null + then pkgs.emptyFile + else + depot.nix.runExecline "${strAttr}-vulnerability-report" { } [ + "foreground" + [ + "importas" + "out" + "out" + "redirfd" + "-w" + "1" + "$out" + depot.tools.rust-crates-advisory.lock-file-report + strAttr + lock + "true" + strMaintainers + ] + # ignore exit status of report + "exit" + "0" + ]; + + # GHMF in issues splits paragraphs on newlines + description = lib.concatMapStringsSep "\n\n" + ( + builtins.replaceStrings [ "\n" ] [ " " ] + ) [ + '' + The vulnerability report below was generated by + [nixpkgs-crate-holes](https://code.tvl.fyi/tree/users/sterni/nixpkgs-crate-holes) + which extracts the `Cargo.lock` file of each package in nixpkgs with a + `cargoDeps` attribute and passes it to + [cargo-audit](https://github.com/RustSec/rustsec/tree/main/cargo-audit) + using RustSec's + [advisory-db at ${builtins.substring 0 7 rustsec-advisory-db.rev}](https://github.com/RustSec/advisory-db/tree/${rustsec-advisory-db.rev}/). + '' + '' + Feel free to report any problems or suggest improvements (I have an email + address on my profile and hang out on Matrix/libera.chat as sterni)! + Tick off any reports that have been fixed in the meantime. + '' + '' + Note: A vulnerability in a dependency does not necessarily mean the dependent + package is vulnerable, e. g. when a vulnerable function isn't used. + '' + ]; + + runInstructions = '' + <details> + <summary> + Generating Cargo.lock vulnerability reports + + </summary> + + If you have a checkout of [depot](https://code.tvl.fyi/about/), you can generate this report using: + + ``` + nix-build -A users.sterni.nixpkgs-crate-holes.full \ + --argstr nixpkgsPath /path/to/nixpkgs + ``` + + If you want a more detailed report for a single attribute of nixpkgs, use: + + ``` + nix-build -A users.sterni.nixpkgs-crate-holes.single \ + --argstr nixpkgsPath /path/to/nixpkgs --arg attr '[ "ripgrep" ]' + ``` + + </details> + ''; + + defaultNixpkgsArgs = { allowBroken = false; }; + + reportForNixpkgs = + { nixpkgsPath + , nixpkgsArgs ? defaultNixpkgsArgs + }@args: + + let + reports = builtins.map reportFor ( + allLockFiles (import nixpkgsPath nixpkgsArgs) + ); + in + + depot.nix.runExecline "nixpkgs-rust-pkgs-vulnerability-report.md" + { + stdin = lib.concatMapStrings (report: "${report}\n") reports; + } [ + "importas" + "out" + "out" + "redirfd" + "-w" + "1" + "$out" + # Print introduction paragraph for the issue + "if" + [ bins.printf "%s\n\n" description ] + # Print all reports + "foreground" + [ + "forstdin" + "-E" + "report" + bins.cat + "$report" + ] + # Print stats at the end (mostly as a gimmick), we already know how many + # attributes there are and count the attributes with vulnerability by + # finding the number of checkable list entries in the output. + "backtick" + "-E" + "vulnerableCount" + [ + "pipeline" + [ + bins.grep + "^- \\[ \\]" + "$out" + ] + bins.wc + "-l" + ] + "if" + [ + bins.printf + "\n%s of %s checked attributes have vulnerable dependencies.\n\n" + "$vulnerableCount" + (toString (builtins.length reports)) + ] + "if" + [ + bins.printf + "%s\n\n" + runInstructions + ] + ]; + + singleReport = + { + # Attribute to check: string or list of strings (attr path) + attr + # Path to importable nixpkgs checkout + , nixpkgsPath + # Arguments to pass to nixpkgs + , nixpkgsArgs ? defaultNixpkgsArgs + }: + + let + attr' = if builtins.isString attr then [ attr ] else attr; + drv = lib.getAttrFromPath attr' (import nixpkgsPath nixpkgsArgs); + lockFile = extractCargoLock drv; + strAttr = lib.concatStringsSep "." attr'; + in + + depot.nix.runExecline "${strAttr}-report.html" { } [ + "importas" + "out" + "out" + "backtick" + "-I" + "-E" + "-N" + "report" + [ + bins.cargo-audit + "audit" + "--quiet" + "-n" + "--db" + rustsec-advisory-db + "-f" + lockFile + ] + "pipeline" + [ + "ifte" + [ + bins.printf + "%s" + "$report" + ] + [ + bins.printf + "%s\n" + "No vulnerabilities found" + ] + bins.test + "-n" + "$report" + ] + "pipeline" + [ + bins.tee + "/dev/stderr" + ] + "redirfd" + "-w" + "1" + "$out" + bins.ansi2html + ]; + +in +{ + full = reportForNixpkgs; + single = singleReport; + + inherit + extractCargoLock + allLockFiles + ; + + # simple sanity check, doesn't cover everything, but testing the full report + # is quite expensive in terms of evaluation. + testSingle = singleReport { + nixpkgsPath = depot.third_party.nixpkgs.path; + attr = [ "ripgrep" ]; + }; + + meta.ci.targets = [ "testSingle" ]; +} diff --git a/users/sterni/secrets/default.nix b/users/sterni/secrets/default.nix new file mode 100644 index 000000000000..5550103c5a66 --- /dev/null +++ b/users/sterni/secrets/default.nix @@ -0,0 +1,3 @@ +{ depot, ... }: + +depot.ops.secrets.mkSecrets ./. (import ./secrets.nix) diff --git a/users/sterni/secrets/minecraft-rcon.age b/users/sterni/secrets/minecraft-rcon.age new file mode 100644 index 000000000000..6531a74b8825 --- /dev/null +++ b/users/sterni/secrets/minecraft-rcon.age Binary files differdiff --git a/users/sterni/secrets/secrets.nix b/users/sterni/secrets/secrets.nix new file mode 100644 index 000000000000..7132fbf8f3a6 --- /dev/null +++ b/users/sterni/secrets/secrets.nix @@ -0,0 +1,15 @@ +let + nonremote = [ + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJk+KvgvI2oJTppMASNUfMcMkA2G5ZNt+HnWDzaXKLlo" + ]; + + ingeborg = [ + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAHQn/j6NCYucpM7qIEIslVJxiFeUEKa0hi+HobTz/12" + ]; +in + +{ + "warteraum-salt.age".publicKeys = nonremote ++ ingeborg; + "warteraum-tokens.age".publicKeys = nonremote ++ ingeborg; + "minecraft-rcon.age".publicKeys = nonremote ++ ingeborg; +} diff --git a/users/sterni/secrets/warteraum-salt.age b/users/sterni/secrets/warteraum-salt.age new file mode 100644 index 000000000000..61fa5e6161ec --- /dev/null +++ b/users/sterni/secrets/warteraum-salt.age Binary files differdiff --git a/users/sterni/secrets/warteraum-tokens.age b/users/sterni/secrets/warteraum-tokens.age new file mode 100644 index 000000000000..e1bf32269ffd --- /dev/null +++ b/users/sterni/secrets/warteraum-tokens.age @@ -0,0 +1,11 @@ +age-encryption.org/v1 +-> ssh-ed25519 aXKGcg G96nS/CgSFqyum5QtOwyCo2d7PRIx7pcQBVyFjtErUE +gkQuhegobZ68Z76h93G57/trz7ixSkpa7Dz+OYMzAIw +-> ssh-ed25519 OaL1CA u9p+ejyLs4cWgB/LjR8XIIE3tRPf+a5Kqwl0nA8pDio +ZfPVZIcqgyep7C68sTybGFa+7HFDwwoDQwAmoDszua4 +-> |WcGV<-grease >a*ke{l }9Iv) ]qz +Ehf2eOTQe0t7mnbgNEjJBtRSNRl+MlgEIiziu9YU206yMQXSLrm04PPo9ycw5x/k +N/5r/M36qnKJfZUVbtcFom85+UYOQDRnfXXvPyTrsA +--- hRzM1BnEG2VPMV6DTZF2j4WZk/2uM65yAFDK3F0rSQc +(iٺ)QZq(gl:Sٺǒ@+fۣ +RNoI}{f \ No newline at end of file |