123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332 |
- ;;; GNU Guix --- Functional package management for GNU
- ;;; Copyright © 2015 David Thompson <davet@gnu.org>
- ;;; Copyright © 2015 Pjotr Prins <pjotr.public01@thebird.nl>
- ;;; Copyright © 2015, 2016 Ben Woodcroft <donttrustben@gmail.com>
- ;;;
- ;;; This file is part of GNU Guix.
- ;;;
- ;;; GNU Guix 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.
- ;;;
- ;;; GNU Guix 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 GNU Guix. If not, see <http://www.gnu.org/licenses/>.
- (define-module (guix build ruby-build-system)
- #:use-module ((guix build gnu-build-system) #:prefix gnu:)
- #:use-module (guix build utils)
- #:use-module (ice-9 ftw)
- #:use-module (ice-9 match)
- #:use-module (ice-9 popen)
- #:use-module (ice-9 rdelim)
- #:use-module (ice-9 regex)
- #:use-module (srfi srfi-1)
- #:use-module (srfi srfi-26)
- #:export (%standard-phases
- ruby-build))
- ;; Commentary:
- ;;
- ;; Builder-side code of the standard Ruby package build procedure.
- ;;
- ;; Code:
- (define (first-matching-file pattern)
- "Return the first file name that matches PATTERN in the current working
- directory."
- (match (find-files "." pattern)
- ((file-name . _) file-name)
- (() (error "No files matching pattern: " pattern))))
- (define gnu:unpack (assq-ref gnu:%standard-phases 'unpack))
- (define (gem-archive? file-name)
- (string-match "^.*\\.gem$" file-name))
- (define* (unpack #:key source #:allow-other-keys)
- "Unpack the gem SOURCE and enter the resulting directory."
- (if (gem-archive? source)
- (begin
- (invoke "gem" "unpack" source)
- ;; The unpacked gem directory is named the same as the archive,
- ;; sans the ".gem" extension. It is renamed to simply "gem" in an
- ;; effort to keep file names shorter to avoid UNIX-domain socket
- ;; file names and shebangs that exceed the system's fixed maximum
- ;; length when running test suites.
- (let ((dir (match:substring (string-match "^(.*)\\.gem$"
- (basename source))
- 1)))
- (rename-file dir "gem")
- (chdir "gem"))
- #t)
- ;; Use GNU unpack strategy for things that aren't gem archives.
- (gnu:unpack #:source source)))
- (define (first-gemspec)
- (first-matching-file "\\.gemspec$"))
- (define* (replace-git-ls-files #:key source #:allow-other-keys)
- "Many gemspec files downloaded from outside rubygems.org use `git ls-files`
- to list of the files to be included in the built gem. However, since this
- operation is not deterministic, we replace it with `find`."
- (when (not (gem-archive? source))
- (let ((gemspec (first-gemspec)))
- (substitute* gemspec
- (("`git ls-files`") "`find . -type f |sort`")
- (("`git ls-files -z`") "`find . -type f -print0 |sort -z`"))))
- #t)
- (define* (extract-gemspec #:key source #:allow-other-keys)
- "Remove the original gemspec, if present, and replace it with a new one.
- This avoids issues with upstream gemspecs requiring tools such as git to
- generate the files list."
- (if (gem-archive? source)
- (let ((gemspec (or (false-if-exception (first-gemspec))
- ;; Make new gemspec if one wasn't shipped.
- ".gemspec")))
- (when (file-exists? gemspec) (delete-file gemspec))
- ;; Extract gemspec from source gem.
- (let ((pipe (open-pipe* OPEN_READ "gem" "spec" "--ruby" source)))
- (dynamic-wind
- (const #t)
- (lambda ()
- (call-with-output-file gemspec
- (lambda (out)
- ;; 'gem spec' writes to stdout, but 'gem build' only reads
- ;; gemspecs from a file, so we redirect the output to a file.
- (while (not (eof-object? (peek-char pipe)))
- (write-char (read-char pipe) out))))
- #t)
- (lambda ()
- (close-pipe pipe)))))
- (display "extract-gemspec: skipping as source is not a gem archive\n"))
- #t)
- (define* (build #:key source #:allow-other-keys)
- "Build a new gem using the gemspec from the SOURCE gem."
- ;; Build a new gem from the current working directory. This also allows any
- ;; dynamic patching done in previous phases to be present in the installed
- ;; gem.
- (invoke "gem" "build" (first-gemspec)))
- (define* (check #:key tests? test-target #:allow-other-keys)
- "Run the gem's test suite rake task TEST-TARGET. Skip the tests if TESTS?
- is #f."
- (if tests?
- (invoke "rake" test-target)
- #t))
- (define* (install #:key inputs outputs (gem-flags '())
- #:allow-other-keys)
- "Install the gem archive SOURCE to the output store item. Additional
- GEM-FLAGS are passed to the 'gem' invocation, if present."
- (let* ((ruby-version
- (match:substring (string-match "ruby-(.*)\\.[0-9]$"
- (assoc-ref inputs "ruby"))
- 1))
- (out (assoc-ref outputs "out"))
- (vendor-dir (string-append out "/lib/ruby/vendor_ruby"))
- (gem-file (first-matching-file "\\.gem$"))
- (gem-file-basename (basename gem-file))
- (gem-name (substring gem-file-basename
- 0
- (- (string-length gem-file-basename) 4)))
- (gem-dir (string-append vendor-dir "/gems/" gem-name)))
- (setenv "GEM_VENDOR" vendor-dir)
- (or (zero?
- ;; 'zero? system*' allows the custom error handling to function as
- ;; expected, while 'invoke' raises its own exception.
- (apply system* "gem" "install" gem-file
- "--verbose"
- "--local" "--ignore-dependencies" "--vendor"
- ;; Executables should go into /bin, not
- ;; /lib/ruby/gems.
- "--bindir" (string-append out "/bin")
- gem-flags))
- (begin
- (let ((failed-output-dir (string-append (getcwd) "/out")))
- (mkdir failed-output-dir)
- (copy-recursively out failed-output-dir))
- (error "installation failed")))
- ;; Remove the cached gem file as this is unnecessary and contains
- ;; timestamped files rendering builds not reproducible.
- (let ((cached-gem (string-append vendor-dir "/cache/" gem-file)))
- (log-file-deletion cached-gem)
- (delete-file cached-gem))
- ;; For gems with native extensions, several Makefile-related files
- ;; are created that contain timestamps or other elements making
- ;; them not reproducible. They are unnecessary so we remove them.
- (when (file-exists? (string-append gem-dir "/ext"))
- (for-each (lambda (file)
- (log-file-deletion file)
- (delete-file file))
- (append
- (find-files (string-append vendor-dir "/doc")
- "page-Makefile.ri")
- (find-files (string-append vendor-dir "/extensions")
- "gem_make.out")
- (find-files (string-append gem-dir "/ext")
- "Makefile"))))
- #t))
- (define* (wrap-ruby-program prog #:key (gem-clear-paths #t) #:rest vars)
- "Make a wrapper for PROG. VARS should look like this:
- '(VARIABLE DELIMITER POSITION LIST-OF-DIRECTORIES)
- where DELIMITER is optional. ':' will be used if DELIMITER is not given.
- For example, this command:
- (wrap-ruby-program \"foo\"
- '(\"PATH\" \":\" = (\"/gnu/.../bar/bin\"))
- '(\"CERT_PATH\" suffix (\"/gnu/.../baz/certs\"
- \"/qux/certs\")))
- will copy 'foo' to '.real/fool' and create the file 'foo' with the following
- contents:
- #!location/of/bin/ruby
- ENV['PATH'] = \"/gnu/.../bar/bin\"
- ENV['CERT_PATH'] = (ENV.key?('CERT_PATH') ? (ENV['CERT_PATH'] + ':') : '') + '/gnu/.../baz/certs:/qux/certs'
- load location/of/.real/foo
- This is useful for scripts that expect particular programs to be in $PATH, for
- programs that expect particular gems to be in the GEM_PATH.
- This is preferable to wrap-program, which uses a bash script, as this prevents
- ruby scripts from being executed with @command{ruby -S ...}.
- If PROG has previously been wrapped by 'wrap-ruby-program', the wrapper is
- extended with definitions for VARS."
- (define wrapped-file
- (string-append (dirname prog) "/.real/" (basename prog)))
- (define already-wrapped?
- (file-exists? wrapped-file))
- (define (last-line port)
- ;; Return the last line read from PORT and leave PORT's cursor right
- ;; before it.
- (let loop ((previous-line-offset 0)
- (previous-line "")
- (position (seek port 0 SEEK_CUR)))
- (match (read-line port 'concat)
- ((? eof-object?)
- (seek port previous-line-offset SEEK_SET)
- previous-line)
- ((? string? line)
- (loop position line (+ (string-length line) position))))))
- (define (export-variable lst)
- ;; Return a string that exports an environment variable.
- (match lst
- ((var sep '= rest)
- (format #f "ENV['~a'] = '~a'"
- var (string-join rest sep)))
- ((var sep 'prefix rest)
- (format #f "ENV['~a'] = '~a' + (ENV.key?('~a') ? ('~a' + ENV['~a']) : '')"
- var (string-join rest sep) var sep var))
- ((var sep 'suffix rest)
- (format #f "ENV['~a'] = (ENV.key?('~a') ? (ENV['~a'] + '~a') : '') + '~a'"
- var var var sep (string-join rest sep)))
- ((var '= rest)
- (format #f "ENV['~a'] = '~a'"
- var (string-join rest ":")))
- ((var 'prefix rest)
- (format #f "ENV['~a'] = '~a' + (ENV.key?('~a') ? (':' + ENV['~a']) : '')"
- var (string-join rest ":") var var))
- ((var 'suffix rest)
- (format #f "ENV['~a'] = (ENV.key?('~a') ? (ENV['~a'] + ':') : '') + '~a'"
- var var var (string-join rest ":")))))
- (if already-wrapped?
- ;; PROG is already a wrapper: add the new "export VAR=VALUE" lines just
- ;; before the last line.
- (let* ((port (open-file prog "r+"))
- (last (last-line port)))
- (for-each (lambda (var)
- (display (export-variable var) port)
- (newline port))
- vars)
- (display last port)
- (close-port port))
- ;; PROG is not wrapped yet: create a shell script that sets VARS.
- (let ((prog-tmp (string-append wrapped-file "-tmp")))
- (mkdir-p (dirname prog-tmp))
- (link prog wrapped-file)
- (call-with-output-file prog-tmp
- (lambda (port)
- (format port
- "#!~a~%~a~%~a~%load '~a'~%"
- (which "ruby")
- (string-join (map export-variable vars) "\n")
- ;; This ensures that if the GEM_PATH has been changed,
- ;; then that change will be noticed.
- (if gem-clear-paths "Gem.clear_paths" "")
- (canonicalize-path wrapped-file))))
- (chmod prog-tmp #o755)
- (rename-file prog-tmp prog))))
- (define* (wrap #:key inputs outputs #:allow-other-keys)
- (define (list-of-files dir)
- (map (cut string-append dir "/" <>)
- (or (scandir dir (lambda (f)
- (let ((s (stat (string-append dir "/" f))))
- (eq? 'regular (stat:type s)))))
- '())))
- (define bindirs
- (append-map (match-lambda
- ((_ . dir)
- (list (string-append dir "/bin")
- (string-append dir "/sbin"))))
- outputs))
- (let* ((out (assoc-ref outputs "out"))
- (var `("GEM_PATH" prefix
- (,(string-append out "/lib/ruby/vendor_ruby")
- ,(getenv "GEM_PATH")))))
- (for-each (lambda (dir)
- (let ((files (list-of-files dir)))
- (for-each (cut wrap-ruby-program <> var)
- files)))
- bindirs))
- #t)
- (define (log-file-deletion file)
- (display (string-append "deleting '" file "' for reproducibility\n")))
- (define %standard-phases
- (modify-phases gnu:%standard-phases
- (delete 'bootstrap)
- (delete 'configure)
- (replace 'unpack unpack)
- (add-before 'build 'extract-gemspec extract-gemspec)
- (add-after 'extract-gemspec 'replace-git-ls-files replace-git-ls-files)
- (replace 'build build)
- (replace 'check check)
- (replace 'install install)
- (add-after 'install 'wrap wrap)))
- (define* (ruby-build #:key inputs (phases %standard-phases)
- #:allow-other-keys #:rest args)
- (apply gnu:gnu-build #:inputs inputs #:phases phases args))
|