antioxidant-workspaces.scm 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. ;;; Antioxidant --- Building Rust without cargo
  2. ;;; Copyright © 2022 Maxime Devos <maximedevos@telenet.be>
  3. ;;;
  4. ;;; This file is part of Antioxidant.
  5. ;;;
  6. ;;; Antioxidant is free software; you can redistribute it and/or modify it
  7. ;;; under the terms of the GNU General Public License as published by
  8. ;;; the Free Software Foundation; either version 3 of the License, or (at
  9. ;;; your option) any later version.
  10. ;;;
  11. ;;; Antioxidant is distributed in the hope that it will be useful, but
  12. ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
  13. ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. ;;; GNU General Public License for more details.
  15. ;;;
  16. ;;; You should have received a copy of the GNU General Public License
  17. ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
  18. (define-module (antioxidant-workspaces)
  19. #:use-module (antioxidant)
  20. #:use-module (guix build gnu-build-system)
  21. #:use-module (guix build syscalls)
  22. #:use-module (guix build utils)
  23. #:use-module (srfi srfi-1)
  24. #:use-module (srfi srfi-26)
  25. #:use-module (topological-sort)
  26. #:use-module (ice-9 match)
  27. #:export (*members*
  28. expand-glob
  29. load-members
  30. member-dependencies
  31. build-workspace-members
  32. extra-member-phases
  33. %antioxidant-member-main-phases
  34. %antioxidant-member-extra-phases
  35. %antioxidant-workspaces-phases
  36. call-with-member
  37. with-member
  38. capture-per-member-environment))
  39. ;;;
  40. ;;; Workspaces are built recursively -- each member is built in turn and
  41. ;;; installed, in topological ordering to satisfy dependencies.
  42. ;;;
  43. ;;; To resolve dev-dependencies cycles within a workspace,
  44. ;;; first each workspace member is built without tests,
  45. ;;; then the tests are built and run for each member.
  46. ;;;
  47. ;; An association list of 'member name' (directory name) → their manifest.
  48. ;; If the workspace itself is a Rust crate, it is included as ".".
  49. (define *members* (make-parameter #false))
  50. (define (capture-per-member-environment)
  51. "Return an association list of environment variable names / values
  52. considered specific to the current workspace member. These should
  53. be restored when the member is entered again and unset when the
  54. member is exited."
  55. (define %precious
  56. ;; Required for libsequoia
  57. '("OUT_DIR" "CARGO_MANIFEST_DIR"
  58. ;; No known cases where preserving them is required, but seems
  59. ;; potentially needed in theoru.
  60. "CARGO_PKG_VERSION_MAJOR" "CARGO_PKG_VESION_MINOR"
  61. "CARGO_PKG_VERSION_PATCH" "CARGO_PKG_VERSION_PRE" "CARGO_PKG_VERSION"
  62. "CARGO_PKG_AUTHORS" "CARGO_PKG_NAME" "CARGO_PKG_DESCRIPTION"
  63. "CARGO_PKG_HOMEPAGE" "CARGO_PKG_REPOSITORY" "CARGO_PKG_LICENSE"
  64. "CARGO_PKG_LICENSE_FILE"))
  65. (define (preserve? v)
  66. (define =-index (string-index v #\=))
  67. (define key (substring v 0 =-index))
  68. (define value (substring v (+ 1 =-index)))
  69. (and (member key %precious)
  70. (cons key value)))
  71. (filter-map preserve? (environ)))
  72. (define (call-with-member-environment-excursion to-restore thunk)
  73. ;; TODO: would be nice if 'environ' was a parameter object
  74. (dynamic-wind
  75. (lambda ()
  76. (for-each (lambda (v)
  77. (setenv (car v) (cdr v)))
  78. to-restore))
  79. thunk
  80. (lambda ()
  81. (for-each (lambda (v)
  82. (unsetenv (car v)))
  83. (capture-per-member-environment)))))
  84. ;; Head: environment variables alist.
  85. ;; Tail: argument for 'call-with-reset-state'
  86. (define *member-state* (make-hash-table))
  87. (define (call-with-member member thunk)
  88. "Call THUNK in an environment where the state arguments for MEMBER are set.
  89. If they are not defined yet, they are set to their initial values. After
  90. a normal return, the new values of the state arguments are saved for the
  91. next call to call-with-member."
  92. (define old-state (hash-ref *member-state* member #false))
  93. (define-values (old-environment optional-arguments)
  94. (if old-state
  95. (values (car old-state) (list (cdr old-state)))
  96. (values '() '())))
  97. (with-directory-excursion member
  98. (apply call-with-reset-state
  99. (lambda ()
  100. (call-with-member-environment-excursion
  101. old-environment
  102. (lambda ()
  103. (thunk)
  104. ;; Save the new state, as the member might need to be re-entered
  105. ;; later.
  106. (hash-set! *member-state* member
  107. (cons (capture-per-member-environment) (capture-state))))))
  108. optional-arguments)))
  109. (define-syntax-rule (with-member member code code* ...)
  110. (call-with-member member (lambda () code code* ...)))
  111. (define (load-members . _)
  112. "Populate *members*."
  113. (define (proc member-directory accumulated)
  114. `((,member-directory . ,(open-manifest
  115. (in-vicinity member-directory "/Cargo.toml")
  116. (in-vicinity member-directory "/Cargo.json")))
  117. . ,accumulated))
  118. (*members* (fold proc '()
  119. (all-workspace-members (*manifest*)))))
  120. (define* (member-dependencies member-manifest #:optional
  121. (kinds '(dependency build)))
  122. "Return the list of members that MEMBER-MANIFEST depends upon.
  123. Only the dependency kinds in KINDS are listed, as understood by
  124. manifest-all-dependencies. The dependencies are returned as a
  125. list of (DIRECTORY . MANIFEST) pairs."
  126. (define crates
  127. (manifest-all-dependencies member-manifest kinds))
  128. (define* (find-matching-members crate)
  129. (define (check member)
  130. (and (string=? (normalise-crate-name
  131. (package-name (manifest-package (cdr member))))
  132. (crate-mapping-dependency-name crate))
  133. member))
  134. (filter-map check (*members*)))
  135. (define (maybe-find-matching-member crate)
  136. (define members (find-matching-members crate))
  137. (match (length members)
  138. (0 #false) ; looks like an external dependency
  139. (1 (car members)) ; unique match
  140. (_ (scm-error 'ambigious-workspace-dependency "member-dependencies"
  141. "~s is ambigious, multiple hits: ~s"
  142. (list crate members) #false))))
  143. (filter-map maybe-find-matching-member crates))
  144. (define* (expand-glob text)
  145. ;; TODO: find reference documentation for glob patterns in Rust.
  146. ;; TODO: does this need to check if a Cargo.toml file exists in
  147. ;; the directory?
  148. (match (string-index text #\*)
  149. ((? number? i)
  150. (let ((before-star (substring text 0 i))
  151. (after-star (substring text (+ i 1))))
  152. (unless (member after-star '("" "/"))
  153. ;; e.g. *baz
  154. (error "this kind of glob pattern is not supported yet"))
  155. (unless (or (string-null? before-star)
  156. (string-suffix? "/" before-star))
  157. ;; e.g. foo*
  158. (error "this kind of glob pattern is not supported yet"))
  159. (define (recurse potential-member)
  160. (define where (in-vicinity before-star (car potential-member)))
  161. (if (and (not (member (car potential-member) '("." "..")))
  162. (case (assq-ref (cdr potential-member) 'type)
  163. ((directory) #true)
  164. ((unknown) (eq? 'directory (stat:type (lstat where))))
  165. (else #false)))
  166. (expand-glob (string-append where after-star))
  167. ;; Not a proper subdirectory, not a member.
  168. '()))
  169. (append-map recurse (scandir* before-star))))
  170. (#false (list text)))) ; base case
  171. (define (all-workspace-members manifest)
  172. "Return a list of directory names corresponding to each workspace members,
  173. relative to the current working directory and without additional
  174. preceding \"./\".
  175. If the workspace itself is a package, it is included as \".\"."
  176. ;; TODO: handle 'if the workspace itself is a package'.
  177. ;; TODO: what to do in case of duplicates?
  178. (append-map expand-glob
  179. (append (if (manifest-package manifest)
  180. '(".")
  181. '())
  182. (workspace-members (manifest-workspace manifest)))))
  183. (define* (build-workspace-members #:key
  184. (member-phases %antioxidant-member-main-phases)
  185. (override-member-crate-type '())
  186. #:allow-other-keys
  187. #:rest arguments)
  188. "Build all the workspace members, excluding anything that might need
  189. 'dev-dependencies' like examples and tests."
  190. (define (build-workspace-member member-directory+manifest)
  191. (format #t "Building the member ~a ...~%" (car member-directory+manifest))
  192. (define directory (car member-directory+manifest))
  193. (define member-crate-type
  194. (match (assoc directory override-member-crate-type)
  195. ((_ . crate-type) crate-type)
  196. (#false #false)))
  197. (with-member directory
  198. (apply gnu-build
  199. ;; #:phases must be set after 'arguments' to override it.
  200. (append arguments
  201. ;; The embedded tests may have dev-dependencies.
  202. ;; TODO: adjust 'build' to support building _only_
  203. ;; the embedded tests and call it from
  204. ;; %antioxidant-member-extra-phases, such that
  205. ;; embedded tests can be run for workspace builds.
  206. (list #:tests? #false
  207. #:phases member-phases
  208. #:rust-crate-type member-crate-type
  209. #:member directory)))))
  210. (define ordered-members
  211. (reverse
  212. (topological-sort* (*members*)
  213. (lambda (+manifest) (member-dependencies (cdr +manifest)))
  214. car)))
  215. (format #t "The workspace members will be built in the following order:~%")
  216. (for-each (lambda (m)
  217. (format #t "* ~a~%" (car m))) ordered-members)
  218. (for-each build-workspace-member ordered-members))
  219. (define* (extra-member-phases #:key
  220. (member-extra-phases %antioxidant-member-extra-phases)
  221. #:allow-other-keys
  222. #:rest arguments)
  223. (define (do-workspace-member member-directory+manifest)
  224. (format #t "Building remainder of member ~a ...~%"
  225. (car member-directory+manifest))
  226. (define directory (car member-directory+manifest))
  227. (with-member directory
  228. (apply gnu-build
  229. (append arguments
  230. (list #:phases member-extra-phases
  231. #:member directory)))))
  232. (format #t "Doing per-workspace member things that might require dev-dependencies~%")
  233. (for-each do-workspace-member (*members*)))
  234. ;; Some workspaces use a Makefile, but not all.
  235. (define (phase-if-makefile phase-name phase)
  236. (lambda arguments
  237. (if (file-exists? "Makefile")
  238. (apply phase arguments)
  239. (format #t "No Makefile exists, skipping ~a phase.~%" phase-name))))
  240. (define %antioxidant-member-main-phases
  241. (modify-phases %standard-antioxidant-phases
  242. ;; Delete phases that don't need to be done on a per-member basis.
  243. (delete 'set-SOURCE-DATE-EPOCH)
  244. (delete 'set-paths)
  245. (delete 'install-locale)
  246. (delete 'unpack)
  247. (delete 'bootstrap)
  248. (delete 'patch-usr-bin-file)
  249. (delete 'patch-source-shebangs)
  250. (delete 'patch-generated-file-shebangs)
  251. (delete 'patch-shebangs)
  252. ;; Requires 'dev-dependencies'
  253. (delete 'build-tests)
  254. (delete 'check)
  255. (delete 'strip)
  256. (delete 'validate-runpath)
  257. (delete 'validate-documentation-location)
  258. (delete 'delete-info-dir-file)
  259. (delete 'patch-dot-desktop-files)
  260. (delete 'make-dynamic-linker-cache)
  261. (delete 'install-license-files)
  262. (delete 'reset-gzip-timestamps)
  263. (delete 'compress-documentation)))
  264. ;; Phases requiring 'dev-dependencies', which may need to be built
  265. ;; at the end to resolve test cycles. TODO: use it.
  266. (define %antioxidant-member-extra-phases
  267. `(,(assq 'build-tests %standard-antioxidant-phases)
  268. ,(assq 'check %standard-antioxidant-phases)))
  269. (define %antioxidant-workspaces-phases
  270. (modify-phases %standard-phases
  271. (add-after 'unpack 'load-members load-members)
  272. (add-after 'unpack 'load-manifest load-manifest)
  273. (delete 'patch-usr-bin-file)
  274. (delete 'configure) ; Cargo has a different setup for build scripts
  275. (delete 'patch-generated-file-shebangs)
  276. (add-after 'build 'build-workspace-members build-workspace-members)
  277. (add-after 'build-workspace-members 'extra-member-phases extra-member-phases)
  278. (delete 'build)
  279. (replace 'check (phase-if-makefile
  280. 'check
  281. (assoc-ref %standard-phases 'check)))
  282. (replace 'install (phase-if-makefile
  283. 'install
  284. (assoc-ref %standard-phases 'install)))))