#!/usr/bin/env nix-shell #!nix-shell -i ysh -p nettools -p oils-for-unix # TODO: simplify variant handling by defining `desko-full = desko`, etc, in nix config. const SELF = $(hostname) # which hosts to deploy to (in order), when "all hosts" is specified or implied. const ALL_HOSTS = [ "moby", "flowy", "desko", "servo" ] # which variants to build (in order), when "all variants" is specified or implied. const ALL_VARIANTS = ["-min", "-light", "", "-next-min", "-next-light", "-next"] # mutated by `main` var DRY_RUN = false var NIX_ARGS = [] # extra args to pass to nix invocations proc usage (;; status=1) { echo "deploy: deploy a nix config to a remote machine, possibly activating it" echo "" echo "usage: deploy [options] [host] [host2 ...]" echo "options:" echo "- --build: only build; don't copy or deploy" echo "- --copy: only build + copy files, nothing more" echo "- --switch (default)" echo "- --test: switch to the new configuration, but do not make it bootable" echo "- --dry-run: show what would be done without actually doing it" echo "- --pre: alias for --action copy ..." echo ' if otherwise unspecified, implies `--variant all`' echo " if no host is specified, copies to all known hosts" echo "- --reboot: reboot the target machine after deploying (if deployed with no errors)" echo "- --reboot-force: reboot the target machine after deploying (even on error)" echo "- --variant light|min|''|all (default: '')" echo "- --wireguard always|never|opportunistic: deploy over wireguard" echo "- --ip
: deploy to the specific IP address" echo "- --deriv /nix/store/...: prebuilt store path (or .drv to realize) to deploy instead of (re-)building the default target" echo "" echo "common idioms:" echo "- deploy all: deploy all hosts, sequentially" echo "- deploy --pre: build and copy all hosts" echo "- deploy desko lappy: build and deploy just those hosts" echo "- deploy: deploy the local host" exit "$status" } proc info (...plain; ...escape) { for e in (escape) { call plain->append(toJson(e)) } echo "[deploy]" @plain >&2 } var CliArgs = Object(null, { action: null, # str, e.g. build, copy, switch, test deriv: null, # str, e.g. /nix/store/... dryRun: false, # bool hosts: [], # list of str, e.g. [ "desko", "moby" ] help: false, ip: null, pre: false, reboot: false, rebootForce: false, variants: [], # list of str, e.g. [ "-min" ] or [ "-light", "" ] wireguard: "opportunistic", # str: e.g. "opportunistic", "never", "always" unhandled: [], # unhandled arguments; forward them to nix? }) # each NixConfig instance represents _one_ nix build target + host-specific deployment strategy, # e.g. "build lappy-min; then copy it to lappy (over wireguard) but don't activate it" var NixConfig = Object(null, { host: null, # str, e.g. "desko" variant: "", # str, e.g. "-min", "-light" drvPath: null, # str, e.g. "/nix/store/...-nixos-system.drv" storePath: null, # str, e.g. "/nix/store/...-nixos-system" action: "switch", # str, e.g. "switch", "copy", .... TODO: convert to enum? ip: null, # str, e.g. "10.78.79.51" wireguard: "opportunistic", # str, e.g. "opportunistic", "never", "always". TODO: convert to enum? reboot: false, # may be false, true, or "force" }) # pass through normal hosts, but expand aliases like `"all"` into `[ "moby", "lappy", ... ]`. # @arg(host): str # @return: list[str] func expandHostAlias (host) { if (host === "all" ) { return (ALL_HOSTS) } else { return ([host]) } } # pass through normal variants, but expand aliases like `"all"` into `[ "-min", "-light", ... ]`. # @arg(v): str # @return: list[str] func expandVariantAlias (v) { if (v === "all") { return (ALL_VARIANTS) } elif (v === "") { # "full" variant return ([""]) } else { return (["-$v"]) } } # internal function for use by `parseArgs`. # transforms argv into `CliArgs` object. should be side-effect free. # @arg(cur, next, ...): each one is str; unpacked only for internal use # @arg(p): intermediate CliArgs object; meant for internal use # @return: CliArgs instance func _parseArgs(p,cur=null, next=null, ...rest) { if (cur === null) { # end return (p) } else { case (cur) { --build | --copy | --switch | --test { setvar p.action = cur[2:] return (_parseArgs (p, next, ...rest)) } --deriv { setvar p.deriv = next return (_parseArgs (p, ...rest)) } --dry-run { setvar p.dryRun = true return (_parseArgs (p, next, ...rest)) } --help { setvar p.help = true return (_parseArgs (p, next, ...rest)) } --ip { setvar p.ip = next return (_parseArgs (p, ...rest)) } --pre { setvar p.pre = true return (_parseArgs (p, next, ...rest)) } --reboot { setvar p.reboot = true return (_parseArgs (p, next, ...rest)) } --reboot-force | --force-reboot { setvar p.rebootForce = true return (_parseArgs (p, next, ...rest)) } --variant { call p.variants->append(next) return (_parseArgs (p, ...rest)) } --wireguard { setvar p.wireguard = next return (_parseArgs (p, ...rest)) } all | crappy | desko | flowy | lappy | moby | servo { call p.hosts->append(cur) return (_parseArgs (p, next, ...rest)) } (else) { call p.unhandled->append(cur) return (_parseArgs (p, next, ...rest)) } } } } # front-facing argument parser. # transforms argv into `CliArgs` object. should be side-effect free. # @arg(...): str # @return: CliArgs instance func parseArgs(...args) { var p=Object(CliArgs, {}) return (_parseArgs (p, ...args)) } proc destructive (...cmd; ; echo=false) { if (DRY_RUN) { info "dry-run:" (...cmd) } else { if (echo) { echo @cmd >&2 } @cmd } } # return "$ip", "$host" or "$host-hn", based on if ip was specified, # or if wireguard was requested. func resolveHost (host, ip, wireguard) { if (host === SELF) { return (host) } elif (ip) { return (ip) } else { case ("$wireguard-$host") { opportunistic-moby { return ("$host-hn") } opportunistic-* { return (host) } never-* { return (host) } always-* { return ("$host-hn") } (else) { return ("$host-hn") } } } } # return the number of seconds to allot to `nix copy` when copying the given variant. # this is done to avoid stalled copies from blocking the entire process, while hopefully not breaking the copies that are actually important func timeoutFor (variant) { case (variant) { -min | -light | -next { return (3600) } -next-min | -next-light { return (1800) } (else) { # this catches the normal variant return (14400) } } } proc runOnTarget (host, ...cmd) { # run the command on the machine we're deploying to. # if that's a remote machine, then do it via ssh, else local shell. if (host !== SELF) { info "running on remote ($host):" @cmd ssh "$host" @cmd } else { info "running locally ($SELF):" @cmd @cmd } } proc deployOneHost (; nixcfg) { # unpack the cfg var timeout = timeoutFor(nixcfg.variant) var host = nixcfg.host var attrName = "$[nixcfg.host]$[nixcfg.variant]" # first, realize the host config derivation if not exists: var myStorePath = nixcfg.storePath if (not myStorePath) { var drvPath = nixcfg.drvPath if (not drvPath) { # `nix-build -A foo` evals and then realizes foo, but it never unloads the memory used to eval foo. # my exprs are heavyweight, we need that memory for building, so do the evals separately from the realizations: info "$attrName: evaluating..." setvar drvPath = $(nix eval --raw -f . "hosts.$attrName.toplevel.drvPath") } info "$attrName: building $drvPath" setvar myStorePath = $(destructive nix-store --realize "$drvPath" @NIX_ARGS) info "$attrName: built $myStorePath" } # mimic `nixos-rebuild --target-host`, in effect: # - nix-copy-closure ... # - nix-env --set ... # - switch-to-configuration