Add a Go Module Importer

DoneSubmitted by Katherine Cox-Buday.
Details
8 participants
  • Helio Machado
  • JOULAUD François
  • Katherine Cox-Buday
  • dftxbs3e
  • Ludovic Courtès
  • Timmy Douglas
  • Marius Bakke
  • Maxim Cournoyer
Owner
unassigned
Severity
normal
K
K
Katherine Cox-Buday wrote on 23 Oct 2020 16:06
(address . guix-patches@gnu.org)
87sga5kpdp.fsf@gmail.com
From cc92cbcf5ae89891f478f319e955419800bdfcf9 Mon Sep 17 00:00:00 2001From: Katherine Cox-Buday <cox.katherine.e@gmail.com>Date: Thu, 22 Oct 2020 19:40:17 -0500Subject: [PATCH] * guix/import/go.scm: Created Go Importer * guix/scripts/import.scm: Created Go Importer Subcommand * guix/import/go.scm (importers): Added Go Importer Subcommand
--- guix/import/go.scm | 276 +++++++++++++++++++++++++++++++++++++ guix/scripts/import.scm | 2 +- guix/scripts/import/go.scm | 118 ++++++++++++++++ 3 files changed, 395 insertions(+), 1 deletion(-) create mode 100644 guix/import/go.scm create mode 100644 guix/scripts/import/go.scm
Toggle diff (421 lines)diff --git a/guix/import/go.scm b/guix/import/go.scmnew file mode 100644index 0000000000..61009f3565--- /dev/null+++ b/guix/import/go.scm@@ -0,0 +1,276 @@+;;; GNU Guix --- Functional package management for GNU+;;; Copyright © 2020 Katherine Cox-Buday <cox.katherine.e@gmail.com>+;;;+;;; This file is part of GNU Guix.+;;;+;;; GNU Guix is free software; you can redistribute it and/or modify it+;;; under the terms of the GNU General Public License as published by+;;; the Free Software Foundation; either version 3 of the License, or (at+;;; your option) any later version.+;;;+;;; GNU Guix is distributed in the hope that it will be useful, but+;;; WITHOUT ANY WARRANTY; without even the implied warranty of+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the+;;; GNU General Public License for more details.+;;;+;;; You should have received a copy of the GNU General Public License+;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.++(define-module (guix import go)+ #:use-module (ice-9 match)+ #:use-module (ice-9 rdelim)+ #:use-module (ice-9 receive)+ #:use-module (ice-9 regex)+ #:use-module (srfi srfi-1)+ #:use-module (srfi srfi-9)+ #:use-module (guix json)+ #:use-module ((guix download) #:prefix download:)+ #:use-module (guix import utils)+ #:use-module (guix import json)+ #:use-module (guix packages)+ #:use-module (guix upstream)+ #:use-module (guix utils)+ #:use-module ((guix licenses) #:prefix license:)+ #:use-module (guix base16)+ #:use-module (guix base32)+ #:use-module (guix build download)+ #:use-module (web uri)++ #:export (go-module->guix-package+ go-module-recursive-import+ infer-module-root))++(define (escape-capital-letters s)+ "To avoid ambiguity when serving from case-insensitive file systems, the+$module and $version elements are case-encoded by replacing every uppercase+letter with an exclamation mark followed by the corresponding lower-case+letter."+ (let ((escaped-string (string)))+ (string-for-each-index+ (lambda (i)+ (let ((c (string-ref s i)))+ (set! escaped-string+ (string-concatenate+ (list escaped-string+ (if (char-upper-case? c) "!" "")+ (string (char-downcase c)))))))+ s)+ escaped-string))++(define (fetch-latest-version goproxy-url module-path)+ "Fetches the version number of the latest version for MODULE-PATH from the+given GOPROXY-URL server."+ (assoc-ref+ (json-fetch (format #f "~a/~a/@latest" goproxy-url+ (escape-capital-letters module-path)))+ "Version"))++(define (fetch-go.mod goproxy-url module-path version file)+ "Fetches go.mod from the given GOPROXY-URL server for the given MODULE-PATH+and VERSION."+ (url-fetch (format #f "~a/~a/@v/~a.mod" goproxy-url+ (escape-capital-letters module-path)+ (escape-capital-letters version))+ file+ #:print-build-trace? #f))++(define (parse-go.mod go.mod-path)+ "Parses a go.mod file and returns an alist of module path to version."+ (with-input-from-file go.mod-path+ (lambda ()+ (let ((in-require? #f)+ (requirements (list)))+ (do ((line (read-line) (read-line)))+ ((eof-object? line))+ (set! line (string-trim line))+ ;; The parser is either entering, within, exiting, or after the+ ;; require block. The Go toolchain is trustworthy so edge-cases like+ ;; double-entry, etc. need not complect the parser.+ (cond+ ((string=? line "require (")+ (set! in-require? #t))+ ((and in-require? (string=? line ")"))+ (set! in-require? #f))+ (in-require?+ (let* ((requirement (string-split line #\space))+ ;; Modules should be unquoted+ (module-path (string-delete #\" (car requirement)))+ (version (list-ref requirement 1)))+ (set! requirements (acons module-path version requirements))))+ ((string-prefix? "replace" line)+ (let* ((requirement (string-split line #\space))+ (module-path (list-ref requirement 1))+ (new-module-path (list-ref requirement 3))+ (version (list-ref requirement 4)))+ (set! requirements (assoc-remove! requirements module-path))+ (set! requirements (acons new-module-path version requirements))))))+ requirements))))++(define (module-path-without-major-version module-path)+ "Go modules can be appended with a major version indicator,+e.g. /v3. Sometimes it is desirable to work with the root module path. For+instance, for a module path github.com/foo/bar/v3 this function returns+github.com/foo/bar."+ (let ((m (string-match "(.*)\\/v[0-9]+$" module-path)))+ (if m+ (match:substring m 1)+ module-path)))++(define (infer-module-root module-path)+ "Go modules can be defined at any level of a repository's tree, but querying+for the meta tag usually can only be done at the webpage at the root of the+repository. Therefore, it is sometimes necessary to try and derive a module's+root path from its path. For a set of well-known forges, the pattern of what+consists of a module's root page is known before hand."+ ;; See the following URL for the official Go equivalent:+ ;; https://github.com/golang/go/blob/846dce9d05f19a1f53465e62a304dea21b99f910/src/cmd/go/internal/vcs/vcs.go#L1026-L1087+ (define-record-type <scs>+ (make-scs url-prefix root-regex type)+ scs?+ (url-prefix scs-url-prefix)+ (root-regex scs-root-regex)+ (type scs-type))+ (let* ((known-scs+ (list+ (make-scs+ "github.com"+ "^(github\\.com/[A-Za-z0-9_.\\-]+/[A-Za-z0-9_.\\-]+)(/[A-Za-z0-9_.\\-]+)*$"+ 'git)+ (make-scs+ "bitbucket.org"+ "^(bitbucket\\.org/([A-Za-z0-9_.\\-]+/[A-Za-z0-9_.\\-]+))(/[A-Za-z0-9_.\\-]+)*$`"+ 'unknown)+ (make-scs+ "hub.jazz.net/git/"+ "^(hub\\.jazz\\.net/git/[a-z0-9]+/[A-Za-z0-9_.\\-]+)(/[A-Za-z0-9_.\\-]+)*$"+ 'git)+ (make-scs+ "git.apache.org"+ "^(git\\.apache\\.org/[a-z0-9_.\\-]+\\.git)(/[A-Za-z0-9_.\\-]+)*$"+ 'git)+ (make-scs+ "git.openstack.org"+ "^(git\\.openstack\\.org/[A-Za-z0-9_.\\-]+/[A-Za-z0-9_.\\-]+)(\\.git)?(/[A-Za-z0-9_.\\-]+)*$"+ 'git)))+ (scs (find (lambda (scs) (string-prefix? (scs-url-prefix scs) module-path))+ known-scs)))+ (if scs+ (match:substring (string-match (scs-root-regex scs) module-path) 1)+ module-path)))++(define (to-guix-package-name module-path)+ "Converts a module's path to the canonical Guix format for Go packages."+ (string-downcase+ (string-append "go-"+ (string-replace-substring+ (string-replace-substring+ ;; Guix has its own field for version+ (module-path-without-major-version module-path)+ "." "-")+ "/" "-"))))++(define (fetch-module-meta-data module-path)+ "Fetches module meta-data from a module's landing page. This is necessary+because goproxy servers don't currently provide all the information needed to+build a package."+ (let* ((port (http-fetch (string->uri (format #f "https://~a?go-get=1" module-path))))+ (module-metadata #f)+ (meta-tag-prefix "<meta name=\"go-import\" content=\"")+ (meta-tag-prefix-length (string-length meta-tag-prefix)))+ (do ((line (read-line port) (read-line port)))+ ((or (eof-object? line)+ module-metadata))+ (let ((meta-tag-index (string-contains line meta-tag-prefix)))+ (when meta-tag-index+ (let* ((start (+ meta-tag-index meta-tag-prefix-length))+ (end (string-index line #\" start)))+ (set! module-metadata+ (string-split (substring/shared line start end) #\space))))))+ (close-port port)+ module-metadata))++(define (module-meta-data-scs meta-data)+ "Return the source control system specified by a module's meta-data."+ (string->symbol (list-ref meta-data 1)))++(define (module-meta-data-repo-url meta-data goproxy-url)+ "Return the URL where the fetcher which will be used can download the source+control."+ (if (member (module-meta-data-scs meta-data) '(fossil mod))+ goproxy-url+ (list-ref meta-data 2)))++(define (source-uri scs-type scs-repo-url file)+ "Generate the `origin' block of a package depending on what type of source+control system is being used."+ (case scs-type+ ((git)+ `(origin+ (method git-fetch)+ (uri (git-reference+ (url ,scs-repo-url)+ (commit (string-append "v" version))))+ (file-name (git-file-name name version))+ (sha256+ (base32+ ,(guix-hash-url file)))))+ ((hg)+ `(origin+ (method hg-fetch)+ (uri (hg-reference+ (url ,scs-repo-url)+ (changeset ,version)))+ (file-name (format #f "~a-~a-checkout" name version))))+ ((svn)+ `(origin+ (method svn-fetch)+ (uri (svn-reference+ (url ,scs-repo-url)+ (revision (string->number version))+ (recursive? #f)))+ (file-name (format #f "~a-~a-checkout" name version))+ (sha256+ (base32+ ,(guix-hash-url file)))))+ (else+ (raise-exception (format #f "unsupported scs type: ~a" scs-type)))))++(define* (go-module->guix-package module-path #:key (goproxy-url "https://proxy.golang.org"))+ (call-with-temporary-output-file+ (lambda (temp port)+ (let* ((latest-version (fetch-latest-version goproxy-url module-path))+ (go.mod-path (fetch-go.mod goproxy-url module-path latest-version+ temp))+ (dependencies (map car (parse-go.mod temp)))+ (guix-name (to-guix-package-name module-path))+ (root-module-path (infer-module-root module-path))+ ;; SCS type and URL are not included in goproxy information. For+ ;; this we need to fetch it from the official module page.+ (meta-data (fetch-module-meta-data root-module-path))+ (scs-type (module-meta-data-scs meta-data))+ (scs-repo-url (module-meta-data-repo-url meta-data goproxy-url)))+ (values+ `(package+ (name ,guix-name)+ ;; Elide the "v" prefix Go uses+ (version ,(string-trim latest-version #\v))+ (source+ ,(source-uri scs-type scs-repo-url temp))+ (build-system go-build-system)+ ,@(maybe-inputs (map to-guix-package-name dependencies))+ ;; TODO(katco): It would be nice to make an effort to fetch this+ ;; from known forges, e.g. GitHub+ (home-page ,(format #f "https://~a" root-module-path))+ (synopsis "A Go package")+ (description ,(format #f "~a is a Go package." guix-name))+ (license #f))+ dependencies)))))++(define* (go-module-recursive-import package-name+ #:key (goproxy-url "https://proxy.golang.org"))+ (recursive-import package-name #f+ #:repo->guix-package+ (lambda (name _)+ (go-module->guix-package name+ #:goproxy-url goproxy-url))+ #:guix-name to-guix-package-name))diff --git a/guix/scripts/import.scm b/guix/scripts/import.scmindex 0a3863f965..1d2b45d942 100644--- a/guix/scripts/import.scm+++ b/guix/scripts/import.scm@@ -77,7 +77,7 @@ rather than \\n." ;;; (define importers '("gnu" "nix" "pypi" "cpan" "hackage" "stackage" "elpa" "gem"- "cran" "crate" "texlive" "json" "opam"))+ "go" "cran" "crate" "texlive" "json" "opam")) (define (resolve-importer name) (let ((module (resolve-interfacediff --git a/guix/scripts/import/go.scm b/guix/scripts/import/go.scmnew file mode 100644index 0000000000..000039769c--- /dev/null+++ b/guix/scripts/import/go.scm@@ -0,0 +1,118 @@+;;; GNU Guix --- Functional package management for GNU+;;; Copyright © 2020 Katherine Cox-Buday <cox.katherine.e@gmail.com>+;;;+;;; This file is part of GNU Guix.+;;;+;;; GNU Guix is free software; you can redistribute it and/or modify it+;;; under the terms of the GNU General Public License as published by+;;; the Free Software Foundation; either version 3 of the License, or (at+;;; your option) any later version.+;;;+;;; GNU Guix is distributed in the hope that it will be useful, but+;;; WITHOUT ANY WARRANTY; without even the implied warranty of+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the+;;; GNU General Public License for more details.+;;;+;;; You should have received a copy of the GNU General Public License+;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.++(define-module (guix scripts import go)+ #:use-module (guix ui)+ #:use-module (guix utils)+ #:use-module (guix scripts)+ #:use-module (guix import go)+ #:use-module (guix scripts import)+ #:use-module (srfi srfi-1)+ #:use-module (srfi srfi-11)+ #:use-module (srfi srfi-37)+ #:use-module (ice-9 match)+ #:use-module (ice-9 format)+ #:export (guix-import-go))++ +;;;+;;; Command-line options.+;;;++(define %default-options+ '())++(define (show-help)+ (display (G_ "Usage: guix import go PACKAGE-PATH+Import and convert the Go module for PACKAGE-PATH.\n"))+ (display (G_ "+ -h, --help display this help and exit"))+ (display (G_ "+ -V, --version display version information and exit"))+ (display (G_ "+ -r, --recursive generate package expressions for all Go modules\+ that are not yet in Guix"))+ (display (G_ "+ -p, --goproxy=GOPROXY specify which goproxy server to use"))+ (newline)+ (show-bug-report-information))++(define %options+ ;; Specification of the command-line options.+ (cons* (option '(#\h "help") #f #f+ (lambda args+ (show-help)+ (exit 0)))+ (option '(#\V "version") #f #f+ (lambda args+ (show-version-and-exit "guix import go")))+ (option '(#\r "recursive") #f #f+ (lambda (opt name arg result)+ (alist-cons 'recursive #t result)))+ (option '(#\p "goproxy") #t #f+ (lambda (opt name arg result)+ (alist-cons 'goproxy+ (string->symbol arg)+ (alist-delete 'goproxy result))))+ %standard-import-options))++ +;;;+;;; Entry point.+;;;++(define (guix-import-go . args)+ (define (parse-options)+ ;; Return the alist of option values.+ (args-fold* args %options+ (lambda (opt name arg result)+ (leave (G_ "~A: unrecognized option~%") name))+ (lambda (arg result)+ (alist-cons 'argument arg result))+ %default-options))++ (let* ((opts (parse-options))+ (args (filter-map (match-lambda+ (('argument . value)+ value)+ (_ #f))+ (reverse opts))))+ (match args+ ((module-name)+ (if (assoc-ref opts 'recursive)+ (map (match-lambda+ ((and ('package ('name name) . rest) pkg)+ `(define-public ,(string->symbol name)+ ,pkg))+ (_ #f))+ (go-module-recursive-import module-name+ #:goproxy-url+ (or (assoc-ref opts 'goproxy)+ "https://proxy.golang.org")))+ (let ((sexp (go-module->guix-package module-name+ #:goproxy-url+ (or (assoc-ref opts 'goproxy)+ "https://proxy.golang.org"))))+ (unless sexp+ (leave (G_ "failed to download meta-data for module '~a'~%")+ module-name))+ sexp)))+ (()+ (leave (G_ "too few arguments~%")))+ ((many ...)+ (leave (G_ "too many arguments~%"))))))-- 2.28.0
-- Katherine
L
L
Ludovic Courtès wrote on 28 Oct 2020 11:41
(name . Katherine Cox-Buday)(address . cox.katherine.e@gmail.com)(address . 44178@debbugs.gnu.org)
87a6w64opi.fsf@gnu.org
Hi Katherine,
Katherine Cox-Buday <cox.katherine.e@gmail.com> skribis:
Toggle quote (7 lines)>>From cc92cbcf5ae89891f478f319e955419800bdfcf9 Mon Sep 17 00:00:00 2001> From: Katherine Cox-Buday <cox.katherine.e@gmail.com>> Date: Thu, 22 Oct 2020 19:40:17 -0500> Subject: [PATCH] * guix/import/go.scm: Created Go Importer *> guix/scripts/import.scm: Created Go Importer Subcommand * guix/import/go.scm> (importers): Added Go Importer Subcommand
Nice! I think that can make a lot of people happy. :-)
Here’s a quick review. I won’t promise I can reply to followups in thecoming days because with the release preparation going on, I’d ratherfocus on that. So perhaps this patch will have to wait until after thisrelease, but certainly before the next one!
Toggle quote (17 lines)> +(define (escape-capital-letters s)> + "To avoid ambiguity when serving from case-insensitive file systems, the> +$module and $version elements are case-encoded by replacing every uppercase> +letter with an exclamation mark followed by the corresponding lower-case> +letter."> + (let ((escaped-string (string)))> + (string-for-each-index> + (lambda (i)> + (let ((c (string-ref s i)))> + (set! escaped-string> + (string-concatenate> + (list escaped-string> + (if (char-upper-case? c) "!" "")> + (string (char-downcase c)))))))> + s)> + escaped-string))
As a general comment, the coding style in Guix is functional “bydefault” (info "(guix) Coding Style"). That means we almost never use‘set!’ and procedures that modify their arguments.
We also avoid idioms like car/cdr and ‘do’, which are more commonly usedin other Lisps, as you know very well. ;-)
In the case above, I’d probably use ‘string-fold’. The resulting codeshould be easier to reason about and likely more efficient.
Toggle quote (8 lines)> +(define (fetch-latest-version goproxy-url module-path)> + "Fetches the version number of the latest version for MODULE-PATH from the> +given GOPROXY-URL server."> + (assoc-ref> + (json-fetch (format #f "~a/~a/@latest" goproxy-url> + (escape-capital-letters module-path)))> + "Version"))
I’d suggest using ‘define-json-mapping’ from (json) like in the otherimporters.
Toggle quote (15 lines)> +(define (infer-module-root module-path)> + "Go modules can be defined at any level of a repository's tree, but querying> +for the meta tag usually can only be done at the webpage at the root of the> +repository. Therefore, it is sometimes necessary to try and derive a module's> +root path from its path. For a set of well-known forges, the pattern of what> +consists of a module's root page is known before hand."> + ;; See the following URL for the official Go equivalent:> + ;; https://github.com/golang/go/blob/846dce9d05f19a1f53465e62a304dea21b99f910/src/cmd/go/internal/vcs/vcs.go#L1026-L1087> + (define-record-type <scs>> + (make-scs url-prefix root-regex type)> + scs?> + (url-prefix scs-url-prefix)> + (root-regex scs-root-regex)> + (type scs-type))
Maybe VCS as “version control system”? (It took me a while to guesswhat “SCS” meant.)
Toggle quote (18 lines)> +(define (fetch-module-meta-data module-path)> + "Fetches module meta-data from a module's landing page. This is necessary> +because goproxy servers don't currently provide all the information needed to> +build a package."> + (let* ((port (http-fetch (string->uri (format #f "https://~a?go-get=1" module-path))))> + (module-metadata #f)> + (meta-tag-prefix "<meta name=\"go-import\" content=\"")> + (meta-tag-prefix-length (string-length meta-tag-prefix)))> + (do ((line (read-line port) (read-line port)))> + ((or (eof-object? line)> + module-metadata))> + (let ((meta-tag-index (string-contains line meta-tag-prefix)))> + (when meta-tag-index> + (let* ((start (+ meta-tag-index meta-tag-prefix-length))> + (end (string-index line #\" start)))> + (set! module-metadata> + (string-split (substring/shared line start end) #\space))))))
I’d suggest a named ‘let’ or ‘fold’ here.
Likewise, instead of concatenating XML strings (which could lead tomalformed XML), I recommend using SXML: you would create an sexp like
(meta (@ (name "go-import") (content …)))
and at the end pass it to ‘sxml->sxml’ (info "(guile) Reading andWriting XML").
Toggle quote (3 lines)> + (else> + (raise-exception (format #f "unsupported scs type: ~a" scs-type)))))
‘raise-exception’ takes an error condition. In this case, we should use(srfi srfi-34) for ‘raise’ write something like:
(raise (condition (formatted-message (G_ "…" …))))
Toggle quote (7 lines)> +(define* (go-module->guix-package module-path #:key (goproxy-url "https://proxy.golang.org"))> + (call-with-temporary-output-file> + (lambda (temp port)> + (let* ((latest-version (fetch-latest-version goproxy-url module-path))> + (go.mod-path (fetch-go.mod goproxy-url module-path latest-version> + temp))
It seems that ‘go.mod-path’ isn’t used, and thus ‘fetch-go.mod’ &co. aren’t used either, or am I overlooking something?
Toggle quote (2 lines)> + (dependencies (map car (parse-go.mod temp)))
Please use ‘match’ instead, or perhaps define a record type for theabstraction at hand.
Toggle quote (22 lines)> + (guix-name (to-guix-package-name module-path))> + (root-module-path (infer-module-root module-path))> + ;; SCS type and URL are not included in goproxy information. For> + ;; this we need to fetch it from the official module page.> + (meta-data (fetch-module-meta-data root-module-path))> + (scs-type (module-meta-data-scs meta-data))> + (scs-repo-url (module-meta-data-repo-url meta-data goproxy-url)))> + (values> + `(package> + (name ,guix-name)> + ;; Elide the "v" prefix Go uses> + (version ,(string-trim latest-version #\v))> + (source> + ,(source-uri scs-type scs-repo-url temp))> + (build-system go-build-system)> + ,@(maybe-inputs (map to-guix-package-name dependencies))> + ;; TODO(katco): It would be nice to make an effort to fetch this> + ;; from known forges, e.g. GitHub> + (home-page ,(format #f "https://~a" root-module-path))> + (synopsis "A Go package")> + (description ,(format #f "~a is a Go package." guix-name))
Maybe something like “fill it out!” so we don’t get patch submissionswith the default synopsis/description. :-)
Toggle quote (2 lines)> + (license #f))
Likewise.
Two more things: could you (1) and an entry under “Invoking guix import”in doc/guix.texi, and (2) add tests, taking inspiration from theexisting importer tests?
Thank you!
Ludo’.
L
L
Ludovic Courtès wrote on 28 Oct 2020 11:42
(name . Katherine Cox-Buday)(address . cox.katherine.e@gmail.com)(address . 44178@debbugs.gnu.org)
878sbq4oof.fsf@gnu.org
Hi Katherine,
Katherine Cox-Buday <cox.katherine.e@gmail.com> skribis:
Toggle quote (7 lines)>>From cc92cbcf5ae89891f478f319e955419800bdfcf9 Mon Sep 17 00:00:00 2001> From: Katherine Cox-Buday <cox.katherine.e@gmail.com>> Date: Thu, 22 Oct 2020 19:40:17 -0500> Subject: [PATCH] * guix/import/go.scm: Created Go Importer *> guix/scripts/import.scm: Created Go Importer Subcommand * guix/import/go.scm> (importers): Added Go Importer Subcommand
Nice! I think that can make a lot of people happy. :-)
Here’s a quick review. I won’t promise I can reply to followups in thecoming days because with the release preparation going on, I’d ratherfocus on that. So perhaps this patch will have to wait until after thisrelease, but certainly before the next one!
Toggle quote (17 lines)> +(define (escape-capital-letters s)> + "To avoid ambiguity when serving from case-insensitive file systems, the> +$module and $version elements are case-encoded by replacing every uppercase> +letter with an exclamation mark followed by the corresponding lower-case> +letter."> + (let ((escaped-string (string)))> + (string-for-each-index> + (lambda (i)> + (let ((c (string-ref s i)))> + (set! escaped-string> + (string-concatenate> + (list escaped-string> + (if (char-upper-case? c) "!" "")> + (string (char-downcase c)))))))> + s)> + escaped-string))
As a general comment, the coding style in Guix is functional “bydefault” (info "(guix) Coding Style"). That means we almost never use‘set!’ and procedures that modify their arguments.
We also avoid idioms like car/cdr and ‘do’, which are more commonly usedin other Lisps, as you know very well. ;-)
In the case above, I’d probably use ‘string-fold’. The resulting codeshould be easier to reason about and likely more efficient.
Toggle quote (8 lines)> +(define (fetch-latest-version goproxy-url module-path)> + "Fetches the version number of the latest version for MODULE-PATH from the> +given GOPROXY-URL server."> + (assoc-ref> + (json-fetch (format #f "~a/~a/@latest" goproxy-url> + (escape-capital-letters module-path)))> + "Version"))
I’d suggest using ‘define-json-mapping’ from (json) like in the otherimporters.
Toggle quote (15 lines)> +(define (infer-module-root module-path)> + "Go modules can be defined at any level of a repository's tree, but querying> +for the meta tag usually can only be done at the webpage at the root of the> +repository. Therefore, it is sometimes necessary to try and derive a module's> +root path from its path. For a set of well-known forges, the pattern of what> +consists of a module's root page is known before hand."> + ;; See the following URL for the official Go equivalent:> + ;; https://github.com/golang/go/blob/846dce9d05f19a1f53465e62a304dea21b99f910/src/cmd/go/internal/vcs/vcs.go#L1026-L1087> + (define-record-type <scs>> + (make-scs url-prefix root-regex type)> + scs?> + (url-prefix scs-url-prefix)> + (root-regex scs-root-regex)> + (type scs-type))
Maybe VCS as “version control system”? (It took me a while to guesswhat “SCS” meant.)
Toggle quote (18 lines)> +(define (fetch-module-meta-data module-path)> + "Fetches module meta-data from a module's landing page. This is necessary> +because goproxy servers don't currently provide all the information needed to> +build a package."> + (let* ((port (http-fetch (string->uri (format #f "https://~a?go-get=1" module-path))))> + (module-metadata #f)> + (meta-tag-prefix "<meta name=\"go-import\" content=\"")> + (meta-tag-prefix-length (string-length meta-tag-prefix)))> + (do ((line (read-line port) (read-line port)))> + ((or (eof-object? line)> + module-metadata))> + (let ((meta-tag-index (string-contains line meta-tag-prefix)))> + (when meta-tag-index> + (let* ((start (+ meta-tag-index meta-tag-prefix-length))> + (end (string-index line #\" start)))> + (set! module-metadata> + (string-split (substring/shared line start end) #\space))))))
I’d suggest a named ‘let’ or ‘fold’ here.
Likewise, instead of concatenating XML strings (which could lead tomalformed XML), I recommend using SXML: you would create an sexp like
(meta (@ (name "go-import") (content …)))
and at the end pass it to ‘sxml->sxml’ (info "(guile) Reading andWriting XML").
Toggle quote (3 lines)> + (else> + (raise-exception (format #f "unsupported scs type: ~a" scs-type)))))
‘raise-exception’ takes an error condition. In this case, we should use(srfi srfi-34) for ‘raise’ write something like:
(raise (condition (formatted-message (G_ "…" …))))
Toggle quote (7 lines)> +(define* (go-module->guix-package module-path #:key (goproxy-url "https://proxy.golang.org"))> + (call-with-temporary-output-file> + (lambda (temp port)> + (let* ((latest-version (fetch-latest-version goproxy-url module-path))> + (go.mod-path (fetch-go.mod goproxy-url module-path latest-version> + temp))
It seems that ‘go.mod-path’ isn’t used, and thus ‘fetch-go.mod’ &co. aren’t used either, or am I overlooking something?
Toggle quote (2 lines)> + (dependencies (map car (parse-go.mod temp)))
Please use ‘match’ instead, or perhaps define a record type for theabstraction at hand.
Toggle quote (22 lines)> + (guix-name (to-guix-package-name module-path))> + (root-module-path (infer-module-root module-path))> + ;; SCS type and URL are not included in goproxy information. For> + ;; this we need to fetch it from the official module page.> + (meta-data (fetch-module-meta-data root-module-path))> + (scs-type (module-meta-data-scs meta-data))> + (scs-repo-url (module-meta-data-repo-url meta-data goproxy-url)))> + (values> + `(package> + (name ,guix-name)> + ;; Elide the "v" prefix Go uses> + (version ,(string-trim latest-version #\v))> + (source> + ,(source-uri scs-type scs-repo-url temp))> + (build-system go-build-system)> + ,@(maybe-inputs (map to-guix-package-name dependencies))> + ;; TODO(katco): It would be nice to make an effort to fetch this> + ;; from known forges, e.g. GitHub> + (home-page ,(format #f "https://~a" root-module-path))> + (synopsis "A Go package")> + (description ,(format #f "~a is a Go package." guix-name))
Maybe something like “fill it out!” so we don’t get patch submissionswith the default synopsis/description. :-)
Toggle quote (2 lines)> + (license #f))
Likewise.
Two more things: could you (1) and an entry under “Invoking guix import”in doc/guix.texi, and (2) add tests, taking inspiration from theexisting importer tests?
Thank you!
Ludo’.
M
M
Marius Bakke wrote on 10 Nov 2020 21:26
(name . Helio Machado)(address . 0x2b3bfa0@gmail.com)
87blg5dkla.fsf@gnu.org
Katherine Cox-Buday <cox.katherine.e@gmail.com> writes:
Toggle quote (7 lines)>>From cc92cbcf5ae89891f478f319e955419800bdfcf9 Mon Sep 17 00:00:00 2001> From: Katherine Cox-Buday <cox.katherine.e@gmail.com>> Date: Thu, 22 Oct 2020 19:40:17 -0500> Subject: [PATCH] * guix/import/go.scm: Created Go Importer *> guix/scripts/import.scm: Created Go Importer Subcommand * guix/import/go.scm> (importers): Added Go Importer Subcommand
I just want to say thanks a lot for this! I tested it, and it prettymuch works as advertised.
Cc'ing Helio who was working on a Go importer as well recently.
-----BEGIN PGP SIGNATURE-----
iQFDBAEBCgAtFiEEu7At3yzq9qgNHeZDoqBt8qM6VPoFAl+q94EPHG1hcml1c0BnbnUub3JnAAoJEKKgbfKjOlT6smgH/ibghemWudkskluP5si3VgUb9JO49uiUgDv3CaKbs/SOvySUPFgl+8ceLWKgLTQeNVGBgcRBA+aiL0hgAYw/JTBndMHEJ1aZMevwWUDhIMojlbtfw8wBJuVROjPKcXZW5WXZIKQ1GyqpQ/AP72uZcgI7xB0Qbh0ztQq4uhZcEiDvgCGfsFWOphpuqxoB2rUpEruLaUj4kkEhccArt79WbgVLF3lTijM1FhffMk3r+4MCdmCL3pZsnDcoY/qNl4jGxqP2mj+rszQFqR6da8gIEhkM52NI3U9VoWLpgaLq4y4jQp4cdMPY4r2WE5jm7KryGcP/4KHFmf4g/8cvD0Lj5dk==hW6A-----END PGP SIGNATURE-----
H
H
Helio Machado wrote on 11 Nov 2020 02:23
(address . 44178@debbugs.gnu.org)
CANe01w5DxwrF+APLbcNMaUhs4h05EXC1HdRJ_YT8CRy6ULxUYw@mail.gmail.com
Thanks for the ping, Marius! I've been quite busy with some yak shavingtasks, but my importer is already working and has some interestingimprovements, like elegant module fetching from the official module proxy,license extraction and recursive import support.
I need to fix an esoteric bug that trips the kernel's out-of-memory killerwhen building a derivation with dependencies, but the importer part workspretty well.
You can take a look to [my changes][1] for some readily backportable ideas,like [the compact algorithm for uppercase path escaping][2].
Please forgive the code quality and the possible backwards-compatibilitymistakes; this is an unfinished proof of concept.
$ guix import go-modules -r github.com/FiloSottile/age # Please refer tothe issue 43872 for more information about the resting environment
[1]:https://github.com/0x2b3bfa0/guix-go-modules/commit/5defe897065c5d3e63740932b360474132c77877[2]:https://github.com/0x2b3bfa0/guix-go-modules/blob/main/guix/build-system/go.scm#L65-L71
On Wed, 11 Nov 2020 at 02:19, Helio Machado <0x2b3bfa0@gmail.com> wrote:
Toggle quote (38 lines)> Thanks for the ping, Marius! I've been quite busy with some yak shaving> tasks, but my importer is already working and has some interesting> improvements, like elegant module fetching from the official module proxy,> license extraction and recursive import support.>> I need to fix an esoteric bug that trips the kernel's out-of-memory killer> when building a derivation with dependencies, but the importer part works> pretty well.>> You can take a look to [my changes][1] for some readily backportable> ideas, like [the compact algorithm for uppercase path escaping][2].>> Please forgive the code quality and the possible backwards-compatibility> mistakes; this is an unfinished proof of concept.>> [1]:> https://github.com/0x2b3bfa0/guix-go-modules/commit/5defe897065c5d3e63740932b360474132c77877> [2]:> https://github.com/0x2b3bfa0/guix-go-modules/blob/main/guix/build-system/go.scm#L65-L71>> On Tue, 10 Nov 2020 at 21:26, Marius Bakke <marius@gnu.org> wrote:>>> Katherine Cox-Buday <cox.katherine.e@gmail.com> writes:>>>> >>From cc92cbcf5ae89891f478f319e955419800bdfcf9 Mon Sep 17 00:00:00 2001>> > From: Katherine Cox-Buday <cox.katherine.e@gmail.com>>> > Date: Thu, 22 Oct 2020 19:40:17 -0500>> > Subject: [PATCH] * guix/import/go.scm: Created Go Importer *>> > guix/scripts/import.scm: Created Go Importer Subcommand *>> guix/import/go.scm>> > (importers): Added Go Importer Subcommand>>>> I just want to say thanks a lot for this! I tested it, and it pretty>> much works as advertised.>>>> Cc'ing Helio who was working on a Go importer as well recently.>>>
Attachment: file
K
K
Katherine Cox-Buday wrote on 11 Nov 2020 21:48
(name . Marius Bakke)(address . marius@gnu.org)
87v9ebpqlx.fsf@gmail.com
Marius Bakke <marius@gnu.org> writes:
Toggle quote (3 lines)> I just want to say thanks a lot for this! I tested it, and it pretty> much works as advertised.
You're very welcome!
I have more changes locally which fix some edge-cases. I'm using`go-ethereum` as my test case since someone mentioned that. Plus I needto make some of the changes Ludovic pointed out. Still, we're underway!
-- Katherine
D
D
dftxbs3e wrote on 9 Dec 2020 15:22
(address . 44178@debbugs.gnu.org)
9395ba7c7499d6c4423982486a0a0fb31cb76e93.camel@free.fr
Thanks a lot for this!
I'm getting some error trying to use it (patching on top of8e2aad26ae9b7365db83d4f6c74e9e79c57766a6), maybe that's fixed in yourlocal changes?
$ ./pre-inst-env guix import go -r github.com/syncthing/syncthingWARNING: (guix import go): `url-fetch' imported from both (guix importutils) and (guix build download)Backtrace:In ice-9/boot-9.scm: 1736:10 7 (with-exception-handler _ _ #:unwind? _ # _)In unknown file: 6 (apply-smob/0 #<thunk 7ff10807d9a0>)In ice-9/boot-9.scm: 718:2 5 (call-with-prompt _ _ #<procedure default-prompt-handle…>)In ice-9/eval.scm: 619:8 4 (_ #(#(#<directory (guile-user) 7ff107cadf00>)))In guix/ui.scm: 2127:12 3 (run-guix-command _ . _)In guix/scripts/import.scm: 120:11 2 (guix-import . _)In ice-9/eval.scm: 159:9 1 (_ #(#(#(#(#(#(#(#(#(#<directo…>) …) …) …) …) …) …) …) …))In guix/import/utils.scm: 429:0 0 (recursive-import _ #:repo->guix-package _ #:guix-name _…)
guix/import/utils.scm:429:0: In procedure recursive-import:Invalid keyword: #f
D
D
dftxbs3e wrote on 10 Dec 2020 03:42
(address . 44178@debbugs.gnu.org)
bc9fc787f49a9722396ed97554e2a7c634f830a0.camel@free.fr
On Wed, 2020-12-09 at 15:22 +0100, dftxbs3e wrote:
Toggle quote (34 lines)> Thanks a lot for this!> > I'm getting some error trying to use it (patching on top of> 8e2aad26ae9b7365db83d4f6c74e9e79c57766a6), maybe that's fixed in your> local changes?> > $ ./pre-inst-env guix import go -r github.com/syncthing/syncthing> WARNING: (guix import go): `url-fetch' imported from both (guix> import> utils) and (guix build download)> Backtrace:> In ice-9/boot-9.scm:> 1736:10 7 (with-exception-handler _ _ #:unwind? _ # _)> In unknown file:> 6 (apply-smob/0 #<thunk 7ff10807d9a0>)> In ice-9/boot-9.scm:> 718:2 5 (call-with-prompt _ _ #<procedure default-prompt-> handle…>)> In ice-9/eval.scm:> 619:8 4 (_ #(#(#<directory (guile-user) 7ff107cadf00>)))> In guix/ui.scm:> 2127:12 3 (run-guix-command _ . _)> In guix/scripts/import.scm:> 120:11 2 (guix-import . _)> In ice-9/eval.scm:> 159:9 1 (_ #(#(#(#(#(#(#(#(#(#<directo…>) …) …) …) …) …) …) …)> …))> In guix/import/utils.scm:> 429:0 0 (recursive-import _ #:repo->guix-package _ #:guix-name _> …)> > guix/import/utils.scm:429:0: In procedure recursive-import:> Invalid keyword: #f
I could fix it using the attached patch!
However, I noticed it doesnt pin versions in GNU Guix to what they arein go.mod file, is that expected? It always takes the latest. It mightwork but I am thinking it might cause breakage at some point?
Thank you!
Toggle diff (25 lines)diff --git a/guix/import/go.scm b/guix/import/go.scmindex 61009f3565..c7a1b1a5d4 100644--- a/guix/import/go.scm+++ b/guix/import/go.scm@@ -23,7 +23,7 @@ #:use-module (ice-9 regex) #:use-module (srfi srfi-1) #:use-module (srfi srfi-9)- #:use-module (guix json)+ #:use-module (json) #:use-module ((guix download) #:prefix download:) #:use-module (guix import utils) #:use-module (guix import json)@@ -268,9 +268,9 @@ control system is being used." (define* (go-module-recursive-import package-name #:key (goproxy-url "https://proxy.golang.org"))- (recursive-import package-name #f+ (recursive-import package-name #:repo->guix-package- (lambda (name _)+ (lambda* (name #:key repo version) (go-module->guix-package name #:goproxy-url goproxy-url)) #:guix-name to-guix-package-name))
D
D
dftxbs3e wrote on 10 Dec 2020 04:14
(address . 44178@debbugs.gnu.org)
97498b5966a4e1cd64bc85ec30bbd8007df09173.camel@free.fr
It now fails with:
$ ./pre-inst-env guix import go -r github.com/hashicorp/consul/apiWARNING: (guix import go): `url-fetch' imported from both (guix importutils) and (guix build download)
Starting download of /tmp/guix-file.i8tqa2From https://proxy.golang.org/github.com/hashicorp/consul/api/@v/v1.8.0.mod... v1.8.0.mod 424B 334KiB/s 00:00[##################] 100.0%Backtrace:In ice-9/boot-9.scm: 1736:10 17 (with-exception-handler _ _ #:unwind? _ # _)In unknown file: 16 (apply-smob/0 #<thunk 7fc5871054a0>)In ice-9/boot-9.scm: 718:2 15 (call-with-prompt _ _ #<procedure default-prompt-handle…>)In ice-9/eval.scm: 619:8 14 (_ #(#(#<directory (guile-user) 7fc586d40f00>)))In guix/ui.scm: 2127:12 13 (run-guix-command _ . _)In guix/scripts/import.scm: 120:11 12 (guix-import . _)In ice-9/eval.scm: 159:9 11 (_ _)In guix/import/utils.scm: 458:31 10 (recursive-import _ #:repo->guix-package _ #:guix-name _…) 449:33 9 (lookup-node "github.com/hashicorp/consul/api" #f)In guix/utils.scm: 697:8 8 (call-with-temporary-output-file _)In ice-9/eval.scm: 293:34 7 (_ #(#(#(#(#<directory (guix import go) 7fc…> …) …) …) …)) 159:9 6 (_ #(#(#(#(#<directory (guix import go) 7fc…> …) …) …) …))In ice-9/ports.scm: 445:17 5 (call-with-input-file _ _ #:binary _ #:encoding _ # _) 470:4 4 (_ _)In ice-9/eval.scm: 619:8 3 (_ #(#(#(#<directory (guix import go) 7fc584cbef00>)) …)) 619:8 2 (_ #(#(#<directory (guix import go) 7fc584cbef00> # …) …)) 293:34 1 (_ #(#(#(#(#(#<directory (guix import go…> …) …) …) …) …))In unknown file: 0 (list-ref ("replace" "github.com/hashicorp/consul/s…" …)…)
ERROR: In procedure list-ref:In procedure list-ref: Argument 2 out of range: 4

It's probably because the go.mod file contains a self-referencingreplace line (seems unsupported by the code):
module github.com/hashicorp/consul/api
go 1.12
replace github.com/hashicorp/consul/sdk => ../sdk
require ( github.com/hashicorp/consul/sdk v0.7.0 github.com/hashicorp/go-cleanhttp v0.5.1 github.com/hashicorp/go-hclog v0.12.0 github.com/hashicorp/go-rootcerts v1.0.2 github.com/hashicorp/go-uuid v1.0.1 github.com/hashicorp/serf v0.9.5 github.com/mitchellh/mapstructure v1.1.2 github.com/stretchr/testify v1.4.0)
K
K
Katherine Cox-Buday wrote on 23 Jan 23:41 +0100
Re: [PATCH] Create importer for Go modules
(name . JOULAUD François)(address . Francois.JOULAUD@radiofrance.com)
87r1mb6zu9.fsf@gmail.com
Thanks so much for the patches, Helio, Joulaud!
I apologize for the long delay before looking at this again. My timeright now is extremely limited due to COVID-19 related childcare andactivities. I was negligent and left a patch to bitrot on mycomputer[1]. This patch supersedes it.
In addition to the things this patch corrects, I was/am working on a fewother bugs:
- There are valid Go Module paths which when queried will not serve the requisite meta tag. I had modified `fetch-module-meta-data` to recursively walk up the module path searching for a valid meta tag (see diff[1]).
- I think Joulaud's patch covers this, but replacements relative to a module are a possibility.
- For reasons Joulaud calls out, a simple line-parser of the HTML for a module is not sufficient. Since we are pulling pages down from the wider internet, we should fall back on a library made for parsing HTML so we handle any edge-cases (e.g. meta tags split across lines). I am currently looking at `sxml`, and if that doesn't pan out `htmlprag`
- Some module homepages issue HTTP redirects. Last time I tested this, `http-fetch` does not handle this properly.
I think that covers everything.
I have pushed everything (including Joulaud's patch with appropriateattribution) here[2]. I am admittedly new at using email to organizecode changes, but using a forge seems easier.
[1] https://github.com/guix-mirror/guix/commit/cce35c6d68a9bddf9558e85d2cb88be323da9247[2] https://github.com/kat-co/guix/tree/create-go-importer
Can I suggest we coordinate there, or is that too much of an imposition?
-Katherine
JOULAUD François <Francois.JOULAUD@radiofrance.com> writes:
Toggle quote (876 lines)> This patch add a `guix import go` command.>> It was tested with several big repositories and seems to mostly work for> the import part (because building Guix packages is an other story). There> is still bugs blocking e.g. use of any k8s.io modules.>> * guix/import/go.scm: Created Go Importer> * guix/scripts/import.scm: Created Go Importer Subcommand> * guix/import/go.scm (importers): Added Go Importer Subcommand>> Signed-off-by: Francois Joulaud <francois.joulaud@radiofrance.com>> ---> The patch is a rebased and modified version of the one proposed by> Katherine Cox-Buday.>> Notable modifications are :> - move from (guix json) to (json)> - new parse-go.mod with no "set!" and parsing some go.mod which were in> error before> - adding comments (maybe too much comments)> - renamed SCS to VCS to be in accordance with vocabulary in use in Guix> and in Go worlds> - replacing escape-capital-letters by Helio Machado's go-path-escape> - no pruning of major version in go module names as they are considered> as completely different artefacts by Go programmers> - fixed recursive-import probably broken by the rebase> - force usage of url-fetch from (guix build download)>> I would be happy to hear about problems and perspective for this patch and> will now focus on my next step which is actually building any package.>> Hope I CCed the right persons, I am not really aware of applicable> netiquette here.>> Interdiff :> diff --git a/guix/import/go.scm b/guix/import/go.scm> index 61009f3565..7f5f300f0a 100644> --- a/guix/import/go.scm> +++ b/guix/import/go.scm> @@ -1,5 +1,7 @@> ;;; GNU Guix --- Functional package management for GNU> ;;; Copyright © 2020 Katherine Cox-Buday <cox.katherine.e@gmail.com>> +;;; Copyright © 2020 Helio Machado <0x2b3bfa0+guix@googlemail.com>> +;;; Copyright © 2021 François Joulaud <francois.joulaud@radiofrance.com>> ;;;> ;;; This file is part of GNU Guix.> ;;;> @@ -16,6 +18,21 @@> ;;; You should have received a copy of the GNU General Public License> ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.> > +;;; (guix import golang) wants to make easier to create Guix package> +;;; declaration for Go modules.> +;;;> +;;; Modules in Go are "collection of related Go packages" which are> +;;; "the unit of source code interchange and versioning".> +;;; Modules are generally hosted in a repository.> +;;;> +;;; At this point it should handle correctly modules which> +;;; - have only Go dependencies;> +;;; - use go.mod;> +;;; - and are accessible from proxy.golang.org (or configured GOPROXY).> +;;;> +;;; We translate Go module paths to a Guix package name under the> +;;; assumption that there will be no collision.> +> (define-module (guix import go)> #:use-module (ice-9 match)> #:use-module (ice-9 rdelim)> @@ -23,7 +40,7 @@> #:use-module (ice-9 regex)> #:use-module (srfi srfi-1)> #:use-module (srfi srfi-9)> - #:use-module (guix json)> + #:use-module (json)> #:use-module ((guix download) #:prefix download:)> #:use-module (guix import utils)> #:use-module (guix import json)> @@ -33,88 +50,129 @@> #:use-module ((guix licenses) #:prefix license:)> #:use-module (guix base16)> #:use-module (guix base32)> - #:use-module (guix build download)> + #:use-module ((guix build download) #:prefix build-download:)> #:use-module (web uri)> > #:export (go-module->guix-package> go-module-recursive-import> infer-module-root))> > -(define (escape-capital-letters s)> - "To avoid ambiguity when serving from case-insensitive file systems, the> -$module and $version elements are case-encoded by replacing every uppercase> -letter with an exclamation mark followed by the corresponding lower-case> -letter."> - (let ((escaped-string (string)))> - (string-for-each-index> - (lambda (i)> - (let ((c (string-ref s i)))> - (set! escaped-string> - (string-concatenate> - (list escaped-string> - (if (char-upper-case? c) "!" "")> - (string (char-downcase c)))))))> - s)> - escaped-string))> +(define (go-path-escape path)> + "Escape a module path by replacing every uppercase letter with an exclamation> +mark followed with its lowercase equivalent, as per the module Escaped Paths> +specification. https://godoc.org/golang.org/x/mod/module#hdr-Escaped_Paths"> + (define (escape occurrence)> + (string-append "!" (string-downcase (match:substring occurrence))))> + (regexp-substitute/global #f "[A-Z]" path 'pre escape 'post))> +> > (define (fetch-latest-version goproxy-url module-path)> "Fetches the version number of the latest version for MODULE-PATH from the> given GOPROXY-URL server."> (assoc-ref> (json-fetch (format #f "~a/~a/@latest" goproxy-url> - (escape-capital-letters module-path)))> + (go-path-escape module-path)))> "Version"))> > (define (fetch-go.mod goproxy-url module-path version file)> "Fetches go.mod from the given GOPROXY-URL server for the given MODULE-PATH> and VERSION."> - (url-fetch (format #f "~a/~a/@v/~a.mod" goproxy-url> - (escape-capital-letters module-path)> - (escape-capital-letters version))> - file> - #:print-build-trace? #f))> + (let ((url (format #f "~a/~a/@v/~a.mod" goproxy-url> + (go-path-escape module-path)> + (go-path-escape version))))> + (parameterize ((current-output-port (current-error-port)))> + (build-download:url-fetch url> + file> + #:print-build-trace? #f))))> > (define (parse-go.mod go.mod-path)> - "Parses a go.mod file and returns an alist of module path to version."> + "PARSE-GO.MOD takes a filename in GO.MOD-PATH and extract a list of> +requirements from it."> + ;; We parse only a subset of https://golang.org/ref/mod#go-mod-file-grammar> + ;; which we think necessary for our use case.> + (define (toplevel results)> + "Main parser, RESULTS is a pair of alist serving as accumulator for> + all encountered requirements and replacements."> + (let ((line (read-line)))> + (cond> + ((eof-object? line)> + ;; parsing ended, give back the result> + results)> + ((string=? line "require (")> + ;; a require block begins, delegate parsing to IN-REQUIRE> + (in-require results))> + ((string-prefix? "require " line)> + ;; a require directive by itself> + (let* ((stripped-line (string-drop line 8))> + (new-results (require-directive results stripped-line)))> + (toplevel new-results)))> + ((string-prefix? "replace " line)> + ;; a replace directive by itself> + (let* ((stripped-line (string-drop line 8))> + (new-results (replace-directive results stripped-line)))> + (toplevel new-results)))> + (#t> + ;; unrecognised line, ignore silently> + (toplevel results)))))> + (define (in-require results)> + (let ((line (read-line)))> + (cond> + ((eof-object? line)> + ;; this should never happen here but we ignore silently> + results)> + ((string=? line ")")> + ;; end of block, coming back to toplevel> + (toplevel results))> + (#t> + (in-require (require-directive results line))))))> + (define (replace-directive results line)> + "Extract replaced modules and new requirements from replace directive> + in LINE and add to RESULTS."> + ;; ReplaceSpec = ModulePath [ Version ] "=>" FilePath newline> + ;; | ModulePath [ Version ] "=>" ModulePath Version newline .> + (let* ((requirements (car results))> + (replaced (cdr results))> + (re (string-concatenate> + '("([^[:blank:]]+)([[:blank:]]+([^[:blank:]]+))?"> + "[[:blank:]]+" "=>" "[[:blank:]]+"> + "([^[:blank:]]+)([[:blank:]]+([^[:blank:]]+))?")))> + (match (string-match re line))> + (module-path (match:substring match 1))> + (version (match:substring match 3))> + (new-module-path (match:substring match 4))> + (new-version (match:substring match 6))> + (new-replaced (acons module-path version replaced))> + (new-requirements> + (if (string-match "^\\.?\\./" new-module-path)> + requirements> + (acons new-module-path new-version requirements))))> + (cons new-requirements new-replaced)))> + (define (require-directive results line)> + "Extract requirement from LINE and add it to RESULTS."> + (let* ((requirements (car results))> + (replaced (cdr results))> + ;; A line in a require directive is composed of a module path and> + ;; a version separated by whitespace and an optionnal '//' comment at> + ;; the end.> + (re (string-concatenate> + '("^[[:blank:]]*"> + "([^[:blank:]]+)[[:blank:]]+([^[:blank:]]+)"> + "([[:blank:]]+//.*)?")))> + (match (string-match re line))> + (module-path (match:substring match 1))> + (version (match:substring match 2)))> + (cons (acons module-path version requirements) replaced)))> (with-input-from-file go.mod-path> (lambda ()> - (let ((in-require? #f)> - (requirements (list)))> - (do ((line (read-line) (read-line)))> - ((eof-object? line))> - (set! line (string-trim line))> - ;; The parser is either entering, within, exiting, or after the> - ;; require block. The Go toolchain is trustworthy so edge-cases like> - ;; double-entry, etc. need not complect the parser.> - (cond> - ((string=? line "require (")> - (set! in-require? #t))> - ((and in-require? (string=? line ")"))> - (set! in-require? #f))> - (in-require?> - (let* ((requirement (string-split line #\space))> - ;; Modules should be unquoted> - (module-path (string-delete #\" (car requirement)))> - (version (list-ref requirement 1)))> - (set! requirements (acons module-path version requirements))))> - ((string-prefix? "replace" line)> - (let* ((requirement (string-split line #\space))> - (module-path (list-ref requirement 1))> - (new-module-path (list-ref requirement 3))> - (version (list-ref requirement 4)))> - (set! requirements (assoc-remove! requirements module-path))> - (set! requirements (acons new-module-path version requirements))))))> - requirements))))> -> -(define (module-path-without-major-version module-path)> - "Go modules can be appended with a major version indicator,> -e.g. /v3. Sometimes it is desirable to work with the root module path. For> -instance, for a module path github.com/foo/bar/v3 this function returns> -github.com/foo/bar."> - (let ((m (string-match "(.*)\\/v[0-9]+$" module-path)))> - (if m> - (match:substring m 1)> - module-path)))> + (let* ((results (toplevel '(() . ())))> + (requirements (car results))> + (replaced (cdr results)))> + ;; At last we remove replaced modules from the requirements list> + (fold> + (lambda (replacedelem requirements)> + (alist-delete! (car replacedelem) requirements))> + requirements> + replaced)))))> > (define (infer-module-root module-path)> "Go modules can be defined at any level of a repository's tree, but querying> @@ -124,38 +182,42 @@ root path from its path. For a set of well-known forges, the pattern of what> consists of a module's root page is known before hand."> ;; See the following URL for the official Go equivalent:> ;; https://github.com/golang/go/blob/846dce9d05f19a1f53465e62a304dea21b99f910/src/cmd/go/internal/vcs/vcs.go#L1026-L1087> - (define-record-type <scs>> - (make-scs url-prefix root-regex type)> - scs?> - (url-prefix scs-url-prefix)> - (root-regex scs-root-regex)> - (type scs-type))> - (let* ((known-scs> + ;;> + ;; FIXME: handle module path with VCS qualifier as described in> + ;; https://golang.org/ref/mod#vcs-find and> + ;; https://golang.org/cmd/go/#hdr-Remote_import_paths> + (define-record-type <vcs>> + (make-vcs url-prefix root-regex type)> + vcs?> + (url-prefix vcs-url-prefix)> + (root-regex vcs-root-regex)> + (type vcs-type))> + (let* ((known-vcs> (list> - (make-scs> + (make-vcs> "github.com"> "^(github\\.com/[A-Za-z0-9_.\\-]+/[A-Za-z0-9_.\\-]+)(/[A-Za-z0-9_.\\-]+)*$"> 'git)> - (make-scs> + (make-vcs> "bitbucket.org"> "^(bitbucket\\.org/([A-Za-z0-9_.\\-]+/[A-Za-z0-9_.\\-]+))(/[A-Za-z0-9_.\\-]+)*$`"> 'unknown)> - (make-scs> + (make-vcs> "hub.jazz.net/git/"> "^(hub\\.jazz\\.net/git/[a-z0-9]+/[A-Za-z0-9_.\\-]+)(/[A-Za-z0-9_.\\-]+)*$"> 'git)> - (make-scs> + (make-vcs> "git.apache.org"> "^(git\\.apache\\.org/[a-z0-9_.\\-]+\\.git)(/[A-Za-z0-9_.\\-]+)*$"> 'git)> - (make-scs> + (make-vcs> "git.openstack.org"> "^(git\\.openstack\\.org/[A-Za-z0-9_.\\-]+/[A-Za-z0-9_.\\-]+)(\\.git)?(/[A-Za-z0-9_.\\-]+)*$"> 'git)))> - (scs (find (lambda (scs) (string-prefix? (scs-url-prefix scs) module-path))> - known-scs)))> - (if scs> - (match:substring (string-match (scs-root-regex scs) module-path) 1)> + (vcs (find (lambda (vcs) (string-prefix? (vcs-url-prefix vcs) module-path))> + known-vcs)))> + (if vcs> + (match:substring (string-match (vcs-root-regex vcs) module-path) 1)> module-path)))> > (define (to-guix-package-name module-path)> @@ -164,8 +226,7 @@ consists of a module's root page is known before hand."> (string-append "go-"> (string-replace-substring> (string-replace-substring> - ;; Guix has its own field for version> - (module-path-without-major-version module-path)> + module-path> "." "-")> "/" "-"))))> > @@ -173,7 +234,9 @@ consists of a module's root page is known before hand."> "Fetches module meta-data from a module's landing page. This is necessary> because goproxy servers don't currently provide all the information needed to> build a package."> - (let* ((port (http-fetch (string->uri (format #f "https://~a?go-get=1" module-path))))> + ;; FIXME: This code breaks on k8s.io which have a meta tag splitted> + ;; on several lines> + (let* ((port (build-download:http-fetch (string->uri (format #f "https://~a?go-get=1" module-path))))> (module-metadata #f)> (meta-tag-prefix "<meta name=\"go-import\" content=\"")> (meta-tag-prefix-length (string-length meta-tag-prefix)))> @@ -185,7 +248,7 @@ build a package."> (let* ((start (+ meta-tag-index meta-tag-prefix-length))> (end (string-index line #\" start)))> (set! module-metadata> - (string-split (substring/shared line start end) #\space))))))> + (string-split (substring/shared line start end) #\space))))))> (close-port port)> module-metadata))> > @@ -244,7 +307,7 @@ control system is being used."> (dependencies (map car (parse-go.mod temp)))> (guix-name (to-guix-package-name module-path))> (root-module-path (infer-module-root module-path))> - ;; SCS type and URL are not included in goproxy information. For> + ;; VCS type and URL are not included in goproxy information. For> ;; this we need to fetch it from the official module page.> (meta-data (fetch-module-meta-data root-module-path))> (scs-type (module-meta-data-scs meta-data))> @@ -268,9 +331,10 @@ control system is being used."> > (define* (go-module-recursive-import package-name> #:key (goproxy-url "https://proxy.golang.org"))> - (recursive-import package-name #f> - #:repo->guix-package> - (lambda (name _)> - (go-module->guix-package name> - #:goproxy-url goproxy-url))> - #:guix-name to-guix-package-name))> + (recursive-import> + package-name> + #:repo->guix-package (lambda* (name . _)> + (go-module->guix-package> + name> + #:goproxy-url goproxy-url))> + #:guix-name to-guix-package-name))>> guix/import/go.scm | 340 +++++++++++++++++++++++++++++++++++++> guix/scripts/import.scm | 2 +-> guix/scripts/import/go.scm | 118 +++++++++++++> 3 files changed, 459 insertions(+), 1 deletion(-)> create mode 100644 guix/import/go.scm> create mode 100644 guix/scripts/import/go.scm>> diff --git a/guix/import/go.scm b/guix/import/go.scm> new file mode 100644> index 0000000000..7f5f300f0a> --- /dev/null> +++ b/guix/import/go.scm> @@ -0,0 +1,340 @@> +;;; GNU Guix --- Functional package management for GNU> +;;; Copyright © 2020 Katherine Cox-Buday <cox.katherine.e@gmail.com>> +;;; Copyright © 2020 Helio Machado <0x2b3bfa0+guix@googlemail.com>> +;;; Copyright © 2021 François Joulaud <francois.joulaud@radiofrance.com>> +;;;> +;;; This file is part of GNU Guix.> +;;;> +;;; GNU Guix is free software; you can redistribute it and/or modify it> +;;; under the terms of the GNU General Public License as published by> +;;; the Free Software Foundation; either version 3 of the License, or (at> +;;; your option) any later version.> +;;;> +;;; GNU Guix is distributed in the hope that it will be useful, but> +;;; WITHOUT ANY WARRANTY; without even the implied warranty of> +;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the> +;;; GNU General Public License for more details.> +;;;> +;;; You should have received a copy of the GNU General Public License> +;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.> +> +;;; (guix import golang) wants to make easier to create Guix package> +;;; declaration for Go modules.> +;;;> +;;; Modules in Go are "collection of related Go packages" which are> +;;; "the unit of source code interchange and versioning".> +;;; Modules are generally hosted in a repository.> +;;;> +;;; At this point it should handle correctly modules which> +;;; - have only Go dependencies;> +;;; - use go.mod;> +;;; - and are accessible from proxy.golang.org (or configured GOPROXY).> +;;;> +;;; We translate Go module paths to a Guix package name under the> +;;; assumption that there will be no collision.> +> +(define-module (guix import go)> + #:use-module (ice-9 match)> + #:use-module (ice-9 rdelim)> + #:use-module (ice-9 receive)> + #:use-module (ice-9 regex)> + #:use-module (srfi srfi-1)> + #:use-module (srfi srfi-9)> + #:use-module (json)> + #:use-module ((guix download) #:prefix download:)> + #:use-module (guix import utils)> + #:use-module (guix import json)> + #:use-module (guix packages)> + #:use-module (guix upstream)> + #:use-module (guix utils)> + #:use-module ((guix licenses) #:prefix license:)> + #:use-module (guix base16)> + #:use-module (guix base32)> + #:use-module ((guix build download) #:prefix build-download:)> + #:use-module (web uri)> +> + #:export (go-module->guix-package> + go-module-recursive-import> + infer-module-root))> +> +(define (go-path-escape path)> + "Escape a module path by replacing every uppercase letter with an exclamation> +mark followed with its lowercase equivalent, as per the module Escaped Paths> +specification. https://godoc.org/golang.org/x/mod/module#hdr-Escaped_Paths"> + (define (escape occurrence)> + (string-append "!" (string-downcase (match:substring occurrence))))> + (regexp-substitute/global #f "[A-Z]" path 'pre escape 'post))> +> +> +(define (fetch-latest-version goproxy-url module-path)> + "Fetches the version number of the latest version for MODULE-PATH from the> +given GOPROXY-URL server."> + (assoc-ref> + (json-fetch (format #f "~a/~a/@latest" goproxy-url> + (go-path-escape module-path)))> + "Version"))> +> +(define (fetch-go.mod goproxy-url module-path version file)> + "Fetches go.mod from the given GOPROXY-URL server for the given MODULE-PATH> +and VERSION."> + (let ((url (format #f "~a/~a/@v/~a.mod" goproxy-url> + (go-path-escape module-path)> + (go-path-escape version))))> + (parameterize ((current-output-port (current-error-port)))> + (build-download:url-fetch url> + file> + #:print-build-trace? #f))))> +> +(define (parse-go.mod go.mod-path)> + "PARSE-GO.MOD takes a filename in GO.MOD-PATH and extract a list of> +requirements from it."> + ;; We parse only a subset of https://golang.org/ref/mod#go-mod-file-grammar> + ;; which we think necessary for our use case.> + (define (toplevel results)> + "Main parser, RESULTS is a pair of alist serving as accumulator for> + all encountered requirements and replacements."> + (let ((line (read-line)))> + (cond> + ((eof-object? line)> + ;; parsing ended, give back the result> + results)> + ((string=? line "require (")> + ;; a require block begins, delegate parsing to IN-REQUIRE> + (in-require results))> + ((string-prefix? "require " line)> + ;; a require directive by itself> + (let* ((stripped-line (string-drop line 8))> + (new-results (require-directive results stripped-line)))> + (toplevel new-results)))> + ((string-prefix? "replace " line)> + ;; a replace directive by itself> + (let* ((stripped-line (string-drop line 8))> + (new-results (replace-directive results stripped-line)))> + (toplevel new-results)))> + (#t> + ;; unrecognised line, ignore silently> + (toplevel results)))))> + (define (in-require results)> + (let ((line (read-line)))> + (cond> + ((eof-object? line)> + ;; this should never happen here but we ignore silently> + results)> + ((string=? line ")")> + ;; end of block, coming back to toplevel> + (toplevel results))> + (#t> + (in-require (require-directive results line))))))> + (define (replace-directive results line)> + "Extract replaced modules and new requirements from replace directive> + in LINE and add to RESULTS."> + ;; ReplaceSpec = ModulePath [ Version ] "=>" FilePath newline> + ;; | ModulePath [ Version ] "=>" ModulePath Version newline .> + (let* ((requirements (car results))> + (replaced (cdr results))> + (re (string-concatenate> + '("([^[:blank:]]+)([[:blank:]]+([^[:blank:]]+))?"> + "[[:blank:]]+" "=>" "[[:blank:]]+"> + "([^[:blank:]]+)([[:blank:]]+([^[:blank:]]+))?")))> + (match (string-match re line))> + (module-path (match:substring match 1))> + (version (match:substring match 3))> + (new-module-path (match:substring match 4))> + (new-version (match:substring match 6))> + (new-replaced (acons module-path version replaced))> + (new-requirements> + (if (string-match "^\\.?\\./" new-module-path)> + requirements> + (acons new-module-path new-version requirements))))> + (cons new-requirements new-replaced)))> + (define (require-directive results line)> + "Extract requirement from LINE and add it to RESULTS."> + (let* ((requirements (car results))> + (replaced (cdr results))> + ;; A line in a require directive is composed of a module path and> + ;; a version separated by whitespace and an optionnal '//' comment at> + ;; the end.> + (re (string-concatenate> + '("^[[:blank:]]*"> + "([^[:blank:]]+)[[:blank:]]+([^[:blank:]]+)"> + "([[:blank:]]+//.*)?")))> + (match (string-match re line))> + (module-path (match:substring match 1))> + (version (match:substring match 2)))> + (cons (acons module-path version requirements) replaced)))> + (with-input-from-file go.mod-path> + (lambda ()> + (let* ((results (toplevel '(() . ())))> + (requirements (car results))> + (replaced (cdr results)))> + ;; At last we remove replaced modules from the requirements list> + (fold> + (lambda (replacedelem requirements)> + (alist-delete! (car replacedelem) requirements))> + requirements> + replaced)))))> +> +(define (infer-module-root module-path)> + "Go modules can be defined at any level of a repository's tree, but querying> +for the meta tag usually can only be done at the webpage at the root of the> +repository. Therefore, it is sometimes necessary to try and derive a module's> +root path from its path. For a set of well-known forges, the pattern of what> +consists of a module's root page is known before hand."> + ;; See the following URL for the official Go equivalent:> + ;; https://github.com/golang/go/blob/846dce9d05f19a1f53465e62a304dea21b99f910/src/cmd/go/internal/vcs/vcs.go#L1026-L1087> + ;;> + ;; FIXME: handle module path with VCS qualifier as described in> + ;; https://golang.org/ref/mod#vcs-find and> + ;; https://golang.org/cmd/go/#hdr-Remote_import_paths> + (define-record-type <vcs>> + (make-vcs url-prefix root-regex type)> + vcs?> + (url-prefix vcs-url-prefix)> + (root-regex vcs-root-regex)> + (type vcs-type))> + (let* ((known-vcs> + (list> + (make-vcs> + "github.com"> + "^(github\\.com/[A-Za-z0-9_.\\-]+/[A-Za-z0-9_.\\-]+)(/[A-Za-z0-9_.\\-]+)*$"> + 'git)> + (make-vcs> + "bitbucket.org"> + "^(bitbucket\\.org/([A-Za-z0-9_.\\-]+/[A-Za-z0-9_.\\-]+))(/[A-Za-z0-9_.\\-]+)*$`"> + 'unknown)> + (make-vcs> + "hub.jazz.net/git/"> + "^(hub\\.jazz\\.net/git/[a-z0-9]+/[A-Za-z0-9_.\\-]+)(/[A-Za-z0-9_.\\-]+)*$"> + 'git)> + (make-vcs> + "git.apache.org"> + "^(git\\.apache\\.org/[a-z0-9_.\\-]+\\.git)(/[A-Za-z0-9_.\\-]+)*$"> + 'git)> + (make-vcs> + "git.openstack.org"> + "^(git\\.openstack\\.org/[A-Za-z0-9_.\\-]+/[A-Za-z0-9_.\\-]+)(\\.git)?(/[A-Za-z0-9_.\\-]+)*$"> + 'git)))> + (vcs (find (lambda (vcs) (string-prefix? (vcs-url-prefix vcs) module-path))> + known-vcs)))> + (if vcs> + (match:substring (string-match (vcs-root-regex vcs) module-path) 1)> + module-path)))> +> +(define (to-guix-package-name module-path)> + "Converts a module's path to the canonical Guix format for Go packages."> + (string-downcase> + (string-append "go-"> + (string-replace-substring> + (string-replace-substring> + module-path> + "." "-")> + "/" "-"))))> +> +(define (fetch-module-meta-data module-path)> + "Fetches module meta-data from a module's landing page. This is necessary> +because goproxy servers don't currently provide all the information needed to> +build a package."> + ;; FIXME: This code breaks on k8s.io which have a meta tag splitted> + ;; on several lines> + (let* ((port (build-download:http-fetch (string->uri (format #f "https://~a?go-get=1" module-path))))> + (module-metadata #f)> + (meta-tag-prefix "<meta name=\"go-import\" content=\"")> + (meta-tag-prefix-length (string-length meta-tag-prefix)))> + (do ((line (read-line port) (read-line port)))> + ((or (eof-object? line)> + module-metadata))> + (let ((meta-tag-index (string-contains line meta-tag-prefix)))> + (when meta-tag-index> + (let* ((start (+ meta-tag-index meta-tag-prefix-length))> + (end (string-index line #\" start)))> + (set! module-metadata> + (string-split (substring/shared line start end) #\space))))))> + (close-port port)> + module-metadata))> +> +(define (module-meta-data-scs meta-data)> + "Return the source control system specified by a module's meta-data."> + (string->symbol (list-ref meta-data 1)))> +> +(define (module-meta-data-repo-url meta-data goproxy-url)> + "Return the URL where the fetcher which will be used can download the source> +control."> + (if (member (module-meta-data-scs meta-data) '(fossil mod))> + goproxy-url> + (list-ref meta-data 2)))> +> +(define (source-uri scs-type scs-repo-url file)> + "Generate the `origin' block of a package depending on what type of source> +control system is being used."> + (case scs-type> + ((git)> + `(origin> + (method git-fetch)> + (uri (git-reference> + (url ,scs-repo-url)> + (commit (string-append "v" version))))> + (file-name (git-file-name name version))> + (sha256> + (base32> + ,(guix-hash-url file)))))> + ((hg)> + `(origin> + (method hg-fetch)> + (uri (hg-reference> + (url ,scs-repo-url)> + (changeset ,version)))> + (file-name (format #f "~a-~a-checkout" name version))))> + ((svn)> + `(origin> + (method svn-fetch)> + (uri (svn-reference> + (url ,scs-repo-url)> + (revision (string->number version))> + (recursive? #f)))> + (file-name (format #f "~a-~a-checkout" name version))> + (sha256> + (base32> + ,(guix-hash-url file)))))> + (else> + (raise-exception (format #f "unsupported scs type: ~a" scs-type)))))> +> +(define* (go-module->guix-package module-path #:key (goproxy-url "https://proxy.golang.org"))> + (call-with-temporary-output-file> + (lambda (temp port)> + (let* ((latest-version (fetch-latest-version goproxy-url module-path))> + (go.mod-path (fetch-go.mod goproxy-url module-path latest-version> + temp))> + (dependencies (map car (parse-go.mod temp)))> + (guix-name (to-guix-package-name module-path))> + (root-module-path (infer-module-root module-path))> + ;; VCS type and URL are not included in goproxy information. For> + ;; this we need to fetch it from the official module page.> + (meta-data (fetch-module-meta-data root-module-path))> + (scs-type (module-meta-data-scs meta-data))> + (scs-repo-url (module-meta-data-repo-url meta-data goproxy-url)))> + (values> + `(package> + (name ,guix-name)> + ;; Elide the "v" prefix Go uses> + (version ,(string-trim latest-version #\v))> + (source> + ,(source-uri scs-type scs-repo-url temp))> + (build-system go-build-system)> + ,@(maybe-inputs (map to-guix-package-name dependencies))> + ;; TODO(katco): It would be nice to make an effort to fetch this> + ;; from known forges, e.g. GitHub> + (home-page ,(format #f "https://~a" root-module-path))> + (synopsis "A Go package")> + (description ,(format #f "~a is a Go package." guix-name))> + (license #f))> + dependencies)))))> +> +(define* (go-module-recursive-import package-name> + #:key (goproxy-url "https://proxy.golang.org"))> + (recursive-import> + package-name> + #:repo->guix-package (lambda* (name . _)> + (go-module->guix-package> + name> + #:goproxy-url goproxy-url))> + #:guix-name to-guix-package-name))> diff --git a/guix/scripts/import.scm b/guix/scripts/import.scm> index 0a3863f965..1d2b45d942 100644> --- a/guix/scripts/import.scm> +++ b/guix/scripts/import.scm> @@ -77,7 +77,7 @@ rather than \\n."> ;;;> > (define importers '("gnu" "nix" "pypi" "cpan" "hackage" "stackage" "elpa" "gem"> - "cran" "crate" "texlive" "json" "opam"))> + "go" "cran" "crate" "texlive" "json" "opam"))> > (define (resolve-importer name)> (let ((module (resolve-interface> diff --git a/guix/scripts/import/go.scm b/guix/scripts/import/go.scm> new file mode 100644> index 0000000000..fde7555973> --- /dev/null> +++ b/guix/scripts/import/go.scm> @@ -0,0 +1,118 @@> +;;; GNU Guix --- Functional package management for GNU> +;;; Copyright © 2020 Katherine Cox-Buday <cox.katherine.e@gmail.com>> +;;;> +;;; This file is part of GNU Guix.> +;;;> +;;; GNU Guix is free software; you can redistribute it and/or modify it> +;;; under the terms of the GNU General Public License as published by> +;;; the Free Software Foundation; either version 3 of the License, or (at> +;;; your option) any later version.> +;;;> +;;; GNU Guix is distributed in the hope that it will be useful, but> +;;; WITHOUT ANY WARRANTY; without even the implied warranty of> +;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the> +;;; GNU General Public License for more details.> +;;;> +;;; You should have received a copy of the GNU General Public License> +;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.> +> +(define-module (guix scripts import go)> + #:use-module (guix ui)> + #:use-module (guix utils)> + #:use-module (guix scripts)> + #:use-module (guix import go)> + #:use-module (guix scripts import)> + #:use-module (srfi srfi-1)> + #:use-module (srfi srfi-11)> + #:use-module (srfi srfi-37)> + #:use-module (ice-9 match)> + #:use-module (ice-9 format)> + #:export (guix-import-go))> +> +> +;;;> +;;; Command-line options.> +;;;> +> +(define %default-options> + '())> +> +(define (show-help)> + (display (G_ "Usage: guix import go PACKAGE-PATH> +Import and convert the Go module for PACKAGE-PATH.\n"))> + (display (G_ "> + -h, --help display this help and exit"))> + (display (G_ "> + -V, --version display version information and exit"))> + (display (G_ "> + -r, --recursive generate package expressions for all Go modules\> + that are not yet in Guix"))> + (display (G_ "> + -p, --goproxy=GOPROXY specify which goproxy server to use"))> + (newline)> + (show-bug-report-information))> +> +(define %options> + ;; Specification of the command-line options.> + (cons* (option '(#\h "help") #f #f> + (lambda args> + (show-help)> + (exit 0)))> + (option '(#\V "version") #f #f> + (lambda args> + (show-version-and-exit "guix import go")))> + (option '(#\r "recursive") #f #f> + (lambda (opt name arg result)> + (alist-cons 'recursive #t result)))> + (option '(#\p "goproxy") #t #f> + (lambda (opt name arg result)> + (alist-cons 'goproxy> + (string->symbol arg)> + (alist-delete 'goproxy result))))> + %standard-import-options))> +> +> +;;;> +;;; Entry point.> +;;;> +> +(define (guix-import-go . args)> + (define (parse-options)> + ;; Return the alist of option values.> + (args-fold* args %options> + (lambda (opt name arg result)> + (leave (G_ "~A: unrecognized option~%") name))> + (lambda (arg result)> + (alist-cons 'argument arg result))> + %default-options))> +> + (let* ((opts (parse-options))> + (args (filter-map (match-lambda> + (('argument . value)> + value)> + (_ #f))> + (reverse opts))))> + (match args> + ((module-name)> + (if (assoc-ref opts 'recursive)> + (map (match-lambda> + ((and ('package ('name name) . rest) pkg)> + `(define-public ,(string->symbol name)> + ,pkg))> + (_ #f))> + (go-module-recursive-import module-name> + #:goproxy-url> + (or (assoc-ref opts 'goproxy)> + "https://proxy.golang.org")))> + (let ((sexp (go-module->guix-package module-name> + #:goproxy-url> + (or (assoc-ref opts 'goproxy)> + "https://proxy.golang.org"))))> + (unless sexp> + (leave (G_ "failed to download meta-data for module '~a'~%")> + module-name))> + sexp)))> + (()> + (leave (G_ "too few arguments~%")))> + ((many ...)> + (leave (G_ "too many arguments~%"))))))
-- Katherine
J
J
JOULAUD François wrote on 23 Jan 22:35 +0100
[PATCH] Create importer for Go modules
(name . 44178@debbugs.gnu.org)(address . 44178@debbugs.gnu.org)
20210123212742.m2thdeuzdvgpkgeo@fjo-extia-HPdeb.example.avalenn.eu
This patch add a `guix import go` command.
It was tested with several big repositories and seems to mostly work forthe import part (because building Guix packages is an other story). Thereis still bugs blocking e.g. use of any k8s.io modules.
* guix/import/go.scm: Created Go Importer* guix/scripts/import.scm: Created Go Importer Subcommand* guix/import/go.scm (importers): Added Go Importer Subcommand
Signed-off-by: Francois Joulaud <francois.joulaud@radiofrance.com>---The patch is a rebased and modified version of the one proposed byKatherine Cox-Buday.
Notable modifications are :- move from (guix json) to (json)- new parse-go.mod with no "set!" and parsing some go.mod which were in error before- adding comments (maybe too much comments)- renamed SCS to VCS to be in accordance with vocabulary in use in Guix and in Go worlds- replacing escape-capital-letters by Helio Machado's go-path-escape- no pruning of major version in go module names as they are considered as completely different artefacts by Go programmers- fixed recursive-import probably broken by the rebase- force usage of url-fetch from (guix build download)
I would be happy to hear about problems and perspective for this patch andwill now focus on my next step which is actually building any package.
Hope I CCed the right persons, I am not really aware of applicablenetiquette here.
Interdiff : diff --git a/guix/import/go.scm b/guix/import/go.scm index 61009f3565..7f5f300f0a 100644 --- a/guix/import/go.scm +++ b/guix/import/go.scm @@ -1,5 +1,7 @@ ;;; GNU Guix --- Functional package management for GNU ;;; Copyright © 2020 Katherine Cox-Buday <cox.katherine.e@gmail.com> +;;; Copyright © 2020 Helio Machado <0x2b3bfa0+guix@googlemail.com> +;;; Copyright © 2021 François Joulaud <francois.joulaud@radiofrance.com> ;;; ;;; This file is part of GNU Guix. ;;; @@ -16,6 +18,21 @@ ;;; You should have received a copy of the GNU General Public License ;;; along with GNU Guix. If not, see http://www.gnu.org/licenses/. +;;; (guix import golang) wants to make easier to create Guix package +;;; declaration for Go modules. +;;; +;;; Modules in Go are "collection of related Go packages" which are +;;; "the unit of source code interchange and versioning". +;;; Modules are generally hosted in a repository. +;;; +;;; At this point it should handle correctly modules which +;;; - have only Go dependencies; +;;; - use go.mod; +;;; - and are accessible from proxy.golang.org (or configured GOPROXY). +;;; +;;; We translate Go module paths to a Guix package name under the +;;; assumption that there will be no collision. + (define-module (guix import go) #:use-module (ice-9 match) #:use-module (ice-9 rdelim) @@ -23,7 +40,7 @@ #:use-module (ice-9 regex) #:use-module (srfi srfi-1) #:use-module (srfi srfi-9) - #:use-module (guix json) + #:use-module (json) #:use-module ((guix download) #:prefix download:) #:use-module (guix import utils) #:use-module (guix import json) @@ -33,88 +50,129 @@ #:use-module ((guix licenses) #:prefix license:) #:use-module (guix base16) #:use-module (guix base32) - #:use-module (guix build download) + #:use-module ((guix build download) #:prefix build-download:) #:use-module (web uri) #:export (go-module->guix-package go-module-recursive-import infer-module-root)) -(define (escape-capital-letters s) - "To avoid ambiguity when serving from case-insensitive file systems, the -$module and $version elements are case-encoded by replacing every uppercase -letter with an exclamation mark followed by the corresponding lower-case -letter." - (let ((escaped-string (string))) - (string-for-each-index - (lambda (i) - (let ((c (string-ref s i))) - (set! escaped-string - (string-concatenate - (list escaped-string - (if (char-upper-case? c) "!" "") - (string (char-downcase c))))))) - s) - escaped-string)) +(define (go-path-escape path) + "Escape a module path by replacing every uppercase letter with an exclamation +mark followed with its lowercase equivalent, as per the module Escaped Paths +specification. https://godoc.org/golang.org/x/mod/module#hdr-Escaped_Paths" + (define (escape occurrence) + (string-append "!" (string-downcase (match:substring occurrence)))) + (regexp-substitute/global #f "[A-Z]" path 'pre escape 'post)) + (define (fetch-latest-version goproxy-url module-path) "Fetches the version number of the latest version for MODULE-PATH from the given GOPROXY-URL server." (assoc-ref (json-fetch (format #f "~a/~a/@latest" goproxy-url - (escape-capital-letters module-path))) + (go-path-escape module-path))) "Version")) (define (fetch-go.mod goproxy-url module-path version file) "Fetches go.mod from the given GOPROXY-URL server for the given MODULE-PATH and VERSION." - (url-fetch (format #f "~a/~a/@v/~a.mod" goproxy-url - (escape-capital-letters module-path) - (escape-capital-letters version)) - file - #:print-build-trace? #f)) + (let ((url (format #f "~a/~a/@v/~a.mod" goproxy-url + (go-path-escape module-path) + (go-path-escape version)))) + (parameterize ((current-output-port (current-error-port))) + (build-download:url-fetch url + file + #:print-build-trace? #f)))) (define (parse-go.mod go.mod-path) - "Parses a go.mod file and returns an alist of module path to version." + "PARSE-GO.MOD takes a filename in GO.MOD-PATH and extract a list of +requirements from it." + ;; We parse only a subset of https://golang.org/ref/mod#go-mod-file-grammar + ;; which we think necessary for our use case. + (define (toplevel results) + "Main parser, RESULTS is a pair of alist serving as accumulator for + all encountered requirements and replacements." + (let ((line (read-line))) + (cond + ((eof-object? line) + ;; parsing ended, give back the result + results) + ((string=? line "require (") + ;; a require block begins, delegate parsing to IN-REQUIRE + (in-require results)) + ((string-prefix? "require " line) + ;; a require directive by itself + (let* ((stripped-line (string-drop line 8)) + (new-results (require-directive results stripped-line))) + (toplevel new-results))) + ((string-prefix? "replace " line) + ;; a replace directive by itself + (let* ((stripped-line (string-drop line 8)) + (new-results (replace-directive results stripped-line))) + (toplevel new-results))) + (#t + ;; unrecognised line, ignore silently + (toplevel results))))) + (define (in-require results) + (let ((line (read-line))) + (cond + ((eof-object? line) + ;; this should never happen here but we ignore silently + results) + ((string=? line ")") + ;; end of block, coming back to toplevel + (toplevel results)) + (#t + (in-require (require-directive results line)))))) + (define (replace-directive results line) + "Extract replaced modules and new requirements from replace directive + in LINE and add to RESULTS." + ;; ReplaceSpec = ModulePath [ Version ] "=>" FilePath newline + ;; | ModulePath [ Version ] "=>" ModulePath Version newline . + (let* ((requirements (car results)) + (replaced (cdr results)) + (re (string-concatenate + '("([^[:blank:]]+)([[:blank:]]+([^[:blank:]]+))?" + "[[:blank:]]+" "=>" "[[:blank:]]+" + "([^[:blank:]]+)([[:blank:]]+([^[:blank:]]+))?"))) + (match (string-match re line)) + (module-path (match:substring match 1)) + (version (match:substring match 3)) + (new-module-path (match:substring match 4)) + (new-version (match:substring match 6)) + (new-replaced (acons module-path version replaced)) + (new-requirements + (if (string-match "^\\.?\\./" new-module-path) + requirements + (acons new-module-path new-version requirements)))) + (cons new-requirements new-replaced))) + (define (require-directive results line) + "Extract requirement from LINE and add it to RESULTS." + (let* ((requirements (car results)) + (replaced (cdr results)) + ;; A line in a require directive is composed of a module path and + ;; a version separated by whitespace and an optionnal '//' comment at + ;; the end. + (re (string-concatenate + '("^[[:blank:]]*" + "([^[:blank:]]+)[[:blank:]]+([^[:blank:]]+)" + "([[:blank:]]+//.*)?"))) + (match (string-match re line)) + (module-path (match:substring match 1)) + (version (match:substring match 2))) + (cons (acons module-path version requirements) replaced))) (with-input-from-file go.mod-path (lambda () - (let ((in-require? #f) - (requirements (list))) - (do ((line (read-line) (read-line))) - ((eof-object? line)) - (set! line (string-trim line)) - ;; The parser is either entering, within, exiting, or after the - ;; require block. The Go toolchain is trustworthy so edge-cases like - ;; double-entry, etc. need not complect the parser. - (cond - ((string=? line "require (") - (set! in-require? #t)) - ((and in-require? (string=? line ")")) - (set! in-require? #f)) - (in-require? - (let* ((requirement (string-split line #\space)) - ;; Modules should be unquoted - (module-path (string-delete #\" (car requirement))) - (version (list-ref requirement 1))) - (set! requirements (acons module-path version requirements)))) - ((string-prefix? "replace" line) - (let* ((requirement (string-split line #\space)) - (module-path (list-ref requirement 1)) - (new-module-path (list-ref requirement 3)) - (version (list-ref requirement 4))) - (set! requirements (assoc-remove! requirements module-path)) - (set! requirements (acons new-module-path version requirements)))))) - requirements)))) - -(define (module-path-without-major-version module-path) - "Go modules can be appended with a major version indicator, -e.g. /v3. Sometimes it is desirable to work with the root module path. For -instance, for a module path github.com/foo/bar/v3 this function returns -github.com/foo/bar." - (let ((m (string-match "(.*)\\/v[0-9]+$" module-path))) - (if m - (match:substring m 1) - module-path))) + (let* ((results (toplevel '(() . ()))) + (requirements (car results)) + (replaced (cdr results))) + ;; At last we remove replaced modules from the requirements list + (fold + (lambda (replacedelem requirements) + (alist-delete! (car replacedelem) requirements)) + requirements + replaced))))) (define (infer-module-root module-path) "Go modules can be defined at any level of a repository's tree, but querying @@ -124,38 +182,42 @@ root path from its path. For a set of well-known forges, the pattern of what consists of a module's root page is known before hand." ;; See the following URL for the official Go equivalent: ;; https://github.com/golang/go/blob/846dce9d05f19a1f53465e62a304dea21b99f910/src/cmd/go/internal/vcs/vcs.go#L1026-L1087 - (define-record-type <scs> - (make-scs url-prefix root-regex type) - scs? - (url-prefix scs-url-prefix) - (root-regex scs-root-regex) - (type scs-type)) - (let* ((known-scs + ;; + ;; FIXME: handle module path with VCS qualifier as described in + ;; https://golang.org/ref/mod#vcs-findand + ;; https://golang.org/cmd/go/#hdr-Remote_import_paths + (define-record-type <vcs> + (make-vcs url-prefix root-regex type) + vcs? + (url-prefix vcs-url-prefix) + (root-regex vcs-root-regex) + (type vcs-type)) + (let* ((known-vcs (list - (make-scs + (make-vcs "github.com" "^(github\\.com/[A-Za-z0-9_.\\-]+/[A-Za-z0-9_.\\-]+)(/[A-Za-z0-9_.\\-]+)*$" 'git) - (make-scs + (make-vcs "bitbucket.org" "^(bitbucket\\.org/([A-Za-z0-9_.\\-]+/[A-Za-z0-9_.\\-]+))(/[A-Za-z0-9_.\\-]+)*$`" 'unknown) - (make-scs + (make-vcs "hub.jazz.net/git/" "^(hub\\.jazz\\.net/git/[a-z0-9]+/[A-Za-z0-9_.\\-]+)(/[A-Za-z0-9_.\\-]+)*$" 'git) - (make-scs + (make-vcs "git.apache.org" "^(git\\.apache\\.org/[a-z0-9_.\\-]+\\.git)(/[A-Za-z0-9_.\\-]+)*$" 'git) - (make-scs + (make-vcs "git.openstack.org" "^(git\\.openstack\\.org/[A-Za-z0-9_.\\-]+/[A-Za-z0-9_.\\-]+)(\\.git)?(/[A-Za-z0-9_.\\-]+)*$" 'git))) - (scs (find (lambda (scs) (string-prefix? (scs-url-prefix scs) module-path)) - known-scs))) - (if scs - (match:substring (string-match (scs-root-regex scs) module-path) 1) + (vcs (find (lambda (vcs) (string-prefix? (vcs-url-prefix vcs) module-path)) + known-vcs))) + (if vcs + (match:substring (string-match (vcs-root-regex vcs) module-path) 1) module-path))) (define (to-guix-package-name module-path) @@ -164,8 +226,7 @@ consists of a module's root page is known before hand." (string-append "go-" (string-replace-substring (string-replace-substring - ;; Guix has its own field for version - (module-path-without-major-version module-path) + module-path "." "-") "/" "-")))) @@ -173,7 +234,9 @@ consists of a module's root page is known before hand." "Fetches module meta-data from a module's landing page. This is necessary because goproxy servers don't currently provide all the information needed to build a package." - (let* ((port (http-fetch (string->uri (format #f "https://~a?go-get=1" module-path)))) + ;; FIXME: This code breaks on k8s.io which have a meta tag splitted + ;; on several lines + (let* ((port (build-download:http-fetch (string->uri (format #f "https://~a?go-get=1" module-path)))) (module-metadata #f) (meta-tag-prefix "<meta name=\"go-import\" content=\"") (meta-tag-prefix-length (string-length meta-tag-prefix))) @@ -185,7 +248,7 @@ build a package." (let* ((start (+ meta-tag-index meta-tag-prefix-length)) (end (string-index line #\" start))) (set! module-metadata - (string-split (substring/shared line start end) #\space)))))) + (string-split (substring/shared line start end) #\space)))))) (close-port port) module-metadata)) @@ -244,7 +307,7 @@ control system is being used." (dependencies (map car (parse-go.mod temp))) (guix-name (to-guix-package-name module-path)) (root-module-path (infer-module-root module-path)) - ;; SCS type and URL are not included in goproxy information. For + ;; VCS type and URL are not included in goproxy information. For ;; this we need to fetch it from the official module page. (meta-data (fetch-module-meta-data root-module-path)) (scs-type (module-meta-data-scs meta-data)) @@ -268,9 +331,10 @@ control system is being used." (define* (go-module-recursive-import package-name #:key (goproxy-url "https://proxy.golang.org")) - (recursive-import package-name #f - #:repo->guix-package - (lambda (name _) - (go-module->guix-package name - #:goproxy-url goproxy-url)) - #:guix-name to-guix-package-name)) + (recursive-import + package-name + #:repo->guix-package (lambda* (name . _) + (go-module->guix-package + name + #:goproxy-url goproxy-url)) + #:guix-name to-guix-package-name))
guix/import/go.scm | 340 +++++++++++++++++++++++++++++++++++++ guix/scripts/import.scm | 2 +- guix/scripts/import/go.scm | 118 +++++++++++++ 3 files changed, 459 insertions(+), 1 deletion(-) create mode 100644 guix/import/go.scm create mode 100644 guix/scripts/import/go.scm
Toggle diff (485 lines)diff --git a/guix/import/go.scm b/guix/import/go.scmnew file mode 100644index 0000000000..7f5f300f0a--- /dev/null+++ b/guix/import/go.scm@@ -0,0 +1,340 @@+;;; GNU Guix --- Functional package management for GNU+;;; Copyright © 2020 Katherine Cox-Buday <cox.katherine.e@gmail.com>+;;; Copyright © 2020 Helio Machado <0x2b3bfa0+guix@googlemail.com>+;;; Copyright © 2021 François Joulaud <francois.joulaud@radiofrance.com>+;;;+;;; This file is part of GNU Guix.+;;;+;;; GNU Guix is free software; you can redistribute it and/or modify it+;;; under the terms of the GNU General Public License as published by+;;; the Free Software Foundation; either version 3 of the License, or (at+;;; your option) any later version.+;;;+;;; GNU Guix is distributed in the hope that it will be useful, but+;;; WITHOUT ANY WARRANTY; without even the implied warranty of+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the+;;; GNU General Public License for more details.+;;;+;;; You should have received a copy of the GNU General Public License+;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.++;;; (guix import golang) wants to make easier to create Guix package+;;; declaration for Go modules.+;;;+;;; Modules in Go are "collection of related Go packages" which are+;;; "the unit of source code interchange and versioning".+;;; Modules are generally hosted in a repository.+;;;+;;; At this point it should handle correctly modules which+;;; - have only Go dependencies;+;;; - use go.mod;+;;; - and are accessible from proxy.golang.org (or configured GOPROXY).+;;;+;;; We translate Go module paths to a Guix package name under the+;;; assumption that there will be no collision.++(define-module (guix import go)+ #:use-module (ice-9 match)+ #:use-module (ice-9 rdelim)+ #:use-module (ice-9 receive)+ #:use-module (ice-9 regex)+ #:use-module (srfi srfi-1)+ #:use-module (srfi srfi-9)+ #:use-module (json)+ #:use-module ((guix download) #:prefix download:)+ #:use-module (guix import utils)+ #:use-module (guix import json)+ #:use-module (guix packages)+ #:use-module (guix upstream)+ #:use-module (guix utils)+ #:use-module ((guix licenses) #:prefix license:)+ #:use-module (guix base16)+ #:use-module (guix base32)+ #:use-module ((guix build download) #:prefix build-download:)+ #:use-module (web uri)++ #:export (go-module->guix-package+ go-module-recursive-import+ infer-module-root))++(define (go-path-escape path)+ "Escape a module path by replacing every uppercase letter with an exclamation+mark followed with its lowercase equivalent, as per the module Escaped Paths+specification. https://godoc.org/golang.org/x/mod/module#hdr-Escaped_Paths"+ (define (escape occurrence)+ (string-append "!" (string-downcase (match:substring occurrence))))+ (regexp-substitute/global #f "[A-Z]" path 'pre escape 'post))+++(define (fetch-latest-version goproxy-url module-path)+ "Fetches the version number of the latest version for MODULE-PATH from the+given GOPROXY-URL server."+ (assoc-ref+ (json-fetch (format #f "~a/~a/@latest" goproxy-url+ (go-path-escape module-path)))+ "Version"))++(define (fetch-go.mod goproxy-url module-path version file)+ "Fetches go.mod from the given GOPROXY-URL server for the given MODULE-PATH+and VERSION."+ (let ((url (format #f "~a/~a/@v/~a.mod" goproxy-url+ (go-path-escape module-path)+ (go-path-escape version))))+ (parameterize ((current-output-port (current-error-port)))+ (build-download:url-fetch url+ file+ #:print-build-trace? #f))))++(define (parse-go.mod go.mod-path)+ "PARSE-GO.MOD takes a filename in GO.MOD-PATH and extract a list of+requirements from it."+ ;; We parse only a subset of https://golang.org/ref/mod#go-mod-file-grammar+ ;; which we think necessary for our use case.+ (define (toplevel results)+ "Main parser, RESULTS is a pair of alist serving as accumulator for+ all encountered requirements and replacements."+ (let ((line (read-line)))+ (cond+ ((eof-object? line)+ ;; parsing ended, give back the result+ results)+ ((string=? line "require (")+ ;; a require block begins, delegate parsing to IN-REQUIRE+ (in-require results))+ ((string-prefix? "require " line)+ ;; a require directive by itself+ (let* ((stripped-line (string-drop line 8))+ (new-results (require-directive results stripped-line)))+ (toplevel new-results)))+ ((string-prefix? "replace " line)+ ;; a replace directive by itself+ (let* ((stripped-line (string-drop line 8))+ (new-results (replace-directive results stripped-line)))+ (toplevel new-results)))+ (#t+ ;; unrecognised line, ignore silently+ (toplevel results)))))+ (define (in-require results)+ (let ((line (read-line)))+ (cond+ ((eof-object? line)+ ;; this should never happen here but we ignore silently+ results)+ ((string=? line ")")+ ;; end of block, coming back to toplevel+ (toplevel results))+ (#t+ (in-require (require-directive results line))))))+ (define (replace-directive results line)+ "Extract replaced modules and new requirements from replace directive+ in LINE and add to RESULTS."+ ;; ReplaceSpec = ModulePath [ Version ] "=>" FilePath newline+ ;; | ModulePath [ Version ] "=>" ModulePath Version newline .+ (let* ((requirements (car results))+ (replaced (cdr results))+ (re (string-concatenate+ '("([^[:blank:]]+)([[:blank:]]+([^[:blank:]]+))?"+ "[[:blank:]]+" "=>" "[[:blank:]]+"+ "([^[:blank:]]+)([[:blank:]]+([^[:blank:]]+))?")))+ (match (string-match re line))+ (module-path (match:substring match 1))+ (version (match:substring match 3))+ (new-module-path (match:substring match 4))+ (new-version (match:substring match 6))+ (new-replaced (acons module-path version replaced))+ (new-requirements+ (if (string-match "^\\.?\\./" new-module-path)+ requirements+ (acons new-module-path new-version requirements))))+ (cons new-requirements new-replaced)))+ (define (require-directive results line)+ "Extract requirement from LINE and add it to RESULTS."+ (let* ((requirements (car results))+ (replaced (cdr results))+ ;; A line in a require directive is composed of a module path and+ ;; a version separated by whitespace and an optionnal '//' comment at+ ;; the end.+ (re (string-concatenate+ '("^[[:blank:]]*"+ "([^[:blank:]]+)[[:blank:]]+([^[:blank:]]+)"+ "([[:blank:]]+//.*)?")))+ (match (string-match re line))+ (module-path (match:substring match 1))+ (version (match:substring match 2)))+ (cons (acons module-path version requirements) replaced)))+ (with-input-from-file go.mod-path+ (lambda ()+ (let* ((results (toplevel '(() . ())))+ (requirements (car results))+ (replaced (cdr results)))+ ;; At last we remove replaced modules from the requirements list+ (fold+ (lambda (replacedelem requirements)+ (alist-delete! (car replacedelem) requirements))+ requirements+ replaced)))))++(define (infer-module-root module-path)+ "Go modules can be defined at any level of a repository's tree, but querying+for the meta tag usually can only be done at the webpage at the root of the+repository. Therefore, it is sometimes necessary to try and derive a module's+root path from its path. For a set of well-known forges, the pattern of what+consists of a module's root page is known before hand."+ ;; See the following URL for the official Go equivalent:+ ;; https://github.com/golang/go/blob/846dce9d05f19a1f53465e62a304dea21b99f910/src/cmd/go/internal/vcs/vcs.go#L1026-L1087+ ;;+ ;; FIXME: handle module path with VCS qualifier as described in+ ;; https://golang.org/ref/mod#vcs-find and+ ;; https://golang.org/cmd/go/#hdr-Remote_import_paths+ (define-record-type <vcs>+ (make-vcs url-prefix root-regex type)+ vcs?+ (url-prefix vcs-url-prefix)+ (root-regex vcs-root-regex)+ (type vcs-type))+ (let* ((known-vcs+ (list+ (make-vcs+ "github.com"+ "^(github\\.com/[A-Za-z0-9_.\\-]+/[A-Za-z0-9_.\\-]+)(/[A-Za-z0-9_.\\-]+)*$"+ 'git)+ (make-vcs+ "bitbucket.org"+ "^(bitbucket\\.org/([A-Za-z0-9_.\\-]+/[A-Za-z0-9_.\\-]+))(/[A-Za-z0-9_.\\-]+)*$`"+ 'unknown)+ (make-vcs+ "hub.jazz.net/git/"+ "^(hub\\.jazz\\.net/git/[a-z0-9]+/[A-Za-z0-9_.\\-]+)(/[A-Za-z0-9_.\\-]+)*$"+ 'git)+ (make-vcs+ "git.apache.org"+ "^(git\\.apache\\.org/[a-z0-9_.\\-]+\\.git)(/[A-Za-z0-9_.\\-]+)*$"+ 'git)+ (make-vcs+ "git.openstack.org"+ "^(git\\.openstack\\.org/[A-Za-z0-9_.\\-]+/[A-Za-z0-9_.\\-]+)(\\.git)?(/[A-Za-z0-9_.\\-]+)*$"+ 'git)))+ (vcs (find (lambda (vcs) (string-prefix? (vcs-url-prefix vcs) module-path))+ known-vcs)))+ (if vcs+ (match:substring (string-match (vcs-root-regex vcs) module-path) 1)+ module-path)))++(define (to-guix-package-name module-path)+ "Converts a module's path to the canonical Guix format for Go packages."+ (string-downcase+ (string-append "go-"+ (string-replace-substring+ (string-replace-substring+ module-path+ "." "-")+ "/" "-"))))++(define (fetch-module-meta-data module-path)+ "Fetches module meta-data from a module's landing page. This is necessary+because goproxy servers don't currently provide all the information needed to+build a package."+ ;; FIXME: This code breaks on k8s.io which have a meta tag splitted+ ;; on several lines+ (let* ((port (build-download:http-fetch (string->uri (format #f "https://~a?go-get=1" module-path))))+ (module-metadata #f)+ (meta-tag-prefix "<meta name=\"go-import\" content=\"")+ (meta-tag-prefix-length (string-length meta-tag-prefix)))+ (do ((line (read-line port) (read-line port)))+ ((or (eof-object? line)+ module-metadata))+ (let ((meta-tag-index (string-contains line meta-tag-prefix)))+ (when meta-tag-index+ (let* ((start (+ meta-tag-index meta-tag-prefix-length))+ (end (string-index line #\" start)))+ (set! module-metadata+ (string-split (substring/shared line start end) #\space))))))+ (close-port port)+ module-metadata))++(define (module-meta-data-scs meta-data)+ "Return the source control system specified by a module's meta-data."+ (string->symbol (list-ref meta-data 1)))++(define (module-meta-data-repo-url meta-data goproxy-url)+ "Return the URL where the fetcher which will be used can download the source+control."+ (if (member (module-meta-data-scs meta-data) '(fossil mod))+ goproxy-url+ (list-ref meta-data 2)))++(define (source-uri scs-type scs-repo-url file)+ "Generate the `origin' block of a package depending on what type of source+control system is being used."+ (case scs-type+ ((git)+ `(origin+ (method git-fetch)+ (uri (git-reference+ (url ,scs-repo-url)+ (commit (string-append "v" version))))+ (file-name (git-file-name name version))+ (sha256+ (base32+ ,(guix-hash-url file)))))+ ((hg)+ `(origin+ (method hg-fetch)+ (uri (hg-reference+ (url ,scs-repo-url)+ (changeset ,version)))+ (file-name (format #f "~a-~a-checkout" name version))))+ ((svn)+ `(origin+ (method svn-fetch)+ (uri (svn-reference+ (url ,scs-repo-url)+ (revision (string->number version))+ (recursive? #f)))+ (file-name (format #f "~a-~a-checkout" name version))+ (sha256+ (base32+ ,(guix-hash-url file)))))+ (else+ (raise-exception (format #f "unsupported scs type: ~a" scs-type)))))++(define* (go-module->guix-package module-path #:key (goproxy-url "https://proxy.golang.org"))+ (call-with-temporary-output-file+ (lambda (temp port)+ (let* ((latest-version (fetch-latest-version goproxy-url module-path))+ (go.mod-path (fetch-go.mod goproxy-url module-path latest-version+ temp))+ (dependencies (map car (parse-go.mod temp)))+ (guix-name (to-guix-package-name module-path))+ (root-module-path (infer-module-root module-path))+ ;; VCS type and URL are not included in goproxy information. For+ ;; this we need to fetch it from the official module page.+ (meta-data (fetch-module-meta-data root-module-path))+ (scs-type (module-meta-data-scs meta-data))+ (scs-repo-url (module-meta-data-repo-url meta-data goproxy-url)))+ (values+ `(package+ (name ,guix-name)+ ;; Elide the "v" prefix Go uses+ (version ,(string-trim latest-version #\v))+ (source+ ,(source-uri scs-type scs-repo-url temp))+ (build-system go-build-system)+ ,@(maybe-inputs (map to-guix-package-name dependencies))+ ;; TODO(katco): It would be nice to make an effort to fetch this+ ;; from known forges, e.g. GitHub+ (home-page ,(format #f "https://~a" root-module-path))+ (synopsis "A Go package")+ (description ,(format #f "~a is a Go package." guix-name))+ (license #f))+ dependencies)))))++(define* (go-module-recursive-import package-name+ #:key (goproxy-url "https://proxy.golang.org"))+ (recursive-import+ package-name+ #:repo->guix-package (lambda* (name . _)+ (go-module->guix-package+ name+ #:goproxy-url goproxy-url))+ #:guix-name to-guix-package-name))diff --git a/guix/scripts/import.scm b/guix/scripts/import.scmindex 0a3863f965..1d2b45d942 100644--- a/guix/scripts/import.scm+++ b/guix/scripts/import.scm@@ -77,7 +77,7 @@ rather than \\n." ;;; (define importers '("gnu" "nix" "pypi" "cpan" "hackage" "stackage" "elpa" "gem"- "cran" "crate" "texlive" "json" "opam"))+ "go" "cran" "crate" "texlive" "json" "opam")) (define (resolve-importer name) (let ((module (resolve-interfacediff --git a/guix/scripts/import/go.scm b/guix/scripts/import/go.scmnew file mode 100644index 0000000000..fde7555973--- /dev/null+++ b/guix/scripts/import/go.scm@@ -0,0 +1,118 @@+;;; GNU Guix --- Functional package management for GNU+;;; Copyright © 2020 Katherine Cox-Buday <cox.katherine.e@gmail.com>+;;;+;;; This file is part of GNU Guix.+;;;+;;; GNU Guix is free software; you can redistribute it and/or modify it+;;; under the terms of the GNU General Public License as published by+;;; the Free Software Foundation; either version 3 of the License, or (at+;;; your option) any later version.+;;;+;;; GNU Guix is distributed in the hope that it will be useful, but+;;; WITHOUT ANY WARRANTY; without even the implied warranty of+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the+;;; GNU General Public License for more details.+;;;+;;; You should have received a copy of the GNU General Public License+;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.++(define-module (guix scripts import go)+ #:use-module (guix ui)+ #:use-module (guix utils)+ #:use-module (guix scripts)+ #:use-module (guix import go)+ #:use-module (guix scripts import)+ #:use-module (srfi srfi-1)+ #:use-module (srfi srfi-11)+ #:use-module (srfi srfi-37)+ #:use-module (ice-9 match)+ #:use-module (ice-9 format)+ #:export (guix-import-go))+++;;;+;;; Command-line options.+;;;++(define %default-options+ '())++(define (show-help)+ (display (G_ "Usage: guix import go PACKAGE-PATH+Import and convert the Go module for PACKAGE-PATH.\n"))+ (display (G_ "+ -h, --help display this help and exit"))+ (display (G_ "+ -V, --version display version information and exit"))+ (display (G_ "+ -r, --recursive generate package expressions for all Go modules\+ that are not yet in Guix"))+ (display (G_ "+ -p, --goproxy=GOPROXY specify which goproxy server to use"))+ (newline)+ (show-bug-report-information))++(define %options+ ;; Specification of the command-line options.+ (cons* (option '(#\h "help") #f #f+ (lambda args+ (show-help)+ (exit 0)))+ (option '(#\V "version") #f #f+ (lambda args+ (show-version-and-exit "guix import go")))+ (option '(#\r "recursive") #f #f+ (lambda (opt name arg result)+ (alist-cons 'recursive #t result)))+ (option '(#\p "goproxy") #t #f+ (lambda (opt name arg result)+ (alist-cons 'goproxy+ (string->symbol arg)+ (alist-delete 'goproxy result))))+ %standard-import-options))+++;;;+;;; Entry point.+;;;++(define (guix-import-go . args)+ (define (parse-options)+ ;; Return the alist of option values.+ (args-fold* args %options+ (lambda (opt name arg result)+ (leave (G_ "~A: unrecognized option~%") name))+ (lambda (arg result)+ (alist-cons 'argument arg result))+ %default-options))++ (let* ((opts (parse-options))+ (args (filter-map (match-lambda+ (('argument . value)+ value)+ (_ #f))+ (reverse opts))))+ (match args+ ((module-name)+ (if (assoc-ref opts 'recursive)+ (map (match-lambda+ ((and ('package ('name name) . rest) pkg)+ `(define-public ,(string->symbol name)+ ,pkg))+ (_ #f))+ (go-module-recursive-import module-name+ #:goproxy-url+ (or (assoc-ref opts 'goproxy)+ "https://proxy.golang.org")))+ (let ((sexp (go-module->guix-package module-name+ #:goproxy-url+ (or (assoc-ref opts 'goproxy)+ "https://proxy.golang.org"))))+ (unless sexp+ (leave (G_ "failed to download meta-data for module '~a'~%")+ module-name))+ sexp)))+ (()+ (leave (G_ "too few arguments~%")))+ ((many ...)+ (leave (G_ "too many arguments~%"))))))-- 2.28.0
J
J
JOULAUD François wrote on 25 Jan 22:03 +0100
(name . Katherine Cox-Buday)(address . cox.katherine.e@gmail.com)
20210125205910.qvmcusm5w5n4pawy@fjo-extia-HPdeb.example.avalenn.eu
Hello,
On Sat, Jan 23, 2021 at 04:41:18PM -0600, Katherine Cox-Buday wrote:
Toggle quote (2 lines)> Thanks so much for the patches, Helio, Joulaud!
You're welcome! As a side note I prefer to be adressed as "François" ;-)
Toggle quote (6 lines)> I have pushed everything (including Joulaud's patch with appropriate> attribution) here[2]. I am admittedly new at using email to organize> code changes, but using a forge seems easier.
> Can I suggest we coordinate there, or is that too much of an imposition?
I have no problem to coordinate in a forge and can push in a sharedbranch if you give me access.
Even if I must say I found it refreshing to be able to work with mails. Ithas the nice property of fully-offline and asynchronous communicationand it helped me to better articulate my problems.
Best regards,François
K
K
Katherine Cox-Buday wrote on 27 Jan 15:31 +0100
Re: packaging a golang package
87wnvymoxe.fsf@gmail.com
Hello again, François! I've redirected this thread to guix-devel, andthe bug since we've begun discussing implementation details.
JOULAUD François <Francois.JOULAUD@radiofrance.com> writes:
Toggle quote (6 lines)> First is to use vendored dependencies (when upstream provides them). This> one has the merits of simplicity (as we just have to download the> source and launch `go build` or whatever is needed) and less risks of> bugs. The downsides are known: difficult to audit Licences, duplication> of code source, difficulty to follow security bugs, etc.
-1 to this approach (explanation below).
Toggle quote (9 lines)> Second is to re-vendor package from go.mod (coming directly from code> source or synthetized from source) by creating a source derivation> including all dependencies. This is the strategy followed by Nixpkgs> as explained in [1]. (It seems to me this is the route followed in> the patches to from Helio in [2] but I did not take the time to read> them.) With this approach we still have some of the downsides of using> vendored upstream but it is at least easier to verify the existence> of upstream.
I don't fully understand this approach, so I don't understand how thisis different to approach three? Does this mean we create a pseudo,non-public package which states all the dependencies as inputs? If so,why wouldn't we just go with option three?
Toggle quote (8 lines)> Third is to package every dependencies. This is the most transparent way> and the one that leads to less code duplication but the downside is the> difficulty to scale with the number of packages (even if the objective of> patch at [3] is to automate it) and the need to have only one reference> version for each package in the distribution which have its own share of> problems (one of them being that we don't use those tested by upstream> as stated in [4]).
I think this is the eternal conflict between distributions andcode-bases. As a software engineer, I am a fan of vendoring. It removesentire classes of issues:
- Dependency on remote servers to be up to fetch dependencies and perform builds- Requiring some kind of system to pin versions- Needing to stay on top of releases even if that doesn't meet your schedule or needs
As a packager for a distribution, I dislike vendoring because of thereasons you outlined above, _but_ I also dislike building upstreamsoftware with versions of dependencies that weren't approved, tested,and verified, upstream. It seems to me like that's a recipe forunstable, maybe even insecure, software.
Normally, distributions are forced to do this because their packagingand build systems are only set up to support one version at a time. Guixcan theoretically support all versions, but doesn't want to take on thedependency graph explosion this approach would cause.
If we go on historical precedent, and how Guix handles the otherlanguage ecosystems, we should only have one version of each dependency,and build all software which relies on that dependency on the version wehave packaged. Guix has already begun taking on the difficult challengeswith this approach, including patching upstream to work with versions ofdependencies upstream was not built for.
I dislike it, but I also don't think we should try to solve the broaderclass of issues while trying to implement an importer. It should be alarger discussion within the Guix community across _all_ languagepackages about how we might achieve upstream parity while stillmaintaining our goals as a distribution, all while not crippling ourinfrastructure and people :)
That's my viewpoint, but I think we should all defer to some of thelonger-term maintainers who have helped guide Guix.
Thanks for writing up these options.
--Katherine
K
K
Katherine Cox-Buday wrote on 27 Jan 15:38 +0100
Re: [PATCH] Create importer for Go modules
(name . JOULAUD François)(address . Francois.JOULAUD@radiofrance.com)
87sg6mmolg.fsf@gmail.com
JOULAUD François <Francois.JOULAUD@radiofrance.com> writes:
Toggle quote (8 lines)> Hello,>> On Sat, Jan 23, 2021 at 04:41:18PM -0600, Katherine Cox-Buday wrote:>> Thanks so much for the patches, Helio, Joulaud!>> You're welcome! As a side note I prefer to be adressed as "François"> ;-)
I'm very sorry, François!
Toggle quote (13 lines)>> I have pushed everything (including Joulaud's patch with appropriate>> attribution) here[2]. I am admittedly new at using email to organize>> code changes, but using a forge seems easier.>>> Can I suggest we coordinate there, or is that too much of an imposition?>> I have no problem to coordinate in a forge and can push in a shared> branch if you give me access.>> Even if I must say I found it refreshing to be able to work with mails. It> has the nice property of fully-offline and asynchronous communication> and it helped me to better articulate my problems.
OK, how about we stick to the preferred Guix approach then andcoordinate here. Maybe you can teach me some things along the way :) Ido like the idea of an email-based forge!
The main problems I've had thus far are:
- The inter-diff patch you sent was not formatted correctly because it contained some extra leading whitespace. I found myself juggling a few different tools just to understand your changes. - The conversation about this seems to be happening in 3 different places: here, help-guix, and guix-devel. I guess we should be centralizing here. - I don't have any experience coordinating with people in this email-style forge. I don't know how the inter-diff patches work with attribution, nor how to get everyone on the same page. It seems like multiple people are making conflicting changes at once (maybe that's just my perception).
-- Katherine
J
J
JOULAUD François wrote on 28 Jan 08:29 +0100
RE: [bug#44178] [PATCH] Create importer for Go modules
(name . Timmy Douglas)(address . mail@timmydouglas.com)(name . 44178@debbugs.gnu.org)(address . 44178@debbugs.gnu.org)
PR0P264MB0425A31012E95E446788A61B96BA9@PR0P264MB0425.FRAP264.PROD.OUTLOOK.COM
Hello, Problem with k8s.io is known. I tested htmlprag quickly yesterday and I think I can come up with a fix soon. Regards, François Envoyé depuis un ordiphone. Veuillez excuser la brièveté.
Attachment: file
T
T
Timmy Douglas wrote on 28 Jan 06:01 +0100
Re: [bug#44178] [PATCH] Create importer for Go modules
87o8h94pv4.fsf@timmydouglas.com
I checked out https://github.com/kat-co/guix/tree/create-go-importer:
$ ./pre-inst-env guix import go -r github.com/coredns/coredns;;; note: source file /s/timmy/guix/guix/import/go.scm;;; newer than compiled /home/timmy/.cache/guile/ccache/3.0-LE-8-4.3/s/timmy/guix/guix/import/go.scm.go
Starting download of /tmp/guix-file.jkncVoFrom https://proxy.golang.org/github.com/coredns/coredns/@v/v1.8.1.mod... v1.8.1.mod 2KiB 870KiB/s 00:00 [##################] 100.0%
Starting download of /tmp/guix-file.GtI9fsFrom https://proxy.golang.org/k8s.io/klog/@v/v1.0.0.mod... v1.0.0.mod 68B 189KiB/s 00:00 [##################] 100.0%Backtrace:In ice-9/boot-9.scm: 1736:10 13 (with-exception-handler _ _ #:unwind? _ # _)In unknown file: 12 (apply-smob/0 #<thunk 7fdc57f964e0>)In ice-9/boot-9.scm: 718:2 11 (call-with-prompt _ _ #<procedure default-prompt-handle?>)In ice-9/eval.scm: 619:8 10 (_ #(#(#<directory (guile-user) 7fdc57be3f00>)))In guix/ui.scm: 2154:12 9 (run-guix-command _ . _)In guix/scripts/import.scm: 120:11 8 (guix-import . _)In ice-9/eval.scm: 159:9 7 (_ _)In guix/import/utils.scm: 464:27 6 (recursive-import _ #:repo->guix-package _ #:guix-name _ ?)In srfi/srfi-1.scm: 586:17 5 (map1 (("k8s.io/klog" #f) ("k8s.io/client-go" #f) (?) ?))In guix/import/utils.scm: 453:33 4 (lookup-node "k8s.io/klog" #f)In guix/utils.scm: 700:8 3 (call-with-temporary-output-file #<procedure 7fdc464b03?>)In ice-9/eval.scm: 293:34 2 (_ #(#(#(#(#(#(#(#(#<directory ?> ?) ?) ?) ?) ?) ?) ?) ?)) 155:9 1 (_ #(#(#<directory (guix import go) 7fdc55b65f00>) #f))In unknown file: 0 (list-ref #f 1)
ERROR: In procedure list-ref:In procedure list-ref: Wrong type argument in position 1: #f


This is due to:
(go-module->guix-package "k8s.io/klog")
The temp file looks like this:
module k8s.io/klog
go 1.12
require github.com/go-logr/logr v0.1.0


-> (fetch-module-meta-data '("github.com/go-logr/logr"))-> (string->uri (format #f "https://~a?go-get=1" module-path)) -> #f-> (http-fetch #f)
Is there a better way to debug this? `guix import` kicked me back to thecmd line instead of the guile debugger, which is understandable forusers, but the stacktrace is missing a lot of information. I openedemacs and geiser and had to eval a bunch of things to narrow it down. Itfeels like I'm doing it wrong. (I don't have much experience with scheme)
T
T
Timmy Douglas wrote on 28 Jan 09:18 +0100
Re: packaging a golang package
87ft2l4gq0.fsf@timmydouglas.com
Katherine Cox-Buday <cox.katherine.e@gmail.com> writes:
Toggle quote (13 lines)> Hello again, François! I've redirected this thread to guix-devel, and> the bug since we've begun discussing implementation details.>> JOULAUD François <Francois.JOULAUD@radiofrance.com> writes:>>> First is to use vendored dependencies (when upstream provides them). This>> one has the merits of simplicity (as we just have to download the>> source and launch `go build` or whatever is needed) and less risks of>> bugs. The downsides are known: difficult to audit Licences, duplication>> of code source, difficulty to follow security bugs, etc.>> -1 to this approach (explanation below).
Seems like there could be two sub-scenarios here: the code could use gomodules or not.
Toggle quote (14 lines)>> Second is to re-vendor package from go.mod (coming directly from code>> source or synthetized from source) by creating a source derivation>> including all dependencies. This is the strategy followed by Nixpkgs>> as explained in [1]. (It seems to me this is the route followed in>> the patches to from Helio in [2] but I did not take the time to read>> them.) With this approach we still have some of the downsides of using>> vendored upstream but it is at least easier to verify the existence>> of upstream.>> I don't fully understand this approach, so I don't understand how this> is different to approach three? Does this mean we create a pseudo,> non-public package which states all the dependencies as inputs? If so,> why wouldn't we just go with option three?
I think this approach is like saying you git clone the upstream repo,run go mod vendor or go mod download, then go build. Or whateverbuildGoModule does in nix (https://github.com/NixOS/nixpkgs/issues/84826)
I read through that issue and would personally vote for this approach ofusing `go` to restore the code.
I think trying to reimplement go module restore process with Guixpackages is bordering on Not Invented Here. There would be a neverending battle of trying to reimplement the go module restore process andthe amount of source packages would really clutter things up. I thinksome of the issues with the distro wanting to change a package could besolved with a feature to patch go.mod before calling `go` to restore.
Toggle quote (36 lines)>> Third is to package every dependencies. This is the most transparent way>> and the one that leads to less code duplication but the downside is the>> difficulty to scale with the number of packages (even if the objective of>> patch at [3] is to automate it) and the need to have only one reference>> version for each package in the distribution which have its own share of>> problems (one of them being that we don't use those tested by upstream>> as stated in [4]).>> I think this is the eternal conflict between distributions and> code-bases. As a software engineer, I am a fan of vendoring. It removes> entire classes of issues:>> - Dependency on remote servers to be up to fetch dependencies and> perform builds> - Requiring some kind of system to pin versions> - Needing to stay on top of releases even if that doesn't meet your> schedule or needs>> As a packager for a distribution, I dislike vendoring because of the> reasons you outlined above, _but_ I also dislike building upstream> software with versions of dependencies that weren't approved, tested,> and verified, upstream. It seems to me like that's a recipe for> unstable, maybe even insecure, software.>> Normally, distributions are forced to do this because their packaging> and build systems are only set up to support one version at a time. Guix> can theoretically support all versions, but doesn't want to take on the> dependency graph explosion this approach would cause.>> If we go on historical precedent, and how Guix handles the other> language ecosystems, we should only have one version of each dependency,> and build all software which relies on that dependency on the version we> have packaged. Guix has already begun taking on the difficult challenges> with this approach, including patching upstream to work with versions of> dependencies upstream was not built for.
That sounds like a pretty difficult challenge. Seems like it couldquickly become untenable if a commonly used library had some sort ofbreaking change between versions and different versions of it were usedby different packages.
I don't think anything is stopping Guix from having multiple versions,but hand-assigning them would feel pretty manual and error-prone. On theother hand, like you said, the dependency graph explosion from importingeverything would be overwhelming.
Toggle quote (10 lines)> I dislike it, but I also don't think we should try to solve the broader> class of issues while trying to implement an importer. It should be a> larger discussion within the Guix community across _all_ language> packages about how we might achieve upstream parity while still> maintaining our goals as a distribution, all while not crippling our> infrastructure and people :)>> That's my viewpoint, but I think we should all defer to some of the> longer-term maintainers who have helped guide Guix.
You wrote up both sides pretty well, but I couldn't tell if you had astrong opinion on having go restore dependencies vs Guix packagingdependencies. You had a lot of strong points for vendoring, but you'rewriting an importer...
Toggle quote (2 lines)> Thanks for writing up these options.
+1
L
L
Ludovic Courtès wrote on 28 Jan 14:27 +0100
Re: [PATCH] Create importer for Go modules
(name . Katherine Cox-Buday)(address . cox.katherine.e@gmail.com)
87czxpp4wy.fsf@gnu.org
Hello!
Katherine Cox-Buday <cox.katherine.e@gmail.com> skribis:
Toggle quote (4 lines)> - The conversation about this seems to be happening in 3 different> places: here, help-guix, and guix-devel. I guess we should be> centralizing here.
+1
Toggle quote (6 lines)> - I don't have any experience coordinating with people in this> email-style forge. I don't know how the inter-diff patches work with> attribution, nor how to get everyone on the same page. It seems like> multiple people are making conflicting changes at once (maybe that's> just my perception).
I think whoever among you is available to work on it these days couldtake the lead and prepare a final version of the patches. It looks likeit’s approaching a first “committable” version (perhaps just missing anaddition to doc/guix.texi and test cases like we have ‘tests/cpan.scm’ &co.)
For attribution, I’d keep Katherine as the commit author and add a‘Co-authored-by’ line for François and for Helio (that’s how we usuallyhandle that given that Git assumes each commit has a single author).
When that first version is committed, you can all submit patches forimprovements. For now, the focus should be on getting the first versionin. :-)
My 2¢,Ludo’.
L
L
Ludovic Courtès wrote on 28 Jan 17:03 +0100
Re: packaging a golang package
(name . adfeno--- via)(address . help-guix@gnu.org)
87mtwtkq0l.fsf@gnu.org
Hi,
adfeno--- via <help-guix@gnu.org> skribis:
Toggle quote (11 lines)> If by vendoring we mean bundling and also make users fetch data from places not explicitly committed to the GNU FSDG, then allow me to jump in to add some important notes.>> Em 27/01/2021 11:31, Katherine Cox-Buday escreveu:>> As a packager for a distribution, I dislike vendoring because of the>> reasons you outlined above, _but_ I also dislike building upstream>> software with versions of dependencies that weren't approved, tested,>> and verified, upstream. It seems to me like that's a recipe for>> unstable, maybe even insecure, software.>> I also agree that this would be problematic, but I fear that if we surrender to vendoring, we might defeat the purpose of GNU Guix.
I sympathize with that feeling.
It’s definitely a hard problem. Even Debian, which has been alighthouse for many on these matters, recently gave up:
https://lwn.net/Articles/843313/
I think both Katherine’s concerns and yours are valid.
IMO, the importer should be able to import things recursively and assumewe’re not going to bundle anything. It’d be up to the packager, then,to opt out and selectively use bundled copies of dependencies, if andwhen that appears necessary.
Toggle quote (2 lines)> I'm OK with the importer approach but, *in my opinion*, I don't think this tackles the true issue described on the 4th paragraph of the “License Rules” described on the GNU FSDG ([1]), this is why I opened Guix bug #45450 ([2]).
IMO, ‘guix import’ does not “steer users towards obtaining any nonfreeinformation” any more than wget does. It’s a tool for packagers thatreturns a package definition or template thereof, and it’s up to thepackager to decide what to do with it.
Thanks,Ludo’.
J
J
JOULAUD François wrote on 29 Jan 17:43 +0100
Re: [PATCH] Create importer for Go modules
(name . Katherine Cox-Buday)(address . cox.katherine.e@gmail.com)
20210129163945.irrdlm3updejkcsg@fjo-extia-HPdeb.example.avalenn.eu
Hello! On Thu, Jan 28, 2021 at 02:27:57PM +0100, Ludovic Courtès wrote:
Toggle quote (5 lines)> I think whoever among you is available to work on it these days could > take the lead and prepare a final version of the patches. It looks like > it’s approaching a first “committable” version (perhaps just missing an > addition to doc/guix.texi and test cases like we have ‘tests/cpan.scm’ & > co.)
I thought I would be able to send a working v2 of this patch today but it seems I was too optimistic. I found that some go.mod out there uses quoted string which our ad-hoc parser don't know how to parse. cf. https://github.com/go-yaml/yaml/blob/496545a6307b2a7d7a710fd516e5e16e8ab62dbc/go.mod I don't know if this is a blocker for a merge or not. Apart from that I don't know how to add guile-lib to the dependencies of Guix (in order to use htmlprag). Help needed. I tested it recursively with github.com/hashicorp/consul (which was one of those with the most dependencies I found) and it mostly works. Regards, François
J
J
JOULAUD François wrote on 29 Jan 17:52 +0100
[PATCHv2] Create importer for Go modules
(name . 44178@debbugs.gnu.org)(address . 44178@debbugs.gnu.org)
20210129164827.vrrty5gmi4paf7xv@fjo-extia-HPdeb.example.avalenn.eu
This patch add a `guix import go` command.
It was tested with several big repositories and seems to mostly work forthe import part (because building Guix packages is an other story).
* guix/import/go.sc: Created Go Importerm* guix/scripts/import.scm: Added Go Importer Subcommand* guix/scripts/import/go.scm: Created Go Importer Subcommand* doc/guix.texi: add a paragraph about `guix import go`* tests/import-go.scm: tests for parse-go.mod procedure
Signed-off-by: Francois Joulaud <francois.joulaud@radiofrance.com>--- doc/guix.texi | 25 +++ guix/import/go.scm | 384 +++++++++++++++++++++++++++++++++++++ guix/scripts/import.scm | 2 +- guix/scripts/import/go.scm | 118 ++++++++++++ tests/import-go.scm | 143 ++++++++++++++ 5 files changed, 671 insertions(+), 1 deletion(-) create mode 100644 guix/import/go.scm create mode 100644 guix/scripts/import/go.scm create mode 100644 tests/import-go.scm
Toggle diff (721 lines)diff --git a/doc/guix.texi b/doc/guix.texiindex 6ea782fd23..d77e2811ae 100644--- a/doc/guix.texi+++ b/doc/guix.texi@@ -860,6 +860,10 @@ substitutes (@pxref{Invoking guix publish}). @uref{https://ngyro.com/software/guile-semver.html, Guile-Semver} for the @code{crate} importer (@pxref{Invoking guix import}). +@item+@uref{https://www.nongnu.org/guile-lib/doc/ref/htmlprag/, guile-lib} for+the @code{crate} importer (@pxref{Invoking guix import}).+ @item When @url{http://www.bzip.org, libbz2} is available, @command{guix-daemon} can use it to compress build logs.@@ -11370,6 +11374,27 @@ Select the given repository (a repository name). Possible values include: of coq packages. @end itemize @end table++@item go+@cindex go+Import metadata for a Go module using+@uref{https://proxy.golang.org, proxy.golang.org}.++This importer is highly experimental.++@example+guix import go gopkg.in/yaml.v2+@end example++Additional options include:++@table @code+@item --recursive+@itemx -r+Traverse the dependency graph of the given upstream package recursively+and generate package expressions for all those packages that are not yet+in Guix.+@end table @end table The structure of the @command{guix import} code is modular. It would bediff --git a/guix/import/go.scm b/guix/import/go.scmnew file mode 100644index 0000000000..cf2d31ce12--- /dev/null+++ b/guix/import/go.scm@@ -0,0 +1,384 @@+;;; GNU Guix --- Functional package management for GNU+;;; Copyright © 2020 Katherine Cox-Buday <cox.katherine.e@gmail.com>+;;; Copyright © 2020 Helio Machado <0x2b3bfa0+guix@googlemail.com>+;;; Copyright © 2021 François Joulaud <francois.joulaud@radiofrance.com>+;;;+;;; This file is part of GNU Guix.+;;;+;;; GNU Guix is free software; you can redistribute it and/or modify it+;;; under the terms of the GNU General Public License as published by+;;; the Free Software Foundation; either version 3 of the License, or (at+;;; your option) any later version.+;;;+;;; GNU Guix is distributed in the hope that it will be useful, but+;;; WITHOUT ANY WARRANTY; without even the implied warranty of+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the+;;; GNU General Public License for more details.+;;;+;;; You should have received a copy of the GNU General Public License+;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.++;;; (guix import golang) wants to make easier to create Guix package+;;; declaration for Go modules.+;;;+;;; Modules in Go are "collection of related Go packages" which are+;;; "the unit of source code interchange and versioning".+;;; Modules are generally hosted in a repository.+;;;+;;; At this point it should handle correctly modules which+;;; - have only Go dependencies;+;;; - use go.mod;+;;; - and are accessible from proxy.golang.org (or configured GOPROXY).+;;;+;;; We translate Go module paths to a Guix package name under the+;;; assumption that there will be no collision.++(define-module (guix import go)+ #:use-module (ice-9 match)+ #:use-module (ice-9 rdelim)+ #:use-module (ice-9 receive)+ #:use-module (ice-9 regex)+ #:use-module (htmlprag)+ #:use-module (sxml xpath)+ #:use-module (srfi srfi-1)+ #:use-module (srfi srfi-9)+ #:use-module (srfi srfi-11)+ #:use-module (json)+ #:use-module ((guix download) #:prefix download:)+ #:use-module (guix import utils)+ #:use-module (guix import json)+ #:use-module (guix packages)+ #:use-module (guix upstream)+ #:use-module (guix utils)+ #:use-module ((guix licenses) #:prefix license:)+ #:use-module (guix base16)+ #:use-module (guix base32)+ #:use-module ((guix build download) #:prefix build-download:)+ #:use-module (web uri)++ #:export (go-module->guix-package+ go-module-recursive-import+ infer-module-root))++(define (go-path-escape path)+ "Escape a module path by replacing every uppercase letter with an exclamation+mark followed with its lowercase equivalent, as per the module Escaped Paths+specification. https://godoc.org/golang.org/x/mod/module#hdr-Escaped_Paths"+ (define (escape occurrence)+ (string-append "!" (string-downcase (match:substring occurrence))))+ (regexp-substitute/global #f "[A-Z]" path 'pre escape 'post))+++(define (fetch-latest-version goproxy-url module-path)+ "Fetches the version number of the latest version for MODULE-PATH from the+given GOPROXY-URL server."+ (assoc-ref+ (json-fetch (format #f "~a/~a/@latest" goproxy-url+ (go-path-escape module-path)))+ "Version"))++(define (fetch-go.mod goproxy-url module-path version file)+ "Fetches go.mod from the given GOPROXY-URL server for the given MODULE-PATH+and VERSION."+ (let ((url (format #f "~a/~a/@v/~a.mod" goproxy-url+ (go-path-escape module-path)+ (go-path-escape version))))+ (parameterize ((current-output-port (current-error-port)))+ (build-download:url-fetch url+ file+ #:print-build-trace? #f))))++(define (parse-go.mod go.mod-path)+ (parse-go.mod-port (open-input-file go.mod-path)))++(define (parse-go.mod-port go.mod-port)+ "PARSE-GO.MOD takes a filename in GO.MOD-PATH and extract a list of+requirements from it."+ ;; We parse only a subset of https://golang.org/ref/mod#go-mod-file-grammar+ ;; which we think necessary for our use case.+ (define (toplevel results)+ "Main parser, RESULTS is a pair of alist serving as accumulator for+ all encountered requirements and replacements."+ (let ((line (read-line)))+ (cond+ ((eof-object? line)+ ;; parsing ended, give back the result+ results)+ ((string=? line "require (")+ ;; a require block begins, delegate parsing to IN-REQUIRE+ (in-require results))+ ((string=? line "replace (")+ ;; a replace block begins, delegate parsing to IN-REPLACE+ (in-replace results))+ ((string-prefix? "require " line)+ ;; a require directive by itself+ (let* ((stripped-line (string-drop line 8))+ (new-results (require-directive results stripped-line)))+ (toplevel new-results)))+ ((string-prefix? "replace " line)+ ;; a replace directive by itself+ (let* ((stripped-line (string-drop line 8))+ (new-results (replace-directive results stripped-line)))+ (toplevel new-results)))+ (#t+ ;; unrecognised line, ignore silently+ (toplevel results)))))+ (define (in-require results)+ (let ((line (read-line)))+ (cond+ ((eof-object? line)+ ;; this should never happen here but we ignore silently+ results)+ ((string=? line ")")+ ;; end of block, coming back to toplevel+ (toplevel results))+ (#t+ (in-require (require-directive results line))))))+ (define (in-replace results)+ (let ((line (read-line)))+ (cond+ ((eof-object? line)+ ;; this should never happen here but we ignore silently+ results)+ ((string=? line ")")+ ;; end of block, coming back to toplevel+ (toplevel results))+ (#t+ (in-replace (replace-directive results line))))))+ (define (replace-directive results line)+ "Extract replaced modules and new requirements from replace directive+ in LINE and add to RESULTS."+ ;; ReplaceSpec = ModulePath [ Version ] "=>" FilePath newline+ ;; | ModulePath [ Version ] "=>" ModulePath Version newline .+ (let* ((requirements (car results))+ (replaced (cdr results))+ (re (string-concatenate+ '("([^[:blank:]]+)([[:blank:]]+([^[:blank:]]+))?"+ "[[:blank:]]+" "=>" "[[:blank:]]+"+ "([^[:blank:]]+)([[:blank:]]+([^[:blank:]]+))?")))+ (match (string-match re line))+ (module-path (match:substring match 1))+ (version (match:substring match 3))+ (new-module-path (match:substring match 4))+ (new-version (match:substring match 6))+ (new-replaced (acons module-path version replaced))+ (new-requirements+ (if (string-match "^\\.?\\./" new-module-path)+ requirements+ (acons new-module-path new-version requirements))))+ (cons new-requirements new-replaced)))+ (define (require-directive results line)+ "Extract requirement from LINE and add it to RESULTS."+ (let* ((requirements (car results))+ (replaced (cdr results))+ ;; A line in a require directive is composed of a module path and+ ;; a version separated by whitespace and an optionnal '//' comment at+ ;; the end.+ (re (string-concatenate+ '("^[[:blank:]]*"+ "([^[:blank:]]+)[[:blank:]]+([^[:blank:]]+)"+ "([[:blank:]]+//.*)?")))+ (match (string-match re line))+ (module-path (match:substring match 1))+ (version (match:substring match 2)))+ (cons (acons module-path version requirements) replaced)))+ (with-input-from-port go.mod-port+ (lambda ()+ (let* ((results (toplevel '(() . ())))+ (requirements (car results))+ (replaced (cdr results)))+ ;; At last we remove replaced modules from the requirements list+ (fold+ (lambda (replacedelem requirements)+ (alist-delete! (car replacedelem) requirements))+ requirements+ replaced)))))++(define (infer-module-root module-path)+ "Go modules can be defined at any level of a repository's tree, but querying+for the meta tag usually can only be done at the webpage at the root of the+repository. Therefore, it is sometimes necessary to try and derive a module's+root path from its path. For a set of well-known forges, the pattern of what+consists of a module's root page is known before hand."+ ;; See the following URL for the official Go equivalent:+ ;; https://github.com/golang/go/blob/846dce9d05f19a1f53465e62a304dea21b99f910/src/cmd/go/internal/vcs/vcs.go#L1026-L1087+ ;;+ ;; TODO: handle module path with VCS qualifier as described in+ ;; https://golang.org/ref/mod#vcs-find and+ ;; https://golang.org/cmd/go/#hdr-Remote_import_paths+ (define-record-type <vcs>+ (make-vcs url-prefix root-regex type)+ vcs?+ (url-prefix vcs-url-prefix)+ (root-regex vcs-root-regex)+ (type vcs-type))+ (let* ((known-vcs+ (list+ (make-vcs+ "github.com"+ "^(github\\.com/[A-Za-z0-9_.\\-]+/[A-Za-z0-9_.\\-]+)(/[A-Za-z0-9_.\\-]+)*$"+ 'git)+ (make-vcs+ "bitbucket.org"+ "^(bitbucket\\.org/([A-Za-z0-9_.\\-]+/[A-Za-z0-9_.\\-]+))(/[A-Za-z0-9_.\\-]+)*$"+ 'unknown)+ (make-vcs+ "hub.jazz.net/git/"+ "^(hub\\.jazz\\.net/git/[a-z0-9]+/[A-Za-z0-9_.\\-]+)(/[A-Za-z0-9_.\\-]+)*$"+ 'git)+ (make-vcs+ "git.apache.org"+ "^(git\\.apache\\.org/[a-z0-9_.\\-]+\\.git)(/[A-Za-z0-9_.\\-]+)*$"+ 'git)+ (make-vcs+ "git.openstack.org"+ "^(git\\.openstack\\.org/[A-Za-z0-9_.\\-]+/[A-Za-z0-9_.\\-]+)(\\.git)?(/[A-Za-z0-9_.\\-]+)*$"+ 'git)))+ (vcs (find (lambda (vcs) (string-prefix? (vcs-url-prefix vcs) module-path))+ known-vcs)))+ (if vcs+ (match:substring (string-match (vcs-root-regex vcs) module-path) 1)+ module-path)))++(define (go-module->guix-package-name module-path)+ "Converts a module's path to the canonical Guix format for Go packages."+ (string-downcase+ (string-append "go-"+ (string-replace-substring+ (string-replace-substring+ module-path+ "." "-")+ "/" "-"))))++(define-record-type <module-meta>+ (make-module-meta import-prefix vcs repo-root)+ module-meta?+ (import-prefix module-meta-import-prefix)+ ;; VCS field is a symbol+ (vcs module-meta-vcs)+ (repo-root module-meta-repo-root))++(define (fetch-module-meta-data module-path)+ "Fetches module meta-data from a module's landing page. This is+ necessary because goproxy servers don't currently provide all the+ information needed to build a package."+ ;; <meta name="go-import" content="import-prefix vcs repo-root">+ (define (meta-go-import->module-meta text)+ "Takes the content of the go-import meta tag as TEXT and gives back+ a MODULE-META record"+ (define (get-component s start)+ (let*+ ((start (string-skip s char-set:whitespace start))+ (end (string-index s char-set:whitespace start))+ (end (if end end (string-length s)))+ (result (substring s start end)))+ (values result end)))+ (let*-values (((import-prefix end) (get-component text 0))+ ((vcs end) (get-component text end))+ ((repo-root end) (get-component text end)))+ (make-module-meta import-prefix (string->symbol vcs) repo-root)))+ (define (html->meta-go-import port)+ "Read PORT with HTML content. Find the go-import meta tag and gives+ back its content as a string."+ (let* ((parsedhtml (html->sxml port))+ (extract-content (node-join+ (select-kids (node-typeof? 'html))+ (select-kids (node-typeof? 'head))+ (select-kids (node-typeof? 'meta))+ (select-kids (node-typeof? '@))+ (node-self+ (node-join+ (select-kids (node-typeof? 'name))+ (select-kids (node-equal? "go-import"))))+ (select-kids (node-typeof? 'content))+ (select-kids (lambda (_) #t))))+ (content (car (extract-content parsedhtml))))+ content))+ (let* ((port (build-download:http-fetch (string->uri (format #f "https://~a?go-get=1" module-path))))+ (meta-go-import (html->meta-go-import port))+ (module-metadata (meta-go-import->module-meta meta-go-import)))+ (close-port port)+ module-metadata))++(define (module-meta-data-repo-url meta-data goproxy-url)+ "Return the URL where the fetcher which will be used can download the source+control."+ (if (member (module-meta-vcs meta-data)'(fossil mod))+ goproxy-url+ (module-meta-repo-root meta-data)))++(define (source-uri vcs-type vcs-repo-url file)+ "Generate the `origin' block of a package depending on what type of source+control system is being used."+ (case vcs-type+ ((git)+ `(origin+ (method git-fetch)+ (uri (git-reference+ (url ,vcs-repo-url)+ (commit (string-append "v" version))))+ (file-name (git-file-name name version))+ (sha256+ (base32+ ,(guix-hash-url file)))))+ ((hg)+ `(origin+ (method hg-fetch)+ (uri (hg-reference+ (url ,vcs-repo-url)+ (changeset ,version)))+ (file-name (format #f "~a-~a-checkout" name version))))+ ((svn)+ `(origin+ (method svn-fetch)+ (uri (svn-reference+ (url ,vcs-repo-url)+ (revision (string->number version))+ (recursive? #f)))+ (file-name (format #f "~a-~a-checkout" name version))+ (sha256+ (base32+ ,(guix-hash-url file)))))+ (else+ (raise-exception (format #f "unsupported vcs type: ~a" vcs-type)))))++(define* (go-module->guix-package module-path #:key (goproxy-url "https://proxy.golang.org"))+ (call-with-temporary-output-file+ (lambda (temp port)+ (let* ((latest-version (fetch-latest-version goproxy-url module-path))+ (go.mod-path (fetch-go.mod goproxy-url module-path latest-version+ temp))+ (dependencies (map car (parse-go.mod temp)))+ (guix-name (go-module->guix-package-name module-path))+ (root-module-path (infer-module-root module-path))+ ;; VCS type and URL are not included in goproxy information. For+ ;; this we need to fetch it from the official module page.+ (meta-data (fetch-module-meta-data root-module-path))+ (vcs-type (module-meta-vcs meta-data))+ (vcs-repo-url (module-meta-data-repo-url meta-data goproxy-url)))+ (values+ `(package+ (name ,guix-name)+ ;; Elide the "v" prefix Go uses+ (version ,(string-trim latest-version #\v))+ (source+ ,(source-uri vcs-type vcs-repo-url temp))+ (build-system go-build-system)+ ,@(maybe-inputs (map go-module->guix-package-name dependencies))+ ;; TODO(katco): It would be nice to make an effort to fetch this+ ;; from known forges, e.g. GitHub+ (home-page ,(format #f "https://~a" root-module-path))+ (synopsis "A Go package")+ (description ,(format #f "~a is a Go package." guix-name))+ (license #f))+ dependencies)))))++(define* (go-module-recursive-import package-name+ #:key (goproxy-url "https://proxy.golang.org"))+ (recursive-import+ package-name+ #:repo->guix-package (lambda* (name . _)+ (go-module->guix-package+ name+ #:goproxy-url goproxy-url))+ #:guix-name go-module->guix-package-name))diff --git a/guix/scripts/import.scm b/guix/scripts/import.scmindex 0a3863f965..1d2b45d942 100644--- a/guix/scripts/import.scm+++ b/guix/scripts/import.scm@@ -77,7 +77,7 @@ rather than \\n." ;;; (define importers '("gnu" "nix" "pypi" "cpan" "hackage" "stackage" "elpa" "gem"- "cran" "crate" "texlive" "json" "opam"))+ "go" "cran" "crate" "texlive" "json" "opam")) (define (resolve-importer name) (let ((module (resolve-interfacediff --git a/guix/scripts/import/go.scm b/guix/scripts/import/go.scmnew file mode 100644index 0000000000..fde7555973--- /dev/null+++ b/guix/scripts/import/go.scm@@ -0,0 +1,118 @@+;;; GNU Guix --- Functional package management for GNU+;;; Copyright © 2020 Katherine Cox-Buday <cox.katherine.e@gmail.com>+;;;+;;; This file is part of GNU Guix.+;;;+;;; GNU Guix is free software; you can redistribute it and/or modify it+;;; under the terms of the GNU General Public License as published by+;;; the Free Software Foundation; either version 3 of the License, or (at+;;; your option) any later version.+;;;+;;; GNU Guix is distributed in the hope that it will be useful, but+;;; WITHOUT ANY WARRANTY; without even the implied warranty of+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the+;;; GNU General Public License for more details.+;;;+;;; You should have received a copy of the GNU General Public License+;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.++(define-module (guix scripts import go)+ #:use-module (guix ui)+ #:use-module (guix utils)+ #:use-module (guix scripts)+ #:use-module (guix import go)+ #:use-module (guix scripts import)+ #:use-module (srfi srfi-1)+ #:use-module (srfi srfi-11)+ #:use-module (srfi srfi-37)+ #:use-module (ice-9 match)+ #:use-module (ice-9 format)+ #:export (guix-import-go))+++;;;+;;; Command-line options.+;;;++(define %default-options+ '())++(define (show-help)+ (display (G_ "Usage: guix import go PACKAGE-PATH+Import and convert the Go module for PACKAGE-PATH.\n"))+ (display (G_ "+ -h, --help display this help and exit"))+ (display (G_ "+ -V, --version display version information and exit"))+ (display (G_ "+ -r, --recursive generate package expressions for all Go modules\+ that are not yet in Guix"))+ (display (G_ "+ -p, --goproxy=GOPROXY specify which goproxy server to use"))+ (newline)+ (show-bug-report-information))++(define %options+ ;; Specification of the command-line options.+ (cons* (option '(#\h "help") #f #f+ (lambda args+ (show-help)+ (exit 0)))+ (option '(#\V "version") #f #f+ (lambda args+ (show-version-and-exit "guix import go")))+ (option '(#\r "recursive") #f #f+ (lambda (opt name arg result)+ (alist-cons 'recursive #t result)))+ (option '(#\p "goproxy") #t #f+ (lambda (opt name arg result)+ (alist-cons 'goproxy+ (string->symbol arg)+ (alist-delete 'goproxy result))))+ %standard-import-options))+++;;;+;;; Entry point.+;;;++(define (guix-import-go . args)+ (define (parse-options)+ ;; Return the alist of option values.+ (args-fold* args %options+ (lambda (opt name arg result)+ (leave (G_ "~A: unrecognized option~%") name))+ (lambda (arg result)+ (alist-cons 'argument arg result))+ %default-options))++ (let* ((opts (parse-options))+ (args (filter-map (match-lambda+ (('argument . value)+ value)+ (_ #f))+ (reverse opts))))+ (match args+ ((module-name)+ (if (assoc-ref opts 'recursive)+ (map (match-lambda+ ((and ('package ('name name) . rest) pkg)+ `(define-public ,(string->symbol name)+ ,pkg))+ (_ #f))+ (go-module-recursive-import module-name+ #:goproxy-url+ (or (assoc-ref opts 'goproxy)+ "https://proxy.golang.org")))+ (let ((sexp (go-module->guix-package module-name+ #:goproxy-url+ (or (assoc-ref opts 'goproxy)+ "https://proxy.golang.org"))))+ (unless sexp+ (leave (G_ "failed to download meta-data for module '~a'~%")+ module-name))+ sexp)))+ (()+ (leave (G_ "too few arguments~%")))+ ((many ...)+ (leave (G_ "too many arguments~%"))))))diff --git a/tests/import-go.scm b/tests/import-go.scmnew file mode 100644index 0000000000..7c59bf2d7c--- /dev/null+++ b/tests/import-go.scm@@ -0,0 +1,143 @@+;;; GNU Guix --- Functional package management for GNU+;;; Copyright © 2021 François Joulaud <francois.joulaud@radiofrance.com>+;;;+;;; This file is part of GNU Guix.+;;;+;;; GNU Guix is free software; you can redistribute it and/or modify it+;;; under the terms of the GNU General Public License as published by+;;; the Free Software Foundation; either version 3 of the License, or (at+;;; your option) any later version.+;;;+;;; GNU Guix is distributed in the hope that it will be useful, but+;;; WITHOUT ANY WARRANTY; without even the implied warranty of+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the+;;; GNU General Public License for more details.+;;;+;;; You should have received a copy of the GNU General Public License+;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.++;;; Summary+;; Tests for guix/import/go.scm++(define-module (test-import-go)+ #:use-module (guix import go)+ #:use-module (guix base32)+ ;#:use-module (guix tests)+ #:use-module (ice-9 iconv)+ #:use-module (ice-9 match)+ #:use-module (srfi srfi-64))++(define fixture-go-mod-simple+ "module my/thing+go 1.12+require other/thing v1.0.2+require new/thing/v2 v2.3.4+exclude old/thing v1.2.3+replace bad/thing v1.4.5 => good/thing v1.4.5+")++(define fixture-go-mod-with-block+ "module M++require (+ A v1+ B v1.0.0+ C v1.0.0+ D v1.2.3+ E dev+)++exclude D v1.2.3+")+++(define fixture-go-mod-complete+ "module M++go 1.13++replace github.com/myname/myproject/myapi => ./api++replace github.com/mymname/myproject/thissdk => ../sdk++replace launchpad.net/gocheck => github.com/go-check/check v0.0.0-20140225173054-eb6ee6f84d0a++require (+ github.com/user/project v1.1.11+ github.com/user/project/sub/directory v1.1.12+ bitbucket.org/user/project v1.11.20+ bitbucket.org/user/project/sub/directory v1.11.21+ launchpad.net/project v1.1.13+ launchpad.net/project/series v1.1.14+ launchpad.net/project/series/sub/directory v1.1.15+ launchpad.net/~user/project/branch v1.1.16+ launchpad.net/~user/project/branch/sub/directory v1.1.17+ hub.jazz.net/git/user/project v1.1.18+ hub.jazz.net/git/user/project/sub/directory v1.1.19+ k8s.io/kubernetes/subproject v1.1.101+ one.example.com/abitrary/repo v1.1.111+ two.example.com/abitrary/repo v0.0.2+)++replace two.example.com/abitrary/repo => github.com/corp/arbitrary-repo v0.0.2++replace (+ golang.org/x/sys => golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a // pinned to release-branch.go1.13+ golang.org/x/tools => golang.org/x/tools v0.0.0-20190821162956-65e3620a7ae7 // pinned to release-branch.go1.13+)++")++(test-begin "import go")++(test-equal "go-path-escape"+ "github.com/!azure/!avere"+ ((@@ (guix import go) go-path-escape) "github.com/Azure/Avere"))++++;; We define a function for all similar tests with different go.mod files+(define (testing-parse-mod name expected input)+ (define (inf? p1 p2)+ (string<? (car p1) (car p2)))+ (let ((input-port (open-input-string input)))+ (test-equal name+ (sort expected inf?)+ (sort+ ( (@@ (guix import go) parse-go.mod-port)+ input-port)+ inf?))))++(testing-parse-mod "parse-go.mod-simple"+ '(("good/thing" . "v1.4.5")+ ("new/thing/v2" . "v2.3.4")+ ("other/thing" . "v1.0.2"))+ fixture-go-mod-simple)++(testing-parse-mod "parse-go.mod-with-block"+ '(("A" . "v1")+ ("B" . "v1.0.0")+ ("C" . "v1.0.0")+ ("D" . "v1.2.3")+ ("E" . "dev"))+ fixture-go-mod-with-block)++(testing-parse-mod "parse-go.mod-complete"+ '(("github.com/corp/arbitrary-repo" . "v0.0.2")+ ("one.example.com/abitrary/repo" . "v1.1.111")+ ("hub.jazz.net/git/user/project/sub/directory" . "v1.1.19")+ ("hub.jazz.net/git/user/project" . "v1.1.18")+ ("launchpad.net/~user/project/branch/sub/directory" . "v1.1.17")+ ("launchpad.net/~user/project/branch" . "v1.1.16")+ ("launchpad.net/project/series/sub/directory" . "v1.1.15")+ ("launchpad.net/project/series" . "v1.1.14")+ ("launchpad.net/project" . "v1.1.13")+ ("bitbucket.org/user/project/sub/directory" . "v1.11.21")+ ("bitbucket.org/user/project" . "v1.11.20")+ ("k8s.io/kubernetes/subproject" . "v1.1.101")+ ("github.com/user/project/sub/directory" . "v1.1.12")+ ("github.com/user/project" . "v1.1.11")+ ("github.com/go-check/check" . "v0.0.0-20140225173054-eb6ee6f84d0a"))+ fixture-go-mod-complete)++(test-end "import go")-- 2.30.0
L
L
Ludovic Courtès wrote on 31 Jan 17:23 +0100
Re: [PATCH] Create importer for Go modules
(name . JOULAUD François)(address . Francois.JOULAUD@radiofrance.com)
877dntnkgw.fsf@gnu.org
Hi,
JOULAUD François <Francois.JOULAUD@radiofrance.com> skribis:
Toggle quote (16 lines)> On Thu, Jan 28, 2021 at 02:27:57PM +0100, Ludovic Courtès wrote:>> I think whoever among you is available to work on it these days could>> take the lead and prepare a final version of the patches. It looks like>> it’s approaching a first “committable” version (perhaps just missing an>> addition to doc/guix.texi and test cases like we have ‘tests/cpan.scm’ &>> co.)>> I thought I would be able to send a working v2 of this patch today but> it seems I was too optimistic.>> I found that some go.mod out there uses quoted string> which our ad-hoc parser don't know how to parse. cf.> https://github.com/go-yaml/yaml/blob/496545a6307b2a7d7a710fd516e5e16e8ab62dbc/go.mod>> I don't know if this is a blocker for a merge or not.
Your call; if it’s an infrequent problem, we could commit it and leave aFIXME in the code. We could also use guile-yaml (or maybe some Gocode?) to parse it correctly.
Toggle quote (3 lines)> Apart from that I don't know how to add guile-lib to the dependencies of> Guix (in order to use htmlprag). Help needed.
So ‘xml->sxml’ isn’t good enough? (If we can avoid the guile-libdependency, the better.)
To depend on Guile-Lib, you would:
1. Add it to (guix self) — this is the code used by ‘guix pull’;
2. Add it to the ‘inputs’ field of the ‘guix’ package;
3. Maybe add a configure check in ‘configure.ac’, though it would be best if we could arrange to make it an optional dependency.
Toggle quote (3 lines)> I tested it recursively with github.com/hashicorp/consul (which was one> of those with the most dependencies I found) and it mostly works.
Yay, sounds promising!
Thanks,Ludo’.
J
J
JOULAUD François wrote on 19 Feb 16:51 +0100
(name . Ludovic Courtès)(address . ludo@gnu.org)
20210219154028.z5aoyozf7qsrz3mt@fjo-extia-HPdeb.example.avalenn.eu
Hello, I will send a v3 of the patch very soon which I hope will be mergeable. It is still very experimental and I noted it as such in documentation. I had problems with the hash in origin which did not work as expected. I prefered to drop this completely for now and replaced it by a full 0 placeholder. At least it is consistently and conspicuously bad. I have a working version of something which download git repo and generates guix hash for it but I'd rather to push this in a subsequent patch as it is very rough for now. On Sun, Jan 31, 2021 at 05:23:59PM +0100, Ludovic Courtès wrote:
Toggle quote (7 lines)> JOULAUD François <Francois.JOULAUD@radiofrance.com> skribis: > > I found that some go.mod out there uses quoted string > > which our ad-hoc parser don't know how to parse. cf. > > Your call; if it’s an infrequent problem, we could commit it and leave a > FIXME in the code. We could also use guile-yaml (or maybe some Go > code?) to parse it correctly.
I found a way to work around the problem. Indeed using "go mod" to parse the go.mod file could perhaps be easier and have been explored[1]. It works for now with the ad-hoc parser. Let's revisit the choice later if neeeded.
Toggle quote (5 lines)> > Apart from that I don't know how to add guile-lib to the dependencies of > > Guix (in order to use htmlprag). Help needed. > > So ‘xml->sxml’ isn’t good enough? (If we can avoid the guile-lib > dependency, the better.)
HTML is not well-formed XML (and the hopes given by XHTML have faded) so no, xml->sxml is unfortunately not good enough.
Toggle quote (5 lines)> To depend on Guile-Lib, you would: > > 1. Add it to (guix self) — this is the code used by ‘guix pull’; > > 2. Add it to the ‘inputs’ field of the ‘guix’ package;
Done 1 and 2 in the patch. Mainly by copy-paste without understanding anything. I hope it will work.
Toggle quote (2 lines)> 3. Maybe add a configure check in ‘configure.ac’, though it would be > best if we could arrange to make it an optional dependency.
I did not touch to configure.ac which is a strange beast to me. Hope that for optional dependency (only used in "guix import go") it is sufficient.
Toggle quote (4 lines)> > I tested it recursively with github.com/hashicorp/consul (which was one > > of those with the most dependencies I found) and it mostly works. > > Yay, sounds promising!
Promising but not there. I have now several recursive dependencies between generated packages I must investigate. Still, 234 package definitions generated on one recursive import (even if I had to retry because of intermittent failure of fetching from proxy.golang.org), so Yay! [1]: https://git.sr.ht/~elais/orange/tree/master/item/guix/import/go.scm#L78
J
J
JOULAUD François wrote on 19 Feb 17:21 +0100
[PATCHv3] Create importer for Go modules
(name . 44178@debbugs.gnu.org)(address . 44178@debbugs.gnu.org)
20210219161737.4l266imcd24gqxwn@fjo-extia-HPdeb.example.avalenn.eu
This patch add a `guix import go` command.
It was tested with several big repositories and mostly works. Severalfeatures are lacking (see TODO in source code) but we will do theimprovments step-by-step in future patches.
* doc/guix.texi: doc about go importer and guile-lib dependency* gnu/packages/package-management.scm: added guile-lib dependency* guix/self.scm: add guile-lib dependency* guix/build-system/go.scm: go-version->git-ref function* guix/import/go.scm: Created Go importer* guix/scripts/import/go.scm: Subcommand for Go importer* guix/scripts/import.scm: Declare subcommand guix import go* tests/import-go.scm: Tests for parse-go.mod procedure
Signed-off-by: Francois Joulaud <francois.joulaud@radiofrance.com>--- doc/guix.texi | 26 ++ gnu/packages/package-management.scm | 2 + guix/build-system/go.scm | 35 ++- guix/import/go.scm | 416 ++++++++++++++++++++++++++++ guix/scripts/import.scm | 2 +- guix/scripts/import/go.scm | 118 ++++++++ guix/self.scm | 5 +- tests/import-go.scm | 144 ++++++++++ 8 files changed, 745 insertions(+), 3 deletions(-) create mode 100644 guix/import/go.scm create mode 100644 guix/scripts/import/go.scm create mode 100644 tests/import-go.scm
Toggle diff (846 lines)diff --git a/doc/guix.texi b/doc/guix.texiindex 5d28fca837..89c8abd261 100644--- a/doc/guix.texi+++ b/doc/guix.texi@@ -861,6 +861,10 @@ substitutes (@pxref{Invoking guix publish}). @uref{https://ngyro.com/software/guile-semver.html, Guile-Semver} for the @code{crate} importer (@pxref{Invoking guix import}). +@item+@uref{https://www.nongnu.org/guile-lib/doc/ref/htmlprag/, guile-lib} for+the @code{crate} importer (@pxref{Invoking guix import}).+ @item When @url{http://www.bzip.org, libbz2} is available, @command{guix-daemon} can use it to compress build logs.@@ -11493,6 +11497,28 @@ Select the given repository (a repository name). Possible values include: of coq packages. @end itemize @end table++@item go+@cindex go+Import metadata for a Go module using+@uref{https://proxy.golang.org, proxy.golang.org}.++This importer is highly experimental. See the source code for more info+about the current state.++@example+guix import go gopkg.in/yaml.v2+@end example++Additional options include:++@table @code+@item --recursive+@itemx -r+Traverse the dependency graph of the given upstream package recursively+and generate package expressions for all those packages that are not yet+in Guix.+@end table @end table The structure of the @command{guix import} code is modular. It would bediff --git a/gnu/packages/package-management.scm b/gnu/packages/package-management.scmindex 9fb8c40a31..06bb5bd2df 100644--- a/gnu/packages/package-management.scm+++ b/gnu/packages/package-management.scm@@ -304,6 +304,7 @@ $(prefix)/etc/init.d\n"))) '((assoc-ref inputs "guile")))) (avahi (assoc-ref inputs "guile-avahi")) (gcrypt (assoc-ref inputs "guile-gcrypt"))+ (guile-lib (assoc-ref inputs "guile-lib")) (json (assoc-ref inputs "guile-json")) (sqlite (assoc-ref inputs "guile-sqlite3")) (zlib (assoc-ref inputs "guile-zlib"))@@ -367,6 +368,7 @@ $(prefix)/etc/init.d\n"))) `(("guile-avahi" ,guile-avahi))) ("guile-gcrypt" ,guile-gcrypt) ("guile-json" ,guile-json-4)+ ("guile-lib" ,guile-lib) ("guile-sqlite3" ,guile-sqlite3) ("guile-zlib" ,guile-zlib) ("guile-lzlib" ,guile-lzlib)diff --git a/guix/build-system/go.scm b/guix/build-system/go.scmindex f8ebaefb27..594e0cb4f3 100644--- a/guix/build-system/go.scm+++ b/guix/build-system/go.scm@@ -26,9 +26,42 @@ #:use-module (guix build-system gnu) #:use-module (guix packages) #:use-module (ice-9 match)+ #:use-module (ice-9 regex) #:export (%go-build-system-modules go-build- go-build-system))+ go-build-system++ go-version->git-ref))++(define (go-version->git-ref version)+ "GO-VERSION->GIT-REF parse pseudo-versions and extract the commit+ hash from it, defaulting to full VERSION if we don't recognise a+ pseudo-version pattern."+ ;; A module version like v1.2.3 is introduced by tagging a revision in+ ;; the underlying source repository. Untagged revisions can be referred+ ;; to using a "pseudo-version" like v0.0.0-yyyymmddhhmmss-abcdefabcdef,+ ;; where the time is the commit time in UTC and the final suffix is the+ ;; prefix of the commit hash.+ ;; cf. https://golang.org/cmd/go/#hdr-Pseudo_versions+ (let* ((version+ ;; if a source code repository has a v2.0.0 or later tag for+ ;; a file tree with no go.mod, the version is considered to be+ ;; part of the v1 module's available versions and is given an+ ;; +incompatible suffix+ ;; https://golang.org/cmd/go/#hdr-Module_compatibility_and_semantic_versioning+ (if (string-suffix? "+incompatible" version)+ (string-drop-right version 13)+ version))+ (re (string-concatenate+ (list+ "(v?[0-9]\\.[0-9]\\.[0-9])" ; "v" prefix can be omitted in version prefix+ "(-|-pre\\.0\\.|-0\\.)" ; separator+ "([0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9])-" ; timestamp+ "([0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f])"))) ; commit hash+ (match (string-match re version)))+ (if match+ (match:substring match 4)+ version))) ;; Commentary: ;;diff --git a/guix/import/go.scm b/guix/import/go.scmnew file mode 100644index 0000000000..fead355bd2--- /dev/null+++ b/guix/import/go.scm@@ -0,0 +1,416 @@+;;; GNU Guix --- Functional package management for GNU+;;; Copyright © 2020 Katherine Cox-Buday <cox.katherine.e@gmail.com>+;;; Copyright © 2020 Helio Machado <0x2b3bfa0+guix@googlemail.com>+;;; Copyright © 2021 François Joulaud <francois.joulaud@radiofrance.com>+;;;+;;; This file is part of GNU Guix.+;;;+;;; GNU Guix is free software; you can redistribute it and/or modify it+;;; under the terms of the GNU General Public License as published by+;;; the Free Software Foundation; either version 3 of the License, or (at+;;; your option) any later version.+;;;+;;; GNU Guix is distributed in the hope that it will be useful, but+;;; WITHOUT ANY WARRANTY; without even the implied warranty of+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the+;;; GNU General Public License for more details.+;;;+;;; You should have received a copy of the GNU General Public License+;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.++;;; (guix import golang) wants to make easier to create Guix package+;;; declaration for Go modules.+;;;+;;; Modules in Go are "collection of related Go packages" which are+;;; "the unit of source code interchange and versioning".+;;; Modules are generally hosted in a repository.+;;;+;;; At this point it should handle correctly modules which+;;; have only Go dependencies and are accessible from proxy.golang.org+;;; (or configured GOPROXY).+;;;+;;; We want it to work more or less this way:+;;; - get latest version for the module from GOPROXY+;;; - infer VCS root repo from which we will check-out source by+;;; + recognising known patterns (like github.com)+;;; + or (TODO) recognising .vcs suffix+;;; + or parsing meta tag in html served at the URL+;;; + or (TODO) if nothing else works by using zip file served by GOPROXY+;;; - get go.mod from GOPROXY (which is able to synthetize one if needed)+;;; - extract list of dependencies from this go.mod+;;;+;;; We translate Go module paths to a Guix package name under the+;;; assumption that there will be no collision.++;;; TODO list+;;; - get correct hash in vcs->origin+;;; - print partial result during recursive imports (need to catch+;;; exceptions)+;;; - infer repo from module path with VCS qualifier+;;; (e.g. site.example/my/path/to/repo.git/and/subdir/module)+;;; - don't print fetch messages to stdout+;;; - pre-fill synopsis, description and license++(define-module (guix import go)+ #:use-module (ice-9 match)+ #:use-module (ice-9 rdelim)+ #:use-module (ice-9 receive)+ #:use-module (ice-9 regex)+ #:use-module (guix build-system go)+ #:use-module (htmlprag)+ #:use-module (sxml xpath)+ #:use-module (srfi srfi-1)+ #:use-module (srfi srfi-9)+ #:use-module (srfi srfi-11)+ #:use-module (json)+ #:use-module ((guix download) #:prefix download:)+ #:use-module (guix git)+ #:use-module (guix import utils)+ #:use-module (guix import json)+ #:use-module (guix packages)+ #:use-module (guix upstream)+ #:use-module (guix utils)+ #:use-module ((guix licenses) #:prefix license:)+ #:use-module (guix base16)+ #:use-module (guix base32)+ #:use-module (guix memoization)+ #:use-module ((guix build download) #:prefix build-download:)+ #:use-module (web uri)++ #:export (go-module->guix-package+ go-module-recursive-import+ infer-module-root-repo))+++(define (go-path-escape path)+ "Escape a module path by replacing every uppercase letter with an exclamation+mark followed with its lowercase equivalent, as per the module Escaped Paths+specification. https://godoc.org/golang.org/x/mod/module#hdr-Escaped_Paths"+ (define (escape occurrence)+ (string-append "!" (string-downcase (match:substring occurrence))))+ (regexp-substitute/global #f "[A-Z]" path 'pre escape 'post))+++(define (go-module-latest-version goproxy-url module-path)+ "Fetches the version number of the latest version for MODULE-PATH from the+given GOPROXY-URL server."+ (assoc-ref+ (json-fetch (format #f "~a/~a/@latest" goproxy-url+ (go-path-escape module-path)))+ "Version"))++(define go-module-latest-version* (memoize go-module-latest-version))++(define (fetch-go.mod goproxy-url module-path version file)+ "Fetches go.mod from the given GOPROXY-URL server for the given MODULE-PATH+and VERSION."+ (let ((url (format #f "~a/~a/@v/~a.mod" goproxy-url+ (go-path-escape module-path)+ (go-path-escape version))))+ (parameterize ((current-output-port (current-error-port)))+ (build-download:url-fetch url+ file+ #:print-build-trace? #f))))++(define (parse-go.mod go.mod-path)+ (parse-go.mod-port (open-input-file go.mod-path)))++(define (parse-go.mod-port go.mod-port)+ "PARSE-GO.MOD takes a filename in GO.MOD-PATH and extract a list of+requirements from it."+ ;; We parse only a subset of https://golang.org/ref/mod#go-mod-file-grammar+ ;; which we think necessary for our use case.+ (define (toplevel results)+ "Main parser, RESULTS is a pair of alist serving as accumulator for+ all encountered requirements and replacements."+ (let ((line (read-line)))+ (cond+ ((eof-object? line)+ ;; parsing ended, give back the result+ results)+ ((string=? line "require (")+ ;; a require block begins, delegate parsing to IN-REQUIRE+ (in-require results))+ ((string=? line "replace (")+ ;; a replace block begins, delegate parsing to IN-REPLACE+ (in-replace results))+ ((string-prefix? "require " line)+ ;; a require directive by itself+ (let* ((stripped-line (string-drop line 8))+ (new-results (require-directive results stripped-line)))+ (toplevel new-results)))+ ((string-prefix? "replace " line)+ ;; a replace directive by itself+ (let* ((stripped-line (string-drop line 8))+ (new-results (replace-directive results stripped-line)))+ (toplevel new-results)))+ (#t+ ;; unrecognised line, ignore silently+ (toplevel results)))))+ (define (in-require results)+ (let ((line (read-line)))+ (cond+ ((eof-object? line)+ ;; this should never happen here but we ignore silently+ results)+ ((string=? line ")")+ ;; end of block, coming back to toplevel+ (toplevel results))+ (#t+ (in-require (require-directive results line))))))+ (define (in-replace results)+ (let ((line (read-line)))+ (cond+ ((eof-object? line)+ ;; this should never happen here but we ignore silently+ results)+ ((string=? line ")")+ ;; end of block, coming back to toplevel+ (toplevel results))+ (#t+ (in-replace (replace-directive results line))))))+ (define (replace-directive results line)+ "Extract replaced modules and new requirements from replace directive+ in LINE and add to RESULTS."+ ;; ReplaceSpec = ModulePath [ Version ] "=>" FilePath newline+ ;; | ModulePath [ Version ] "=>" ModulePath Version newline .+ (let* ((requirements (car results))+ (replaced (cdr results))+ (re (string-concatenate+ '("([^[:blank:]]+)([[:blank:]]+([^[:blank:]]+))?"+ "[[:blank:]]+" "=>" "[[:blank:]]+"+ "([^[:blank:]]+)([[:blank:]]+([^[:blank:]]+))?")))+ (match (string-match re line))+ (module-path (match:substring match 1))+ (version (match:substring match 3))+ (new-module-path (match:substring match 4))+ (new-version (match:substring match 6))+ (new-replaced (acons module-path version replaced))+ (new-requirements+ (if (string-match "^\\.?\\./" new-module-path)+ requirements+ (acons new-module-path new-version requirements))))+ (cons new-requirements new-replaced)))+ (define (require-directive results line)+ "Extract requirement from LINE and add it to RESULTS."+ (let* ((requirements (car results))+ (replaced (cdr results))+ ;; A line in a require directive is composed of a module path and+ ;; a version separated by whitespace and an optionnal '//' comment at+ ;; the end.+ (re (string-concatenate+ '("^[[:blank:]]*"+ "([^[:blank:]]+)[[:blank:]]+([^[:blank:]]+)"+ "([[:blank:]]+//.*)?")))+ (match (string-match re line))+ (module-path (match:substring match 1))+ ;; we saw double-quoted string in the wild without escape+ ;; sequences so we just trim the quotes+ (module-path (string-trim-both module-path #\"))+ (version (match:substring match 2)))+ (cons (acons module-path version requirements) replaced)))+ (with-input-from-port go.mod-port+ (lambda ()+ (let* ((results (toplevel '(() . ())))+ (requirements (car results))+ (replaced (cdr results)))+ ;; At last we remove replaced modules from the requirements list+ (fold+ (lambda (replacedelem requirements)+ (alist-delete! (car replacedelem) requirements))+ requirements+ replaced)))))++(define (infer-module-root-repo module-path)+ "Go modules can be defined at any level of a repository's tree, but querying+for the meta tag usually can only be done at the webpage at the root of the+repository. Therefore, it is sometimes necessary to try and derive a module's+root path from its path. For a set of well-known forges, the pattern of what+consists of a module's root page is known before hand."+ ;; See the following URL for the official Go equivalent:+ ;; https://github.com/golang/go/blob/846dce9d05f19a1f53465e62a304dea21b99f910/src/cmd/go/internal/vcs/vcs.go#L1026-L1087+ ;;+ ;; TODO: handle module path with VCS qualifier as described in+ ;; https://golang.org/ref/mod#vcs-find and+ ;; https://golang.org/cmd/go/#hdr-Remote_import_paths+ (define-record-type <vcs>+ (make-vcs url-prefix root-regex type)+ vcs?+ (url-prefix vcs-url-prefix)+ (root-regex vcs-root-regex)+ (type vcs-type))+ (let* ((known-vcs+ (list+ (make-vcs+ "github.com"+ "^(github\\.com/[A-Za-z0-9_.\\-]+/[A-Za-z0-9_.\\-]+)(/[A-Za-z0-9_.\\-]+)*$"+ 'git)+ (make-vcs+ "bitbucket.org"+ "^(bitbucket\\.org/([A-Za-z0-9_.\\-]+/[A-Za-z0-9_.\\-]+))(/[A-Za-z0-9_.\\-]+)*$"+ 'unknown)+ (make-vcs+ "hub.jazz.net/git/"+ "^(hub\\.jazz\\.net/git/[a-z0-9]+/[A-Za-z0-9_.\\-]+)(/[A-Za-z0-9_.\\-]+)*$"+ 'git)+ (make-vcs+ "git.apache.org"+ "^(git\\.apache\\.org/[a-z0-9_.\\-]+\\.git)(/[A-Za-z0-9_.\\-]+)*$"+ 'git)+ (make-vcs+ "git.openstack.org"+ "^(git\\.openstack\\.org/[A-Za-z0-9_.\\-]+/[A-Za-z0-9_.\\-]+)(\\.git)?(/[A-Za-z0-9_.\\-]+)*$"+ 'git)))+ (vcs (find (lambda (vcs) (string-prefix? (vcs-url-prefix vcs) module-path))+ known-vcs)))+ (if vcs+ (match:substring (string-match (vcs-root-regex vcs) module-path) 1)+ module-path)))++(define (go-module->guix-package-name module-path)+ "Converts a module's path to the canonical Guix format for Go packages."+ (string-downcase+ (string-append "go-"+ (string-replace-substring+ (string-replace-substring+ module-path+ "." "-")+ "/" "-"))))++(define-record-type <module-meta>+ (make-module-meta import-prefix vcs repo-root)+ module-meta?+ (import-prefix module-meta-import-prefix)+ ;; VCS field is a symbol+ (vcs module-meta-vcs)+ (repo-root module-meta-repo-root))++(define (fetch-module-meta-data module-path)+ "Fetches module meta-data from a module's landing page. This is+ necessary because goproxy servers don't currently provide all the+ information needed to build a package."+ ;; <meta name="go-import" content="import-prefix vcs repo-root">+ (define (meta-go-import->module-meta text)+ "Takes the content of the go-import meta tag as TEXT and gives back+ a MODULE-META record"+ (define (get-component s start)+ (let*+ ((start (string-skip s char-set:whitespace start))+ (end (string-index s char-set:whitespace start))+ (end (if end end (string-length s)))+ (result (substring s start end)))+ (values result end)))+ (let*-values (((import-prefix end) (get-component text 0))+ ((vcs end) (get-component text end))+ ((repo-root end) (get-component text end)))+ (make-module-meta import-prefix (string->symbol vcs) repo-root)))+ (define (html->meta-go-import port)+ "Read PORT with HTML content. Find the go-import meta tag and gives+ back its content as a string."+ (let* ((parsedhtml (html->sxml port))+ (extract-content (node-join+ (select-kids (node-typeof? 'html))+ (select-kids (node-typeof? 'head))+ (select-kids (node-typeof? 'meta))+ (select-kids (node-typeof? '@))+ (node-self+ (node-join+ (select-kids (node-typeof? 'name))+ (select-kids (node-equal? "go-import"))))+ (select-kids (node-typeof? 'content))+ (select-kids (lambda (_) #t))))+ (content (car (extract-content parsedhtml))))+ content))+ (let* ((port (build-download:http-fetch (string->uri (format #f "https://~a?go-get=1" module-path))))+ (meta-go-import (html->meta-go-import port))+ (module-metadata (meta-go-import->module-meta meta-go-import)))+ (close-port port)+ module-metadata))++(define (module-meta-data-repo-url meta-data goproxy-url)+ "Return the URL where the fetcher which will be used can download the source+control."+ (if (member (module-meta-vcs meta-data)'(fossil mod))+ goproxy-url+ (module-meta-repo-root meta-data)))++(define (vcs->origin vcs-type vcs-repo-url version file)+ "Generate the `origin' block of a package depending on what type of source+control system is being used."+ (case vcs-type+ ((git)+ `(origin+ (method git-fetch)+ (uri (git-reference+ (url ,vcs-repo-url)+ (commit (go-version->git-ref version))))+ (file-name (git-file-name name version))+ (sha256+ (base32+ ;; FIXME: get hash for git repo checkout+ "0000000000000000000000000000000000000000000000000000"))))+ ((hg)+ `(origin+ (method hg-fetch)+ (uri (hg-reference+ (url ,vcs-repo-url)+ (changeset ,version)))+ (file-name (format #f "~a-~a-checkout" name version))))+ ((svn)+ `(origin+ (method svn-fetch)+ (uri (svn-reference+ (url ,vcs-repo-url)+ (revision (string->number version))+ (recursive? #f)))+ (file-name (format #f "~a-~a-checkout" name version))+ (sha256+ (base32+ ,(guix-hash-url file)))))+ (else+ (raise-exception (format #f "unsupported vcs type: ~a" vcs-type)))))++(define* (go-module->guix-package module-path #:key (goproxy-url "https://proxy.golang.org"))+ (call-with-temporary-output-file+ (lambda (temp port)+ (let* ((latest-version (go-module-latest-version* goproxy-url module-path))+ (go.mod-path (fetch-go.mod goproxy-url module-path latest-version+ temp))+ (dependencies (map car (parse-go.mod temp)))+ (guix-name (go-module->guix-package-name module-path))+ (root-module-path (infer-module-root-repo module-path))+ ;; VCS type and URL are not included in goproxy information. For+ ;; this we need to fetch it from the official module page.+ (meta-data (fetch-module-meta-data root-module-path))+ (vcs-type (module-meta-vcs meta-data))+ (vcs-repo-url (module-meta-data-repo-url meta-data goproxy-url)))+ (values+ `(package+ (name ,guix-name)+ ;; Elide the "v" prefix Go uses+ (version ,(string-trim latest-version #\v))+ (source+ ,(vcs->origin vcs-type vcs-repo-url latest-version temp))+ (build-system go-build-system)+ (arguments+ '(#:import-path ,root-module-path))+ ,@(maybe-inputs (map go-module->guix-package-name dependencies))+ ;; TODO(katco): It would be nice to make an effort to fetch this+ ;; from known forges, e.g. GitHub+ (home-page ,(format #f "https://~a" root-module-path))+ (synopsis "A Go package")+ (description ,(format #f "~a is a Go package." guix-name))+ (license #f))+ dependencies)))))++(define go-module->guix-package* (memoize go-module->guix-package))++(define* (go-module-recursive-import package-name+ #:key (goproxy-url "https://proxy.golang.org"))+ (recursive-import+ package-name+ #:repo->guix-package (lambda* (name . _)+ (go-module->guix-package*+ name+ #:goproxy-url goproxy-url))+ #:guix-name go-module->guix-package-name))diff --git a/guix/scripts/import.scm b/guix/scripts/import.scmindex 0a3863f965..1d2b45d942 100644--- a/guix/scripts/import.scm+++ b/guix/scripts/import.scm@@ -77,7 +77,7 @@ rather than \\n." ;;; (define importers '("gnu" "nix" "pypi" "cpan" "hackage" "stackage" "elpa" "gem"- "cran" "crate" "texlive" "json" "opam"))+ "go" "cran" "crate" "texlive" "json" "opam")) (define (resolve-importer name) (let ((module (resolve-interfacediff --git a/guix/scripts/import/go.scm b/guix/scripts/import/go.scmnew file mode 100644index 0000000000..fde7555973--- /dev/null+++ b/guix/scripts/import/go.scm@@ -0,0 +1,118 @@+;;; GNU Guix --- Functional package management for GNU+;;; Copyright © 2020 Katherine Cox-Buday <cox.katherine.e@gmail.com>+;;;+;;; This file is part of GNU Guix.+;;;+;;; GNU Guix is free software; you can redistribute it and/or modify it+;;; under the terms of the GNU General Public License as published by+;;; the Free Software Foundation; either version 3 of the License, or (at+;;; your option) any later version.+;;;+;;; GNU Guix is distributed in the hope that it will be useful, but+;;; WITHOUT ANY WARRANTY; without even the implied warranty of+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the+;;; GNU General Public License for more details.+;;;+;;; You should have received a copy of the GNU General Public License+;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.++(define-module (guix scripts import go)+ #:use-module (guix ui)+ #:use-module (guix utils)+ #:use-module (guix scripts)+ #:use-module (guix import go)+ #:use-module (guix scripts import)+ #:use-module (srfi srfi-1)+ #:use-module (srfi srfi-11)+ #:use-module (srfi srfi-37)+ #:use-module (ice-9 match)+ #:use-module (ice-9 format)+ #:export (guix-import-go))+++;;;+;;; Command-line options.+;;;++(define %default-options+ '())++(define (show-help)+ (display (G_ "Usage: guix import go PACKAGE-PATH+Import and convert the Go module for PACKAGE-PATH.\n"))+ (display (G_ "+ -h, --help display this help and exit"))+ (display (G_ "+ -V, --version display version information and exit"))+ (display (G_ "+ -r, --recursive generate package expressions for all Go modules\+ that are not yet in Guix"))+ (display (G_ "+ -p, --goproxy=GOPROXY specify which goproxy server to use"))+ (newline)+ (show-bug-report-information))++(define %options+ ;; Specification of the command-line options.+ (cons* (option '(#\h "help") #f #f+ (lambda args+ (show-help)+ (exit 0)))+ (option '(#\V "version") #f #f+ (lambda args+ (show-version-and-exit "guix import go")))+ (option '(#\r "recursive") #f #f+ (lambda (opt name arg result)+ (alist-cons 'recursive #t result)))+ (option '(#\p "goproxy") #t #f+ (lambda (opt name arg result)+ (alist-cons 'goproxy+ (string->symbol arg)+ (alist-delete 'goproxy result))))+ %standard-import-options))+++;;;+;;; Entry point.+;;;++(define (guix-import-go . args)+ (define (parse-options)+ ;; Return the alist of option values.+ (args-fold* args %options+ (lambda (opt name arg result)+ (leave (G_ "~A: unrecognized option~%") name))+ (lambda (arg result)+ (alist-cons 'argument arg result))+ %default-options))++ (let* ((opts (parse-options))+ (args (filter-map (match-lambda+ (('argument . value)+ value)+ (_ #f))+ (reverse opts))))+ (match args+ ((module-name)+ (if (assoc-ref opts 'recursive)+ (map (match-lambda+ ((and ('package ('name name) . rest) pkg)+ `(define-public ,(string->symbol name)+ ,pkg))+ (_ #f))+ (go-module-recursive-import module-name+ #:goproxy-url+ (or (assoc-ref opts 'goproxy)+ "https://proxy.golang.org")))+ (let ((sexp (go-module->guix-package module-name+ #:goproxy-url+ (or (assoc-ref opts 'goproxy)+ "https://proxy.golang.org"))))+ (unless sexp+ (leave (G_ "failed to download meta-data for module '~a'~%")+ module-name))+ sexp)))+ (()+ (leave (G_ "too few arguments~%")))+ ((many ...)+ (leave (G_ "too many arguments~%"))))))diff --git a/guix/self.scm b/guix/self.scmindex 35fba1152d..ed5ee9ddea 100644--- a/guix/self.scm+++ b/guix/self.scm@@ -814,6 +814,9 @@ itself." (define guile-ssh (specification->package "guile-ssh")) + (define guile-lib+ (specification->package "guile-lib"))+ (define guile-git (specification->package "guile-git")) @@ -842,7 +845,7 @@ itself." (append-map transitive-package-dependencies (list guile-gcrypt gnutls guile-git guile-avahi guile-json guile-semver guile-ssh guile-sqlite3- guile-zlib guile-lzlib guile-zstd)))+ guile-lib guile-zlib guile-lzlib guile-zstd))) (define *core-modules* (scheme-node "guix-core"diff --git a/tests/import-go.scm b/tests/import-go.scmnew file mode 100644index 0000000000..ad4185684c--- /dev/null+++ b/tests/import-go.scm@@ -0,0 +1,144 @@+;;; GNU Guix --- Functional package management for GNU+;;; Copyright © 2021 François Joulaud <francois.joulaud@radiofrance.com>+;;;+;;; This file is part of GNU Guix.+;;;+;;; GNU Guix is free software; you can redistribute it and/or modify it+;;; under the terms of the GNU General Public License as published by+;;; the Free Software Foundation; either version 3 of the License, or (at+;;; your option) any later version.+;;;+;;; GNU Guix is distributed in the hope that it will be useful, but+;;; WITHOUT ANY WARRANTY; without even the implied warranty of+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the+;;; GNU General Public License for more details.+;;;+;;; You should have received a copy of the GNU General Public License+;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.++;;; Summary+;; Tests for guix/import/go.scm++(define-module (test-import-go)+ #:use-module (guix import go)+ #:use-module (guix base32)+ #:use-module (ice-9 iconv)+ #:use-module (ice-9 match)+ #:use-module (srfi srfi-64))++(define fixture-go-mod-simple+ "module my/thing+go 1.12+require other/thing v1.0.2+require new/thing/v2 v2.3.4+exclude old/thing v1.2.3+replace bad/thing v1.4.5 => good/thing v1.4.5+")++(define fixture-go-mod-with-block+ "module M++require (+ A v1+ B v1.0.0+ C v1.0.0+ D v1.2.3+ E dev+)++exclude D v1.2.3+")+++(define fixture-go-mod-complete+ "module M++go 1.13++replace github.com/myname/myproject/myapi => ./api++replace github.com/mymname/myproject/thissdk => ../sdk++replace launchpad.net/gocheck => github.com/go-check/check v0.0.0-20140225173054-eb6ee6f84d0a++require (+ github.com/user/project v1.1.11+ github.com/user/project/sub/directory v1.1.12+ bitbucket.org/user/project v1.11.20+ bitbucket.org/user/project/sub/directory v1.11.21+ launchpad.net/project v1.1.13+ launchpad.net/project/series v1.1.14+ launchpad.net/project/series/sub/directory v1.1.15+ launchpad.net/~user/project/branch v1.1.16+ launchpad.net/~user/project/branch/sub/directory v1.1.17+ hub.jazz.net/git/user/project v1.1.18+ hub.jazz.net/git/user/project/sub/directory v1.1.19+ k8s.io/kubernetes/subproject v1.1.101+ one.example.com/abitrary/repo v1.1.111+ two.example.com/abitrary/repo v0.0.2+ \"quoted.example.com/abitrary/repo\" v0.0.2+)++replace two.example.com/abitrary/repo => github.com/corp/arbitrary-repo v0.0.2++replace (+ golang.org/x/sys => golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a // pinned to release-branch.go1.13+ golang.org/x/tools => golang.org/x/tools v0.0.0-20190821162956-65e3620a7ae7 // pinned to release-branch.go1.13+)++")++(test-begin "import go")++(test-equal "go-path-escape"+ "github.com/!azure/!avere"+ ((@@ (guix import go) go-path-escape) "github.com/Azure/Avere"))++++;; We define a function for all similar tests with different go.mod files+(define (testing-parse-mod name expected input)+ (define (inf? p1 p2)+ (string<? (car p1) (car p2)))+ (let ((input-port (open-input-string input)))+ (test-equal name+ (sort expected inf?)+ (sort+ ( (@@ (guix import go) parse-go.mod-port)+ input-port)+ inf?))))++(testing-parse-mod "parse-go.mod-simple"+ '(("good/thing" . "v1.4.5")+ ("new/thing/v2" . "v2.3.4")+ ("other/thing" . "v1.0.2"))+ fixture-go-mod-simple)++(testing-parse-mod "parse-go.mod-with-block"+ '(("A" . "v1")+ ("B" . "v1.0.0")+ ("C" . "v1.0.0")+ ("D" . "v1.2.3")+ ("E" . "dev"))+ fixture-go-mod-with-block)++(testing-parse-mod "parse-go.mod-complete"+ '(("github.com/corp/arbitrary-repo" . "v0.0.2")+ ("quoted.example.com/abitrary/repo" . "v0.0.2")+ ("one.example.com/abitrary/repo" . "v1.1.111")+ ("hub.jazz.net/git/user/project/sub/directory" . "v1.1.19")+ ("hub.jazz.net/git/user/project" . "v1.1.18")+ ("launchpad.net/~user/project/branch/sub/directory" . "v1.1.17")+ ("launchpad.net/~user/project/branch" . "v1.1.16")+ ("launchpad.net/project/series/sub/directory" . "v1.1.15")+ ("launchpad.net/project/series" . "v1.1.14")+ ("launchpad.net/project" . "v1.1.13")+ ("bitbucket.org/user/project/sub/directory" . "v1.11.21")+ ("bitbucket.org/user/project" . "v1.11.20")+ ("k8s.io/kubernetes/subproject" . "v1.1.101")+ ("github.com/user/project/sub/directory" . "v1.1.12")+ ("github.com/user/project" . "v1.1.11")+ ("github.com/go-check/check" . "v0.0.0-20140225173054-eb6ee6f84d0a"))+ fixture-go-mod-complete)++(test-end "import go")-- 2.28.0
L
L
Ludovic Courtès wrote on 2 Mar 22:54 +0100
Re: bug#44178: Add a Go Module Importer
(name . JOULAUD François)(address . Francois.JOULAUD@radiofrance.com)
871rcxte52.fsf_-_@gnu.org
Hi,
JOULAUD François <Francois.JOULAUD@radiofrance.com> skribis:
Toggle quote (15 lines)> This patch add a `guix import go` command.>> It was tested with several big repositories and mostly works. Several> features are lacking (see TODO in source code) but we will do the> improvments step-by-step in future patches.>> * doc/guix.texi: doc about go importer and guile-lib dependency> * gnu/packages/package-management.scm: added guile-lib dependency> * guix/self.scm: add guile-lib dependency> * guix/build-system/go.scm: go-version->git-ref function> * guix/import/go.scm: Created Go importer> * guix/scripts/import/go.scm: Subcommand for Go importer> * guix/scripts/import.scm: Declare subcommand guix import go> * tests/import-go.scm: Tests for parse-go.mod procedure
Nitpick: please mention the sections (for documentation) or variableschanged (seehttps://guix.gnu.org/manual/en/html_node/Submitting-Patches.html).
Some comments below, mostly stylistic as I’m not familiar with theactual file formats etc. that the importer implements.
Toggle quote (4 lines)> +@item> +@uref{https://www.nongnu.org/guile-lib/doc/ref/htmlprag/, guile-lib} for> +the @code{crate} importer (@pxref{Invoking guix import}).
s/crate/go/s/guile-lib/Guile-Lib/
Toggle quote (7 lines)> + (re (string-concatenate> + (list> + "(v?[0-9]\\.[0-9]\\.[0-9])" ; "v" prefix can be omitted in version prefix> + "(-|-pre\\.0\\.|-0\\.)" ; separator> + "([0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9])-" ; timestamp> + "([0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f])"))) ; commit hash
You can use ‘string-append’ instead of (string-concatenate (list …)).Use [[:xdigit:]] instead of [0-9A-Fa-f] for clarity andlocale-independence.
Also, you can arrange to use ‘make-regexp’ so that the regexp iscompiled once for all, and then just ‘regexp-exec’:
(define %go-version-rx (make-regexp …))
(define (go-version->git-ref version) (… (regexp-exec %go-version-rx …) …))
It’s not critical though.
Toggle quote (8 lines)> + (define (replace-directive results line)> + "Extract replaced modules and new requirements from replace directive> + in LINE and add to RESULTS."> + ;; ReplaceSpec = ModulePath [ Version ] "=>" FilePath newline> + ;; | ModulePath [ Version ] "=>" ModulePath Version newline .> + (let* ((requirements (car results))> + (replaced (cdr results))
Please use ‘match’ instead of car/cdr (throughout):
https://guix.gnu.org/manual/en/html_node/Coding-Style.html
Toggle quote (6 lines)> + (re (string-concatenate> + '("([^[:blank:]]+)([[:blank:]]+([^[:blank:]]+))?"> + "[[:blank:]]+" "=>" "[[:blank:]]+"> + "([^[:blank:]]+)([[:blank:]]+([^[:blank:]]+))?")))> + (match (string-match re line))
As above, you should arrange to pre-compile the regexp.
Toggle quote (6 lines)> + (module-path (match:substring match 1))> + (version (match:substring match 3))> + (new-module-path (match:substring match 4))> + (new-version (match:substring match 6))> + (new-replaced (acons module-path version replaced))
s/acons/alist-cons/ for consistency with the rest of the code.
Toggle quote (6 lines)> + (re (string-concatenate> + '("^[[:blank:]]*"> + "([^[:blank:]]+)[[:blank:]]+([^[:blank:]]+)"> + "([[:blank:]]+//.*)?")))> + (match (string-match re line))
Same as above.
Toggle quote (10 lines)> + ;; TODO: handle module path with VCS qualifier as described in> + ;; https://golang.org/ref/mod#vcs-find and> + ;; https://golang.org/cmd/go/#hdr-Remote_import_paths> + (define-record-type <vcs>> + (make-vcs url-prefix root-regex type)> + vcs?> + (url-prefix vcs-url-prefix)> + (root-regex vcs-root-regex)> + (type vcs-type))
You could rename ‘make-vcs’ above to ‘%make-vcs’ and do:
(define (make-vcs prefix regexp type) (%make-vcs prefix (make-regex regexp) type))
so that again you can rely on pre-compiled regexps.
Toggle quote (25 lines)> + (let* ((known-vcs> + (list> + (make-vcs> + "github.com"> + "^(github\\.com/[A-Za-z0-9_.\\-]+/[A-Za-z0-9_.\\-]+)(/[A-Za-z0-9_.\\-]+)*$"> + 'git)> + (make-vcs> + "bitbucket.org"> + "^(bitbucket\\.org/([A-Za-z0-9_.\\-]+/[A-Za-z0-9_.\\-]+))(/[A-Za-z0-9_.\\-]+)*$"> + 'unknown)> + (make-vcs> + "hub.jazz.net/git/"> + "^(hub\\.jazz\\.net/git/[a-z0-9]+/[A-Za-z0-9_.\\-]+)(/[A-Za-z0-9_.\\-]+)*$"> + 'git)> + (make-vcs> + "git.apache.org"> + "^(git\\.apache\\.org/[a-z0-9_.\\-]+\\.git)(/[A-Za-z0-9_.\\-]+)*$"> + 'git)> + (make-vcs> + "git.openstack.org"> + "^(git\\.openstack\\.org/[A-Za-z0-9_.\\-]+/[A-Za-z0-9_.\\-]+)(\\.git)?(/[A-Za-z0-9_.\\-]+)*$"> + 'git)))> + (vcs (find (lambda (vcs) (string-prefix? (vcs-url-prefix vcs) module-path))> + known-vcs)))
Keep ‘known-vcs’ in a global variable so it doesn’t have to berecomputed every time.
Toggle quote (14 lines)> + `(package> + (name ,guix-name)> + ;; Elide the "v" prefix Go uses> + (version ,(string-trim latest-version #\v))> + (source> + ,(vcs->origin vcs-type vcs-repo-url latest-version temp))> + (build-system go-build-system)> + (arguments> + '(#:import-path ,root-module-path))> + ,@(maybe-inputs (map go-module->guix-package-name dependencies))> + ;; TODO(katco): It would be nice to make an effort to fetch this> + ;; from known forges, e.g. GitHub> + (home-page ,(format #f "https://~a" root-module-path))> + (synopsis "A Go package")
^‘guix lint’ wouldn’t like it. :-) Maybe "Write synopsis here" instead?
Toggle quote (3 lines)> + (description ,(format #f "~a is a Go package." guix-name))> + (license #f))
Is there no info about the license?
Toggle quote (18 lines)> +++ b/guix/self.scm> @@ -814,6 +814,9 @@ itself."> (define guile-ssh> (specification->package "guile-ssh"))> > + (define guile-lib> + (specification->package "guile-lib"))> +> (define guile-git> (specification->package "guile-git"))> > @@ -842,7 +845,7 @@ itself."> (append-map transitive-package-dependencies> (list guile-gcrypt gnutls guile-git guile-avahi> guile-json guile-semver guile-ssh guile-sqlite3> - guile-zlib guile-lzlib guile-zstd)))> + guile-lib guile-zlib guile-lzlib guile-zstd)))
New dependency; it’s a bit of a commitment, but hopefully Guile-Lib isstable enough and works on all the supported architectures.
Please add guix/scripts/import/go.scm to ‘po/guix/POTFILES.in’ so it canbe translated.
Toggle quote (2 lines)> +++ b/tests/import-go.scm
Looks nice! It should be called ‘tests/go.scm’ for consistency, with:
(test-begin "go") (test-end "go")
Would it be an option to also have an end-to-end test (checking theresulting ‘package’ sexp)? That’d be nice, but perhaps we can add itafterwards if you prefer.
Let’s see how much of the comments above you can address for a v4, andthen we can get that in and improve it from there if needed!
Thanks again,Ludo’.
M
M
Maxim Cournoyer wrote on 4 Mar 06:40 +0100
(name . JOULAUD François)(address . Francois.JOULAUD@radiofrance.com)
8735xbqxwr.fsf_-_@gmail.com
Hi François, Ludovic, et al!
Sorry for bumping in the review, but I have been experimenting with thisimporter, and it looks promising; thanks for everyone involved! I madea couple changes, mostly with regard to integrating support for thesynopsis, description and license field of the package, plus othercosmetic changes. I thought I should share it quickly so that it can beused as the basis for a v5, so here's the patch, attached.
I hope you don't mind!
I tested it with:
$ ./pre-inst-env guix environment guix
$ ./pre-inst-env guix import go -r github.com/dgraph-io/badger/v2
Toggle snippet (54 lines)[...]
(define-public go-github-com-dgraph-io-badger-v2 (package (name "go-github-com-dgraph-io-badger-v2") (version "2.2007.2") (source (origin (method git-fetch) (uri (git-reference (url "https://github.com/dgraph-io/badger.git") (commit (go-version->git-ref version)))) (file-name (git-file-name name version)) (sha256 (base32 "0000000000000000000000000000000000000000000000000000")))) (build-system go-build-system) (arguments '(#:import-path "github.com/dgraph-io/badger")) (inputs `(("go-gopkg-in-check-v1" ,go-gopkg-in-check-v1) ("go-golang-org-x-sys" ,go-golang-org-x-sys) ("go-golang-org-x-net" ,go-golang-org-x-net) ("go-github-com-stretchr-testify" ,go-github-com-stretchr-testify) ("go-github-com-spf13-cobra" ,go-github-com-spf13-cobra) ("go-github-com-spaolacci-murmur3" ,go-github-com-spaolacci-murmur3) ("go-github-com-pkg-errors" ,go-github-com-pkg-errors) ("go-github-com-kr-pretty" ,go-github-com-kr-pretty) ("go-github-com-golang-snappy" ,go-github-com-golang-snappy) ("go-github-com-golang-protobuf" ,go-github-com-golang-protobuf) ("go-github-com-dustin-go-humanize" ,go-github-com-dustin-go-humanize) ("go-github-com-dgryski-go-farm" ,go-github-com-dgryski-go-farm) ("go-github-com-dgraph-io-ristretto" ,go-github-com-dgraph-io-ristretto) ("go-github-com-cespare-xxhash" ,go-github-com-cespare-xxhash) ("go-github-com-datadog-zstd" ,go-github-com-datadog-zstd))) (home-page "https://github.com/dgraph-io/badger") (synopsis "BadgerDB") (description "Package badger implements an embeddable, simple and fast key-value database, written in pure Go. It is designed to be highly performant for both reads and writes simultaneously. Badger uses Multi-Version Concurrency Control (MVCC), and supports transactions. It runs transactions concurrently, with serializable snapshot isolation guarantees.") (license (license:asl2.0))))
Attached is the fixup commit which should apply cleanly on top of yourv3 patch on master, along a (now required) commit to use a temporaryfork of guile-lib:
From 16c07537375ab5d18ee76a5fdfb2b8ed7192b395 Mon Sep 17 00:00:00 2001From: Maxim Cournoyer <maxim.cournoyer@gmail.com>Date: Wed, 3 Mar 2021 16:20:22 -0500Subject: [PATCH] gnu: guile-lib: Update to a temporary fork.
This fork add support to enable stricter/more correct parsing of HTML inhtmlprag, which is used by the go importer.
* gnu/packages/guile-xyz.scm (guile-lib)[source]: Fetch from git.Remove snippet and modules field.[native-inputs]: Add autoconf, automake, gettext and texinfo.--- gnu/packages/guile-xyz.scm | 96 ++++++++++++++++++-------------------- 1 file changed, 46 insertions(+), 50 deletions(-)
Toggle diff (120 lines)diff --git a/gnu/packages/guile-xyz.scm b/gnu/packages/guile-xyz.scmindex ce5aad8ec7..c14193921b 100644--- a/gnu/packages/guile-xyz.scm+++ b/gnu/packages/guile-xyz.scm@@ -16,7 +16,7 @@ ;;; Copyright © 2017 Theodoros Foradis <theodoros@foradis.org> ;;; Copyright © 2017 Nikita <nikita@n0.is> ;;; Copyright © 2017, 2018 Tobias Geerinckx-Rice <me@tobias.gr>-;;; Copyright © 2018 Maxim Cournoyer <maxim.cournoyer@gmail.com>+;;; Copyright © 2018, 2021 Maxim Cournoyer <maxim.cournoyer@gmail.com> ;;; Copyright © 2018, 2019, 2020 Arun Isaac <arunisaac@systemreboot.net> ;;; Copyright © 2018 Pierre-Antoine Rouby <pierre-antoine.rouby@inria.fr> ;;; Copyright © 2018 Eric Bavier <bavier@member.fsf.org>@@ -2167,59 +2167,55 @@ library.") ("guile" ,guile-3.0))))) (define-public guile-lib- (package- (name "guile-lib")- (version "0.2.6.1")- (source (origin- (method url-fetch)- (uri (string-append "mirror://savannah/guile-lib/guile-lib-"- version ".tar.gz"))- (sha256- (base32- "0aizxdif5dpch9cvs8zz5g8ds5s4xhfnwza2il5ji7fv2h7ks7bd"))- (modules '((guix build utils)))- (snippet- '(begin- ;; Work around miscompilation on Guile 3.0.0 at -O2:- ;; <https://bugs.gnu.org/39251>.- (substitute* "src/md5.scm"- (("\\(define f-ash ash\\)")- "(define f-ash (@ (guile) ash))\n")- (("\\(define f-add \\+\\)")- "(define f-add (@ (guile) +))\n"))- #t))))- (build-system gnu-build-system)- (arguments- '(#:make-flags- '("GUILE_AUTO_COMPILE=0") ; to prevent guild errors- #:phases- (modify-phases %standard-phases- (add-before 'configure 'patch-module-dir- (lambda _- (substitute* "src/Makefile.in"- (("^moddir = ([[:graph:]]+)")- "moddir = $(datadir)/guile/site/@GUILE_EFFECTIVE_VERSION@\n")- (("^godir = ([[:graph:]]+)")- "godir = \-$(libdir)/guile/@GUILE_EFFECTIVE_VERSION@/site-ccache\n"))- #t)))))- (native-inputs- `(("guile" ,guile-3.0)- ("pkg-config" ,pkg-config)))- (inputs- `(("guile" ,guile-3.0)))- (home-page "https://www.nongnu.org/guile-lib/")- (synopsis "Collection of useful Guile Scheme modules")- (description- "Guile-Lib is intended as an accumulation place for pure-scheme Guile+ (let ((revision "1")+ (commit "c059f13e332347201eaa4a32ef27c53d064f2d17"))+ (package+ (name "guile-lib")+ (version (git-version "0.2.6.1" revision commit))+ (source (origin+ (method git-fetch)+ (uri (git-reference+ (url "https://notabug.org/apteryx/guile-lib/")+ (commit commit)))+ (file-name (git-file-name name version))+ (sha256+ (base32+ "1dl2f53p737n637n2805slci5i32s6cy0bq1j0xkmzd5piymg4f8"))))+ (build-system gnu-build-system)+ (arguments+ '(#:make-flags+ '("GUILE_AUTO_COMPILE=0") ;to prevent guild errors+ #:phases+ (modify-phases %standard-phases+ (add-before 'configure 'patch-module-dir+ (lambda _+ (substitute* "src/Makefile.in"+ (("^moddir = ([[:graph:]]+)")+ "moddir = $(datadir)/guile/site/@GUILE_EFFECTIVE_VERSION@\n")+ (("^godir = ([[:graph:]]+)")+ "godir = \+$(libdir)/guile/@GUILE_EFFECTIVE_VERSION@/site-ccache\n")))))))+ (native-inputs+ `(("autoconf" ,autoconf)+ ("automake" ,automake)+ ("gettext" ,gettext-minimal)+ ("guile" ,guile-3.0)+ ("pkg-config" ,pkg-config)+ ("texinfo" ,texinfo)))+ (inputs+ `(("guile" ,guile-3.0)))+ (home-page "https://www.nongnu.org/guile-lib/")+ (synopsis "Collection of useful Guile Scheme modules")+ (description+ "Guile-Lib is intended as an accumulation place for pure-scheme Guile modules, allowing for people to cooperate integrating their generic Guile modules into a coherent library. Think \"a down-scaled, limited-scope CPAN for Guile\".") - ;; The whole is under GPLv3+, but some modules are under laxer- ;; distribution terms such as LGPL and public domain. See `COPYING' for- ;; details.- (license license:gpl3+)))+ ;; The whole is under GPLv3+, but some modules are under laxer+ ;; distribution terms such as LGPL and public domain. See `COPYING' for+ ;; details.+ (license license:gpl3+)))) (define-public guile2.0-lib (package-- 2.30.1
From f3a6130577252e3d079a6209ec2e21bf5d8baf25 Mon Sep 17 00:00:00 2001From: Maxim Cournoyer <maxim.cournoyer@gmail.com>Date: Wed, 3 Mar 2021 16:45:11 -0500Subject: [PATCH] fixup! Create importer for Go modules
--- guix/build-system/go.scm | 34 ++-- guix/import/go.scm | 420 ++++++++++++++++++++++----------------- 2 files changed, 257 insertions(+), 197 deletions(-)
Toggle diff (626 lines)diff --git a/guix/build-system/go.scm b/guix/build-system/go.scmindex 594e0cb4f3..d07c703a6a 100644--- a/guix/build-system/go.scm+++ b/guix/build-system/go.scm@@ -34,30 +34,28 @@ go-version->git-ref)) (define (go-version->git-ref version)- "GO-VERSION->GIT-REF parse pseudo-versions and extract the commit- hash from it, defaulting to full VERSION if we don't recognise a- pseudo-version pattern."- ;; A module version like v1.2.3 is introduced by tagging a revision in- ;; the underlying source repository. Untagged revisions can be referred- ;; to using a "pseudo-version" like v0.0.0-yyyymmddhhmmss-abcdefabcdef,- ;; where the time is the commit time in UTC and the final suffix is the- ;; prefix of the commit hash.- ;; cf. https://golang.org/cmd/go/#hdr-Pseudo_versions+ "GO-VERSION->GIT-REF parse pseudo-versions and extract the commit hash from+it, defaulting to full VERSION if a pseudo-version pattern is not recognized."+ ;; A module version like v1.2.3 is introduced by tagging a revision in the+ ;; underlying source repository. Untagged revisions can be referred to+ ;; using a "pseudo-version" like v0.0.0-yyyymmddhhmmss-abcdefabcdef, where+ ;; the time is the commit time in UTC and the final suffix is the prefix of+ ;; the commit hash (see: https://golang.org/cmd/go/#hdr-Pseudo_versions). (let* ((version- ;; if a source code repository has a v2.0.0 or later tag for- ;; a file tree with no go.mod, the version is considered to be- ;; part of the v1 module's available versions and is given an- ;; +incompatible suffix- ;; https://golang.org/cmd/go/#hdr-Module_compatibility_and_semantic_versioning+ ;; If a source code repository has a v2.0.0 or later tag for a file+ ;; tree with no go.mod, the version is considered to be part of the+ ;; v1 module's available versions and is given an +incompatible+ ;; suffix+ ;; (see:https://golang.org/cmd/go/#hdr-Module_compatibility_and_semantic_versioning). (if (string-suffix? "+incompatible" version) (string-drop-right version 13) version)) (re (string-concatenate (list- "(v?[0-9]\\.[0-9]\\.[0-9])" ; "v" prefix can be omitted in version prefix- "(-|-pre\\.0\\.|-0\\.)" ; separator- "([0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9])-" ; timestamp- "([0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f])"))) ; commit hash+ "(v?[0-9]\\.[0-9]\\.[0-9])" ;"v" prefix can be omitted in version prefix+ "(-|-pre\\.0\\.|-0\\.)" ;separator+ "([0-9]{14})-" ;timestamp+ "([0-9A-Fa-f]{12})"))) ;commit hash (match (string-match re version))) (if match (match:substring match 4)diff --git a/guix/import/go.scm b/guix/import/go.scmindex fead355bd2..7bc97c5c92 100644--- a/guix/import/go.scm+++ b/guix/import/go.scm@@ -2,6 +2,7 @@ ;;; Copyright © 2020 Katherine Cox-Buday <cox.katherine.e@gmail.com> ;;; Copyright © 2020 Helio Machado <0x2b3bfa0+guix@googlemail.com> ;;; Copyright © 2021 François Joulaud <francois.joulaud@radiofrance.com>+;;; Copyright © 2021 Maxim Cournoyer <maxim.cournoyer@gmail.com> ;;; ;;; This file is part of GNU Guix. ;;;@@ -18,51 +19,37 @@ ;;; You should have received a copy of the GNU General Public License ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>. -;;; (guix import golang) wants to make easier to create Guix package-;;; declaration for Go modules.+;;; (guix import golang) attempts to make it easier to create Guix package+;;; declarations for Go modules. ;;;-;;; Modules in Go are "collection of related Go packages" which are-;;; "the unit of source code interchange and versioning".-;;; Modules are generally hosted in a repository.+;;; Modules in Go are a "collection of related Go packages" which are "the+;;; unit of source code interchange and versioning". Modules are generally+;;; hosted in a repository. ;;;-;;; At this point it should handle correctly modules which-;;; have only Go dependencies and are accessible from proxy.golang.org-;;; (or configured GOPROXY).+;;; At this point it should handle correctly modules which have only Go+;;; dependencies and are accessible from proxy.golang.org (or configured via+;;; GOPROXY). ;;; ;;; We want it to work more or less this way: ;;; - get latest version for the module from GOPROXY ;;; - infer VCS root repo from which we will check-out source by ;;; + recognising known patterns (like github.com)-;;; + or (TODO) recognising .vcs suffix-;;; + or parsing meta tag in html served at the URL+;;; + or recognizing .vcs suffix+;;; + or parsing meta tag in HTML served at the URL ;;; + or (TODO) if nothing else works by using zip file served by GOPROXY ;;; - get go.mod from GOPROXY (which is able to synthetize one if needed) ;;; - extract list of dependencies from this go.mod ;;;-;;; We translate Go module paths to a Guix package name under the+;;; The Go module paths are translated to a Guix package name under the ;;; assumption that there will be no collision. ;;; TODO list ;;; - get correct hash in vcs->origin ;;; - print partial result during recursive imports (need to catch ;;; exceptions)-;;; - infer repo from module path with VCS qualifier-;;; (e.g. site.example/my/path/to/repo.git/and/subdir/module)-;;; - don't print fetch messages to stdout-;;; - pre-fill synopsis, description and license (define-module (guix import go)- #:use-module (ice-9 match)- #:use-module (ice-9 rdelim)- #:use-module (ice-9 receive)- #:use-module (ice-9 regex) #:use-module (guix build-system go)- #:use-module (htmlprag)- #:use-module (sxml xpath)- #:use-module (srfi srfi-1)- #:use-module (srfi srfi-9)- #:use-module (srfi srfi-11)- #:use-module (json) #:use-module ((guix download) #:prefix download:) #:use-module (guix git) #:use-module (guix import utils)@@ -75,49 +62,134 @@ #:use-module (guix base32) #:use-module (guix memoization) #:use-module ((guix build download) #:prefix build-download:)+ #:use-module (htmlprag)+ #:use-module (ice-9 match)+ #:use-module (ice-9 rdelim)+ #:use-module (ice-9 receive)+ #:use-module (ice-9 regex)+ #:use-module (json)+ #:use-module (rnrs io ports)+ #:use-module (srfi srfi-1)+ #:use-module (srfi srfi-9)+ #:use-module (srfi srfi-11)+ #:use-module (srfi srfi-26)+ #:use-module (sxml xpath)+ #:use-module (web client)+ #:use-module (web response) #:use-module (web uri) #:export (go-module->guix-package- go-module-recursive-import- infer-module-root-repo))+ go-module-recursive-import)) +;;; Parameterize htmlprag to parse valid HTML more reliably.+(%strict-tokenizer? #t) (define (go-path-escape path)- "Escape a module path by replacing every uppercase letter with an exclamation-mark followed with its lowercase equivalent, as per the module Escaped Paths-specification. https://godoc.org/golang.org/x/mod/module#hdr-Escaped_Paths"+ "Escape a module path by replacing every uppercase letter with an+exclamation mark followed with its lowercase equivalent, as per the module+Escaped Paths specification (see:+https://godoc.org/golang.org/x/mod/module#hdr-Escaped_Paths)." (define (escape occurrence) (string-append "!" (string-downcase (match:substring occurrence)))) (regexp-substitute/global #f "[A-Z]" path 'pre escape 'post)) - (define (go-module-latest-version goproxy-url module-path)- "Fetches the version number of the latest version for MODULE-PATH from the+ "Fetch the version number of the latest version for MODULE-PATH from the given GOPROXY-URL server."- (assoc-ref- (json-fetch (format #f "~a/~a/@latest" goproxy-url- (go-path-escape module-path)))- "Version"))+ (assoc-ref (json-fetch (format #f "~a/~a/@latest" goproxy-url+ (go-path-escape module-path)))+ "Version"))++(define (go-package-licenses name)+ "Retrieve the list of licenses that apply to NAME, a Go package or module+name (e.g. \"github.com/golang/protobuf/proto\"). The data is scraped from+the https://pkg.go.dev/ web site."+ (let*-values (((url) (string-append "https://pkg.go.dev/" name+ "?tab=licenses"))+ ((response body) (http-get url))+ ;; Extract the text contained in a h2 child node of any+ ;; element marked with a "License" class attribute.+ ((select) (sxpath `(// (* (@ (equal? (class "License"))))+ h2 // *text*))))+ (and (eq? (response-code response) 200)+ (match (select (html->sxml body))+ (() #f) ;nothing selected+ (licenses licenses)))))++(define (go-package-description name)+ "Retrieve a short description for NAME, a Go package name,+e.g. \"google.golang.org/protobuf/proto\". The data is scraped from the+https://pkg.go.dev/ web site."+ (let*-values (((url) (string-append "https://pkg.go.dev/" name))+ ((response body) (http-get url))+ ;; Extract the text contained in a h2 child node of any+ ;; element marked with a "License" class attribute.+ ((select) (sxpath+ `(// (section+ (@ (equal? (class "Documentation-overview"))))+ (p 1)))))+ (and (eq? (response-code response) 200)+ (match (select (html->sxml body))+ (() #f) ;nothing selected+ (((p . strings))+ ;; The paragraph text is returned as a list of strings embedding+ ;; newline characters. Join them and strip the newline+ ;; characters.+ (string-delete #\newline (string-join strings)))))))++(define (go-package-synopsis module-name)+ "Retrieve a short synopsis for a Go module named MODULE-NAME,+e.g. \"google.golang.org/protobuf\". The data is scraped from+the https://pkg.go.dev/ web site."+ ;; Note: Only the *module* (rather than package) page has the README title+ ;; used as a synopsis on the https://pkg.go.dev web site.+ (let*-values (((url) (string-append "https://pkg.go.dev/" module-name))+ ((response body) (http-get url))+ ;; Extract the text contained in a h2 child node of any+ ;; element marked with a "License" class attribute.+ ((select) (sxpath+ `(// (div (@ (equal? (class "UnitReadme-content"))))+ // h3 *text*))))+ (and (eq? (response-code response) 200)+ (match (select (html->sxml body))+ (() #f) ;nothing selected+ ((title more ...) ;title is the first string of the list+ (string-trim-both title)))))) -(define go-module-latest-version* (memoize go-module-latest-version))+(define (list->licenses licenses)+ "Given a list of LICENSES mostly following the SPDX conventions, return the+corresponding Guix license or 'unknown-license!"+ (filter-map (lambda (license)+ (and (not (string-null? license))+ (not (any (cut string=? <> license)+ '("AND" "OR" "WITH")))+ ;; Adjust the license names scraped from+ ;; https://pkg.go.dev to an equivalent SPDX identifier,+ ;; if they differ (see: https://github.com/golang/pkgsite+ ;; /internal/licenses/licenses.go#L174).+ (or (spdx-string->license+ (match license+ ("BlueOak-1.0" "BlueOak-1.0.0")+ ("BSD-0-Clause" "0BSD")+ ("BSD-2-Clause" "BSD-2-Clause-FreeBSD")+ ("GPL2" "GPL-2.0")+ ("GPL3" "GPL-3.0")+ ("NIST" "NIST-PD")+ (_ license)))+ 'unknown-license!)))+ licenses)) -(define (fetch-go.mod goproxy-url module-path version file)- "Fetches go.mod from the given GOPROXY-URL server for the given MODULE-PATH-and VERSION."+(define (fetch-go.mod goproxy-url module-path version)+ "Fetch go.mod from the given GOPROXY-URL server for the given MODULE-PATH+and VERSION and return an input port." (let ((url (format #f "~a/~a/@v/~a.mod" goproxy-url (go-path-escape module-path) (go-path-escape version))))- (parameterize ((current-output-port (current-error-port)))- (build-download:url-fetch url- file- #:print-build-trace? #f))))+ (build-download:http-fetch (string->uri url)))) -(define (parse-go.mod go.mod-path)- (parse-go.mod-port (open-input-file go.mod-path)))--(define (parse-go.mod-port go.mod-port)- "PARSE-GO.MOD takes a filename in GO.MOD-PATH and extract a list of-requirements from it."+(define (parse-go.mod port)+ "Parse the go.mod file accessible via the input PORT, returning a list of+requirements." ;; We parse only a subset of https://golang.org/ref/mod#go-mod-file-grammar ;; which we think necessary for our use case. (define (toplevel results)@@ -147,6 +219,7 @@ requirements from it." (#t ;; unrecognised line, ignore silently (toplevel results)))))+ (define (in-require results) (let ((line (read-line))) (cond@@ -158,6 +231,7 @@ requirements from it." (toplevel results)) (#t (in-require (require-directive results line))))))+ (define (in-replace results) (let ((line (read-line))) (cond@@ -169,6 +243,7 @@ requirements from it." (toplevel results)) (#t (in-replace (replace-directive results line))))))+ (define (replace-directive results line) "Extract replaced modules and new requirements from replace directive in LINE and add to RESULTS."@@ -191,6 +266,7 @@ requirements from it." requirements (acons new-module-path new-version requirements)))) (cons new-requirements new-replaced)))+ (define (require-directive results line) "Extract requirement from LINE and add it to RESULTS." (let* ((requirements (car results))@@ -209,7 +285,8 @@ requirements from it." (module-path (string-trim-both module-path #\")) (version (match:substring match 2))) (cons (acons module-path version requirements) replaced)))- (with-input-from-port go.mod-port++ (with-input-from-port port (lambda () (let* ((results (toplevel '(() . ()))) (requirements (car results))@@ -221,120 +298,102 @@ requirements from it." requirements replaced))))) -(define (infer-module-root-repo module-path)- "Go modules can be defined at any level of a repository's tree, but querying-for the meta tag usually can only be done at the webpage at the root of the-repository. Therefore, it is sometimes necessary to try and derive a module's-root path from its path. For a set of well-known forges, the pattern of what-consists of a module's root page is known before hand."+(define (module-path->repository-root module-path)+ "Infer the repository root from a module path. Go modules can be+defined at any level of a repository tree, but querying for the meta tag+usually can only be done from the web page at the root of the repository,+hence the need to derive this information." ;; See the following URL for the official Go equivalent: ;; https://github.com/golang/go/blob/846dce9d05f19a1f53465e62a304dea21b99f910/src/cmd/go/internal/vcs/vcs.go#L1026-L1087- ;;- ;; TODO: handle module path with VCS qualifier as described in- ;; https://golang.org/ref/mod#vcs-find and- ;; https://golang.org/cmd/go/#hdr-Remote_import_paths+ (define-record-type <vcs> (make-vcs url-prefix root-regex type) vcs? (url-prefix vcs-url-prefix) (root-regex vcs-root-regex) (type vcs-type))- (let* ((known-vcs- (list- (make-vcs- "github.com"- "^(github\\.com/[A-Za-z0-9_.\\-]+/[A-Za-z0-9_.\\-]+)(/[A-Za-z0-9_.\\-]+)*$"- 'git)- (make-vcs- "bitbucket.org"- "^(bitbucket\\.org/([A-Za-z0-9_.\\-]+/[A-Za-z0-9_.\\-]+))(/[A-Za-z0-9_.\\-]+)*$"- 'unknown)- (make-vcs- "hub.jazz.net/git/"- "^(hub\\.jazz\\.net/git/[a-z0-9]+/[A-Za-z0-9_.\\-]+)(/[A-Za-z0-9_.\\-]+)*$"- 'git)- (make-vcs- "git.apache.org"- "^(git\\.apache\\.org/[a-z0-9_.\\-]+\\.git)(/[A-Za-z0-9_.\\-]+)*$"- 'git)- (make-vcs- "git.openstack.org"- "^(git\\.openstack\\.org/[A-Za-z0-9_.\\-]+/[A-Za-z0-9_.\\-]+)(\\.git)?(/[A-Za-z0-9_.\\-]+)*$"- 'git)))- (vcs (find (lambda (vcs) (string-prefix? (vcs-url-prefix vcs) module-path))- known-vcs)))- (if vcs- (match:substring (string-match (vcs-root-regex vcs) module-path) 1)- module-path)))++ (define known-vcs+ (list+ (make-vcs+ "github.com"+ "^(github\\.com/[A-Za-z0-9_.\\-]+/[A-Za-z0-9_.\\-]+)(/[A-Za-z0-9_.\\-]+)*$"+ 'git)+ (make-vcs+ "bitbucket.org"+ "^(bitbucket\\.org/([A-Za-z0-9_.\\-]+/[A-Za-z0-9_.\\-]+))(/[A-Za-z0-9_.\\-]+)*$"+ 'unknown)+ (make-vcs+ "hub.jazz.net/git/"+ "^(hub\\.jazz\\.net/git/[a-z0-9]+/[A-Za-z0-9_.\\-]+)(/[A-Za-z0-9_.\\-]+)*$"+ 'git)+ (make-vcs+ "git.apache.org"+ "^(git\\.apache\\.org/[a-z0-9_.\\-]+\\.git)(/[A-Za-z0-9_.\\-]+)*$"+ 'git)+ (make-vcs+ "git.openstack.org"+ "^(git\\.openstack\\.org/[A-Za-z0-9_.\\-]+/[A-Za-z0-9_.\\-]+)(\\.git)?\+(/[A-Za-z0-9_.\\-]+)*$"+ 'git)))++ ;; For reference, see: https://golang.org/ref/mod#vcs-find.+ (define vcs-qualifiers '(".bzr" ".fossil" ".git" ".hg" ".svn"))++ (define (vcs-qualified-module-path->root-repo-url module-path)+ (let* ((vcs-qualifiers-group (string-join vcs-qualifiers "|"))+ (pattern (format #f "^(.*(~a))(/|$)" vcs-qualifiers-group))+ (m (string-match pattern module-path)))+ (and=> m (cut match:substring <> 1))))++ (or (and=> (find (lambda (vcs)+ (string-prefix? (vcs-url-prefix vcs) module-path))+ known-vcs)+ (lambda (vcs)+ (match:substring (string-match (vcs-root-regex vcs)+ module-path) 1)))+ (vcs-qualified-module-path->root-repo-url module-path)+ module-path)) (define (go-module->guix-package-name module-path) "Converts a module's path to the canonical Guix format for Go packages."- (string-downcase- (string-append "go-"- (string-replace-substring- (string-replace-substring- module-path- "." "-")- "/" "-"))))+ (string-downcase (string-append "go-" (string-replace-substring+ (string-replace-substring+ module-path+ "." "-")+ "/" "-")))) (define-record-type <module-meta> (make-module-meta import-prefix vcs repo-root) module-meta? (import-prefix module-meta-import-prefix)- ;; VCS field is a symbol- (vcs module-meta-vcs)+ (vcs module-meta-vcs) ;a symbol (repo-root module-meta-repo-root)) (define (fetch-module-meta-data module-path)- "Fetches module meta-data from a module's landing page. This is- necessary because goproxy servers don't currently provide all the- information needed to build a package."+ "Retrieve the module meta-data from its landing page. This is necessary+because goproxy servers don't currently provide all the information needed to+build a package." ;; <meta name="go-import" content="import-prefix vcs repo-root">- (define (meta-go-import->module-meta text)- "Takes the content of the go-import meta tag as TEXT and gives back- a MODULE-META record"- (define (get-component s start)- (let*- ((start (string-skip s char-set:whitespace start))- (end (string-index s char-set:whitespace start))- (end (if end end (string-length s)))- (result (substring s start end)))- (values result end)))- (let*-values (((import-prefix end) (get-component text 0))- ((vcs end) (get-component text end))- ((repo-root end) (get-component text end)))- (make-module-meta import-prefix (string->symbol vcs) repo-root)))- (define (html->meta-go-import port)- "Read PORT with HTML content. Find the go-import meta tag and gives- back its content as a string."- (let* ((parsedhtml (html->sxml port))- (extract-content (node-join- (select-kids (node-typeof? 'html))- (select-kids (node-typeof? 'head))- (select-kids (node-typeof? 'meta))- (select-kids (node-typeof? '@))- (node-self- (node-join- (select-kids (node-typeof? 'name))- (select-kids (node-equal? "go-import"))))- (select-kids (node-typeof? 'content))- (select-kids (lambda (_) #t))))- (content (car (extract-content parsedhtml))))- content))- (let* ((port (build-download:http-fetch (string->uri (format #f "https://~a?go-get=1" module-path))))- (meta-go-import (html->meta-go-import port))- (module-metadata (meta-go-import->module-meta meta-go-import)))- (close-port port)- module-metadata))+ (let* ((port (build-download:http-fetch+ (string->uri (format #f "https://~a?go-get=1" module-path))))+ (select (sxpath `(// head (meta (@ (equal? (name "go-import"))))+ // content))))+ (match (select (call-with-port port html->sxml))+ (() #f) ;nothing selected+ (((content content-text))+ (match (string-split content-text #\space)+ ((root-path vcs repo-url)+ (make-module-meta root-path (string->symbol vcs) repo-url))))))) (define (module-meta-data-repo-url meta-data goproxy-url)- "Return the URL where the fetcher which will be used can download the source-control."- (if (member (module-meta-vcs meta-data)'(fossil mod))+ "Return the URL where the fetcher which will be used can download the+source."+ (if (member (module-meta-vcs meta-data) '(fossil mod)) goproxy-url (module-meta-repo-root meta-data))) -(define (vcs->origin vcs-type vcs-repo-url version file)+(define (vcs->origin vcs-type vcs-repo-url version) "Generate the `origin' block of a package depending on what type of source control system is being used." (case vcs-type@@ -347,61 +406,64 @@ control system is being used." (file-name (git-file-name name version)) (sha256 (base32- ;; FIXME: get hash for git repo checkout- "0000000000000000000000000000000000000000000000000000"))))+ ;; FIXME: populate hash for git repo checkout+ "0000000000000000000000000000000000000000000000000000")))) ((hg) `(origin (method hg-fetch) (uri (hg-reference (url ,vcs-repo-url) (changeset ,version)))- (file-name (format #f "~a-~a-checkout" name version))))+ (file-name (string-append name "-" version "-checkout"))+ (sha256+ (base32+ ;; FIXME: populate hash for hg repo checkout+ "0000000000000000000000000000000000000000000000000000")))) ((svn) `(origin (method svn-fetch) (uri (svn-reference (url ,vcs-repo-url)- (revision (string->number version))- (recursive? #f)))- (file-name (format #f "~a-~a-checkout" name version))+ (revision (string->number version))))+ (file-name (string-append name "-" version "-checkout")) (sha256 (base32- ,(guix-hash-url file)))))+ ;; FIXME: populate hash for svn repo checkout+ "0000000000000000000000000000000000000000000000000000")))) (else (raise-exception (format #f "unsupported vcs type: ~a" vcs-type))))) -(define* (go-module->guix-package module-path #:key (goproxy-url "https://proxy.golang.org"))- (call-with-temporary-output-file- (lambda (temp port)- (let* ((latest-version (go-module-latest-version* goproxy-url module-path))- (go.mod-path (fetch-go.mod goproxy-url module-path latest-version- temp))- (dependencies (map car (parse-go.mod temp)))- (guix-name (go-module->guix-package-name module-path))- (root-module-path (infer-module-root-repo module-path))- ;; VCS type and URL are not included in goproxy information. For- ;; this we need to fetch it from the official module page.- (meta-data (fetch-module-meta-data root-module-path))- (vcs-type (module-meta-vcs meta-data))- (vcs-repo-url (module-meta-data-repo-url meta-data goproxy-url)))- (values- `(package- (name ,guix-name)- ;; Elide the "v" prefix Go uses- (version ,(string-trim latest-version #\v))- (source- ,(vcs->origin vcs-type vcs-repo-url latest-version temp))- (build-system go-build-system)- (arguments- '(#:import-path ,root-module-path))- ,@(maybe-inputs (map go-module->guix-package-name dependencies))- ;; TODO(katco): It would be nice to make an effort to fetch this- ;; from known forges, e.g. GitHub- (home-page ,(format #f "https://~a" root-module-path))- (synopsis "A Go package")- (description ,(format #f "~a is a Go package." guix-name))- (license #f))- dependencies)))))+(define* (go-module->guix-package module-path #:key+ (goproxy-url "https://proxy.golang.org"))+ (let* ((latest-version (go-module-latest-version goproxy-url module-path))+ (port (fetch-go.mod goproxy-url module-path latest-version))+ (dependencies (map car (call-with-port port parse-go.mod)))+ (guix-name (go-module->guix-package-name module-path))+ (root-module-path (module-path->repository-root module-path))+ ;; The VCS type and URL are not included in goproxy information. For+ ;; this we need to fetch it from the official module page.+ (meta-data (fetch-module-meta-data root-module-path))+ (vcs-type (module-meta-vcs meta-data))+ (vcs-repo-url (module-meta-data-repo-url meta-data goproxy-url))+ (synopsis (go-package-synopsis root-module-path))+ (description (go-package-description module-path))+ (licenses (go-package-licenses module-path)))+ (values+ `(package+ (name ,guix-name)+ ;; Elide the "v" prefix Go uses+ (version ,(string-trim latest-version #\v))+ (source+ ,(vcs->origin vcs-type vcs-repo-url latest-version))+ (build-system go-build-system)+ (arguments+ '(#:import-path ,root-module-path))+ ,@(maybe-inputs (map go-module->guix-package-name dependencies))+ (home-page ,(format #f "https://~a" root-module-path))+ (synopsis ,synopsis)+ (description ,description)+ (license ,(and=> licenses list->licenses)))+ dependencies))) (define go-module->guix-package* (memoize go-module->guix-package)) -- 2.30.1
I hope I'm not making things more difficult for you!
Thank you for working on it! :-)
Maxim
J
J
JOULAUD François wrote on 4 Mar 15:14 +0100
(name . Maxim Cournoyer)(address . maxim.cournoyer@gmail.com)
20210304113606.jilxbgp72tmelw2j@fjo-extia-HPdeb.example.avalenn.eu
Hi,
On Thu, Mar 04, 2021 at 12:40:36AM -0500, Maxim Cournoyer wrote:
Toggle quote (5 lines)> I made a couple changes, mostly with regard to integrating support for the> synopsis, description and license field of the package, plus other> cosmetic changes. I thought I should share it quickly so that it can be> used as the basis for a v5, so here's the patch, attached.
First quick glance and the code look a lot better after your work.
I will rebase my work in progress on top of it and will provide a v5this week.
Thanks a lot.
François
M
M
Maxim Cournoyer wrote on 4 Mar 16:47 +0100
(name . JOULAUD François)(address . Francois.JOULAUD@radiofrance.com)
87r1kuq5si.fsf@gmail.com
Hi François,
JOULAUD François <Francois.JOULAUD@radiofrance.com> writes:
Toggle quote (17 lines)> Hi,>> On Thu, Mar 04, 2021 at 12:40:36AM -0500, Maxim Cournoyer wrote:>> I made a couple changes, mostly with regard to integrating support for the>> synopsis, description and license field of the package, plus other>> cosmetic changes. I thought I should share it quickly so that it can be>> used as the basis for a v5, so here's the patch, attached.>> First quick glance and the code look a lot better after your work.>> I will rebase my work in progress on top of it and will provide a v5> this week.>> Thanks a lot.>> François
Sounds good! Thanks to you!
Maxim
J
J
JOULAUD François wrote on 8 Mar 14:54 +0100
Re: bug#44178: Add a Go Module Importer
(name . Ludovic Courtès)(address . ludo@gnu.org)
20210308135025.vn32lypnivpsilcg@fjo-extia-HPdeb.example.avalenn.eu
Hello, Please find attached v5 version of the patch. Hopefully this is the last one. I took quite all changes from Maxim's proposal. Things I did not took are related to html parsing. I did not use of "%strict-tokenizer" because it needs a yet-to-be-packaged version of Guile-Libe and did not change the result on any of my tests. I did not take either the short-form sxpath expression for go-import meta parsing as it is buggy on my test cases. We can revisit those choices on future patches but for now I think I have a working version. Other changes are mainly responses to Ludovic review. On Tue, Mar 02, 2021 at 10:54:49PM +0100, Ludovic Courtès wrote:
Toggle quote (2 lines)> Nitpick: please mention the sections (for documentation) or variables > changed
I tried to do it. Don't hesitate to modify message if needed before commiting.
Toggle quote (1 lines)> (see https://guix.gnu.org/manual/en/html_node/Submitting-Patches.html).
I finally understood that the document refers to GNU Guidelines for Changelogs. Some examples specific to Guix would be nice for noobs like me.
Toggle quote (2 lines)> Some comments below, mostly stylistic as I’m not familiar with the > actual file formats etc. that the importer implements.
I am not yet completely familiar with it either. All languages now try to live in their own ecosystem with their own set of incompatible build and distribution tools. I am just beginning to grasp how Go fo it.
Toggle quote (2 lines)> s/crate/go/ > s/guile-lib/Guile-Lib/
done.
Toggle quote (3 lines)> You can use ‘string-append’ instead of (string-concatenate (list …)). > Use [[:xdigit:]] instead of [0-9A-Fa-f] for clarity and > locale-independence.
Thanks for the string-append tip.
Toggle quote (3 lines)> > Also, you can arrange to use ‘make-regexp’ so that the regexp is > compiled once for all, and then just ‘regexp-exec’:
I thought about it but was lazy. Thanks to your remark it is now done.
Toggle quote (1 lines)> Please use ‘match’ instead of car/cdr (throughout):
This one was more difficult than I thought. It lead me to create some specific record type, probably for the better.
Toggle quote (1 lines)> s/acons/alist-cons/ for consistency with the rest of the code.
I still must look at the difference between different type of alists. I trusted you and just applied the substitution.
Toggle quote (6 lines)> You could rename ‘make-vcs’ above to ‘%make-vcs’ and do: > > (define (make-vcs prefix regexp type) > (%make-vcs prefix (make-regex regexp) type)) > > so that again you can rely on pre-compiled regexps.
Thanks for the tip. I wonder when we use "%" prefix versus "*" suffix. I was under the impression that "%" prefix was more for global (possibly mutable) variables but you don't use it that way here.
Toggle quote (2 lines)> Keep ‘known-vcs’ in a global variable so it doesn’t have to be > recomputed every time.
known-vcs is now a top-level variable with precompiled regexs.
Toggle quote (3 lines)> ‘guix lint’ wouldn’t like it. :-) Maybe "Write synopsis here" instead? > > Is there no info about the license?
Maxim's patch parse pkg.go.dev for synopsis, license and description. It is not without flaws (Human review badly needed as it uses README for trying to extract synopsis) but still better than before.
Toggle quote (2 lines)> New dependency; it’s a bit of a commitment, but hopefully Guile-Lib is > stable enough and works on all the supported architectures.
It is a bit of commitment but we really needed a library for parsing HTML. It is only useful on "import go" as of now so nothing critical for using Guix itself if we can keep it optionnal.
Toggle quote (2 lines)> Please add guix/scripts/import/go.scm to ‘po/guix/POTFILES.in’ so it can > be translated.
Done.
Toggle quote (3 lines)> > +++ b/tests/import-go.scm > > Looks nice! It should be called ‘tests/go.scm’ for consistency, with:
I renamed it. I also put in it one test for guix/build-system/go.scm. I still am not satisfied with the overall look of this file which is really difficult to read, but at least we have some basic tests.
Toggle quote (3 lines)> Would it be an option to also have an end-to-end test (checking the > resulting ‘package’ sexp)? That’d be nice, but perhaps we can add it > afterwards if you prefer.
I added one end-to-end test loosely based on github.com/go-check/check example. For end-to-end tests I reused the "mock" syntax from guix/tests.scm by doing copy-paste because use-module of "(guix tests)" was really too slow for me. I don't know what's going on here (it seems to rebuild all of "gnu" scheme modules) but feel free to delete the copy and import "(guix tests)" if you prefer.
Toggle quote (2 lines)> Let’s see how much of the comments above you can address for a v4, and > then we can get that in and improve it from there if needed!
I hope all needed to get that in the tree is done now ;-) François
L
L
Ludovic Courtès wrote on 10 Mar 18:12 +0100
(name . JOULAUD François)(address . Francois.JOULAUD@radiofrance.com)
87tupj0w6m.fsf@gnu.org
Hi François, Katherine, & all!
I’m happy to say that it’s finally pushed, on behalf of Katherine andthe rest of you!
https://git.savannah.gnu.org/cgit/guix.git/commit/?id=02e2e093e858e8a0ca7bd66c1f1f6fd0a1705edb
I had to make a number of changes, among which (off the top of my head):
• Add files to Makefile.am. One can now run:
make check TESTS=tests/go.scm
See https://guix.gnu.org/manual/en/html_node/Running-the-Test-Suite.html.
• Update ‘specification->package’ in (guix self).
• Fix version handling in the generated sexp (thanks Maxim for helping out on IRC!).
• Fix the generated ‘license’ field.
• Fix minor issues reported by compiler warnings.
• Compute the hash of Git checkouts (done in a followup commit) since that’s part of the minimum one expects from importers.
• Recode (guix import go) as UTF-8 rather than Latin-1.
• Move commentary below ‘define-module’ form.
Let me know if I broke anything on the way or if anything’s unclear!
Now, you’ve already identified things that could be improved, so feelfree to send focused patches addressing specific issues.
Thanks everyone for the great team work! :-)
Ludo’.
Closed
?
Your comment

Commenting via the web interface is currently disabled.

To comment on this conversation send email to 44178@debbugs.gnu.org