summaryrefslogtreecommitdiff
path: root/content/windows2.md
blob: b151f047ceb3811f165461715a151d2bb365e18e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
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.