(define-module (env) #:use-module (gcrypt hash) #:use-module (guix base32) #:use-module (guix build syscalls) #:use-module (guix build utils) #:use-module (guix hash) #:use-module (ice-9 ftw) #:use-module (ice-9 match) #:use-module (ice-9 textual-ports) #:use-module (rnrs bytevectors) #:use-module ((scheme base) #:select (bytevector-append)) #:use-module (srfi srfi-1) #:use-module (srfi srfi-9) #:export (add-layer call-with-env collect-garbage! make-overlay-mount overlay-mount? overlay-mount-lower overlay-mount-upper overlay-mount-work overlay-mount-merged environment-current environment-name environment-root environment-tag-layers environment-tag-root? environment-tag-value environment-tags environment-tree environment? init-environment layers-in-use list-environments lookup-environment mount-environment remove-env-no-checks! roll-back! environment->overlay-mount-maybe unmount-environment-maybe environment-mounted?)) (define directory-files (@@ (ice-9 ftw) directory-files)) (define (overlayfs-command lower upper work merged) ;; NOTE lower dirs are listed top to bottom from left to right! (let ((lower* (match lower (() (throw 'overlayfs-no-lower-dir)) ((l ...) (string-join l ":")) (l l)))) (list "fuse-overlayfs" "-o" (string-append "lowerdir=" lower* ",upperdir=" upper ",workdir=" work) merged))) ;; TODO handle missing env variables ;; TODO handle missing directory (define-syntax-rule (define-env-path name s s* ...) ;; Canonicalize path? (define name (string-append s s* ...))) (define-env-path %cache-directory (or (getenv "XDG_CACHE_HOME") (string-append (getenv "HOME") "/.cache"))) (define-env-path %env-cache %cache-directory "/env") (define-env-path %env-layers %env-cache "/layers") (define-env-path %env-links %env-layers "/l") (define-env-path %env-by-name %env-cache "/by-name") (define-env-path %env-directory (getenv "HOME") "/env") (define (metadata-file name . path) (apply string-append (cons* %env-by-name "/" (symbol->string name) path))) (define (layer-file hash) (string-append %env-layers "/" hash)) (define (symlink-layer! hash) ;; TODO: check for collisions (symlink (string-append "../" hash) (string-append %env-links "/" (string-take hash 7)))) (define (ensure-environment-metadata-directory! name) (for-each mkdir-p (list %env-layers %env-links (metadata-file name) %env-directory (metadata-file name "/tags")))) ;; TODO Should we detect symlink loops here? (define (file->string file) (call-with-input-file file get-string-all)) (define (file->sexp file) (with-input-from-file file read)) (define-record-type (%make-environment name current root tags tree) environment? (name environment-name) (current environment-current) (root environment-root) (tags environment-tags) (tree environment-tree)) (define-record-type (make-overlay-mount lower upper work merged) overlay-mount? (lower overlay-mount-lower) (upper overlay-mount-upper) (work overlay-mount-work) (merged overlay-mount-merged)) (define (lookup-environment name) ;; NOTE will fail if .../by-name/name is a symlink (if (directory-exists? (metadata-file name)) (%make-environment name (file->string (metadata-file name "/tags/current")) (file->string (metadata-file name "/tags/root")) (map string->symbol (directory-files (metadata-file name "/tags"))) (file->sexp (metadata-file name "/tree"))) #f)) (define (environment-tag-value env tag) (file->string (metadata-file (environment-name env) "/tags/" (symbol->string tag)))) (define (hash-link v1 v2) (bytevector-hash (bytevector-append v1 v2) (hash-algorithm sha256))) (define (decode-hash-link-encode s1 s2) (bytevector->base32-string (apply hash-link (map base32-string->bytevector (list s1 s2))))) (define (decode-hash-link-encode/chain hashes) (match hashes (() (throw 'missformed-argument)) ((h) h) ((h h* ...) (fold (λ (e acc) (decode-hash-link-encode acc e)) h h*)))) (define (file-hash/overlay file) (define trim (let ((n (1+ (string-length file)))) (λ (f) (string-drop f n)))) (define (canonicalize-deleted l) (if (null? l) l (let ((bvs (map string->utf8 (sort (map trim l) stringbase32-string (file-hash* dir))) (loc (layer-file dirhash))) (copy-recursively dir loc) (symlink-layer! dirhash) (with-directory-excursion (metadata-file name) (with-output-to-file "./tags/root" (λ () (display dirhash))) (symlink "./root" "./tags/current") (symlink "./tags/current" "current") (with-output-to-file "tree" (λ () (write (list dirhash)))))) (lookup-environment name)))) (define* (add-layer env layer #:optional (onto 'current) (tag onto)) ;; TODO deduplicate with init-env (unless (file-exists? layer) (throw 'add-layer-directory-doesnt-exist!)) (let* ((name (environment-name env)) (dirhash (bytevector->base32-string (file-hash/overlay layer))) (metadata-dir (metadata-file (environment-name env))) (pin (environment-tag-value env onto)) (new-pin (decode-hash-link-encode pin dirhash)) (stack (environment-tag-layers env onto)) (loc (layer-file dirhash))) (define (grow tree pinned layer) ;; TODO abstract away (let ((atom? (compose not pair?))) (match tree (() (throw 'missformed-tree)) ((root) (if (equal? pinned root) (list root layer) (throw 'missformed-tree))) ((root _ ...) (cons root (let loop ((tree (cdr tree)) (hash (environment-root env))) (match tree (() '()) (((? atom? head)) (let ((h (decode-hash-link-encode hash head))) (if (equal? pinned h) (list head layer) (list head)))) (((? atom? head) tail ...) (let ((h (decode-hash-link-encode hash head))) (if (equal? pinned h) (list (list head (list layer) tail)) (cons head (loop tail h))))) ((((? atom? head) (? pair? branch1) (? pair? branch2) (? pair? branch3) ...)) (let* ((h (decode-hash-link-encode hash head))) (if (eq? pinned h) (list (cons head (cons (list layer) (cons* branch1 branch2 branch3)))) (list (cons head (map (λ (br) (loop br h)) (cons* branch1 branch2 branch3))))))) (else (throw 'missformed-tree))))))))) ;; TODO check if appendable (unless (file-exists? loc) (copy-recursively/overlay layer loc) ;; TODO: check for collisions (symlink-layer! dirhash)) (with-directory-excursion (metadata-file name) (let ((tags/tag (string-append "./tags/" (symbol->string tag)))) (when (and (file-exists? tags/tag) (symbolic-link? tags/tag)) (delete-file tags/tag)) (with-output-to-file tags/tag (λ () (display new-pin)))) (with-output-to-file "./tree" (λ () (write (grow (environment-tree env) pin dirhash))))) (lookup-environment name))) (define* (roll-back! env #:optional (tag 'current)) (let* ((stack (environment-tag-layers env tag)) (new-stack (match stack (() (throw 'empty-stack)) ((a) (throw 'first-generation-cant-roll-back)) ((a a* ...) (drop-right (cons a a*) 1))))) (with-directory-excursion (metadata-file (environment-name env)) (with-output-to-file "./tree" (λ () (write new-stack))) (with-output-to-file "./tags/current" (λ () (display (decode-hash-link-encode/chain new-stack))))) (values (lookup-environment (environment-name env)) (last stack)))) (define (remove-env-no-checks! name) "Remove environment brutally. No checks whether any layers are shared by other environments made." (let* ((metadata-dir (string-append %env-by-name "/" name)) (tree (with-input-from-file (string-append metadata-dir "/tree") read)) (layers (let loop ((tree tree) (layers '())) (match tree (() layers) (((a ...) tail ...) (append (loop a '()) (loop tail layers))) ((a tail ...) (loop tail (cons a layers))))))) (for-each (λ (l) (format (current-error-port) "Deleting layer: ~a\n" l) (delete-file-recursively (string-append %env-layers "/" l)) (format (current-error-port) "Cleaning up links for: ~a\n" l) (delete-file (string-append %env-links "/" (string-take l 7)))) layers) (format (current-error-port) "Deleting metadata: ~a\n" name) (delete-file-recursively metadata-dir))) (define (list-environments) (let loop ((names (map string->symbol (directory-files %env-by-name))) (envs '())) (match names (() envs) ((n n* ...) (let ((e (lookup-environment n))) (if e (loop n* (cons e envs)) (begin (format (current-error-port) "Warning: Clobbered by-name directory: ~a\n" n) (loop n* envs)))))))) (define (layers-in-use) (let loop ((envs (list-environments)) (used '())) (match envs (() used) ((e e* ...) (loop e* (fold (λ (t acc) (lset-union equal? acc (environment-tag-layers e t))) used (environment-tags e))))))) (define* (collect-garbage! #:optional dry-run) (with-directory-excursion %env-layers (for-each (λ (l) (if dry-run (format (current-error-port) "Would delete layer ~a\n" l) (begin (format (current-error-port) "Deleting layer ~a\n" l) (delete-file-recursively l) (delete-file (string-append "l/" (string-take l 7)))))) (lset-xor equal? (delete "l" (directory-files ".")) (layers-in-use))))) (define* (mount-environment env #:key (tag 'current) target) (let* ((name (environment-name env)) (metadata-dir (metadata-file name)) (tree (environment-tree env)) ;; TODO sanity check (current (environment-current env)) (layers-stack (environment-tag-layers env tag))) (when (or (not layers-stack) (null? layers-stack)) (throw 'no-layers-stack-found)) (let* ((dir (begin (mkdir-p "/tmp/genv") (mkdtemp "/tmp/genv/XXXXXX"))) (id (string-take-right dir 6)) (merged (let ((f (or target (string-append %env-directory "/" (symbol->string name))))) (if (directory-exists? f) f (begin (mkdir-p f) f)))) (upper (string-append dir "/upper")) (work (string-append dir "/work")) ;; TODO abstract away (layers-links (map (λ (l) (string-take l 7)) layers-stack))) (mkdir upper) (mkdir work) (let ((dirln (string-append "/tmp/genv/" (symbol->string name)))) (when (file-exists? dirln) (delete-file dirln)) (symlink dir dirln) (let loop ((links layers-links) (n 0)) (unless (nil? links) (symlink (car links) (string-append dir "/layer" (number->string n))) (loop (cdr links) (1+ n))))) (with-directory-excursion %env-links ;; TODO report missing links? or global sanity check. ;; TODO handle errors (match (apply system* (overlayfs-command (reverse layers-links) upper work merged)) (0 (values (make-overlay-mount layers-links upper work merged) 0)) (err (values #f err))))))) (define* (unmount-environment-maybe env #:optional target) (let* ((name (symbol->string (environment-name env))) (dir (or target (string-append %env-directory "/" name))) (tmpdirln (string-append "/tmp/genv/" name))) (if (file-exists? (string-append tmpdirln "/lock")) #f (begin (when (file-exists? dir) ;; TODO check if actually mounted (system* "umount" dir)) (when (file-exists? tmpdirln) (delete-file tmpdirln)))))) (define (fuse-overlayfs-mounts) (filter (λ (m) (equal? "fuse.fuse-overlayfs" (mount-type m) )) (mounts))) (define (fuse-overlayfs-mount-points) (map mount-point (fuse-overlayfs-mounts))) (define (environment-mounted? env) (if (member (string-append %env-directory "/" (symbol->string (environment-name env))) (fuse-overlayfs-mount-points)) #t #f)) (define* (environment->overlay-mount-maybe env #:optional target) (let* ((name (symbol->string (environment-name env))) (dir (readlink (string-append "/tmp/genv/" name)))) (if (file-exists? dir) (let ((upper (string-append dir "/upper")) (work (string-append dir "/work")) (merged (or target (string-append %env-directory "/" name))) (layers-links (map (λ (s) (readlink (string-append dir "/" s))) (filter (λ (s) (string-prefix? "layer" s)) (directory-files dir))))) (make-overlay-mount layers-links upper work merged)) #f))) (define* (call-with-env env proc #:optional save? clean?) (match (mount-environment env) (($ lower upper work merged) (let ((res (proc merged))) (sync) (when save? (when (not (equal? 0 (system* "umount" merged))) (throw 'umount-error)) (add-layer env upper)) (when clean? (when file-exists? upper (delete-file-recursively upper)) (delete-file-recursively merged) (delete-file-recursively work)) res)) (#f (throw 'mount-failure))))