rdallasgray / pallet

A package management tool for Emacs, built on Cask.
279 stars 15 forks source link

Synchronizing installed packages across machines? #49

Open DarwinAwardWinner opened 8 years ago

DarwinAwardWinner commented 8 years ago

I use pallet in my Emacs config which is shared across multiple machines. When I uninstall a package on one machine, Pallet removes it from the cask file, which gets propagated to the other machines. However, nothing tells the other machines to uninstall this package. Would it be possible to provide a command to uninstall every package except for the packages explicitly requested in the cask file and their dependencies?

I could maybe try my hand at implementing it if you're interested.

rdallasgray commented 8 years ago

Hi Ryan -- if you're up for trying an implementation, I'd certainly be interested. The main difficulty I can foresee is that it's impossible to distinguish between top-level dependencies and sub-dependencies. You could get round this by deleting packages not referenced in the Cask file and then running pallet-install to reinstall any missing dependencies. I leave it up to you though.

Thanks a lot!

DarwinAwardWinner commented 8 years ago

I'm hoping it will be possible to collect the packages referenced in the Cask file and then recursively walk the dependency graph to get all their dependencies, and finally just uninstall everything else. But I don't know much about the internals of package.el. Is there anything that would prevent this approach from working?

rdallasgray commented 8 years ago

There's certainly no simple, public API way to do it using package.el. I think there might be a way in epl, though: https://github.com/cask/epl/blob/master/epl.el#L73

DarwinAwardWinner commented 8 years ago

Here's a POC dependency walking function that works for my Cask file:

(defun bundle-recursive-deps (bundle &optional universe)
  (unless universe
    (setq universe (epl-available-packages)))
  (let* ((universe-pkgnames
          (delete-dups (mapcar #'epl-package-name universe)))
         (dep-list nil)
         (dep-ring (make-ring (length universe))))
    ;; Initialize the ring with the explicitly required package names
    ;; from the Cask file
    (cl-loop
     for dep in (cask-bundle-runtime-dependencies bundle)
     do (ring-insert dep-ring (cask-dependency-name dep)))
    ;; Pop each package off the ring, push it into the dependency
    ;; list, and then push its not-previously-seen dependencies into
    ;; the ring.
    (cl-loop
     while (> (ring-length dep-ring) 0)
     ;; Pop the next package off the ring and add it to the list
     for pkgname = (ring-remove dep-ring)
     do (push pkgname dep-list)
     for seen-pkgnames = (nconc (ring-elements dep-ring) dep-list)
     ;; Select all packages in universe with the specified name (there
     ;; might be multiple ones)
     for pkgs = (cl-remove-if-not (lambda (pkg) (eq (epl-package-name pkg) pkgname)) universe)
     ;; Get all dependencies of selected packages, then filter out
     ;; built-in packages and already-seen packages
     for new-pkg-deps =
     (cl-set-difference
      (delete-dups
       (cl-remove-if
        #'epl-built-in-p
        (cl-mapcan (lambda (pkg)
                     (mapcar #'epl-requirement-name
                             (epl-package-requirements pkg)))
                   pkgs)))
      seen-pkgnames)
     ;; Push the new dependencies into the ring
     do (cl-loop for depname in new-pkg-deps
                 do (ring-insert dep-ring depname)))
    dep-list))

;; Simple test
(setq x (cask-initialize))
;; Direct requirements
(setq reqs (mapcar #'cask-dependency-name (cask-bundle-runtime-dependencies x)))
;; Direct requirements plus all their non-builtin recursive
;; dependencies
(setq recreqs (bundle-recursive-deps x (epl-installed-packages)))
;; Should be nil
(cl-set-difference reqs recreqs)
;; Indirect requirements only
(cl-set-difference recreqs reqs)

I have no idea if I'm doing things idiomatically, since epl has lots of struct-based layers of indirection. And also maybe this function belongs in epl itself, or Cask, instead of Pallet. Let me know what you think.

DarwinAwardWinner commented 8 years ago

I should add, I haven't actually implemented the cleanup function, but getting the list of packages to uninstall is just a simple set-difference operation of all installed packages against the return value of the above dependency walker.

rdallasgray commented 8 years ago

Sorry it's taken me so long to get to this. OK, it looks doable, and the way you're using epl seems fine to me. Few points:

Thanks again!

DarwinAwardWinner commented 8 years ago

Sure, I'm happy to rewrite it more idiomatically. That was just my proof of concept.

rdallasgray commented 8 years ago

Great stuff, thanks.