Don't use on an existing installation. Theoretically, it can work, but theoretically I could become a millionaire.
effuvv
is "functional arch". It allows (and requires) you to express your arch
linux installation in the terms of a function of a configuration. Essentially,
you have a set of configuration files that you would normally edit, such as
/effuvv/etc/fstab and /effuvv/etc/default/grub, and a main config file,
/effuvv/effuvv_config.js
that specifies how everything should be bound together,
and a tool, effuvv-sync
, that makes sure the system reflects your
configuration.
This is super pre-alpha stuff. I'm using it on my systems, but seriously new bugs are being found every day. It's usable, but only if you wanna tinker. Hopefully one day I can remove this warning but I don't know if it'll come. That being said, it's not that scary once you get the hang of it.
NixOS is a big influence in this project. NixOS makes the functional operating system perfect; that is, everything fits perfectly into the NixOS world. The downside of this perfection is that it's not an easy to transfer an existing linux skill set into working NixOS skills.
I wanted a tool that let me express my archlinux system in terms of a single set of configuration files / directives, similar to NixOS, but one that didn't require me to throw away all the time and energy that has gone into building such a great distro like Arch.
Arch Install Guide: https://wiki.archlinux.org/index.php/installation_guide
Follow the Arch Install Guide up to the part about pacstrap
. Do
pacstrap /mnt base
(just base
, nothing more as we want to only have the most
minimal outside-of-effuvv system possible), and then chroot into the new install.
Immediately do this:
# cd /tmp
# curl -L http://effuvv-pkg.shawndellysse.com -o effuvv.tar.xz
# pacman -U effuvv.tar.xz
# rm effuvv.tar.xz
# effuvv-init
Now effuvv is intalled on your system. effuvv-init
has created an extremely
basic skeleton configuration. Now we're going to follow the next few steps of
the install guide, but showing how they're done in effuvv.
First step, let's deviate from the guide for a moment and install vim
. We're
doing this now to both show the packaged installation workflow and also to fix
the fact that Arch sets $EDITOR
to that by default, even though it's not
installed by default. Run
# EDITOR=vi effuvv-edit -c
to open the effuvv configuration in vi
.
Scroll down to the packages
array, and add "vim",
to the end of the array:
exports.packages = [
{external: "effuvv"},
{group: "base"},
//{group: "base-devel"},
"vim",
]
Note the quotes and the trailing comma in "vim",
. The quotes are necessary,
and while the trailing comma isn't necessary, it makes it easier to add more
entries later without having to remember to add the comma to the last entry so
unless you have a strong opinion otherwise I suggest you add it. You can also
take this time to uncomment the base-devel
group if you like. Note the special
syntax for specifying packages groups that should be on the system, and ignore
the external
clause for now, we'll get to that later. (TODO Later Text:
external
clauses mark packages that aren't controlled by this tool. Examples
are effuvv, which was installed from a url, and AUR packages. You need to add
them here so that effuvv doesn't remove them or their dependencies when
effuvv-sync
is ran.)
Close vi
and run effuvv-sync
. This is the only effuvv command that alters your
system. This execution of it should see that you want vim
to be installed, and
right now it isn't, and it should bring the system in line with what your
configuration demands.
The next step in the guide is to append some generated stuff to the fstab. In effuvv, we don't touch anything outside /effuvv or /home (or other state directories), so let's pull fstab into our config.
# effuvv-edit -n /etc/fstab
effuvv-edit
copies a file from the system into the correct place of the config
(in this case it gets placed in /effuvv/etc/fstab
) and lets you edit the file.
In this case, we don't want to edit the file right now, so we pass -n
to tell
it not to run $EDITOR
.
Exit the chroot and append the generated entries to our config:
# genfstab -U /mnt >> /mnt/effuvv/etc/fstab
Re-enter the chroot and check the file.
# effuvv-edit /etc/fstab
Now we need to tell the configuration to install this file. Run
# effuvv-edit -c
and in the install
array of the afterPackages
clause, add the file:
exports.afterPackages = async ({ install, run, changes }) => {
await install([
"/etc/fstab",
])
}
This goes in the afterPackages
clause due to it not being something that
pacman needs set up to run. Generally, most file installs will go here, with
only things like pacman configs, networking, timezones / locales, etc will go
in the beforePackages
clause.
Next step in the guide is to set the time zone. For this, we don't want to copy a file from the system:
# ln -sf /usr/share/zoneinfo/Region/City "$( effuvv-edit -d -p /etc/localtime )"
This line creates a symlink to the City you specify into the right location in
the configuration. effuvv-edit -d -p
's -d
doesn't actually copy the file from
the system but instead just gets the configuration's directory structure ready,
and the -p
prints the location of where the file would be in the configuration
instead of attempting to edit it. We combine this with shell subsitition for
nicety.
Don't forget, we need to add it to the beforePackages
clause:
exports.beforePackages = async ({ install, run, changes }) => {
await install([
"/etc/pacman.conf",
"/etc/pacman.d/mirrorlist",
"/etc/localtime",
])
if (changes.files["/etc/localtime"]) {
await run("hwclock --systohc")
}
}
What's this if-clause you ask? Looking at the Arch install guide, after
installing /etc/localtime
, you want to run a command. This if-clause checks to
see if the file got installed (it will get installed this first run, but it
won't get re-installed on subsequent runs unless the file in the configuration
is changed).
Let's repeat the same process for the locale. I'm going to do less hand-holding here now that we have the pattern down.
Copy the file to the configuration and make your changes:
# effuvv-edit /etc/locale.gen
# effuvv-edit /etc/locale.conf
Update the beforePackages
clause:
exports.beforePackages = async ({ install, run, changes }) => {
await install([
"/etc/pacman.conf",
"/etc/pacman.d/mirrorlist",
"/etc/localtime",
"/etc/locale.gen",
"/etc/locale.conf",
])
if (changes.files["/etc/localtime"]) {
await run("hwclock --systohc")
}
if (changes.files["/etc/locale.gen"]) {
await run("locale-gen")
}
}
Note that if the file doesn't exist on the system, effuvv-edit
will create one
in the configuration:
# effuvv-edit /etc/hostname
# effuvv-edit /etc/hosts
Add both of these files to the beforePackages
clause.
Should be able to tackle this yourself now
How I handle this is like this, if you need to have a custom initramfs:
# effuvv-edit /etc/mkinitcpio.conf
afterPackages
clause looks something like this:
exports.afterPackages = async ({ install, run, changes }) => {
await install([
// stuff...
"/etc/mkinitcpio.conf",
])
if (changes.files["/etc/mkinitcpio.conf"]) {
await run("mkinitcpio -p linux")
}
}
This way the initramfs will only be rebuilt if you change the source file.
Okay this is pretty cool. Included in effuvv is a tool that generated encrypted
password strings, effuvv-mkpasswd
. Protip: to get that big string into the
configuration without copy and paste, close your editor and do:
# effuvv-mkpasswd >> /effuvv/effuvv_conf.js
# effuvv-edit -c
The password hash will now be at the end of the file. Move that string up inside the quotes in the right stanza to set the password of a user.
exports.usersAndGroups = {
users: {
root: {
encryptedPassword: "sdfhuyiurwyeiurywuioeryuioqyuiysdouivyxuozicyvuoiysu",
},
shawn: {
encryptedPassword: "wuqeporiuqicxuvppisaufwepiuvpuzxpioupwe",
groups: [ "wheel" ],
},
},
}
effuvv-sync
will then make sure that the users in the system match the users in
the configuration.
Grub EFI example:
exports.packages = [
// stuff...
"grub",
"efibootmgr",
]
exports.afterPackages = async ({ install, run, changes }) => {
// stuff...
if (changes.packages.added["grub"]) {
await run("grub-install --target=x86_64-efi --efi-directory=/boot --bootloader-id=grub")
await run("grub-mkconfig -o /boot/grub/grub.cfg")
}
}
This will install grub
package and on package-install, install grub as the
boot loader. This can be easily extended to have a grub config file managed by
the configuration, and have these commands run whenever it's installed or those
are changed as well:
exports.afterPackages = async ({ install, run, changes }) => {
await install([
// stuff...
"/etc/default/grub",
])
if (changes.packages.added["grub"]) {
await run("grub-install --target=x86_64-efi --efi-directory=/boot --booloader-id=grub")
}
if (changes.files["/etc/default/grub"]) {
await run("grub-mkconfig -o /boot/grub/grub.cfg")
}
}
# EDITOR="visudo -f" effuvv-edit /etc/sudoers
To start and enable-at-start a service, add it to the services
clause:
exports.services = [
"NetworkManager.service",
"lightdm.service",
]
To stop and disable-at-start a service, remove it from the clause.