git-download.scm 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. ;;; GNU Guix --- Functional package management for GNU
  2. ;;; Copyright © 2014-2021, 2023 Ludovic Courtès <ludo@gnu.org>
  3. ;;; Copyright © 2017 Mathieu Lirzin <mthl@gnu.org>
  4. ;;; Copyright © 2017 Christopher Baines <mail@cbaines.net>
  5. ;;; Copyright © 2020 Jakub Kądziołka <kuba@kadziolka.net>
  6. ;;; Copyright © 2023 Simon Tournier <zimon.toutoune@gmail.com>
  7. ;;;
  8. ;;; This file is part of GNU Guix.
  9. ;;;
  10. ;;; GNU Guix is free software; you can redistribute it and/or modify it
  11. ;;; under the terms of the GNU General Public License as published by
  12. ;;; the Free Software Foundation; either version 3 of the License, or (at
  13. ;;; your option) any later version.
  14. ;;;
  15. ;;; GNU Guix is distributed in the hope that it will be useful, but
  16. ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
  17. ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  18. ;;; GNU General Public License for more details.
  19. ;;;
  20. ;;; You should have received a copy of the GNU General Public License
  21. ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
  22. (define-module (guix git-download)
  23. #:use-module (guix gexp)
  24. #:use-module (guix store)
  25. #:use-module (guix monads)
  26. #:use-module (guix records)
  27. #:use-module (guix packages)
  28. #:use-module (guix modules)
  29. #:autoload (guix build-system gnu) (standard-packages)
  30. #:autoload (git bindings) (libgit2-init!)
  31. #:autoload (git repository) (repository-open
  32. repository-close!
  33. repository-discover
  34. repository-head
  35. repository-working-directory)
  36. #:autoload (git submodule) (repository-submodules
  37. submodule-lookup
  38. submodule-path)
  39. #:autoload (git commit) (commit-lookup commit-tree)
  40. #:autoload (git reference) (reference-target)
  41. #:autoload (git tree) (tree-list)
  42. #:use-module (ice-9 match)
  43. #:use-module (ice-9 vlist)
  44. #:use-module (srfi srfi-1)
  45. #:use-module (srfi srfi-34)
  46. #:use-module (srfi srfi-35)
  47. #:export (git-reference
  48. git-reference?
  49. git-reference-url
  50. git-reference-commit
  51. git-reference-recursive?
  52. git-fetch
  53. git-version
  54. git-file-name
  55. git-predicate))
  56. ;;; Commentary:
  57. ;;;
  58. ;;; An <origin> method that fetches a specific commit from a Git repository.
  59. ;;; The repository URL and commit hash are specified with a <git-reference>
  60. ;;; object.
  61. ;;;
  62. ;;; Code:
  63. (define-record-type* <git-reference>
  64. git-reference make-git-reference
  65. git-reference?
  66. (url git-reference-url)
  67. (commit git-reference-commit)
  68. (recursive? git-reference-recursive? ; whether to recurse into sub-modules
  69. (default #f)))
  70. (define (git-package)
  71. "Return the default Git package."
  72. (let ((distro (resolve-interface '(gnu packages version-control))))
  73. (module-ref distro 'git-minimal)))
  74. (define* (git-fetch ref hash-algo hash
  75. #:optional name
  76. #:key (system (%current-system)) (guile (default-guile))
  77. (git (git-package)))
  78. "Return a fixed-output derivation that fetches REF, a <git-reference>
  79. object. The output is expected to have recursive hash HASH of type
  80. HASH-ALGO (a symbol). Use NAME as the file name, or a generic name if #f."
  81. (define inputs
  82. `(("git" ,git)
  83. ;; When doing 'git clone --recursive', we need sed, grep, etc. to be
  84. ;; available so that 'git submodule' works.
  85. ,@(if (git-reference-recursive? ref)
  86. (standard-packages)
  87. ;; The 'swh-download' procedure requires tar and gzip.
  88. `(("gzip" ,(module-ref (resolve-interface '(gnu packages compression))
  89. 'gzip))
  90. ("tar" ,(module-ref (resolve-interface '(gnu packages base))
  91. 'tar))))))
  92. (define guile-json
  93. (module-ref (resolve-interface '(gnu packages guile)) 'guile-json-4))
  94. (define guile-lzlib
  95. (module-ref (resolve-interface '(gnu packages guile)) 'guile-lzlib))
  96. (define gnutls
  97. (module-ref (resolve-interface '(gnu packages tls)) 'guile-gnutls))
  98. (define glibc-locales
  99. ;; Note: pick the '-final' variant to avoid circular dependency on
  100. ;; i586-gnu, where 'glibc-utf8-locales' indirectly depends on Git.
  101. (module-ref (resolve-interface '(gnu packages commencement))
  102. 'glibc-utf8-locales-final))
  103. (define modules
  104. (delete '(guix config)
  105. (source-module-closure '((guix build git)
  106. (guix build utils)
  107. (guix build download-nar)
  108. (guix swh)))))
  109. (define build
  110. (with-imported-modules modules
  111. (with-extensions (list guile-json gnutls ;for (guix swh)
  112. guile-lzlib)
  113. #~(begin
  114. (use-modules (guix build git)
  115. (guix build utils)
  116. (guix build download-nar)
  117. (guix swh)
  118. (ice-9 match))
  119. (define recursive?
  120. (call-with-input-string (getenv "git recursive?") read))
  121. ;; Let Guile interpret file names as UTF-8, otherwise
  122. ;; 'delete-file-recursively' might fail to delete all of
  123. ;; '.git'--see <https://issues.guix.gnu.org/54893>.
  124. (setenv "GUIX_LOCPATH"
  125. #+(file-append glibc-locales "/lib/locale"))
  126. (setlocale LC_ALL "en_US.utf8")
  127. ;; The 'git submodule' commands expects Coreutils, sed,
  128. ;; grep, etc. to be in $PATH.
  129. (set-path-environment-variable "PATH" '("bin")
  130. (match '#+inputs
  131. (((names dirs outputs ...) ...)
  132. dirs)))
  133. (setvbuf (current-output-port) 'line)
  134. (setvbuf (current-error-port) 'line)
  135. (or (git-fetch (getenv "git url") (getenv "git commit")
  136. #$output
  137. #:recursive? recursive?
  138. #:git-command "git")
  139. (download-nar #$output)
  140. ;; As a last resort, attempt to download from Software Heritage.
  141. ;; Disable X.509 certificate verification to avoid depending
  142. ;; on nss-certs--we're authenticating the checkout anyway.
  143. ;; XXX: Currently recursive checkouts are not supported.
  144. (and (not recursive?)
  145. (parameterize ((%verify-swh-certificate? #f))
  146. (format (current-error-port)
  147. "Trying to download from Software Heritage...~%")
  148. (swh-download (getenv "git url") (getenv "git commit")
  149. #$output)
  150. (when (file-exists?
  151. (string-append #$output "/.gitattributes"))
  152. ;; Perform CR/LF conversion and other changes
  153. ;; specificied by '.gitattributes'.
  154. (invoke "git" "-C" #$output "init")
  155. (invoke "git" "-C" #$output "config" "--local"
  156. "user.email" "you@example.org")
  157. (invoke "git" "-C" #$output "config" "--local"
  158. "user.name" "Your Name")
  159. (invoke "git" "-C" #$output "add" ".")
  160. (invoke "git" "-C" #$output "commit" "-am" "init")
  161. (invoke "git" "-C" #$output "read-tree" "--empty")
  162. (invoke "git" "-C" #$output "reset" "--hard")
  163. (delete-file-recursively
  164. (string-append #$output "/.git"))))))))))
  165. (mlet %store-monad ((guile (package->derivation guile system)))
  166. (gexp->derivation (or name "git-checkout") build
  167. ;; Use environment variables and a fixed script name so
  168. ;; there's only one script in store for all the
  169. ;; downloads.
  170. #:script-name "git-download"
  171. #:env-vars
  172. `(("git url" . ,(git-reference-url ref))
  173. ("git commit" . ,(git-reference-commit ref))
  174. ("git recursive?" . ,(object->string
  175. (git-reference-recursive? ref))))
  176. #:leaked-env-vars '("http_proxy" "https_proxy"
  177. "LC_ALL" "LC_MESSAGES" "LANG"
  178. "COLUMNS")
  179. #:system system
  180. #:local-build? #t ;don't offload repo cloning
  181. #:hash-algo hash-algo
  182. #:hash hash
  183. #:recursive? #t
  184. #:guile-for-build guile)))
  185. (define (git-version version revision commit)
  186. "Return the version string for packages using git-download."
  187. ;; git-version is almost exclusively executed while modules are being loaded.
  188. ;; This makes any errors hide their backtrace. Avoid the mysterious error
  189. ;; "Value out of range 0 to N: 7" when the commit ID is too short, which
  190. ;; can happen, for example, when the user swapped the revision and commit
  191. ;; arguments by mistake.
  192. (when (< (string-length commit) 7)
  193. (raise
  194. (condition
  195. (&message (message "git-version: commit ID unexpectedly short")))))
  196. (string-append version "-" revision "." (string-take commit 7)))
  197. (define (git-file-name name version)
  198. "Return the file-name for packages using git-download."
  199. (string-append name "-" version "-checkout"))
  200. ;;;
  201. ;;; 'git-predicate'.
  202. ;;;
  203. (define* (git-file-list directory #:optional prefix #:key (recursive? #t))
  204. "Return the list of files checked in in the Git repository at DIRECTORY.
  205. The result is similar to that of the 'git ls-files' command, except that it
  206. also includes directories, not just regular files.
  207. When RECURSIVE? is true, also list files in submodules, similar to the 'git
  208. ls-files --recurse-submodules' command. This is enabled by default.
  209. The returned file names are relative to DIRECTORY, which is not necessarily
  210. the root of the checkout. If a PREFIX is provided, it is prepended to each
  211. file name."
  212. (let* (;; 'repository-working-directory' always returns a trailing "/",
  213. ;; so add one here to ease the comparisons below.
  214. (directory (string-append (canonicalize-path directory) "/"))
  215. (dot-git (repository-discover directory))
  216. (repository (repository-open dot-git))
  217. (workdir (repository-working-directory repository))
  218. (head (repository-head repository))
  219. (oid (reference-target head))
  220. (commit (commit-lookup repository oid))
  221. (tree (commit-tree commit))
  222. (files (tree-list tree))
  223. (submodules (if recursive?
  224. (map (lambda (name)
  225. (submodule-path
  226. (submodule-lookup repository name)))
  227. (repository-submodules repository))
  228. '()))
  229. (relative (and (not (string=? workdir directory))
  230. (string-drop directory (string-length workdir))))
  231. (included? (lambda (path)
  232. (or (not relative)
  233. (string-prefix? relative path))))
  234. (make-relative (lambda (path)
  235. (if relative
  236. (string-drop path (string-length relative))
  237. path)))
  238. (add-prefix (lambda (path)
  239. (if prefix
  240. (string-append prefix "/" path)
  241. path)))
  242. (rectify (compose add-prefix make-relative)))
  243. (repository-close! repository)
  244. (append
  245. (if (or relative prefix)
  246. (filter-map (lambda (file)
  247. (and (included? file)
  248. (rectify file)))
  249. files)
  250. files)
  251. (append-map (lambda (submodule)
  252. (if (included? submodule)
  253. (git-file-list
  254. (string-append workdir submodule)
  255. (rectify submodule))
  256. '()))
  257. submodules))))
  258. (define* (git-predicate directory #:key (recursive? #t))
  259. "Return a predicate that returns true if a file is part of the Git checkout
  260. living at DIRECTORY. If DIRECTORY does not lie within a Git checkout, and
  261. upon Git errors, return #f instead of a predicate.
  262. When RECURSIVE? is true, the predicate also returns true if a file is part of
  263. any Git submodule under DIRECTORY. This is enabled by default.
  264. The returned predicate takes two arguments FILE and STAT where FILE is an
  265. absolute file name and STAT is the result of 'lstat'."
  266. (libgit2-init!)
  267. (catch 'git-error
  268. (lambda ()
  269. (let* ((files (git-file-list directory #:recursive? recursive?))
  270. (inodes (fold (lambda (file result)
  271. (let* ((file (string-append directory "/" file))
  272. (stat (false-if-exception (lstat file))))
  273. ;; Ignore FILE if it has been deleted.
  274. (if stat
  275. (vhash-consv (stat:ino stat) (stat:dev stat)
  276. result)
  277. result)))
  278. vlist-null
  279. files)))
  280. (lambda (file stat)
  281. ;; Comparing file names is always tricky business so we rely on inode
  282. ;; numbers instead.
  283. (match (vhash-assv (stat:ino stat) inodes)
  284. ((_ . dev) (= dev (stat:dev stat)))
  285. (#f #f)))))
  286. (const #f)))
  287. ;;; git-download.scm ends here