symlink-manager.scm 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  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. ;;; Copyright © 2022 Ludovic Courtès <ludo@gnu.org>
  5. ;;;
  6. ;;; This file is part of GNU Guix.
  7. ;;;
  8. ;;; GNU Guix is free software; you can redistribute it and/or modify it
  9. ;;; under the terms of the GNU General Public License as published by
  10. ;;; the Free Software Foundation; either version 3 of the License, or (at
  11. ;;; your option) any later version.
  12. ;;;
  13. ;;; GNU Guix is distributed in the hope that it will be useful, but
  14. ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
  15. ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. ;;; GNU General Public License for more details.
  17. ;;;
  18. ;;; You should have received a copy of the GNU General Public License
  19. ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
  20. (define-module (gnu home services symlink-manager)
  21. #:use-module (gnu home services)
  22. #:use-module (guix gexp)
  23. #:use-module (guix modules)
  24. #:export (home-symlink-manager-service-type))
  25. ;;; Comment:
  26. ;;;
  27. ;;; symlink-manager cares about xdg configurations and other files: it backs
  28. ;;; up files created by user, removes symlinks and directories created by a
  29. ;;; previous generation, and creates new directories and symlinks to files
  30. ;;; according to the content of directories (created by home-files-service) of
  31. ;;; the current home environment generation.
  32. ;;;
  33. ;;; Code:
  34. (define (update-symlinks-script)
  35. (program-file
  36. "update-symlinks"
  37. (with-imported-modules (source-module-closure
  38. '((guix build utils)
  39. (guix i18n)))
  40. #~(begin
  41. (use-modules (ice-9 ftw)
  42. (ice-9 match)
  43. (srfi srfi-1)
  44. (guix i18n)
  45. (guix build utils))
  46. (define home-directory
  47. (getenv "HOME"))
  48. (define xdg-config-home
  49. (or (getenv "XDG_CONFIG_HOME")
  50. (string-append (getenv "HOME") "/.config")))
  51. (define xdg-data-home
  52. (or (getenv "XDG_DATA_HOME")
  53. (string-append (getenv "HOME") "/.local/share")))
  54. (define backup-directory
  55. (string-append home-directory "/" (number->string (current-time))
  56. "-guix-home-legacy-configs-backup"))
  57. (define (preprocess-file file)
  58. "If file is in XDG-CONFIGURATION-FILES-DIRECTORY use
  59. subdirectory from XDG_CONFIG_HOME to generate a target path."
  60. (cond
  61. ((string-prefix? #$xdg-configuration-files-directory file)
  62. (string-append
  63. (substring xdg-config-home
  64. (1+ (string-length home-directory)))
  65. (substring file
  66. (string-length #$xdg-configuration-files-directory))))
  67. ((string-prefix? #$xdg-data-files-directory file)
  68. (string-append
  69. (substring xdg-data-home
  70. (1+ (string-length home-directory)))
  71. (substring file
  72. (string-length #$xdg-data-files-directory))))
  73. (else file)))
  74. (define (target-file file)
  75. ;; Return the target of FILE, a config file name sans leading dot
  76. ;; such as "config/fontconfig/fonts.conf" or "bashrc".
  77. (string-append home-directory "/" (preprocess-file file)))
  78. (define (no-follow-file-exists? file)
  79. "Return #t if file exists, even if it's a dangling symlink."
  80. (->bool (false-if-exception (lstat file))))
  81. (define (symlink-to-store? file)
  82. (catch 'system-error
  83. (lambda ()
  84. (store-file-name? (readlink file)))
  85. (lambda args
  86. (if (= EINVAL (system-error-errno args))
  87. #f
  88. (apply throw args)))))
  89. (define (backup-file file)
  90. (define backup
  91. (string-append backup-directory "/" (preprocess-file file)))
  92. (mkdir-p backup-directory)
  93. (format #t (G_ "Backing up ~a...") (target-file file))
  94. (mkdir-p (dirname backup))
  95. (rename-file (target-file file) backup)
  96. (display (G_ " done\n")))
  97. (define (cleanup-symlinks home-generation)
  98. ;; Delete from $HOME files that originate in HOME-GENERATION, the
  99. ;; store item containing a home generation.
  100. (define config-file-directory
  101. ;; Note: Trailing slash is needed because "files" is a symlink.
  102. (string-append home-generation "/" #$home-files-directory "/"))
  103. (define (strip file)
  104. (string-drop file
  105. (+ 1 (string-length config-file-directory))))
  106. (format #t (G_ "Cleaning up symlinks from previous home at ~a.~%")
  107. home-generation)
  108. (newline)
  109. (file-system-fold
  110. (const #t)
  111. (lambda (file stat _) ;leaf
  112. (let ((file (target-file (strip file))))
  113. (when (no-follow-file-exists? file)
  114. ;; DO NOT remove the file if it is no longer a symlink to
  115. ;; the store, it will be backed up later during
  116. ;; create-symlinks phase.
  117. (if (symlink-to-store? file)
  118. (begin
  119. (format #t (G_ "Removing ~a...") file)
  120. (delete-file file)
  121. (display (G_ " done\n")))
  122. (format
  123. #t
  124. (G_ "Skipping ~a (not a symlink to store)... done\n")
  125. file)))))
  126. (const #t) ;down
  127. (lambda (directory stat _) ;up
  128. (unless (string=? directory config-file-directory)
  129. (let ((directory (target-file (strip directory))))
  130. (catch 'system-error
  131. (lambda ()
  132. (rmdir directory)
  133. (format #t (G_ "Removed ~a.\n") directory))
  134. (lambda args
  135. (let ((errno (system-error-errno args)))
  136. (cond
  137. ((= ENOTEMPTY errno)
  138. (format
  139. #t
  140. (G_ "Skipping ~a (not an empty directory)... done\n")
  141. directory))
  142. ((= ENOENT errno) #t)
  143. ((= ENOTDIR errno) #t)
  144. (else
  145. (apply throw args)))))))))
  146. (const #t) ;skip
  147. (const #t) ;error
  148. #t ;init
  149. config-file-directory
  150. lstat)
  151. (display (G_ "Cleanup finished.\n\n")))
  152. (define (create-symlinks home-generation)
  153. ;; Create in $HOME symlinks for the files in HOME-GENERATION.
  154. (define config-file-directory
  155. ;; Note: Trailing slash is needed because "files" is a symlink.
  156. (string-append home-generation "/" #$home-files-directory "/"))
  157. (define (strip file)
  158. (string-drop file
  159. (+ 1 (string-length config-file-directory))))
  160. (define (source-file file)
  161. (readlink (string-append config-file-directory file)))
  162. (file-system-fold
  163. (const #t) ;enter?
  164. (lambda (file stat result) ;leaf
  165. (let ((source (source-file (strip file)))
  166. (target (target-file (strip file))))
  167. (when (no-follow-file-exists? target)
  168. (backup-file (strip file)))
  169. (format #t (G_ "Symlinking ~a -> ~a...")
  170. target source)
  171. (symlink source target)
  172. (display (G_ " done\n"))))
  173. (lambda (directory stat result) ;down
  174. (unless (string=? directory config-file-directory)
  175. (let ((target (target-file (strip directory))))
  176. (when (and (no-follow-file-exists? target)
  177. (not (file-is-directory? target)))
  178. (backup-file (strip directory)))
  179. (catch 'system-error
  180. (lambda ()
  181. (mkdir target))
  182. (lambda args
  183. (let ((errno (system-error-errno args)))
  184. (unless (= EEXIST errno)
  185. (format #t (G_ "failed to create directory ~a: ~s~%")
  186. target (strerror errno))
  187. (apply throw args))))))))
  188. (const #t) ;up
  189. (const #t) ;skip
  190. (const #t) ;error
  191. #t ;init
  192. config-file-directory))
  193. #$%initialize-gettext
  194. (let* ((home (string-append home-directory "/.guix-home"))
  195. (pivot (string-append home ".new"))
  196. (new-home (getenv "GUIX_NEW_HOME"))
  197. (old-home (getenv "GUIX_OLD_HOME")))
  198. (when old-home
  199. (cleanup-symlinks old-home))
  200. (create-symlinks new-home)
  201. (symlink new-home pivot)
  202. (rename-file pivot home)
  203. (display (G_" done\nFinished updating symlinks.\n\n")))))))
  204. (define (update-symlinks-gexp _)
  205. #~(primitive-load #$(update-symlinks-script)))
  206. (define home-symlink-manager-service-type
  207. (service-type (name 'home-symlink-manager)
  208. (extensions
  209. (list
  210. (service-extension
  211. home-activation-service-type
  212. update-symlinks-gexp)))
  213. (default-value #f)
  214. (description "Provide an @code{update-symlinks}
  215. script, which creates symlinks to configuration files and directories
  216. on every activation. If an existing file would be overwritten by a
  217. symlink, backs up that file first.")))