Toggle diff (405 lines)
diff --git a/Makefile.am b/Makefile.am
index b5fb81f412..12446e6bb4 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -168,6 +168,7 @@ MODULES = \
guix/build-system/glib-or-gtk.scm \
guix/build-system/gnu.scm \
guix/build-system/go.scm \
+ guix/build-system/go-module.scm \
guix/build-system/guile.scm \
guix/build-system/haskell.scm \
guix/build-system/julia.scm \
@@ -227,6 +228,7 @@ MODULES = \
guix/build/minify-build-system.scm \
guix/build/font-build-system.scm \
guix/build/go-build-system.scm \
+ guix/build/go-module-build-system.scm \
guix/build/android-repo.scm \
guix/build/asdf-build-system.scm \
guix/build/bzr.scm \
diff --git a/guix/build-system/go-module.scm b/guix/build-system/go-module.scm
new file mode 100644
index 0000000000..a61f591431
--- /dev/null
+++ b/guix/build-system/go-module.scm
@@ -0,0 +1,267 @@
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2025 Jørgen Kvalsvik <j@lambda.is>
+;;;
+;;; 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 build-system go-module)
+ #:use-module (guix gexp)
+ #:use-module (guix monads)
+ #:use-module (guix packages)
+ #:use-module (guix store)
+ #:use-module (guix utils)
+ #:use-module (guix search-paths)
+ #:use-module (guix build-system)
+ #:use-module (guix build-system gnu)
+ #:use-module ((guix build-system go) #:prefix go-build:)
+ #:use-module (srfi srfi-1)
+ #:export (%go-module-build-system-modules
+ go-module-build
+ go-module-build-system))
+
+;;; Commentary:
+;;;
+;;; Build procedure for packages using the module aware Go build system.
+;;;
+;;; Code:
+
+(define %go-module-build-system-modules
+ ;; Build-side modules imported by default.
+ `((guix build go-module-build-system)
+ (guix build union)
+ ,@%default-gnu-imported-modules))
+
+(define (default-go)
+ ;; Lazily resolve the binding to avoid a circular dependency.
+ (let ((go (resolve-interface '(gnu packages golang))))
+ (module-ref go 'go)))
+
+(define (default-gccgo)
+ ;; Lazily resolve the binding to avoid a circular dependency.
+ (let ((gcc (resolve-interface '(gnu packages gcc))))
+ (module-ref gcc 'gccgo-12)))
+
+(define (default-zip)
+ "Return the 'zip' package. This is a lazy reference so that we don't
+depend on (gnu packages compression)."
+ (let ((distro (resolve-interface '(gnu packages compression))))
+ (module-ref distro 'zip)))
+
+(define* (lower name
+ #:key source inputs native-inputs outputs system target
+ (go (if (supported-package? (default-go))
+ (default-go)
+ (default-gccgo)))
+ (zip (default-zip))
+ #:allow-other-keys
+ #:rest arguments)
+ "Return a bag for NAME."
+ (define private-keywords
+ '(#:target #:inputs #:native-inputs #:go #:zip))
+
+ (bag
+ (name name)
+ (system system)
+ (target target)
+ (build-inputs `(,@(if source `(("source" ,source)) '())
+ ,@`(("go" ,go) ("zip" ,zip))
+ ,@native-inputs
+ ,@(if target '() inputs)
+ ,@(if target (standard-cross-packages target 'host) '())
+ ;; Keep the standard inputs of 'gnu-build-system'.
+ ,@(standard-packages)))
+ (host-inputs (if target inputs '()))
+ (target-inputs (if target (standard-cross-packages target 'target) '()))
+ (outputs outputs)
+ (build (if target go-cross-module-build go-module-build))
+ (arguments (strip-keyword-arguments private-keywords arguments))))
+
+(define* (go-module-build name inputs
+ #:key
+ source
+ (phases '%standard-phases)
+ (outputs '("out"))
+ (search-paths '())
+ (go-flags '())
+ (ld-flags '("-s" "-w"))
+ (tags '())
+ (build-targets '("./..."))
+ (test-targets '("./..."))
+ (install-targets '())
+ (test-flags '())
+ (module-path #f)
+ (trimpath? #t)
+ (cgo? #f)
+ (tests? #t)
+ (build-output-dir? #f)
+ (skip-build? #f)
+ (install-source? #t)
+ (install-cache? #t)
+ (parallel-build? #t)
+ (parallel-tests? #t)
+ (environment-variables '())
+ (system (%current-system))
+ (goarch #f)
+ (goos #f)
+ (guile #f)
+ (substitutable? #t)
+ (imported-modules %go-module-build-system-modules)
+ (modules '((guix build go-module-build-system)
+ (guix build utils))))
+
+ (define builder
+ (with-imported-modules
+ imported-modules
+ #~(begin
+ (use-modules #$@(sexp->gexp modules))
+ (go-module-build #:name #$name
+ #:source #+source
+ #:system #$system
+ #:go-flags '#$go-flags
+ #:ld-flags '#$ld-flags
+ #:tags '#$tags
+ #:build-targets '#$build-targets
+ #:test-targets '#$test-targets
+ #:install-targets '#$install-targets
+ #:test-flags '#$test-flags
+ #:module-path '#$module-path
+ #:trimpath? #$trimpath?
+ #:cgo? '#$cgo?
+ #:tests? #$tests?
+ #:build-output-dir? #$build-output-dir?
+ #:skip-build? #$skip-build?
+ #:install-source? #$install-source?
+ #:install-cache? #$install-cache?
+ #:parallel-build? #$parallel-build?
+ #:parallel-tests? #$parallel-tests?
+ #:environment-variables '#$environment-variables
+ #:goarch #$goarch
+ #:goos #$goos
+ #:phases #$phases
+ #:outputs #$(outputs->gexp outputs)
+ #:search-paths '#$(map
+ search-path-specification->sexp
+ search-paths)
+ #:inputs #$(input-tuples->gexp inputs)))))
+
+ (mlet %store-monad ((guile (package->derivation (or guile (default-guile))
+ system #:graft? #f)))
+ (gexp->derivation name builder
+ #:system system
+ #:guile-for-build guile)))
+
+(define* (go-cross-module-build name
+ #:key
+ source target
+ build-inputs target-inputs host-inputs
+ (phases '%standard-phases)
+ (outputs '("out"))
+ (search-paths '())
+ (native-search-paths '())
+ (go-flags '())
+ (ld-flags '("-s" "-w"))
+ (tags '())
+ (build-targets '("./..."))
+ (test-targets '())
+ (install-targets '())
+ (tests? #f) ; nothing can be done
+ (test-flags '())
+ (module-path #f)
+ (trimpath? #t)
+ (cgo? #f)
+ (build-output-dir? #f)
+ (skip-build? #f)
+ (install-source? #t)
+ (install-cache? #t)
+ (parallel-build? #t)
+ (parallel-tests? #f)
+ (environment-variables '())
+ (system (%current-system))
+ (goarch (if target (first (go-build:go-target target)) #f))
+ (goos (if target (last (go-build:go-target target)) #f))
+ (guile #f)
+ (imported-modules %go-module-build-system-modules)
+ (modules '((guix build go-module-build-system)
+ (guix build utils)))
+ (substitutable? #t))
+
+ (define builder
+ (with-imported-modules
+ imported-modules
+ #~(begin
+ (use-modules #$@(sexp->gexp modules))
+
+ (define %build-host-inputs
+ #+(input-tuples->gexp build-inputs))
+
+ (define %build-target-inputs
+ (append #$(input-tuples->gexp host-inputs)
+ #+(input-tuples->gexp target-inputs)))
+
+ (define %build-inputs
+ (append %build-host-inputs %build-target-inputs))
+
+ (go-module-build #:name #$name
+ #:source #+source
+ #:system #$system
+ #:go-flags '#$go-flags
+ #:ld-flags '#$ld-flags
+ #:tags '#$tags
+ #:build-targets '#$build-targets
+ #:test-targets '#$test-targets
+ #:install-targets '#$install-targets
+ #:test-flags '#$test-flags
+ #:module-path '#$module-path
+ #:trimpath? #$trimpath?
+ #:cgo? '#$cgo?
+ #:tests? #$tests?
+ #:build-output-dir? #$build-output-dir?
+ #:skip-build? #$skip-build?
+ #:install-source? #$install-source?
+ #:install-cache? #$install-cache?
+ #:parallel-build? #$parallel-build?
+ #:parallel-tests? #$parallel-tests?
+ #:environment-variables '#$environment-variables
+ #:target #$target
+ #:goarch #$goarch
+ #:goos #$goos
+ #:phases #$phases
+ #:outputs #$(outputs->gexp outputs)
+ #:make-dynamic-linker-cache? #f
+ #:search-paths '#$(map
+ search-path-specification->sexp
+ search-paths)
+ #:native-search-paths '#$(map
+ search-path-specification->sexp
+ native-search-paths)
+ #:native-inputs %build-host-inputs
+ #:inputs %build-inputs))))
+
+ (mlet %store-monad ((guile (package->derivation (or guile (default-guile))
+ system #:graft? #f)))
+ (gexp->derivation name builder
+ #:system system
+ #:target target
+ #:graft? #f
+ #:substitutable? substitutable?
+ #:guile-for-build guile)))
+
+(define go-module-build-system
+ (build-system
+ (name 'go-module)
+ (description "Go Module Build System")
+ (lower lower)))
+
+;;; go-module.scm ends here
diff --git a/guix/build-system/zig.scm b/guix/build-system/zig.scm
index 43d6ee977c..238964eb22 100644
--- a/guix/build-system/zig.scm
+++ b/guix/build-system/zig.scm
@@ -206,9 +206,7 @@ (define private-keywords
;; Keep the standard inputs of 'gnu-build-system'.
,@(standard-packages)))
(host-inputs (if target inputs '()))
- (target-inputs (if target
- (standard-cross-packages target 'target)
- '()))
+ (target-inputs (if target (standard-cross-packages target 'target) '()))
(outputs outputs)
(build (if target zig-cross-build zig-build))
(arguments (strip-keyword-arguments private-keywords arguments))))
diff --git a/guix/build/go-module-build-system.scm b/guix/build/go-module-build-system.scm
new file mode 100644
index 0000000000..adc36df356
--- /dev/null
+++ b/guix/build/go-module-build-system.scm
@@ -0,0 +1,473 @@
+(define-module (guix build go-module-build-system)
+ #:use-module ((guix build gnu-build-system) #:prefix gnu:)
+ #:use-module (guix build union)
+ #:use-module (guix build utils)
+ #:use-module (srfi srfi-71)
+ #:use-module (ice-9 rdelim)
+ #:use-module (ice-9 regex)
+ #:use-module (ice-9 match)
+ #:export (%standard-phases
+ go-module-build))
+
+;;; Commentary:
+;;;
+;;; Build procedure for packages using the module aware Go build
+;;; system. The go build system aggressively tries to fetch dependencies
+;;; or even compiler toolchains. While it may be possible to convince it to
+;;; not do that, we opt for not fighting it, and instead let it fetch
+;;; everything it wants to, served from the local filesystem in directories we
+;;; populate.
+;;;
+;;; The GOPROXY protocol [1] permits using file:// urls. From the manual:
+;;;
+;;; A module proxy is an HTTP server that can respond to GET requests for
+;;; paths specified below. The requests have no query parameters, and no
+;;; specific headers are required, so even a site serving from a fixed file
+;;; system (including a file:// URL) can be a module proxy.
+;;;
+;;; Go dependencies tend to be rigidly specified to very specific versions,
+;;; with hashes, which the go build tooling will figure out. This does not
+;;; work too well with guix' model, where we want to specify dependencies more
+;;; fludily (e.g. with input substitutions). Go modules also tend to specify
+;;; (minimum) toolchains which is not strictly necessary from a language
+;;; feature perspective, which breaks builds with older compilers.
+;;;
+;;; To address these problems, we always write a fresh go.mod file based on
+;;; the build-inputs. There is no guarantee that there even is a go.mod file
+;;; in the source, especially for older projects. Go build uses this file to
+;;; "download" from our just-assembled goproxy, which makes it happy. This
+;;; also clears any toolchain directive which makes the build accept the go
+;;; compiler through build-inputs. We populate the goproxy with just-in-time
+;;; built zips, version, and info files. This is a separate phase so that
+;;; additional build steps can be added between building the proxy and running go
+;;; build.
+;;;
+;;; The build system is compatible with go-build-system, in the sense that
+;;; go-build-system can be used as build-inputs, and vice versa, because they
+;;; both use the same $out/source/.
+;;;
+;;; We re-used compiled packages. The Go build system creates a
+;;; content-addressable build cache, which we install into build output, and
+;;; use to seed downstream builds. Go programs are (mostly) statically
+;;; linked, so this is roughly equivalent of installing lib.a. Note that this
+;;; only works when the build-input is built with go-module-build-system.
+;;;
+;;; [1] https://go.dev/ref/mod#goproxy-protocol
+;;;
+;;; Code:
+
+(define (find-single-file dir regex)
+ "Find the file in DIR matching the REGEX, and fail unless there is
+exactly one match."
+ (let ((files (find-files dir regex #:directories? #f)))
+ (unless (eq? 1 (length files))
+ (error "Expected exactly one file matching pattern, found:" files))
+ (car files)))
+
+(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 (call-with-append-file path f)
+ "call-with-output-file, but appends to the file if it exists rather
+than truncating it"
+ (let ((file (open-file path "a")))
+ (f file)
+ (close file)))
+
+(define (set-cache-action-epoch f)
+ "Set go build cache action entry timestamp to 0
+
+The go build cache action entries (xxxx-a) record a timestamp, which
+would break reproducibility of the build cache. Set it to all-zeros."
+ ;; The file has 5 columns, tand the timestamp is the rightmost one
+ ;; <version> <action-id> <output-id> <size> <timestamp>
+ ;;
+ ;; The time