Skip to content

Emacs interactively installing its own 3rd party dependencies

License

Notifications You must be signed in to change notification settings

positron-solutions/yakspkgs

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

24 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Yaks Packages

melpa package melpa stable package CI workflow status DCO Check

Emacs interactively installing its own 3rd party dependencies

Emacs can do this:

nix profile install nixpkgs#hello --profile emacs-profile

export PATH=$PATH:./emacs-profile/bin

which hello
# /home/user/.emacs.d/emacs-profile/bin/hello

hello
# Hello, world!

Binaries, reproducible, when you need them. No need for restarting Emacs or using Nix or Guix commands. Let Emacs control everything, as the world should be. Portable across Darwin, WSL2, Linux etc. Pure-build system agnostic so both Guix and Nix users can play together. Re-use work in CI & sanbox tests.

⚠️ WARNING ⚠️

This package is very incomplete and this repo is being rushed out as a proof-of-concept.

Too much of the Nix & Guix work out there has adopted a workflow that forces users to restart or run Nix builds just to change Elisp dependencies. Read the Design Vision to understand what we should be doing and what this package will be. Read Current Status to figure out how to where to start working on it.

Contents

Introduction to pure profiles

Pure dependency management tools (Nix & Guix) have a concept of a profile, a way to present entire sets of programs and their dependencies an environment. This package gives Emacs control of its very own profile. Emacs with a profile can do all of its own 3rd party dependency management without affecting the rest of the user shell. Freedom. Independence. Reproducibility and portability. This use manner will revolutionize Emacs’s relationship with binary packages.

The Design Vision

Emacs should be it’s own package manager for 3rd party packages, using Nix / Guix as backends. Emacs should be in the driver seat. No restarts necessary.

While nixpkgs and guixpkgs include a lot of packages, Emacs may need special builds or bleeding edge upstreams, and this repository can provide those packages. Yakspkgs is an especially good place for dynamically loaded modules that are otherwise a major portability headache.

Implementation points

  • Infrastructure for using Nix & Guix profiles from within Emacs
  • Packages tuned for Emacs whenever package upstreams need some help
  • Live or fully declarative profile management
  • Integration infrastructure for elisp to predictably consume 3rd party deps from profiles
  • Home manager integration driven from within Emacs
  • Lots of packaging examples for people to provide their binaries without requiring hosting

Note ⚠️ Current development is favoring the declarative profile style. This is covered in the Hacking Guide section. ⚠️

Interactive profile

The interactive profile can be edited by the user live using Emacs commands to experiment with different binary dependencies from yakspkgs

Lock file

Record the current state of the interactive profile into a portable lock file, using elisp, consumed in Emacs or through interpretation in batch workflows.

Re-hydrating the lock file into a profile results in a reproducible profile. It only changes when the lock file changes.

Declarative Profile

After a frozen interactive profile is working well enough, a declarative profile can be created for human-maintained fully reproducible 3rd party dependency sets.

Adding dependencies to Elisp packages

Either we say what elisp packages depend on or elisp packges tell us what they depend on. Both workflows will be needed during adoption.

New Elisp Package Headers

Elisp packages like emacs-vterm can adopt a new header instructing yakspkgs about what 3rd party dependencies to install into the profile.

Elisp Package Decoration

Not all elisp packages will declare their 3rd party dependencies overnight. It takes time to normalize this and make it as ubiquitous as declaring elisp dependencies. Package decoration allows elisp package managers to get 3rd party dependencies when yakspkgs moves faster than elisp packages.

Install yakspkgs

⚠️ Stub for elisp modules after POC implementation. Won’t run yet. ⚠️

;; straight-use-package-by-default t and you want a git version
(use-package yakspkgs
  :straight
  (yakspkgs :type git :host github :repo "positron-solutions/yakspkgs"))

;; if straight-use-package-by-default is nil
(straight-use-package '(yakspkgs :type git :host github
                                       :repo "positron-solutions/yakspkgs"))

;; using elpaca (recommended to add a hash for reproducibility)
(elpaca-use-package
 (example :host github
          :repo "positron-solutions/yaskpkgs")
 :demand t)

Current Status

An initial package is being created to demonstrate all the necessary capabilities and figure out what the user interface should be.

There’s a lot to get on top of. To start, a profile is being created inside the user Emacs directory, possibly using no-littering conventions. The initialization will verify the sanity of the package manager (nix) and install a dummy package that will cause a manifest to be created so that the other commands will provide feedback.

Dynamic profile management is pretty easy. Any nix flake style package path is easy to install and they can be listed and uninstalled. This will complete the very basic POC.

After that, some more planning is needed. We can create package sets. We can generate Nix expressions from elisp. We can install them and continue supporting more of the Nix profile API.

The best user experience and utility for hard to do things is going to drive most decisions.

Hacking Guide

This is a short re-index of the information related to providing Emacs with an independent profile, similar to what home manager does. Links and examples work in Emacs, as literate org content.

⚠️ This is work in progress. Many headlines are just stubs about information we will need eventually. ⚠️

Environment

  • The regular envionment data for nix is carried into to the Emacs process (unless you do something to stop this of course)
  • You can list the environment using the list-environment package.
  • Emacs does not by default use buffer local environments. The envrc package by Steve Purcell is very useful for buffer-local style direnv integration.

    This does mean that process-variable will change for buffers that have activated a direnv. The errata caused by functions using a temp buffer without propagating the environment are not that hard to clean up.

  • Inside of vterm, you can run env. The environment inherits process-variable but the two are decoupled after this inheritance, so updating the Emacs global profile variables will not change the local variables.
  • Modifying the PATH in process-variable has the desired effect so that shell-command and other functions correctly discover binaries within the nix profile.

Profiles

Profiles are versioned sets of packages. We want to give Emacs a profile. To do a batch style update to a profile, we can use symlinkJoin to create a package that points to other packages and install that into the profile.

Creation

Nix profiles are maintained using the nix binary. You can create a basic profile manually and “activate” it by just appending the $PATH.

nix profile install nixpkgs#hello --profile emacs-profile

export PATH=$PATH:./emacs-profile/bin

which hello # /home/user/.emacs.d/emacs-profile/bin/hello

hello Hello, world!

Note, for libraries, such as Emacs modules, you will need to state the path in a nix variable and export that dynamic path to a static path.

You can also link the dynamic path to a predictable location and use that location from within elisp. This is done now in Posimacs to expose the vterm.so to Emacs.

The file manifest.json records the provenance of the packages that are installed in this version of the profile.

So where is our new manifest?

cat emacs-profile/manifest.json | jq

# {
#   "elements": [
#     {
#       "active": true,
#       "attrPath": "legacyPackages.x86_64-linux.hello",
#       "originalUrl": "flake:nixpkgs",
#       "outputs": null,
#       "priority": 5,
#       "storePaths": [
#         "/nix/store/g2m8kfw7kpgpph05v2fxcx4d5an09hl3-hello-2.12.1"
#       ],
#       "url": "github:NixOS/nixpkgs/a2d2f70b82ada0eadbcb1df2bca32d841a3c1bf1"
#     }
#   ],
#   "version": 1
# }

Flake Registries

Flake registries contain flakes mappings between a short name and a full path, which are used when we specify packages using shorthand paths such as nixpkgs#hello.

Registry support is kind of a replacement for the older nix channel configuration style. However, installing packages in thi way is still a bit unspecific. In order to rehydrate such a path, we need the expanded URL. Pinning a flake is an option.

Yakspkgs will need registry suport to make it easy to switch between different package sets for dynamic profile usage.

For the declarative style, we usually specify nixpkgs as an input and pin it using the flake lock. There is no question what version of nixpkgs is in use.

We can use a custom registry specific to Emacs with the --registry or --option flag on most commands. This could be maintained in the yakspkgs-profile-dir. It’s just a registry.json file.

# Specifying a registry to nix search
nix search --option flake-registry "file://$(pwd)/registry.json" nixpkgs hello

# Specifying a registry in nix add
nix registry add --registry ./custom-flake-registry.json nixpkgs github:nixos/nixpkgs

The registry is obviously convenient for times when the user approximately knows the package name and doesn’t really care which nixpkgs version the package is from, usually because the tool is very stable across versions.

We can offer similar shortcuts in Emacs, using completions across flake attributes from a frozen flake in the registry.

The profile manifest, stored in manifest.json, can also be used to uniquely describe package sources.

Versioned flakes and packages very nearly approximate the same information as a declaraed package set.

Search

Registries are somewhat related to search in that they specify flakes from which we might install other packages. In the case of something like nixpkgs, the user probably wants to search without having to declare nixpkgs as a flake input only to re-export the packages for local completions. The search interface is good for this.

Searching the flakes in the registry is also valuable becuase flakes that are added to a regsitry are added essentially to provide convenient access.

Installing a Declarative Package Set

We don’t want to use the old school (dirty) nix-env -i style stuff all the time. Use flakes. Be pure.

Building a set of profile contents

We can use pkgs.symlinkJoin to bundle together a pile of packages into one super package. Then, this super-package has all the versioned goodness of the profile. Thus we can roll back to a previous working declaration if needed.

An example is in the profile-contents directory.

Beautiful Nix Profile diagrams. It is a superposition of outputs basically. If we install this package into the profile, the profile will have a new version, one per each time we update and install our overall declaration.

Installation of profile contents into the profile

Home manager also builds itself as a package. You can see in the activation script that it executes commands like below:

# The docs in the help are good
nix profile --help
nix profile install /nix/store/yz31iyfqdw4n20l6bpbbxx1y5hrxz4l7-home-manager-path

# You can see the derivation that outputs this path.  Nix infers
# this fact from the /nix directory in the path.
nix show-derivation /nix/store/yz31iyfqdw4n20l6bpbbxx1y5hrxz4l7-home-manager-path

Look inside the profile-contents directory for a flake. This flake just provides a super-package including cowsay and hello. Let’s install this super-package into our Emacs profile:

nix profile install ./profile-contents#profile-contents --profile emacs-profile

# If you get a collision, you should remove the hello package or just delete the profile and start over
# nix profile remove /nix/store/g2m8kfw7kpgpph05v2fxcx4d5an09hl3-hello-2.12.1 --profile emacs-profile

# after these get installed, the manifest will be updated.  If the profile is on
# the $PATH, you will be able to run cowsay and hello.  No magic, but cool.

# Note, the structure includes our silly `emacs-god-profile' package name from the flake.nix
tree emacs-profile/bin -L 2
emacs-profile/bin
├── cowsay -> /nix/store/3d54xbrqj5zixa0cfnyki09jrffr0g3a-cowsay-3.04/bin/cowsay
├── cowthink -> cowsay
└── hello -> /nix/store/g2m8kfw7kpgpph05v2fxcx4d5an09hl3-hello-2.12.1/bin/hello

So there it is. You can build a set of packages. You can attach them to a profile. You can update them declaratively. You can make the available on the bin path. This is almost a POC for giving Emacs an independent profile already.

Activation

Let’s reverse-engineer home manager’s activation to get some ideas. If you have home manager installed, after building a completed home manager profile, just take a look at ~/.nixpkgs/config/result/activate. It contains a lot of stuff that applies to starting systemd processes. The path and environment stuff is what we’re interested in.

The “activation” script basically:

  • Check for file symlink collisions on any files we plan to link into place
  • Check that the profile we are upgrading to is not a downgrade
  • Link stuff into place and make the profile active

The money hunk here updates the PATH, desktop environment, and systemd state. It’s not very magical.

XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR:-/run/user/$(id -u)} \
    PATH=/nix/store/9fhmhbfkdcarrl1d75h1zbfsnbmwrw57-systemd-250.4/bin:$PATH \
bash /nix/store/lyvazadz3v9nck27nwcczqi4s9m402ix-systemd-activate.sh "${oldGenPath=}" "$newGenPath"

Yes, updating the PATH is 99% of “activation.” For Emacs to obtain binaries, it’s almost 100%. We only need to reproduce the file linking support for linking libraries into place if elisp scripts expect them to be in a known. (The slightly longer way is to include the generated paths into the elisp files, basically using nix to finish the elisp files as tempaltes.)

Note it is not recommended to use most elisp files as generated outputs of Nix because they are immutable. We want to be able to modify our elisp without running a bunch of (sometimes costly) nix builds.

We can reverse-engineer the symlink system and even perform “activation” using elisp instead of yucky bash or less common (to Emacs users) Nix lang.

Packages

We are gluing nix packages onto Emacs packages. Either the package asks for it or we provide the package when we see its dependent.

Emacs packages have package headers. They can contain 3rd party dependency declaration or we can provide a map. We can consume header information or we can decorate those packages with information they don’t know yet.

Systemd and the Emacs Daemon

If you want Emacs to run when the DE starts, home manager has this integration ready to go with its Emacs module.

If we try to drive this stuff from within Emacs’ own profile, it may introduce some bootstrap issues with low value, so that’s not a goal right now.

There’s an analog for Darwin. (Sorry WSL2).

Contributing

First decide if you want to work on this repository or fork it to something entirely different. Non-exhaustive list of changes that are very welcome:

  • Guix package declarations
  • Guix translation of elisp package headers
  • Guix elisp package decoration
  • Guix profile generation from lock file
  • Guix commands to interact with Guix daemon

Changes will likely be rejected if it is aimed at:

  • Batch style workflows that require the user to restart Emacs except for CI & sandbox cases, which must use a lock file

License

The CI files in the project are distributed with the MIT license. For elisp files, Nix expressions, and Guix expressios, only files with GPL3 headers will be accepted. DCO sign-off is mandatory.

Developer Certificate of Origin (DCO)

A copy of the DCO is distributed with this project. Read its text to understand the significance of configuring for sign-off.

Sign-off

A sign-off means adding a “trailer” to your commit that looks like the following:

Signed-off-by: Random J Developer <random@developer.example.org>

GPG signature

A GPG signed commit shows that the owner of the private key submitted the changes. Wherever signatures are recorded in chains, they can demonstrate participation in changes elsewhere and awareness of what the submitter is participating in. The lack of such a proof elsewhere and the presence of a verifiable proof in this repo’s history prevent improper claims of originating source code or introducing relabelled source code.

User setup for submitting changes

Follow these instructions before you get ready to submit a pull-request.

Refer to the Github signing commits instructions to set up your git client to add GPG signatures. File issues if you run into Emacs-specific problems.

Because signing is intended to be a conscious process, please remember to read and understand the Developer Certificate of Origin before confinguring your client to automatically sign-off on commits.

Automatically add sign-off

In magit, set the -s switch. Use C-x C-s (transient-save) to preserve this switch on future uses. (Note, this is not per-project).You can also set the signature flag this way.

Automatic GPG signing with per-project keys

In order to specify which projects you intend to sign with which keys, you will want to configure your git client using path-specific configurations.

Configuing git for this can be done with the following directory structure:

/home/rjdeveloper/
├── .gitconfig
└── .gitconfig.d
    ├── sco-linux-projects.conf
    ├── other-projects.conf
    └── gpg-signing-projects.conf

In your root config, .gitconfig, add an includeIf directive that will load the configuration you use for projects you intend to GPG sign commits for.

[includeIf "gitdir:/home/rjdeveloper/**/gpg-signing/**/.git"]
  path = "~/.gitconfig.d/gpg-signing-projects.conf"

In the gpg-signing-projects.conf add your GPG signing configuration from earlier. sign adds the GPG signature automatically. File an issue if you need help with multiple GPG homes or other configurations.

[user]
  name = "Random J Developer"
  email = "random@developer.example.org"
  signingkey = "5FF0EBDC623B3AD4"

[commit]
  sign = true
  gpgSign = true

Manually signing & adding sign-off

If you don’t like these configurations and want to individually indicate you have read and intend to apply the DCO to your changes, these commands are equivalent:

git commit -s -S --message "I don't like using .gitconfig"

# To clean up a commit
git commit --amend -s -S --no-message

# Combine with rebase to sign / sign-off multiple existing commits
git rebase -i

Shout-outs

  • alphapapa for being super prolific at everything, including package writing, documentation, and activity on various social platforms
  • adisbladis for the Nix overlay that makes the CI and local development so nice
  • FSF for the Yak shaving club
  • NobbZ for being all over the Nix & Emacs interwebs

Footnote on FSF and Emacs Core Licensing

Free Software Foundation currently requires copyright assignment on all code that goes into Emacs core. Many GNU projects have since switched to using a Developer Certificate of Origin. DCO sign-off is a practice accepted by git, GCC, and the Linux Kernel. Doing DCO sign-off is not the same as copyright assignment, and serves a slightlly different purpose. DCO is more defensive of any users while copyright assignment is offensive in the case of GPL non-compliance. In any case, with DCO sign-off, you can be assured that changes submitted to a code base you control are incontrovertibly covered by the license you chose. Using the DCO may make it easier for code in your project to be included in Emacs core later.

About

Emacs interactively installing its own 3rd party dependencies

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published