emacs-helm / helm-system-packages

A Helm interface to the package manager of your operating system
GNU General Public License v3.0
106 stars 11 forks source link

Lacking abstraction #25

Open DamienCassou opened 6 years ago

DamienCassou commented 6 years ago

I've just found your blog and this package. I was excited. When I saw there was no support for dnf I started writing one starting from the support for dpkg. Then I realized that it was a lot of code to duplicate from dpkg even though I was expecting to mostly only implement a list of strings of command line arguments (in the same way as https://gitlab.com/jabranham/system-packages/).

Why is there so much stuff to implement? Is there any abstraction missing? Why isn't helm-system-packages depending on system-packages?

Ambrevar commented 6 years ago

Hi Damien! Thanks for the feedback :D

In many ways you are right, Helm System Packages lacks a good abstraction. Reasons are mostly historical: when I started this package, I took over @thierryvolpiatto's two initial implementations for dpkg and portage. I wanted it to re-use the existing code base and to make sure everything was as uniform as possible between pacman, guix, xbps and other package managers to come.

Ensued the current code base: it was the easiest and most efficient to Get Things Done™. Now would be a good time for a massive refactoring, be it's a bit harder than it seems at first glance: if you give it a deeper look you'll soon notice that much of the code is very similar up to some tiny annoying details that make everything quite hard to factor.

The core issue here is that we need to parse the command outputs and they are all massively different. Some actions don't even map exactly across the various package managers (e.g. some actions don't exist in brew).

Regarding https://gitlab.com/jabranham/system-packages/: it only deals with commands, not with output parsing, so it's much easier to make a uniform abstraction. We could re-use system-packages.el in this package, but it would add a depedency for not much benefit: about 3-5 commands are needed per package and that's it. The rest of the job must be done by us.

If you've got some free time, it would be fantastic if you could contribute a dnf interface. It's pretty easy once you got the dnf output parsed. I suggest you start from pacman though, it's much more finished (I used to be a Arch Linux user but never really into Debian).

Let me know if you are down for it! Cheers!

DamienCassou commented 6 years ago

Please see #26 for a prototype. Initial thoughts on what is lacking:

Ambrevar commented 6 years ago
thierryvolpiatto commented 6 years ago

Pierre Neidhardt notifications@github.com writes:

Some actions don't even map exactly across the various package managers (e.g. some actions don't exist in brew).

Instead of using :action slot use :action-transformer, like this you can use only one set of actions for all.

-- Thierry

Ambrevar commented 6 years ago

Didn't know about :action-transformer, thanks for the hint!

Ambrevar commented 6 years ago

I just remembered one strong argument why modules are not more factorized: simply because it's very hard to test and maintain by a single person. Testing a foreign package manager usually requires virtual machines, which can be costly and cumbersome to setup.

This is why we must be conservative and make sure every module works independently from the rest.

That said, we can work on factorization and move the modules to the new abstraction one by one.

Ambrevar commented 6 years ago

I've committed a draft of a package manager abstraction. See #27.

Ambrevar commented 6 years ago

27 has been merged with some modifications.

Let me know if there is anything else you'd like to see factored.

DamienCassou commented 6 years ago

Great job, thank you. Some more abstractions:

(defvar helm-system-packages-guix
  (helm-system-packages-manager-create
   :name "guix"
   :actions '(
      :info #'helm-system-packages-guix-info)))
Ambrevar commented 6 years ago
DamienCassou commented 6 years ago

The [...] block is rather idiomatic in Helm

I agree but my whole point is about abstracting away from helm for 2 reasons:

info actrion : Isn't it what helm-system-packages-show-information is already doing?

Not quite. Look at my PR #26. You will see a dnf-info function that is doing the dirty work I would like an abstraction to do instead. Now, look at dnf--info which is the only thing I would like to provide.

Helm actions: your solution sounds less flexible

you are right. This is always the case when you abstract things away. Moreover, this is probably the only way to implement the above (abstracting away from helm to get the selected packages as parameter).

What if a module wants to define a new type of action?

you could provide a :extra-actions that would be the equivalent of :actions right now. That way, you have both flexibility for those who need it and abstraction/simplification for most cases.

Using a separate variable also lets the user define their own actions without redefining the whole manager interface

this could be done at the level of helm-system-packages instead of per-module. This doesn't prevent the user from adding an action to a specific manager:

(helm-system-packages-add-manager-action "dnf" '("Cleanup dnf database" . ...))
thierryvolpiatto commented 6 years ago

Damien Cassou notifications@github.com writes:

The [...] block is rather idiomatic in Helm

I agree but my whole point is about abstracting away from helm for 2 reasons:

  • maybe tomorrow I would like to use ivy instead;

So why developing a helm package? Once you have removed all the helm related code you endup with simple completing-read's without the benefit of all the helm features not present in other packages, also the "helm-" prefix of your package name have non sense once you have removed all helm related code.

-- Thierry

Ambrevar commented 6 years ago

@DamienCassou: I had initially misunderstood what you wanted to abstract. But you are right: the output processing of the various package managers should be abstracted away from the Helm interface. Much of the current implementation is due to laziness because no one expressed interest in an Ivy interface before.

That said:

Conclusion: It will take a significant effort to factor Helm aside and I think it makes little sense to go down that road unless someone is really interested in making an Ivy interface.

dnf-info

OK, it's not much but yes, we can abstract that away.

:extra-actions

Why separating actions at all? Isn't it simpler to provide a list of functions instead? This is what the current implementation does. Note that "actions" do not have a Helm-specific type, it's just an alist of (DESCRIPTION . FUNCTION). Why not sticking to that? Maybe I'm misunderstanding what you are trying to achieve here.

this could be done at the level of helm-system-packages instead of per-module.

OK.

DamienCassou commented 6 years ago

I'm not sure Ivy would be as suited as a package manager

that was not my point, sorry. My point was that if modules depend less on helm and stay focused on the package manager they support, the developers life will be simpler when developing a new module or when the completion interface changes.

Why separating actions at all? Isn't it simpler to provide a list of functions instead?

you may, but this approach has some drawbacks:

This is more code to copy-paste which means it will be harder to maintain every module.

Ambrevar commented 6 years ago

Some of the Helm specific code found in modules would not be shorter with an abstraction. A good example is actions: with an abstraction, you'd need to provide a list of functions, which is what Helm does anyways. Adding an abstraction layer here would be duplicating code.

I thought about using shared keybindings but in practice they are too different I'm afraid.

I'm not sure what you have in mind when it comes to abstracting the actions. Could you provide an example?

DamienCassou commented 6 years ago

I'm not sure what you have in mind when it comes to abstracting the actions. Could you provide an example?

I provided one above. Here is it again:

(defvar helm-system-packages-guix
  (helm-system-packages-manager-create
   :name "guix"
   :actions '(
      :info #'helm-system-packages-guix-info)
  :extra-actions: helm-system-packages-guix-extra-actions)) ; <- if needed
Ambrevar commented 6 years ago

Yes, but I don't understand what

(defvar helm-system-packages-guix
  (helm-system-packages-manager-create
   :name "guix"
   :actions '(
      :info #'helm-system-packages-guix-info)
  :extra-actions: helm-system-packages-guix-extra-actions)) ; <- if needed

improves over

(defvar helm-system-packages-guix
  (helm-system-packages-manager-create
   :name "guix"
   :actions (list helm-system-package-guix-actions helm-system-packages-default-actions))

with helm-system-packages-default-actions being anything that can be factored.

DamienCassou commented 6 years ago

This looks good as well. Can you please tell me how helm-system-packages.el would define helm-system-packages-default-actions? Using well known names such as `(intern (concat "helm-system-packages-" manager "-info")) would work quite fine and would simplify even more what I suggested above. The only downside is things becomes a big magic. More documentation is needed so module developers don't have to guess what to implement.

Ambrevar commented 6 years ago

Hmm, looks like I wrote too fast, I don't see anything that can be factored :(

Magic names with intern are one way to go, but as you said it's a bit magic and I think it's better to avoid it. The last factorization was one step in that direction: it avoided one intern.

So if we don't go by magic names, then, regardless of the interface, the action-specific functions will have to be glued to the interface at some point. So I'm not sure what your suggestion simplifies.

Currently:

(defcustom helm-system-packages-guix-actions
  '(("Show package(s)" . helm-system-packages-guix-info)
    ("Install" . helm-system-packages-guix-install)
    ("Uninstall" . helm-system-packages-guix-uninstall)
    ("Browse homepage URL" . helm-system-packages-guix-browse-url)
    ("Find files" . helm-system-packages-guix-find-files)
    ("Show dependencies" . helm-system-packages-guix-show-dependencies)
    ("Show reverse dependencies" . helm-system-packages-guix-show-reverse-dependencies))
  "Actions for Helm guix."
  :group 'helm-system-packages
  :type '(alist :key-type string :value-type function))

(defvar helm-system-packages-guix
  (helm-system-packages-manager-create
   :name "guix"
   :refresh-function #'helm-system-packages-guix-refresh
   :dependencies '("guix" "recsel")
   :help-message 'helm-system-packages-guix-help-message
   :keymap helm-system-packages-guix-map
   :transformer #'helm-system-packages-guix-transformer
   :actions helm-system-packages-guix-actions))

Your suggestion:

    (defvar helm-system-packages-guix
      (helm-system-packages-manager-create
       :name "guix"
       :refresh-function #'helm-system-packages-guix-refresh
       :dependencies '("guix" "recsel")
       :help-message 'helm-system-packages-guix-help-message
       :keymap helm-system-packages-guix-map
       :transformer #'helm-system-packages-guix-transformer
       :actions '(
          :info #'helm-system-packages-guix-info
          :install #'helm-system-packages-guix-install
          :uninstall #'helm-system-packages-guix-uninstall
          :browse-url #'helm-system-packages-guix-browse-url
          :find-files #'helm-system-packages-guix-find-files
          :show-dependencies #'helm-system-packages-guix-show-dependencies
          :show-reverse-dependencies #'helm-system-packages-guix-show-reverse-dependencies)
      :extra-actions: helm-system-packages-guix-extra-actions)) ; <- if needed
DamienCassou commented 6 years ago

I'm not sure what your suggestion simplifies

I think it simplifies 2 things:

  1. helm action functions are now defined by helm-system-package directly. What each module defines is a function whose interface is imposed by helm-system-package to do only what is specific to the package manager itself. For :info, this means
    • helm-system-package is responsible for (1) converting helm candidates into a list of packages, (2) pass this list to the module-specific function (e.g., helm-system-packages-guix-info), (3) write the result to a dedicated Org buffer through helm-system-packages-show-information.
    • each module is now responsible for transforming a list of packages passed as argument into a list of Org-formatted strings
  2. modules are not responsible for defining the helm actions themselves which means:
    • abstracting away from the helm API: modules won't have to change if helm's API changes, module authors don't have to learn helm's API, ...
    • define one set of descriptions and keybindings instead of asking modules to copy/paste

I feel that I'm repeating myself a lot. If you are not convinced by above arguments, I suggest we agree to disagree and move on :-).

Ambrevar commented 6 years ago

OK, I understand what you mean. Sorry for the previous confusion. I've had a similar architecture in mind at the beginning, but that was without counting on the existing code and the friction that arises when maintaining the various modules. Since I've been using only pacman and now guix, I did not feel the effort was worth the gain considering I could update only one package manager to a new interface (while dpkg and portage were stuck to the old one).

The road we are going down now is to move dnf and guix to a new architecture while we must keep the old. It's not pretty, and the changes you are suggesting are going to double the code base. We would also have to warn newcomers to only refer to dnf and guix: if they don't, they will be heavily confused.

What we would ideally need is a test suite embedding various pre-recorded outputs (info, search, etc.) so that the modules can be safely worked on without having the actual package manager at hand. From there, we could safely change the interface to something more data-centric as you suggest.

Any idea on how to produce those output recordings without access to the package manager?

DamienCassou commented 6 years ago

Sorry for the previous confusion

no need to apologize. It's at least half my fault :-).

What we would ideally need is a test suite embedding various pre-recorded outputs

that would be a really good start. If you want to go the integration test route, you could also test your package with several docker images, each built with a different OS. Gitlab has a CI supporting that by default.

Any idea on how to produce those output recordings without access to the package manager?

you can easily get access to any package manager you want through virtualization. If you give me a list of package-manager commands, I will execute them for one or two package managers.

Ambrevar commented 6 years ago

Absolutely, but I don't have the resources to virtualize anything at the moment. You can find the list of commands by looking up the "process-file" and "call-process" functions.

Continuous integration would also be welcome of course.

DamienCassou commented 6 years ago

I found another reason for abstraction: if modules are kept independent from helm, helm-system-packages could use multiple modules at the same time: e.g., if the user has both dnf and guix, M-x helm-system-packages would show a list of packages coming from both package managers.

Ambrevar commented 6 years ago

At the moment it should already be possible to run helm-system-packages over dnf and guix separately.

As to displaying both of them at the same time... I guess this would work in different sources. Is that what you were thinking? If so, I think it's a good idea.

DamienCassou commented 6 years ago

Pierre Neidhardt notifications@github.com writes:

As to displaying both of them at the same time... I guess this would work in different sources. Is that what you were thinking? If so, I think it's a good idea.

you could get a list of all packages from both managers, each one in a separate helm section (I don't know Helm's vocabulary, sorry). The package is not there yet, but it could become reasonable with the refactorings I suggest.

-- Damien Cassou http://damiencassou.seasidehosting.st

"Success is the ability to go from one failure to another without losing enthusiasm." --Winston Churchill

Ambrevar commented 6 years ago

Yes, we talk about the same thing: "source" is a section in Helm's parlance.

Ambrevar commented 4 years ago

@DamienCassou: It's been a while! In the meantime I've worked on a similar universal package manager abstraction for Nyxt, this time starting from scratch. The abstraction should be much better, plus it has support for functional package managers like Guix and Nix.

https://nyxt.atlas.engineer/article/release-2-pre-release-4.org

DamienCassou commented 4 years ago

Good job!