~/bend-docscommunity

cli/vmac.bend source

cli/vmac.bend on the hub · documented module

import Baseimport ../lib/effs/io.bend as Ximport ../lib/vstr.bend as Simport bend-net-url@0.4.0.0/url.bend as Url# The Mac's nix-darwin-labeled state outside `system.defaults` scalars, read for `v system import`.# Each reader answers setting lines for mac.now.bend; values are Nix expressions (T.KNix) unless scalar.def nix_line(name: String, v: String) -> String:  S.setting_line(name, "KNix", v)def bool_line(name: String, b: Bool) -> String:  S.setting_line(name, "KBool", S.pick(b, "true", "false"))def str_line(name: String, v: String) -> String:  S.setting_line(name, "KStr", S.nix_esc_chars(String.to_list(v)))def int_line(name: String, v: String) -> String:  S.setting_line(name, "KInt", v)def nonempty_line(+v: String, l: String) -> List<&2, String>:  S.line_if(Bool.not(String.is_empty(v)), l)def lines_of(+s: String) -> List<&2, String>:  S.nonempty(String.lines(String.trim(s)))# ---- numbers ----def digit_nat(c: Char) -> Nat:  U32.to_nat(U32.sub(Char.to_u32(c), 48))def nat_go(cs: List<&2, Char>, acc: Nat) -> Nat:  match cs:    case []:      acc    case c <> rest:      nat_go(rest, Nat.add(Nat.mul(acc, 10n), digit_nat(c)))def nat_of(s: String) -> Nat:  nat_go(String.to_list(S.digits(s)), 0n)# ---- URLs as paths: "file:///Applications/Google%20Chrome.app/" -> "/Applications/Google Chrome.app" ----# Percent-decoding is bend-net-url's (UTF-8 aware); a malformed URL is kept as it is.def decoded_or(m: Maybe<&2, String>, raw: String) -> String:  match m:    case None{}:      raw    case Some{v}:      vdef pct_decode(+s: String) -> String:  decoded_or(Url.pct.decode(s), s)def drop_prefix(+s: String, +p: String) -> String:  S.pick(String.starts_with(s, p), String.drop(s, String.length(p)), s)def drop_slash(+s: String) -> String:  S.pick(String.ends_with(s, "/") && Nat.is_gt(String.length(s), 1n), String.take(s, Nat.sub(String.length(s), 1n)), s)def url_path(+u: String) -> String:  pct_decode(drop_slash(drop_prefix(u, "file://")))# ---- Dock tiles: system.defaults.dock.persistent-apps / persistent-others ----def dock() -> String:  "\"$HOME/Library/Preferences/com.apple.dock.plist\""def tile_cmd(+key: String, +i: Nat) -> String:  "P=" ++ dock() ++ "; K=" ++ key ++ "." ++ Nat.show(i) ++  "; t=$(plutil -extract $K.tile-type raw $P 2>/dev/null) || exit 1" ++  "; printf '%s|%s|%s|%s|%s' \"$t\" \"$(plutil -extract $K.tile-data.file-data._CFURLString raw $P 2>/dev/null)\"" ++  " \"$(plutil -extract $K.tile-data.arrangement raw $P 2>/dev/null)\" \"$(plutil -extract $K.tile-data.displayas raw $P 2>/dev/null)\" \"$(plutil -extract $K.tile-data.showas raw $P 2>/dev/null)\""def arrangement(+n: String) -> String:  S.pick(String.eq(n, "2"), "date-added", S.pick(String.eq(n, "3"), "date-modified", S.pick(String.eq(n, "4"), "date-created", S.pick(String.eq(n, "5"), "kind", "name"))))def displayas(+n: String) -> String:  S.pick(String.eq(n, "1"), "folder", "stack")def showas(+n: String) -> String:  S.pick(String.eq(n, "1"), "fan", S.pick(String.eq(n, "2"), "grid", S.pick(String.eq(n, "3"), "list", "automatic")))def app_tile(+t: String, +p: String) -> String:  S.pick(String.eq(t, "spacer-tile"), "{ spacer = { small = false; }; }",  S.pick(String.eq(t, "small-spacer-tile"), "{ spacer = { small = true; }; }",  S.pick(String.eq(t, "directory-tile"), "{ folder = " ++ S.nix_str(p) ++ "; }",  S.pick(String.ends_with(p, ".app"), "{ app = " ++ S.nix_str(p) ++ "; }", "{ file = " ++ S.nix_str(p) ++ "; }"))))def other_tile(+t: String, +p: String, +f: List<&2, String>) -> String:  S.pick(String.eq(t, "directory-tile"),    "{ folder = { path = " ++ S.nix_str(p) ++ "; arrangement = \"" ++ arrangement(S.nth(f, 2n)) ++ "\"; displayas = \"" ++ displayas(S.nth(f, 3n)) ++ "\"; showas = \"" ++ showas(S.nth(f, 4n)) ++ "\"; }; }",    "{ file = " ++ S.nix_str(p) ++ "; }")def tile_nix(+key: String, +f: List<&2, String>) -> String:  S.pick(String.eq(key, "persistent-apps"), app_tile(S.nth(f, 0n), url_path(S.nth(f, 1n))), other_tile(S.nth(f, 0n), url_path(S.nth(f, 1n)), f))def tiles(n: Nat, +key: String, +i: Nat) -> IO(List<&2, String>):  match n:    case 0n:      IO.pure(List<&2, String>, [])    case 1n+m:      do IO<List<&2, String>>:        t : String <- X.sh(tile_cmd(key, i))        rest : List<&2, String> <- tiles(m, key, 1n+i)        return (" " ++ tile_nix(key, String.split(t, '|'))) <> restdef dock_list(+key: String) -> IO(List<&2, String>):  do IO<List<&2, String>>:    +n : String <- X.sh("plutil -extract " ++ key ++ " raw " ++ dock() ++ " 2>/dev/null")    ts : List<&2, String> <- tiles(nat_of(n), key, 0n)    return S.line_if(Bool.not(String.is_empty(String.trim(n))), nix_line("system.defaults.dock." ++ key, "[" ++ String.concat(ts) ++ " ]"))# ---- firewall, network, power ----# ---- the firewall, as a T.Firewall expression (its options exist only while it is on) ----def bend_bool(b: Bool) -> String:  S.pick(b, "True{}", "False{}")def firewall_expr(on: Bool, +f: String) -> String:  match on:    case True{}:      "T.FirewallOn{" ++ bend_bool(String.contains(f, "built-in signed software ENABLED")) ++ ", " ++      bend_bool(String.contains(f, "downloaded signed software ENABLED")) ++ ", " ++      bend_bool(String.contains(f, "block all state set to enabled")) ++ ", " ++      bend_bool(String.contains(f, "stealth mode is on")) ++ "}"    case False{}:      "T.FirewallOff{}"def firewall() -> IO(String):  do IO<String>:    +f : String <- X.sh("F=/usr/libexec/ApplicationFirewall/socketfilterfw; $F --getglobalstate; $F --getallowsigned; $F --getblockall; $F --getstealthmode")    return firewall_expr(String.contains(f, "Firewall is enabled"), f)# nix-darwin gives every known service the same DNS servers and search domains; a Mac where they differ cannot be declared.def same_for_all(+what: String, +outs: List<&2, String>) -> String:  S.pick(Nat.is_gt(S.count(outs), 1n), "throw \"V: " ++ what ++ " differ per network service\"", S.nix_list(S.words(S.nth(outs, 0n))))def per_service(+get: String) -> String:  "networksetup -listallnetworkservices | tail -n +2 | grep -v '^\\*' | while read -r s; do networksetup " ++ get ++ " \"$s\" | grep -v \"aren't any\" | tr '\\n' ' '; echo; done | sort -u"def network() -> IO(List<&2, String>):  do IO<List<&2, String>>:    svcs : String <- X.sh("networksetup -listallnetworkservices | tail -n +2 | grep -v '^\\*'")    dns : String <- X.sh(per_service("-getdnsservers"))    search : String <- X.sh(per_service("-getsearchdomains"))    return [nix_line("networking.knownNetworkServices", S.nix_list(lines_of(svcs))),            nix_line("networking.dns", same_for_all("DNS servers", String.lines(String.trim(dns)))),            nix_line("networking.search", same_for_all("search domains", String.lines(String.trim(search))))]def power() -> IO(List<&2, String>):  do IO<List<&2, String>>:    +b : String <- X.sh("pmset -g | grep 'Sleep On Power Button' | awk '{print $NF}'")    # power.restartAfterFreeze is left out: only `systemsetup` reports it, and only to an administrator.    return nonempty_line(String.trim(b), bool_line("power.sleep.allowSleepByPowerButton", String.eq(String.trim(b), "1")))# ---- security ----def is_nix_link(path: String) -> IO(Bool):  X.ok("readlink " ++ path ++ " | grep -q '^/etc/static/'")def sudo_extra(ls: List<&2, String>) -> List<&2, String>:  match ls:    case []:      []    case +l <> rest:      S.append(S.line_if(Bool.not(String.is_empty(String.trim(l)) || String.starts_with(l, "#") || String.contains(l, "env_keep+=TERMINFO")), l), sudo_extra(rest))def security() -> IO(List<&2, String>):  do IO<List<&2, String>>:    +pam : String <- X.sh("cat /etc/pam.d/sudo_local 2>/dev/null")    pam_nix : Bool <- is_nix_link("/etc/pam.d/sudo_local")    +sudo : String <- X.sh("cat /etc/sudoers.d/10-nix-darwin-extra-config 2>/dev/null")    pki : Bool <- is_nix_link("/etc/ssl/certs/ca-certificates.crt")    return [bool_line("security.pam.services.sudo_local.enable", pam_nix),            bool_line("security.pam.services.sudo_local.touchIdAuth", String.contains(pam, "pam_tid.so")),            bool_line("security.pam.services.sudo_local.watchIdAuth", String.contains(pam, "pam_watchid")),            bool_line("security.pam.services.sudo_local.reattach", String.contains(pam, "pam_reattach")),            bool_line("security.sudo.keepTerminfo", String.contains(sudo, "TERMINFO_DIRS")),            str_line("security.sudo.extraConfig", String.join(sudo_extra(String.lines(sudo)), "\n")),            bool_line("security.pki.installCACerts", pki)]# ---- keyboard: hidutil's UserKeyMapping ----def after_word(+w: String, ws: List<&2, String>) -> List<&2, String>:  match ws:    case []:      []    case +x <> +rest:      S.append(S.line_if(String.eq(x, w), S.digits(S.nth(rest, 1n))), after_word(w, rest))def pairs(src: List<&2, String>, dst: List<&2, String>) -> String:  match src:    case []:      ""    case s <> srest:      match dst:        case []:          ""        case d <> drest:          " { HIDKeyboardModifierMappingSrc = " ++ s ++ "; HIDKeyboardModifierMappingDst = " ++ d ++ "; }" ++ pairs(srest, drest)def keyboard() -> IO(List<&2, String>):  do IO<List<&2, String>>:    +km : String <- X.sh("hidutil property --get UserKeyMapping")    +none : Bool = String.eq(String.trim(km), "(null)") || String.eq(String.trim(km), "(\n)")    return [bool_line("system.keyboard.enableKeyMapping", Bool.not(none)),            nix_line("system.keyboard.userKeyMapping", "[" ++ pairs(after_word("HIDKeyboardModifierMappingSrc", S.words(String.join(String.lines(km), " "))), after_word("HIDKeyboardModifierMappingDst", S.words(String.join(String.lines(km), " ")))) ++ " ]"),            bool_line("system.keyboard.remapCapsLockToControl", False{}),            bool_line("system.keyboard.remapCapsLockToEscape", False{}),            bool_line("system.keyboard.swapCapsLockAndEscape", False{}),            bool_line("system.keyboard.swapLeftCommandAndLeftAlt", False{}),            bool_line("system.keyboard.swapLeftCtrlAndFn", False{}),            bool_line("system.keyboard.swapRightCommandAndRightOption", False{}),            bool_line("system.keyboard.nonUS.remapTilde", False{})]# ---- startup chime, time zone, primary user, Nix, shells, fonts, SSH ----def system() -> IO(List<&2, String>):  do IO<List<&2, String>>:    +mute : String <- X.sh("nvram StartupMute 2>/dev/null | awk '{print $2}'")    +tz : String <- X.sh("readlink /etc/localtime | sed 's|.*/zoneinfo/||'")    user : String <- X.sh("stat -f %Su /dev/console")    nix : Bool <- is_nix_link("/etc/nix/nix.conf")    shells : String <- X.sh("grep -v '^#' /etc/shells | grep -v -x -e /bin/bash -e /bin/csh -e /bin/dash -e /bin/ksh -e /bin/sh -e /bin/tcsh -e /bin/zsh")    +fonts : String <- X.sh("ls '/Library/Fonts/Nix Fonts' 2>/dev/null")    zsh : Bool <- is_nix_link("/etc/zshrc")    bash : Bool <- is_nix_link("/etc/bashrc")    fish : Bool <- is_nix_link("/etc/fish/config.fish")    ssh : Bool <- X.ok("launchctl print system/com.openssh.sshd >/dev/null 2>&1")    return S.append(nonempty_line(String.trim(mute), bool_line("system.startup.chime", Bool.not(String.eq(String.trim(mute), "%01")))),           [str_line("time.timeZone", String.trim(tz)),            str_line("system.primaryUser", String.trim(user)),            bool_line("nix.enable", nix),            nix_line("environment.shells", S.nix_list(lines_of(shells))),            nix_line("fonts.packages", S.pick(String.is_empty(String.trim(fonts)), "[ ]", "throw \"V: /Library/Fonts/Nix Fonts has fonts from no declared package\"")),            bool_line("programs.zsh.enable", zsh),            bool_line("programs.bash.enable", bash),            bool_line("programs.fish.enable", fish),            bool_line("services.openssh.enable", ssh)])# ---- users and groups (uid/gid >= 500, not system accounts) ----def dscl_get(+kind: String, +name: String, +attr: String) -> IO(String):  do IO<String>:    +v : String <- X.sh("dscl . -read '/" ++ kind ++ "/" ++ name ++ "' " ++ attr ++ " 2>/dev/null | sed -e '1s/^" ++ attr ++ "://' | tr '\\n' ' '")    return String.trim(v)def user_lines(names: List<&2, String>) -> IO(List<&2, String>):  match names:    case []:      IO.pure(List<&2, String>, [])    case +n <> rest:      do IO<List<&2, String>>:        uid : String <- dscl_get("Users", n, "UniqueID")        gid : String <- dscl_get("Users", n, "PrimaryGroupID")        dir : String <- dscl_get("Users", n, "NFSHomeDirectory")        real : String <- dscl_get("Users", n, "RealName")        shell : String <- dscl_get("Users", n, "UserShell")        +hidden : String <- dscl_get("Users", n, "IsHidden")        more : List<&2, String> <- user_lines(rest)        +u : String = "users.users.\"" ++ n ++ "\"."        return S.append([int_line(u ++ "uid", uid), int_line(u ++ "gid", gid), str_line(u ++ "home", dir), str_line(u ++ "description", real),                         nix_line(u ++ "shell", shell), bool_line(u ++ "isHidden", String.eq(hidden, "1"))], more)def group_lines(names: List<&2, String>) -> IO(List<&2, String>):  match names:    case []:      IO.pure(List<&2, String>, [])    case +n <> rest:      do IO<List<&2, String>>:        gid : String <- dscl_get("Groups", n, "PrimaryGroupID")        real : String <- dscl_get("Groups", n, "RealName")        members : String <- dscl_get("Groups", n, "GroupMembership")        more : List<&2, String> <- group_lines(rest)        +g : String = "users.groups.\"" ++ n ++ "\"."        return S.append([int_line(g ++ "gid", gid), str_line(g ++ "description", real), nix_line(g ++ "members", S.nix_list(S.words(members)))], more)def users() -> IO(List<&2, String>):  do IO<List<&2, String>>:    us : String <- X.sh("dscl . -list /Users UniqueID | awk '$2>=500 && $1 !~ /^_/ {print $1}'")    gs : String <- X.sh("dscl . -list /Groups PrimaryGroupID | awk '$2>=500 && $1 !~ /^_/ {print $1}'")    a : List<&2, String> <- user_lines(lines_of(us))    b : List<&2, String> <- group_lines(lines_of(gs))    return S.append([nix_line("users.knownUsers", "[ ]"), nix_line("users.knownGroups", "[ ]")], S.append(a, b))# ---- launchd files other than nix-darwin's own (org.nixos.*), the project's services and Determinate Nix's (nix.enable = false) ----# Each is kept in nix/launch/ under its content hash, so a changed file on the Mac shows up as a change.def launch_file(+opt: String, +dir: String, +f: String) -> IO(List<&2, String>):  do IO<List<&2, String>>:    +h : String <- X.sh("shasum -a 256 \"" ++ dir ++ "/" ++ f ++ "\" | cut -c1-12")    cp : Bool <- X.ok("mkdir -p nix/launch && cp \"" ++ dir ++ "/" ++ f ++ "\" nix/launch/" ++ String.trim(h) ++ "-" ++ f)    return [nix_line(opt ++ ".\"" ++ f ++ "\".source", "./launch/" ++ String.trim(h) ++ "-" ++ f)]def launch_files(+opt: String, +dir: String, fs: List<&2, String>) -> IO(List<&2, String>):  match fs:    case []:      IO.pure(List<&2, String>, [])    case +f <> rest:      do IO<List<&2, String>>:        a : List<&2, String> <- launch_file(opt, dir, f)        b : List<&2, String> <- launch_files(opt, dir, rest)        return S.append(a, b)def plists(xs: List<&2, String>) -> String:  match xs:    case []:      ""    case x <> rest:      " " ++ x ++ ".plist" ++ plists(rest)def launch_dir(+opt: String, +dir: String, +labels: List<&2, String>) -> IO(List<&2, String>):  do IO<List<&2, String>>:    fs : String <- X.sh("cd \"" ++ dir ++ "\" && for f in *; do [ -f \"$f\" ] || continue; case \" " ++ plists(labels) ++ " \" in *\" $f \"*) continue;; esac; case \"$f\" in org.nixos.*|systems.determinate.*) ;; *) echo \"$f\";; esac; done")    launch_files(opt, dir, lines_of(fs))def launch(+labels: List<&2, String>) -> IO(List<&2, String>):  do IO<List<&2, String>>:    a : List<&2, String> <- launch_dir("environment.launchDaemons", "/Library/LaunchDaemons", labels)    b : List<&2, String> <- launch_dir("environment.launchAgents", "/Library/LaunchAgents", labels)    c : List<&2, String> <- launch_dir("environment.userLaunchAgents", "$HOME/Library/LaunchAgents", labels)    return S.append(a, S.append(b, c))# ---- all of it ----# `labels`: the project's own launchd jobs (nix-darwin writes them; they are not imported).def read_all(+labels: List<&2, String>) -> IO(List<&2, String>):  do IO<List<&2, String>>:    apps : List<&2, String> <- dock_list("persistent-apps")    others : List<&2, String> <- dock_list("persistent-others")    net : List<&2, String> <- network()    pw : List<&2, String> <- power()    sec : List<&2, String> <- security()    kb : List<&2, String> <- keyboard()    sys : List<&2, String> <- system()    us : List<&2, String> <- users()    ld : List<&2, String> <- launch(labels)    return S.append(apps, S.append(others, S.append(net, S.append(pw, S.append(sec, S.append(kb, S.append(sys, S.append(us, ld))))))))