#!/bin/sh # Aside from this initial boilerplate, this is actually -*- scheme -*- code. main="(module-ref (resolve-interface '(hydra-eval-guile-jobs)) 'eval-guile-jobs)" # Keep the host's GUILE_LOAD_PATH unchanged to allow the installed Guix to # be used. This moves Guix modules possibly out of control, but solves # bootstrapping issues. # # Use `--fresh-auto-compile' to ignore any available .go, and force # recompilation. This is because checkouts in the store has mtime set to # the epoch, and thus .go files look newer, even though they may not # correspond. exec ${GUILE:-@GUILE@} --no-auto-compile --fresh-auto-compile \ -l "$0" -c "(apply $main (cdr (command-line)))" "$@" !# ;;; Copyright © 2012, 2013, 2014 Ludovic Courtès ;;; ;;; This file is part of Hydra. ;;; ;;; Hydra 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. ;;; ;;; Hydra 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 Hydra. If not, see . (define-module (hydra-eval-guile-jobs) #:use-module (sxml simple) #:use-module (ice-9 match) #:use-module (ice-9 regex) #:use-module (srfi srfi-1) #:use-module (srfi srfi-11) #:export (job-evaluations->xml eval-guile-jobs)) (define (guix-variable module name) "Dynamically link variable NAME under Guix module MODULE and return it. Note: this is used instead of `@', because when using `@' in an uncompiled file, Guile tries to load the module directly as it reads the source, which fails in our case, leading to the creation of empty (guix ...) modules." ;; TODO: fail with an XML error description (let ((m (resolve-interface `(guix ,module)))) (module-ref m name))) (define (%derivation-system drv) ;; XXX: Awful hack to workaround the fact that `derivation-system', which ;; is a macro, cannot be referred to dynamically. (struct-ref drv 3)) (define strip-store-path (let* ((store (or (getenv "NIX_STORE_DIR") "/nix/store")) (store-path-rx (make-regexp (string-append "^.*" (regexp-quote store) "/[^-]+-(.+)$")))) (lambda (path) (or (and=> (regexp-exec store-path-rx path) (lambda (match) (let ((path (match:substring match 1))) path))) path)))) (define (derivation-path->name drv) "Return the base name of DRV, sans hash and `.drv' extension." (let ((d (strip-store-path drv))) (if (string-suffix? ".drv" d) (string-drop-right d 4) d))) (define (register-gc-root drv roots-dir) "Register a permanent garbage collector root under ROOTS-DIR for DRV." (let ((root (string-append roots-dir "/" (basename drv)))) (unless (file-exists? root) (symlink drv root)))) (define* (job-evaluations->sxml jobs #:key gc-roots-dir) "Return the hydra-eval-jobs SXML form for the result of JOBS, a list of symbol/thunk pairs." `(*TOP* (*PI* xml "version='1.0' encoding='utf-8'") "\n" (jobs "\n" ,@(map (match-lambda (((? symbol? name) . (? thunk? thunk)) (let* ((result (save-module-excursion (lambda () (set-current-module %user-module) (with-output-to-port (%make-void-port "w") thunk)))) (drv (assoc-ref result 'derivation))) (define (opt-attr xml-name name) (match (assoc name result) ((_ . value) `((,xml-name ,value))) (_ '()))) (when gc-roots-dir ;; Register DRV as a GC root so that it's not collected by ;; the time 'hydra-queue-runner' attempts to build it. (register-gc-root drv gc-roots-dir)) ;; XXX: Add tags? `(job (@ (jobName ,name) (drvPath ,drv) ,@(opt-attr 'homepage 'home-page) (license ,(let loop ((license (assoc-ref result 'license))) (match license ((? struct?) (struct-ref license 0)) ((l ...) (string-join (map loop l))) (_ "")))) ,@(opt-attr 'description 'description) (maintainers ,(string-join (or (assoc-ref result 'maintainers) '()) ", ")) (maxSilent ,(number->string (or (assoc-ref result 'max-silent-time) 3600))) (timeout ,(number->string (or (assoc-ref result 'timeout) 72000))) (nixName ,(derivation-path->name drv)) (schedulingPriority ,(number->string (or (assoc-ref result 'scheduling-priority) 10))) (system ,(call-with-input-file drv (compose %derivation-system (guix-variable 'derivations 'read-derivation))))) ;; Resolve Guix modules lazily. ,(map (match-lambda ((name . path) `(output (@ (name ,name) (path ,path))))) ((guix-variable 'derivations 'derivation-path->output-paths) drv)) "\n")))) jobs)))) (define* (job-evaluations->xml jobs port #:key gc-roots-dir) (set-port-encoding! port "UTF-8") (sxml->xml (job-evaluations->sxml jobs #:gc-roots-dir gc-roots-dir) port)) ;;; ;;; Command-line entry point. ;;; (define (parse-arguments args) "Traverse ARGS, a list of command-line arguments compatible with `hydra-eval-jobs', and return the name of the file that defines the jobs, an expression that returns the entry point in that file (a unary procedure), the list of name/value pairs passed to that entry point, as well as a GC root directory or #f." (define (module-directory dir) (let ((d (string-append dir "/share/guile/site/2.0"))) (if (file-exists? d) d dir))) (let loop ((args args) (result '()) (file #f) (entry 'hydra-jobs) (roots-dir #f)) (match args (() (if (not file) (error "hydra-eval-guile-jobs: no expression file given") (values file entry (reverse result) roots-dir))) (("-I" name=dir rest ...) (let* ((dir (match (string-tokenize name=dir (char-set-complement (char-set #\=))) ((_ dir) dir) ((dir) dir))) (dir* (module-directory dir))) (format (current-error-port) "adding `~a' to the load path~%" dir*) (set! %load-path (cons dir* %load-path)) (set! %load-compiled-path (cons dir* %load-compiled-path))) (loop rest result file entry roots-dir)) (("--argstr" name value rest ...) (loop rest (alist-cons (string->symbol name) value result) file entry roots-dir)) (("--arg" name expr rest ...) (let ((value (eval (call-with-input-string expr read) (current-module)))) (loop rest (alist-cons (string->symbol name) value result) file entry roots-dir))) (("--gc-roots-dir" dir rest ...) (loop rest result file entry dir)) (("-j" _ rest ...) ; XXX: what's this? (loop rest result file entry roots-dir)) (("--entry" expr rest ...) ; entry point, like `guile -e' (let ((expr (call-with-input-string expr read))) (loop rest result file expr roots-dir))) ((file rest ...) ; source file that defines the jobs (loop rest result file entry roots-dir)) (_ (error "hydra-eval-guile-jobs: invalid arguments" args))))) (define %user-module ;; Hydra user module. ;; TODO: Make it a sandbox. (let ((m (make-module))) (beautify-user-module! m) m)) (define (eval-guile-jobs . args) (setlocale LC_ALL "") (let-values (((file entry args gc-roots-dir) (parse-arguments args))) (save-module-excursion (lambda () (set-current-module %user-module) ;; The standard output must contain only XML. (with-output-to-port (%make-void-port "w") (lambda () (primitive-load file))))) (let* ((entry (eval entry %user-module)) (store ((guix-variable 'store 'open-connection))) (jobs (entry store args))) (unless (string? gc-roots-dir) (format (current-error-port) "warning: --gc-roots-dir not specified~%")) (job-evaluations->xml jobs (current-output-port) #:gc-roots-dir gc-roots-dir))))