summaryrefslogtreecommitdiff
path: root/posts/2015-08-30-ruby-on-guix.skr
blob: 5da4ab4b8ac6f01211edb7e82dad2f56c9eb8a40 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
(post
 :title "Ruby on Guix"
 :date (make-date* 2015 08 30)
 :tags '("gnu" "guix" "scheme" "guile" "ruby" "wsu")
 :summary "How to use Guix + some elbow grease to replace RVM and
Bundler on GNU/Linux"

 (p [I’ve been working with Ruby professionally for over 3 years now
and I’ve grown frustrated with two of its most popular development
tools: RVM and Bundler.  For those that may not know, RVM is the Ruby
version manager and it allows unprivileged users to download, compile,
install, and manage many versions of Ruby instead of being stuck with
the one that is installed globally by your distro’s package manager.
Bundler is the tool that allows developers to keep a version
controlled “Gemfile” that specifies all of the project’s dependencies
and provides utilities to install and update those gems.  These tools
are crucial because Ruby developers often work with many applications
that use different versions of Ruby and/or different versions of gems
such as Rails.  Traditional GNU/Linux distributions install packages
to the global ,(code [/usr]) directory, limiting users to a single
version of Ruby and associated gems, if they are packaged at all.
Traditional package management fails to meet the needs of a lot of
users, so many niche package managers have been developed to
supplement them.])

 (source-code
  (scheme-source
   ";;; guile-syntax-highlight --- General-purpose syntax highlighter
;;; Copyright © 2015 David Thompson <davet@gnu.org>
;;;
;;; Guile-syntax-highlight is free software; you can redistribute it
;;; and/or modify it under the terms of the GNU Lesser General Public
;;; License as published by the Free Software Foundation; either
;;; version 3 of the License, or (at your option) any later version.
;;;
;;; Guile-syntax-highlight 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 Lesser General Public License for more details.
;;;
;;; You should have received a copy of the GNU Lesser General Public
;;; License along with guile-syntax-highlight.  If not, see
;;; <http://www.gnu.org/licenses/>.

;;; Commentary:
;;
;; Lexing utilities.
;;
;;; Code:

(define-module (syntax-highlight lexers)
  #:use-module (ice-9 match)
  #:use-module (ice-9 regex)
  #:use-module (srfi srfi-1)
  #:use-module (srfi srfi-9)
  #:use-module (srfi srfi-11)
  #:use-module (srfi srfi-26)
  #:export (make-cursor
            cursor?
            cursor-text
            cursor-position
            cursor-end?
            move-cursor
            move-cursor-by

            lex-fail
            lex-bind
            lex-return
            lex-lift
            lex-map
            lex-filter
            lex-any*
            lex-any
            lex-all*
            lex-all
            lex-consume
            lex-regexp
            lex-string
            lex-char-set
            lex-delimited
            lex-tag))

(define (string-prefix?* s1 s2 start-s2)
  (string-prefix? s1 s2 0 (string-length s1) start-s2))


;;;
;;; Cursor
;;;

(define-record-type <cursor>
  (make-cursor text position)
  cursor?
  (text cursor-text)
  (position cursor-position))

(define (cursor-end? cursor)
  \"Return #t if the cursor is at the end of the text.\"
  (>= (cursor-position cursor) (string-length (cursor-text cursor))))

(define (move-cursor cursor position)
  \"Move CURSOR to the character at POSITION.\"
  (make-cursor (cursor-text cursor) position))

(define (move-cursor-by cursor offset)
  \"Move CURSOR by OFFSET characters relative to its current
position.\"
  (move-cursor cursor (+ (cursor-position cursor) offset)))


;;;
;;; Lexers
;;;

(define (lex-fail cursor)
  \"Always fail to lex STR without consuming any of it.\"
  (values #f cursor))

(define (lex-bind proc lexer)
  \"Return a lexer that applies the result of LEXER to PROC, a
procedure that returns a lexer, and then applies that new lexer.\"
  (lambda (cursor)
    (let-values (((result remainder) (lexer cursor)))
      (if result
          ((proc result) remainder)
          (lex-fail cursor)))))

(define (lex-return x)
  \"Return a lexer that always yields X as the lex result.\"
  (lambda (cursor)
    (values x cursor)))

(define (lex-lift proc)
  \"Return a procedure that wraps the result of PROC in a lexer.\"
  (lambda args
    (lex-return (apply proc args))))

(define (lex-map proc lexer)
  \"Return a lexer that applies PROC to result of LEXER.\"
  (lex-bind (lex-lift proc) lexer))

(define (lex-any* lexers)
  \"Return a lexer that succeeds with the result of the first
successful lexer in LEXERS or fails if all lexers fail.\"
  (define (either a b)
    (lambda (cursor)
      (let-values (((result remainder) (a cursor)))
        (if result
            (values result remainder)
            (b cursor)))))

  (fold-right either lex-fail lexers))

(define (lex-any . lexers)
  \"Return a lexer that succeeds with the result of the first
successful lexer in LEXERS or fails if all lexers fail.\"
  (lex-any* lexers))

(define (lex-all* lexers)
  \"Return a lexer that succeeds with the results of all LEXERS in
order, or fails if any lexer fails.\"
  (define (both a b)
    (lambda (cursor)
      (let-values (((result-a remainder-a) (a cursor)))
        (if result-a
            (let-values (((result-b remainder-b) (b remainder-a)))
              (if result-b
                  (values (cons result-a result-b) remainder-b)
                  (lex-fail cursor)))
            (lex-fail cursor)))))

  (fold-right both (lex-return '()) lexers))

(define (lex-all . lexers)
  \"Return a lexer that succeeds with the results of all LEXERS in
order, or fails if any lexer fails.\"
  (lex-all* lexers))

(define (lex-consume lexer)
  \"Return a lexer that always succeeds with a list of as many
consecutive successful applications of LEXER as possible, consuming
the entire input text.  Sections of text that could not be lexed are
returned as plain strings.\"
  (define (substring* cursor start)
    (substring (cursor-text cursor) start (cursor-position cursor)))

  (lambda (cursor)
    (let loop ((cursor cursor)
               (memo '())
               (fail-start #f))
      (if (cursor-end? cursor)
          (values (reverse memo) cursor)
          (let-values (((result remainder) (lexer cursor)))
            (cond
             ;; Regular successful result.
             ((and result (not fail-start))
              (loop remainder (cons result memo) #f))
             ;; Successful result after some amount of unmatched
             ;; characters.
             ((and result fail-start)
              (loop remainder
                    (cons* result (substring* cursor fail-start) memo)
                    #f))
             ;; Consecutive failure.
             (fail-start
              (loop (move-cursor-by cursor 1)
                    memo
                    fail-start))
             ;; First failure.
             (else
              (loop (move-cursor-by cursor 1)
                    memo
                    (cursor-position cursor)))))))))

(define (lex-regexp pattern)
  \"Return a lexer that succeeds with the matched substring when the
input matches the string PATTERN.\"
  (let ((rx (make-regexp (string-append \"^\" pattern))))
    (lambda (cursor)
      (if (cursor-end? cursor)
          (lex-fail cursor)
          (let ((result (regexp-exec rx (cursor-text cursor)
                                     (cursor-position cursor))))
            (if result
                (let ((str (match:substring result 0)))
                  (values str (move-cursor-by cursor (string-length str))))
                (lex-fail cursor)))))))

(define (lex-string str)
  \"Return a lexer that succeeds with STR when the input starts with
STR.\"
  (lambda (cursor)
    (if (string-prefix?* str (cursor-text cursor) (cursor-position cursor))
        (values str (move-cursor-by cursor (string-length str)))
        (lex-fail cursor))))

(define (lex-char-set char-set)
  \"Return a lexer that succeeds with the nonempty input prefix that
matches CHAR-SET, or fails if the first input character does not
belong to CHAR-SET.\"
  (define (char-set-substring str start)
    (let ((len (string-length str)))
      (let loop ((index start))
        (cond
         ((>= index len)
          (substring str start len))
         ((char-set-contains? char-set (string-ref str index))
          (loop (1+ index)))
         (else
          (substring str start index))))))

  (lambda (cursor)
    (match (char-set-substring (cursor-text cursor) (cursor-position cursor))
      (\"\" (lex-fail cursor))
      (str (values str (move-cursor-by cursor (string-length str)))))))

(define* (lex-delimited open #:key (until open) (escape #\\) nested?)
  \"Return a lexer that succeeds with the string delimited by the
opening string OPEN and the closing string UNTIL.  Characters within
the delimited expression may be escaped with the character ESCAPE.  If
NESTED?, allow for delimited expressions to be arbitrarily nested
within.\"
  (define (delimit str start)
    (let ((len (string-length str)))
      (let loop ((index start))
        (cond
         ;; Out of bounds.
         ((>= index len)
          len)
         ;; Escape character.
         ((eqv? escape (string-ref str index))
          (loop (+ index 2)))
         ;; Closing delimiter.
         ((string-prefix?* until str index)
          (+ index (string-length until)))
         ;; Nested delimited string.
         ((and nested? (string-prefix?* open str index))
          (loop (delimit str (+ index (string-length open)))))
         (else
          (loop (1+ index)))))))

  (lambda (cursor)
    (let ((str (cursor-text cursor))
          (pos (cursor-position cursor)))
      (if (string-prefix?* open str pos)
          (let ((end (delimit str (+ pos (string-length open)))))
            (values (substring str pos end) (move-cursor cursor end)))
          (lex-fail cursor)))))

(define (lex-tag tag lexer)
  \"Wrap the results of LEXER in a two-element tuple whose head is
TAG.\"
  (lex-map (cut list tag <>) lexer))
"))

 (p [Taking a step back, it becomes apparent that dependency isolation
is a general problem that isn’t confined to software written in Ruby:
Node has npm and nvm, Python has pip and virtualenv, and so on.  A big
limitation of all these language-specific package managers is that
they cannot control what is outside of their language domain.  In
order to use RVM to successfully compile a version of Ruby, you need
to make sure you have the GCC toolchain, OpenSSL, readline, libffi,
etc. installed using the system package manager (note: I’ve seen RVM
try to build prerequisites like OpenSSL before, which I then disabled
to avoid duplication and security issues and I recommend you do the
same.)  In order to use Bundler to install Nokogiri, you need to make
sure libxml2 has been installed using the system package manager.  If
you work with more than a single language, the number of different
package management tools needed to get work done is staggering.  For
web applications, it’s not uncommon to use RVM, Bundler, NPM, Bower,
and the system package manager simultaneously to get all of the
necessary programs and libraries.  Large web applications are
notoriously difficult to deploy, and companies hire a bunch of
operations folk like me to try to wrangle it all.])

 (p [Anyway, let’s forget about Node, Python, etc. and just focus on
Ruby.  Have you or someone you work with encountered hard to debug
issues and Git merge conflicts due to a problem with
,(code [Gemfile.lock])?  Bundler’s fast and loose versioning in the
,(code [Gemfile]) (e.g. ,(code [rails >= 4.0])) causes headaches when
different users update different gems at different times and check the
resulting auto-generated ,(code [Gemfile.lock]) into the repository.
Have you ever been frustrated that it’s difficult to deduplicate gems
that are shared between multiple bundled gem sets?  Have you looked at
the ,(anchor [RVM home page] "https://rvm.io") and been frustrated
that they recommend you to ,(code [curl bash]) to install their
software?  Have you been annoyed by RVM’s strange system of overriding
shell built-ins in order to work its magic?  I’m not sure how you
feel, dear reader, but my Ruby environments feel like one giant,
brittle hack, and I’m often enough involved in fixing issues with them
on my own workstation, that of my colleagues, and on production
servers.])

 (p [So, if you’re still with me, what do we do about this?  How can we
work to improve upon the status quo?  Just use Docker?  Docker is
helpful, and certainly much better than no isolation at all, but it
hides the flaws of package management inside an opaque disk image and
restricts the environments in which your application is built to
function.  The general problem of dependency isolation is orthogonal
to the runtime environment, be it container, virtual machine, or “bare
metal.”  Enter functional package management.  What does it mean for a
package manager to be functional?  GNU Guix, the functional package
manager that I contribute to and recommend, has this to say:])

 (blockquote
    (p [GNU Guix is a functional package management tool for the GNU
system.  Package management consists of all activities that relate
to building packages from sources, honoring their build-time and
run-time dependencies, installing packages in user environments,
upgrading installed packages to new versions or rolling back to a
previous set, removing unused software packages, etc.])
    (p [The term functional refers to a specific package management
discipline.  In Guix, the package build and installation process
is seen as a function, in the mathematical sense.  That function
takes inputs, such as build scripts, a compiler, and libraries,
and returns an installed package.]))

 (p [Guix has a rich set of features, some of which you may find in
other package managers, but not all of them (unless you use another
functional package manager such as Nix.)  Gem/Bundler can do
unprivileged gem installation, but it cannot do transactional upgrades
and rollbacks or install non-Ruby dependencies.  Dpkg/yum/pacman can
install all build-time and runtime dependencies, but it cannot do
unprivileged package installation to isolated user environments.  And
none of them can precisely describe the ,(em [full]) dependency
graph (all the way down to the C compiler’s compiler) but ,(em [Guix
can]).])

 (p [Guix is written in Guile, an implementation of the Scheme
programming language.  The upcoming release of Guix will feature a
Ruby build system that captures the process of installing gems from
,(code [.gem]) archives and a RubyGems import utility to make it
easier to write Guix packages by using the metadata available on
,(anchor "RubyGems.org" "https://rubygems.org").  Ruby developers
interested in functional package management are encouraged to try
packaging their gems (and dependencies) for Guix.])

 (p [Now, how exactly can Guix replace RVM and Bundler?  Guix uses an
abstraction called a “profile” that represents a user-defined set of
packages that should work together.  Think of it as having many
,(code [/usr]) file system trees that can be used in isolation from the
others (without invoking virtualization technologies such as virtual
machines or containers.)  To install multiple versions of Ruby and
various gems, the user need only create a separate profile for them:])

 (source-code
  "$ guix package --profile=project-1 --install ruby-2.2 ruby-rspec-3
# Hypothetical packages:
$ guix package --profile=project-2 --install ruby-1.9 ruby-rspec-2")

 (p [A profile is a “symlink forest” that is the union of all the
packages it includes, and files are deduplicated among all of them.
To actually use the profile, the relevant environment variables must
be configured.  Guix is aware of such variables, and can tell you what
to set by running the following:])

 (source-code
  "$ guix package --search-paths --profile=project-1")

 (p [Additionally, you can also create ad-hoc development environments
with the ,(code [guix environment]) tool.  This tool will spawn a
sub-shell (or another program of your choice) in an environment in
which a set of specified packages are available.  This is my preferred
method as it automagically sets all of the environment variables for
me and Guix is free to garbage collect the packages when I close the
sub-shell:])

 (source-code
  "# Launch a Ruby REPL with ActiveSupport available.
$ guix environment --ad-hoc ruby ruby-activesupport -E irb")

 (p [In order to make this environment reproducible for others, I
recommend keeping a ,(code [package.scm]) file in version control that
describes the complete dependency graph for your project, as well as
metadata such as the license, version, and description:])

 (source-code
  (scheme-source
   "(use-modules (guix packages)
             (guix licenses)
             (guix build-system ruby)
             (gnu packages)
             (gnu packages version-control)
             (gnu packages ssh)
             (gnu packages ruby))

(package
  (name \"cool-ruby-project\")
  (version \"1.0\")
  (source #f) ; not needed just to create dev environment
  (build-system ruby-build-system)
  ;; These correspond roughly to \"development\" dependencies.
  (native-inputs
   `((\"git\" ,git)
     (\"openssh\" ,openssh)
     (\"ruby-rspec\" ,ruby-rspec)))
  (propagated-inputs
   `((\"ruby-pg\" ,ruby-pg)
     (\"ruby-nokogiri\" ,ruby-nokogiri)
     (\"ruby-i18n\" ,ruby-i18n)
     (\"ruby-rails\" ,ruby-rails)))
  (synopsis \"A cool Ruby project\")
  (description \"This software does some cool stuff, trust me.\")
  (home-page \"https://example.com\")
  (license expat))"))

 (p [With this package file, it is simple to an instantiate a
development environment:])

 (pre (code [$ guix environment -l package.scm]))

  (p [I’m not covering it in this post, but properly filling out the
blank ,(code [source]) field above would allow for building
development snapshots, including running the test suite, in an
isolated build container using the ,(code [guix build]) utility.  This
is very useful when composed with a continuous integration system.
Guix itself uses ,(anchor "Hydra" "https://nixos.org/hydra/") as its
CI system to perform all package builds.])

  (p [As mentioned earlier, one of the big advantages of writing Guix
package recipes is that the full dependency graph can be captured,
including non-Ruby components.  The pg gem provides a good example:])

  (source-code
   (scheme-source
"(define-public ruby-pg
  (package
    (name \"ruby-pg\")
    (version \"0.18.2\")
    (source
     (origin
       (method url-fetch)
       (uri (rubygems-uri \"pg\" version))
       (sha256
        (base32
         \"1axxbf6ij1iqi3i1r3asvjc80b0py5bz0m2wy5kdi5xkrpr82kpf\"))))
    (build-system ruby-build-system)
    (arguments
     '(#:test-target \"spec\"))
    ;; Native inputs are used only at build and test time.
    (native-inputs
     `((\"ruby-rake-compiler\" ,ruby-rake-compiler)
       (\"ruby-hoe\" ,ruby-hoe)
       (\"ruby-rspec\" ,ruby-rspec)))
    ;; Native extension links against PostgreSQL shared library.
    (inputs
     `((\"postgresql\" ,postgresql)))
    (synopsis \"Ruby interface to PostgreSQL\")
    (description \"Pg is the Ruby interface to the PostgreSQL RDBMS.  It works
  with PostgreSQL 8.4 and later.\")
     (home-page \"https://bitbucket.org/ged/ruby-pg\")
     (license license:ruby)))"))

  (p [Note how the recipe specifies the PostgreSQL dependency.  Below
is the dependency graph for ruby-pg as produced by ,(code [guix
graph]), excluding the GCC compiler toolchain and other low-level
tools for brevity.  Pretty neat, eh?])

  (image/caption "/images/ruby-pg-graph.png"
                 "Abbreviated dependency graph for the pg gem")

  (p [Given that Guix doesn’t yet have many gems packaged (help
wanted), it can still be advantageous to use it for getting more
up-to-date packages than many distros provide, but in conjuction with
Bundler for fetching Ruby gems.  This gets RVM out of your hair whilst
creating a migration path away from Bundler at a later time once the
required gems have been packaged:])

  (source-code
   "$ cd my-project/
$ guix environment --ad-hoc ruby bundler libxml2 libxslt # etc.
# A small bash script can be used to make these gem sets.
$ mkdir .gems
$ export GEM_HOME=$PWD/.gems
$ export GEM_PATH=$GEM_HOME:$GEM_PATH
$ export PATH=$GEM_HOME/bin:$PATH
$ bundle install")

  (p [As you’ve seen in the above package snippets, Guix package
definitions are typically very short and rather easy to write
yourself.  The ,(code [guix import gem]) tool was made to lower ,(code
                                                                    [foo bar]) the barrier even more by generating most of the boilerplate
code.  For example:])

  (source-code "$ guix import gem pry")

  (p [Produces this Scheme code:])

(source-code
 (scheme-source
  [;;; hello there
(+ 1 2 3)]))

  (source-code
   (scheme-source
     "(package
  (name \"ruby-pry\")
  (version \"0.10.1\")
  (source
   (origin
     (method url-fetch)
     (uri (rubygems-uri \"pry\" version))
     (sha256
      (base32
       \"1j0r5fm0wvdwzbh6d6apnp7c0n150hpm9zxpm5xvcgfqr36jaj8z\"))))
  (build-system ruby-build-system)
  (propagated-inputs
   `((\"ruby-coderay\" ,ruby-coderay)
     (\"ruby-method-source\" ,ruby-method-source)
     (\"ruby-slop\" ,ruby-slop)))
  (synopsis
   \"An IRB alternative and runtime developer console\")
  (description
   \"An IRB alternative and runtime developer console\")
  (home-page \"http://pryrepl.org\")
  (license expat))"))

  (p [One still has to package the propagated inputs if they aren’t
yet available, add the necessary inputs for building native extensions
if needed, and fiddle with the native inputs needed to run the test
suite, but for most pure Ruby gems this gets you close to a working
package quickly.])

  (p [In conclusion, while support for Ruby in Guix is still in its
early days, I hope that you have seen the benefits that using a
general-purpose, functional package manager can bring to your Ruby
environments (and all other environments, too.)  For more information
about Guix concepts, installation instructions, programming interface,
and tools, please refer to the
,(anchor [official manual] "https://gnu.org/software/guix/manual").

Check out the ,(anchor [help] "https://gnu.org/software/guix/help")
page for ways to contact the development team for help or to report
bugs.  If you are interested in getting your hands dirty, please
,(anchor [contribute] "https://gnu.org/software/guix/contribute").
Besides contributions of code, art, and docs, we also need ,(anchor
[hardware donations] "https://gnu.org/software/guix/donate") to grow
our build farm to meet the needs of all our users.  Happy hacking!]))