Z6KKTECIOJI43234XW3KBUXXQNTG2JDSMMCARI7TZLNJGJRFMRIAC ;; SPDX-License-Identifier: BSD-2-Clause(macro while(lambda (args)`(label ((loop (lambda (),@(cdr args)(cond (,(car args) (loop))))))(cond (,(car args) (loop))))))(macro if (lambda (args)`(cond (,(car args) ,(cadr args))(true ,@(cddr args)))))(macro when (lambda (args)`(cond (,(car args),@(cdr args)))))(defun dolines-until (pred op)(let* ((result (getline))(line (car result))(rv (cadr result)))(cond((pred line) true)((> rv 0) (op line)(dolines-until pred op))((= rv 0) nil)((< rv 0) nil))))(defun any (pred lis)(cond ((null lis) false)((pred (car lis)) true)(true (any pred (cdr lis)))))(defun mapcars args(let ((f (car args))(as (cdr args)))(label ((map(lambda (f as res)(cond((any null as) (nreverse res))(true (map f (mapcar cdr as)(cons (apply f (mapcar car as)) res)))))))(map f as nil))))(defun zip args (apply mapcars list args))(defun dolines (fn)(dolines-until (lambda (line) false) fn))(defun getlines ()(let ((lines '()))(dolines (lambda (line)(setq lines (cons line lines))))(nreverse lines)))(defun output-lines-of argv(let ((command (apply make-safe-shell-command argv)))(with-input-from "|" command(prog1(getlines)(close command)))))(defun output-of argv(apply string-join "\n" (apply output-lines-of argv)))(defun split-at-first (pred lis)(label ((spla (lambda (pred lis first-lis)(cond((pred (car lis))(list (nreverse first-lis) lis))(true (spla pred(cdr lis)(cons (car lis) first-lis)))))))(spla pred lis nil)))(defun *rooted* (file-path) (if file-path file-path nil))(defun trim (s) (sub "[[:space:]]*$" "" (sub "^[[:space:]]*" "" s)))(defun path-join argv(gsub "/+" "/" (apply string-join "/" argv)))
;; SPDX-License-Identifier: BSD-2-Clause;; Probes are pieces of code that find things out about the system;; under configuration, informing what changes we need.;; for use by probes, to avoid log confusion(defun subprobe-output argv(apply output-of argv))(defun probe-output argv(with-indent(log-probe "output of %s" (repr argv))(apply subprobe-output argv)));; returns true or false, not an exitcode(defun subprobe-success argv(let ((exitcode (apply system argv)))(eq exitcode 0)))(defun probe-success argv(with-indent(log-probe "exitcode of %s" (repr argv))(apply subprobe-success argv)))(defun subprobe-sed-crypto-digest (script fp)(let* ((command (append (list "sed" "-e" script fp)(pipe-crypto-digest))))(apply output-of command)));; note: this one roots its file-path!(defun probe-sed-would-change (script file-path)(with-indent(log-probe "would %s be changed by sed %s ?" file-path script)(let ((fp (*rooted* file-path)))(not (equal (crypto-digest fp)(subprobe-sed-crypto-digest script fp))))))(defun -f (fn)(let* ((rv (subprobe-success "/bin/test" "-f" fn)))(log-probe "is %s a file? %s" fn (if rv "yes" "no"))rv))(defun -e (fn)(let* ((rv (subprobe-success "/bin/test" "-e" fn)))(log-probe "does %s exist? %s" fn (if rv "yes" "no"))rv))(defun -d (dn)(let* ((rv (subprobe-success "/bin/test" "-d" dn)))(log-probe "is %s a directory? %s" dn (if rv "yes" "no"))rv))
;; SPDX-License-Identifier: BSD-2-Clause;; Platform-specific code (shall we say - not specified by POSIX).;; Until otherwise specified, the platform is assumed to be FreeBSD;; (base, no third-party packages installed).;; why sha512t256? "On 64-bit hardware, this algorithm is;; approximately 50% faster than SHA-256 but with the same level of;; security..." -- md5(1)(defun crypto-digests filenames(let ((command (append '("sha512t256" "-q") filenames)))(or (apply output-lines-of command)(error "no output from command %s" (repr command)))))(defun crypto-digest (filename)(some-output-or-error "sha512t256" "-q" filename));; see probe-sed-crypto-digest(defun pipe-crypto-digest () (list :pipe "sha512t256" "-q"));; -r: random (version 4) UUID(defun get-new-uuid () (some-output-or-error "uuidgen" "-r"));; %z: size in bytes (st_size). GNU and Busybox use %s for that, but;; FreeBSD stat says "bad format" about %s.(defun get-size-of-file (filename)(as-number (some-output-or-error "stat" "-f" "%z" filename)));; maybe we could do some other checks on this, but nonempty should;; suffice to begin with(defun make-temp-filename-from (based-on-path)(some-output-or-error "mktemp" (sprintf "%s.XXXXXXXX" based-on-path)))(defun get-ogm (source-path);; The jail has its own set of users, so to get the owner and group;; of a file as names, we have to run stat inside the jail, so it;; will query the right passwd file. (The alternative is to use only;; numeric uids and gids everywhere; but some users and groups are;; just created with the next available id, including some created;; by packages; so you can't know their numeric ids ahead of time.)(let ((user (apply some-output-or-error(*run-in-chroot* "stat" "-f" "%Su" source-path)))(group (apply some-output-or-error(*run-in-chroot* "stat" "-f" "%Sg" source-path)));; Lp: "low perms." user, group, and other bits from perms,;; but not file type bits. this will be an octal number; let;; us not treat it as a decimal number.(mode (some-output-or-error "stat" "-f" "%Lp" source-path)))(list user group mode)))(defun set-ogm (dest-path user group mode);; Likewise, in order to consume the right mapping of users to;; numeric uids, we have to run the chown and chmod inside the jail;; and pass the unmodified path.(apply system-or-error (*run-in-chroot*"chown" (sprintf "%s:%s" user group) dest-path))(apply system-or-error (*run-in-chroot*"chmod" mode dest-path)));; for GNU, you could chown --reference and chmod --reference(defun copy-ogm (source-path dest-path)(apply set-ogm dest-path (get-ogm source-path)))
;; SPDX-License-Identifier: BSD-2-Clause(setq *indent* 0)(setq *log-tags* '( :info :warning :error :probe :rule :change))(macro with-indent(lambda (args)`(let ((oldindent *indent*))(setq *indent* (+ *indent* 2))(prog1(progn ,@args)(setq *indent* oldindent)))))(defun indented-printf args(let ((fmt (car args))(args (cdr args)))(apply printf (sprintf "%%-%ds%s" *indent* fmt) "" args)))(defun err-ind-printf args(with-output-to ">>" "/dev/stderr"(apply indented-printf args)(printf "\n")(fflush)))(defun log-debug args(when (memq :debug *log-tags*)(apply err-ind-printf(cons (strcat "%s " (car args))(cons "DEBUG " (cdr args))))))(defun log-info args(when (memq :info *log-tags*)(apply err-ind-printf(cons (strcat "%s " (car args))(cons "INFO " (cdr args))))))(defun log-warning args(when (memq :warning *log-tags*)(apply err-ind-printf(cons (strcat "%s " (car args))(cons "WARN " (cdr args))))))(defun log-error args(when (memq :error *log-tags*)(apply err-ind-printf(cons (strcat "%s " (car args))(cons "ERROR " (cdr args))))))(defun log-bucket (file-path meta new-path)(err-ind-printf "%s -> %s (meta: %s)\n" file-path new-path meta))(defun log-probe args(when (memq :probe *log-tags*)(apply err-ind-printf(cons (strcat "%s " (car args))(cons "probe " (cdr args))))))(defun log-rule-start args(when (memq :rule *log-tags*)(apply err-ind-printf(cons (strcat "%s\n%s " (car args))(cons "-----" (cons "rule " (cdr args)))))))(defun log-rule-end ()(when (memq :rule *log-tags*)(err-ind-printf ".....\n")))(macro with-log-rule(lambda (args)`(progn(log-rule-start ,@(car args))(with-indent(progn,@(cdr args)))(log-rule-end))))(defun log-change args(when (memq :change *log-tags*)(apply err-ind-printf(cons (strcat "%s " (car args))(cons "CHANGE " (cdr args))))));; see the set-x macro(defun log-command (cmd)(err-ind-printf "+ %s" cmd))
(defun use-canned-facts-freebsd-jail (hostname alt-root)(setq *fact-short-hostname* hostname)(setq *fact-uname-os* "FreeBSD")(setq *fact-alt-root* alt-root)(setq *rooted* (lambda (pn) (strcat alt-root pn)))(setq *run-in-chroot*(lambda argv (append (list "chroot" *fact-alt-root*) argv)))(log-info "working with alternate root %s" alt-root))(defun use-this-host ()(setq *fact-short-hostname* (output-of "hostname" "-s"))(setq *fact-uname-os* (output-of "uname" "-s"))(setq *fact-alt-root* nil)(setq *rooted* (lambda (pn) pn))(setq *run-in-chroot*(lambda argv argv))(log-info "working on this machine, %s, with the real root"*fact-short-hostname*))
;; SPDX-License-Identifier: BSD-2-Clause(macro set-x (lambda (args)`(if (memq :set-x *log-tags*)(let ((ossc *show-system-command*))(setq *show-system-command* log-command)(prog1(progn ,@args)(setq *show-system-command* ossc)))(progn ,@args))));;;; This is something like a distant child of an Either monad and the;; "set -e" mechanism whereby shells quit when errors happen.;;(defun system-or-error args(let ((rv (apply system args)))(when (not (eq rv 0))(error "system with arguments %s returned exitcode %d" (repr args) rv))rv))(defun some-output-or-error args(let ((output (apply output-of args)))(if (equal "" output)(error "command %s returned no output" (repr args))output)))
;; SPDX-License-Identifier: BSD-2-Clause;; Herein, we shall generally use "file-path" to mean a path as;; written in a rule, e.g. /etc/passwd; and the shorter "fp" to mean;; that path as accessible during the build process, e.g.;; /home/jaredj/buildin-ma-jails/jail326/root/etc/passwd.(defun sed (script file-path)(let ((new (make-temp-filename-from file-path))(fp (*rooted* file-path))(nfp (*rooted* new)))(system-or-error "sed" "-e" script fp :gt nfp)(copy-ogm file-path new)(system-or-error "mv" nfp fp)));; e.g.;; (defun do-a-thing-with-a-file (file-path);; (with-existing-rooted-plain-file-a-rule;; (file-path fp);; ("we are doing a thing with file %s" file-path);; (do-thing-with fp))));;;; fp in the above example is *introduced* inside the first argument;; of w-e-r-p-f-a-r.(macro with-existing-rooted-plain-file-a-rule(lambda (args)(let ((file-path-name (caar args))(fpname (cadar args))(rule-log-args (cadr args))(body (cddr args)))`(with-log-rule ,rule-log-args(let ((,fpname (*rooted* ,file-path-name)))(if (-f ,fpname)(progn ,@body)(log-warning "plain file %s not found. no changes made" ,fpname)))))))(defun replace-line-in-file (file-path detect-regex replace-line)(with-existing-rooted-plain-file-a-rule (file-path fp)("replace-line-in-file: in %s, regex %s -> line %s" file-path detect-regex replace-line)(let ((script (sprintf "/%s/c \\\n%s\n" detect-regex replace-line)))(if (probe-sed-would-change script file-path)(log-info "no change necessary")(sed script file-path)))));; this only adds to the _end_ of the file, at present(defun add-line-to-file (file-path line detect-regex)(with-existing-rooted-plain-file-a-rule (file-path fp)("add-line-to-file: in %s, add %s" file-path line)(let* ((grep-command (if detect-regex(list "grep" detect-regex fp)(list "grep" "-xF" line fp)))(output (apply probe-output grep-command))(script (sprintf "$a\\\n%s\n" line)))(if (null output)(sed script file-path)(log-info "no change necessary")))))(defun delete-lines-matching (file-path regex)(with-existing-rooted-plain-file-a-rule (file-path fp)("delete-lines-matching: in %s, regex %s" file-path regex)(let* ((script (sprintf "/%s/d" regex)))(if (probe-sed-would-change script file-path)(sed script file-path)(log-info "no change necessary")))))(defun file-exists (file-path owner group mode)(with-log-rule ("file-exists: %s, owned by %d:%d, mode %s"file-path owner group mode)(let ((fp (*rooted* file-path)))(cond((-f fp)(if (equal (get-ogm file-path) (list owner group mode))(log-info "no change necessary")(set-ogm file-path owner group mode)))((-e fp)(error "%s exists but is not a plain file" fp))(true(system-or-error "touch" fp)(set-ogm file-path owner group mode))))))(defun file-does-not-exist (file-path)(with-log-rule ("file-does-not-exist: %s" file-path)(let ((fp (*rooted* file-path)))(cond((-f fp)(system-or-error "rm" fp))(true(log-info "no change necessary"))))))(defun dir-exists (dir-path owner group mode)(with-log-rule ("dir-exists: %s" dir-path)(let ((dp (*rooted* dir-path)))(cond((-d dp)(if (equal (get-ogm dp) (list owner group mode))(log-info "no change necessary")(set-ogm dp owner group mode)))((-e dp)(error "%s exists but is not a directory" dp))(true(system-or-error "mkdir" dp)(set-ogm dp owner n-group mode))))))(defun editing-nonexistent-file-error (file-path)(error "editing nonexistent file %s. ensure file-exists first" file-path))(defun file-has-immediate-contents (file-path contents)(with-log-rule ("file-has-immediate-contents: %s (contents not shown)"file-path)(let ((new (make-temp-filename-from file-path))(fp (*rooted* file-path))(new-fp (*rooted* new)))(if (-f fp)(progn(with-output-to ">" new-fp (printf "%s" contents))(if (equal (crypto-digest fp)(crypto-digest new-fp))(progn(log-info "no change necessary")(system "rm" "-f" new-fp))(copy-ogm file-path new)(system-or-error "mv" new-fp fp)))(editing-nonexistent-file-error file-path)))))(defun file-text-copied-from (dest-file-path src-file-path);; We're not going to assume the src-file-path has the right perms.;; Also the src-file-path is expected to be outside any chroot.(when (not (-f src-file-path))(error "source file %s does not exist" src-file-path))(with-existing-rooted-plain-file-a-rule (dest-file-path dfp)("file-contents-copied-from: %s <- %s" dest-file-path src-file-path)(if (equal (crypto-digest dfp) (crypto-digest src-file-path))(log-info "no change necessary")(let ((tmp (make-temp-filename-from dest-file-path))(dfp-tmp (*rooted* tmp)))(with-input-from "<" src-file-path(with-output-to ">" dfp-tmp(dolines (lambda (l) (printf "%s\n" l)))))(copy-ogm dest-file-path tmp)(system-or-error "mv" dfp-tmp dfp)))))(defun file-contents-from-m4-template (dest tmpl vars)(when (not (-f tmpl))(error "nonexistent template %s" tmpl))(with-existing-rooted-plain-file-a-rule (dest dfp)(label((make-dash-D(lambda (kv)(let ((k (car kv)) (v (cadr kv)))(sprintf "-D%s:%s" k v)))))(let ((ft (make-temp-filename-from "fcfm4t-XXXXXXXX"))(rft (*rooted* ft))(dash-Ds (mapcar make-dash-D vars)))(apply system-or-error `("m4" ,@dash-Ds :gt ,rft))(if (equal (crypto-digest rft)(crypto-digest dfp))(log-info "no change necessary")(copy-ogm dest ft)(system-or-error "mv" rft dfp))))))(defun file-exists-with-entire-contents (file-path o g m desc contents)(file-exists file-path o g m)(file-has-immediate-contents file-path contents))(defun local-user-exists (user-name additional-groups)(let ((alt-root-addendum(if *fact-alt-root* (list "-R" *fact-alt-root*) nil))(groups-addendum(if additional-groups(list "-G" (apply string-join "," additional-groups))nil)))(if (probe-success "grep" "-q" (sprintf "^%s:" user-name)(*rooted* "/etc/passwd"))(log-info "no change necessary")(apply system-or-error `("pw" ,@alt-root-addendum"useradd" "-n" ,user-name "-m",@group-addendum)))))(defun package-installed (name pkg-opts)(let* ((pkg-opts (if *fact-alt-root*(append (list "-r" *fact-alt-root*) pkg-opts)pkg-opts))(installed (apply probe-success `("pkg" ,@pkg-opts"info" "-e" name))))(if installed(log-info "no change necessary");; "If pkg is already installed, nothing is done."(apply system-or-error `("pkg" ,@pkg-opts "bootstrap"))(apply system-or-error `("pkg" ,@pkg-opts "install" "-y" ,name)))));; if we have a sysrc variable whose name is "varname", whose existing;; value is "value1 value2", and we run "sysrc varname+=value3", then;; sysrc says "varname: value1 value2 -> value1 value2 value3".;; between the colon and the arrow is the old value; between the arrow;; and the end of the line is the new value.(defun parse-sysrc-output (output)(let* ((just-old-and-new (trim (sub "^[^:]+:" "" res)))(oan (split just-old-and-new " -> "))(o (car oan))(n (cadr oan)))(list o n)));; op is :set, :add, or :remove(defun sysrc (op name value)(let ((alt-root(if *fact-alt-root* (list "-R" *fact-alt-root*) nil)))(cond((eq op :set)(let ((setting (sprintf "%s=%s" name value)))(if (apply probe-success `("sysrc" "-c" ,@alt-root ,setting))(log-info "no change necessary")(apply system-or-error `("sysrc" ,@alt-root ,setting)))))((eq op :add);; You can't sysrc -c ...+=... But if you try to add a value;; onto a variable when it's already in there, nothing happens:;; the += operation is idempotent. But we don't want to just;; always say we changed it: that'll drown real changes in;; chaff.;;;;;; If we are trying to add value3 and it wasn't there to begin;; with, the old value will be different from the new value. but;; if it was already in there, the two complete values will be;; the same.(let* ((add-s (sprintf "%s+=$s" name value))(sub-s (sprintf "%s-=%s" name value))(res (apply output-of `("sysrc" ,@alt-root ,add-s)))(old-and-new (parse-sysrc-output res))(o (car old-and-new))(n (cadr old-and-new)))(if (equal o n);; The value was unchanged by our snooping. That means the;; thing we were trying to add is already in there, so our;; check succeeds and our change will not be done (nor;; superfluously reported).(log-info "no change necessary");; We changed it while trying to check. The added value will;; be on the end, so if we remove it, everything will be;; back how we found it, so we can then change it. :);;;; -- except here in lacrum, we aren't separating checks and;; changes yet, nor worrying about how to undo a change, so;; we shall just be happy. we did the change!true)))((eq op :remove);; Same as above: you can't check (-c) with -=.(let* ((sub-s (sprintf "%s-=%s" name value))(res (apply output-of `("sysrc" ,@alt-root ,sub-s)))(old-and-new (parse-sysrc-output res))(o (car old-and-new))(n (cadr old-and-new)))(if (equal o n)(log-info "no change necessary");; while checking it we removed it. be happytrue)))(true(error "unknown sysrc op %s; use :set, :add, or :remove" op)))))
BUILD_AWK = awkGLOTAWK = ../glotawkSOURCES = \util.glotawk \logging.glotawk \exitcodes.glotawk \platform.glotawk \probes.glotawk \facts.glotawk \changes.glotawklacrum: $(SOURCES)(for s in $(SOURCES); do echo "(load \"$$s\")"; done; echo "(save-lisp-and-die \"$(GLOTAWK)\" \"$@\" 'repl)") | $(BUILD_AWK) -v LOG_LEVEL=3 -f $(GLOTAWK)clean:rm -f lacrum