ruby-build-system.scm 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. ;;; GNU Guix --- Functional package management for GNU
  2. ;;; Copyright © 2015 David Thompson <davet@gnu.org>
  3. ;;; Copyright © 2015 Pjotr Prins <pjotr.public01@thebird.nl>
  4. ;;; Copyright © 2015, 2016 Ben Woodcroft <donttrustben@gmail.com>
  5. ;;; Copyright © 2020 Maxim Cournoyer <maxim.cournoyer@gmail.com>
  6. ;;;
  7. ;;; This file is part of GNU Guix.
  8. ;;;
  9. ;;; GNU Guix is free software; you can redistribute it and/or modify it
  10. ;;; under the terms of the GNU General Public License as published by
  11. ;;; the Free Software Foundation; either version 3 of the License, or (at
  12. ;;; your option) any later version.
  13. ;;;
  14. ;;; GNU Guix is distributed in the hope that it will be useful, but
  15. ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
  16. ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. ;;; GNU General Public License for more details.
  18. ;;;
  19. ;;; You should have received a copy of the GNU General Public License
  20. ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
  21. (define-module (guix build ruby-build-system)
  22. #:use-module ((guix build gnu-build-system) #:prefix gnu:)
  23. #:use-module (guix build utils)
  24. #:use-module (ice-9 ftw)
  25. #:use-module (ice-9 match)
  26. #:use-module (ice-9 popen)
  27. #:use-module (ice-9 rdelim)
  28. #:use-module (ice-9 regex)
  29. #:use-module (srfi srfi-1)
  30. #:use-module (srfi srfi-26)
  31. #:export (%standard-phases
  32. ruby-build))
  33. ;; Commentary:
  34. ;;
  35. ;; Builder-side code of the standard Ruby package build procedure.
  36. ;;
  37. ;; Code:
  38. (define (first-matching-file pattern)
  39. "Return the first file name that matches PATTERN in the current working
  40. directory."
  41. (match (find-files "." pattern)
  42. ((file-name . _) file-name)
  43. (() (error "No files matching pattern: " pattern))))
  44. (define gnu:unpack (assq-ref gnu:%standard-phases 'unpack))
  45. (define (gem-archive? file-name)
  46. (string-match "^.*\\.gem$" file-name))
  47. (define* (unpack #:key source #:allow-other-keys)
  48. "Unpack the gem SOURCE and enter the resulting directory."
  49. (if (gem-archive? source)
  50. (begin
  51. (invoke "gem" "unpack" source)
  52. ;; The unpacked gem directory is named the same as the archive,
  53. ;; sans the ".gem" extension. It is renamed to simply "gem" in an
  54. ;; effort to keep file names shorter to avoid UNIX-domain socket
  55. ;; file names and shebangs that exceed the system's fixed maximum
  56. ;; length when running test suites.
  57. (let ((dir (match:substring (string-match "^(.*)\\.gem$"
  58. (basename source))
  59. 1)))
  60. (rename-file dir "gem")
  61. (chdir "gem"))
  62. #t)
  63. ;; Use GNU unpack strategy for things that aren't gem archives.
  64. (gnu:unpack #:source source)))
  65. (define (first-gemspec)
  66. (first-matching-file "\\.gemspec$"))
  67. (define* (replace-git-ls-files #:key source #:allow-other-keys)
  68. "Many gemspec files downloaded from outside rubygems.org use `git ls-files`
  69. to list the files to be included in the built gem. However, since this
  70. operation is not deterministic, we replace it with `find`."
  71. (unless (gem-archive? source)
  72. (let ((gemspec (first-gemspec)))
  73. ;; Do not include the freshly built .gem itself as it causes problems.
  74. ;; Strip the first 2 characters ("./") to more exactly match the output
  75. ;; given by 'git ls-files'. This is useful to prevent breaking regexps
  76. ;; that could be used to filter the list of files.
  77. (substitute* gemspec
  78. (("`git ls-files`")
  79. "`find . -type f -not -regex '.*\\.gem$' | sort | cut -c3-`")
  80. (("`git ls-files -z`")
  81. "`find . -type f -not -regex '.*\\.gem$' -print0 | sort -z | cut -zc3-`"))))
  82. #t)
  83. (define* (extract-gemspec #:key source #:allow-other-keys)
  84. "Remove the original gemspec, if present, and replace it with a new one.
  85. This avoids issues with upstream gemspecs requiring tools such as git to
  86. generate the files list."
  87. (if (gem-archive? source)
  88. (let ((gemspec (or (false-if-exception (first-gemspec))
  89. ;; Make new gemspec if one wasn't shipped.
  90. ".gemspec")))
  91. (when (file-exists? gemspec) (delete-file gemspec))
  92. ;; Extract gemspec from source gem.
  93. (let ((pipe (open-pipe* OPEN_READ "gem" "spec" "--ruby" source)))
  94. (dynamic-wind
  95. (const #t)
  96. (lambda ()
  97. (call-with-output-file gemspec
  98. (lambda (out)
  99. ;; 'gem spec' writes to stdout, but 'gem build' only reads
  100. ;; gemspecs from a file, so we redirect the output to a file.
  101. (while (not (eof-object? (peek-char pipe)))
  102. (write-char (read-char pipe) out))))
  103. #t)
  104. (lambda ()
  105. (close-pipe pipe)))))
  106. (display "extract-gemspec: skipping as source is not a gem archive\n"))
  107. #t)
  108. (define* (build #:key source #:allow-other-keys)
  109. "Build a new gem using the gemspec from the SOURCE gem."
  110. ;; Build a new gem from the current working directory. This also allows any
  111. ;; dynamic patching done in previous phases to be present in the installed
  112. ;; gem.
  113. (invoke "gem" "build" (first-gemspec)))
  114. (define* (check #:key tests? test-target #:allow-other-keys)
  115. "Run the gem's test suite rake task TEST-TARGET. Skip the tests if TESTS?
  116. is #f."
  117. (if tests?
  118. (invoke "rake" test-target)
  119. #t))
  120. (define* (install #:key inputs outputs (gem-flags '())
  121. #:allow-other-keys)
  122. "Install the gem archive SOURCE to the output store item. Additional
  123. GEM-FLAGS are passed to the 'gem' invocation, if present."
  124. (let* ((out (assoc-ref outputs "out"))
  125. (vendor-dir (string-append out "/lib/ruby/vendor_ruby"))
  126. (gem-file (first-matching-file "\\.gem$"))
  127. (gem-file-basename (basename gem-file))
  128. (gem-name (substring gem-file-basename
  129. 0
  130. (- (string-length gem-file-basename) 4)))
  131. (gem-dir (string-append vendor-dir "/gems/" gem-name)))
  132. (setenv "GEM_VENDOR" vendor-dir)
  133. (or (zero?
  134. ;; 'zero? system*' allows the custom error handling to function as
  135. ;; expected, while 'invoke' raises its own exception.
  136. (apply system* "gem" "install" gem-file
  137. "--verbose"
  138. "--local" "--ignore-dependencies" "--vendor"
  139. ;; Executables should go into /bin, not
  140. ;; /lib/ruby/gems.
  141. "--bindir" (string-append out "/bin")
  142. gem-flags))
  143. (begin
  144. (let ((failed-output-dir (string-append (getcwd) "/out")))
  145. (mkdir failed-output-dir)
  146. (copy-recursively out failed-output-dir))
  147. (error "installation failed")))
  148. ;; Remove the cached gem file as this is unnecessary and contains
  149. ;; timestamped files rendering builds not reproducible.
  150. (let ((cached-gem (string-append vendor-dir "/cache/" gem-file)))
  151. (log-file-deletion cached-gem)
  152. (delete-file cached-gem))
  153. ;; For gems with native extensions, several Makefile-related files
  154. ;; are created that contain timestamps or other elements making
  155. ;; them not reproducible. They are unnecessary so we remove them.
  156. (when (file-exists? (string-append gem-dir "/ext"))
  157. (for-each (lambda (file)
  158. (log-file-deletion file)
  159. (delete-file file))
  160. (append
  161. (find-files (string-append vendor-dir "/doc")
  162. "page-Makefile.ri")
  163. (find-files (string-append vendor-dir "/extensions")
  164. "gem_make.out")
  165. (find-files (string-append gem-dir "/ext")
  166. "Makefile"))))
  167. #t))
  168. (define* (wrap-ruby-program prog #:key (gem-clear-paths #t) #:rest vars)
  169. "Make a wrapper for PROG. VARS should look like this:
  170. '(VARIABLE DELIMITER POSITION LIST-OF-DIRECTORIES)
  171. where DELIMITER is optional. ':' will be used if DELIMITER is not given.
  172. For example, this command:
  173. (wrap-ruby-program \"foo\"
  174. '(\"PATH\" \":\" = (\"/gnu/.../bar/bin\"))
  175. '(\"CERT_PATH\" suffix (\"/gnu/.../baz/certs\"
  176. \"/qux/certs\")))
  177. will copy 'foo' to '.real/fool' and create the file 'foo' with the following
  178. contents:
  179. #!location/of/bin/ruby
  180. ENV['PATH'] = \"/gnu/.../bar/bin\"
  181. ENV['CERT_PATH'] = (ENV.key?('CERT_PATH') ? (ENV['CERT_PATH'] + ':') : '') + '/gnu/.../baz/certs:/qux/certs'
  182. load location/of/.real/foo
  183. This is useful for scripts that expect particular programs to be in $PATH, for
  184. programs that expect particular gems to be in the GEM_PATH.
  185. This is preferable to wrap-program, which uses a bash script, as this prevents
  186. ruby scripts from being executed with @command{ruby -S ...}.
  187. If PROG has previously been wrapped by 'wrap-ruby-program', the wrapper is
  188. extended with definitions for VARS."
  189. (define wrapped-file
  190. (string-append (dirname prog) "/.real/" (basename prog)))
  191. (define already-wrapped?
  192. (file-exists? wrapped-file))
  193. (define (last-line port)
  194. ;; Return the last line read from PORT and leave PORT's cursor right
  195. ;; before it.
  196. (let loop ((previous-line-offset 0)
  197. (previous-line "")
  198. (position (seek port 0 SEEK_CUR)))
  199. (match (read-line port 'concat)
  200. ((? eof-object?)
  201. (seek port previous-line-offset SEEK_SET)
  202. previous-line)
  203. ((? string? line)
  204. (loop position line (+ (string-length line) position))))))
  205. (define (export-variable lst)
  206. ;; Return a string that exports an environment variable.
  207. (match lst
  208. ((var sep '= rest)
  209. (format #f "ENV['~a'] = '~a'"
  210. var (string-join rest sep)))
  211. ((var sep 'prefix rest)
  212. (format #f "ENV['~a'] = '~a' + (ENV.key?('~a') ? ('~a' + ENV['~a']) : '')"
  213. var (string-join rest sep) var sep var))
  214. ((var sep 'suffix rest)
  215. (format #f "ENV['~a'] = (ENV.key?('~a') ? (ENV['~a'] + '~a') : '') + '~a'"
  216. var var var sep (string-join rest sep)))
  217. ((var '= rest)
  218. (format #f "ENV['~a'] = '~a'"
  219. var (string-join rest ":")))
  220. ((var 'prefix rest)
  221. (format #f "ENV['~a'] = '~a' + (ENV.key?('~a') ? (':' + ENV['~a']) : '')"
  222. var (string-join rest ":") var var))
  223. ((var 'suffix rest)
  224. (format #f "ENV['~a'] = (ENV.key?('~a') ? (ENV['~a'] + ':') : '') + '~a'"
  225. var var var (string-join rest ":")))))
  226. (if already-wrapped?
  227. ;; PROG is already a wrapper: add the new "export VAR=VALUE" lines just
  228. ;; before the last line.
  229. (let* ((port (open-file prog "r+"))
  230. (last (last-line port)))
  231. (for-each (lambda (var)
  232. (display (export-variable var) port)
  233. (newline port))
  234. vars)
  235. (display last port)
  236. (close-port port))
  237. ;; PROG is not wrapped yet: create a shell script that sets VARS.
  238. (let ((prog-tmp (string-append wrapped-file "-tmp")))
  239. (mkdir-p (dirname prog-tmp))
  240. (link prog wrapped-file)
  241. (call-with-output-file prog-tmp
  242. (lambda (port)
  243. (format port
  244. "#!~a~%~a~%~a~%load '~a'~%"
  245. (which "ruby")
  246. (string-join (map export-variable vars) "\n")
  247. ;; This ensures that if the GEM_PATH has been changed,
  248. ;; then that change will be noticed.
  249. (if gem-clear-paths "Gem.clear_paths" "")
  250. (canonicalize-path wrapped-file))))
  251. (chmod prog-tmp #o755)
  252. (rename-file prog-tmp prog))))
  253. (define* (wrap #:key inputs outputs #:allow-other-keys)
  254. (define (list-of-files dir)
  255. (map (cut string-append dir "/" <>)
  256. (or (scandir dir (lambda (f)
  257. (let ((s (stat (string-append dir "/" f))))
  258. (eq? 'regular (stat:type s)))))
  259. '())))
  260. (define bindirs
  261. (append-map (match-lambda
  262. ((_ . dir)
  263. (list (string-append dir "/bin")
  264. (string-append dir "/sbin"))))
  265. outputs))
  266. (let* ((out (assoc-ref outputs "out"))
  267. (var `("GEM_PATH" prefix
  268. (,(string-append out "/lib/ruby/vendor_ruby")
  269. ,(getenv "GEM_PATH")))))
  270. (for-each (lambda (dir)
  271. (let ((files (list-of-files dir)))
  272. (for-each (cut wrap-ruby-program <> var)
  273. files)))
  274. bindirs))
  275. #t)
  276. (define (log-file-deletion file)
  277. (display (string-append "deleting '" file "' for reproducibility\n")))
  278. (define %standard-phases
  279. (modify-phases gnu:%standard-phases
  280. (delete 'bootstrap)
  281. (delete 'configure)
  282. (replace 'unpack unpack)
  283. (add-before 'build 'extract-gemspec extract-gemspec)
  284. (add-after 'extract-gemspec 'replace-git-ls-files replace-git-ls-files)
  285. (replace 'build build)
  286. (replace 'check check)
  287. (replace 'install install)
  288. (add-after 'install 'wrap wrap)))
  289. (define* (ruby-build #:key inputs (phases %standard-phases)
  290. #:allow-other-keys #:rest args)
  291. (apply gnu:gnu-build #:inputs inputs #:phases phases args))