+The Linux boot sequence can be seen as a bit convoluted, as multiple options exist, and the more recent advent of UEFI has shuffled this space again. While GRUB is still a very popular option, especially since it is compatible with both traditional BIOS boot and more recent UEFI firmwares, it is not the only one.
+
+But the step of going through a third party software to boot your Linux kernel is not a compulsory step anymore. Linux kernels are able to boot as standalone UEFI images for a while now, and so as long as you had a firmware capable of providing your boot arguments, you could bypass the bootloader stage. Even more recently, Linux now has something called *UKI*, or “Unified Kernel Image”. It is a way of taking a Linux kernel, its initramfs, and its command line, and make it a truly standalone bootable EFI image that can be simply booted from anything capable of booting EFI images.
+
+Arch Linux is a generic distribution geared towards tinkerers. It notably has no defaults in regard to its bootloader. And it fully supports (albeit with a bit of tinkering) booting from an UKI image.
+
+## Prerequisites
+
+We are going to make an Arch Linux installation boot from UEFI. This can be done either from an existing installation, or during the initial installation process. The only true requirement is to be on an UEFI-compatible machine and [booting into this mode](https://wiki.archlinux.org/title/Installation_guide#Verify_the_boot_mode). Of course, this also means having an [UEFI boot partition](https://wiki.archlinux.org/title/EFI_system_partition). In this article, I have mounted mine at `/boot/efi`, although some installs mount it directly into `/boot`. It does not matter much, but you might have to correct some paths if it is the case for you.
+
+This guide will also show how to make it compatible with [Secure Boot](https://wiki.archlinux.org/title/Unified_Extensible_Firmware_Interface/Secure_Boot). Secure Boot is a bit of a problematic feature in the Linux community, and some people prefer to leave it out. I am not going to argue for or against it, I am just presenting how to make it compatible, but this part is completely optional. However, if you want to go with Secure Boot, you need to boot your machine with Secure Boot enabled and in Setup mode.
+
+<Note>
+ Arch Linux now provides multiple choices for the [initramfs](https://wiki.archlinux.org/title/Arch_boot_process#initramfs). [mkinitcpio](https://wiki.archlinux.org/title/Mkinitcpio) has been the historical and still default choice today, but it is also possible to use [dracut](https://wiki.archlinux.org/title/Dracut) or [booster](https://wiki.archlinux.org/title/Booster). dracut supports making an UKI image directly, and there is `systemd-ukify` that can make an UKI image from separate kernel and initramfs. However, this article will focus on mkinitcpio.
+</Note>
+
+## Making the installation
+
+### mkinitcpio
+
+On your existing system, or on a system at the bootloader phase of the installation, we will need to configure mkinitcpio to write to an UKI image. Ensure the `mkinitcpio` package is installed. Then, we will edit the preset file for mkinitcpio, located at `/etc/mkinitcpio.d/linux.preset` (or a similar name depending on your kernel flavour of choice like `linux-lts.preset`). Disable the classical initramfs generation by commenting the `default_image` and `fallback_image` options, then enable UKI generation by uncommenting the `default_uki` and `fallback_uki` options. Ensure the paths are correct. For a UEFI boot partition mounted at `/boot/efi`:
+Finally, mkinitcpio expects the parent folder to already exist which is not by default the case. Use `mkdir` to ensure the folder is created.
+
+### Command Line
+
+In a traditional Linux setup, the kernel options (or _command line_) is stored and passed by the bootloader, and usually configured there. Here, we have no bootloader, but still need to provide the command line options.
+
+The UKI build process assemble the command line from files in `/etc/cmdline.d`. You can create `.conf` files in this folder containing command line arguments (which also allows you to group related arguments together). Simplest example is to create a `/etc/cmdline.d/root.conf` with the root configuration:
+
+```
+root="/dev/vda2" rw
+```
+
+Of course, adapt it to your configuration and needs.
+
+### Building the image
+
+Now that things are configured, you can just build the image like you do traditionally:
+
+```
+mkinitcpio -P
+```
+
+The process should end without errors, and you should find the new files at the location indicated above.
+
+### Registering
+
+UEFI boot programs are normally registered into the firmware settings as boot entries. If we want to boot our kernel, we need to register our kernel in the firmware with `efibootmgr`. First, ensure the `efibootmgr` package is installed.
+efibootmgr --create-only --disk=/dev/vda --part=1 --label="Arch Linux Fallback" --loader='\EFI\Linux\arch-linux-fallback.efi'
+```
+
+Note that the second command for the fallback image is created with `--create-only` instead of `--create`. This way, it will not be booted automatically, but still be available as a boot option.
+
+### Rebooting
+
+Everything is set up! Now, you can finalise the installation steps (if applicable) and reboot your machine, then check that your system boots directly into Arch Linux. You can also trigger the UEFI boot menu to check for the additional entry for the fallback image.
+
+Congratulations! You now boot entirely through your UKI image.
+
+## Secure Boot
+
+<Note>
+ As said above, this step is optional, if you do not want to use Secure Boot.
+</Note>
+
+Secure Boot is an addition to UEFI that ensures that your boot sequence has not been tampered with, by requiring all boot images to be signed with a valid certificate. By default, machines have two Microsoft-provided certificates, one for Windows, one for third party systems, the latter being used by some distributions to provide some level of support to Secure Boot. You can also register your own certificate into the machine to sign your own binaries, which is what we are going to set up here. Once setup correctly, the signing process is automatic.
+
+Install the `sbctl` package. Ensure that your machine has Secure Boot is in setup mode by issuing `sbctl status`:
+
+```
+Installed: ✓ sbctl is installed
+Owner GUID: 00000000-0000-4000-0000-000000000000
+Setup Mode: ✗ Enabled
+Secure Boot: ✗ Disabled
+Vendor Keys: none
+```
+
+Note: `sbctl` might also indicate that it is _not_ installed, but this will correct itself in the next steps.
+
+If it is not, you will have to reboot your machine into the firmware interface to enable it. Some firmware will go into Setup mode by choosing to erase all certificates.
+
+`sbctl` is a nice command to simplify the process of dealing with Secure Boot. To create a certificate and record it into our firmware, we issue the following commands:
+
+```
+sbctl create-keys
+sbctl enroll-keys -m
+```
+
+<Note level="warning">
+ `-m` tells `sbctl` to also insert the standard Microsoft certificates, which allows to boot Windows. Even if you plan on never booting Windows, some systems might have UEFI-drivers to load on boot that use one of these certificates. Not including them could break your boot sequence. You can technically do without, but be sure of what you are doing there.
+</Note>
+
+Now that we have our certificates, we need to sign our UKI images. Fortunately, `sbctl` also includes a hook for `mkinitcpio` to do it for you. You should just have to regenerate images with `mkinitcpio -P`. Once this done, ensure it is all good with `sbctl verify`:
+
+```
+Verifying file database and EFI images in /boot/efi...
+✗ /boot/efi/EFI/Linux/arch-linux.efi is not signed
+✗ /boot/efi/EFI/Linux/arch-linux-fallback.efi is not signed
+```
+
+If you have static UEFI binaries, like the UEFI Shell or a third party bootloader like rEFInd, you will have to sign those too. They should be given to you by `sbctl verify`. For each binary that is to be signed, issue the following command:
+
+```
+sbctl sign -s /boot/efi/EFI/…
+```
+
+The `-s` flag tells `sbctl` to remember this path. In the future, if you modify any of those files, you can just issue `sbctl sign-all` to check and resign every known file.
+
+<Note level="warning">
+ Ensure that `sbctl verify` validates your boot files before rebooting! If you reboot without signing your files, your system will refuse to boot, now that the certificates are registered.
+</Note>
+
+You can now reboot your machine to check that it still boots. Once you reboot, `sbctl status` should tell you that all is well:
+
+```
+Installed: ✓ sbctl is installed
+Owner GUID: 00000000-0000-4000-0000-000000000000
+Setup Mode: ✓ Disabled
+Secure Boot: ✓ Enabled
+Vendor Keys: microsoft
+```
+
+## Bonuses
+
+While this is enough for a functional setup, I have some personal tweaks to this that may be useful to you.
+
+### Automatic root decryption
+
+<Note>
+ Requires Secure Boot, a [TPM](https://wiki.archlinux.org/title/Trusted_Platform_Module), and a LUKS 2 encrypted root.
+</Note>
+
+If your root partition is encrypted, you have to enter the passphrase at every boot. If you have a TPM, you can use it to automatically provide this passphrase to the system. While this seem like a downgrade in security, your system is normally still password protected at the login prompt, so your data should be safe, but it is your choice between more security and ease of use. However, it becomes dangerous if coupled with auto-login!
+
+The boot process and the TPM will ensure that your system has not been tempered with. It means that it will compare Secure Boot certificates at boot and will refuse to use the saved credentials if they are modified, which is even stronger than the base Secure Boot check.
+
+<Note>
+ This does not save your passphrase in the firmware. It uses a different LUKS slot with its own data saved into the TPM. The safety of the passphrase itself is not affected.
+</Note>
+
+systemd provides the `systemd-cryptenroll` command to make the process easy. First, check that your TPM device is detected:
+
+```
+systemd-cryptenroll --tpm2-device=list
+```
+
+If it is visible, you can then use it with your root partition:
+
+```
+systemd-cryptenroll --tpm2-device=auto /dev/vda2
+```
+
+Replace `/dev/vda2` by your root partition physical device (the encrypted layer, not the `/dev/mapper` mapping). It should ask you for the current encryption passphrase before registering the new TPM slot.
+
+Reboot, and you should not have to input your passphrase anymore.
+
+Note: Security of this depends on what is allowed to boot on your machine. I would advise to lock down your UEFI settings with a password, including the boot menu if possible, making changing the default boot device impossible for an external person. Of course, they can reset the firmware with a physical access, but that would also reset the boot certificates and make the TPM decryption impossible. If you still keep a bootloader, be careful of what it allows to load.
+
+### rEFInd
+
+If you have another system you often use besides your Arch Linux, you may want to still keep a bootloader, depending on how tedious your UEFI firmware is to override the default boot setting. rEFInd is a generic UEFI bootloader, but a very capable one, as well as providing a graphical user interface to choose where to boot from.
+
+While rEFInd will find and boot your Linux configuration in the current state, a few tweaks will improve the experience a bit.
+
+First step is to install the `refind` package, then run its built-in script to make the installation automatically:
+
+```
+refind-install
+```
+
+Now, we need to tweak mkinitcpio configuration a bit. If you already have your UKI images generated at this point, I would advise you to remove completely the `/boot/efi/EFI/Linux` folder.
+
+rEFInd has some automatic heuristics to try to guess what kind of system is an image for to display an appropriate icon. However, it seems to not be good with UKI images (yet), but we can help it. If it cannot determine the type of image, it will use the name of the parent folder as a hint. In the current configuration, it will see the `Linux` folder, and will display a generic Tux icon. If you want to have Arch Linux branding, we need to rename the folder to `arch`. Go back to the `linux.preset` file from earlier, and change the two lines to replace `Linux` with `arch`. Do not close the file yet, we have another change to make.
+
+For distributions that may install different kernel versions at the same time, rEFInd will try to determine which version is the latest to use it as default. However, it gets a bit lost with Arch Linux and believes that the default and fallback images are of the same system, but since no kernel version is given in the file name, it will fall back to the file creation date to determine the newer (and default) kernel. By default, mkinitcpio creates the default image first, and the fallback one in second, which, in our case, would make the fallback image the default.
+
+There is a little trick to solve this: make mkinitcpio build the images the other way around. In the `.preset` file, simply switch the presets around:
+
+```bash
+PRESETS=('fallback' 'default')
+```
+
+You can now close the preset file. Recreate the parent folder with `mkdir`, then run `mkinitcpio -P` to re-create the images in the right order.
+
+<Note>
+ If you have Secure Boot set up, do not forget to sign its image with `sbctl`.
+When writing the tooling to generate this blog, I picked up Gulp as a task executor/file processor. However, with the development, I had some issues, including incompatibility with Deno that I wanted to move onto. Gulp streams reminded me of another library I used in the past, RxJS. I decided to see how I could use it to replace Gulp with a bit of custom code, and it was rather easy.
+
+It was a nice practical exercise, and made me learn more about how RxJS works. I focus myself here on the file processing aspect, but things shown here are valid for RxJS in other workflows too. On the other hand, I will be focusing more on the file processing/stream aspect here than the task runner aspect of Gulp, as RxJS is not relevant for that part.
+
+<Note>
+ As this was made to move into Deno, this article is all based on Deno. Deno is a new JavaScript runner that bundles a lot of nice features like built-in TypeScript support. While it should be mostly compatible with the classical NodeJS environment, there will be some adjustments to do to match with it, including using a `package.json` instead of using URLs in `import` statements, and some library mappings from `@std` to their node equivalent.
+</Note>
+
+## The basics
+
+Gulp is a JavaScript tool that allows to write and execute tasks to transform files into others by applying a series of stream transformations. Tasks basically look like that:
+Gulp is leveraging object streams of Node.js, passing instance of `vinyl` objects (virtual files) along different transformers. Gulp has a wide array of “plugins” available usable as transformers. In the case you need something custom, you can simply leverage `Transformer`.
+
+RxJS is a library that supports a programming style called “Reactive programming”. Basically, it provides data streams and a wide array of transformer functions to work with these streams. The stream format and intermediate transforms seemed to be replicating Gulp streams well enough to be a suitable replacement.
+
+## Sourcing files
+
+The first thing we will need to replicate is the `gulp.src` function. It is a function that takes a glob pattern, matching files and creating a stream of virtual files from them. For the files, it leverages `vinyl` to provide for virtual files. These objects contain a path, file contents, and history of paths. Transformers work on these files, altering the contents and paths at will.
+
+To do the glob step, we can simply rely on the `glob` library. Its `Glob` class is directly pluggable into RxJS streams:
+
+```ts
+import {Glob} from "https://esm.sh/glob@^10.4.2";
+`vinyl` is perfectly usable outside the Gulp environment, but in this I will favour the `vfile` library, notably because it is based on `UInt8Array` instead of `Buffer` as file contents, which play better with modern JavaScript and especially Deno.
+
+So now that we have an observable of paths, we need to actually load those files into `VFile` objects. This can simply be done by inserting a `map` operation in our stream:
+
+```ts
+import {Glob} from "https://esm.sh/glob@^10.4.2";
+import {from, map} from "https://esm.sh/rxjs@^7.8.1";
+import {readSync} from "https://esm.sh/to-vfile@^8.0.0";
+import {resolve} from "jsr:@std/path@^0.225.2";
+
+from(new Glob("**/*.ts", {}))
+ .pipe(map((file) => readSync(resolve(file))))
+ .subscribe((file) => console.log(file));
+```
+
+I leveraged the `to-vfile` library that provides helpers to bridge virtual files to the actual filesystem. The `readSync` method will take an absolute path (that we get through `resolve`) and returns a `VFile`.
+
+## Going async
+
+Modern JavaScript leverages promises a lot, and RxJS is completely compatible with them. Actually, you can see Promises as streams with a single element, and that is exactly how RxJS handles them. So RxJS helpers that handle observables can also be called transparently with promises.
+
+In our previous example, I used `readSync` that does a blocking operation, but we can totally make it async using the `read` function. However, `map` is not fit for this, as using it with a function that returns a promise will create a stream of promises (`Observable<Promise<VFile>>`), while we want to continue working with virtual files directly. For this, there is the `mergeMap` operator that takes functions returning streams and merge them into a single stream. Since promises are valid streams, we can directly use this.
+
+```ts
+import {Glob} from "https://esm.sh/glob@^10.4.2";
+import {from, mergeMap} from "https://esm.sh/rxjs@^7.8.1";
+import {read} from "https://esm.sh/to-vfile@^8.0.0";
+import {resolve} from "jsr:@std/path@^0.225.2";
+
+from(new Glob("**/*.ts", {}))
+ .pipe(mergeMap((file) => read(resolve(file))))
+ .subscribe((file) => console.log(file));
+```
+
+Not much is changed, but we are now using async code, which can provide a performance boost in some workflows.
+
+`map` and `readMap` are going to be the primary operators to work with to do transformations, depending on if your transformation is `async` or not.
+
+We can go one step further and make our code a little bit more reusable. The combination of `Glob` and `readSync` can be abstracted away in a simple function. Observables are fully composable, and `.pipe` can be called several times if needed, yielding a new observable. So let us make a function to create our source observable, mimicking the origin `src` from Gulp:
+
+```ts
+const src = (globs: string | string[]) =>
+ from(new Glob(globs, {})).pipe(
+ mergeMap((file) => read(resolve(file))),
+ );
+```
+
+Now that we have a `src` function, we can use it to simplify our pipeline:
+We now need to have the counterpart to `src`, `dest`, in order to write our files. This time it requires a bit more code, but nothing too difficult. We simply need to handle file writing at the right location.
+We declare a simple `dest` function that takes a prefix, which will be where all files will be output. It returns a new function that has the right prototype to be fed into our pipeline through `mergeAll`.
+We then resolve the final path where to write the file. We have an absolute path that we transform into a relative path, then again into an absolute path injecting our prefix in to obtain the final filename. Note however that `VFile` can also have a relative path, but for this simple example this is enough.
+ await Deno.writeFile(finalPath, file.value as UInt8Array);
+```
+
+The next part of the function simply ensures the parent directory is created, then uses `writeFile` to output the file data. Once again, I take a small shortcut for demonstration by using `as UInt8Array`, as `file.value` can also be a `string` but I am ignoring this case for the moment.
+
+```ts
+ console.log("Written", finalPath);
+ return file;
+}
+```
+
+The end of the function simply logs that we wrote the file, then returns it in case we want to do more stuff down the pipeline.
+
+We also need to update our `src` function a bit: by default, `VFile` will set its `cwd` to `/`, while we want to have it set to the actual current working directory. This can be done simply with a `tap` operation:
+
+```ts
+const src = (globs: string | string[]) =>
+ from(new Glob(globs, {})).pipe(
+ mergeMap((file) => read(resolve(file))),
+ tap((file) => file.cwd = Deno.cwd()),
+ );
+```
+
+You can then use `mergeMap` to inject this function into our pipeline. Here is the complete file:
+
+```ts
+import {VFile} from "https://esm.sh/vfile@^6.0.1";
+import {Glob} from "https://esm.sh/glob@^10.4.2";
+import {read} from "https://esm.sh/to-vfile@^8.0.0";
+import {from, mergeMap, tap} from "https://esm.sh/rxjs@^7.8.1";
+import {dirname, relative, resolve} from "jsr:@std/path@^0.225.2";
+ await Deno.writeFile(finalPath, file.value as Uint8Array);
+
+ console.log("Written", finalPath);
+ return file;
+};
+
+src("**/*.ts")
+ .pipe(mergeMap(dest("../dist")))
+ .subscribe({});
+```
+
+Note that I use `.subscribe({})` to make the pipeline process as it is lazy by default.
+
+## Make it a promise
+
+If we want to have tasks that we can chain, we need to know when the task is finished. By default, subscribing to a stream just has it running in the background without way of tracking it. So we are going to make it into a promise.
+
+RxJS provides a `.toPromise()` method on the pipeline, but this function is deprecated in favour of using the `lastValueFrom`. This gives you a promise that resolves to the last `VFile` processed.
+
+I am going to use a slightly longer way but a bit cleaner for me, that returns a proper `Promise<void>`. We are going to pair the `Promise` constructor with the `subscribe` method.
+ new Promise((complete, error) => observable.subscribe({ complete, error }));
+
+runPipeline(
+ src("**/*.ts").pipe(mergeMap(dest("../dist"))),
+).then(() => console.log("Done."));
+```
+
+The `runPipeline` function we define is rather simple: It creates a promise, and binds the `resolve` and `reject` (here named `complete` and `error` to correspond to RxJS naming) functions passed by the promise constructor into a subscription to the stream, then returning the new promise.
+
+Using `runPipeline` you can turn any RxJS pipeline into an await-able task.
+
+## Inject files
+
+For now, we have 1:1 transformations, but sometimes, we want to inject new files during the pipeline, for example a file name mapping manifest or assets included by a file.
+
+We already have the tools for this here. For now, we used `mergeMap` to return a `Promise<VFile>`, but it handles returning an `Observable<VFile>` just as easily.
+
+As a first example, we are going to emit a copy for every file we are passed:
+
+```ts
+const copyFile = (file: VFile) =>
+ new Observable<VFile>((subscriber) => {
+ subscriber.next(file);
+
+ const copy = new VFile(file);
+ copy.stem = copy.stem + "-copy";
+
+ subscriber.next(copy);
+ subscriber.complete();
+ });
+```
+
+This function creates a new observable to which it passes the given file unmodified, then creates a copy, changes its path, and passes it to the subscriber. It ends by completing the observable.
+
+You can insert `mergeMap(copyFile)` in the `pipe` before `dest` to create these clones.
+
+For a slightly more complicated example, we are going to emit a list of all processed files in a manifest file. For this, we need to write a custom operator for RxJS, as just using `mergeMap` will not allow us to do this.
+RxJS operators like `mergeAll` are functions that take an `Observable` and return another `Observable`, which is what the `manifest` function written here returns. We simply create a new observable, and use `subscribe` on the source observable to insert our functions. In `next`, which is called for each item, we just add the file path to a local `Set`. Then, `complete` is called on completion (after the last element), in which we inject a new `VFile` into the `subscriber` before completing it.
+
+You do not need `mergeMap` to use this operator:
+
+```ts
+src("**/*.ts")
+ .pipe(
+ manifest("manifest.json"),
+ mergeMap(dest("../dist")),
+ );
+```
+
+If you run it with the previous code, it will not work, as we hit both noted limitations of our previous `dest` function. Here is an updated version:
+It has been modified to support relative paths, as well as `VFile` objects that are backed by a `string` instead of a `UInt8Array`, using `TextEncoder` to convert the string.
+
+And that’s it! This reduced set of tool should be enough to go through files and apply all sorts of transformations. Privilege existing RxJS operators if you can, but if you need, custom operators are available for the most complex use cases.
+
+## Closing notes
+
+Replacing Gulp’s streams with RxJS ones was not straightforward, and it is expected, as Gulp is specialised on the exact task of file transformation, and RxJS is a more generic stream tooling. However, it is still rather easily to do. RxJS is incredibly powerful and this article barely touches what it can actually do with streams, especially since we work on a very short execution time, while RxJS is also made for long-running applications, to handle, transform and re-dispatch events.
+By default, macOS’s filesystem, either HFS+ or APFS, are case-insensitive. It means that the system will treat names with different casing as being the same, for example `taxes` and `TaXEs` being the same file. You cannot have both in the same directory.
+
+However, a lot of programming tooling is aimed towards case-sensitive filesystems, which is the standard under UNIX-based systems line Linux. This can lead to some issues if you have projects where people under Linux and macOS work together: creating two git branches with different casing can wreak havoc on macOS clients. The same way, something can work on macOS and fail on Linux because it relies on the case-insensitive behaviour.
+
+If you are an advanced user, it is possible to install macOS on a case-sensitive system: APFS and HFS+ both support a case-sensitive variant, and macOS itself works well on that configuration too. However, by experience, I would not recommend it at all, as while macOS itself does not mind, some software for macOS do mind, written for a case-insensitive file system. Some software, like Adobe products, will flat-out refuse to install on such a filesystem.
+
+APFS introduced another feature to the file system that is very useful for this: **Volumes**. A partition can hold several volumes, which are shown as separate drives, like if they were separate partitions, however they all share the same disk space, so you do not have to divide your available space in a rigid distribution like you would have to do with a separate partition.
+
+Making a volume for development files is very easy. Open the Disk utility app, select your root container, and select the “Add APFS Volume…” option or click the `+` icon in the toolbar. A new dialog will ask you to name your volume and select the volume format. Open the format list, and select one of the “Case-sensitive” option. If you elect to encrypt the volume, it will ask for a password that macOS will then save in your keychain to automatically unlock, so you do not have to enter this passphrase at each boot. You can also click the “Size options” button if you want to assign a maximum quota to your volume if you want macOS to impose a strict limit on the volume size. Then click on “Add”, and your new volume should appear in the Finder, mounted as a generic disk drive.
+
+
+
+You can access it by its mount point under `/Volumes`, and, as a disk drive, it should appear in the Finder sidebar permanently for easy access. You can give it a custom icon like any other drive by pasting an icon into the one found in its Info window for maximum customisation.
+
+Inside this folder, case-sensitive rules apply, making it great to store your programming projects, while standard case-insensitive rules continue to apply to the rest of the system.
+description = "macOS's case-insensitive filesystem can sometimes be a problem to developpers. Hopefully, there is a nice trick to get a case-sensitive haven for your developer needs."
+I have to admit, I am fully infected by nix, and today cannot see myself starting a project without first putting the `flake.nix` file into my work directory. While nix itself as a package manager is very neat, its feature of a development shell in a Nix flake is _excellent_ to me. Basically, it allows me to provide through a single file in a repository how to make a fully functional development environment for the current project. This compatible with any Linux distribution (not limited to NixOS) as well as macOS which is still my daily driver.
+
+Let me show you how I do it.
+
+## Prerequisites
+
+If you want to follow along this article, all you need is a computer where Nix is installed and functional, be it Linux or macOS. Furthermore, you should ensure [Flakes](https://nixos.wiki/wiki/Flakes) are enabled as I rely on them.
+
+## On the fly packages
+
+Before going with a full project, the first and easiest step is to use `nix shell` which allows to get packages on-the fly in a new shell, without prior configuration. For example, if I need `drill` for a quick test, which comes from the `ldns` package, I can simply type the following command:
+
+ nix shell nixpkgs#ldns
+
+And after a small time to download the dependency – if it is not already on the system – I am presented with a prompt in a subshell with `drill` available in the path. When I close the shell, I do not have access to the dependency anymore. It means that while I keep my global setup lean and with the minimal tools common to all projects, I have (near) instant access to any tool I need. You can provide multiple arguments for multiple packages.
+
+However, if we always need the same tool in a project, and want to share this configuration through the repository, we need to write it down in a file.
+
+## Starting a new project
+
+As I said above, the first thing I do when creating a new project repository is to initialize a new `flake.nix` file that will contain the information on how to setup my work environment. Here is a minimal skeleton:
+It is a little bit of boilerplate that probably does not make much sense if you never touched Nix. Basically, it setups some dependencies in `inputs`, then leverages `flake-parts` to generate a configuration for all the platforms nix supports. Then it declares in these outputs a `devShell` entry, which nix can use to create a crafted shell environment to work with, for now with no settings included.
+
+At this point, you can run the `nix develop` command. After some preparation time, nix provides you with a new shell modified for development. By default it includes some development tools already, including a compiler.
+
+## Adding some dependencies
+
+The `pkgs.mkShell` is the most important part of the file above as it is where we will write our dependencies. If we are doing a Ruby project, we can add the ruby interpreter as a dependency like so:
+
+```nix
+pkgs.mkShell {
+ packages = with pkgs; [ ruby_3_4 ];
+};
+```
+
+Run `nix develop` anew, and your ruby interpreter and associated tools (`gem`, `bundle`, …) will be available.
+
+Sometimes you need some build dependencies, not only the binary packages, that I usually put in `buildInputs`. They may work in `packages` but I find it a bit clearer. So, for Ruby projects, we often need the `libyaml` development files to build Psych, that we can add like so:
+
+```nix
+pkgs.mkShell {
+ buildInputs = with pkgs; [ libyaml.dev ];
+ packages = with pkgs; [ ruby_3_4 ];
+};
+```
+
+Note that for `libyaml`, since I need the development output, I suffix it with `.dev`, otherwise it just gets me the runtime elements which do not allow to build against.
+
+<Note>
+ You can install servers and other dependencies with Nix as well, like a PostgreSQL or Redis server. However, there are no way of starting a service like you would use `brew services` on macOS for example. You can find a way to run then through `nix run`, but personally, I leverage Podman/Docker for background servers additionally to Nix for local development tools. For example, I will put my PostgreSQL server in Podman, but also install PostgreSQL clients and libraries through Nix.
+</Note>
+
+A `flake.lock` file is also generated when running the shell for the first time. It will record at which commit of the input dependencies you were, and use that for future invocations, even on new machines. This way, another user that uses your repository will get the _exact_ same setup, package version included, all automatically through a single command.
+
+If you need to update some packages, you can invoke `nix flake update` that will update the dependencies, including the package source `nixpkgs`. Next invocation of the shell should fetch the updated packages if there are any.
+
+## Custom packages
+
+Let us say that you have a custom package of scripts that come with your development environment. You can totally include them in your flake configuration as a custom package and make them be loaded as a package alongside other dependencies:
+With the above setup, you need to invoke `nix develop` inside your directory to do stuff with the environment. If you prefer to have things a bit more automated, you can setup your shell to automatically load an environment when you enter the directory. Furthermore, it will modify your current shell instead of spawning a new one, keeping all your settings in.
+
+This is done by the excellent `direnv` tool. Install it through your package manager (or Nix!) and see [the instructions](https://direnv.net/docs/hook.html) on how to hook it into your shell.
+
+Now, in your work directory, add a `.envrc` file that tells `direnv` what to do:
+
+```sh
+use flake
+```
+
+And that is all. No, really. Now, direnv should be telling you that it refuses to load your file: as a safety measure, you have to explicitely allow it to load a file. You can do so with `direnv allow`.
+
+Then direnv will work its magic and after a short loading time, will give you your shell back with modifications done. `cd` outside of the repository, and it will undo the changes, all automatically. Also, if you edit your `.envrc` or `flake.nix`, it will reload it automatically.
+
+If you have a `.env` file with environment variables to load, `direnv` can do that for you too. Add to the `.envrc` file:
+
+```sh
+dotenv
+```
+
+Again, nothing more than that. You can also use `dotenv_if_exists` if you want to load a `.env` file while keeping the option of it not existing, and both commands can take an optional path for a different file. For example, if you want to have a permanent `.env` in your repository, and an optional `.env.local` file for a developper to put their overrides, you can use:
+
+```sh
+dotenv
+dotenv_if_exists .env.local
+```
+
+## Working out of directory
+
+You have an existing repository you want to work in that you cannot – or do not want to – add a Nix configuration. You can manage it in a different directory, and still have the entire benefits of Nix. It just requires some additional settings.
+
+When running `nix develop`, you can give a path where to find your `flake.nix` file:
+
+```sh
+nix develop /Volumes/Work/flake
+```
+
+If you are using the `.envrc` technique, `use flake` works the same way:
+
+```sh
+use flake /Volumes/Work/flake
+```
+
+What if you have multiple projects? Well, you can also have multiple shell configurations in your flake. In our previous configuration, we defined a development shell named `default`, but you can define them with different names and then use that name in invocations using `#`:
+
+```nix
+devShells.foo = pkgs.mkShell {};
+devShells.bar = pkgs.mkShell {};
+```
+
+Then:
+
+```sh
+nix develop /Volumes/Work/flake#foo
+```
+
+Or:
+
+```sh
+use flake /Volumes/Work/flake#bar
+```
+
+However, if you want direnv to work, you have to keep the `.envrc` file in the repository. You can add it (and the accompying `.direnv` folder) to your global git ignore or `.git/config/exclude` as needed.
+description = "Nix provides for a wonderful development tool that allows for shared development setup that are easy to share. Here's how I included this tool into my workflow."
+Rust is a relatively new language that is gaining more and more popularity. Its strength is in having a strict compiler that tries to remove entirely some common development errors you would find in other languages, primarily C and its whole class of memory issues.
+
+One of the interesting things about Rust is the error management. Things are done so that it is (normally) impossible to just ignore an error case and force you to deal with them all. Let’s take this very small C snippet:
+ int fd = open("foo.bar", O_WRONLY | O_CREAT, 0666);
+ write(fd, HELLO, strlen(HELLO));
+ close(fd);
+ return 0;
+}
+```
+
+An experimented C developer would be able to tell you that the `open`, `write` and `close` calls can fail, and that you _should_ check them. In the current case, if the program cannot execute any of these calls, it just fails silently. More annoyingly, even when enabling all compiler warning flags, even the very broad `-Weverything` clang provides, there is absolutely no warning on this behavior.
+
+You should, after each of these calls, check if they returned `-1`, and in that case, use `errno` and other functions to determine what went wrong, manually print a diagnostic message and exit the program, cleaning resources as needed. The error information – or at least, the information of the presence of an error, not the error itself is the same type and saved in the same place as a valid return, which makes it painfully easy to ignore the error and use that invalid value in next calls like we do here. Again, compiler does not even emit a warning on this, making it very easy to forget to check an error.
+
+Also, amusingly and to add to the pile of issues, in my first writing I forgot the mode argument to `open`, which created a file with nonsensical permissions. Again, not a warning to be seen.
+
+So, how does Rust handle that?
+
+## The `Result` type
+
+Let’s check what the prototype of Rust’s equivalent to `open` is:
+Rust actually provides, in `std::fs::File`, two functions, `open` and `create`, to easily represent opening in `O_RDONLY` and `O_WRONLY | O_CREAT` modes respectively, which are the most common. It also provides, for more specific cases, the `options` function giving you a builder pattern to set your options safely, but our concern here is the result type. For our example, the equivalent is `create`.
+
+The return type of `create` is `Result<File>`. Actually, Rust takes some shortcuts in its standard library, and in this case, uses an alias to remove an often repeated argument. The actual type is `std::io::Result<File>` which expands to `Result<File, std::io::Error>`. It is an implementation detail, but we need the expanded type here to understand.
+
+A result is an `enum`, which can takes a closed amount of values. Unlike C enums, which are just numerical values with labels, Rust enum variants can be simple tagged values as well as more complex structures. Take a closer look at the `Result` type definition:
+
+```rust
+enum Result<T, E> {
+ Ok(T),
+ Err(E),
+}
+```
+
+The enum Result can take only two values: `Ok`, or `Err`. Each of these variants are a 1-item tuple, containing an item of type `T` or `E` from the generic typing.
+You cannot access the `T` value directly, however: you have in some way or another eliminate the possibility of the result being `Err`. The most straightforward way would be with `match`:
+You cannot work on the file if an error happens, as you will not be able to get a `File` value from `Err`, needed to do write operations. You have to _at least_ throw the error away, which is then an active choice.
+
+If you put this code in a Rust file and compile it, you will actually get a warning:
+
+```
+warning: unused `Result` that must be used
+ --> src/main.rs:7:13
+ |
+7 | write!(f, "Hello, world!\n");
+ | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+ |
+ = note: this `Result` may be an `Err` variant, which should be handled
+ = note: `#[warn(unused_must_use)]` on by default
+```
+
+The call to `write!` is also returning a `Result`, and by default, the compiler will warn you if you call something that returns a `Result` and you ignore it. This way, it is way harder to accidentally ignore an error case.
+
+Now, you can add another `match` block around `write!` to check for error at this point, but doing so gets heavy quickly code-wise. Thankfully, Rust provides tools to ease all of that.
+
+## Managing errors
+
+`Result` provides some useful functions to deal with errors without having to `match` all the time. The first and simplest one is `unwrap`. Calling it on a `Result` containing an `Err` will cause a `panic!`, crashing the program instantly, and otherwise just returning the `T` value. So, using it, we can reduce our code to this:
+
+```rust
+let mut f = File::open("foo.bar").unwrap();
+write!(f, "Hello, world!\n").unwrap();
+```
+
+The code is still rather small, and unlike the C sample above, an error will actually close the program instantly with both an error code and a message printed explaining what happened:
+
+```
+thread 'main' panicked at src/main.rs:5:42:
+called `Result::unwrap()` on an `Err` value: Os { code: 13, kind: PermissionDenied, message: "Permission denied" }
+```
+
+However, while very practical, `unwrap` is not really known as best practice. It can be useful for small tests or temporary handling, but you really should not rely on it for long-lasting code. Or put it for cases where you are **absolutely sure** that you cannot have an error.
+
+First of all, it does not give a lot of information. You do have the source file and line, but it can still make it hard to get good information on what happened. Thankfully, `unwrap` has a little brother, `expect`, which allows to make the error a bit more informative by adjoining a custom message:
+
+```rust
+let mut f = File::create("foo.bar").expect("Cannot open file");
+write!(f, "Hello, world!\n").expect("Cannot write to file");
+```
+
+If you run this code, you will see that the message in `expect` is prefixed to the lower error code. In big code bases, this additional context can save a lot of debugging time, and it is barely longer to write.
+
+It is advised to always favor `expect` to `unwrap` so you have a bit more context in case of failure.
+
+What if, however, you do not want to treat an error as an exit? Take for example reading an environment variable: if it does not exist, I want to continue my program anyway. Well, `match` still works:
+
+```rust
+let var = match std::env::var("MY_VAR") {
+ Ok(v) => v,
+ Err(_) => "foo".to_string(),
+};
+```
+
+You can see we completely ignore the error in this case. However, there are shorter ways! `Result` provides `unwrap_or` and some other functions for these use cases.
+
+```rust
+let var = std::env::var("MY_VAR").unwrap_or("foo".to_string());
+```
+
+Note that in this case, the value given to `unwrap_or` is always calculated to be passed to the function, which is OK for trivial values, but not for expensive to calculate functions or stuff that has side effects. In that case, you have `unwrap_or_else` which takes a lambda, executed only if the `Err` case is encountered:
+
+```rust
+let var = std::env::var("MY_VAR").unwrap_or_else(|| "foo".to_string());
+```
+
+In this last example, the `"foo"` string copy is allocated only if the environment variable is not found, while on the previous one, it was always allocated, then wasted if you had a `Ok`.
+
+Also, if your `T` type implements `Default`, you can just use `unwrap_or_default`. If an empty string is OK for you:
+
+```rust
+let var = std::env::var("MY_VAR").unwrap_or_default();
+```
+
+And that is only scratching the surface.
+
+- `map` and `map_err` allow you to call a mapping function in `Ok` and `Err` cases respectively, allowing you to change the types of your `Result` without deconstructing it.
+- `or` and `and` allow you to combine multiple results into a single one based on their respective values. Their counterpart `or_else` and `and_then` do the same but with a lambda called in the appropriate case. It allows transformation like `map` and `map_err` but fallible.
+- `is_ok` and `is_err` are simple methods to check if you have a `Ok` or `Err` without retrieving their inner values. Their siblings, `is_ok_and` and `is_err_and`, allows to do the same and perform an additional check on the contained value while not restructuring the `Result`.
+
+And some more that I did not describe for some more use cases you could have. You can check [`Result`’s documentation](https://doc.rust-lang.org/std/result/enum.Result.html) to see all the available methods and a complete description for each.
+
+## Going back up
+
+What if I told you that Rust makes it easy to make errors bubble up your stack, and making writing your program to exit gracefully from its `main` even in deep error cases much easier?
+
+Let us take our example back, and put it in a separate function returning a `Result`:
+
+```rust
+use std::fs::File;
+use std::io::Write;
+
+fn say_hello() -> Result<(), std::io::Error> {
+ let mut f = match File::create("foo.bar") {
+ Ok(f) => f,
+ Err(e) => return Err(e),
+ };
+ match write!(f, "Hello, world!\n") {
+ Ok(_) => Ok(()),
+ Err(e) => return Err(e),
+ }
+}
+
+fn main() {
+ say_hello().unwrap();
+}
+```
+
+Too verbose. Thankfully, we can do shorter. First, the second `match` is completely useless, we can just return the `Result` from `write!` directly. Easy.
+
+The `File::create` part however is a bit more tricky, as we need to do an early return. Well, Rust provides an operator for that: `?`. It is basically the `match` seen above, but compacted into a single character. So our code above now reads:
+
+```rust
+fn say_hello() -> Result<(), std::io::Error> {
+ let mut f = File::create("foo.bar")?;
+ write!(f, "Hello, world!\n")
+}
+```
+
+Much shorter, but still handing every error case and passing it up in the stack. Also, like in C++ for example, objects with destructors (`Drop` trait) will be destroyed with the early `return`, clearing resources on the way.
+
+The `unwrap` in `main` handles all errors coming from the `say_hello` function in a single place. But that still triggers a `panic!`. You can also `match` at this level to print the error cleanly. But Rust has another small trick up its sleeve to ease that too: you are perfectly allowed to make `main` return a `Result`, and it will automatically take care of printing the error and exiting with an error status code for you.
+
+```rust
+fn main() -> Result<(), std::io::Error> {
+ say_hello()
+}
+```
+
+Voilà, you have complete error management in your program without having to write much. So while Rust prevents you from just throwing errors away, it also makes it easy to handle them properly.
+
+However, there are rare cases where you do not actually care about a result value, neither its success nor error value. If you just do your call, the compiler will complain that you have an unused `Result` (rightfully), and once again, you have to actively tell it you do not actually want to use the value. There are two ways of doing that: either you assign it to a `_` variable, dropping it instantly, or you use `ok` to transform it to an `Option` which will clear the warning.
+
+```rust
+let _ = say_hello();
+say_hello().ok();
+```
+
+I prefer the second solution personally as it makes it more visible you are “OK’ing” an error.
+
+## Multiple error types
+
+You have used another function which has a different error type, and you do not know how to manage it. For example:
+
+```rust
+fn main() -> Result<(), std::io::Error> {
+ let n = std::env::var("FOOBAR")?;
+ let mut f = File::open(n)?;
+ write!(f, "foobar")
+}
+```
+
+While `std::env::var` does return a `Result`, its error part is `std::env::VarError`, which is not compatible with `std::io::Error`. The compiler will argue that it cannot do the conversion from one to another. We need to manage this.
+
+Thankfully, error types are (normally) implementing the `std::error::Error` trait. We can use that to have a generic error return, using a `Box`.
+Note that this still does not compile, but we will get back to this later. The error moved to the `write!` call, meaning it solved the issue for the `?` calls.
+You can notice that you do not have to do any conversion using the `?`. The `?` operator will automatically call `.into()` on the error value before returning it, and there is a `From` conversion to automatically convert any implementor of `Error` into a `Box<dyn Error>` for convenience. So as long as you have a compatible type, this will work automagically.
+
+For the remaining error, though, we do not have this automatic conversion. Amusingly, the compiler proposes the following solution:
+
+```rust
+Ok(write!(f, "foobar")?)
+```
+
+It certainly works: if there is an error, it does the early return automatically, otherwise it extracts the `Ok` value and puts it in a new `Ok` with the right type. Certainly works but, in my option, a bit silly. You can just make the `into` call by hand:
+
+```rust
+write!(f, "foobar").map_err(Into::into)
+```
+
+It is a bit longer to write though, but personally I find it clearer to have `into` written rather than a wild `Ok` which does not hint at the error conversion happening.
+
+Note however that while using `dyn Error` is very practical, it can be more adapted to make your own error type and provide conversions from the “lower” types into yours that you use everywhere, especially for libraries. We will see how to do that in a few paragraphs.
+
+## Making your own errors
+
+Sometimes, you want to emit errors on your own. Rust also makes you able to do that, with varying degrees of complexity depending on your needs.
+The most simple is to use basic strings. Rust provides ways of turning strings into full-fledged errors for your convenience.
+Once again, a `From` implementation makes the conversion using `into()` trivial. Actually, there are `From` implementation for `&str`, `String` and even `Cow`, so you are covered in all string cases.
+
+Since our type supports `into()`, it means it also works with `?`:
+Using strings as error is good for quick programming, but if you want to be more robust, you will prefer to make your own, full-fledged error type. To do that, you need to implement the `Error` type from the standard library. This is a very basic one to start with:
+You can use the `Failure` value as an error anywhere, even with the automatic conversion to `Box<dyn Error>` if needed, or use it as a `E` for the return value of `main`. It all works seamlessly.
+
+`Error` requires you to implement both `Display` and `Debug` so your error can be inspected: you can use them to display basic or complex information at your will. For reference, the value printed by `main`’s return comes from `Debug`.
+You can of course put values in your structure, and use them for the error messages to give more information. You can also use an enum.
+
+### Wrapping errors
+
+As you make your own error type, you may want to use it as a wrapper for other types of error that can happen. Let’s broaden our type so it can also represent errors of `std::io::Error` type that are common in the standard library.
+
+We can write a `From` implementation to make the conversion to our type possible:
+
+```rust
+impl From<std::io::Error> for Failure {
+ fn from(_: std::io::Error) -> Failure {
+ Failure
+ }
+}
+```
+
+And voilà, we can now use `?` to convert from `std::io::Error` to `Failure` automatically and use our own type everywhere. However, we are wasting precious information that we could keep. Let us modify our type so it can represent both our own emitted errors and the I/O errors while keeping information, leveraging an enum.
+
+```rust
+enum Failure {
+ Own,
+ Io(std::io::Error),
+}
+
+impl From<std::io::Error> for Failure {
+ fn from(inner: std::io::Error) -> Failure {
+ Failure::Io(inner)
+ }
+}
+```
+
+However, we are still not using this information (the compiler will gently inform you that you never use your field), and the error message on runtime is not any more informative. Naturally, we can just modify our printing functions to do so:
+If you try it now, you will see that you can see the inner error. But there is a better way to do so. Rust planned that errors can bubble up and generalize along the way, and provides a mechanism specifically to represent that. In the `Error` trait, there is an optional method to implement, `source`, made explicitly to give a source error to the current one, cascading down. Let us implement that:
+Note however that Rust’s implementation of the `main` return does not use this information (it only relies on `Debug` implementation), but some other libraries managing errors do take it into account. You can also use it if you write your own generic error handler.
+
+## Wrapping up
+
+We have seen some important points about error handling in Rust:
+
+- It is conceived to make it impossible to accidentally ignore an error case. The `Result` type, one of the most important in the language, ensures that you deal with the error appropriately, or at least make by yourself the active choice of ignoring an error instead of silently dropping it. Furthermore, it gives a unified interface for error management, with different types for the return of a success and an error, removing the need for special values like `-1`, things like `errno` or a writable pointer passed to a function to get an error.
+- While Rust does not have throwable exceptions, it provides an easy way to bubble errors up your stack and get them to elegantly be managed at the right place. The `?` operator is a cornerstone of Rust code, allowing efficient and non-verbose management of all error sources.
+- It is trivial to create your own error types that integrate into the Rust way of doing things, making managing your custom errors and adding context or generalization while bubbling up trivial.
+
+All of this is one of the most basic foundations of Rust and what makes code written in it more robust. Even if you willingly ignore an error with `unwrap`, it will not silently fail with weird side effects but actually crash your program, and finding uses of these methods across a codebase is easy, compared to having to check that every call to `open` is followed by a `if (fd == -1)`.