diff options
Diffstat (limited to 'tv')
35 files changed, 751 insertions, 180 deletions
diff --git a/tv/1systems/bu/config.nix b/tv/1systems/bu/config.nix index 22e5f1484..c7f7da24d 100644 --- a/tv/1systems/bu/config.nix +++ b/tv/1systems/bu/config.nix @@ -11,8 +11,6 @@ with import ./lib; <stockholm/tv/2configs/xsessions> ]; - environment.homeBinInPath = true; - krebs.build.host = config.krebs.hosts.bu; networking.hostId = lib.mkDefault "00000000"; diff --git a/tv/1systems/nomic/config.nix b/tv/1systems/nomic/config.nix index fb67814db..5bc25fd36 100644 --- a/tv/1systems/nomic/config.nix +++ b/tv/1systems/nomic/config.nix @@ -41,8 +41,6 @@ with import ./lib; fsType = "btrfs"; }; - environment.homeBinInPath = true; - environment.systemPackages = with pkgs; [ (writeDashBin "play" '' set -euf diff --git a/tv/2configs/bash/default.nix b/tv/2configs/bash/default.nix index e38566b78..57801d964 100644 --- a/tv/2configs/bash/default.nix +++ b/tv/2configs/bash/default.nix @@ -39,6 +39,10 @@ with import ./lib; esac ${pkgs.bash-fzf-history.bind} + + if test -n "''${BASH_EXTRA_INIT-}"; then + . "$BASH_EXTRA_INIT" + fi ''; promptInit = /* sh */ '' case $UID in diff --git a/tv/2configs/br.nix b/tv/2configs/br.nix index 4a8db2e38..47d657c46 100644 --- a/tv/2configs/br.nix +++ b/tv/2configs/br.nix @@ -1,8 +1,8 @@ with import ./lib; -{ config, pkgs, ... }: { +{ config, modulesPath, pkgs, ... }: { imports = [ - <nixpkgs/nixos/modules/services/hardware/sane_extra_backends/brscan4.nix> + (modulesPath + "/services/hardware/sane_extra_backends/brscan4.nix") ]; krebs.nixpkgs.allowUnfreePredicate = pkg: any (eq (packageName pkg)) [ diff --git a/tv/2configs/default.nix b/tv/2configs/default.nix index 9babb92c2..d1384845a 100644 --- a/tv/2configs/default.nix +++ b/tv/2configs/default.nix @@ -16,6 +16,7 @@ with import ./lib; ./nets/hkw.nix ./networkd.nix ./nginx + ./nix.nix ./pki ./ssh.nix ./sshd.nix @@ -45,20 +46,11 @@ with import ./lib; } { - nix.extraOptions = '' - auto-optimise-store = true - ''; - - # TODO check if both are required: - nix.settings.extra-sandbox-paths = [ - "/etc/protocols" - pkgs.iana-etc.outPath - ]; - } - { nixpkgs.config.allowUnfree = false; } { + environment.homeBinInPath = true; + environment.profileRelativeEnvVars.PATH = mkForce [ "/bin" ]; environment.systemPackages = with pkgs; [ @@ -137,4 +129,11 @@ with import ./lib; ]; } ]; + + nixpkgs.overlays = + mkAfter (optional config.hardware.video.hidpi.enable (self: super: { + alacritty-tv = super.alacritty-tv.override { + variant = "hidpi"; + }; + })); } diff --git a/tv/2configs/hw/AO753.nix b/tv/2configs/hw/AO753.nix index b998fcf7c..f2268a9b2 100644 --- a/tv/2configs/hw/AO753.nix +++ b/tv/2configs/hw/AO753.nix @@ -4,8 +4,8 @@ with import ./lib; ../smartd.nix { - nix.buildCores = 2; - nix.maxJobs = 2; + nix.settings.cores = 2; + nix.settings.max-jobs = 2; } (if lib.versionAtLeast (lib.versions.majorMinor lib.version) "21.11" then { nix.daemonCPUSchedPolicy = "batch"; diff --git a/tv/2configs/hw/winmax2.nix b/tv/2configs/hw/winmax2.nix new file mode 100644 index 000000000..b52ab0fa5 --- /dev/null +++ b/tv/2configs/hw/winmax2.nix @@ -0,0 +1,43 @@ +{ pkgs, ... }: { + + imports = [ + ../smartd.nix + ]; + + boot.initrd.availableKernelModules = [ "nvme" "xhci_pci" "thunderbolt" "usbhid" ]; + boot.initrd.kernelModules = [ "amdgpu" ]; + boot.kernelModules = [ "kvm-amd" ]; + + hardware.bluetooth.enable = true; + + hardware.cpu.amd.updateMicrocode = true; + hardware.enableRedistributableFirmware = true; + + hardware.opengl.enable = true; + hardware.opengl.extraPackages = [ + pkgs.amdvlk + pkgs.rocm-opencl-icd + pkgs.rocm-opencl-runtime + ]; + + hardware.video.hidpi.enable = true; + + networking.wireless.enable = true; + networking.wireless.interfaces = [ + "wlp1s0" + ]; + networking.interfaces.wlp1s0.useDHCP = true; + + nixpkgs.hostPlatform = "x86_64-linux"; + + services.illum.enable = true; + + services.logind.extraConfig = /* ini */ '' + HandlePowerKey=ignore + ''; + + tv.lidControl.enable = true; + + tv.hw.screens.primary.width = 2560; + tv.hw.screens.primary.height = 1600; +} diff --git a/tv/2configs/nix.nix b/tv/2configs/nix.nix new file mode 100644 index 000000000..fa96d459f --- /dev/null +++ b/tv/2configs/nix.nix @@ -0,0 +1,9 @@ +{ pkgs, ... }: { + nix.settings.auto-optimise-store = true; + + # TODO check if both are required: + nix.settings.extra-sandbox-paths = [ + "/etc/protocols" + pkgs.iana-etc.outPath + ]; +} diff --git a/tv/2configs/urxvt.nix b/tv/2configs/urxvt.nix deleted file mode 100644 index 89bb421aa..000000000 --- a/tv/2configs/urxvt.nix +++ /dev/null @@ -1,24 +0,0 @@ -{ pkgs, ... }: - -with builtins; - -let - users = [ "tv" ]; - urxvt = pkgs.rxvt_unicode; - mkService = user: { - description = "urxvt terminal daemon"; - wantedBy = [ "multi-user.target" ]; - restartIfChanged = false; - serviceConfig = { - Restart = "always"; - User = user; - ExecStart = "${urxvt}/bin/urxvtd"; - }; - }; - -in - -{ - environment.systemPackages = [ urxvt ]; - systemd.services = listToAttrs (map (u: { name = "${u}-urxvtd"; value = mkService u; }) users); -} diff --git a/tv/2configs/vim.nix b/tv/2configs/vim.nix index b8819ee36..8aee31082 100644 --- a/tv/2configs/vim.nix +++ b/tv/2configs/vim.nix @@ -11,18 +11,31 @@ with import ./lib; environment.variables.VIMINIT = ":so /etc/vimrc"; }; - extra-runtimepath = pkgs.tv.vim.makeRuntimePath [ - pkgs.tv.vimPlugins.elixir + base-plugins = [ pkgs.tv.vimPlugins.file-line - pkgs.tv.vimPlugins.fzf pkgs.tv.vimPlugins.hack + pkgs.vimPlugins.undotree + (pkgs.tv.vim.makePlugin (pkgs.write "vim-tv-base" { + "/ftplugin/haskell.vim".text = '' + if exists("g:vim_tv_ftplugin_haskell_loaded") + finish + endif + let g:vim_tv_ftplugin_haskell_loaded = 1 + + setlocal iskeyword+=' + ''; + })) + ]; + + extra-plugins = [ + pkgs.tv.vimPlugins.elixir + pkgs.tv.vimPlugins.fzf pkgs.tv.vimPlugins.jq pkgs.tv.vimPlugins.nix pkgs.tv.vimPlugins.showsyntax pkgs.tv.vimPlugins.tv pkgs.tv.vimPlugins.vim pkgs.vimPlugins.fzfWrapper - pkgs.vimPlugins.undotree pkgs.vimPlugins.vim-nftables ]; @@ -58,7 +71,9 @@ with import ./lib; ]; }; - vimrc = pkgs.writeText "vimrc" '' + vimrc = pkgs.writeText "vimrc" /* vim */ '' + vim9script + set nocompatible set autoindent @@ -71,7 +86,7 @@ with import ./lib; set mouse=a set noruler set pastetoggle=<INS> - set runtimepath=${extra-runtimepath},$VIMRUNTIME + set runtimepath=${pkgs.tv.vim.makeRuntimePath base-plugins},$VIMRUNTIME set shortmess+=I set showcmd set showmatch @@ -88,13 +103,15 @@ with import ./lib; set wildmenu set wildmode=longest,full + set runtimepath^=${pkgs.tv.vim.makeRuntimePath extra-plugins} + syntax on + set et ts=2 sts=2 sw=2 filetype plugin indent on set t_Co=256 colorscheme hack - syntax on au Syntax * syn match Garbage containedin=ALL /\s\+$/ \ | syn match TabStop containedin=ALL /\t\+/ @@ -115,30 +132,52 @@ with import ./lib; nnoremap <f1> :tabp<cr> nnoremap <f2> :tabn<cr> - inoremap <f1> <esc>:tabp<cr> - inoremap <f2> <esc>:tabn<cr> + imap <f1> <esc><f1> + imap <f2> <esc><f2> + + nnoremap <S-f1> :tabm -1<cr> + nnoremap <S-f2> :tabm +1<cr> + imap <S-f1> <esc><S-f1> + imap <S-f2> <esc><S-f2> noremap <f3> :ShowSyntax<cr> - " <C-{Up,Down,Right,Left> + # <C-{Up,Down,Right,Left}> noremap <esc>Oa <nop> | noremap! <esc>Oa <nop> noremap <esc>Ob <nop> | noremap! <esc>Ob <nop> noremap <esc>Oc <nop> | noremap! <esc>Oc <nop> noremap <esc>Od <nop> | noremap! <esc>Od <nop> - " <[C]S-{Up,Down,Right,Left> + # <[C]S-{Up,Down,Right,Left}> noremap <esc>[a <nop> | noremap! <esc>[a <nop> noremap <esc>[b <nop> | noremap! <esc>[b <nop> noremap <esc>[c <nop> | noremap! <esc>[c <nop> noremap <esc>[d <nop> | noremap! <esc>[d <nop> vnoremap u <nop> - " fzf + # fzf nnoremap <esc>q :Buffers<cr> nnoremap <esc>f :Files<cr> nnoremap <esc>w :Rg<cr> - " edit alternate buffer - " For some reason neither putting <ctrl>6 nor <ctrl>^ works here... + # edit alternate buffer + # For some reason neither putting <ctrl>6 nor <ctrl>^ works here... nnoremap <esc>a + + if $TOUCHSCREEN == "1" + nnoremap <ScrollWheelUp> <C-y> + nnoremap <ScrollWheelDown> <C-e> + nnoremap <C-ScrollWheelUp> 3<C-y> + nnoremap <C-ScrollWheelDown> 3<C-e> + nnoremap <S-ScrollWheelUp> 3<C-y> + nnoremap <S-ScrollWheelDown> 3<C-e> + nnoremap <C-S-ScrollWheelUp> <PageUp> + nnoremap <C-S-ScrollWheelDown> <PageDown> + endif + + # remember last position + autocmd BufReadPost * + \ if line("'\"") > 0 && line("'\"") <= line("$") | + \ exe "normal! g`\"" | + \ endif ''; } diff --git a/tv/3modules/default.nix b/tv/3modules/default.nix index b6b4faa51..1a0971ec6 100644 --- a/tv/3modules/default.nix +++ b/tv/3modules/default.nix @@ -1,16 +1,8 @@ +with import ./lib; { - imports = [ - ./charybdis - ./dnsmasq.nix - ./ejabberd - ./focus.nix - ./hosts.nix - ./hw.nix - ./im.nix - ./iptables.nix - ./org.freedesktop.machine1.host-shell.nix - ./slock.nix - ./x0vncserver.nix - ./Xresources.nix - ]; + imports = + map + (name: ./. + "/${name}") + (attrNames + (filterAttrs isNixDirEntry (readDir ./.))); } diff --git a/tv/3modules/iptables.nix b/tv/3modules/iptables.nix index c4bf4644d..5b36c5acb 100644 --- a/tv/3modules/iptables.nix +++ b/tv/3modules/iptables.nix @@ -34,6 +34,10 @@ with import ./lib; type = with types; listOf str; default = []; }; + filter.Wiregrill = mkOption { + type = with types; listOf str; + default = []; + }; }; }; }; @@ -66,6 +70,16 @@ with import ./lib; default = []; }; + input-wiregrill-accept-tcp = mkOption { + type = with types; listOf (either int str); + default = []; + }; + + input-wiregrill-accept-udp = mkOption { + type = with types; listOf (either int str); + default = []; + }; + extra = mkOption { default = {}; type = extraTypes.rules; @@ -141,6 +155,7 @@ with import ./lib; :FORWARD DROP [0:0] :OUTPUT ACCEPT [0:0] :Retiolum - [0:0] + :Wiregrill - [0:0] ${concatMapStringsSep "\n" (rule: "-A INPUT ${rule}") ([] ++ [ "-m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT" @@ -150,6 +165,7 @@ with import ./lib; ++ map accept-tcp (unique (map toString cfg.input-internet-accept-tcp)) ++ map accept-udp (unique (map toString cfg.input-internet-accept-udp)) ++ ["-i retiolum -j Retiolum"] + ++ ["-i wiregrill -j Wiregrill"] )} ${formatTable cfg.extra.filter} ${formatTable cfg."extra${toString iptables-version}".filter} @@ -170,6 +186,23 @@ with import ./lib; ]; }."ip${toString iptables-version}tables" )} + ${concatMapStringsSep "\n" (rule: "-A Wiregrill ${rule}") ([] + ++ optional (cfg.accept-echo-request == "wiregrill") accept-echo-request + ++ map accept-tcp (unique (map toString cfg.input-wiregrill-accept-tcp)) + ++ map accept-udp (unique (map toString cfg.input-wiregrill-accept-udp)) + ++ { + ip4tables = [ + "-p tcp -j REJECT --reject-with tcp-reset" + "-p udp -j REJECT --reject-with icmp-port-unreachable" + "-j REJECT --reject-with icmp-proto-unreachable" + ]; + ip6tables = [ + "-p tcp -j REJECT --reject-with tcp-reset" + "-p udp -j REJECT --reject-with icmp6-port-unreachable" + "-j REJECT" + ]; + }."ip${toString iptables-version}tables" + )} COMMIT ''; } diff --git a/tv/3modules/lidControl.nix b/tv/3modules/lidControl.nix new file mode 100644 index 000000000..6a48da18d --- /dev/null +++ b/tv/3modules/lidControl.nix @@ -0,0 +1,45 @@ +with import ./lib; +{ config, pkgs, ... }: { + options = { + tv.lidControl.enable = mkEnableOption "tv.lidControl"; + }; + config = let + cfg = config.tv.lidControl; + in mkIf cfg.enable { + services.acpid.enable = true; + services.acpid.lidEventCommands = /* sh */ '' + set -- $1 + + # usage: vt_is_xserver NUMBER + vt_is_xserver() { + ${pkgs.iproute}/bin/ss -lp src unix:/tmp/.X11-unix/X* | + ${pkgs.gnused}/bin/sed -n 's|.*/tmp/.X11-unix/X\([0-9]\+\)\>.*|\1|p' | + ${pkgs.gnugrep}/bin/grep -Fqx "$1" + } + + console=$(${pkgs.kbd}/bin/fgconsole) + + if vt_is_xserver "$console"; then + # usage: run_on_display COMMAND [ARG...] + run_on_display() { + owner=$(${pkgs.coreutils}/bin/stat -c %u /tmp/.X11-unix/X$console) + ${pkgs.systemd}/bin/systemd-run -GPq \ + -E DISPLAY=:$console \ + --uid=$owner \ + "$@" + } + case $3 in + open) + run_on_display ${pkgs.xorg.xset}/bin/xset dpms force on + ;; + close) + run_on_display ${pkgs.xorg.xset}/bin/xset dpms force off + ;; + esac + fi + ''; + services.logind.lidSwitch = "ignore"; + services.logind.lidSwitchDocked = "ignore"; + services.logind.lidSwitchExternalPower = "ignore"; + }; +} diff --git a/tv/3modules/systemd.nix b/tv/3modules/systemd.nix new file mode 100644 index 000000000..db8a51994 --- /dev/null +++ b/tv/3modules/systemd.nix @@ -0,0 +1,47 @@ +with import ./lib; +{ config, ... }: let + normalUsers = filterAttrs (_: getAttr "isNormalUser") config.users.users; +in { + options = { + tv.systemd.services = mkOption { + type = types.attrsOf (types.submodule (self: { + options = { + operators = mkOption { + type = with types; listOf (enum (attrNames normalUsers)); + default = []; + }; + }; + })); + default = {}; + }; + }; + config = { + security.polkit.extraConfig = let + access = + mapAttrs' + (name: cfg: + nameValuePair "${name}.service" + (genAttrs cfg.operators (const true)) + ) + config.tv.systemd.services; + in optionalString (access != {}) /* js */ '' + polkit.addRule(function () { + const access = ${lib.toJSON access}; + return function (action, subject) { + if (action.id === "org.freedesktop.systemd1.manage-units") { + const unit = action.lookup("unit"); + if ( + (access[unit]||{})[subject.user] || + ( + unit.includes("@") && + (access[unit.replace(/@[^.]+/, "@")]||{})[subject.user] + ) + ) { + return polkit.Result.YES; + } + } + } + }()); + ''; + }; +} diff --git a/tv/3modules/wwan.nix b/tv/3modules/wwan.nix new file mode 100644 index 000000000..03cd512e4 --- /dev/null +++ b/tv/3modules/wwan.nix @@ -0,0 +1,181 @@ +with import ./lib; +{ config, pkgs, ... }: { + options = { + tv.wwan.enable = mkEnableOption "tv.wwan"; + tv.wwan.apn = mkOption { + type = with types; filename; + }; + tv.wwan.device = mkOption { + type = with types; pathname; + default = "/dev/cdc-wdm0"; + }; + tv.wwan.interface = mkOption { + type = with types; nullOr filename; + default = null; + }; + tv.wwan.operators = mkOption { + type = with types; listOf username; + default = []; + }; + tv.wwan.secrets = mkOption { + type = with types; pathname; + default = toString <secrets/wwan.json>; + # format: {"pin1":number} + }; + }; + config = let + cfg = config.tv.wwan; + in mkIf cfg.enable { + nixpkgs.overlays = singleton (self: super: { + uqmi-wrapper = pkgs.symlinkJoin { + name = "uqmi-wrapper"; + paths = [ + (pkgs.writeDashBin "uqmi" '' + exec ${pkgs.uqmi}/bin/uqmi --device=${cfg.device} "$@" + '') + (pkgs.writeTextDir "share/bash-completion/completions/uqmi" /* sh */'' + _uqmi_complete() { + case ''${#COMP_WORDS[@]} in + 2) + COMPREPLY=($(compgen -W "$( + ${pkgs.uqmi}/bin/uqmi --help 2>&1 | + ${pkgs.coreutils}/bin/tr , \\n | + ${pkgs.gnused}/bin/sed -nr 's/^ *(-[a-z-]+).*/\1/p' + )" -- "''${COMP_WORDS[1]}")) + ;; + esac + } + complete -F _uqmi_complete uqmi + '') + pkgs.uqmi + ]; + }; + }); + systemd.services.wwan = { + environment = { + SECRETS = "%d/secrets"; + }; + path = [ + pkgs.busybox + pkgs.coreutils + pkgs.iproute2 + pkgs.jq + pkgs.uqmi-wrapper + (pkgs.writeDashBin "get-interface" ( + if cfg.interface != null then /* sh */ '' + echo ${cfg.interface} + '' else /* sh */ '' + exec ${pkgs.libqmi}/bin/qmicli -d ${cfg.device} -p --get-wwan-iface + '' + )) + ]; + serviceConfig = { + LoadCredential = [ + "secrets:${cfg.secrets}" + ]; + Type = "oneshot"; + RemainAfterExit = true; + SyslogIdentifier = "wwan"; + ExecStart = pkgs.writeDash "tv.wwan.start.sh" '' + set -efu + + interface=$(get-interface) + + pin1_status=$( + uqmi --uim-get-sim-state | + jq -r '"\(.pin1_status)/\(.pin1_verify_tries)"' + ) + case $pin1_status in + verified/*) + : + ;; + not_verified/3) + pin1=$(jq .pin1 "$SECRETS") + echo "verifying PIN1" >&2 + if ! uqmi --uim-verify-pin1 "$pin1"; then + echo "error: failed to verify PIN1" >&2 + exit 1 + fi + ;; + not_verified/*) + echo "error: not trying to verify PIN1: not enough tries left" >&2 + echo \ + "please check your configuration in ${cfg.secrets}" \ + " and verify if manually using:" \ + " ${pkgs.uqmi}/bin/uqmi -d $device --uim-veriy-pin1 XXXX" \ + >&2 + exit 1 + esac + + raw_ip_path=/sys/class/net/$interface/qmi/raw_ip + raw_ip=$(cat "$raw_ip_path") + if [ "$raw_ip" != Y ]; then + echo "enabling raw-ip" >&2 + if ! echo Y > "$raw_ip_path"; then + echo "error: failed to enable raw-ip" >&2 + exit 1 + fi + fi + + operating_mode=$(uqmi --get-device-operating-mode | tr -d \") + case $operating_mode in + online) + : + ;; + persistent_low_power|low_power) + echo "settings device operating mode to online" >&2 + uqmi --set-device-operating-mode online + operating_mode=$(uqmi --get-device-operating-mode | tr -d \") + if test "$operating_mode" != online; then + echo "error: failed to set device operating mode to online" >&2 + exit 1 + fi + ;; + *) + echo "error: don't know how to change device operating mode to online: $operating_mode" >&2 + exit 1 + esac + + ip link set dev "$interface" up + + data_status=$(uqmi --get-data-status | tr -d \") + case $data_status in + connected) + : + ;; + disconnected) + echo "starting network (APN=${cfg.apn})" >&2 + sleep 1 + uqmi \ + --start-network \ + --autoconnect \ + --apn ${cfg.apn} \ + --ip-family ipv4 + sleep 1 + ;; + *) + echo "error: unsupported data status: $data_status" >&2 + exit 1 + esac + + udhcpc -q -f -n -i "$interface" + ''; + Restart = "on-failure"; + ExecStop = pkgs.writeDash "tv.wwan.stop.sh" '' + set -efu + + interface=$(get-interface) + + ip link set dev "$interface" down + uqmi --stop-network 0xFFFFFFFF --autoconnect + uqmi --sync + uqmi --set-device-operating-mode persistent_low_power + ''; + }; + }; + users.users.root.packages = [ + pkgs.uqmi-wrapper + ]; + tv.systemd.services.wwan.operators = cfg.operators; + }; +} diff --git a/tv/5pkgs/haskell/xmonad-tv/src/Build.hs b/tv/5pkgs/haskell/xmonad-tv/src/Build.hs deleted file mode 100644 index 553a129b1..000000000 --- a/tv/5pkgs/haskell/xmonad-tv/src/Build.hs +++ /dev/null @@ -1,24 +0,0 @@ -{-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE TypeApplications #-} - -module Build where - -import XMonad (Dimension) -import THEnv.JSON (getCompileEnvJSONExp) - - -myFont :: String -myFont = - "-schumacher-*-*-*-*-*-*-*-*-*-*-*-iso10646-*" - -myScreenWidth :: Dimension -myScreenWidth = - $(getCompileEnvJSONExp (id @Dimension) "XMONAD_BUILD_SCREEN_WIDTH") - -myTermFontWidth :: Dimension -myTermFontWidth = - $(getCompileEnvJSONExp (id @Dimension) "XMONAD_BUILD_TERM_FONT_WIDTH") - -myTermPadding :: Dimension -myTermPadding = - 2 diff --git a/tv/5pkgs/haskell/xmonad-tv/src/THEnv/JSON.hs b/tv/5pkgs/haskell/xmonad-tv/src/THEnv/JSON.hs deleted file mode 100644 index 2a3a0e523..000000000 --- a/tv/5pkgs/haskell/xmonad-tv/src/THEnv/JSON.hs +++ /dev/null @@ -1,18 +0,0 @@ -{-# LANGUAGE ScopedTypeVariables #-} - -module THEnv.JSON where - -import Data.Aeson (eitherDecode,FromJSON) -import Data.ByteString.Lazy.Char8 (pack) -import Language.Haskell.TH.Syntax (Exp,Lift(lift),Q) -import THEnv (getCompileEnv) -import Control.Monad - -getCompileEnvJSON :: (FromJSON a) => String -> Q a |