symlink-manager.scm 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  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. (guix i18n))
  42. #$%initialize-gettext
  43. (define ((simplify-file-tree parent) file)
  44. "Convert the result produced by `file-system-tree' to less
  45. verbose and more suitable for further processing format.
  46. Extract dir/file info from stat and compose a relative path to the
  47. root of the file tree.
  48. Sample output:
  49. ((dir . \".\")
  50. ((dir . \"config\")
  51. ((dir . \"config/fontconfig\")
  52. (file . \"config/fontconfig/fonts.conf\"))
  53. ((dir . \"config/isync\")
  54. (file . \"config/isync/mbsyncrc\"))))
  55. "
  56. (match file
  57. ((name stat) `(file . ,(string-append parent name)))
  58. ((name stat children ...)
  59. (cons `(dir . ,(string-append parent name))
  60. (map (simplify-file-tree
  61. (if (equal? name ".")
  62. ""
  63. (string-append parent name "/")))
  64. children)))))
  65. (define ((file-tree-traverse preordering) node)
  66. "Traverses the file tree in different orders, depending on PREORDERING.
  67. if PREORDERING is @code{#t} resulting list will contain directories
  68. before files located in those directories, otherwise directory will
  69. appear only after all nested items already listed."
  70. (let ((prepend (lambda (a b) (append b a))))
  71. (match node
  72. (('file . path) (list node))
  73. ((('dir . path) . rest)
  74. ((if preordering append prepend)
  75. (list (cons 'dir path))
  76. (append-map (file-tree-traverse preordering) rest))))))
  77. (use-modules (guix build utils))
  78. (let* ((config-home (or (getenv "XDG_CONFIG_HOME")
  79. (string-append (getenv "HOME") "/.config")))
  80. (he-path (string-append (getenv "HOME") "/.guix-home"))
  81. (new-he-path (string-append he-path ".new"))
  82. (new-home (getenv "GUIX_NEW_HOME"))
  83. (old-home (getenv "GUIX_OLD_HOME"))
  84. (new-files-path (string-append new-home "/files"))
  85. ;; Trailing dot is required, because files itself is symlink and
  86. ;; to make file-system-tree works it should be a directory.
  87. (new-files-dir-path (string-append new-files-path "/."))
  88. (home-path (getenv "HOME"))
  89. (backup-dir (string-append home-path "/"
  90. (number->string (current-time))
  91. "-guix-home-legacy-configs-backup"))
  92. (old-tree (if old-home
  93. ((simplify-file-tree "")
  94. (file-system-tree
  95. (string-append old-home "/files/.")))
  96. #f))
  97. (new-tree ((simplify-file-tree "")
  98. (file-system-tree new-files-dir-path)))
  99. (get-source-path
  100. (lambda (path)
  101. (readlink (string-append new-files-path "/" path))))
  102. (get-target-path
  103. (lambda (path)
  104. (string-append home-path "/." path)))
  105. (get-backup-path
  106. (lambda (path)
  107. (string-append backup-dir "/." path)))
  108. (directory?
  109. (lambda (path)
  110. (equal? (stat:type (stat path)) 'directory)))
  111. (empty-directory?
  112. (lambda (dir)
  113. (equal? (scandir dir) '("." ".."))))
  114. (symlink-to-store?
  115. (lambda (path)
  116. (and
  117. (equal? (stat:type (lstat path)) 'symlink)
  118. (store-file-name? (readlink path)))))
  119. (backup-file
  120. (lambda (path)
  121. (mkdir-p backup-dir)
  122. (format #t (G_ "Backing up ~a...") (get-target-path path))
  123. (mkdir-p (dirname (get-backup-path path)))
  124. (rename-file (get-target-path path) (get-backup-path path))
  125. (display (G_ " done\n"))))
  126. (cleanup-symlinks
  127. (lambda ()
  128. (let ((to-delete ((file-tree-traverse #f) old-tree)))
  129. (display
  130. (G_
  131. "Cleaning up symlinks from previous home-environment.\n\n"))
  132. (map
  133. (match-lambda
  134. (('dir . ".")
  135. (display (G_ "Cleanup finished.\n\n")))
  136. (('dir . path)
  137. (if (and
  138. (file-exists? (get-target-path path))
  139. (directory? (get-target-path path))
  140. (empty-directory? (get-target-path path)))
  141. (begin
  142. (format #t (G_ "Removing ~a...")
  143. (get-target-path path))
  144. (rmdir (get-target-path path))
  145. (display (G_ " done\n")))
  146. (format
  147. #t
  148. (G_ "Skipping ~a (not an empty directory)... done\n")
  149. (get-target-path path))))
  150. (('file . path)
  151. (when (file-exists? (get-target-path path))
  152. ;; DO NOT remove the file if it is no longer
  153. ;; a symlink to the store, it will be backed
  154. ;; up later during create-symlinks phase.
  155. (if (symlink-to-store? (get-target-path path))
  156. (begin
  157. (format #t (G_ "Removing ~a...") (get-target-path path))
  158. (delete-file (get-target-path path))
  159. (display (G_ " done\n")))
  160. (format
  161. #t
  162. (G_ "Skipping ~a (not a symlink to store)... done\n")
  163. (get-target-path path))))))
  164. to-delete))))
  165. (create-symlinks
  166. (lambda ()
  167. (let ((to-create ((file-tree-traverse #t) new-tree)))
  168. (map
  169. (match-lambda
  170. (('dir . ".")
  171. (display
  172. (G_ "New symlinks to home-environment will be created soon.\n"))
  173. (format
  174. #t (G_ "All conflicting files will go to ~a.\n\n") backup-dir))
  175. (('dir . path)
  176. (let ((target-path (get-target-path path)))
  177. (when (and (file-exists? target-path)
  178. (not (directory? target-path)))
  179. (backup-file path))
  180. (if (file-exists? target-path)
  181. (format
  182. #t (G_ "Skipping ~a (directory already exists)... done\n")
  183. target-path)
  184. (begin
  185. (format #t (G_ "Creating ~a...") target-path)
  186. (mkdir target-path)
  187. (display (G_ " done\n"))))))
  188. (('file . path)
  189. (when (file-exists? (get-target-path path))
  190. (backup-file path))
  191. (format #t (G_ "Symlinking ~a -> ~a...")
  192. (get-target-path path) (get-source-path path))
  193. (symlink (get-source-path path) (get-target-path path))
  194. (display (G_ " done\n"))))
  195. to-create)))))
  196. (when old-tree
  197. (cleanup-symlinks))
  198. (create-symlinks)
  199. (symlink new-home new-he-path)
  200. (rename-file new-he-path he-path)
  201. (display (G_" done\nFinished updating symlinks.\n\n"))))))
  202. (define (update-symlinks-gexp _)
  203. #~(primitive-load #$(update-symlinks-script)))
  204. (define home-symlink-manager-service-type
  205. (service-type (name 'home-symlink-manager)
  206. (extensions
  207. (list
  208. (service-extension
  209. home-activation-service-type
  210. update-symlinks-gexp)))
  211. (default-value #f)
  212. (description "Provide an @code{update-symlinks}
  213. script, which creates symlinks to configuration files and directories
  214. on every activation. If an existing file would be overwritten by a
  215. symlink, backs up that file first.")))