aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xbin/ravanan37
-rw-r--r--ravanan/command-line-tool.scm1049
-rw-r--r--ravanan/job-state.scm15
-rw-r--r--ravanan/propnet.scm22
-rw-r--r--ravanan/reader.scm29
-rw-r--r--ravanan/store.scm88
-rw-r--r--ravanan/utils.scm4
-rw-r--r--ravanan/work/command-line-tool.scm176
-rw-r--r--ravanan/workflow.scm318
-rw-r--r--tests/store.scm80
10 files changed, 1098 insertions, 720 deletions
diff --git a/bin/ravanan b/bin/ravanan
index 6ad5167..ba04718 100755
--- a/bin/ravanan
+++ b/bin/ravanan
@@ -36,7 +36,9 @@ exec guile --no-auto-compile -e main -s "$0" "$@"
(ravanan utils)
(ravanan verbosity)
(ravanan workflow)
- (ravanan work utils))
+ (ravanan work ui)
+ (ravanan work utils)
+ (ravanan work vectors))
(define %options
(list (option (list "batch-system" "batchSystem") #t #f
@@ -44,7 +46,7 @@ exec guile --no-auto-compile -e main -s "$0" "$@"
(if (member arg (list "single-machine" "slurm-api"))
(acons 'batch-system (string->symbol arg)
result)
- (error "Unknown batch system" arg))))
+ (user-error "Unknown batch system ~a" arg))))
(option (list "guix-channels") #t #f
(lambda (opt name arg result)
(acons 'guix-channels-file arg result)))
@@ -87,7 +89,7 @@ exec guile --no-auto-compile -e main -s "$0" "$@"
(cons 'traces
(cons (string->symbol arg)
(assq-ref result 'traces))))
- (error "Unknown trace subsystem" arg)))))
+ (user-error "Unknown trace subsystem ~a" arg)))))
(option (list "help") #f #t
(lambda (opt name arg result)
(acons 'help #t result)))
@@ -96,7 +98,13 @@ exec guile --no-auto-compile -e main -s "$0" "$@"
(acons 'version #t result)))))
(define (invalid-option opt name arg result)
- (error "Invalid option" name))
+ (user-error "Invalid option ~a" name))
+
+(define (print-short-usage program)
+ (format (current-error-port)
+ "Usage: ~a [OPTIONS] CWL-WORKFLOW INPUTS
+Run CWL-WORKFLOW with INPUTS.~%"
+ program))
(define (print-usage program)
(format (current-error-port)
@@ -211,13 +219,13 @@ files that have the token in the @verbatim{SLURM_JWT=token} format."
(exit #t))
;; Check for required arguments.
(unless (assq-ref args 'store)
- (error "--store not specified"))
+ (user-error "--store not specified"))
(case (assq-ref args 'batch-system)
((slurm-api)
(unless (assq-ref args 'scratch)
- (error "--scratch not specified"))
+ (user-error "--scratch not specified"))
(unless (assq-ref args 'slurm-jwt)
- (error "--slurm-jwt not specified"))))
+ (user-error "--slurm-jwt not specified"))))
(match (reverse (assq-ref args 'args))
((workflow-file inputs-file)
;; We must not try to compile guix manifest files.
@@ -238,13 +246,13 @@ files that have the token in the @verbatim{SLURM_JWT=token} format."
(let ((file (manifest-file-error-file c)))
(cond
((not file)
- (error "--guix-manifest not specified"))
+ (user-error "--guix-manifest not specified"))
((not (file-exists? file))
- (error "Manifest file ~a does not exist"
- file))
+ (user-error "Manifest file ~a does not exist"
+ file))
(else
- (error "Error loading manifest file"
- file)
+ (user-error "Error loading manifest file ~a"
+ file)
(raise-exception c))))))
(parameterize ((%traces (assq-ref args 'traces)))
(run-workflow (file-name-stem workflow-file)
@@ -282,4 +290,7 @@ files that have the token in the @verbatim{SLURM_JWT=token} format."
outputs)
outputs)
#:pretty #t))
- (newline)))))))
+ (newline))
+ (_
+ (print-short-usage program)
+ (exit #f)))))))
diff --git a/ravanan/command-line-tool.scm b/ravanan/command-line-tool.scm
index c47bb3c..62785cc 100644
--- a/ravanan/command-line-tool.scm
+++ b/ravanan/command-line-tool.scm
@@ -53,9 +53,11 @@
#:use-module (ravanan work ui)
#:use-module (ravanan work utils)
#:use-module (ravanan work vectors)
- #:export (run-command-line-tool
+ #:export (build-command-line-tool-script
+ run-command-line-tool
check-requirements
inherit-requirements
+ find-requirement
%command-line-tool-supported-requirements
command-line-tool-supported-requirements
capture-command-line-tool-output
@@ -95,15 +97,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,95 +285,37 @@ 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}.
-
-When @var{guix-daemon-socket} is provided, connect to that Guix daemon."
- (if guix-daemon-socket
- (parameterize ((%daemon-socket-uri guix-daemon-socket))
- (build-gexp-script name exp))
- (with-store store
- (run-with-store store
- (mlet %store-monad ((drv (gexp->script name exp)))
- (mbegin %store-monad
- (built-derivations (list drv))
- (return (derivation->output-path drv))))))))
+(define* (build-gexp-script name exp)
+ "Build script named @var{name} using G-expression @var{exp}. Return the path to
+the built script as a monadic value."
+ (mlet %store-monad ((drv (gexp->script name exp)))
+ (mbegin %store-monad
+ (built-derivations (list drv))
+ (return (derivation->output-path drv)))))
-(define* (run-command-line-tool name manifest-file channels cwl inputs
- scratch store batch-system
- #:key guix-daemon-socket)
- "Run @code{CommandLineTool} class workflow @var{cwl} named @var{name} with
-@var{inputs} using tools from Guix manifest in @var{manifest-file}. Return a
-state-monadic job state object.
+(define* (run-command-line-tool name script inputs resource-requirement
+ store batch-system)
+ "Run @code{CommandLineTool} class workflow @var{script} named @var{name} with
+@var{inputs}. Return a state-monadic job state object.
-@var{channels}, @var{scratch}, @var{store}, @var{batch-system} and
-@var{guix-daemon-socket} are the same as in @code{run-workflow} from
+@var{resource-requirement} is the @code{ResourceRequirement} of the workflow.
+@var{store} and @var{batch-system} 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
- scratch store batch-system
- guix-daemon-socket))
- (requirements (inherit-requirements (or (assoc-ref cwl "requirements")
- #())
- (or (assoc-ref cwl "hints")
- #())))
- (cpus (from-maybe
- (maybe-bind (maybe-assoc-ref (find-requirement requirements
- "ResourceRequirement")
- "coresMin")
- (compose just
- inexact->exact
- ceiling
- (cut coerce-type <> 'number)
- (cut coerce-expression
- <>
- `(("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)))
+ (let ((cpus (from-maybe
+ (maybe-bind (maybe-assoc-ref resource-requirement
+ "coresMin")
+ (compose just
+ inexact->exact
+ ceiling
+ (cut coerce-type <> 'number)
+ (cut coerce-expression
+ <>
+ `(("inputs" . ,inputs)))))
+ 1))
+ (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 +324,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
@@ -401,44 +336,41 @@ state-monadic job state object.
(when (file-exists? store-files-directory)
(delete-file-recursively store-files-directory))
(mkdir store-files-directory)
- (cond
- ((eq? batch-system 'single-machine)
- (state-let* ((success? (single-machine:submit-job
- `(("WORKFLOW_OUTPUT_DIRECTORY" .
- ,store-files-directory)
- ("WORKFLOW_OUTPUT_DATA_FILE" .
- ,store-data-file))
- stdout-file
- stderr-file
- script)))
- (state-return (single-machine-job-state script success?))))
- ((slurm-api-batch-system? batch-system)
- (state-let* ((job-id
- (slurm:submit-job `(("WORKFLOW_OUTPUT_DIRECTORY" .
- ,store-files-directory)
- ("WORKFLOW_OUTPUT_DATA_FILE" .
- ,store-data-file))
- stdout-file
- stderr-file
- cpus
- name
- script
- #:api-endpoint (slurm-api-batch-system-endpoint batch-system)
- #:jwt (slurm-api-batch-system-jwt batch-system)
- #:partition (slurm-api-batch-system-partition batch-system)
- #:nice (slurm-api-batch-system-nice batch-system))))
- (format (current-error-port)
- "~a submitted as job ID ~a~%"
- script
- job-id)
- (state-return (slurm-job-state script job-id))))
- (else
- (assertion-violation batch-system "Invalid batch system")))))))
+ (let ((environment
+ `(("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)
+ (state-let* ((success? (single-machine:submit-job environment
+ stdout-file
+ stderr-file
+ script)))
+ (state-return (single-machine-job-state script inputs success?))))
+ ((slurm-api-batch-system? batch-system)
+ (state-let* ((job-id
+ (slurm:submit-job environment
+ stdout-file
+ stderr-file
+ cpus
+ name
+ script
+ #:api-endpoint (slurm-api-batch-system-endpoint batch-system)
+ #:jwt (slurm-api-batch-system-jwt batch-system)
+ #:partition (slurm-api-batch-system-partition batch-system)
+ #:nice (slurm-api-batch-system-nice batch-system))))
+ (format (current-error-port)
+ "~a submitted as job ID ~a~%"
+ 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.
@@ -455,7 +387,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)
@@ -478,52 +410,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
- (expand-file-name #$(store-item-name (assoc-ref input "path"))
- 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."
@@ -548,64 +434,60 @@ maybe-monadic value."
(guix profiles))))
(raise-exception (manifest-file-error manifest-file))))
-(define (call-with-inferior inferior proc)
- "Call @var{proc} with @var{inferior} and return the return value of @var{proc}.
-Close @var{inferior} when done, even if @var{proc} exits non-locally."
- (dynamic-wind (const #t)
- (cut proc inferior)
- (cut close-inferior inferior)))
+;; Monadic version of inferior-eval-with-store; Argument order is rearranged
+;; slightly to suit store-lift.
+(define inferior-meval-with-store
+ (store-lift (lambda (store inferior proc)
+ (inferior-eval-with-store inferior store proc))))
-(define (manifest-file->environment manifest-file channels guix-daemon-socket)
- "Build @var{manifest-file} and return an association list of environment
-variables to set to use the built profile. Connect to the Guix daemon specified
-by @var{guix-daemon-socket}. If @var{channels} is not @code{#f}, build manifest
-in a Guix inferior with @var{channels}."
- (if channels
- (call-with-inferior (inferior-for-channels channels)
- (cut inferior-eval
- `(begin
- (use-modules (ice-9 match)
- (guix search-paths)
- (gnu packages)
- (guile)
- (guix gexp)
- (guix profiles))
+(define (manifest-file->search-path-sexps manifest-file inferior)
+ "Return a list of search path S-expressions for a profile described by
+@var{manifest-file}. Load manifest in @var{inferior} unless it is @code{#f}.
+Return value is monadic."
+ (define proc
+ `(lambda (store)
+ ;; Do not auto-compile manifest files.
+ (set! %load-should-auto-compile #f)
+ (map search-path-specification->sexp
+ (manifest-search-paths (load ,manifest-file)))))
+
+ (if inferior
+ (begin
+ (inferior-eval '(use-modules (guix profiles)
+ (guix search-paths))
+ inferior)
+ (mbegin %store-monad
+ (inferior-meval-with-store inferior proc)))
+ (mbegin %store-monad
+ (return (map search-path-specification->sexp
+ (manifest-search-paths (load-manifest manifest-file)))))))
- (define (build-derivation drv guix-daemon-socket)
- (if guix-daemon-socket
- (parameterize ((%daemon-socket-uri guix-daemon-socket))
- (build-derivation drv))
- (with-store store
- (run-with-store store
- (mlet %store-monad ((drv drv))
- (mbegin %store-monad
- (built-derivations (list drv))
- (return (derivation->output-path drv))))))))
+(define (manifest-file->profile-derivation manifest-file inferior)
+ "Return a derivation to build @var{manifest-file}. Build manifest in
+@var{inferior} unless it is @code{#f}. Return value is monadic."
+ (define proc
+ `(lambda (store)
+ ;; Do not auto-compile manifest files.
+ (set! %load-should-auto-compile #f)
+ (derivation-file-name
+ (run-with-store store
+ (profile-derivation (load ,manifest-file)
+ #:allow-collisions? #t)))))
- ;; Do not auto-compile manifest files.
- (set! %load-should-auto-compile #f)
- (let ((manifest (load ,(canonicalize-path manifest-file))))
- (map (match-lambda
- ((specification . value)
- (cons (search-path-specification-variable specification)
- value)))
- (evaluate-search-paths
- (manifest-search-paths manifest)
- (list (build-derivation
- (profile-derivation manifest
- #:allow-collisions? #t)
- ,guix-daemon-socket))))))
- <>))
- (manifest->environment (load-manifest manifest-file)
- guix-daemon-socket)))
+ (if inferior
+ (begin
+ (inferior-eval '(use-modules (guix profiles))
+ inferior)
+ (mlet %store-monad ((drv-file (inferior-meval-with-store inferior proc)))
+ (return (read-derivation-from-file drv-file))))
+ (let ((manifest (load-manifest manifest-file)))
+ (profile-derivation manifest
+ #:allow-collisions? #t))))
-(define (software-packages->environment packages channels guix-daemon-socket)
- "Build a profile with @var{packages} and return an association list
-of environment variables to set to use the built profile. @var{packages} is a
-vector of @code{SoftwarePackage} assocation lists as defined in the CWL
-standard. Connect to the Guix daemon specified by @var{guix-daemon-socket}. If
-@var{channels} is not @code{#f}, look up packages in a Guix inferior with
-@var{channels}."
+(define (software-packages->manifest packages inferior)
+ "Return a manifest with @var{packages}. @var{packages} is a vector of
+@code{SoftwarePackage} assocation lists as defined in the CWL standard. Look up
+packages in @var{inferior} unless it is @code{#f}."
(define (software-package->package-specification package)
(string-append (assoc-ref* package "package")
(from-maybe
@@ -614,63 +496,48 @@ standard. Connect to the Guix daemon specified by @var{guix-daemon-socket}. If
(cut string-append "@" <>)))
"")))
- (define packages->environment
- (compose (cut manifest->environment <> guix-daemon-socket)
- packages->manifest))
-
- (if channels
- (call-with-inferior (inferior-for-channels channels)
- (lambda (inferior)
- (packages->environment
- (vector-map->list (lambda (package)
- (let ((name (assoc-ref package "package"))
- (version (assoc-ref package "version")))
- (match (lookup-inferior-packages inferior
- name
- version)
- ((inferior-package _ ...)
- inferior-package))))
- packages))))
- (packages->environment
+ (packages->manifest
+ (if inferior
+ (vector-map->list (lambda (package)
+ (let ((name (assoc-ref package "package"))
+ (version (assoc-ref package "version")))
+ (match (lookup-inferior-packages inferior
+ name
+ version)
+ ((inferior-package _ ...)
+ inferior-package))))
+ packages)
(vector-map->list (compose specification->package
software-package->package-specification)
packages))))
-(define (manifest->environment manifest guix-daemon-socket)
- "Build @var{manifest} and return an association list of environment
-variables to set to use the built profile. Connect to the Guix daemon specified
-by @var{guix-daemon-socket}."
- (define (build-derivation drv guix-daemon-socket)
- (if guix-daemon-socket
- (parameterize ((%daemon-socket-uri guix-daemon-socket))
- (build-derivation drv #f))
- (with-store store
- (run-with-store store
- (mlet %store-monad ((drv drv))
- (mbegin %store-monad
- (built-derivations (list drv))
- (return (derivation->output-path drv))))))))
+(define (software-packages->search-path-sexps packages inferior)
+ "Return a list of search path S-expressions for a profile with @var{packages}.
+@var{packages} is a vector of @code{SoftwarePackage} assocation lists as defined
+in the CWL standard. Look up packages in @var{inferior} unless it is @code{#f}.
+Return value is monadic."
+ (mbegin %store-monad
+ (return (map search-path-specification->sexp
+ (manifest-search-paths
+ (software-packages->manifest packages inferior))))))
- (map (match-lambda
- ((specification . value)
- (cons (search-path-specification-variable specification)
- value)))
- (evaluate-search-paths
- (manifest-search-paths manifest)
- (list (build-derivation
- (profile-derivation manifest
- #:allow-collisions? #t)
- guix-daemon-socket)))))
+(define (software-packages->profile-derivation packages inferior)
+ "Return a derivation to build a profile with @var{packages}. @var{packages} is a
+vector of @code{SoftwarePackage} assocation lists as defined in the CWL
+standard. Look up packages in @var{inferior} unless it is @code{#f}. Return
+value is monadic."
+ (profile-derivation (software-packages->manifest packages inferior)
+ #:allow-collisions? #t))
-(define (build-command-line-tool-script name manifest-file channels cwl inputs
- scratch store batch-system
- guix-daemon-socket)
+(define (build-command-line-tool-script name manifest-file inferior cwl
+ scratch store batch-system)
"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}. Use @var{inferior} to build manifests, unless it is
+@code{#f}. Return value is monadic.
-@var{channels}, @var{scratch}, @var{store} and @var{guix-daemon-socket} are the
-same as in @code{run-workflow} from @code{(ravanan workflow)}."
+@var{scratch} and @var{store} are the same as in @code{run-workflow} from
+@code{(ravanan workflow)}."
(define (environment-variables env-var-requirement)
(just (vector-map->list (lambda (environment-definition)
#~(list #$(assoc-ref* environment-definition
@@ -697,12 +564,12 @@ same as in @code{run-workflow} from @code{(ravanan workflow)}."
(define (cores batch-system)
(cond
- ((slurm-api-batch-system? batch-system)
- #~(string->number (getenv "SLURM_CPUS_ON_NODE")))
- ((eq? batch-system 'single-machine)
- #~(total-processor-count))
- (else
- (assertion-violation batch-system "Unknown batch system"))))
+ ((slurm-api-batch-system? batch-system)
+ #~(string->number (getenv "SLURM_CPUS_ON_NODE")))
+ ((eq? batch-system 'single-machine)
+ #~(total-processor-count))
+ (else
+ (assertion-violation batch-system "Unknown batch system"))))
(define stdout-filename
(cond
@@ -725,12 +592,35 @@ 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 (vector->gexp vec)
+ ;; FIXME: G-expressions in vectors are not properly substituted. Fix this in
+ ;; Guix.
+ #~(vector #$@(vector->list vec)))
+
+ (define (alist->gexp alist)
+ ;; FIXME: G-expressions as values in dotted alists are not properly
+ ;; substituted. Fix this in Guix.
+ #~(list #$@(map (match-lambda
+ ((key . value)
+ #~(cons #$key #$value)))
+ alist)))
+
(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->gexp
+ (vector-map (compose alist->gexp coerce-argument)
+ (assoc-ref cwl "arguments")))
+ #$(assoc-ref cwl "inputs")
+ inputs))
#$(coerce-expression (assoc-ref cwl "stdin"))
#$stdout-filename
'#$(from-maybe
@@ -812,7 +702,7 @@ same as in @code{run-workflow} from @code{(ravanan workflow)}."
(maybe-let* ((work-reuse (find-requirement requirements "WorkReuse")))
(and (not (coerce-type (assoc-ref* work-reuse "enableReuse")
'boolean))
- (user-error "Disabling WorkReuse is not supported. ravanan's strong caching using Guix makes it unnecessary."))))
+ (user-error "Disabling WorkReuse is not supported. With ravanan's strong caching using Guix, there is no need to disable WorkReuse."))))
(maybe-let* ((hints (maybe-assoc-ref (just cwl) "hints")))
(check-requirements hints
batch-system
@@ -823,276 +713,329 @@ same as in @code{run-workflow} from @code{(ravanan workflow)}."
(maybe-let* ((work-reuse (find-requirement hints "WorkReuse")))
(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")
- #())
- (or (assoc-ref cwl "hints")
- #())))
- (initial-work-dir-requirement (find-requirement requirements
- "InitialWorkDirRequirement"))
- (manifest-file
- (from-maybe (maybe-assoc-ref
- (find-requirement requirements "SoftwareRequirement")
- "manifest")
- manifest-file))
- (packages
- (from-maybe (maybe-assoc-ref
- (find-requirement requirements "SoftwareRequirement")
- "packages")
- #())))
- (with-imported-modules (source-module-closure '((ravanan work command-line-tool)
- (ravanan work monads)
- (ravanan work ui)
- (ravanan work vectors)
- (ravanan glob)
- (guix search-paths))
- #:select? (match-lambda
- (('ravanan work . _) #t)
- (('guix . _) #t)
- (('json . _) #t)
- (_ #f)))
- (with-extensions (list guile-filesystem guile-gcrypt)
- #~(begin
- (use-modules (ravanan work command-line-tool)
- (ravanan work monads)
- (ravanan work types)
- (ravanan work ui)
- (ravanan work utils)
- (ravanan work vectors)
- (ravanan glob)
- (rnrs io ports)
- (srfi srfi-1)
- (srfi srfi-26)
- (ice-9 filesystem)
- (ice-9 match)
- (ice-9 threads)
- (guix search-paths)
- (json))
+ (warning "Ignoring disable of WorkReuse. With ravanan's strong caching using Guix, there is no need to disable WorkReuse."))))
+
+ (let* ((requirements (inherit-requirements (or (assoc-ref cwl "requirements")
+ #())
+ (or (assoc-ref cwl "hints")
+ #())))
+ (initial-work-dir-requirement (find-requirement requirements
+ "InitialWorkDirRequirement"))
+ (manifest-file
+ (from-maybe (maybe-assoc-ref
+ (find-requirement requirements "SoftwareRequirement")
+ "manifest")
+ manifest-file))
+ (packages
+ (from-maybe (maybe-assoc-ref
+ (find-requirement requirements "SoftwareRequirement")
+ "packages")
+ #())))
+ (mlet %store-monad ((search-path-sexps
+ (match packages
+ ;; No package specifications; try the manifest file.
+ (#()
+ (manifest-file->search-path-sexps manifest-file
+ inferior))
+ ;; Use package specifications to build an
+ ;; environment.
+ (_
+ (software-packages->search-path-sexps packages
+ inferior))))
+ (profile-derivation
+ (match packages
+ ;; No package specifications; try the manifest file.
+ (#()
+ (manifest-file->profile-derivation manifest-file
+ inferior))
+ ;; Use package specifications to build an
+ ;; environment.
+ (_
+ (software-packages->profile-derivation packages
+ inferior)))))
+ (build-gexp-script name
+ (with-imported-modules (source-module-closure '((ravanan work command-line-tool)
+ (ravanan work monads)
+ (ravanan work ui)
+ (ravanan work vectors)
+ (ravanan glob)
+ (guix search-paths))
+ #:select? (match-lambda
+ (('ravanan work . _) #t)
+ (('guix . _) #t)
+ (('json . _) #t)
+ (_ #f)))
+ (with-extensions (list guile-filesystem guile-gcrypt)
+ #~(begin
+ (use-modules (ravanan work command-line-tool)
+ (ravanan work monads)
+ (ravanan work types)
+ (ravanan work ui)
+ (ravanan work utils)
+ (ravanan work vectors)
+ (ravanan glob)
+ (rnrs io ports)
+ (srfi srfi-1)
+ (srfi srfi-26)
+ (ice-9 filesystem)
+ (ice-9 match)
+ (ice-9 threads)
+ (guix search-paths)
+ (json))
- (define (copy-file-value value directory)
- ;; Copy file represented by value to directory and return the
- ;; new File value.
- (let* ((path (assoc-ref* value "path"))
- (destination-path (expand-file-name (basename path)
- directory)))
- (copy-file path destination-path)
- (assoc-set value
- (cons "location" (string-append "file://" destination-path))
- (cons "path" destination-path))))
+ (define (copy-input-files input inputs-directory)
+ ;; Copy input files and update corresponding input objects.
+ (cond
+ ((vector? input)
+ (vector-map (cut copy-input-files <> inputs-directory)
+ 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 (cut copy-input-files <> inputs-directory)
+ secondary-files)))))))
+ (else input)))
- (define (capture-secondary-file path secondary-file
- workflow-output-directory)
- "Capture @var{secondary-file} for primary @var{path} and return its
+ (define (copy-file-value value directory)
+ ;; Copy file represented by value to directory and return the
+ ;; new File value.
+ (let* ((path (assoc-ref* value "path"))
+ (destination-path (expand-file-name (basename path)
+ directory)))
+ (copy-file path destination-path)
+ (assoc-set value
+ (cons "location" (string-append "file://" destination-path))
+ (cons "path" destination-path))))
+
+ (define (capture-secondary-file path secondary-file
+ workflow-output-directory)
+ "Capture @var{secondary-file} for primary @var{path} and return its
canonicalized value. If @var{required} is @code{#t} and no such secondary file
is found, error out. @var{workflow-output-directory} is path to the output
directory of the workflow."
- (let* ((secondary-file-path (secondary-path path secondary-file))
- (secondary-file-value
- (and (file-exists? secondary-file-path)
- (copy-file-value (canonicalize-file-value
- `(("class" . "File")
- ("path" . ,secondary-file-path)))
- workflow-output-directory))))
- (if (and (assoc-ref* secondary-file "required")
- (not secondary-file-value))
- (user-error "Secondary file ~a missing for output path ~a"
- pattern
- path)
- secondary-file-value)))
-
- (define (path+sha1->value path sha1 workflow-output-directory maybe-secondary-files)
- (maybe-assoc-set (copy-file-value (canonicalize-file-value
- `(("class" . "File")
- ("path" . ,path)
- ("checksum" . ,(string-append "sha1$" sha1))))
- workflow-output-directory)
- (cons "secondaryFiles"
- (maybe-let* ((secondary-files maybe-secondary-files))
- (just (vector-filter-map (cut capture-secondary-file
- path
- <>
- workflow-output-directory)
- secondary-files))))))
+ (let* ((secondary-file-path (secondary-path path secondary-file))
+ (secondary-file-value
+ (and (file-exists? secondary-file-path)
+ (copy-file-value (canonicalize-file-value
+ `(("class" . "File")
+ ("path" . ,secondary-file-path)))
+ workflow-output-directory))))
+ (if (and (assoc-ref* secondary-file "required")
+ (not secondary-file-value))
+ (user-error "Secondary file ~a missing for output path ~a"
+ pattern
+ path)
+ secondary-file-value)))
- (define (path->value path workflow-output-directory maybe-secondary-files)
- (path+sha1->value path
- (sha1-hash path)
- workflow-output-directory
- maybe-secondary-files))
+ (define (path+sha1->value path sha1 workflow-output-directory maybe-secondary-files)
+ (maybe-assoc-set (copy-file-value (canonicalize-file-value
+ `(("class" . "File")
+ ("path" . ,path)
+ ("checksum" . ,(string-append "sha1$" sha1))))
+ workflow-output-directory)
+ (cons "secondaryFiles"
+ (maybe-let* ((secondary-files maybe-secondary-files))
+ (just (vector-filter-map (cut capture-secondary-file
+ path
+ <>
+ workflow-output-directory)
+ secondary-files))))))
- (define (stdout-output->value workflow-output-directory
- stdout-directory
- stdout-filename
- output)
- (cons (assoc-ref output "id")
- (let ((sha1 (sha1-hash stdout-filename)))
- ;; Use path+sha1->value instead of path->value to avoid
- ;; recomputing the SHA1 hash.
- (path+sha1->value
- (if (string=? stdout-filename
- (file-name-join* stdout-directory "stdout"))
- ;; If stdout filename is unspecified, rename it to a
- ;; hash of its contents.
- (let ((hashed-filename
- (file-name-join* stdout-directory sha1)))
- (rename-file stdout-filename
- hashed-filename)
- hashed-filename)
- ;; Else, return the stdout filename as it is.
- stdout-filename)
- sha1
- workflow-output-directory
- %nothing))))
+ (define (path->value path workflow-output-directory maybe-secondary-files)
+ (path+sha1->value path
+ (sha1-hash path)
+ workflow-output-directory
+ maybe-secondary-files))
- (define (other-output->value workflow-output-directory
- output-id output-type-tree
- maybe-secondary-files glob-pattern)
- (cons output-id
- ;; TODO: Support all types.
- (let* ((output-type (formal-parameter-type output-type-tree))
- (paths (glob glob-pattern))
- (matched-type (glob-match-type paths output-type)))
- (unless matched-type
- (user-error "Type ~a mismatch for globbed paths ~a"
- output-type
- paths))
- ;; Coerce output value into matched type.
- (let ((output-values (map (cut path->value
- <>
- workflow-output-directory
- maybe-secondary-files)
- paths)))
- (cond
- ((memq matched-type (list 'File 'Directory))
- (match output-values
- ((output-file)
- output-file)))
- ;; TODO: Recurse.
- ((and (array-type? matched-type)
- (memq (array-type-subtype matched-type)
- (list 'File 'Directory)))
- (list->vector output-values))
- ((eq? matched-type 'null)
- 'null))))))
+ (define (stdout-output->value workflow-output-directory
+ stdout-directory
+ stdout-filename
+ output)
+ (cons (assoc-ref output "id")
+ (let ((sha1 (sha1-hash stdout-filename)))
+ ;; Use path+sha1->value instead of path->value to avoid
+ ;; recomputing the SHA1 hash.
+ (path+sha1->value
+ (if (string=? stdout-filename
+ (file-name-join* stdout-directory "stdout"))
+ ;; If stdout filename is unspecified, rename it to a
+ ;; hash of its contents.
+ (let ((hashed-filename
+ (file-name-join* stdout-directory sha1)))
+ (rename-file stdout-filename
+ hashed-filename)
+ hashed-filename)
+ ;; Else, return the stdout filename as it is.
+ stdout-filename)
+ sha1
+ workflow-output-directory
+ %nothing))))
- (define (stage-file file entry-name)
- ;; Stage file as entry-name and return the staged File value.
- (rename-file (assoc-ref* file "path")
- entry-name)
- (canonicalize-file-value
- (maybe-assoc-set `(("class" . "File")
- ("path" . ,entry-name))
- (cons "secondaryFiles"
- (maybe-let* ((secondary-files
- (maybe-assoc-ref (just file) "secondaryFiles")))
- (just (vector-map (lambda (file)
- (stage-file file (assoc-ref* file "basename")))
- secondary-files)))))))
+ (define (other-output->value workflow-output-directory
+ output-id output-type-tree
+ maybe-secondary-files glob-pattern)
+ (cons output-id
+ ;; TODO: Support all types.
+ (let* ((output-type (formal-parameter-type output-type-tree))
+ (paths (glob glob-pattern))
+ (matched-type (glob-match-type paths output-type)))
+ (unless matched-type
+ (user-error "Type ~a mismatch for globbed paths ~a"
+ output-type
+ paths))
+ ;; Coerce output value into matched type.
+ (let ((output-values (map (cut path->value
+ <>
+ workflow-output-directory
+ maybe-secondary-files)
+ paths)))
+ (cond
+ ((memq matched-type (list 'File 'Directory))
+ (match output-values
+ ((output-file)
+ output-file)))
+ ;; TODO: Recurse.
+ ((and (array-type? matched-type)
+ (memq (array-type-subtype matched-type)
+ (list 'File 'Directory)))
+ (list->vector output-values))
+ ((eq? matched-type 'null)
+ 'null))))))
- ;; Stage files.
- ;; We currently support File and Dirent only. TODO: Support others.
- (define (stage-files entries outputs-directory)
- ;; Stage entries and return an association list mapping files
- ;; (presumably input files) that were staged.
- (filter-map (match-lambda
- ((entry-name entry)
- (cond
- ;; Stuff string literal into a file.
- ((string? entry)
- (call-with-input-file entry-name
- (cut put-string <> entry))
- #f)
- ;; Symlink to the file.
- ((eq? (object-type entry)
- 'File)
- (cons entry
- (stage-file entry entry-name))))))
- entries))
+ (define (stage-file file entry-name)
+ ;; Stage file as entry-name and return the staged File value.
+ (rename-file (assoc-ref* file "path")
+ entry-name)
+ (canonicalize-file-value
+ (maybe-assoc-set `(("class" . "File")
+ ("path" . ,entry-name))
+ (cons "secondaryFiles"
+ (maybe-let* ((secondary-files
+ (maybe-assoc-ref (just file) "secondaryFiles")))
+ (just (vector-map (lambda (file)
+ (stage-file file (assoc-ref* file "basename")))
+ secondary-files)))))))
- (define (set-staged-path input staging-mapping)
- ;; If input is a File type input that was staged, return new
- ;; staged value. Else, return as is.
- (cond
- ;; Recurse on vector inputs.
- ((vector? input)
- (list->vector
- (map (cut set-staged-path <> staging-mapping)
- (vector->list input))))
- ;; Try to replace File input value with staged value.
- ((eq? (object-type input)
- 'File)
- (or (any (match-lambda
- ((old-value . new-value)
- (and (alist=? input old-value)
- new-value)))
- staging-mapping)
- input))
- ;; Else, return as is.
- (else input)))
+ ;; Stage files.
+ ;; We currently support File and Dirent only. TODO: Support others.
+ (define (stage-files entries outputs-directory)
+ ;; Stage entries and return an association list mapping files
+ ;; (presumably input files) that were staged.
+ (filter-map (match-lambda
+ ((entry-name entry)
+ (cond
+ ;; Stuff string literal into a file.
+ ((string? entry)
+ (call-with-input-file entry-name
+ (cut put-string <> entry))
+ #f)
+ ;; Symlink to the file.
+ ((eq? (object-type entry)
+ 'File)
+ (cons entry
+ (stage-file entry entry-name))))))
+ entries))
- ;; Set search paths for manifest.
- (for-each (match-lambda
- ((name . value)
- (setenv name value)))
- '#$(match packages
- ;; No package specifications; try the manifest
- ;; file.
- (#()
- (manifest-file->environment manifest-file
- channels
- guix-daemon-socket))
- ;; Use package specifications to build an
- ;; environment.
- (_
- (software-packages->environment packages
- channels
- guix-daemon-socket))))
+ (define (set-staged-path input staging-mapping)
+ ;; If input is a File type input that was staged, return new
+ ;; staged value. Else, return as is.
+ (cond
+ ;; Recurse on vector inputs.
+ ((vector? input)
+ (list->vector
+ (map (cut set-staged-path <> staging-mapping)
+ (vector->list input))))
+ ;; Try to replace File input value with staged value.
+ ((eq? (object-type input)
+ 'File)
+ (or (any (match-lambda
+ ((old-value . new-value)
+ (and (alist=? input old-value)
+ new-value)))
+ staging-mapping)
+ input))
+ ;; Else, return as is.
+ (else input)))
- (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)))
- (runtime `(("cores" . ,#$(cores batch-system)))))
+ ;; Set search paths for manifest.
+ (for-each (match-lambda
+ ((specification . value)
+ (setenv (search-path-specification-variable specification)
+ value)))
+ (evaluate-search-paths
+ (map sexp->search-path-specification
+ '#$search-path-sexps)
+ '(#$profile-derivation)))
+ (call-with-temporary-directory
+ (lambda (inputs-directory)
+ (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.
- (for-each (match-lambda
- ((name value)
- (setenv name value)))
- (list #$@(from-maybe
- (maybe-bind
- (find-requirement requirements
- "EnvVarRequirement")
- environment-variables)
- (list))))
+ ;; Set environment defined by workflow.
+ (for-each (match-lambda
+ ((name value)
+ (setenv name value)))
+ (list #$@(from-maybe
+ (maybe-bind
+ (find-requirement requirements
+ "EnvVarRequirement")
+ environment-variables)
+ (list))))
- (call-with-temporary-directory
- (lambda (stdout-directory)
- (call-with-temporary-directory
- (lambda (outputs-directory)
- (call-with-current-directory outputs-directory
- (lambda ()
- (let* ((staging-mapping
- (stage-files (list #$@(from-maybe
- (maybe-bind initial-work-dir-requirement
- (compose just files-to-stage))
- (list)))
- outputs-directory))
- (inputs
- (map (match-lambda
- ((id . input)
- (cons id
- (set-staged-path input
- staging-mapping))))
- inputs)))
- ;; Actually run the command.
- #$run-command-gexp
- ;; Capture outputs.
- #$capture-outputs-gexp))))
- #$scratch))
- #$scratch)))
- #$scratch)))))
- guix-daemon-socket))
+ (call-with-temporary-directory
+ (lambda (stdout-directory)
+ (call-with-temporary-directory
+ (lambda (outputs-directory)
+ (call-with-current-directory outputs-directory
+ (lambda ()
+ (let* ((staging-mapping
+ (stage-files (list #$@(from-maybe
+ (maybe-bind initial-work-dir-requirement
+ (compose just files-to-stage))
+ (list)))
+ outputs-directory))
+ (inputs
+ (map (match-lambda
+ ((id . input)
+ (cons id
+ (set-staged-path input
+ staging-mapping))))
+ inputs)))
+ ;; Actually run the command.
+ #$run-command-gexp
+ ;; Capture outputs.
+ #$capture-outputs-gexp))))
+ #$scratch))
+ #$scratch)))
+ #$scratch))))))))
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/propnet.scm b/ravanan/propnet.scm
index 34624b5..610991d 100644
--- a/ravanan/propnet.scm
+++ b/ravanan/propnet.scm
@@ -26,22 +26,26 @@
#:use-module (ravanan work monads)
#:use-module (ravanan work utils)
#:export (propnet
+ propnet?
propnet-propagators
propnet-value=?
propnet-merge-values
propnet-scheduler
propagator
+ propagator?
propagator-name
propagator-proc
propagator-inputs
propagator-optional-inputs
propagator-outputs
scheduler
+ scheduler?
scheduler-schedule
scheduler-poll
scheduler-capture-output
schedule-propnet
state+status
+ state+status?
state+status-state
state+status-status
poll-propnet
@@ -64,6 +68,12 @@
(optional-inputs propagator-optional-inputs)
(outputs propagator-outputs))
+(set-record-type-printer! <propagator>
+ (lambda (record port)
+ (display "#<<propagator> " port)
+ (write (propagator-name record) port)
+ (display ">" port)))
+
(define-immutable-record-type <scheduler>
(scheduler schedule poll capture-output)
scheduler?
@@ -86,6 +96,18 @@
(propagators-in-flight propnet-state-propagators-in-flight)
(propagators-inbox propnet-state-propagators-inbox))
+(set-record-type-printer! <propnet-state>
+ (lambda (record port)
+ (display "#<<propnet-state> cells: " port)
+ (write (run-with-state (propnet-state-cells record)) port)
+ (display " cells-inbox: " port)
+ (write (run-with-state (propnet-state-cells-inbox record)) port)
+ (display " propagators-in-flight: " port)
+ (write (run-with-state (propnet-state-propagators-in-flight record)) port)
+ (display " propagators-inbox: " port)
+ (write (run-with-state (propnet-state-propagators-inbox record)) port)
+ (display ">" port)))
+
(define (partition-map pred proc lst)
"Partition @var{lst} into two lists using @var{pred} like @code{partition}. Then,
map @var{proc} over both the lists and return the resulting lists."
diff --git a/ravanan/reader.scm b/ravanan/reader.scm
index a4e57c7..fd959de 100644
--- a/ravanan/reader.scm
+++ b/ravanan/reader.scm
@@ -101,18 +101,31 @@ each association list of the returned vector of association lists. If
(define (normalize-env-var-requirement env-var-requirement)
(assoc-set env-var-requirement
- (cons "envDef"
- (coerce-alist->vector
- (assoc-ref env-var-requirement "envDef")
- "envName" "envValue"))))
+ (cons "envDef"
+ (coerce-alist->vector
+ (assoc-ref env-var-requirement "envDef")
+ "envName" "envValue"))))
+
+(define (normalize-software-requirement software-requirement)
+ (maybe-assoc-set software-requirement
+ ;; Canonicalize manifest file path so that we look it up with respect to the
+ ;; path of the workflow file.
+ (cons "manifest"
+ (maybe-bind (maybe-assoc-ref (just software-requirement)
+ "manifest")
+ (compose just canonicalize-path)))))
(define (normalize-requirements maybe-requirements)
(maybe-let* ((requirements maybe-requirements))
(just (vector-map (lambda (requirement)
- (if (string=? (assoc-ref requirement "class")
- "EnvVarRequirement")
- (normalize-env-var-requirement requirement)
- requirement))
+ (let ((class (assoc-ref requirement "class")))
+ (cond
+ ((string=? class "EnvVarRequirement")
+ (normalize-env-var-requirement requirement))
+ ((string=? class "SoftwareRequirement")
+ (normalize-software-requirement requirement))
+ (else
+ requirement))))
(coerce-alist->vector requirements "class")))))
(define (normalize-secondary-files secondary-files default-required)
diff --git a/ravanan/store.scm b/ravanan/store.scm
index 5cb4a64..ba66076 100644
--- a/ravanan/store.scm
+++ b/ravanan/store.scm
@@ -18,21 +18,26 @@
(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 utils)
#:use-module (ravanan work vectors)
#:export (%store-files-directory
%store-data-directory
%store-logs-directory
make-store
- script->store-files-directory
- script->store-data-file
- script->store-stdout-file
- script->store-stderr-file
- intern-file
- store-item-name))
+ step-store-files-directory
+ step-store-data-file
+ step-store-stdout-file
+ step-store-stderr-file
+ intern-file))
(define %store-files-directory
"files")
@@ -57,29 +62,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 (canonicalize-json 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)
@@ -96,8 +128,9 @@ interned path and location."
(checksum (assoc-ref file "checksum"))
(sha1 (if (and checksum
(string-prefix? "sha1$" checksum))
- (string-drop checksum (string-length "sha1$"))
- (sha1-hash path)))
+ (base16-string->bytevector
+ (string-drop checksum (string-length "sha1$")))
+ (sha1-hash-bytes path)))
(interned-path
(if (string-prefix? store path)
;; If file is already a store path, return it as is.
@@ -108,9 +141,10 @@ interned path and location."
(let ((interned-path
(expand-file-name
(file-name-join* %store-files-directory
- (string-append sha1
+ (string-append (bytevector->base32-string sha1)
"-"
- (basename path)))
+ (basename path))
+ (basename path))
store)))
(if (file-exists? interned-path)
(format (current-error-port)
@@ -120,6 +154,7 @@ interned path and location."
(format (current-error-port)
"Interning ~a into store as ~a~%"
path interned-path)
+ (mkdir (dirname interned-path))
;; Hard link if on the same filesystem. Else, copy.
((if (same-filesystem? path
(expand-file-name %store-files-directory
@@ -140,11 +175,4 @@ interned path and location."
(just (vector-map (cut intern-file <> store)
secondary-files)))))))
-;; Length of a base-16 encoded SHA1 hash
-(define %store-hash-length 40)
-(define (store-item-name path)
- "Return the basename of store item @var{path} with the store hash stripped out."
- (string-drop (basename path)
- ;; the hash and the dash after the hash
- (1+ %store-hash-length)))
diff --git a/ravanan/utils.scm b/ravanan/utils.scm
index a76a14c..e4fad9c 100644
--- a/ravanan/utils.scm
+++ b/ravanan/utils.scm
@@ -1,5 +1,5 @@
;;; ravanan --- High-reproducibility CWL runner powered by Guix
-;;; Copyright © 2024 Arun Isaac <arunisaac@systemreboot.net>
+;;; Copyright © 2024, 2025 Arun Isaac <arunisaac@systemreboot.net>
;;;
;;; This file is part of ravanan.
;;;
@@ -54,4 +54,4 @@ before loading script."
;; define-module invocation" warning during compilation. But, it is
;; probably safe to ignore this warning since we use load only within a
;; dummy module.
- (load (canonicalize-path script-file))))))
+ (load script-file)))))
diff --git a/ravanan/work/command-line-tool.scm b/ravanan/work/command-line-tool.scm
index d33c80c..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
@@ -39,11 +41,22 @@
formal-parameter-type
run-command
sha1-hash
+ sha1-hash-bytes
checksum
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."
@@ -202,11 +215,14 @@ status in @var{success-codes} as success. Error out otherwise."
(with-output-to-port (current-error-port)
(cut invoke command))))))
+(define (sha1-hash-bytes file)
+ "Return the SHA1 hash of @var{file} as a bytevector."
+ (file-hash (lookup-hash-algorithm 'sha1)
+ file))
+
(define (sha1-hash file)
"Return the SHA1 hash of @var{file} as a hexadecimal string."
- (bytevector->base16-string
- (file-hash (lookup-hash-algorithm 'sha1)
- file)))
+ (bytevector->base16-string (sha1-hash-bytes file)))
(define (checksum file)
"Return the checksum of @var{file} as defined in the CWL specification."
@@ -262,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 2770f31..2de254b 100644
--- a/ravanan/workflow.scm
+++ b/ravanan/workflow.scm
@@ -27,6 +27,8 @@
#:use-module (ice-9 filesystem)
#:use-module (ice-9 match)
#:use-module (web uri)
+ #:use-module (guix inferior)
+ #:use-module (guix store)
#:use-module (ravanan batch-system)
#:use-module (ravanan command-line-tool)
#:use-module (ravanan job-state)
@@ -59,16 +61,28 @@
(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)
+ (-scheduler-proc name script-or-propnet formal-inputs formal-outputs
+ resource-requirement scatter scatter-method)
scheduler-proc?
(name scheduler-proc-name)
- (cwl scheduler-proc-cwl)
+ (script-or-propnet scheduler-proc-script-or-propnet)
+ (formal-inputs scheduler-proc-formal-inputs)
+ (formal-outputs scheduler-proc-formal-outputs)
+ (resource-requirement scheduler-proc-resource-requirement)
(scatter scheduler-proc-scatter)
(scatter-method scheduler-proc-scatter-method))
+(define* (scheduler-proc name script-or-propnet formal-inputs formal-outputs
+ #:optional
+ (resource-requirement %nothing)
+ (scatter %nothing) (scatter-method %nothing))
+ (-scheduler-proc name script-or-propnet formal-inputs formal-outputs
+ resource-requirement scatter scatter-method))
+
(define-immutable-record-type <command-line-tool-state>
(command-line-tool-state job-state formal-outputs)
command-line-tool-state?
@@ -167,67 +181,120 @@ requirements and hints of the step."
(assoc-ref* input "type"))))
(assoc-ref input "id")))
-(define* (command-line-tool->propagator name cwl)
- "Convert @code{CommandLineTool} workflow @var{cwl} of @var{name} to a
-propagator."
- (propagator name
- (scheduler-proc name cwl %nothing %nothing)
- (vector-map->list (lambda (input)
- (cons (assoc-ref input "id")
- (assoc-ref input "id")))
- (assoc-ref cwl "inputs"))
- ;; Inputs that either have a default or accept null values are
- ;; optional.
- (vector-filter-map->list optional-input?
- (assoc-ref cwl "inputs"))
- (vector-map->list (lambda (output)
- (cons (assoc-ref output "id")
- (assoc-ref output "id")))
- (assoc-ref cwl "outputs"))))
-
-(define* (workflow-class->propnet name cwl scheduler batch-system)
+(define* (workflow->scheduler-proc name cwl scheduler
+ manifest-file inferior scratch store
+ batch-system guix-store
+ #:optional
+ (scatter %nothing)
+ (scatter-method %nothing))
+ "Return a @code{<scheduler-proc>} object for @var{cwl} workflow named @var{name}
+scheduled using @var{scheduler}. @var{scatter} and @var{scatter-method} are the
+CWL scattering properties of this step. Build @code{CommandLineTool} workflow
+scripts using @var{guix-store}.
+
+@var{manifest-file}, @var{scratch}, @var{store} and @var{batch-system} are the
+same as in @code{run-workflow}. @var{inferior} is the same as in
+@code{build-command-line-tool-script} from @code{(ravanan command-line-tool)}."
+ (scheduler-proc name
+ (let ((class (assoc-ref* cwl "class")))
+ (cond
+ ((string=? class "CommandLineTool")
+ (run-with-store guix-store
+ (build-command-line-tool-script name
+ manifest-file
+ inferior
+ cwl
+ scratch
+ store
+ batch-system)))
+ ((string=? class "ExpressionTool")
+ (error "Workflow class not implemented yet" class))
+ ((string=? class "Workflow")
+ (workflow-class->propnet cwl
+ scheduler
+ manifest-file
+ inferior
+ scratch
+ store
+ batch-system
+ guix-store))
+ (else
+ (assertion-violation class "Unexpected workflow class"))))
+ (assoc-ref* cwl "inputs")
+ (assoc-ref* cwl "outputs")
+ (find-requirement (inherit-requirements
+ (or (assoc-ref cwl "requirements")
+ #())
+ (or (assoc-ref cwl "hints")
+ #()))
+ "ResourceRequirement")
+ scatter
+ scatter-method))
+
+(define* (workflow-class->propnet cwl scheduler
+ manifest-file inferior scratch store
+ batch-system guix-store)
"Return a propagator network scheduled using @var{scheduler} on
-@var{batch-system} for @var{cwl}, a @code{Workflow} class workflow with
-@var{name}."
+@var{batch-system} for @var{cwl}, a @code{Workflow} class workflow. Build
+@code{CommandLineTool} workflow scripts using @var{guix-store}.
+
+@var{manifest-file}, @var{scratch}, @var{store}, @var{batch-system} and
+@var{guix-daemon-socket} are the same as in @code{run-workflow}. @var{inferior}
+is the same as in @code{build-command-line-tool-script} from @code{(ravanan
+command-line-tool)}."
(define (normalize-scatter-method scatter-method)
(assoc-ref* '(("dotproduct" . dot-product)
("nested_crossproduct" . nested-cross-product)
("flat_crossproduct" . flat-cross-product))
scatter-method))
+ (define (step->scheduler-proc step parent-requirements parent-hints)
+ (let ((run (assoc-ref* step "run")))
+ (workflow->scheduler-proc (assoc-ref* step "id")
+ (inherit-requirements-and-hints
+ run
+ parent-requirements
+ parent-hints
+ (or (assoc-ref step "requirements")
+ #())
+ (or (assoc-ref step "hints")
+ #()))
+ scheduler
+ manifest-file
+ inferior
+ scratch
+ store
+ batch-system
+ guix-store
+ (maybe-assoc-ref (just step) "scatter")
+ (maybe-bind (maybe-assoc-ref (just step) "scatterMethod")
+ (compose just normalize-scatter-method)))))
+
(define (step->propagator step)
- (let* ((step-id (assoc-ref* step "id"))
- (step-propagator
- (command-line-tool->propagator step-id (assoc-ref* step "run"))))
- (propagator (propagator-name step-propagator)
- (let ((proc (propagator-proc step-propagator)))
- (scheduler-proc (scheduler-proc-name proc)
- (inherit-requirements-and-hints
- (scheduler-proc-cwl proc)
- (or (assoc-ref cwl "requirements")
- #())
- (or (assoc-ref cwl "hints")
- #())
- (or (assoc-ref step "requirements")
- #())
- (or (assoc-ref step "hints")
- #()))
- (maybe-assoc-ref (just step) "scatter")
- (maybe-bind (maybe-assoc-ref (just step) "scatterMethod")
- (compose just normalize-scatter-method))))
- (map (match-lambda
- ((input-id . _)
- (cons input-id
- (json-ref step "in" input-id))))
- (propagator-inputs step-propagator))
- (propagator-optional-inputs step-propagator)
- (filter-map (match-lambda
- ((output . cell)
- (and (vector-member output
- (assoc-ref* step "out"))
- (cons output
- (string-append step-id "/" cell)))))
- (propagator-outputs step-propagator)))))
+ (let ((step-id (assoc-ref* step "id"))
+ (run (assoc-ref* step "run")))
+ (propagator step-id
+ (step->scheduler-proc step
+ (or (assoc-ref cwl "requirements")
+ #())
+ (or (assoc-ref cwl "hints")
+ #()))
+ (vector-map->list (lambda (input)
+ (let ((input-id (assoc-ref input "id")))
+ (cons input-id
+ (json-ref step "in" input-id))))
+ (assoc-ref run "inputs"))
+ ;; Inputs that either have a default or accept null values are
+ ;; optional.
+ (vector-filter-map->list optional-input?
+ (assoc-ref run "inputs"))
+ (vector-map->list (lambda (output)
+ (let ((output-id (assoc-ref output "id")))
+ (and (vector-member output-id
+ (assoc-ref* step "out"))
+ (cons output-id
+ (string-append step-id "/" output-id)))))
+ (assoc-ref run "outputs")))))
(maybe-let* ((requirements (maybe-assoc-ref (just cwl) "requirements")))
(check-requirements requirements
@@ -246,26 +313,28 @@ propagator."
merge-values
scheduler))
-(define* (workflow-scheduler manifest-file channels scratch store batch-system
- #:key guix-daemon-socket)
+(define* (workflow-scheduler store batch-system)
(define (schedule proc inputs scheduler)
"Schedule @var{proc} with inputs from the @var{inputs} association list. Return a
-state-monadic job state object. @var{proc} may either be a @code{<propnet>}
-object or a @code{<scheduler-proc>} object."
+state-monadic job state object. @var{proc} must be a @code{<scheduler-proc>}
+object."
(let* ((name (scheduler-proc-name proc))
- (cwl (scheduler-proc-cwl proc))
+ (script-or-propnet (scheduler-proc-script-or-propnet proc))
(scatter (from-maybe (scheduler-proc-scatter proc)
#f))
(scatter-method (from-maybe (scheduler-proc-scatter-method proc)
- #f))
- (class (assoc-ref* cwl "class")))
+ #f)))
(if scatter
(case scatter-method
((dot-product)
(apply state-map
(lambda input-elements
;; Recurse with scattered inputs spliced in.
- (schedule (scheduler-proc name cwl %nothing %nothing)
+ (schedule (scheduler-proc name
+ script-or-propnet
+ (scheduler-proc-formal-inputs proc)
+ (scheduler-proc-formal-outputs proc)
+ (scheduler-proc-resource-requirement proc))
;; Replace scattered inputs with single
;; elements.
(apply assoc-set
@@ -281,38 +350,29 @@ object or a @code{<scheduler-proc>} object."
((nested-cross-product flat-cross-product)
(error scatter-method
"Scatter method not implemented yet")))
- (let* ((formal-inputs (assoc-ref* cwl "inputs"))
- ;; We need to resolve inputs after adding defaults since the
- ;; default values may contain uninterned File objects.
- (inputs (resolve-inputs (add-defaults inputs formal-inputs)
- formal-inputs
- store)))
- (cond
- ((string=? class "CommandLineTool")
- (state-let* ((job-state
- (run-command-line-tool name
- manifest-file
- channels
- cwl
- inputs
- scratch
- store
- batch-system
- #:guix-daemon-socket guix-daemon-socket)))
- (state-return (command-line-tool-state job-state
- (assoc-ref* cwl "outputs")))))
- ((string=? class "ExpressionTool")
- (error "Workflow class not implemented yet" class))
- ((string=? class "Workflow")
- (state-let* ((propnet-state
- (schedule-propnet (workflow-class->propnet name
- cwl
- scheduler
- batch-system)
- inputs)))
+ (if (propnet? script-or-propnet)
+ (state-let* ((propnet-state (schedule-propnet script-or-propnet inputs)))
(state-return
(workflow-state propnet-state
- (assoc-ref* cwl "outputs"))))))))))
+ (scheduler-proc-formal-outputs proc))))
+ (let* ((formal-inputs (scheduler-proc-formal-inputs proc))
+ ;; We need to resolve inputs after adding defaults since
+ ;; the default values may contain uninterned File objects.
+ (inputs (resolve-inputs (add-defaults inputs formal-inputs)
+ formal-inputs
+ store))
+ (resource-requirement
+ (scheduler-proc-resource-requirement proc)))
+ (state-let* ((job-state
+ (run-command-line-tool name
+ script-or-propnet
+ inputs
+ resource-requirement
+ store
+ batch-system)))
+ (state-return
+ (command-line-tool-state job-state
+ (scheduler-proc-formal-outputs proc)))))))))
(define (poll state)
"Return updated state and current status of job @var{state} object as a
@@ -341,7 +401,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 +475,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))
@@ -455,8 +517,8 @@ files found into the @var{store} and return a tree of the fully resolved inputs.
"Return @code{#t} if @var{secondary-file} matches at least one secondary file in
@var{input}."
(vector-any (lambda (candidate)
- (string=? (store-item-name (assoc-ref* candidate "path"))
- (secondary-path (store-item-name (assoc-ref* input "path"))
+ (string=? (basename (assoc-ref* candidate "path"))
+ (secondary-path (basename (assoc-ref* input "path"))
secondary-file)))
(or (assoc-ref input "secondaryFiles")
(user-error "Missing secondaryFiles in input ~a"
@@ -524,6 +586,39 @@ error out."
formal-inputs))
formal-inputs))
+(define (call-with-inferior inferior proc)
+ "Call @var{proc} with @var{inferior} and return the return value of @var{proc}.
+Close @var{inferior} when done, even if @var{proc} exits non-locally."
+ (dynamic-wind (const #t)
+ (cut proc inferior)
+ (cut close-inferior inferior)))
+
+(define* (build-workflow name cwl scheduler
+ manifest-file channels scratch store
+ batch-system
+ #:optional guix-daemon-socket)
+ "Build @var{cwl} workflow named @var{name} into a @code{<scheduler-proc>} object
+scheduled using @var{scheduler}. When @var{guix-daemon-socket} is specified,
+connect to the Guix daemon at that specific socket. Else, connect to the default
+socket.
+
+@var{manifest-file}, @var{channels}, @var{scratch}, @var{store} and
+@var{batch-system} are the same as in @code{run-workflow}."
+ (define builder
+ (cut workflow->scheduler-proc name cwl scheduler
+ manifest-file <> scratch store
+ batch-system <>))
+
+ (if guix-daemon-socket
+ (parameterize ((%daemon-socket-uri guix-daemon-socket))
+ (build-workflow name cwl scheduler manifest-file channels
+ scratch store batch-system))
+ (with-store guix-store
+ (if channels
+ (call-with-inferior (inferior-for-channels channels)
+ (cut builder <> guix-store))
+ (builder #f guix-store)))))
+
(define* (run-workflow name manifest-file channels cwl inputs
scratch store batch-system
#:key guix-daemon-socket)
@@ -537,18 +632,25 @@ 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)))))
- (let ((scheduler (workflow-scheduler
- manifest-file channels scratch store batch-system
- #:guix-daemon-socket guix-daemon-socket)))
+ (step-store-stdout-file script inputs store)
+ (step-store-stderr-file script inputs store)))))
+ (let ((scheduler (workflow-scheduler store batch-system)))
(run-with-state
(let loop ((mstate ((scheduler-schedule scheduler)
- (scheduler-proc name cwl %nothing %nothing)
+ (build-workflow name
+ cwl
+ scheduler
+ manifest-file
+ channels
+ scratch
+ store
+ batch-system
+ guix-daemon-socket)
inputs
scheduler)))
;; Poll.
diff --git a/tests/store.scm b/tests/store.scm
new file mode 100644
index 0000000..f209583
--- /dev/null
+++ b/tests/store.scm
@@ -0,0 +1,80 @@
+;;; ravanan --- High-reproducibility CWL runner powered by Guix
+;;; Copyright © 2025 Arun Isaac <arunisaac@systemreboot.net>
+;;;
+;;; This file is part of ravanan.
+;;;
+;;; ravanan is free software: you can redistribute it and/or modify it
+;;; under the terms of the GNU General Public License as published by
+;;; the Free Software Foundation, either version 3 of the License, or
+;;; (at your option) any later version.
+;;;
+;;; ravanan is distributed in the hope that it will be useful, but
+;;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+;;; General Public License for more details.
+;;;
+;;; You should have received a copy of the GNU General Public License
+;;; along with ravanan. If not, see <https://www.gnu.org/licenses/>.
+
+(use-modules (srfi srfi-64)
+ (ravanan store))
+
+(test-begin "store")
+
+(test-equal "step-store-files-directory must be insensitive to order of inputs"
+ (step-store-files-directory "/gnu/store/hl96c0xd19ngvl8rf4cyw452rpgqsi1b-foo"
+ '(("foo" . 1)
+ ("foobar" . 3)
+ ("bar" . (("aal" . 1)
+ ("vel" . 2))))
+ "store")
+ (step-store-files-directory "/gnu/store/hl96c0xd19ngvl8rf4cyw452rpgqsi1b-foo"
+ '(("foo" . 1)
+ ("bar" . (("vel" . 2)
+ ("aal" . 1)))
+ ("foobar" . 3))
+ "store"))
+
+(test-equal "step-store-data-file must be insensitive to order of inputs"
+ (step-store-data-file "/gnu/store/hl96c0xd19ngvl8rf4cyw452rpgqsi1b-foo"
+ '(("foo" . 1)
+ ("foobar" . 3)
+ ("bar" . (("aal" . 1)
+ ("vel" . 2))))
+ "store")
+ (step-store-data-file "/gnu/store/hl96c0xd19ngvl8rf4cyw452rpgqsi1b-foo"
+ '(("foo" . 1)
+ ("bar" . (("vel" . 2)
+ ("aal" . 1)))
+ ("foobar" . 3))
+ "store"))
+
+(test-equal "step-store-stdout-file must be insensitive to order of inputs"
+ (step-store-stdout-file "/gnu/store/hl96c0xd19ngvl8rf4cyw452rpgqsi1b-foo"
+ '(("foo" . 1)
+ ("foobar" . 3)
+ ("bar" . (("aal" . 1)
+ ("vel" . 2))))
+ "store")
+ (step-store-stdout-file "/gnu/store/hl96c0xd19ngvl8rf4cyw452rpgqsi1b-foo"
+ '(("foo" . 1)
+ ("bar" . (("vel" . 2)
+ ("aal" . 1)))
+ ("foobar" . 3))
+ "store"))
+
+(test-equal "step-store-stderr-file must be insensitive to order of inputs"
+ (step-store-stderr-file "/gnu/store/hl96c0xd19ngvl8rf4cyw452rpgqsi1b-foo"
+ '(("foo" . 1)
+ ("foobar" . 3)
+ ("bar" . (("aal" . 1)
+ ("vel" . 2))))
+ "store")
+ (step-store-stderr-file "/gnu/store/hl96c0xd19ngvl8rf4cyw452rpgqsi1b-foo"
+ '(("foo" . 1)
+ ("bar" . (("vel" . 2)
+ ("aal" . 1)))
+ ("foobar" . 3))
+ "store"))
+
+(test-end "store")