zilch/core/src/file.sld

222 lines
11 KiB
Text
Raw Normal View History

2024-10-03 23:57:22 +00:00
(define-library (zilch file)
(import
(scheme base) (scheme case-lambda)
(zilch magic) (zilch nix binproto) (zilch nix daemon) (zilch nix drv) (zilch zexpr)
(chicken base) (chicken format)
(srfi 128) (srfi 132) (srfi 146) (srfi 151))
(export zfile zsymlink zdir
zfile->store)
(begin
(define-record-type <z-file>
(make-z-file contents executable cache)
z-file?
(contents z-file-contents)
(executable z-file-executable)
(cache z-file-cache z-file-set-cache))
(define-record-printer (<z-file> file out)
(if (z-file-executable file)
(fprintf out "#<z-file (executable)>")
(fprintf out "#<z-file>")))
(define-record-type <z-directory>
(make-z-directory contents cache)
z-directory?
(contents z-directory-contents)
(cache z-directory-cache z-directory-set-cache))
(define-record-printer (<z-directory> dir out)
(fprintf out "#<z-directory")
(for-each (lambda (kv) (fprintf out " ~S -> ~S" (car kv) (cdr kv))) (z-directory-contents dir))
(fprintf out ">"))
(define-record-type <z-symlink>
(make-z-symlink target cache)
z-symlink?
(target z-symlink-target)
(cache z-symlink-cache z-symlink-set-cache))
(define-record-printer (<z-symlink> symlink out)
(fprintf out "#<z-symlink -> ~S>" (z-symlink-target symlink)))
(define (env-pair<? l r) (string<? (car l) (car r)))
;; Create a `<z-file>` object with given contents and optional `executable` flag.
;; The contents may either be a string or a `<zexp>`.
(define zfile
(case-lambda
((contents) (make-z-file contents #f #f))
((contents executable) (make-z-file contents executable #f))))
;; Create a `<z-symlink>` record. The target may be any string, *or* a `<zexp>` containing one.
(define (zsymlink target) (make-z-symlink target #f))
;; Create a `<z-directory>` record. The contents is an alist of file name -> zfile/zsymlink/zdir.
;; For simplicity, one can also write e.g. `(zdir "key" value "key2" value)`.
(define zdir
(case-lambda
((contents) (make-z-directory (list-sort env-pair<? contents) #f))
(rest (do
((out '()))
((eq? rest '()) (make-z-directory (list-sort env-pair<? out) #f))
(set! out (cons (cons (car rest) (cadr rest)) out))
(set! rest (cddr rest))))))
; Here is where the weirdness starts.
; To make it possible to build store paths that depend on non-fixed-output dependencies,
; with as little system-specific dependencies as possible, this is implemented in a bit of a weird way.
; To do this, it uses the builtin:unpack-channel builder. This builder unpacks a tarball or zip file
; (or other formats, in some older versions of CppNix and Lix) and moves the one top-level file to a known name.
; To do this, we implement a tiny ustar serializer.
; First, build the baseline header used for every item in the tarball. This has precalculated checksums, so is cheap to modify and rechecksum.
(define baseline-ustar-header (make-bytevector 512 0))
; TODO(puck): should we default to 0 instead?
(do ((i 100 (+ i 1))) ((= i 157) #f) (bytevector-u8-set! baseline-ustar-header i 32))
(for-each (lambda (i) (bytevector-u8-set! baseline-ustar-header i 0)) '(108 116 136 329 337))
; TODO(puck): are these needed still?
(for-each (lambda (i) (bytevector-u8-set! baseline-ustar-header i (char->integer #\a))) '(0 157))
(bytevector-copy! baseline-ustar-header 257 (string->utf8 "ustar\x00;00"))
; We subtract 32*21 here to exclude the mode, size, and typeflag fields from the baseline checksum.
(define baseline-ustar-header-checksum
(do ((i 0 (+ i 1)) (csum 0 csum))
((= i 512) (- csum (* 32 21)))
(set! csum (+ csum (bytevector-u8-ref baseline-ustar-header i)))))
;; Write a tar header with specified mode, content length, and type byte.
;; For simplicity, all paths and link paths must be defined in PAX attributes.
(define (make-ustar-header mode size type)
(define output (bytevector-copy baseline-ustar-header))
(define octal-mode (number->string mode 8))
(bytevector-copy! output 100 (string->utf8 octal-mode))
(define octal-size (number->string size 8))
(bytevector-copy! output 124 (string->utf8 octal-size))
(bytevector-u8-set! output 156 (char->integer type))
(define csum (+ baseline-ustar-header-checksum (char->integer type)))
(do ((i 100 (+ i 1))) ((= i 108) #f) (set! csum (+ csum (bytevector-u8-ref output i))))
(do ((i 124 (+ i 1))) ((= i 136) #f) (set! csum (+ csum (bytevector-u8-ref output i))))
(define octal-csum (number->string csum 8))
(bytevector-copy! output 148 (string->utf8 octal-csum))
output)
;; PAX extended attributes contain the length of the entire line, including the length bytes.
;; Calculate the length of " {key}={value}\n" and the length of that length in decimal.
;; If adding the length of the length would make the length overflow, add one more byte; then
;; return the full serialized key-value pair as a string.
(define make-pax-extended-header
(case-lambda
((key value) (make-pax-extended-header key value (string-length value)))
((key value value-length)
(define kv-length (+ (string-length key) value-length 3))
(define length-length (string-length (number->string kv-length)))
(unless (eqv? length-length (string-length (number->string (+ length-length kv-length)))) (set! length-length (+ 1 length-length)))
(string-append (number->string (+ length-length kv-length)) " " key "=" value "\n"))))
(define padding-block (make-bytevector 512 0))
(define (write-padding len)
(define rem (bitwise-and len 511))
(unless (= rem 0)
(write-bytevector padding-block (current-output-port) 0 (- 512 rem))))
(define (unwrap-zexp-to-placeholder zexp)
(define contents-zexp (zexp-unwrap zexp))
(define contents (zexp-evaluation-value contents-zexp))
(zexp-context-register-items (zexp-evaluation-drvs contents-zexp) (zexp-evaluation-srcs contents-zexp))
(when (string? contents)
(set! contents (string->utf8 contents)))
(define placeholder-mapping (mapping (make-default-comparator)))
(define has-mapping #f)
(for-each
(lambda (drv)
(for-each
(lambda (output)
(define output-obj (cdr (assoc output (derivation-outputs (car drv)))))
(when (derivation-output-placeholder? output-obj)
(set! placeholder-mapping (mapping-set! placeholder-mapping (derivation-output-path output-obj) (derivation-output-path-length (car drv) output)))
(set! has-mapping #t)))
(cdr drv)))
(zexp-evaluation-drvs contents-zexp))
(define contents-length (bytevector-length contents))
(define sliced (make-bytevector 53))
(when has-mapping
(do ((i 0 (+ 1 i)) (ref #f #f))
((> i (- (bytevector-length contents) 53)) #f)
(when (= (bytevector-u8-ref contents i) #x2F)
(bytevector-copy! sliced 0 contents i (+ i 53))
(set! ref (mapping-ref/default placeholder-mapping (utf8->string sliced) #f))
(when ref (set! contents-length (+ (- contents-length 53) ref))))))
(values contents contents-length))
(define (write-pax-directory path)
(define header (string->utf8 (make-pax-extended-header "path" path)))
(write-bytevector (make-ustar-header 0 (bytevector-length header) #\x))
(write-bytevector header)
(write-padding (bytevector-length header))
(write-bytevector (make-ustar-header #o777 0 #\5)))
(define (write-pax-file path executable contents-zexp)
(define-values (contents contents-length) (unwrap-zexp-to-placeholder contents-zexp))
(define header (string->utf8 (make-pax-extended-header "path" path)))
(write-bytevector (make-ustar-header 0 (bytevector-length header) #\x))
(write-bytevector header)
(write-padding (bytevector-length header))
(write-bytevector (make-ustar-header (if executable #o555 #o444) contents-length #\0))
(if (bytevector? contents) (write-bytevector contents) (write-string contents))
(write-padding contents-length))
(define (write-pax-symlink path linkpath-zexp)
(define-values (linkpath linkpath-length) (unwrap-zexp-to-placeholder linkpath-zexp))
(define header (string->utf8 (string-append (make-pax-extended-header "path" path) (make-pax-extended-header "linkpath" (utf8->string linkpath) linkpath-length))))
(define total-len (+ (bytevector-length header) (- linkpath-length (bytevector-length linkpath))))
(write-bytevector (make-ustar-header 0 total-len #\x))
(write-bytevector header)
(write-padding total-len)
(write-bytevector (make-ustar-header #o777 0 #\2)))
;; Serialize the specified structure as a ustar-style (with pax extensions) tape archive to the standard output port. The filename is specified by `name`.
(define (serialize-as-tar f name)
(cond
((z-file? f) (write-pax-file name (z-file-executable f) (z-file-contents f)))
((z-directory? f)
(write-pax-directory name)
(map (lambda (entry) (serialize-as-tar (cdr entry) (string-append name "/" (car entry)))) (z-directory-contents f)))
((z-symlink? f) (write-pax-symlink name (z-symlink-target f)))))
;; Serialize a file-like (`zfile`, `zsymlink`, `zdir`) to a `<store-path>`.
;; This function should not depend on the system of the builder.
;;
2024-10-03 23:57:22 +00:00
;; TODO(puck): due to limitations, whatever you pass in ends up at `<store-path>/-` instead.
(define (zfile->store val)
(define cached
(cond
((z-file? val) (z-file-cache val))
((z-directory? val) (z-directory-cache val))
((z-symlink? val) (z-symlink-cache val))))
(unless cached
(set! cached
(cdar
(store-path-for-ca-drv* "zilchfile" "builtin"
(zexp ("builtin:unpack-channel"))
(zexp (("src" . ".attr-1s42g1c76fxb77skzq0b4wdhcrg8jmzb54czmxvh1qm7psgsbcni")
("contents" . (zexp-unquote (call-with-port (open-output-bytevector) (lambda (port) (parameterize ((current-output-port port)) (serialize-as-tar val "-")) (get-output-bytevector port)))))
("passAsFile" . "contents")
("allowSubstitutes" . "")
("channelName" . "-")))
'("out")))))
2024-10-03 23:57:22 +00:00
(cond
((z-file? val) (z-file-set-cache val cached))
((z-directory? val) (z-directory-set-cache val cached))
((z-symlink? val) (z-symlink-set-cache val cached)))
cached)
(zexp-add-unquote-handler
(lambda (val)
(if (or (z-file? val) (z-symlink? val) (z-directory? val))
(string-append (zexp-unquote (zfile->store val)) "/-")
#f)))))