Spellbook: An Elixir story
Introduction
Lately, I have been wanting to finally wanting to dive deep into a dynamic language. In the past, I have written a little about Ruby because of Ruby On Rails. While I do like Ruby on Rails, I am not exactly a huge fan of Ruby. It is not bad, but it will still missing something. I have heard quiet a bit about Elixir, but never thought about it until recently. It is a dynamic language like Ruby, but with more a more functional nature. There is also some overlap in the Rust and Elixir fans. So I decided to give it a go being since I do like Rust and Swift, with Swift having some overlap in language features and concept with Rust. Also, I had heard some great things about Phoenix, a web framework inspired by Ruby on Rails, but in Elixir.
At first glance, Elixir looked pretty nice. But you can’t really get a feel for a language until you actually start to use it for something. So, being as there is homebrew written in Ruby, I decided to embark on a project to make a little system package manager in Elixir.
Why a package manager?
First a foremost, it has been something I’ve wanted to try my hand at for a long time. Since I was a young lad trying to bumble my way through LFS (Linux From Scratch). Outside of just doing apt install
or yum install
or pacman -Ss
, I hadn’t looked too deeply in package management at the time until LFS and also subsequently checking out Slackware. But that was many moons ago. But, seems like a fun project for Elixir.
Also, as noted, I have a little experience packaging software. In the past, I packaged, or wrote a port in the ports tree, for a few packages in Ravenports (I have some articles about this). Not too long ago, I packaged comtrya for homebrew. You can learn a lot about software development and software by packaging. Just seems like the next level to go for learning more about packaging and distributing software on UNIX-y systems is to now write my own package manager.
In comes spellbook
. A witch-y theme package manager written in elixir.
What do I want it to do?
Alright, so now, what does it need to do? Well quiet a bit of it, for anyone with experience using a system package manager, is obvious. It needs to install, uninstall, and search for software. That is kind of the low hanging fruit, so I won’t spend much time on that. But specifically how do I want it to do these things?
Right now, I have mostly only thought through the installing. I want to have a ports tree. For those not familiar, a ports tree is a directory of recipies for building packages from source. These recipies cover things like downloading a package’s source code and then building it from source. So right now, I am ignoring installing binaries from a repository or grabbing binaries from github or other source. I am putting that off to later. So right now, it will be limited to building from source.
Okay, homebrew kind of does this! First, so what. Technically this is a “competitor” with homebrew, but I am mostly doing this out of fun and for learning. But, there is a future feature that I want to support that it doesn’t look like homebrew has. But, it is a feature of Ravenports. As a side tangent, I would love to use Ravenports on macOS (my daily driver now), but it can’t support any newer versions because Ravenports relies on nullfs, which is no longer very viable on macOS. At least that is what I remember when I was looking into a few years ago. But that feature is the concept of variants.
Variants in Ravenports are like supporting multiple versions of the same package, but with different feature sets. For those familiar with ebuilds, portage, and gentoo, variants in Ravenports is sort of like customizing your FLAGS
. For an example for those not familiar, lets take a package like git. If you build git in the most basic way possible, you get the full package. But if you change some build variables, you can get a lighter version of git. In Ravenports, you can get the git-full
or git-lite
package. Its your choice. Another example is less
. You can build less so it uses colors in it’s output, or for less
to be colorless. I want to eventually support this idea of variants. But that is a future feature.
What does a port look like
Before going further, lets take a look at a current working sample of a port definition. Or, as it is called in spellbook
(a package/port), a spell definition.
defmodule Htop do
@behaviour Spellbook.Spell
@impl true
def name(), do: "htop"
@impl true
def version(), do: "3.4.1"
@impl true
def homepage(), do: "https://htop.dev"
@impl true
def license(), do: "GPL-2.0"
@impl true
def type(), do: "bin"
@impl true
def checksum(), do: ""
@impl true
def deps(), do: []
@impl true
def source(), do: "https://github.com/htop-dev/htop/archive/refs/tags/3.4.1.tar.gz"
@impl true
def install(args) do
System.cmd("sh", ["autogen.sh", "--prefix=#{args.prefix}"], cd: args.cwd)
System.cmd("sh", ["configure", "--prefix=#{args.prefix}"], cd: args.cwd)
System.cmd("make", [], cd: args.cwd)
System.cmd("make", ["install"], cd: args.cwd)
:ok
end
end
Each spell is a module and it conforms to the Spellbook.Spell
behavior, which is sort of like an interface. Quiet a bit of it is just package information like the name, version, license information, and where to get the source.
Then a spell definition needs to implement an install/1
function, these are the build instructions for building the software.
Building the port
To build a port right now, a function is ran that does a few things. First, it creates the ‘environment’ for building. It does this by creating a temp directory and downloding the package there, then it extracts it’s contents. Afterwards, it will construct some environment variables and also create the information that will be injected into install/1
via the args
parameter, then the method is called. The method executes it’s instructions. Returning from that, need to ‘install’ it into the right place(s). So this would be some bin
folder as an example, which it does by iterating over the build artifacts and creating symlinks to the appropraite spots. That is atleast the tldr of it. So far at least.
Output from a build
~/ElixirProjects/spellbook/ [main*] ./spellbook cast ~/ElixirProjects/StandardBookOfSpells/htop.exs
Casting spell: /Users/toddmartin/ElixirProjects/StandardBookOfSpells/htop.exs
Warming up to perform cast...
Casting /Users/toddmartin/ElixirProjects/StandardBookOfSpells/htop.exs
Build directory: /var/folders/vf/7r2g86991c788ppg6zq147r80000gn/T/htop-3.4.1
Now on to something else
Downloading https://github.com/htop-dev/htop/archive/refs/tags/3.4.1.tar.gz to /var/folders/vf/7r2g86991c788ppg6zq147r80000gn/T/htop-3.4.1/3.4.1.tar.gz
22:13:14.923 [debug] redirecting to https://codeload.github.com/htop-dev/htop/tar.gz/refs/tags/3.4.1
Download complete
Extracing /var/folders/vf/7r2g86991c788ppg6zq147r80000gn/T/htop-3.4.1/3.4.1.tar.gz....
Extraction complete: /var/folders/vf/7r2g86991c788ppg6zq147r80000gn/T/htop-3.4.1
autoreconf: export WARNINGS=all
autoreconf: Entering directory '.'
autoreconf: configure.ac: not using Gettext
autoreconf: running: aclocal --force
autoreconf: configure.ac: tracing
autoreconf: configure.ac: not using Libtool
autoreconf: configure.ac: not using Intltool
autoreconf: configure.ac: not using Gtkdoc
autoreconf: running: /opt/homebrew/Cellar/autoconf/2.72/bin/autoconf --force
autoreconf: running: /opt/homebrew/Cellar/autoconf/2.72/bin/autoheader --force
autoreconf: running: automake --add-missing --copy --force-missing
autoreconf: Leaving directory '.'
Successfully installed htop
Linking...
[lib/spellbook/linker.ex:12: Spellbook.Linker.link_spell/2]
base #=> "/opt/spellbook/Spells/htop/3.4.1"
Creating symlink /opt/spellbook/Spells/htop/3.4.1/bin/htop -> /opt/spellbook/bin/htop
Creating symlink /opt/spellbook/Spells/htop/3.4.1/share/man/man1/htop.1 -> /opt/spellbook/share/man/man1/htop.1
Creating symlink /opt/spellbook/Spells/htop/3.4.1/share/pixmaps/htop.png -> /opt/spellbook/share/pixmaps/htop.png
Creating symlink /opt/spellbook/Spells/htop/3.4.1/share/icons/hicolor/scalable/apps/htop.svg -> /opt/spellbook/share/icons/hicolor/scalable/apps/htop.svg
Creating symlink /opt/spellbook/Spells/htop/3.4.1/share/applications/htop.desktop -> /opt/spellbook/share/applications/htop.desktop
Done casting...
As shown above, the build artifacts are in /opt/spellbook/Spells/htop/3.4.1
, following a conventions of /opt/spellbook/Spells/<PACKAGE_NAME>/<VERSION>
. When symlinking, it will see that there is a bin
directory in /opt/spellbook/Spells/htop/3.4.1/bin
. It will iterates over the files there and create symlinks in /opt/spellbook/bin
.
Going full witch-y with it
Here is a the help menu from spellbook
.
~/ElixirProjects/spellbook/ [main] ./spellbook
Magical system package manager 0.0.1
Todd Martin
A magical system package manager
USAGE:
spellbook
spellbook --version
spellbook --help
spellbook help subcommand
SUBCOMMANDS:
cast Cast (install) a spell (package)
dispel Dispel (uninstall) a spell (package)
scry Search for a spell
grimoire List casted spells
reveal Reveal information about a spell
renew Renew your spellbook shelf
empower Upgrade a casted spell
As shown above, to install
we cast
. To list all casted spells, we use the subcommand grimoire
which will list all of our casted spells.
~/ElixirProjects/spellbook/ [main] ./spellbook help cast
Cast (install) a spell (package)
USAGE:
cast SPELL
cast --version
cast --help
ARGS:
SPELL Package
And this is what the /opt/spellbook
directory looks like.
/opt/spellbook/ ls
bin etc lib sbin share Shelves Spells
Conclusion
This is still very early days with a lot more left to implement. But all in all, I am enjoying my time with Elixir and as I get further in, I plan to post more about this project. And as I ‘live’ in Elixir a little but more, I will post more about my experience with that. Afterall, it is hard to really know if you like something until you’ve lived in it for a bit.