Subject: [PATCH 1/2] build: Add the Go build system.

From: Leo Famulari <[email-masked]>
Date: Sun, 24 Sep 2017 16:40:52 -0400

* guix/build-system/go.scm,
guix/build/go-build-system.scm: New files.
* Makefile.am (MODULES): Add new files.
* doc/guix.texi (Build Systems): Document the go-build-system.
---
 Makefile.am                    |   2 +
 doc/guix.texi                  |  18 ++++
 guix/build-system/go.scm       | 132 ++++++++++++++++++++++++++
 guix/build/go-build-system.scm | 207 +++++++++++++++++++++++++++++++++++++++++
 4 files changed, 359 insertions(+)
 create mode 100644 guix/build-system/go.scm
 create mode 100644 guix/build/go-build-system.scm

diff --git a/Makefile.am b/Makefile.am
index e35bdac30..1da8e3fb5 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -79,6 +79,7 @@ MODULES =					\
   guix/build-system/dub.scm			\
   guix/build-system/emacs.scm			\
   guix/build-system/font.scm			\
+  guix/build-system/go.scm			\
   guix/build-system/meson.scm			\
   guix/build-system/minify.scm			\
   guix/build-system/asdf.scm			\
@@ -110,6 +111,7 @@ MODULES =					\
   guix/build/meson-build-system.scm		\
   guix/build/minify-build-system.scm		\
   guix/build/font-build-system.scm		\
+  guix/build/go-build-system.scm		\
   guix/build/asdf-build-system.scm		\
   guix/build/git.scm				\
   guix/build/hg.scm				\
diff --git a/doc/guix.texi b/doc/guix.texi
index 0462a6419..894cd329b 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -3566,6 +3566,24 @@ debugging information''), which roughly means that code is compiled with
 @code{-O2 -g}, as is the case for Autoconf-based packages by default.
 @end defvr
 
+@defvr {Scheme Variable} go-build-system
+This variable is exported by @code{(guix build-system go)}.  It
+implements a build procedure for Go packages using the standard
+@url{https://golang.org/cmd/go/#hdr-Compile_packages_and_dependencies,
+Go build mechanisms}.
+
+The user is expected to provide a value for the key @code{#:import-path}
+and, in some cases, @code{#:unpack-path}.  The
+@url{https://golang.org/doc/code.html#ImportPaths, import path}
+corresponds to the filesystem path expected by the package's build
+scripts and any referring packages, and provides a unique way to
+refer to a Go package.  It is typically based on a combination of the
+package source code's remote URI and filesystem hierarchy structure.  In
+some cases, you will need to unpack the package's source code to a
+different directory structure than the one indicated by the import path,
+and @code{#:unpack-path} should be used in such cases.
+@end defvr
+
 @defvr {Scheme Variable} glib-or-gtk-build-system
 This variable is exported by @code{(guix build-system glib-or-gtk)}.  It
 is intended for use with packages making use of GLib or GTK+.
diff --git a/guix/build-system/go.scm b/guix/build-system/go.scm
new file mode 100644
index 000000000..43599df6f
--- /dev/null
+++ b/guix/build-system/go.scm
@@ -0,0 +1,132 @@
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2016 Petter <[email-masked]>
+;;; Copyright © 2017 Leo Famulari <[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 build-system go)
+  #:use-module (guix utils)
+  #:use-module (guix derivations)
+  #:use-module (guix search-paths)
+  #:use-module (guix build-system)
+  #:use-module (guix build-system gnu)
+  #:use-module (guix packages)
+  #:use-module (ice-9 match)
+  #:export (%go-build-system-modules
+            go-build
+            go-build-system))
+
+;; Commentary:
+;;
+;; Standard build procedure for packages using the Go build system.  It is
+;; implemented as an extension of 'gnu-build-system'.
+;;
+;; Code:
+
+(define %go-build-system-modules
+  ;; Build-side modules imported and used by default.
+  `((guix build go-build-system)
+    ,@%gnu-build-system-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* (lower name
+                #:key source inputs native-inputs outputs system target
+                (go (default-go))
+                #:allow-other-keys
+                #:rest arguments)
+  "Return a bag for NAME."
+  (define private-keywords
+    '(#:source #:target #:go #:inputs #:native-inputs))
+
+  (and (not target)                               ;XXX: no cross-compilation
+       (bag
+         (name name)
+         (system system)
+         (host-inputs `(,@(if source
+                              `(("source" ,source))
+                              '())
+                        ,@inputs
+
+                        ;; Keep the standard inputs of 'gnu-build-system'.
+                        ,@(standard-packages)))
+         (build-inputs `(("go" ,go)
+                         ,@native-inputs))
+         (outputs outputs)
+         (build go-build)
+         (arguments (strip-keyword-arguments private-keywords arguments)))))
+
+(define* (go-build store name inputs
+                   #:key
+                   (phases '(@ (guix build go-build-system)
+                               %standard-phases))
+                   (outputs '("out"))
+                   (search-paths '())
+                   (import-path "")
+                   (unpack-path "")
+                   (tests? #t)
+                   (system (%current-system))
+                   (guile #f)
+                   (imported-modules %go-build-system-modules)
+                   (modules '((guix build go-build-system)
+                              (guix build utils))))
+  (define builder
+   `(begin
+      (use-modules ,@modules)
+      (go-build #:name ,name
+                #:source ,(match (assoc-ref inputs "source")
+                                 (((? derivation? source))
+                                  (derivation->output-path source))
+                                 ((source)
+                                  source)
+                                 (source
+                                  source))
+                #:system ,system
+                #:phases ,phases
+                #:outputs %outputs
+                #:search-paths ',(map search-path-specification->sexp
+                                      search-paths)
+                #:import-path ,import-path
+                #:unpack-path ,unpack-path
+                #:tests? ,tests?
+                #:inputs %build-inputs)))
+
+  (define guile-for-build
+    (match guile
+      ((? package?)
+       (package-derivation store guile system #:graft? #f))
+      (#f                                         ; the default
+       (let* ((distro (resolve-interface '(gnu packages commencement)))
+              (guile  (module-ref distro 'guile-final)))
+         (package-derivation store guile system
+                             #:graft? #f)))))
+
+  (build-expression->derivation store name builder
+                                #:inputs inputs
+                                #:system system
+                                #:modules imported-modules
+                                #:outputs outputs
+                                #:guile-for-build guile-for-build))
+
+(define go-build-system
+  (build-system
+    (name 'go)
+    (description
+     "Build system for Go programs")
+    (lower lower)))
diff --git a/guix/build/go-build-system.scm b/guix/build/go-build-system.scm
new file mode 100644
index 000000000..c2a488023
--- /dev/null
+++ b/guix/build/go-build-system.scm
@@ -0,0 +1,207 @@
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2016 Petter <[email-masked]>
+;;; Copyright © 2017 Leo Famulari <[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 build go-build-system)
+  #:use-module ((guix build gnu-build-system) #:prefix gnu:)
+  #:use-module (guix build utils)
+  #:use-module (ice-9 match)
+  #:use-module (srfi srfi-1)
+  #:export (%standard-phases
+            go-build))
+
+;; Commentary:
+;;
+;; Build procedures for Go packages.  This is the builder-side code.
+;;
+;; Software written in Go is either a 'package' (i.e. library) or 'command'
+;; (i.e. executable).  Both types can be built with either the `go build` or `go
+;; install` commands.  However, `go build` discards the result of the build
+;; process for Go libraries, so we use `go install`, which preserves the
+;; results. [0]
+
+;; Go software is developed and built within a particular filesystem hierarchy
+;; structure called a 'workspace' [1].  This workspace is found by Go
+;; via the GOPATH environment variable.  Typically, all Go source code
+;; and compiled objects are kept in a single workspace, but it is
+;; possible for GOPATH to contain a list of directories, and that is
+;; what we do in this go-build-system. [2]
+;;
+;; Go software, whether a package or a command, is uniquely named using
+;; an 'import path'.  The import path is based on the URL of the
+;; software's source.  Since most source code is provided over the
+;; internet, the import path is typically a combination of the remote
+;; URL and the source repository's filesystem structure. For example,
+;; the Go port of the common `du` command is hosted on github.com, at
+;; <https://github.com/calmh/du>.  Thus, the import path is
+;; <github.com/calmh/du>. [3]
+;;
+;; It may be possible to programatically guess a package's import path
+;; based on the source URL, but we don't try that in this revision of
+;; the go-build-system.
+;;
+;; Modules of modular Go libraries are named uniquely with their
+;; filesystem paths.  For example, the supplemental but "standardized"
+;; libraries developed by the Go upstream developers are available at
+;; <https://golang.org/x/{net,text,crypto, et cetera}>.  The Go IPv4
+;; library's import path is <golang.org/x/net/ipv4>.  The source of
+;; such modular libraries must be unpacked at the top-level of the
+;; filesystem structure of the library.  So the IPv4 library should be
+;; unpacked to <golang.org/x/net>.  This is handled in the
+;; go-build-system with the optional #:unpack-path key.
+;;
+;; In general, Go software is built using a standardized build mechanism
+;; that does not require any build scripts like Makefiles.  This means
+;; that all modules of modular libraries cannot be built with a single
+;; command.  Each module must be built individually.  This complicates
+;; certain cases, and these issues are currently resolved by creating a
+;; filesystem union of the required modules of such libraries.  I think
+;; this could be improved in future revisions of the go-build-system.
+;;
+;; [0] `go build`:
+;; https://golang.org/cmd/go/#hdr-Compile_packages_and_dependencies
+;; `go install`:
+;; https://golang.org/cmd/go/#hdr-Compile_and_install_packages_and_dependencies
+;; [1] https://golang.org/doc/code.html#Workspaces
+;; bin/
+;;     hello                          # command executable
+;;     outyet                         # command executable
+;; pkg/
+;;     linux_amd64/
+;;         github.com/golang/example/
+;;             stringutil.a           # package object
+;; src/
+;;     github.com/golang/example/
+;;         .git/                      # Git repository metadata
+;; 	   hello/
+;; 	       hello.go               # command source
+;; 	   outyet/
+;; 	        main.go               # command source
+;; 	        main_test.go          # test source
+;; 	   stringutil/
+;; 	       reverse.go             # package source
+;; 	       reverse_test.go        # test source
+;;         golang.org/x/image/
+;;             .git/                  # Git repository metadata
+;; 	       bmp/
+;; 	           reader.go          # package source
+;; 	           writer.go          # package source
+;;     ... (many more repositories and packages omitted) ...
+;;
+;; [2] https://golang.org/doc/code.html#GOPATH
+;; [3] https://golang.org/doc/code.html#ImportPaths
+;;
+;; Code:
+
+(define* (unpack #:key source import-path unpack-path #:allow-other-keys)
+  "Unpack SOURCE in the UNPACK-PATH, or the IMPORT-PATH is the UNPACK-PATH is
+unset.  When SOURCE is a directory, copy it instead of unpacking."
+  (if (string-null? import-path)
+      ((display "WARNING: The Go import path is unset.\n")))
+  (if (string-null? unpack-path)
+      (set! unpack-path import-path))
+  (mkdir "src")
+  (let ((dest (string-append "src/" unpack-path)))
+    (mkdir-p dest)
+    (if (file-is-directory? source)
+      (begin
+        (copy-recursively source dest #:keep-mtime? #t)
+        #t)
+      (if (string-suffix? ".zip" source)
+        (zero? (system* "unzip" "-d" dest source))
+        (zero? (system* "tar" "-C" dest "-xvf" source))))))
+
+(define* (install-source #:key outputs #:allow-other-keys)
+  "Install the source code to the output directory."
+  (let* ((out (assoc-ref outputs "out"))
+         (source "src")
+         (dest (string-append out "/" source)))
+    (copy-recursively source dest #:keep-mtime? #t)
+    #t))
+
+(define (golang-package? name)
+  (string-prefix? "golang-" name))
+
+(define (golang-inputs inputs)
+  "Return the alist of INPUTS that are Go software."
+  ;; XXX This should not check the file name of the store item. Instead we
+  ;; should pass, from the host side, the list of inputs that are packages using
+  ;; the go-build-system.
+  (alist-delete "source"
+    (filter (match-lambda
+              ((label . directory)
+               (golang-package? ((compose package-name->name+version
+                                          strip-store-file-name)
+                                 directory)))
+              (_ #f))
+            inputs)))
+
+(define* (setup-environment #:key inputs outputs #:allow-other-keys)
+  "Export the variables GOPATH and GOBIN, which are based on INPUTS and OUTPUTS,
+respectively."
+  (let ((out (assoc-ref outputs "out")))
+    ;; GOPATH is where Go looks for the source code of the build's dependencies.
+    (set-path-environment-variable "GOPATH"
+                                   ;; XXX Matching "." hints that we could do
+                                   ;; something simpler here...
+                                   (list ".")
+                                   (match (golang-inputs inputs)
+                                     (((_ . dir) ...)
+                                      dir)))
+
+    ;; Add the source code of the package being built to GOPATH.
+    (if (getenv "GOPATH")
+      (setenv "GOPATH" (string-append (getcwd) ":" (getenv "GOPATH")))
+      (setenv "GOPATH" (getcwd)))
+    ;; Where to install compiled executable files ('commands' in Go parlance').
+    (setenv "GOBIN" out)
+    #t))
+
+(define* (build #:key import-path #:allow-other-keys)
+  "Build the package named by IMPORT-PATH."
+  (zero? (system* "go" "install" "-v" import-path)))
+
+(define* (check #:key tests? import-path #:allow-other-keys)
+  "Run the tests for the package named by IMPORT-PATH."
+  (if tests?
+    (zero? (system* "go" "test" import-path))))
+
+(define* (install #:key outputs #:allow-other-keys)
+  "Install the compiled libraries. `go install` installs these files to
+$GOPATH/pkg, so we have to copy them into the output direcotry manually.
+Compiled executable files should have already been installed to the store based
+on $GOBIN in the build phase."
+  (when (file-exists? "pkg")
+    (copy-recursively "pkg" (string-append (assoc-ref outputs "out") "/pkg")))
+  #t)
+
+(define %standard-phases
+  (modify-phases gnu:%standard-phases
+    (delete 'configure)
+    (delete 'patch-generated-file-shebangs)
+    (replace 'unpack unpack)
+    (add-after 'unpack 'install-source install-source)
+    (add-before 'build 'setup-environment setup-environment)
+    (replace 'build build)
+    (replace 'check check)
+    (replace 'install install)))
+
+(define* (go-build #:key inputs (phases %standard-phases)
+                      #:allow-other-keys #:rest args)
+  "Build the given Go package, applying all of PHASES in order."
+  (apply gnu:gnu-build #:inputs inputs #:phases phases args))
-- 
2.14.1