💾 Archived View for gemini.hitchhiker-linux.org › gemlog › daily_driving_void.gmi captured on 2023-09-08 at 16:15:29. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2023-06-14)
-=-=-=-=-=-=-
I've had Void as a second boot option for months now, and before that I had a fairly long running VM. I've been interested in moving to Void for a while for a couple of reasons, but the biggest would be that I wanted a distro based on Musl libc.
I already made this particular jump on one machine, the Raspberry Pi 4 which runs this gemlog, my Gitea instance and an Apache server. It's been rock solid and I haven't had to do anything other than install updates since the first week I brought it up. I haven't had that kind of stability in a long time.
One of the things that had been holding me back for a while was that the versions of Gtk and LibAdwaita in the Void repositories were behind what I needed to compile some of my projects, namely Eva, Gfret and Vapad. I've been used to getting library updates on Arch very soon after they become available. That highlights one of the differences between the Arch and Void release models. Both are rolling, but Void is more "Rolling Stable" for lack of a better term. More on that below. Anyway, short of compiling libraries myself this one was beyond my control. In the future I'll hold off updating my code until the libraries are available on Void, which coincidentally matches up better with what a lot of the LTS distros are doing.
The other issue I was having it turns out was my own fault. It's no secret I do most of my coding these days in Rust, and it has definitely spoiled me in a lot of ways. The version of Rust in the Void repos was, up until last week, 1.64. There were a number of really cool features release in Rust 1.65 which I bought into right away. The biggest two were let..else and labeled blocks.
// old syntax let thing = if let Some(t) = some_function_returning_option() { t } else { return Err(NoThingError); }; // with let..else let Some(thing) = some_function_returning_option() else { return Err(NoThingError); };
The other feature, labeled blocks, is pretty much the same thing as Zig's named blocks, and it's a geat feature. It allows one to return early from a code block with a value, including breaking out of an outer loop in nested loops. The syntax is a little odd, but I've already used it in a couple of places.
let liff = 'outer: loop { loop { break 'outer 42; } }
Anyway, I really wanted those newer features and not having them was going to be a deal breaker for me. Ordinarily I'd be using Rust from the official installer, rustup, but I was getting weird random issues every time I tried using the toolchain provided that way. I thought it had something to do with Musl, and that the Void devs had found a workaround. Turns out it was just me not realizing that I had previously installed Rust via the distro packages and had neglected to remove the `rust-std` package. Doh.
Note that about the same time I fixed that issue, the distro packages were updated. But I'm sticking with the rustup toolchain because the distro packages are missing rustfmt and clippy, which are too nice to not use.
This seems like a subtle distinction, but in practice is has some noticable effects. In Arch, every package gets updated as soon after a new stable version is released as it can be packaged and tested. This includes libraries and kernels. This is rolling release.
In Void, those libraries don't get updates beyond path releases until the applications that are using them require the new versions. Void also uses the most recent LTS series kernel. This leads to less churn overall, while still having the benefit of a system that never has to be reinstalled or go through an upgrade from one OS version to another.
This leads to some interesting contradictions. On Arch, I had gtk-4.10 for months, while all of the Gnome applications that were installed only needed gtk-4.8. But when Gnome43 came out, it took almost two months for the dev team to build, test and release it. Void, on the other hand, waited until the desktop was being updated and updated all of the libraries at the same time. So when Gnome44 came out last week Void got a large influx of updates and the entire desktop was available within days. It's probably not as well tested as Arch, but considering the manpower constraints how could it be?
Note: Fedora and Ubuntu both time their releases to occur shortly after Gnome major releases, so when the next major version of those distros ship it can take advantage of what is at the time the latest version. So the way Void works is actually pretty similar, except no need to download an iso and upgrade from one distro release to another or to change repositories.
At any rate, I really rather like this "Rolling Stable" approach. It definitely calmed things down when it came to running the OS as a server, and it seems to work out nicely on the desktop as well.
As most OSS these days is written for Linux, and the vast majority of distros ship Glibc rather than Musl, it's only natural that some differences would be noticable, particularly if like me you tend to experiment and write a fair bit of low level code. Just this morning, I was working on bringing up an aarch64 cross compiler toolchain (as per the instructions in my post about building cross compilers) and Binutils failed to build. The offending code was in the `gprofng` module, where it was looking at the members of the `sigevent` structure individually, and expecting one to be there that does not exist in Musl. Rather than investigate and patch I just disabled gprofng via the configure script. Luckily that was a thing that I could do. This sort of thing is not always the case.
Another difference I encountered when writing some code which read usernames and validated passwords using the libc `pw` and `spw` structs. My original pass at this (using unsafe Rust) compiled and ran fine with Glibc. With Musl, it compiled and errored at runtime because some of the data I was trying to read was a null pointer (I did at least have proper error handling and null checks in the code). Anyway the reason it was happening was that Musl re-uses the same memory address for each subsequent construction of `pw`. When I looked at the POSIX spec, sure enough it said that some Libc implementations do this and it is fine. There were two possible solutions. One was to copy the bytes from that memory location into an owned piece of memory. The other method was to move all of the logic using the data in the first `pw` instance to before the creation of the second `pw` instance. The second method, requiring one less allocation, is what I used. This is a pretty nice demonstration of the sort of trouble you can get into when writing `unsafe` Rust, but because the language has trained me to account for all code paths instead of just the happy path it was a pretty easy diagnosis and fix.
That's two examples of the sort of things I've encountered in Musl, and I think they're pretty indicative of the sort of subtle differences one might encounter. That is to say, you're only going to encounter them if you're poking at the system. The average person really isn't going to notice any difference whatsoever because everything pretty much just works. A look at the relative sizes of tthe code is enlightening.
# Musl, according to Tokei =============================================================================== Language Files Lines Code Comments Blanks =============================================================================== GNU Style Assembly 312 7938 7165 256 517 Autoconf 35 6624 236 6273 115 C 1583 65083 50968 7551 6564 C Header 668 40754 35683 301 4770 Makefile 12 314 201 34 79 Shell 4 925 638 172 115 =============================================================================== Total 2614 121638 94891 14587 12160 =============================================================================== # Glibc =============================================================================== Language Files Lines Code Comments Blanks =============================================================================== GNU Style Assembly 2345 463470 328440 93738 41292 Autoconf 94 112969 112642 182 145 C 10186 1015110 708606 209622 96882 C Header 3341 416395 304183 79574 32638 C++ 35 2447 1660 525 262 Happy 1 387 330 0 57 JSON 2 100 100 0 0 LD Script 3 25 7 17 1 Makefile 202 21349 15600 2869 2880 Module-Definition 21 2958 2759 0 199 Perl 3 864 635 142 87 Python 68 18705 14720 2383 1602 Shell 83 8713 6102 1671 940 TeX 1 11672 7162 3697 813 Plain Text 8 63381 0 63159 222 =============================================================================== Total 16393 2138545 1502946 457579 178020 ===============================================================================
I've compiled both Musl and Glibc numerous times while playing around with HitchHiker. Glibc takes a solid 15 minutes start to finish, while Musl is done in around a minute or less. Also, why do I want or need both Perl and Python in a Libc distribution? They might not be available (much less wanted) while bootstrapping a system from source.
The coding *style* is also enlightening. Drew Devault did a great writeup on one of his investigations into why a certain piece of code segfaulted with Glibc and not with Musl.
Maybe, just maybe, the behavior of this function should not depend on five macros, whether or not you’re using a C++ compiler, the endianness of your machine, a look-up table, thread-local storage, and two pointer dereferences.
Drew Devault: A tale of two libcs
Anyway, even though I'm not writing much C these days it cannot be denied that C is the foundation upon which Linux (and most other modern software) is built. So for a long time I've really wanted to switch to a distro that uses what I firmly believe is a better Libc implementation. Void scratches that itch quite nicely. Bonus points because it uses Runit for Init, and I'm a big fan of service supervision suites like Runit and S6.
All content for this site is licensed as CC BY-SA.
© 2023 by JeanG3nie