summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEkaitz Zarraga <ekaitz@elenq.tech>2023-12-15 12:49:11 +0100
committerEkaitz Zarraga <ekaitz@elenq.tech>2023-12-15 12:49:32 +0100
commit1129e8199fc52755feb37283fb9dcb3e6658bbea (patch)
tree75898433a7b1d5f5f320baf95233ef7cfe6e60ce
parentd57c3494dc79dfa5d46bdca530b821c030f72572 (diff)
Windows 2
-rw-r--r--content/windows2.md498
1 files changed, 498 insertions, 0 deletions
diff --git a/content/windows2.md b/content/windows2.md
new file mode 100644
index 0000000..b151f04
--- /dev/null
+++ b/content/windows2.md
@@ -0,0 +1,498 @@
+Title: Guix + Zig + NSIS for the win...DOWS?
+Date: 2023-12-15
+Category:
+Tags:
+Slug: windows2
+Lang: en
+Summary:
+ How I made a program for Windows and GNU/Linux without touching any Windows
+ machine. The tools and the tricks to be effective (Zig and NSIS for the
+ win).
+
+Some months later, it's time to talk about [the post I made about writing
+software for windows, without windows][win] because I put some of those things
+in practice.
+
+I made a simple application with one external audio library (OpenAL) and simple
+networking. No GUI this time, but some complexity was there.
+
+[win]: https://ekaitz.elenq.tech/windows.html
+
+### The program
+
+The program I'll discuss here is Karkarkar, a tool to read a Twitch chat out
+loud in Basque. I made this for the Basque streaming community of gamers, which
+is really cool but they had to rely on Text-To-Speech systems for other
+languages as the main services for streamers don't include Basque in their
+service, and the closest one, Spanish, is not good at some words' pronunciation
+defer.
+
+Most of these people are gamers, and they use Windows, which I don't like, but
+in the end they are also people and they deserve Free Software regardless of
+the Operating System they decide to (or are forced to) use.
+
+I took this as a cool exercise to test all we talked about in the past, and as
+a learning experience for the times I need to do something for a client that
+requires Windows support or anything like that.
+
+#### The Text-To-Speech system
+
+I used a TTS library by Aholab, a research group from the Bilbao School of
+Engineering, the university where I studied. They released AhoTTS (*ahots*
+means *voice* in Basque) some years ago in Github, a working TTS library for
+Basque and Spanish, with all the extra data it needs to work.
+
+The *only* problem it has is the AhoTTS codebase is a mess, with tons of
+horrible decisions and undercover bugs. I had to fork the lib and make it work
+more or less before adding it to the project (I won't discuss the issues here),
+but I did.
+
+#### Connection to the Twitch chat
+
+This simply uses IRC to connect to Twitch chat. The protocol is simple, and I
+only implemented an embarrassingly minimal amount of it. Enough to make it kind
+of work. I hope to make more of this in the future.
+
+#### Playing the audio
+
+I started with `libao`, a simple audio library and then moved to OpenAL for
+reasons I'll mention next.
+
+#### All together
+
+The program listens to IRC, when a message arrives it sends it to AhoTTS,
+receives the samples to play and sends them to OpenAL which does its magic to
+make it play out loud.
+
+Everything is the simplest thing possible as I didn't have a lot of time to
+spend on this and also wanted to focus on the release process and being able to
+make a package for windows, and some linux distributions. **Adding more code on
+top of that is easy once the problem of the distribution is solved.**
+
+
+### The tooling
+
+My strongest dependency[^1], AhoTTS, is written in C++, so I decided to go for
+binary file distribution. And as [I discussed in aforementioned post][win-zig],
+I was looking for an excuse to make a project in Zig, and its cross-compilation
+capabilities could help me in this case, so I went for that. I made everything
+with Zig 0.10.1, as that is the latest version packaged for Guix, but I was
+forced to move to my Zig 0.11.0 package (see [Testing](#testing)).
+
+[^1]: There are not that many Basque TTS systems out there... You know?
+
+[win-zig]: https://ekaitz.elenq.tech/windows.html#zig
+
+For binary distribution, I didn't have many ideas when I wrote the post, but a
+person in the Lisp Game Jam of that time suggested me to use NSIS for the
+Windows installer. It was already packaged in Guix, my distro of choice, so I
+just went for that, as it looked the simplest way to solve this.
+
+For testing all this, I relied in `wine`, what else could I do?
+
+So tl;dr:
+
+- I'm coding on [Guix](https://guix.gnu.org/).
+- Programming in [Zig](https://ziglang.org/) 0.10.1 but then moved to
+ [Zig](https://ziglang.org/) 0.11.0.
+- Installer using [NSIS](https://nsis.sourceforge.io/Main_Page).
+- Testing done with [Wine64](https://www.winehq.org/).
+
+
+### Keeping it small
+
+First of all, publishing software for Windows from Linux is painful if you have
+to compile everything for it. Guix helps a little bit with that as you can use
+`--target=` with mingw and have some luck. Sadly, many packages (most of them)
+need `bash-minimal`, which [is not buildable for mingw at the
+moment][issue-mingw].
+
+[issue-mingw]: https://issues.guix.gnu.org/62226
+
+In many software projects cross-compilation is not even possible.
+
+Knowing that I decided to keep my project as small as possible, because I
+wanted to actually deliver something without losing my sanity.
+
+Each library you depend on you have to compile and deliver to your target, too.
+It's not that common to have Windows users to install things by themselves as a
+GNU/Linux person would.
+
+#### Audio library
+
+`libao` is the smallest audio library I could find, but I didn't manage to
+build it for Windows myself, so I had to rely on something else. The
+OpenAL-Soft maintainers have a binary distribution for windows, so I decided to
+go for that one instead. It's way harder to use, and I had to do some weird
+stuff to make it work, but it's easier for me: no need to build it myself.
+
+This is even more important in the case of having an audio library, interacting
+with the system is always a pain in the ass, and trying to cross-compile a
+library like this is not always easy, as their configure scripts are complex as
+they need to check for many things.
+
+This is why I also avoided a GUI for the moment. Too much.
+
+#### AhoTTS
+
+AhoTTS is written in C++, it has zero dependencies and uses CMake as a build
+system.
+
+At the beginning I compiled it using Guix to mingw, because it doesn't have
+dependencies, it just worked. Later I encountered some issues though:
+`libstdc++` and `libgcc` were not found at runtime and I had to statically link
+them (`-static-libgcc -static-libstdc++` ftw).
+
+Later I started digging in its code, fixing some **horrible** things inside and
+then I managed to add it as a submodule (yes, I hate that too) and compile it
+directly with Zig. This is the best option so far: you can statically link the
+library, it is built with the same process and it is cross-compiled by Zig. No
+more missing library problems.
+
+> Extra: In order to interact with AhoTTS from Zig, I had to make [a small C++
+> to C bridge][c++-c], that I never did before. It's easy stuff, just convert
+> the API a little bit, put some `extern "C"` on it and everything will go
+> fine. `void *` is your friend.
+
+[c++-c]: https://github.com/ekaitz-zarraga/karkarkar/blob/master/ahotts_c/htts.cpp
+
+#### The rest of it
+
+That's just writing some actual code and making the program run. That's the
+easiest part.
+
+
+### Bringing it to the users
+
+One thing is making something that builds and runs in your computer and a very
+different thing is making it work in other people's computers. And this is the
+main problem that I wanted to discuss here.
+
+I have some requirements:
+
+- I made a terminal application, but I don't expect my users to know how to run
+ it. I need to make something that is click and run.
+- I need it to have some icon in the desktop/startup-menu.
+- The AhoTTS library needs some extra data files. These are searched by the
+ library and need to have some specific structure. These need to be installed
+ properly, too.
+- I need to provide a simple way to uninstall the application.
+
+#### Windows
+
+The cool thing of this is I don't own a windows machine since ~2010 and I have
+no access to any (living the good life!). I don't have any plans to change that
+neither I have plans to really learn about Windows. So we have to be clever to
+solve this.
+
+First things first:
+
+``` bash
+zig build -Dcpu=baseline -Dtarget=x86_64-windows-gnu
+```
+
+Damn! That was actually very easy to do. Why isn't this the norm in other
+languages?
+
+Of course, in order to do this I needed to provide a proper `build.zig` file
+that was able to find the DLLs I was linking against and the header files.
+That's not that difficult after all[^dll-names]. But has to be done. Still,
+easy. Kudos for Zig.
+
+[^dll-names]: I still had weird issues with this. In Zig 0.10.1 DLLs where
+ found even if their name started with `lib`, but moving to Zig 0.11.0
+ didn't find the libraries starting with `lib`. But Wine did find them so I
+ didn't know what to do. When I added the AhoTTS as a submodule the problem
+ disappeared, but not because it was solved, it was just avoided.
+
+Now, knowing where Windows searches the DLLs we depend on is important as our
+installation process will depend on it. They have [some good documentation for
+that][win-dll] which has a very interesting point:
+
+> Standard search order for unpackaged apps:
+> ...
+> 7. The folder from which the application loaded.
+
+This seems good enough for my purpose, so I can just install my binary and the
+libraries in the very same folder. I thought I would need to install stuff
+mixed somewhere else and learn a lot about windows, but I didn't have to.
+Simple!
+
+[win-dll]: https://learn.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-search-order#standard-search-order-for-unpackaged-apps
+
+The extra data can be stored anywhere I want, because I am the one that
+controls the search algorithm, but I still need to have easy access to it. I
+decided to go for `LOCALAPPDATA`, but I'm thinking on putting it in the same
+folder as the rest of the program. At the time of writing that's not done yet.
+
+##### NSIS
+
+Having clear where everything should be installed, it's time to make it
+actually install it.
+
+NSIS is a great tool. The look of the vanilla installer is old-school but I
+didn't even bother to use the modern interface because that required me to
+think, activity I like to reserve to special occasions (like when I'm paid
+extreme amounts of money for it or the time I'm in bed before falling asleep).
+
+NSIS in a nutshell: you write a script, run `makensis` with the script as an
+input and, boom! You have a `.exe` that installs your stuff.
+
+It took me a little while to understand the structure of the script but once
+you learn it is really easy (for the basic things, after that you can go as
+hard as you want). It has two concepts you need to understand: Pages and
+Sections.
+
+- Pages: define the different *pages* the user will navigate through during the
+ process. There are many pages pre-defined, and they cover all the basic
+ functionality: license agreement, component selection, installation directory
+ selection...
+- Sections: define the different parts of your installation process. You can
+ mark some as optional or include them in different installation profiles
+ (all, minimal, recommended... You've seen this before). Then, if you use
+ `components` page, the sections will be listed to the user to choose which
+ one they want to install. You can also add sections for the uninstaller,
+ which will only be run when the user uninstalls the program.
+
+> EXAMPLE: I decided to include the sources also in the installer, but they are
+> not required to run the program. It's as simple as adding a Section with the
+> `/o` flag and they won't be automatically checked in the components step.
+> Really cool stuff!
+
+The rest of the thing is just commands you can read in the documentation. You
+can do many-many things with it. It has environment variables for almost
+anything you'll need, so you don't need to hardcode things in the script.
+
+In my case I decided to ask the user for a configuration (the Twitch username)
+during the installation using a custom page (this requires some digging in the
+plugins' documentation, but it's not hard either), and created a launcher that
+automagically inserts the username in the call to the program (not great for
+later configuration, I know). This is done with a Batch file, which also makes
+the dirty job of opening the terminal when it's double clicked.
+
+Here's the installer script I did, if you want to read it:
+
+<https://github.com/ekaitz-zarraga/karkarkar/blob/master/windows/installer.nsi>
+
+
+#### GNU/Linux
+
+GNU/Linux world is really diverse, so it's not easy to know about every single
+system's requirements. At the moment I stayed with Guix and Debian because they
+are the only distros I use and I'm more familiar with them.
+
+In Windows I was asking to the user to add their username in the installation
+process, but in Linux I don't have any simple (for the user) way to do it, so
+for the moment I ask for the username when the program is called with no input
+arguments. Ugly, but works for the moment. The goal was to deliver this thing,
+not to make it perfect. I can do that later.
+
+The cool part is they provide different approaches for packaging: source vs
+binary distribution.
+
+##### XDG standard: Desktop file and icons
+
+We need, of course, to arrange the same things we arranged in Windows: the
+desktop icon and launching the terminal automatically. That's not hard to do
+using XDG specification!
+
+Just add the `Terminal=true` line and it should open a terminal emulator when
+clicked[^should]. The `Exec=` line in the desktop file has the program you want
+to run, and it has to point to it correctly.
+
+[^should]: *"Should"* is the best word choice here, because it doesn't work for
+ some people, like in GNOME, as the terminals are hardcoded:
+ <https://gitlab.gnome.org/GNOME/glib/-/blob/main/gio/gdesktopappinfo.c#L2685>
+
+Once the `.desktop` file is done, we realize we need to deal with the icons. I
+went just for a SVG icon, but I could add the rest. The only thing I needed to
+do is put everything in the correct folder. Something like this:
+
+``` bash
+ usr
+ └── share
+ ├── applications
+ │   └── karkarkar.desktop
+ └── icons
+ └── hicolor
+ └── scalable
+ └── apps
+ ├── karkarkar.svg
+ └── karkarkar-symbolic.svg
+```
+
+Both Guix and Debian detect them properly once they are installed in the folder
+where they expect them.
+
+##### Guix
+
+I wrote quite a few Guix packages lately so I'm pretty comfortable with this.
+
+I just need to tell Guix how it should build the program and put some files in
+the proper directory.
+
+Also, I added the `zig-build-system` myself to Guix not that long ago. It's
+pretty straightforward to use.
+
+The icons and the desktop file needs to put everything in the correct place,
+`#$output/share/...`, and patch the Desktop file to point to the correct
+binary, `#$output/bin/...` that is. For this, I kept a desktop file as a
+template, with some reasonable defaults and just patched it in the Guix
+package. That's easy.
+
+Not the best Guix package ever but it simply works, and that's everything I
+want at this point.
+
+##### Debian
+
+Debian packages can be exported from Guix package definitions, but it exports
+its file structure with every single dependency. In our case, that meant
+hundreds of MegaBytes. That's too much so I did it manually, and the final size
+was around 10 MegaBytes. Not bad.
+
+I never made a Debian package before, and I did the most minimalistic thing I
+could think of.
+
+First, we need to build the thing:
+
+``` bash
+zig build -Dcpu=baseline
+```
+
+And then just place everything in place in a folder, and call `dpkg-deb` on top
+of it. In order to do that, I just wrote a bash script that did this whole
+thing, it explains what I did way better than I can write in English:
+
+``` bash
+# Run me in the linux/ folder
+# I need the version in an argument, which should be Major.Minor-Revision
+# for example 0.1-3.
+version=$1
+outfolder="karkarkar-$version"
+mkdir "$outfolder"
+mkdir -p "$outfolder/usr/bin"
+mkdir -p "$outfolder/usr/share/applications"
+mkdir -p "$outfolder/usr/share/AhoTTS/"
+mkdir -p "$outfolder/usr/share/icons/hicolor/scalable/apps"
+
+cp -r "../AhoTTS/data_tts" "$outfolder/usr/share/AhoTTS/"
+cp "karkarkar.desktop" "$outfolder/usr/share/applications"
+cp "../icons/karkarkar.svg" "$outfolder/usr/share/icons/hicolor/scalable/apps"
+cp "../zig-out/bin/karkarkar" "$outfolder/usr/bin/"
+patchelf "$outfolder/usr/bin/karkarkar" --set-interpreter "/lib64/ld-linux-x86-64.so.2"
+
+mkdir -p "$outfolder/DEBIAN"
+cat > $outfolder/DEBIAN/control <<EOF
+Package: karkarkar
+Version: $version
+Section: base
+Priority: optional
+Architecture: amd64
+Depends: libopenal1 (>=1.19.1)
+Maintainer: Ekaitz Zarraga <blablablah>
+Description: Karkarkar
+ Listen to a Twitch chat in Basque.
+EOF
+
+dpkg-deb --root-owner-group --build "$outfolder"
+rm -rf "$outfolder"
+```
+
+I need to highlight here the `zig build` I did automatically adds the Guix
+dynamic linker to the binary, but that is not where the dynamic linker is in
+Debian. I decided to patch the binary (`patchelf`) instead of trying to
+configure the compilation process, I thought this would be easier. I don't know
+if it was easier or not, but it was easy, so that's ok.
+
+Also note that the `DEBIAN/control` file has the bare minimum fields, but it's
+enough to work. In Debian, everything is installed in `/usr/whatever`, but
+that's the only detail I changed.
+
+Something I want to write down to remember later is the debian packages (`.deb`
+files) can be extracted with `ar -x` and they have a couple of `tar.xz` files
+inside. The `data.tar.xz` file has the file structure that will later installed
+in the system.
+
+
+### Testing {#testing}
+
+Yeah, you have to do it too. I tested it in Wine, but still needed to be tested
+in Windows. I had a working version done in Zig 0.10.0 that was running well in
+Wine, but it exploded in Windows because of [this issue][issue], that didn't
+happen in Wine. I needed to use Zig 0.11 because of this error, which wasn't a
+big deal anyway, because I already had it more or less packaged.
+
+So, yes, you have to test in Windows just in case, if you can. I have to thank
+my friend (you know who you are!) for testing this program in his computer and
+reporting the error.
+
+[issue]: https://github.com/ziglang/zig/issues/8943
+
+### Conclusions
+
+The program itself it's really underdeveloped, but I actually made a lot of
+progress understanding how to make a program reach users in different systems
+easily. In the end, **being a solo developer forces me to be clever, and do
+everything as simple as I can**.
+
+I don't normally have many dependencies because I believe software has to be
+simple and tailor-made. This makes me control every aspect of the projects I do
+but also greatly simplify the distribution. That's my context.
+
+I already talked about this in the [previous post on the subject][win], but in
+the end, being able to do this relieves me from the "Web is Cross-platform"
+mentality, which I don't think is a silver bullet, even though in some cases
+might be a simple solution.
+
+I think developers today tend to avoid making native applications and software
+quality suffers from that. There are many reasons for this (corporate control,
+subscription systems, easy deployments, controlled environments...) but one
+might be that the code people is used to write has too many dependencies, and
+it is hard to package and distribute. I don't usually have that problem. I
+already said I don't like having many dependencies.
+
+Now I have an easy way to do this, I can simply focus on writing the actual
+code, which is, in the end, the most important part.
+
+Key points:
+
+- **Zig** happened to make this process really simple, just because the
+ developers decided to make it simple and they had the engineering skill to
+ back the decision.
+
+- **NSIS** gave me all I needed to put my application in a windows machine
+ without learning almost anything about Windows. I have the enough information
+ to make the thing work, not more. **NSIS** was the key I was missing.
+
+- **Guix** lets me cross-compile some of the dependencies to mingw, and I did
+ that at the beginning (and it worked!) so it's a powerful tool even for that.
+
+- **Wine** is a good way to see everything was working well, but I found some
+ discrepancies between it and Windows.
+
+The full problem is not completely solved yet. Finding a **GUI toolkit** I can
+cross compile easily is still in the TODO list. But I'm pretty satisfied when
+the result until now.
+
+> My colleague Andrius Štikonas talked to me about
+> [MXE](https://github.com/mxe/mxe). I may try it in the future but I don't
+> like the fact that it downloads things by itself. I leave it here just in the
+> case I need it in the future.
+
+In fact, I think I can try other programming languages, even interpreted ones.
+**NSIS** is surely capable of dealing with it. This opens a whole world of
+possibilities for me.
+
+In the end, everything has been a very interesting process, making applications
+targeted to the non-programmer (and non-gnu/linux) user has always been in my
+mind. Now, I can say I almost solved the problem and I have the base to work
+more on this in the future.
+
+Finally, the program is released in a very alpha stage in
+[itch.io](https://ekaitz-zarraga.itch.io/karkarkar) where you can find the
+installers and packages and the code is hosted in
+[Github](https://github.com/ekaitz-zarraga/karkarkar/) until I get completely
+mad and I delete my account entirely (which I have no doubt it will happen one
+day or another).
+
+And that's mostly it, now I can focus on extending the behavior.