summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSarah Morgensen <iskarian@mgsn.dev>2021-07-16 21:01:28 -0700
committerMaxim Cournoyer <maxim.cournoyer@gmail.com>2021-07-18 01:57:17 -0400
commit793ba333c6039545684e8af7e384704e76bc0f2f (patch)
tree2705d7a6da51a129ff441820199a485b1cce852b
parent3217a04b0352c2dd13323257b369604eeabfccc3 (diff)
import: go: Upgrade go.mod parser.
Upgrade the go.mod parser to handle the full go.mod spec, and to gracefully handle unexpected/malformed syntax. Restructure parser usage, making the parse tree available for other uses. guix/import/go.scm (parse-go.mod): Parse using (ice-9 peg) instead of regex matching for more robustness. Return a list of directives. (go.mod-directives): New procedure. (go.mod-requirements): Likewise. (go-module->guix-package): Use it. (%go.mod-replace-directive-rx): Remove unused variable. tests/go.scm (testing-parse-mod): Adjust accordingly. (go.mod-requirements) (fixture-go-mod-unparseable) (fixture-go-mod-retract) (fixture-go-mod-strings): New variables. ("parse-go.mod: simple") ("parse-go.mod: comments and unparseable lines") ("parse-go.mod: retract") ("parse-go.mod: raw strings and quoted strings") ("parse-go.mod: complete"): New tests. Signed-off-by: Maxim Cournoyer <maxim.cournoyer@gmail.com>
-rw-r--r--guix/import/go.scm249
-rw-r--r--tests/go.scm133
2 files changed, 262 insertions, 120 deletions
diff --git a/guix/import/go.scm b/guix/import/go.scm
index 24d12acd973..a6e2af215f0 100644
--- a/guix/import/go.scm
+++ b/guix/import/go.scm
@@ -5,7 +5,7 @@
5;;; Copyright © 2021 Maxim Cournoyer <maxim.cournoyer@gmail.com> 5;;; Copyright © 2021 Maxim Cournoyer <maxim.cournoyer@gmail.com>
6;;; Copyright © 2021 Ludovic Courtès <ludo@gnu.org> 6;;; Copyright © 2021 Ludovic Courtès <ludo@gnu.org>
7;;; Copyright © 2021 Xinglu Chen <public@yoctocell.xyz> 7;;; Copyright © 2021 Xinglu Chen <public@yoctocell.xyz>
8;; Copyright © 2021 Sarah Morgensen <iskarian@mgsn.dev> 8;;; Copyright © 2021 Sarah Morgensen <iskarian@mgsn.dev>
9;;; 9;;;
10;;; This file is part of GNU Guix. 10;;; This file is part of GNU Guix.
11;;; 11;;;
@@ -41,6 +41,7 @@
41 #:autoload (guix base32) (bytevector->nix-base32-string) 41 #:autoload (guix base32) (bytevector->nix-base32-string)
42 #:autoload (guix build utils) (mkdir-p) 42 #:autoload (guix build utils) (mkdir-p)
43 #:use-module (ice-9 match) 43 #:use-module (ice-9 match)
44 #:use-module (ice-9 peg)
44 #:use-module (ice-9 rdelim) 45 #:use-module (ice-9 rdelim)
45 #:use-module (ice-9 receive) 46 #:use-module (ice-9 receive)
46 #:use-module (ice-9 regex) 47 #:use-module (ice-9 regex)
@@ -244,129 +245,139 @@ and VERSION and return an input port."
244 (go-path-escape version)))) 245 (go-path-escape version))))
245 (http-fetch* url))) 246 (http-fetch* url)))
246 247
247(define %go.mod-require-directive-rx
248 ;; A line in a require directive is composed of a module path and
249 ;; a version separated by whitespace and an optionnal '//' comment at
250 ;; the end.
251 (make-regexp
252 (string-append
253 "^[[:blank:]]*([^[:blank:]]+)[[:blank:]]+" ;the module path
254 "([^[:blank:]]+)" ;the version
255 "([[:blank:]]+//.*)?"))) ;an optional comment
256
257(define %go.mod-replace-directive-rx
258 ;; ReplaceSpec = ModulePath [ Version ] "=>" FilePath newline
259 ;; | ModulePath [ Version ] "=>" ModulePath Version newline .
260 (make-regexp
261 (string-append
262 "([^[:blank:]]+)" ;the module path
263 "([[:blank:]]+([^[:blank:]]+))?" ;optional version
264 "[[:blank:]]+=>[[:blank:]]+"
265 "([^[:blank:]]+)" ;the file or module path
266 "([[:blank:]]+([^[:blank:]]+))?"))) ;the version (if a module path)
267 248
268(define (parse-go.mod content) 249(define (parse-go.mod content)
269 "Parse the go.mod file CONTENT, returning a list of requirements." 250 "Parse the go.mod file CONTENT, returning a list of directives, comments,
270 ;; We parse only a subset of https://golang.org/ref/mod#go-mod-file-grammar 251and unknown lines. Each sublist begins with a symbol (go, module, require,
271 ;; which we think necessary for our use case. 252replace, exclude, retract, comment, or unknown) and is followed by one or more
272 (define (toplevel requirements replaced) 253sublists. Each sublist begins with a symbol (module-path, version, file-path,
273 "This is the main parser. The results are accumulated in THE REQUIREMENTS 254comment, or unknown) and is followed by the indicated data."
274and REPLACED lists." 255 ;; https://golang.org/ref/mod#go-mod-file-grammar
275 (let ((line (read-line))) 256 (define-peg-pattern NL none "\n")
276 (cond 257 (define-peg-pattern WS none (or " " "\t" "\r"))
277 ((eof-object? line) 258 (define-peg-pattern => none (and (* WS) "=>"))
278 ;; parsing ended, give back the result 259 (define-peg-pattern punctuation none (or "," "=>" "[" "]" "(" ")"))
279 (values requirements replaced)) 260 (define-peg-pattern comment all
280 ((string=? line "require (") 261 (and (ignore "//") (* WS) (* (and (not-followed-by NL) peg-any))))
281 ;; a require block begins, delegate parsing to IN-REQUIRE 262 (define-peg-pattern EOL body (and (* WS) (? comment) NL))
282 (in-require requirements replaced)) 263 (define-peg-pattern block-start none (and (* WS) "(" EOL))
283 ((string=? line "replace (") 264 (define-peg-pattern block-end none (and (* WS) ")" EOL))
284 ;; a replace block begins, delegate parsing to IN-REPLACE 265 (define-peg-pattern any-line body
285 (in-replace requirements replaced)) 266 (and (* WS) (* (and (not-followed-by NL) peg-any)) EOL))
286 ((string-prefix? "require " line) 267
287 ;; a require directive by itself 268 ;; Strings and identifiers
288 (let* ((stripped-line (string-drop line 8))) 269 (define-peg-pattern identifier body
289 (call-with-values 270 (+ (and (not-followed-by (or NL WS punctuation)) peg-any)))
290 (lambda () 271 (define-peg-pattern string-raw body
291 (require-directive requirements replaced stripped-line)) 272 (and (ignore "`") (+ (and (not-followed-by "`") peg-any)) (ignore "`")))
292 toplevel))) 273 (define-peg-pattern string-quoted body
293 ((string-prefix? "replace " line) 274 (and (ignore "\"")
294 ;; a replace directive by itself 275 (+ (or (and (ignore "\\") peg-any)
295 (let* ((stripped-line (string-drop line 8))) 276 (and (not-followed-by "\"") peg-any)))
296 (call-with-values 277 (ignore "\"")))
297 (lambda () 278 (define-peg-pattern string-or-ident body
298 (replace-directive requirements replaced stripped-line)) 279 (and (* WS) (or string-raw string-quoted identifier)))
299 toplevel))) 280
300 (#t 281 (define-peg-pattern version all string-or-ident)
301 ;; unrecognised line, ignore silently 282 (define-peg-pattern module-path all string-or-ident)
302 (toplevel requirements replaced))))) 283 (define-peg-pattern file-path all string-or-ident)
303 284
304 (define (in-require requirements replaced) 285 ;; Non-directive lines
305 (let ((line (read-line))) 286 (define-peg-pattern unknown all any-line)
306 (cond 287 (define-peg-pattern block-line body
307 ((eof-object? line) 288 (or EOL (and (not-followed-by block-end) unknown)))
308 ;; this should never happen here but we ignore silently 289
309 (values requirements replaced)) 290 ;; GoDirective = "go" GoVersion newline .
310 ((string=? line ")") 291 (define-peg-pattern go all (and (ignore "go") version EOL))
311 ;; end of block, coming back to toplevel 292
312 (toplevel requirements replaced)) 293 ;; ModuleDirective = "module" ( ModulePath | "(" newline ModulePath newline ")" ) newline .
313 (#t 294 (define-peg-pattern module all
314 (call-with-values (lambda () 295 (and (ignore "module") (or (and block-start module-path EOL block-end)
315 (require-directive requirements replaced line)) 296 (and module-path EOL))))
316 in-require))))) 297
317 298 ;; The following directives may all be used solo or in a block
318 (define (in-replace requirements replaced) 299 ;; RequireSpec = ModulePath Version newline .
319 (let ((line (read-line))) 300 (define-peg-pattern require all (and module-path version EOL))
320 (cond 301 (define-peg-pattern require-top body
321 ((eof-object? line) 302 (and (ignore "require")
322 ;; this should never happen here but we ignore silently 303 (or (and block-start (* (or require block-line)) block-end) require)))
323 (values requirements replaced)) 304
324 ((string=? line ")") 305 ;; ExcludeSpec = ModulePath Version newline .
325 ;; end of block, coming back to toplevel 306 (define-peg-pattern exclude all (and module-path version EOL))
326 (toplevel requirements replaced)) 307 (define-peg-pattern exclude-top body
327 (#t 308 (and (ignore "exclude")
328 (call-with-values (lambda () 309 (or (and block-start (* (or exclude block-line)) block-end) exclude)))
329 (replace-directive requirements replaced line)) 310
330 in-replace))))) 311 ;; ReplaceSpec = ModulePath [ Version ] "=>" FilePath newline
331 312 ;; | ModulePath [ Version ] "=>" ModulePath Version newline .
332 (define (replace-directive requirements replaced line) 313 (define-peg-pattern original all (or (and module-path version) module-path))
333 "Extract replaced modules and new requirements from the replace directive 314 (define-peg-pattern with all (or (and module-path version) file-path))
334in LINE and add them to the REQUIREMENTS and REPLACED lists." 315 (define-peg-pattern replace all (and original => with EOL))
335 (let* ((rx-match (regexp-exec %go.mod-replace-directive-rx line)) 316 (define-peg-pattern replace-top body
336 (module-path (match:substring rx-match 1)) 317 (and (ignore "replace")
337 (version (match:substring rx-match 3)) 318 (or (and block-start (* (or replace block-line)) block-end) replace)))
338 (new-module-path (match:substring rx-match 4)) 319
339 (new-version (match:substring rx-match 6)) 320 ;; RetractSpec = ( Version | "[" Version "," Version "]" ) newline .
340 (new-replaced (cons (list module-path version) replaced)) 321 (define-peg-pattern range all
341 (new-requirements 322 (and (* WS) (ignore "[") version
342 (if (string-match "^\\.?\\./" new-module-path) 323 (* WS) (ignore ",") version (* WS) (ignore "]")))
343 requirements 324 (define-peg-pattern retract all (and (or range version) EOL))
344 (cons (list new-module-path new-version) requirements)))) 325 (define-peg-pattern retract-top body
345 (values new-requirements new-replaced))) 326 (and (ignore "retract")
346 327 (or (and block-start (* (or retract block-line)) block-end) retract)))
347 (define (require-directive requirements replaced line) 328
348 "Extract requirement from LINE and augment the REQUIREMENTS and REPLACED 329 (define-peg-pattern go-mod body
349lists." 330 (* (and (* WS) (or go module require-top exclude-top replace-top
350 (let* ((rx-match (regexp-exec %go.mod-require-directive-rx line)) 331 retract-top EOL unknown))))
351 (module-path (match:substring rx-match 1)) 332
352 ;; Double-quoted strings were seen in the wild without escape 333 (let ((tree (peg:tree (match-pattern go-mod content)))
353 ;; sequences; trim the quotes to be on the safe side. 334 (keywords '(go module require replace exclude retract comment unknown)))
354 (module-path (string-trim-both module-path #\")) 335 (keyword-flatten keywords tree)))
355 (version (match:substring rx-match 2)))
356 (values (cons (list module-path version) requirements) replaced)))
357
358 (with-input-from-string content
359 (lambda ()
360 (receive (requirements replaced)
361 (toplevel '() '())
362 ;; At last remove the replaced modules from the requirements list.
363 (remove (lambda (r)
364 (assoc (car r) replaced))
365 requirements)))))
366 336
367;; Prevent inlining of this procedure, which is accessed by unit tests. 337;; Prevent inlining of this procedure, which is accessed by unit tests.
368(set! parse-go.mod parse-go.mod) 338(set! parse-go.mod parse-go.mod)
369 339
340(define (go.mod-directives go.mod directive)
341 "Return the list of top-level directive bodies in GO.MOD matching the symbol
342DIRECTIVE."
343 (filter-map (match-lambda
344 (((? (cut eq? <> directive) head) . rest) rest)
345 (_ #f))
346 go.mod))
347
348(define (go.mod-requirements go.mod)
349 "Compute and return the list of requirements specified by GO.MOD."
350 (define (replace directive requirements)
351 (define (maybe-replace module-path new-requirement)
352 ;; Do not allow version updates for indirect dependencies (see:
353 ;; https://golang.org/ref/mod#go-mod-file-replace).
354 (if (and (equal? module-path (first new-requirement))
355 (not (assoc-ref requirements module-path)))
356 requirements
357 (cons new-requirement (alist-delete module-path requirements))))
358
359 (match directive
360 ((('original ('module-path module-path) . _) with . _)
361 (match with
362 (('with ('file-path _) . _)
363 (alist-delete module-path requirements))
364 (('with ('module-path new-module-path) ('version new-version) . _)
365 (maybe-replace module-path
366 (list new-module-path new-version)))))))
367
368 (define (require directive requirements)
369 (match directive
370 ((('module-path module-path) ('version version) . _)
371 (cons (list module-path version) requirements))))
372
373 (let* ((requires (go.mod-directives go.mod 'require))
374 (replaces (go.mod-directives go.mod 'replace))
375 (requirements (fold require '() requires)))
376 (fold replace requirements replaces)))
377
378;; Prevent inlining of this procedure, which is accessed by unit tests.
379(set! go.mod-requirements go.mod-requirements)
380
370(define-record-type <vcs> 381(define-record-type <vcs>
371 (%make-vcs url-prefix root-regex type) 382 (%make-vcs url-prefix root-regex type)
372 vcs? 383 vcs?
@@ -592,7 +603,7 @@ When VERSION is unspecified, the latest version available is used."
592hint: use one of the following available versions ~a\n" 603hint: use one of the following available versions ~a\n"
593 version* available-versions)))) 604 version* available-versions))))
594 (content (fetch-go.mod goproxy module-path version*)) 605 (content (fetch-go.mod goproxy module-path version*))
595 (dependencies+versions (parse-go.mod content)) 606 (dependencies+versions (go.mod-requirements (parse-go.mod content)))
596 (dependencies (if pin-versions? 607 (dependencies (if pin-versions?
597 dependencies+versions 608 dependencies+versions
598 (map car dependencies+versions))) 609 (map car dependencies+versions)))
diff --git a/tests/go.scm b/tests/go.scm
index 2dfdc97eb5c..6749f4585ff 100644
--- a/tests/go.scm
+++ b/tests/go.scm
@@ -1,5 +1,6 @@
1;;; GNU Guix --- Functional package management for GNU 1;;; GNU Guix --- Functional package management for GNU
2;;; Copyright © 2021 François Joulaud <francois.joulaud@radiofrance.com> 2;;; Copyright © 2021 François Joulaud <francois.joulaud@radiofrance.com>
3;;; Copyright © 2021 Sarah Morgensen <iskarian@mgsn.dev>
3;;; 4;;;
4;;; This file is part of GNU Guix. 5;;; This file is part of GNU Guix.
5;;; 6;;;
@@ -31,6 +32,9 @@
31 #:use-module (srfi srfi-64) 32 #:use-module (srfi srfi-64)
32 #:use-module (web response)) 33 #:use-module (web response))
33 34
35(define go.mod-requirements
36 (@@ (guix import go) go.mod-requirements))
37
34(define parse-go.mod 38(define parse-go.mod
35 (@@ (guix import go) parse-go.mod)) 39 (@@ (guix import go) parse-go.mod))
36 40
@@ -95,6 +99,41 @@ replace (
95 99
96") 100")
97 101
102(define fixture-go-mod-unparseable
103 "module my/thing
104go 1.12 // avoid feature X
105require other/thing v1.0.2
106// Security issue: CVE-XXXXX
107exclude old/thing v1.2.3
108new-directive another/thing yet-another/thing
109replace (
110 bad/thing v1.4.5 => good/thing v1.4.5
111 // Unparseable
112 bad/thing [v1.4.5, v1.9.7] => good/thing v2.0.0
113)
114")
115
116(define fixture-go-mod-retract
117 "retract v0.9.1
118
119retract (
120 v1.9.2
121 [v1.0.0, v1.7.9]
122)
123")
124
125(define fixture-go-mod-strings
126 "require `example.com/\"some-repo\"` v1.9.3
127require (
128 `example.com/\"another.repo\"` v1.0.0
129 \"example.com/special!repo\" v9.3.1
130)
131replace \"example.com/\\\"some-repo\\\"\" => `launchpad.net/some-repo` v1.9.3
132replace (
133 \"example.com/\\\"another.repo\\\"\" => launchpad.net/another-repo v1.0.0
134)
135")
136
98(define fixtures-go-check-test 137(define fixtures-go-check-test
99 (let ((version 138 (let ((version
100 "{\"Version\":\"v0.0.0-20201130134442-10cb98267c6c\",\"Time\":\"2020-11-30T13:44:42Z\"}") 139 "{\"Version\":\"v0.0.0-20201130134442-10cb98267c6c\",\"Time\":\"2020-11-30T13:44:42Z\"}")
@@ -178,7 +217,7 @@ require github.com/kr/pretty v0.2.1
178 (string<? (car p1) (car p2))) 217 (string<? (car p1) (car p2)))
179 (test-equal name 218 (test-equal name
180 (sort expected inf?) 219 (sort expected inf?)
181 (sort ((@@ (guix import go) parse-go.mod) input) inf?))) 220 (sort (go.mod-requirements (parse-go.mod input)) inf?)))
182 221
183(testing-parse-mod "parse-go.mod-simple" 222(testing-parse-mod "parse-go.mod-simple"
184 '(("good/thing" "v1.4.5") 223 '(("good/thing" "v1.4.5")
@@ -214,6 +253,98 @@ require github.com/kr/pretty v0.2.1
214 ("github.com/go-check/check" "v0.0.0-20140225173054-eb6ee6f84d0a")) 253 ("github.com/go-check/check" "v0.0.0-20140225173054-eb6ee6f84d0a"))
215 fixture-go-mod-complete) 254 fixture-go-mod-complete)
216 255
256(test-equal "parse-go.mod: simple"
257 `((module (module-path "my/thing"))
258 (go (version "1.12"))
259 (require (module-path "other/thing") (version "v1.0.2"))
260 (require (module-path "new/thing/v2") (version "v2.3.4"))
261 (exclude (module-path "old/thing") (version "v1.2.3"))
262 (replace (original (module-path "bad/thing") (version "v1.4.5"))
263 (with (module-path "good/thing") (version "v1.4.5"))))
264 (parse-go.mod fixture-go-mod-simple))
265
266(test-equal "parse-go.mod: comments and unparseable lines"
267 `((module (module-path "my/thing"))
268 (go (version "1.12") (comment "avoid feature X"))
269 (require (module-path "other/thing") (version "v1.0.2"))
270 (comment "Security issue: CVE-XXXXX")
271 (exclude (module-path "old/thing") (version "v1.2.3"))
272 (unknown "new-directive another/thing yet-another/thing")
273 (replace (original (module-path "bad/thing") (version "v1.4.5"))
274 (with (module-path "good/thing") (version "v1.4.5")))
275 (comment "Unparseable")
276 (unknown "bad/thing [v1.4.5, v1.9.7] => good/thing v2.0.0"))
277 (parse-go.mod fixture-go-mod-unparseable))
278
279(test-equal "parse-go.mod: retract"
280 `((retract (version "v0.9.1"))
281 (retract (version "v1.9.2"))
282 (retract (range (version "v1.0.0") (version "v1.7.9"))))
283 (parse-go.mod fixture-go-mod-retract))
284
285(test-equal "parse-go.mod: raw strings and quoted strings"
286 `((require (module-path "example.com/\"some-repo\"") (version "v1.9.3"))
287 (require (module-path "example.com/\"another.repo\"") (version "v1.0.0"))
288 (require (module-path "example.com/special!repo") (version "v9.3.1"))
289 (replace (original (module-path "example.com/\"some-repo\""))
290 (with (module-path "launchpad.net/some-repo") (version "v1.9.3")))
291 (replace (original (module-path "example.com/\"another.repo\""))
292 (with (module-path "launchpad.net/another-repo") (version "v1.0.0"))))
293 (parse-go.mod fixture-go-mod-strings))
294
295(test-equal "parse-go.mod: complete"
296 `((module (module-path "M"))
297 (go (version "1.13"))
298 (replace (original (module-path "github.com/myname/myproject/myapi"))
299 (with (file-path "./api")))
300 (replace (original (module-path "github.com/mymname/myproject/thissdk"))
301 (with (file-path "../sdk")))
302 (replace (original (module-path "launchpad.net/gocheck"))
303 (with (module-path "github.com/go-check/check")
304 (version "v0.0.0-20140225173054-eb6ee6f84d0a")))
305 (require (module-path "github.com/user/project")
306 (version "v1.1.11"))
307 (require (module-path "github.com/user/project/sub/directory")
308 (version "v1.1.12"))
309 (require (module-path "bitbucket.org/user/project")
310 (version "v1.11.20"))
311 (require (module-path "bitbucket.org/user/project/sub/directory")
312 (version "v1.11.21"))
313 (require (module-path "launchpad.net/project")
314 (version "v1.1.13"))
315 (require (module-path "launchpad.net/project/series")
316 (version "v1.1.14"))
317 (require (module-path "launchpad.net/project/series/sub/directory")
318 (version "v1.1.15"))
319 (require (module-path "launchpad.net/~user/project/branch")
320 (version "v1.1.16"))
321 (require (module-path "launchpad.net/~user/project/branch/sub/directory")
322 (version "v1.1.17"))
323 (require (module-path "hub.jazz.net/git/user/project")
324 (version "v1.1.18"))
325 (require (module-path "hub.jazz.net/git/user/project/sub/directory")
326 (version "v1.1.19"))
327 (require (module-path "k8s.io/kubernetes/subproject")
328 (version "v1.1.101"))
329 (require (module-path "one.example.com/abitrary/repo")
330 (version "v1.1.111"))
331 (require (module-path "two.example.com/abitrary/repo")
332 (version "v0.0.2"))
333 (require (module-path "quoted.example.com/abitrary/repo")
334 (version "v0.0.2"))
335 (replace (original (module-path "two.example.com/abitrary/repo"))
336 (with (module-path "github.com/corp/arbitrary-repo")
337 (version "v0.0.2")))
338 (replace (original (module-path "golang.org/x/sys"))
339 (with (module-path "golang.org/x/sys")
340 (version "v0.0.0-20190813064441-fde4db37ae7a"))
341 (comment "pinned to release-branch.go1.13"))
342 (replace (original (module-path "golang.org/x/tools"))
343 (with (module-path "golang.org/x/tools")
344 (version "v0.0.0-20190821162956-65e3620a7ae7"))
345 (comment "pinned to release-branch.go1.13")))
346 (parse-go.mod fixture-go-mod-complete))
347
217;;; End-to-end tests for (guix import go) 348;;; End-to-end tests for (guix import go)
218(define (mock-http-fetch testcase) 349(define (mock-http-fetch testcase)
219 (lambda (url . rest) 350 (lambda (url . rest)