diff options
-rw-r--r-- | ravanan/command-line-tool.scm | 206 | ||||
-rw-r--r-- | ravanan/job-state.scm | 15 | ||||
-rw-r--r-- | ravanan/store.scm | 64 | ||||
-rw-r--r-- | ravanan/work/command-line-tool.scm | 166 | ||||
-rw-r--r-- | ravanan/workflow.scm | 22 |
5 files changed, 314 insertions, 159 deletions
diff --git a/ravanan/command-line-tool.scm b/ravanan/command-line-tool.scm index 0103441..9e96270 100644 --- a/ravanan/command-line-tool.scm +++ b/ravanan/command-line-tool.scm @@ -95,15 +95,6 @@ (secondary-files formal-output-secondary-files) (binding formal-output-binding)) -(define-immutable-record-type <command-line-binding> - (command-line-binding position prefix type value item-separator) - command-line-binding? - (position command-line-binding-position) - (prefix command-line-binding-prefix) - (type command-line-binding-type) - (value command-line-binding-value) - (item-separator command-line-binding-item-separator)) - (define-immutable-record-type <output-binding> (output-binding glob load-contents? load-listing? output-eval) output-binding? @@ -292,47 +283,6 @@ G-expressions are inserted." (< (command-line-binding-position binding1) (command-line-binding-position binding2)))))) -(define (command-line-binding->args binding) - "Return a list of arguments for @var{binding}. The returned list may -contain strings or G-expressions. The G-expressions may reference an -@code{inputs-directory} variable that must be defined in the context -in which the G-expressions are inserted." - (let ((prefix (command-line-binding-prefix binding)) - (type (command-line-binding-type binding)) - (value (command-line-binding-value binding))) - (cond - ((eq? type 'boolean) - (if value - ;; TODO: Error out if boolean input has no prefix? - (maybe->list prefix) - (list))) - ((eq? type 'null) (list)) - ((array-type? type) - (match value - ;; Empty arrays should be noops. - (() (list)) - (_ - (let ((args (append-map command-line-binding->args - value))) - (append (maybe->list prefix) - (from-maybe - (maybe-let* ((item-separator (command-line-binding-item-separator binding))) - (just (list #~(string-join (list #$@args) - #$item-separator)))) - args)))))) - (else - (append (maybe->list prefix) - (list (case type - ((string) - value) - ((int float) - #~(number->string #$value)) - ((File) - #~(assoc-ref* #$value "path")) - (else - (user-error "Invalid formal input type ~a" - type))))))))) - (define* (build-gexp-script name exp #:optional guix-daemon-socket) "Build script named @var{name} using G-expression @var{exp}. @@ -358,7 +308,7 @@ state-monadic job state object. @var{guix-daemon-socket} are the same as in @code{run-workflow} from @code{(ravanan workflow)}." (let* ((script - (build-command-line-tool-script name manifest-file channels cwl inputs + (build-command-line-tool-script name manifest-file channels cwl scratch store batch-system guix-daemon-socket)) (requirements (inherit-requirements (or (assoc-ref cwl "requirements") @@ -377,10 +327,10 @@ state-monadic job state object. <> `(("inputs" . ,inputs))))) 1)) - (store-files-directory (script->store-files-directory script store)) - (store-data-file (script->store-data-file script store)) - (stdout-file (script->store-stdout-file script store)) - (stderr-file (script->store-stderr-file script store))) + (store-files-directory (step-store-files-directory script inputs store)) + (store-data-file (step-store-data-file script inputs store)) + (stdout-file (step-store-stdout-file script inputs store)) + (stderr-file (step-store-stderr-file script inputs store))) (if (file-exists? store-data-file) ;; Return a dummy success state object if script has already ;; been run successfully. @@ -389,7 +339,7 @@ state-monadic job state object. (format (current-error-port) "~a previously run; retrieving result from store~%" script) - (single-machine-job-state script #t))) + (single-machine-job-state script inputs #t))) ;; Run script if it has not already been run. (begin ;; Delete output files directory if an incomplete one exists @@ -402,7 +352,8 @@ state-monadic job state object. (delete-file-recursively store-files-directory)) (mkdir store-files-directory) (let ((environment - `(("WORKFLOW_OUTPUT_DIRECTORY" . ,store-files-directory) + `(("WORKFLOW_INPUTS" . ,(scm->json-string inputs)) + ("WORKFLOW_OUTPUT_DIRECTORY" . ,store-files-directory) ("WORKFLOW_OUTPUT_DATA_FILE" . ,store-data-file)))) (cond ((eq? batch-system 'single-machine) @@ -410,7 +361,7 @@ state-monadic job state object. stdout-file stderr-file script))) - (state-return (single-machine-job-state script success?)))) + (state-return (single-machine-job-state script inputs success?)))) ((slurm-api-batch-system? batch-system) (state-let* ((job-id (slurm:submit-job environment @@ -427,14 +378,14 @@ state-monadic job state object. "~a submitted as job ID ~a~%" script job-id) - (state-return (slurm-job-state script job-id)))) + (state-return (slurm-job-state script inputs job-id)))) (else (assertion-violation batch-system "Invalid batch system")))))))) -(define (capture-command-line-tool-output script store) +(define (capture-command-line-tool-output script inputs store) "Capture and return output of @code{CommandLineTool} class workflow that ran -@var{script}. @var{store} is the path to the ravanan store." - (let* ((store-data-file (script->store-data-file script store)) +@var{script} with @var{inputs}. @var{store} is the path to the ravanan store." + (let* ((store-data-file (step-store-data-file script inputs store)) (output-json (call-with-input-file store-data-file json->scm))) ;; Recursively rewrite file paths in output JSON. @@ -451,7 +402,7 @@ state-monadic job state object. (string=? (assoc-ref tree "class") "File")) (let* ((store-files-directory - (script->store-files-directory script store)) + (step-store-files-directory script inputs store)) (path (expand-file-name (relative-file-name (assoc-ref tree "path") store-files-directory) @@ -474,60 +425,6 @@ state-monadic job state object. (call-with-input-file store-data-file json->scm))) -(define (copy-input-files-gexp inputs) - "Return a G-expression that copies @code{File} type inputs (along with secondary -files) from @var{inputs} into @code{inputs-directory} and return a new -association list with updated @code{location} and @code{path} fields. - -The returned G-expression will reference an @code{inputs-directory} variable." - (define (copy-input-files input) - (cond - ((vector? input) - #~,(list->vector - `#$(map copy-input-files - (vector->list input)))) - ((eq? (object-type input) - 'File) - #~,(let ((path-in-inputs-directory - ;; Input files may have the same filename. So, we take the - ;; additional precaution of copying input files into their own - ;; hash-prefixed subdirectories, just like they are in the - ;; ravanan store. - (expand-file-name #$(file-name-join - (take-right (file-name-split - (assoc-ref input "path")) - 2)) - inputs-directory))) - (make-directories (file-dirname path-in-inputs-directory)) - (copy-file #$(assoc-ref input "path") - path-in-inputs-directory) - (maybe-assoc-set '#$input - (cons "location" - (just path-in-inputs-directory)) - (cons "path" - (just path-in-inputs-directory)) - (cons "basename" - (just (basename path-in-inputs-directory))) - (cons "nameroot" - (just (file-name-stem path-in-inputs-directory))) - (cons "nameext" - (just (file-name-extension path-in-inputs-directory))) - (cons "secondaryFiles" - #$(from-maybe - (maybe-let* ((secondary-files - (maybe-assoc-ref (just input) "secondaryFiles"))) - (just #~(just (list->vector - `#$(vector-map->list copy-input-files - secondary-files))))) - #~%nothing))))) - (else input))) - - #~(list->dotted-list - `#$(map (match-lambda - ((id . input) - (list id (copy-input-files input)))) - inputs))) - (define (find-requirement requirements class) "Find requirement of @var{class} among @var{requirements} and return a maybe-monadic value." @@ -666,12 +563,12 @@ by @var{guix-daemon-socket}." #:allow-collisions? #t) guix-daemon-socket))))) -(define (build-command-line-tool-script name manifest-file channels cwl inputs +(define (build-command-line-tool-script name manifest-file channels cwl scratch store batch-system guix-daemon-socket) "Build and return script to run @code{CommandLineTool} class workflow @var{cwl} -named @var{name} with @var{inputs} using tools from Guix manifest in -@var{manifest-file} and on @var{batch-system}. +named @var{name} using tools from Guix manifest in @var{manifest-file} and on +@var{batch-system}. @var{channels}, @var{scratch}, @var{store} and @var{guix-daemon-socket} are the same as in @code{run-workflow} from @code{(ravanan workflow)}." @@ -729,12 +626,21 @@ same as in @code{run-workflow} from @code{(ravanan workflow)}." (list 'File 'Directory)) (error #f "glob output binding not specified")))) + (define (coerce-argument argument) + (assoc-set argument + (cons "valueFrom" + (coerce-expression (assoc-ref* argument "valueFrom"))))) + (define run-command-gexp - #~(run-command (list #$@(append-map (lambda (arg) - (if (command-line-binding? arg) - (command-line-binding->args arg) - (list arg))) - (build-command cwl inputs))) + #~(run-command (append-map (lambda (arg) + (if (command-line-binding? arg) + (command-line-binding->args arg) + (list arg))) + (build-command #$(assoc-ref cwl "baseCommand") + #$(vector-map coerce-argument + (assoc-ref cwl "arguments")) + #$(assoc-ref cwl "inputs") + inputs)) #$(coerce-expression (assoc-ref cwl "stdin")) #$stdout-filename '#$(from-maybe @@ -828,7 +734,6 @@ same as in @code{run-workflow} from @code{(ravanan workflow)}." (and (not (coerce-type (assoc-ref* work-reuse "enableReuse") 'boolean)) (warning "Ignoring disable of WorkReuse. ravanan's strong caching using Guix makes it unnecessary.")))) - ;; Copy input files and update corresponding input objects. (build-gexp-script name (let* ((requirements (inherit-requirements (or (assoc-ref cwl "requirements") #()) @@ -875,6 +780,45 @@ same as in @code{run-workflow} from @code{(ravanan workflow)}." (guix search-paths) (json)) + (define (copy-input-files input inputs-directory) + ;; Copy input files and update corresponding input objects. + (cond + ((vector? input) + (vector-map copy-input-files + input)) + ((eq? (object-type input) + 'File) + (let ((path-in-inputs-directory + ;; Input files may have the same filename. So, we take + ;; the additional precaution of copying input files + ;; into their own hash-prefixed subdirectories, just + ;; like they are in the ravanan store. + (expand-file-name (file-name-join + (take-right (file-name-split + (assoc-ref input "path")) + 2)) + inputs-directory))) + (make-directories (file-dirname path-in-inputs-directory)) + (copy-file (assoc-ref input "path") + path-in-inputs-directory) + (maybe-assoc-set input + (cons "location" + (just path-in-inputs-directory)) + (cons "path" + (just path-in-inputs-directory)) + (cons "basename" + (just (basename path-in-inputs-directory))) + (cons "nameroot" + (just (file-name-stem path-in-inputs-directory))) + (cons "nameext" + (just (file-name-extension path-in-inputs-directory))) + (cons "secondaryFiles" + (maybe-let* ((secondary-files + (maybe-assoc-ref (just input) "secondaryFiles"))) + (just (vector-map copy-input-files + secondary-files))))))) + (else input))) + (define (copy-file-value value directory) ;; Copy file represented by value to directory and return the ;; new File value. @@ -1056,10 +1000,12 @@ directory of the workflow." (call-with-temporary-directory (lambda (inputs-directory) - ;; We need to canonicalize JSON trees before inserting them - ;; into G-expressions. If we don't, we would have degenerate - ;; G-expressions that produce exactly the same result. - (let ((inputs #$(copy-input-files-gexp (canonicalize-json inputs))) + (let ((inputs (map (match-lambda + ((id . input) + (cons id + (copy-input-files input inputs-directory)))) + (json-string->scm + (getenv "WORKFLOW_INPUTS")))) (runtime `(("cores" . ,#$(cores batch-system))))) ;; Set environment defined by workflow. diff --git a/ravanan/job-state.scm b/ravanan/job-state.scm index a769698..2894618 100644 --- a/ravanan/job-state.scm +++ b/ravanan/job-state.scm @@ -34,18 +34,21 @@ slurm-job-state job-state-script + job-state-inputs job-state-status)) (define-immutable-record-type <single-machine-job-state> - (single-machine-job-state script success?) + (single-machine-job-state script inputs success?) single-machine-job-state? (script single-machine-job-state-script) + (inputs single-machine-job-state-inputs) (success? single-machine-job-state-success?)) (define-immutable-record-type <slurm-job-state> - (slurm-job-state script job-id) + (slurm-job-state script inputs job-id) slurm-job-state? (script slurm-job-state-script) + (inputs slurm-job-state-inputs) (job-id slurm-job-state-job-id)) (define (job-state-script state) @@ -56,6 +59,14 @@ slurm-job-state-script)) state)) +(define (job-state-inputs state) + ((cond + ((single-machine-job-state? state) + single-machine-job-state-inputs) + ((slurm-job-state? state) + slurm-job-state-inputs)) + state)) + (define* (job-state-status state batch-system) "Return current status of job with @var{state} on @var{batch-system}. The status is one of the symbols @code{completed}, @code{failed} or @code{pending} diff --git a/ravanan/store.scm b/ravanan/store.scm index f9d1939..9bd5bcc 100644 --- a/ravanan/store.scm +++ b/ravanan/store.scm @@ -18,9 +18,12 @@ (define-module (ravanan store) #:use-module (srfi srfi-26) + #:use-module (srfi srfi-71) #:use-module (ice-9 filesystem) + #:use-module (gcrypt hash) #:use-module (guix base16) #:use-module (guix base32) + #:use-module (guix build utils) #:use-module (ravanan work command-line-tool) #:use-module (ravanan work monads) #:use-module (ravanan work vectors) @@ -29,10 +32,10 @@ %store-logs-directory make-store - script->store-files-directory - script->store-data-file - script->store-stdout-file - script->store-stderr-file + step-store-files-directory + step-store-data-file + step-store-stdout-file + step-store-stderr-file intern-file)) (define %store-files-directory @@ -58,29 +61,56 @@ already exists, do nothing." %store-data-directory %store-logs-directory)))) -(define (script->store-files-directory script store) - "Return the store files directory in @var{store} corresponding to @var{script} -path." +(define (sha1-hash-sexp tree) + (bytevector->base32-string + (let ((port get-hash (open-hash-port (hash-algorithm sha1)))) + ;; tree should probably be canonicalized using canonical S-expressions or + ;; similar. But, it doesn't matter much for our purposes. write already + ;; canonicalizes in a way. In the unlikely case of a problem, the worst + ;; that can happen is that we recompute all steps of the workflow. + (write tree port) + (close port) + (get-hash)))) + +(define (step-store-basename script inputs) + "Return the basename in the store for files of CWL step with @var{script} and +@var{inputs}." + (string-append (sha1-hash-sexp (cons script inputs)) + "-" + (strip-store-file-name script))) + +(define (step-store-files-directory script inputs store) + "Return the @var{store} files directory for CWL step with @var{script} and +@var{inputs}." (expand-file-name (file-name-join* %store-files-directory - (basename script)) + (step-store-basename script inputs)) store)) -(define (script->store-data-file script store) - "Return the store data file in @var{store} corresponding to @var{script} path." +(define (step-store-data-file script inputs store) + "Return the @var{store} data file for CWL step with @var{script} and +@var{inputs}." (expand-file-name (file-name-join* %store-data-directory - (string-append (basename script) ".json")) + (string-append + (step-store-basename script inputs) + ".json")) store)) -(define (script->store-stdout-file script store) - "Return the store stdout file in @var{store} corresponding to @var{script} path." +(define (step-store-stdout-file script inputs store) + "Return the @var{store} stdout file for CWL step with @var{script} and +@var{inputs}." (expand-file-name (file-name-join* %store-logs-directory - (string-append (basename script) ".stdout")) + (string-append + (step-store-basename script inputs) + ".stdout")) store)) -(define (script->store-stderr-file script store) - "Return the store stderr file in @var{store} corresponding to @var{script} path." +(define (step-store-stderr-file script inputs store) + "Return the @var{store} stderr file for CWL step with @var{script} and +@var{inputs}." (expand-file-name (file-name-join* %store-logs-directory - (string-append (basename script) ".stderr")) + (string-append + (step-store-basename script inputs) + ".stderr")) store)) (define (same-filesystem? path1 path2) diff --git a/ravanan/work/command-line-tool.scm b/ravanan/work/command-line-tool.scm index 0ba735a..3c9934f 100644 --- a/ravanan/work/command-line-tool.scm +++ b/ravanan/work/command-line-tool.scm @@ -19,6 +19,7 @@ (define-module (ravanan work command-line-tool) #:use-module (rnrs exceptions) #:use-module (srfi srfi-1) + #:use-module (srfi srfi-9 gnu) #:use-module (srfi srfi-26) #:use-module (ice-9 filesystem) #:use-module (ice-9 format) @@ -29,6 +30,7 @@ #:use-module (json) #:use-module (ravanan work monads) #:use-module (ravanan work types) + #:use-module (ravanan work ui) #:use-module (ravanan work utils) #:use-module (ravanan work vectors) #:export (value->string @@ -44,7 +46,17 @@ location->path canonicalize-file-value secondary-path - evaluate-javascript)) + evaluate-javascript + + command-line-binding + command-line-binding? + command-line-binding-position + command-line-binding-prefix + command-line-binding-type + command-line-binding-value + command-line-binding-item-separator + command-line-binding->args + build-command)) (define (value->string x) "Convert value @var{x} to a string." @@ -266,3 +278,155 @@ actually paths." (format #f "--eval=~a console.log(\"%j\", ~a)" preamble expression)) json->scm))) + +(define-immutable-record-type <command-line-binding> + (command-line-binding position prefix type value item-separator) + command-line-binding? + (position command-line-binding-position) + (prefix command-line-binding-prefix) + (type command-line-binding-type) + (value command-line-binding-value) + (item-separator command-line-binding-item-separator)) + +(define (command-line-binding->args binding) + "Return a list of arguments for @var{binding}. The returned list may +contain strings or G-expressions. The G-expressions may reference an +@code{inputs-directory} variable that must be defined in the context in which +the G-expressions are inserted." + (let ((prefix (command-line-binding-prefix binding)) + (type (command-line-binding-type binding)) + (value (command-line-binding-value binding))) + (cond + ((eq? type 'boolean) + (if value + ;; TODO: Error out if boolean input has no prefix? + (maybe->list prefix) + (list))) + ((eq? type 'null) (list)) + ((array-type? type) + (match value + ;; Empty arrays should be noops. + (() (list)) + (_ + (let ((args (append-map command-line-binding->args + value))) + (append (maybe->list prefix) + (from-maybe + (maybe-let* ((item-separator (command-line-binding-item-separator binding))) + (just (list (string-join args item-separator)))) + args)))))) + (else + (append (maybe->list prefix) + (list (case type + ((string) + value) + ((int float) + (number->string value)) + ((File) + (assoc-ref* value "path")) + (else + (user-error "Invalid formal input type ~a" + type))))))))) + +(define (build-command base-command arguments formal-inputs inputs) + "Return a list of @code{<command-line-binding>} objects for a +@code{CommandLineTool} class workflow with @var{base-command}, @var{arguments}, +@var{formal-inputs} and @var{inputs}. The @code{value} field of the returned +@code{<command-line-binding>} objects may be strings or G-expressions. The +G-expressions may reference @var{inputs} and @var{runtime} variables that must +be defined in the context in which the G-expressions are inserted." + (define (argument->command-line-binding i argument) + (command-line-binding (cond + ((assoc-ref argument "position") + => string->number) + (else i)) + (maybe-assoc-ref (just argument) "prefix") + 'string + (value->string (assoc-ref* argument "valueFrom")) + %nothing)) + + (define (collect-bindings ids+inputs+types+bindings) + (append-map id+input+type-tree+binding->command-line-binding + ids+inputs+types+bindings)) + + (define id+input+type-tree+binding->command-line-binding + (match-lambda + ;; We stretch the idea of an input id, by making it an address that + ;; identifies the exact location of a value in a tree that possibly + ;; contains array types. For example, '("foo") identifies the input "foo"; + ;; '("foo" 1) identifies the 1th element of the array input "foo"; '("foo" + ;; 37 1) identifies the 1th element of the 37th element of the array input + ;; "foo"; etc. + ((id input type-tree binding) + ;; Check type. + (let* ((type (formal-parameter-type type-tree)) + (matched-type (match-type input type))) + (unless matched-type + (error input "Type mismatch" input type)) + (let ((position + (from-maybe + (maybe-let* ((position (maybe-assoc-ref binding "position"))) + (just (string->number position))) + ;; FIXME: Why a default value of 0? + 0)) + (prefix (maybe-assoc-ref binding "prefix"))) + (cond + ;; Recurse over array types. + ;; TODO: Implement record and enum types. + ((array-type? matched-type) + (list (command-line-binding + position + prefix + matched-type + (append-map (lambda (i input) + (id+input+type-tree+binding->command-line-binding + (list (append id (list i)) + input + (assoc-ref type-tree "items") + (maybe-assoc-ref (just type-tree) + "inputBinding")))) + (iota (vector-length input)) + (vector->list input)) + (maybe-assoc-ref binding "itemSeparator")))) + (else + (list (command-line-binding position + prefix + matched-type + (apply json-ref inputs id) + %nothing))))))))) + + ;; For details of this algorithm, see ยง4.1 Input binding of the CWL + ;; 1.2 CommandLineTool specification: + ;; https://www.commonwl.org/v1.2/CommandLineTool.html#Input_binding + (append + ;; Insert elements from baseCommand. + (vector->list (or base-command + (vector))) + (sort + (append + ;; Collect CommandLineBinding objects from arguments; assign a sorting key. + (vector->list + (vector-map-indexed argument->command-line-binding + (or arguments + #()))) + ;; Collect CommandLineBinding objects from the inputs schema; assign a + ;; sorting key. + (collect-bindings + (filter-map (lambda (formal-input) + ;; Exclude formal inputs without an inputBinding. + (and (assoc "inputBinding" formal-input) + (let ((id (assoc-ref formal-input "id"))) + (list (list id) + (or (assoc-ref inputs id) + (assoc-ref formal-input "default") + 'null) + (or (assoc-ref formal-input "type") + (user-error "Type of input ~a not specified" + id)) + (maybe-assoc-ref (just formal-input) + "inputBinding"))))) + (vector->list formal-inputs)))) + ;; Sort elements using the assigned sorting keys. + (lambda (binding1 binding2) + (< (command-line-binding-position binding1) + (command-line-binding-position binding2)))))) diff --git a/ravanan/workflow.scm b/ravanan/workflow.scm index b8fff80..ee01942 100644 --- a/ravanan/workflow.scm +++ b/ravanan/workflow.scm @@ -59,7 +59,8 @@ (define-condition-type &job-failure &error job-failure job-failure? - (script job-failure-script)) + (script job-failure-script) + (inputs job-failure-inputs)) (define-immutable-record-type <scheduler-proc> (scheduler-proc name cwl scatter scatter-method) @@ -341,7 +342,8 @@ state-monadic @code{<state+status>} object. The status is one of the symbols (case status ((failed) (raise-exception (job-failure - (job-state-script job-state)))) + (job-state-script job-state) + (job-state-inputs job-state)))) (else => identity))))))) ;; Poll sub-workflow state. We do not need to check the status here since ;; job failures only occur at the level of a CommandLineTool. @@ -414,16 +416,17 @@ is the class of the workflow." head-output)))))) (else ;; Log progress and return captured output. - (let ((script (job-state-script (command-line-tool-state-job-state state)))) + (let ((script (job-state-script (command-line-tool-state-job-state state))) + (inputs (job-state-inputs (command-line-tool-state-job-state state)))) (state-return (begin (format (current-error-port) "~a completed; logs at ~a and ~a~%" script - (script->store-stdout-file script store) - (script->store-stderr-file script store)) + (step-store-stdout-file script inputs store) + (step-store-stderr-file script inputs store)) (filter-outputs "CommandLineTool" - (capture-command-line-tool-output script store) + (capture-command-line-tool-output script inputs store) (command-line-tool-state-formal-outputs state)))))))) (scheduler schedule poll capture-output)) @@ -537,12 +540,13 @@ area need not be shared. @var{store} is the path to the shared ravanan store. @var{guix-daemon-socket} is the Guix daemon socket to connect to." (guard (c ((job-failure? c) - (let ((script (job-failure-script c))) + (let ((script (job-failure-script c)) + (inputs (job-failure-inputs c))) (user-error "~a failed; logs at ~a and ~a~%" script - (script->store-stdout-file script store) - (script->store-stderr-file script store))))) + (step-store-stdout-file script inputs store) + (step-store-stderr-file script inputs store))))) (let ((scheduler (workflow-scheduler manifest-file channels scratch store batch-system #:guix-daemon-socket guix-daemon-socket))) |