about summary refs log tree commit diff
diff options
context:
space:
mode:
-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)))