Commit | Line | Data |
---|---|---|
a4154748 FB |
1 | ;;; GNU Guix --- Functional package management for GNU |
2 | ;;; Copyright © 2015 Federico Beffa <beffa@fbengineering.ch> | |
f6078422 | 3 | ;;; Copyright © 2018 Ricardo Wurmus <rekado@elephly.net> |
a4154748 FB |
4 | ;;; |
5 | ;;; This file is part of GNU Guix. | |
6 | ;;; | |
7 | ;;; GNU Guix is free software; you can redistribute it and/or modify it | |
8 | ;;; under the terms of the GNU General Public License as published by | |
9 | ;;; the Free Software Foundation; either version 3 of the License, or (at | |
10 | ;;; your option) any later version. | |
11 | ;;; | |
12 | ;;; GNU Guix is distributed in the hope that it will be useful, but | |
13 | ;;; WITHOUT ANY WARRANTY; without even the implied warranty of | |
14 | ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
15 | ;;; GNU General Public License for more details. | |
16 | ;;; | |
17 | ;;; You should have received a copy of the GNU General Public License | |
18 | ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>. | |
19 | ||
20 | (define-module (guix import cabal) | |
21 | #:use-module (ice-9 match) | |
22 | #:use-module (ice-9 regex) | |
23 | #:use-module (ice-9 rdelim) | |
24 | #:use-module (ice-9 receive) | |
25 | #:use-module (srfi srfi-26) | |
26 | #:use-module (srfi srfi-34) | |
27 | #:use-module (srfi srfi-35) | |
28 | #:use-module (srfi srfi-11) | |
29 | #:use-module (srfi srfi-1) | |
30 | #:use-module (srfi srfi-9) | |
31 | #:use-module (srfi srfi-9 gnu) | |
32 | #:use-module (system base lalr) | |
33 | #:use-module (rnrs enums) | |
9be54eb1 | 34 | #:use-module (guix utils) |
a4154748 FB |
35 | #:export (read-cabal |
36 | eval-cabal | |
37 | ||
d804d0b9 DM |
38 | cabal-custom-setup-dependencies |
39 | ||
a4154748 FB |
40 | cabal-package? |
41 | cabal-package-name | |
42 | cabal-package-version | |
ca45da9f | 43 | cabal-package-revision |
a4154748 FB |
44 | cabal-package-license |
45 | cabal-package-home-page | |
46 | cabal-package-source-repository | |
47 | cabal-package-synopsis | |
48 | cabal-package-description | |
49 | cabal-package-executables | |
50 | cabal-package-library | |
51 | cabal-package-test-suites | |
52 | cabal-package-flags | |
53 | cabal-package-eval-environment | |
d804d0b9 | 54 | cabal-package-custom-setup |
a4154748 FB |
55 | |
56 | cabal-source-repository? | |
57 | cabal-source-repository-use-case | |
58 | cabal-source-repository-type | |
59 | cabal-source-repository-location | |
60 | ||
61 | cabal-flag? | |
62 | cabal-flag-name | |
63 | cabal-flag-description | |
64 | cabal-flag-default | |
65 | cabal-flag-manual | |
66 | ||
67 | cabal-dependency? | |
68 | cabal-dependency-name | |
69 | cabal-dependency-version | |
70 | ||
71 | cabal-executable? | |
72 | cabal-executable-name | |
73 | cabal-executable-dependencies | |
74 | ||
75 | cabal-library? | |
76 | cabal-library-dependencies | |
77 | ||
78 | cabal-test-suite? | |
79 | cabal-test-suite-name | |
80 | cabal-test-suite-dependencies)) | |
81 | ||
82 | ;; Part 1: | |
83 | ;; | |
84 | ;; Functions used to read a Cabal file. | |
85 | ||
86 | ;; Comment: | |
87 | ;; | |
88 | ;; The use of virtual closing braces VCCURLY and some lexer functions were | |
89 | ;; inspired from http://hackage.haskell.org/package/haskell-src | |
90 | ||
91 | ;; Object containing information about the structure of a block: (i) delimited | |
92 | ;; by braces or by indentation, (ii) minimum indentation. | |
93 | (define-record-type <parse-context> | |
94 | (make-parse-context mode indentation) | |
95 | parse-context? | |
96 | (mode parse-context-mode) ; 'layout or 'no-layout | |
97 | (indentation parse-context-indentation)) ; #f for 'no-layout | |
98 | ||
99 | ;; <parse-context> mode set universe | |
100 | (define-enumeration context (layout no-layout) make-context) | |
101 | ||
102 | (define (make-stack) | |
103 | "Creates a simple stack closure. Actions on the generated stack are | |
104 | requested by calling it with one of the following symbols as the first | |
105 | argument: 'empty?, 'push!, 'top, 'pop! and 'clear!. The action 'push! is the | |
106 | only one requiring a second argument corresponding to the object to be added | |
107 | to the stack." | |
108 | (let ((stack '())) | |
109 | (lambda (msg . args) | |
110 | (cond ((eqv? msg 'empty?) (null? stack)) | |
111 | ((eqv? msg 'push!) (set! stack (cons (first args) stack))) | |
112 | ((eqv? msg 'top) (if (null? stack) '() (first stack))) | |
113 | ((eqv? msg 'pop!) (match stack | |
114 | ((e r ...) (set! stack (cdr stack)) e) | |
115 | (_ #f))) | |
116 | ((eqv? msg 'clear!) (set! stack '())) | |
117 | (else #f))))) | |
118 | ||
119 | ;; Stack to track the structure of nested blocks and simple interface | |
120 | (define context-stack (make-parameter (make-stack))) | |
121 | ||
122 | (define (context-stack-empty?) ((context-stack) 'empty?)) | |
123 | ||
124 | (define (context-stack-push! e) ((context-stack) 'push! e)) | |
125 | ||
126 | (define (context-stack-top) ((context-stack) 'top)) | |
127 | ||
128 | (define (context-stack-pop!) ((context-stack) 'pop!)) | |
129 | ||
130 | (define (context-stack-clear!) ((context-stack) 'clear!)) | |
131 | ||
132 | ;; Indentation of the line being parsed. | |
133 | (define current-indentation (make-parameter 0)) | |
134 | ||
135 | ;; Signal to reprocess the beginning of line, in case we need to close more | |
136 | ;; than one indentation level. | |
137 | (define check-bol? (make-parameter #f)) | |
138 | ||
139 | ;; Name of the file being parsed. Used in error messages. | |
140 | (define cabal-file-name (make-parameter "unknowk")) | |
141 | ||
142 | ;; Specify the grammar of a Cabal file and generate a suitable syntax analyser. | |
143 | (define (make-cabal-parser) | |
144 | "Generate a parser for Cabal files." | |
145 | (lalr-parser | |
146 | ;; --- token definitions | |
ecba50bb | 147 | (CCURLY VCCURLY OPAREN CPAREN TEST ID VERSION RELATION TRUE FALSE -ANY -NONE |
314b63e0 | 148 | (right: IF FLAG EXEC TEST-SUITE CUSTOM-SETUP SOURCE-REPO BENCHMARK LIB OCURLY) |
a4154748 FB |
149 | (left: OR) |
150 | (left: PROPERTY AND) | |
151 | (right: ELSE NOT)) | |
152 | ;; --- rules | |
153 | (body (properties sections) : (append $1 $2)) | |
154 | (sections (sections flags) : (append $1 $2) | |
155 | (sections source-repo) : (append $1 (list $2)) | |
156 | (sections executables) : (append $1 $2) | |
157 | (sections test-suites) : (append $1 $2) | |
314b63e0 | 158 | (sections custom-setup) : (append $1 $2) |
a4154748 FB |
159 | (sections benchmarks) : (append $1 $2) |
160 | (sections lib-sec) : (append $1 (list $2)) | |
161 | () : '()) | |
162 | (flags (flags flag-sec) : (append $1 (list $2)) | |
163 | (flag-sec) : (list $1)) | |
164 | (flag-sec (FLAG OCURLY properties CCURLY) : `(section flag ,$1 ,$3) | |
165 | (FLAG open properties close) : `(section flag ,$1 ,$3) | |
166 | (FLAG) : `(section flag ,$1 '())) | |
167 | (source-repo (SOURCE-REPO OCURLY properties CCURLY) | |
168 | : `(section source-repository ,$1 ,$3) | |
169 | (SOURCE-REPO open properties close) | |
170 | : `(section source-repository ,$1 ,$3)) | |
171 | (properties (properties PROPERTY) : (append $1 (list $2)) | |
172 | (PROPERTY) : (list $1)) | |
173 | (executables (executables exec-sec) : (append $1 (list $2)) | |
174 | (exec-sec) : (list $1)) | |
175 | (exec-sec (EXEC OCURLY exprs CCURLY) : `(section executable ,$1 ,$3) | |
176 | (EXEC open exprs close) : `(section executable ,$1 ,$3)) | |
177 | (test-suites (test-suites ts-sec) : (append $1 (list $2)) | |
178 | (ts-sec) : (list $1)) | |
179 | (ts-sec (TEST-SUITE OCURLY exprs CCURLY) : `(section test-suite ,$1 ,$3) | |
180 | (TEST-SUITE open exprs close) : `(section test-suite ,$1 ,$3)) | |
314b63e0 | 181 | (custom-setup (CUSTOM-SETUP exprs) : (list `(section custom-setup ,$1 ,$2))) |
a4154748 FB |
182 | (benchmarks (benchmarks bm-sec) : (append $1 (list $2)) |
183 | (bm-sec) : (list $1)) | |
184 | (bm-sec (BENCHMARK OCURLY exprs CCURLY) : `(section benchmark ,$1 ,$3) | |
185 | (BENCHMARK open exprs close) : `(section benchmark ,$1 ,$3)) | |
186 | (lib-sec (LIB OCURLY exprs CCURLY) : `(section library ,$3) | |
187 | (LIB open exprs close) : `(section library ,$3)) | |
188 | (exprs (exprs PROPERTY) : (append $1 (list $2)) | |
189 | (PROPERTY) : (list $1) | |
190 | (exprs if-then-else) : (append $1 (list $2)) | |
191 | (if-then-else) : (list $1) | |
192 | (exprs if-then) : (append $1 (list $2)) | |
193 | (if-then) : (list $1)) | |
194 | (if-then-else (IF tests OCURLY exprs CCURLY ELSE OCURLY exprs CCURLY) | |
195 | : `(if ,$2 ,$4 ,$8) | |
196 | (IF tests open exprs close ELSE OCURLY exprs CCURLY) | |
197 | : `(if ,$2 ,$4 ,$8) | |
198 | ;; The 'open' token after 'tests' is shifted after an 'exprs' | |
199 | ;; is found. This is because, instead of 'exprs' a 'OCURLY' | |
200 | ;; token is a valid alternative. For this reason, 'open' | |
201 | ;; pushes a <parse-context> with a line indentation equal to | |
202 | ;; the indentation of 'exprs'. | |
203 | ;; | |
204 | ;; Differently from this, without the rule above this | |
205 | ;; comment, when an 'ELSE' token is found, the 'open' token | |
206 | ;; following the 'ELSE' would be shifted immediately, before | |
207 | ;; the 'exprs' is found (because there are no other valid | |
208 | ;; tokens). The 'open' would therefore create a | |
209 | ;; <parse-context> with the indentation of 'ELSE' and not | |
210 | ;; 'exprs', creating an inconsistency. We therefore allow | |
211 | ;; mixed style conditionals. | |
212 | (IF tests open exprs close ELSE open exprs close) | |
213 | : `(if ,$2 ,$4 ,$8)) | |
214 | (if-then (IF tests OCURLY exprs CCURLY) : `(if ,$2 ,$4 ()) | |
215 | (IF tests open exprs close) : `(if ,$2 ,$4 ())) | |
216 | (tests (TEST OPAREN ID CPAREN) : `(,$1 ,$3) | |
7716f55c FB |
217 | (TRUE) : 'true |
218 | (FALSE) : 'false | |
a4154748 FB |
219 | (TEST OPAREN ID RELATION VERSION CPAREN) |
220 | : `(,$1 ,(string-append $3 " " $4 " " $5)) | |
ecba50bb DM |
221 | (TEST OPAREN ID -ANY CPAREN) |
222 | : `(,$1 ,(string-append $3 " -any")) | |
223 | (TEST OPAREN ID -NONE CPAREN) | |
224 | : `(,$1 ,(string-append $3 " -none")) | |
a4154748 FB |
225 | (TEST OPAREN ID RELATION VERSION AND RELATION VERSION CPAREN) |
226 | : `(and (,$1 ,(string-append $3 " " $4 " " $5)) | |
227 | (,$1 ,(string-append $3 " " $7 " " $8))) | |
228 | (NOT tests) : `(not ,$2) | |
229 | (tests AND tests) : `(and ,$1 ,$3) | |
230 | (tests OR tests) : `(or ,$1 ,$3) | |
231 | (OPAREN tests CPAREN) : $2) | |
232 | (open () : (context-stack-push! | |
233 | (make-parse-context (context layout) | |
234 | (current-indentation)))) | |
235 | (close (VCCURLY)))) | |
236 | ||
237 | (define (peek-next-line-indent port) | |
238 | "This function can be called when the next character on PORT is #\newline | |
239 | and returns the indentation of the line starting after the #\newline | |
240 | character. Discard (and consume) empty and comment lines." | |
876fd23a FB |
241 | (if (eof-object? (peek-char port)) |
242 | ;; If the file is missing the #\newline on the last line, add it and act | |
243 | ;; as if it were there. This is needed for proper operation of | |
244 | ;; indentation based block recognition (based on ‘port-column’). | |
245 | (begin (unread-char #\newline port) (read-char port) 0) | |
246 | (let ((initial-newline (string (read-char port)))) | |
247 | (let loop ((char (peek-char port)) | |
248 | (word "")) | |
249 | (cond ((eqv? char #\newline) (read-char port) | |
250 | (loop (peek-char port) "")) | |
251 | ((or (eqv? char #\space) (eqv? char #\tab)) | |
252 | (let ((c (read-char port))) | |
253 | (loop (peek-char port) (string-append word (string c))))) | |
254 | ((comment-line port char) (loop (peek-char port) "")) | |
255 | (else | |
256 | (let ((len (string-length word))) | |
257 | (unread-string (string-append initial-newline word) port) | |
258 | len))))))) | |
a4154748 FB |
259 | |
260 | (define* (read-value port value min-indent #:optional (separator " ")) | |
261 | "The next character on PORT must be #\newline. Append to VALUE the | |
262 | following lines with indentation larger than MIN-INDENT." | |
263 | (let loop ((val (string-trim-both value)) | |
264 | (x (peek-next-line-indent port))) | |
265 | (if (> x min-indent) | |
266 | (begin | |
267 | (read-char port) ; consume #\newline | |
268 | (loop (string-append | |
269 | val (if (string-null? val) "" separator) | |
270 | (string-trim-both (read-delimited "\n" port 'peek))) | |
271 | (peek-next-line-indent port))) | |
272 | val))) | |
273 | ||
959c9d15 RV |
274 | (define* (read-braced-value port) |
275 | "Read up to a closing brace." | |
276 | (string-trim-both (read-delimited "}" port 'trim))) | |
277 | ||
a4154748 FB |
278 | (define (lex-white-space port bol) |
279 | "Consume white spaces and comment lines on PORT. If a new line is started return #t, | |
280 | otherwise return BOL (beginning-of-line)." | |
281 | (let loop ((c (peek-char port)) | |
282 | (bol bol)) | |
283 | (cond | |
284 | ((and (not (eof-object? c)) | |
285 | (or (char=? c #\space) (char=? c #\tab))) | |
286 | (read-char port) | |
287 | (loop (peek-char port) bol)) | |
288 | ((and (not (eof-object? c)) (char=? c #\newline)) | |
289 | (read-char port) | |
290 | (loop (peek-char port) #t)) | |
291 | ((comment-line port c) | |
292 | (lex-white-space port bol)) | |
293 | (else | |
294 | bol)))) | |
295 | ||
296 | (define (lex-bol port) | |
297 | "Process the beginning of a line on PORT: update current-indentation and | |
298 | check the end of an indentation based context." | |
299 | (let ((loc (make-source-location (cabal-file-name) (port-line port) | |
300 | (port-column port) -1 -1))) | |
301 | (current-indentation (source-location-column loc)) | |
302 | (case (get-offside port) | |
303 | ((less-than) | |
304 | (check-bol? #t) ; need to check if closing more than 1 indent level. | |
305 | (unless (context-stack-empty?) (context-stack-pop!)) | |
306 | (make-lexical-token 'VCCURLY loc #f)) | |
307 | (else | |
308 | (lex-token port))))) | |
309 | ||
310 | (define (bol? port) (or (check-bol?) (= (port-column port) 0))) | |
311 | ||
312 | (define (comment-line port c) | |
313 | "If PORT starts with a comment line, consume it up to, but not including | |
314 | #\newline. C is the next character on PORT." | |
315 | (cond ((and (not (eof-object? c)) (char=? c #\-)) | |
316 | (read-char port) | |
317 | (let ((c2 (peek-char port))) | |
318 | (if (char=? c2 #\-) | |
319 | (read-delimited "\n" port 'peek) | |
320 | (begin (unread-char c port) #f)))) | |
321 | (else #f))) | |
322 | ||
323 | (define-enumeration ordering (less-than equal greater-than) make-ordering) | |
324 | ||
325 | (define (get-offside port) | |
326 | "In an indentation based context return the symbol 'greater-than, 'equal or | |
327 | 'less-than to signal if the current column number on PORT is greater-, equal-, | |
328 | or less-than the indentation of the current context." | |
329 | (let ((x (port-column port))) | |
330 | (match (context-stack-top) | |
331 | (($ <parse-context> 'layout indentation) | |
332 | (cond | |
333 | ((> x indentation) (ordering greater-than)) | |
334 | ((= x indentation) (ordering equal)) | |
335 | (else (ordering less-than)))) | |
336 | (_ (ordering greater-than))))) | |
337 | ||
338 | ;; (Semi-)Predicates for individual tokens. | |
339 | ||
340 | (define (is-relation? c) | |
341 | (and (char? c) (any (cut char=? c <>) '(#\< #\> #\=)))) | |
342 | ||
d8b6fcde FB |
343 | (define* (make-rx-matcher pat #:optional (flag #f)) |
344 | "Compile PAT into a regular expression with FLAG and creates a function | |
345 | matching a string against the created regexp." | |
346 | (let ((rx (if flag | |
347 | (make-regexp pat flag) | |
348 | (make-regexp pat)))) | |
349 | (cut regexp-exec rx <>))) | |
a4154748 | 350 | |
959c9d15 RV |
351 | (define is-layout-property (make-rx-matcher "([a-z0-9-]+)[ \t]*:[ \t]*(\\w?[^{}]*)$" |
352 | regexp/icase)) | |
353 | ||
354 | (define is-braced-property (make-rx-matcher "([a-z0-9-]+)[ \t]*:[ \t]*\\{[ \t]*$" | |
355 | regexp/icase)) | |
a4154748 | 356 | |
d8b6fcde FB |
357 | (define is-flag (make-rx-matcher "^flag +([a-z0-9_-]+)" |
358 | regexp/icase)) | |
a4154748 FB |
359 | |
360 | (define is-src-repo | |
d8b6fcde FB |
361 | (make-rx-matcher "^source-repository +([a-z0-9_-]+)" |
362 | regexp/icase)) | |
a4154748 | 363 | |
d8b6fcde FB |
364 | (define is-exec (make-rx-matcher "^executable +([a-z0-9_-]+)" |
365 | regexp/icase)) | |
a4154748 | 366 | |
d8b6fcde FB |
367 | (define is-test-suite (make-rx-matcher "^test-suite +([a-z0-9_-]+)" |
368 | regexp/icase)) | |
a4154748 | 369 | |
314b63e0 DM |
370 | (define is-custom-setup (make-rx-matcher "^(custom-setup)" |
371 | regexp/icase)) | |
372 | ||
d8b6fcde FB |
373 | (define is-benchmark (make-rx-matcher "^benchmark +([a-z0-9_-]+)" |
374 | regexp/icase)) | |
a4154748 | 375 | |
d8b6fcde | 376 | (define is-lib (make-rx-matcher "^library *" regexp/icase)) |
a4154748 | 377 | |
d8b6fcde | 378 | (define is-else (make-rx-matcher "^else" regexp/icase)) |
a4154748 | 379 | |
d8b6fcde | 380 | (define (is-if s) (string-ci=? s "if")) |
a4154748 | 381 | |
7716f55c FB |
382 | (define (is-true s) (string-ci=? s "true")) |
383 | ||
384 | (define (is-false s) (string-ci=? s "false")) | |
385 | ||
ecba50bb DM |
386 | (define (is-any s) (string-ci=? s "-any")) |
387 | ||
388 | (define (is-none s) (string-ci=? s "-none")) | |
389 | ||
a4154748 FB |
390 | (define (is-and s) (string=? s "&&")) |
391 | ||
392 | (define (is-or s) (string=? s "||")) | |
393 | ||
94abc848 | 394 | (define (is-id s port) |
a4154748 | 395 | (let ((cabal-reserved-words |
314b63e0 | 396 | '("if" "else" "library" "flag" "executable" "test-suite" "custom-setup" |
94abc848 FB |
397 | "source-repository" "benchmark")) |
398 | (spaces (read-while (cut char-set-contains? char-set:blank <>) port)) | |
399 | (c (peek-char port))) | |
400 | (unread-string spaces port) | |
a4154748 | 401 | (and (every (cut string-ci<> s <>) cabal-reserved-words) |
94abc848 FB |
402 | (and (not (char=? (last (string->list s)) #\:)) |
403 | (not (char=? #\: c)))))) | |
a4154748 FB |
404 | |
405 | (define (is-test s port) | |
406 | (let ((tests-rx (make-regexp "os|arch|flag|impl")) | |
94abc848 | 407 | (spaces (read-while (cut char-set-contains? char-set:blank <>) port)) |
a4154748 | 408 | (c (peek-char port))) |
94abc848 FB |
409 | (if (and (regexp-exec tests-rx s) (char=? #\( c)) |
410 | #t | |
411 | (begin (unread-string spaces port) #f)))) | |
a4154748 FB |
412 | |
413 | ;; Lexers for individual tokens. | |
414 | ||
415 | (define (lex-relation loc port) | |
416 | (make-lexical-token 'RELATION loc (read-while is-relation? port))) | |
417 | ||
418 | (define (lex-version loc port) | |
419 | (make-lexical-token 'VERSION loc | |
314b63e0 DM |
420 | (read-while (lambda (x) |
421 | (or (char-numeric? x) | |
422 | (char=? x #\*) | |
423 | (char=? x #\.))) | |
424 | port))) | |
a4154748 FB |
425 | |
426 | (define* (read-while is? port #:optional | |
427 | (is-if-followed-by? (lambda (c) #f)) | |
428 | (is-allowed-follower? (lambda (c) #f))) | |
429 | "Read from PORT as long as: (i) either the read character satisfies the | |
430 | predicate IS?, or (ii) it satisfies the predicate IS-IF-FOLLOWED-BY? and the | |
431 | character immediately following it satisfies IS-ALLOWED-FOLLOWER?. Returns a | |
432 | string with the read characters." | |
433 | (let loop ((c (peek-char port)) | |
434 | (res '())) | |
435 | (cond ((and (not (eof-object? c)) (is? c)) | |
436 | (let ((c (read-char port))) | |
437 | (loop (peek-char port) (append res (list c))))) | |
438 | ((and (not (eof-object? c)) (is-if-followed-by? c)) | |
439 | (let ((c (read-char port)) | |
440 | (c2 (peek-char port))) | |
441 | (if (and (not (eof-object? c2)) (is-allowed-follower? c2)) | |
442 | (loop c2 (append res (list c))) | |
443 | (begin (unread-char c) (list->string res))))) | |
444 | (else (list->string res))))) | |
445 | ||
959c9d15 | 446 | (define (lex-layout-property k-v-rx-res loc port) |
a4154748 FB |
447 | (let ((key (string-downcase (match:substring k-v-rx-res 1))) |
448 | (value (match:substring k-v-rx-res 2))) | |
449 | (make-lexical-token | |
450 | 'PROPERTY loc | |
451 | (list key `(,(read-value port value (current-indentation))))))) | |
452 | ||
959c9d15 RV |
453 | (define (lex-braced-property k-rx-res loc port) |
454 | (let ((key (string-downcase (match:substring k-rx-res 1)))) | |
455 | (make-lexical-token | |
456 | 'PROPERTY loc | |
457 | (list key `(,(read-braced-value port)))))) | |
458 | ||
a4154748 FB |
459 | (define (lex-rx-res rx-res token loc) |
460 | (let ((name (string-downcase (match:substring rx-res 1)))) | |
461 | (make-lexical-token token loc name))) | |
462 | ||
463 | (define (lex-flag flag-rx-res loc) (lex-rx-res flag-rx-res 'FLAG loc)) | |
464 | ||
465 | (define (lex-src-repo src-repo-rx-res loc) | |
466 | (lex-rx-res src-repo-rx-res 'SOURCE-REPO loc)) | |
467 | ||
468 | (define (lex-exec exec-rx-res loc) (lex-rx-res exec-rx-res 'EXEC loc)) | |
469 | ||
470 | (define (lex-test-suite ts-rx-res loc) (lex-rx-res ts-rx-res 'TEST-SUITE loc)) | |
471 | ||
314b63e0 DM |
472 | (define (lex-custom-setup ts-rx-res loc) (lex-rx-res ts-rx-res 'CUSTOM-SETUP loc)) |
473 | ||
a4154748 FB |
474 | (define (lex-benchmark bm-rx-res loc) (lex-rx-res bm-rx-res 'BENCHMARK loc)) |
475 | ||
476 | (define (lex-lib loc) (make-lexical-token 'LIB loc #f)) | |
477 | ||
478 | (define (lex-else loc) (make-lexical-token 'ELSE loc #f)) | |
479 | ||
480 | (define (lex-if loc) (make-lexical-token 'IF loc #f)) | |
481 | ||
7716f55c FB |
482 | (define (lex-true loc) (make-lexical-token 'TRUE loc #t)) |
483 | ||
484 | (define (lex-false loc) (make-lexical-token 'FALSE loc #f)) | |
485 | ||
ecba50bb DM |
486 | (define (lex-any loc) (make-lexical-token '-ANY loc #f)) |
487 | ||
488 | (define (lex-none loc) (make-lexical-token '-NONE loc #f)) | |
489 | ||
a4154748 FB |
490 | (define (lex-and loc) (make-lexical-token 'AND loc #f)) |
491 | ||
492 | (define (lex-or loc) (make-lexical-token 'OR loc #f)) | |
493 | ||
494 | (define (lex-id w loc) (make-lexical-token 'ID loc w)) | |
495 | ||
496 | (define (lex-test w loc) (make-lexical-token 'TEST loc (string->symbol w))) | |
497 | ||
498 | ;; Lexer for tokens recognizable by single char. | |
499 | ||
500 | (define* (is-ref-char->token ref-char next-char token loc port | |
501 | #:optional (hook-fn #f)) | |
502 | "If the next character NEXT-CHAR on PORT is REF-CHAR, then read it, | |
503 | execute HOOK-FN if it isn't #f and return a lexical token of type TOKEN with | |
504 | location information LOC." | |
505 | (cond ((char=? next-char ref-char) | |
506 | (read-char port) | |
507 | (when hook-fn (hook-fn)) | |
508 | (make-lexical-token token loc (string next-char))) | |
509 | (else #f))) | |
510 | ||
511 | (define (is-ocurly->token c loc port) | |
512 | (is-ref-char->token #\{ c 'OCURLY loc port | |
513 | (lambda () | |
514 | (context-stack-push! (make-parse-context | |
515 | (context no-layout) #f))))) | |
516 | ||
517 | (define (is-ccurly->token c loc port) | |
518 | (is-ref-char->token #\} c 'CCURLY loc port (lambda () (context-stack-pop!)))) | |
519 | ||
520 | (define (is-oparen->token c loc port) | |
521 | (is-ref-char->token #\( c 'OPAREN loc port)) | |
522 | ||
523 | (define (is-cparen->token c loc port) | |
524 | (is-ref-char->token #\) c 'CPAREN loc port)) | |
525 | ||
526 | (define (is-not->token c loc port) | |
527 | (is-ref-char->token #\! c 'NOT loc port)) | |
528 | ||
529 | (define (is-version? c) (char-numeric? c)) | |
530 | ||
531 | ;; Main lexer functions | |
532 | ||
533 | (define (lex-single-char port loc) | |
534 | "Process tokens which can be recognised by peeking the next character on | |
535 | PORT. If no token can be recognized return #f. LOC is the current port | |
536 | location." | |
537 | (let* ((c (peek-char port))) | |
538 | (cond ((eof-object? c) (read-char port) '*eoi*) | |
539 | ((is-ocurly->token c loc port)) | |
540 | ((is-ccurly->token c loc port)) | |
541 | ((is-oparen->token c loc port)) | |
542 | ((is-cparen->token c loc port)) | |
543 | ((is-not->token c loc port)) | |
544 | ((is-version? c) (lex-version loc port)) | |
545 | ((is-relation? c) (lex-relation loc port)) | |
546 | (else | |
547 | #f)))) | |
548 | ||
549 | (define (lex-word port loc) | |
550 | "Process tokens which can be recognized by reading the next word form PORT. | |
551 | LOC is the current port location." | |
9be54eb1 | 552 | (let* ((w (read-delimited " <>=()\t\n" port 'peek))) |
a4154748 FB |
553 | (cond ((is-if w) (lex-if loc)) |
554 | ((is-test w port) (lex-test w loc)) | |
7716f55c FB |
555 | ((is-true w) (lex-true loc)) |
556 | ((is-false w) (lex-false loc)) | |
ecba50bb DM |
557 | ((is-any w) (lex-any loc)) |
558 | ((is-none w) (lex-none loc)) | |
a4154748 FB |
559 | ((is-and w) (lex-and loc)) |
560 | ((is-or w) (lex-or loc)) | |
94abc848 | 561 | ((is-id w port) (lex-id w loc)) |
a4154748 FB |
562 | (else (unread-string w port) #f)))) |
563 | ||
564 | (define (lex-line port loc) | |
565 | "Process tokens which can be recognised by reading a line from PORT. LOC is | |
566 | the current port location." | |
567 | (let* ((s (read-delimited "\n{}" port 'peek))) | |
568 | (cond | |
a4154748 FB |
569 | ((is-flag s) => (cut lex-flag <> loc)) |
570 | ((is-src-repo s) => (cut lex-src-repo <> loc)) | |
571 | ((is-exec s) => (cut lex-exec <> loc)) | |
572 | ((is-test-suite s) => (cut lex-test-suite <> loc)) | |
314b63e0 | 573 | ((is-custom-setup s) => (cut lex-custom-setup <> loc)) |
a4154748 FB |
574 | ((is-benchmark s) => (cut lex-benchmark <> loc)) |
575 | ((is-lib s) (lex-lib loc)) | |
576 | ((is-else s) (lex-else loc)) | |
959c9d15 RV |
577 | (else (unread-string s port) #f)))) |
578 | ||
579 | (define (lex-property port loc) | |
580 | (let* ((s (read-delimited "\n" port 'peek))) | |
581 | (cond | |
582 | ((is-braced-property s) => (cut lex-braced-property <> loc port)) | |
583 | ((is-layout-property s) => (cut lex-layout-property <> loc port)) | |
584 | (else #f)))) | |
a4154748 FB |
585 | |
586 | (define (lex-token port) | |
587 | (let* ((loc (make-source-location (cabal-file-name) (port-line port) | |
588 | (port-column port) -1 -1))) | |
959c9d15 RV |
589 | (or (lex-single-char port loc) |
590 | (lex-word port loc) | |
591 | (lex-line port loc) | |
592 | (lex-property port loc)))) | |
a4154748 FB |
593 | |
594 | ;; Lexer- and error-function generators | |
595 | ||
596 | (define (errorp) | |
597 | "Generates the lexer error function." | |
598 | (let ((p (current-error-port))) | |
599 | (lambda (message . args) | |
600 | (format p "~a" message) | |
601 | (if (and (pair? args) (lexical-token? (car args))) | |
602 | (let* ((token (car args)) | |
603 | (source (lexical-token-source token)) | |
604 | (line (source-location-line source)) | |
605 | (column (source-location-column source))) | |
606 | (format p "~a " (or (lexical-token-value token) | |
607 | (lexical-token-category token))) | |
608 | (when (and (number? line) (number? column)) | |
609 | (format p "(at line ~a, column ~a)" (1+ line) column))) | |
610 | (for-each display args)) | |
611 | (format p "~%")))) | |
612 | ||
613 | (define (make-lexer port) | |
614 | "Generate the Cabal lexical analyser reading from PORT." | |
615 | (let ((p port)) | |
616 | (lambda () | |
617 | (let ((bol (lex-white-space p (bol? p)))) | |
618 | (check-bol? #f) | |
619 | (if bol (lex-bol p) (lex-token p)))))) | |
620 | ||
621 | (define* (read-cabal #:optional (port (current-input-port)) | |
622 | (file-name #f)) | |
623 | "Read a Cabal file from PORT. FILE-NAME is a string used in error messages. | |
624 | If #f use the function 'port-filename' to obtain it." | |
625 | (let ((cabal-parser (make-cabal-parser))) | |
626 | (parameterize ((cabal-file-name | |
627 | (or file-name (port-filename port) "standard input")) | |
628 | (current-indentation 0) | |
629 | (check-bol? #f) | |
630 | (context-stack (make-stack))) | |
631 | (cabal-parser (make-lexer port) (errorp))))) | |
632 | ||
633 | ;; Part 2: | |
634 | ;; | |
635 | ;; Evaluate the S-expression returned by 'read-cabal'. | |
636 | ||
637 | ;; This defines the object and interface that we provide to access the Cabal | |
638 | ;; file information. Note that this does not include all the pieces of | |
639 | ;; information of the Cabal file, but only the ones we currently are | |
640 | ;; interested in. | |
641 | (define-record-type <cabal-package> | |
ca45da9f | 642 | (make-cabal-package name version revision license home-page source-repository |
a4154748 FB |
643 | synopsis description |
644 | executables lib test-suites | |
d804d0b9 | 645 | flags eval-environment custom-setup) |
a4154748 FB |
646 | cabal-package? |
647 | (name cabal-package-name) | |
648 | (version cabal-package-version) | |
ca45da9f | 649 | (revision cabal-package-revision) |
a4154748 FB |
650 | (license cabal-package-license) |
651 | (home-page cabal-package-home-page) | |
652 | (source-repository cabal-package-source-repository) | |
653 | (synopsis cabal-package-synopsis) | |
654 | (description cabal-package-description) | |
655 | (executables cabal-package-executables) | |
656 | (lib cabal-package-library) ; 'library' is a Scheme keyword | |
657 | (test-suites cabal-package-test-suites) | |
658 | (flags cabal-package-flags) | |
d804d0b9 DM |
659 | (eval-environment cabal-package-eval-environment) ; alist |
660 | (custom-setup cabal-package-custom-setup)) | |
a4154748 FB |
661 | |
662 | (set-record-type-printer! <cabal-package> | |
663 | (lambda (package port) | |
74e667d1 | 664 | (format port "#<cabal-package ~a@~a>" |
a4154748 FB |
665 | (cabal-package-name package) |
666 | (cabal-package-version package)))) | |
667 | ||
668 | (define-record-type <cabal-source-repository> | |
669 | (make-cabal-source-repository use-case type location) | |
670 | cabal-source-repository? | |
671 | (use-case cabal-source-repository-use-case) | |
672 | (type cabal-source-repository-type) | |
673 | (location cabal-source-repository-location)) | |
674 | ||
675 | ;; We need to be able to distinguish the value of a flag from the Scheme #t | |
676 | ;; and #f values. | |
677 | (define-record-type <cabal-flag> | |
678 | (make-cabal-flag name description default manual) | |
679 | cabal-flag? | |
680 | (name cabal-flag-name) | |
681 | (description cabal-flag-description) | |
682 | (default cabal-flag-default) ; 'true or 'false | |
683 | (manual cabal-flag-manual)) ; 'true or 'false | |
684 | ||
685 | (set-record-type-printer! <cabal-flag> | |
686 | (lambda (package port) | |
687 | (format port "#<cabal-flag ~a default:~a>" | |
688 | (cabal-flag-name package) | |
689 | (cabal-flag-default package)))) | |
690 | ||
691 | (define-record-type <cabal-dependency> | |
692 | (make-cabal-dependency name version) | |
693 | cabal-dependency? | |
694 | (name cabal-dependency-name) | |
695 | (version cabal-dependency-version)) | |
696 | ||
697 | (define-record-type <cabal-executable> | |
698 | (make-cabal-executable name dependencies) | |
699 | cabal-executable? | |
700 | (name cabal-executable-name) | |
701 | (dependencies cabal-executable-dependencies)) ; list of <cabal-dependency> | |
702 | ||
703 | (define-record-type <cabal-library> | |
704 | (make-cabal-library dependencies) | |
705 | cabal-library? | |
706 | (dependencies cabal-library-dependencies)) ; list of <cabal-dependency> | |
707 | ||
708 | (define-record-type <cabal-test-suite> | |
709 | (make-cabal-test-suite name dependencies) | |
710 | cabal-test-suite? | |
711 | (name cabal-test-suite-name) | |
712 | (dependencies cabal-test-suite-dependencies)) ; list of <cabal-dependency> | |
713 | ||
314b63e0 DM |
714 | (define-record-type <cabal-custom-setup> |
715 | (make-cabal-custom-setup name dependencies) | |
716 | cabal-custom-setup? | |
e29067d2 | 717 | (name cabal-custom-setup-name) |
314b63e0 DM |
718 | (dependencies cabal-custom-setup-dependencies)) ; list of <cabal-dependency> |
719 | ||
a4154748 FB |
720 | (define (cabal-flags->alist flag-list) |
721 | "Retrun an alist associating the flag name to its default value from a | |
722 | list of <cabal-flag> objects." | |
723 | (map (lambda (flag) (cons (cabal-flag-name flag) (cabal-flag-default flag))) | |
724 | flag-list)) | |
725 | ||
726 | (define (eval-cabal cabal-sexp env) | |
727 | "Given the CABAL-SEXP produced by 'read-cabal', evaluate all conditionals | |
728 | and return a 'cabal-package' object. The values of all tests can be | |
729 | overwritten by specifying the desired value in ENV. ENV must be an alist. | |
730 | The accepted keys are: \"os\", \"arch\", \"impl\" and a name of a flag. The | |
731 | value associated with a flag has to be either \"true\" or \"false\". The | |
732 | value associated with other keys has to conform to the Cabal file format | |
733 | definition." | |
734 | (define (os name) | |
735 | (let ((env-os (or (assoc-ref env "os") "linux"))) | |
736 | (string-match env-os name))) | |
737 | ||
738 | (define (arch name) | |
739 | (let ((env-arch (or (assoc-ref env "arch") "x86_64"))) | |
740 | (string-match env-arch name))) | |
741 | ||
742 | (define (comp-name+version haskell) | |
743 | "Extract the compiler name and version from the string HASKELL." | |
744 | (let* ((matcher-fn (make-rx-matcher "([a-zA-Z0-9_]+)-([0-9.]+)")) | |
745 | (name (or (and=> (matcher-fn haskell) (cut match:substring <> 1)) | |
746 | haskell)) | |
747 | (version (and=> (matcher-fn haskell) (cut match:substring <> 2)))) | |
748 | (values name version))) | |
749 | ||
750 | (define (comp-spec-name+op+version spec) | |
751 | "Extract the compiler specification from SPEC. Return the compiler name, | |
752 | the ordering operation and the version." | |
753 | (let* ((with-ver-matcher-fn (make-rx-matcher | |
754 | "([a-zA-Z0-9_-]+) *([<>=]+) *([0-9.]+) *")) | |
755 | (without-ver-matcher-fn (make-rx-matcher "([a-zA-Z0-9_-]+)")) | |
ecba50bb | 756 | (without-ver-matcher-fn-2 (make-rx-matcher "([a-zA-Z0-9_-]+) (-any|-none)")) |
a4154748 FB |
757 | (name (or (and=> (with-ver-matcher-fn spec) |
758 | (cut match:substring <> 1)) | |
ecba50bb DM |
759 | (and=> (without-ver-matcher-fn-2 spec) |
760 | (cut match:substring <> 1)) | |
a4154748 | 761 | (match:substring (without-ver-matcher-fn spec) 1))) |
ecba50bb DM |
762 | (operator (or (and=> (with-ver-matcher-fn spec) |
763 | (cut match:substring <> 2)) | |
764 | (and=> (without-ver-matcher-fn-2 spec) | |
765 | (cut match:substring <> 2)))) | |
766 | (version (or (and=> (with-ver-matcher-fn spec) | |
767 | (cut match:substring <> 3)) | |
768 | (and=> (without-ver-matcher-fn-2 spec) | |
769 | (cut match:substring <> 2))))) | |
a4154748 FB |
770 | (values name operator version))) |
771 | ||
772 | (define (impl haskell) | |
773 | (let*-values (((comp-name comp-ver) | |
774 | (comp-name+version (or (assoc-ref env "impl") "ghc"))) | |
775 | ((spec-name spec-op spec-ver) | |
776 | (comp-spec-name+op+version haskell))) | |
777 | (if (and spec-ver comp-ver) | |
9be54eb1 FB |
778 | (cond |
779 | ((not (string= spec-name comp-name)) #f) | |
780 | ((string= spec-op "==") (string= spec-ver comp-ver)) | |
781 | ((string= spec-op ">=") (version>=? comp-ver spec-ver)) | |
782 | ((string= spec-op ">") (version>? comp-ver spec-ver)) | |
783 | ((string= spec-op "<=") (not (version>? comp-ver spec-ver))) | |
784 | ((string= spec-op "<") (not (version>=? comp-ver spec-ver))) | |
e39a44f3 DM |
785 | ((string= spec-op "-any") #t) |
786 | ((string= spec-op "-none") #f) | |
9be54eb1 FB |
787 | (else |
788 | (raise (condition | |
789 | (&message (message "Failed to evaluate 'impl' test.")))))) | |
a4154748 | 790 | (string-match spec-name comp-name)))) |
9be54eb1 | 791 | |
a4154748 FB |
792 | (define (cabal-flags) |
793 | (make-cabal-section cabal-sexp 'flag)) | |
794 | ||
795 | (define (flag name) | |
796 | (let ((value (or (assoc-ref env name) | |
797 | (assoc-ref (cabal-flags->alist (cabal-flags)) name)))) | |
798 | (if (eq? value 'false) #f #t))) | |
a4154748 FB |
799 | (define (eval sexp) |
800 | (match sexp | |
801 | (() '()) | |
802 | ;; nested 'if' | |
803 | ((('if predicate true-group false-group) rest ...) | |
804 | (append (if (eval predicate) | |
805 | (eval true-group) | |
806 | (eval false-group)) | |
807 | (eval rest))) | |
808 | (('if predicate true-group false-group) | |
809 | (if (eval predicate) | |
810 | (eval true-group) | |
811 | (eval false-group))) | |
812 | (('flag name) (flag name)) | |
813 | (('os name) (os name)) | |
814 | (('arch name) (arch name)) | |
815 | (('impl name) (impl name)) | |
7716f55c FB |
816 | ('true #t) |
817 | ('false #f) | |
a4154748 FB |
818 | (('not name) (not (eval name))) |
819 | ;; 'and' and 'or' aren't functions, thus we can't use apply | |
820 | (('and args ...) (fold (lambda (e s) (and e s)) #t (eval args))) | |
821 | (('or args ...) (fold (lambda (e s) (or e s)) #f (eval args))) | |
822 | ;; no need to evaluate flag parameters | |
823 | (('section 'flag name parameters) | |
824 | (list 'section 'flag name parameters)) | |
314b63e0 DM |
825 | (('section 'custom-setup parameters) |
826 | (list 'section 'custom-setup parameters)) | |
a4154748 FB |
827 | ;; library does not have a name parameter |
828 | (('section 'library parameters) | |
829 | (list 'section 'library (eval parameters))) | |
830 | (('section type name parameters) | |
831 | (list 'section type name (eval parameters))) | |
832 | (((? string? name) values) | |
833 | (list name values)) | |
834 | ((element rest ...) | |
835 | (cons (eval element) (eval rest))) | |
836 | (_ (raise (condition | |
837 | (&message (message "Failed to evaluate Cabal file. \ | |
838 | See the manual for limitations."))))))) | |
839 | ||
840 | (define (cabal-evaluated-sexp->package evaluated-sexp) | |
841 | (let* ((name (lookup-join evaluated-sexp "name")) | |
842 | (version (lookup-join evaluated-sexp "version")) | |
ca45da9f | 843 | (revision (lookup-join evaluated-sexp "x-revision")) |
a4154748 FB |
844 | (license (lookup-join evaluated-sexp "license")) |
845 | (home-page (lookup-join evaluated-sexp "homepage")) | |
846 | (home-page-or-hackage | |
847 | (if (string-null? home-page) | |
848 | (string-append "http://hackage.haskell.org/package/" name) | |
849 | home-page)) | |
850 | (source-repository (make-cabal-section evaluated-sexp | |
851 | 'source-repository)) | |
852 | (synopsis (lookup-join evaluated-sexp "synopsis")) | |
853 | (description (lookup-join evaluated-sexp "description")) | |
854 | (executables (make-cabal-section evaluated-sexp 'executable)) | |
855 | (lib (make-cabal-section evaluated-sexp 'library)) | |
856 | (test-suites (make-cabal-section evaluated-sexp 'test-suite)) | |
857 | (flags (make-cabal-section evaluated-sexp 'flag)) | |
d804d0b9 | 858 | (eval-environment '()) |
f6078422 RW |
859 | (custom-setup (match (make-cabal-section evaluated-sexp 'custom-setup) |
860 | ((x) x) | |
861 | (_ #f)))) | |
ca45da9f | 862 | (make-cabal-package name version revision license home-page-or-hackage |
a4154748 | 863 | source-repository synopsis description executables lib |
d804d0b9 | 864 | test-suites flags eval-environment custom-setup))) |
a4154748 FB |
865 | |
866 | ((compose cabal-evaluated-sexp->package eval) cabal-sexp)) | |
867 | ||
868 | (define (make-cabal-section sexp section-type) | |
869 | "Given an SEXP as produced by 'read-cabal', produce a list of objects | |
870 | pertaining to SECTION-TYPE sections. SECTION-TYPE must be one of: | |
314b63e0 DM |
871 | 'executable, 'flag, 'test-suite, 'custom-setup, 'source-repository or |
872 | 'library." | |
a4154748 FB |
873 | (filter-map (cut match <> |
874 | (('section (? (cut equal? <> section-type)) name parameters) | |
875 | (case section-type | |
876 | ((test-suite) (make-cabal-test-suite | |
877 | name (dependencies parameters))) | |
314b63e0 DM |
878 | ((custom-setup) (make-cabal-custom-setup |
879 | name (dependencies parameters "setup-depends"))) | |
a4154748 FB |
880 | ((executable) (make-cabal-executable |
881 | name (dependencies parameters))) | |
882 | ((source-repository) (make-cabal-source-repository | |
883 | name | |
884 | (lookup-join parameters "type") | |
885 | (lookup-join parameters "location"))) | |
886 | ((flag) | |
887 | (let* ((default (lookup-join parameters "default")) | |
888 | (default-true-or-false | |
889 | (if (and default (string-ci=? "false" default)) | |
890 | 'false | |
891 | 'true)) | |
892 | (description (lookup-join parameters "description")) | |
893 | (manual (lookup-join parameters "manual")) | |
894 | (manual-true-or-false | |
895 | (if (and manual (string-ci=? "true" manual)) | |
896 | 'true | |
897 | 'false))) | |
898 | (make-cabal-flag name description | |
899 | default-true-or-false | |
900 | manual-true-or-false))) | |
901 | (else #f))) | |
902 | (('section (? (cut equal? <> section-type) lib) parameters) | |
903 | (make-cabal-library (dependencies parameters))) | |
904 | (_ #f)) | |
905 | sexp)) | |
906 | ||
907 | (define* (lookup-join key-values-list key #:optional (delimiter " ")) | |
908 | "Lookup and joint all values pertaining to keys of value KEY in | |
909 | KEY-VALUES-LIST. The optional DELIMITER is used to specify a delimiter string | |
910 | to be added between the values found in different key/value pairs." | |
911 | (string-join | |
912 | (filter-map (cut match <> | |
913 | (((? (lambda(x) (equal? x key))) value) | |
914 | (string-join value delimiter)) | |
915 | (_ #f)) | |
916 | key-values-list) | |
917 | delimiter)) | |
918 | ||
919 | (define dependency-name-version-rx | |
920 | (make-regexp "([a-zA-Z0-9_-]+) *(.*)")) | |
921 | ||
314b63e0 | 922 | (define* (dependencies key-values-list #:optional (key "build-depends")) |
a4154748 FB |
923 | "Return a list of 'cabal-dependency' objects for the dependencies found in |
924 | KEY-VALUES-LIST." | |
314b63e0 | 925 | (let ((deps (string-tokenize (lookup-join key-values-list key ",") |
a4154748 FB |
926 | (char-set-complement (char-set #\,))))) |
927 | (map (lambda (d) | |
928 | (let ((rx-result (regexp-exec dependency-name-version-rx d))) | |
929 | (make-cabal-dependency | |
930 | (match:substring rx-result 1) | |
931 | (match:substring rx-result 2)))) | |
932 | deps))) | |
933 | ||
934 | ;;; cabal.scm ends here |