symlink-manager.scm 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. ;;; GNU Guix --- Functional package management for GNU
  2. ;;; Copyright © 2021 Andrew Tropin <andrew@trop.in>
  3. ;;; Copyright © 2021 Xinglu Chen <public@yoctocell.xyz>
  4. ;;;
  5. ;;; This file is part of GNU Guix.
  6. ;;;
  7. ;;; GNU Guix is free software; you can redistribute it and/or modify it
  8. ;;; under the terms of the GNU General Public License as published by
  9. ;;; the Free Software Foundation; either version 3 of the License, or (at
  10. ;;; your option) any later version.
  11. ;;;
  12. ;;; GNU Guix is distributed in the hope that it will be useful, but
  13. ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
  14. ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. ;;; GNU General Public License for more details.
  16. ;;;
  17. ;;; You should have received a copy of the GNU General Public License
  18. ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
  19. (define-module (gnu home services symlink-manager)
  20. #:use-module (gnu home services)
  21. #:use-module (guix gexp)
  22. #:export (home-symlink-manager-service-type))
  23. ;;; Comment:
  24. ;;;
  25. ;;; symlink-manager cares about configuration files: it backs up files
  26. ;;; created by user, removes symlinks and directories created by a
  27. ;;; previous generation, and creates new directories and symlinks to
  28. ;;; configuration files according to the content of files/ directory
  29. ;;; (created by home-files-service) of the current home environment
  30. ;;; generation.
  31. ;;;
  32. ;;; Code:
  33. (define (update-symlinks-script)
  34. (program-file
  35. "update-symlinks"
  36. #~(begin
  37. (use-modules (ice-9 ftw)
  38. (ice-9 curried-definitions)
  39. (ice-9 match)
  40. (srfi srfi-1))
  41. (define ((simplify-file-tree parent) file)
  42. "Convert the result produced by `file-system-tree' to less
  43. verbose and more suitable for further processing format.
  44. Extract dir/file info from stat and compose a relative path to the
  45. root of the file tree.
  46. Sample output:
  47. ((dir . \".\")
  48. ((dir . \"config\")
  49. ((dir . \"config/fontconfig\")
  50. (file . \"config/fontconfig/fonts.conf\"))
  51. ((dir . \"config/isync\")
  52. (file . \"config/isync/mbsyncrc\"))))
  53. "
  54. (match file
  55. ((name stat) `(file . ,(string-append parent name)))
  56. ((name stat children ...)
  57. (cons `(dir . ,(string-append parent name))
  58. (map (simplify-file-tree
  59. (if (equal? name ".")
  60. ""
  61. (string-append parent name "/")))
  62. children)))))
  63. (define ((file-tree-traverse preordering) node)
  64. "Traverses the file tree in different orders, depending on PREORDERING.
  65. if PREORDERING is @code{#t} resulting list will contain directories
  66. before files located in those directories, otherwise directory will
  67. appear only after all nested items already listed."
  68. (let ((prepend (lambda (a b) (append b a))))
  69. (match node
  70. (('file . path) (list node))
  71. ((('dir . path) . rest)
  72. ((if preordering append prepend)
  73. (list (cons 'dir path))
  74. (append-map (file-tree-traverse preordering) rest))))))
  75. (use-modules (guix build utils))
  76. (let* ((config-home (or (getenv "XDG_CONFIG_HOME")
  77. (string-append (getenv "HOME") "/.config")))
  78. (he-path (string-append (getenv "HOME") "/.guix-home"))
  79. (new-he-path (string-append he-path ".new"))
  80. (new-home (getenv "GUIX_NEW_HOME"))
  81. (old-home (getenv "GUIX_OLD_HOME"))
  82. (new-files-path (string-append new-home "/files"))
  83. ;; Trailing dot is required, because files itself is symlink and
  84. ;; to make file-system-tree works it should be a directory.
  85. (new-files-dir-path (string-append new-files-path "/."))
  86. (home-path (getenv "HOME"))
  87. (backup-dir (string-append home-path "/"
  88. (number->string (current-time))
  89. "-guix-home-legacy-configs-backup"))
  90. (old-tree (if old-home
  91. ((simplify-file-tree "")
  92. (file-system-tree
  93. (string-append old-home "/files/.")))
  94. #f))
  95. (new-tree ((simplify-file-tree "")
  96. (file-system-tree new-files-dir-path)))
  97. (get-source-path
  98. (lambda (path)
  99. (readlink (string-append new-files-path "/" path))))
  100. (get-target-path
  101. (lambda (path)
  102. (string-append home-path "/." path)))
  103. (get-backup-path
  104. (lambda (path)
  105. (string-append backup-dir "/." path)))
  106. (directory?
  107. (lambda (path)
  108. (equal? (stat:type (stat path)) 'directory)))
  109. (empty-directory?
  110. (lambda (dir)
  111. (equal? (scandir dir) '("." ".."))))
  112. (symlink-to-store?
  113. (lambda (path)
  114. (and
  115. (equal? (stat:type (lstat path)) 'symlink)
  116. (store-file-name? (readlink path)))))
  117. (backup-file
  118. (lambda (path)
  119. (mkdir-p backup-dir)
  120. (format #t "Backing up ~a..." (get-target-path path))
  121. (mkdir-p (dirname (get-backup-path path)))
  122. (rename-file (get-target-path path) (get-backup-path path))
  123. (display " done\n")))
  124. (cleanup-symlinks
  125. (lambda ()
  126. (let ((to-delete ((file-tree-traverse #f) old-tree)))
  127. (display
  128. "Cleaning up symlinks from previous home-environment.\n\n")
  129. (map
  130. (match-lambda
  131. (('dir . ".")
  132. (display "Cleanup finished.\n\n"))
  133. (('dir . path)
  134. (if (and
  135. (file-exists? (get-target-path path))
  136. (directory? (get-target-path path))
  137. (empty-directory? (get-target-path path)))
  138. (begin
  139. (format #t "Removing ~a..."
  140. (get-target-path path))
  141. (rmdir (get-target-path path))
  142. (display " done\n"))
  143. (format
  144. #t "Skipping ~a (not an empty directory)... done\n"
  145. (get-target-path path))))
  146. (('file . path)
  147. (when (file-exists? (get-target-path path))
  148. ;; DO NOT remove the file if it is no longer
  149. ;; a symlink to the store, it will be backed
  150. ;; up later during create-symlinks phase.
  151. (if (symlink-to-store? (get-target-path path))
  152. (begin
  153. (format #t "Removing ~a..." (get-target-path path))
  154. (delete-file (get-target-path path))
  155. (display " done\n"))
  156. (format
  157. #t
  158. "Skipping ~a (not a symlink to store)... done\n"
  159. (get-target-path path))))))
  160. to-delete))))
  161. (create-symlinks
  162. (lambda ()
  163. (let ((to-create ((file-tree-traverse #t) new-tree)))
  164. (map
  165. (match-lambda
  166. (('dir . ".")
  167. (display
  168. "New symlinks to home-environment will be created soon.\n")
  169. (format
  170. #t "All conflicting files will go to ~a.\n\n" backup-dir))
  171. (('dir . path)
  172. (let ((target-path (get-target-path path)))
  173. (when (and (file-exists? target-path)
  174. (not (directory? target-path)))
  175. (backup-file path))
  176. (if (file-exists? target-path)
  177. (format
  178. #t "Skipping ~a (directory already exists)... done\n"
  179. target-path)
  180. (begin
  181. (format #t "Creating ~a..." target-path)
  182. (mkdir target-path)
  183. (display " done\n")))))
  184. (('file . path)
  185. (when (file-exists? (get-target-path path))
  186. (backup-file path))
  187. (format #t "Symlinking ~a -> ~a..."
  188. (get-target-path path) (get-source-path path))
  189. (symlink (get-source-path path) (get-target-path path))
  190. (display " done\n")))
  191. to-create)))))
  192. (when old-tree
  193. (cleanup-symlinks))
  194. (create-symlinks)
  195. (symlink new-home new-he-path)
  196. (rename-file new-he-path he-path)
  197. (display " done\nFinished updating symlinks.\n\n")))))
  198. (define (update-symlinks-gexp _)
  199. #~(primitive-load #$(update-symlinks-script)))
  200. (define home-symlink-manager-service-type
  201. (service-type (name 'home-symlink-manager)
  202. (extensions
  203. (list
  204. (service-extension
  205. home-activation-service-type
  206. update-symlinks-gexp)))
  207. (default-value #f)
  208. (description "Provide an @code{update-symlinks}
  209. script, which creates symlinks to configuration files and directories
  210. on every activation. If an existing file would be overwritten by a
  211. symlink, backs up that file first.")))