Subject: [PATCH 1/4] guix: Add downloader for go modules from GOPROXY

From: Jørgen Kvalsvik <[email-masked]>
Date: Thu, 15 May 2025 10:16:32 +0200

Add a new downloader which implements, approximately, the download step of go
get $module. This is a convenient way of downloading zips with go modules by
just specifying the version and import path, as an alternative to git clone,
or awkward https:// fetches. This is particularly useful for sources that are
processed before release (like autotools generated files in tarballs) or
generated modules.

* guix/go-mod-download.scm: New file.
* Makefile.am (MODULES): Add it.

Change-Id: Ibb3b3ee70833fd0ea0c64278c95b8cb96a0be639
---
 Makefile.am              |   1 +
 guix/go-mod-download.scm | 126 +++++++++++++++++++++++++++++++++++++++
 2 files changed, 127 insertions(+)
 create mode 100644 guix/go-mod-download.scm

diff --git a/Makefile.am b/Makefile.am
index ec5220333e..b5fb81f412 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -102,6 +102,7 @@ MODULES =					\
   guix/android-repo-download.scm		\
   guix/bzr-download.scm            		\
   guix/git-download.scm				\
+  guix/go-mod-download.scm			\
   guix/hg-download.scm				\
   guix/hash.scm					\
   guix/swh.scm					\
diff --git a/guix/go-mod-download.scm b/guix/go-mod-download.scm
new file mode 100644
index 0000000000..7024362318
--- /dev/null
+++ b/guix/go-mod-download.scm
@@ -0,0 +1,126 @@
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2025 Jørgen Kvalsvik <[email-masked]>
+;;;
+;;; 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 go-mod-download)
+  #:use-module (guix gexp)
+  #:use-module (guix store)
+  #:use-module (guix packages)
+  #:use-module (guix monads)
+  #:use-module (guix records)
+  #:use-module (guix modules)
+  #:use-module (guix download)
+  #:use-module (ice-9 string-fun)
+  #:use-module (ice-9 regex)
+  #:autoload (guix build-system gnu) (standard-packages)
+  #:export (go-mod-reference
+            go-mod-reference?
+            go-mod-reference-path
+            go-mod-reference-version
+            go-mod-fetch))
+
+;;; Commentary:
+;;;
+;;; An <origin> method that fetches a go module [1] from a GOPROXY.  A go
+;;; module is usually identified as a vcs (usually git) repository,
+;;; e.g. github.com/calmh/du or golang.org/x/net.
+;;;
+;;; This is mostly a regular http(s) fetch some custom url building. Unless
+;;; goproxy is specified, it fetches from the default goproxy
+;;; https://proxy.golang.org.  This is mostly just a convenience -- the same
+;;; code could be fetched directly, but sometimes libraries are only
+;;; practically available through a goproxy. Such a module would be
+;;; https://pkg.go.dev/buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go
+;;;
+;;; [1] https://go.dev/ref/mod
+;;;
+;;; Code:
+
+(define-record-type* <go-mod-reference>
+  go-mod-reference make-go-mod-reference
+  go-mod-reference?
+  (path go-mod-reference-path)
+  (version go-mod-reference-version)
+  (goproxy go-mod-reference-goproxy (default "https://proxy.golang.org")))
+
+(define (default-unzip)
+  "Return the 'unzip' package.  This is a lazy reference so that we don't
+depend on (gnu packages compression)."
+  (module-ref (resolve-interface '(gnu packages compression)) 'unzip))
+
+;; Fetch a go module e.g. golang.org/x/net from a goproxy.
+(define* (go-mod-fetch ref hash-algo hash
+                       #:optional name
+                       #:key (system (%current-system))
+                       (guile (default-guile))
+                       (unzip (default-unzip)))
+  (define inputs
+    `(("unzip" ,unzip)
+      ,@(standard-packages)))
+
+  (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 (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 modules
+    (source-module-closure '((guix build utils))))
+
+  (define (build mod.zip)
+    (with-imported-modules modules
+      #~(begin
+          (use-modules (guix build utils))
+          (let* ((pkg-path (getenv "go-mod path"))
+                 (pkg-version (getenv "go-mod version"))
+                 (pkg-root (string-append pkg-path "@v" pkg-version))
+                 (go.mod (string-append pkg-root "/go.mod")))
+
+            (invoke (string-append #+unzip "/bin/unzip") "-q" #$mod.zip)
+            ;; The sources in the zip are in the subdir
+            ;; $path@v$version/, but we want our sources at root.
+            (copy-recursively pkg-root #$output)))))
+
+    (define path-as-store-name
+      (string-append
+       (string-replace-substring (go-mod-reference-path ref) "/" "-")
+       "-" (go-mod-reference-version ref)))
+
+    (define url/zip
+      (format #f "~a/~a/@v/v~a.zip"
+              (go-mod-reference-goproxy ref)
+              (go-path-escape (go-mod-reference-path ref))
+              (go-mod-reference-version ref)))
+
+    (mlet* %store-monad ((guile-for-build (package->derivation guile system))
+                         (mod (url-fetch url/zip hash-algo hash
+                                         (or name (string-append path-as-store-name ".zip"))
+                                         #:system system
+                                         #:guile guile)))
+      (gexp->derivation (or name path-as-store-name) (build mod)
+                        #:script-name "go-mod-fetch"
+                        #:env-vars
+                        `(("go-mod path" . ,(go-mod-reference-path ref))
+                          ("go-mod version" . ,(go-mod-reference-version ref)))
+                        #:leaked-env-vars '("http_proxy" "https_proxy"
+                                            "LC_ALL" "LC_MESSAGES" "LANG"
+                                            "COLUMNS")
+                        #:system system
+                        #:local-build? #t)))
-- 
2.39.5