aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorArun Isaac2025-06-05 23:46:12 +0100
committerArun Isaac2025-06-26 14:50:28 +0100
commit93c590d1467f747c3845d95922b6f9b42c8bd92c (patch)
tree34971ddc451f7ab5f7a10e40c2113e96bbe21757
parentfd46885b7c831e39c813d3e9709b6b218fd32d5c (diff)
downloadravanan-93c590d1467f747c3845d95922b6f9b42c8bd92c.tar.gz
ravanan-93c590d1467f747c3845d95922b6f9b42c8bd92c.tar.lz
ravanan-93c590d1467f747c3845d95922b6f9b42c8bd92c.zip
command-line-tool: Build script without inputs.
We build the script using only the CommandLineTool workflow itself, and not its inputs. We pass the inputs in through a WORKFLOW_INPUTS environment variable. To maintain bullet-proof caching, we construct ravanan store paths by hashing both the script and the inputs together. This change paves the way to pre-building all scripts ahead of time. * ravanan/command-line-tool.scm (<command-line-binding>, command-line-binding->args): Move to (ravanan work command-line-tool). (run-command-line-tool): Do not pass inputs to build-command-line-tool-script. Call step-store-files-directory instead of script->store-files-directory, step-store-data-file instead of script->store-data-file, step-store-stdout-file instead of script->store-stdout-file and step-store-stderr-file instead of script->store-stderr-file. Put inputs into job state objects. Pass inputs to the script through the WORKFLOW_INPUTS environment variable. (capture-command-line-tool-output): Accept inputs. Call step-store-data-file instead of script->store-data-file and step-store-files-directory instead of script->store-files-directory. (copy-input-files-gexp): Delete function. (build-command-line-tool-script): Do not accept inputs; get them from the WORKFLOW_INPUTS environment variable. [coerce-argument]: New function. [run-command-gexp]: Build command inside the G-expression. [gexp]{copy-input-files}: New function. * ravanan/job-state.scm (<single-machine-job-state>)[inputs]: New field. * ravanan/job-state.scm (<slurm-job-state>)[inputs]: New field. * ravanan/job-state.scm (job-state-inputs): New public function. * ravanan/store.scm: Import (srfi srfi-71), (gcrypt hash) and (guix build utils). (sha1-hash-sexp): New function. (script->store-files-directory, script->store-data-file, script->store-stdout-file, script->store-stderr-file): Delete functions. (step-store-files-directory, step-store-data-file, step-store-stdout-file, step-store-stderr-file): New public functions. * ravanan/work/command-line-tool.scm: Import (srfi srfi-9 gnu) and (ravanan work ui). * ravanan/workflow.scm (&job-failure)[inputs]: New field. * ravanan/workflow.scm (workflow-scheduler)[poll]: Put inputs into &job-failure condition. [capture-output]: Call step-store-stdout-file instead of script->store-stdout-file and step-store-stderr-file instead of script->store-stderr-file. Pass inputs to capture-command-line-tool-output. (run-workflow): Call step-store-stdout-file instead of script->store-stdout-file and step-store-stderr-file instead of script->store-stderr-file.
-rw-r--r--ravanan/command-line-tool.scm206
-rw-r--r--ravanan/job-state.scm15
-rw-r--r--ravanan/store.scm64
-rw-r--r--ravanan/work/command-line-tool.scm166
-rw-r--r--ravanan/workflow.scm22
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)))