I cope with my depression by finding distractions, and what better way to do that in the long run than by learning something new or rediscovering something I thought I’d forgotten? These are often small projects that can be completed in a few days or even a few hours. Sometimes I can’t sustain my interest, or unexpected costs come up that I hadn’t planned for, and then it’s over before it’s really even begun. But sometimes a small idea can turn into a project that keeps me captivated for weeks, and that’s the kind of project I’d like to share here. This won’t be a strict timeline, but rather a series of connections between decisions and events.
In the late 1980s, I completed a retraining program to become a data processing specialist and, as part of that training, learned COBOL and C. Today, you can quickly satisfy your curiosity by installing the GNU Compiler Collection (GCC) with a suitable toolchain, which is available for both languages. Although you can quickly put together a “Hello World” program in COBOL, the potential benefit for personal use is rather marginal with a language that was explicitly developed for business logic, so I focused on C. After taking my first steps with sample projects and tutorials, I just needed to find a task that would make it worthwhile to stick with it even if things started to get rough. Just a few weeks ago, I wrote about my welcome.sh script, which I’ve been using for a very long time and have to adapt for every platform. Originally, the reason for writing the Bash script was to get rid of the annoying MOTD (Message of the Day) messages that appeared whenever I logged in to a server or a virtual machine (VM) via SSH. Instead of constantly being told the exact name of the kernel and where to look up help for the distribution, I’d much rather see if the system is having any issues or when I last logged in to install updates.
So I started typing a few lines of C in Notepad++ and feeding them to the GCC in an Ubuntu 24.04 environment running in WSL2 on Windows 11. Since Windows acts as a bridge between the systems, it’s quite easy to code in a Windows program like Notepad++ and compile under Ubuntu. In File Explorer, simply open the \\WSL$\ share and navigate through the WSL distribution (Ubuntu-24.04) to your home directory, create a new directory for your project, and a new file for the source code (main.c). You can now edit these files easily with any Windows editor. You can compile and run the code in a terminal where Ubuntu WSL is running:
gcc main.c -o hello
./hello
Sooner or later, constantly switching between windows does start to get on your nerves, but the need for an integrated tool becomes unavoidable when it’s almost impossible to narrow down errors without debugging. I’ve already installed Visual Studio Code (VS Code) for various smaller projects, so all it takes is typing code . in WSL within the current project directory to open it in the IDE. I’ve installed the appropriate extensions from Microsoft there (C/C++, C/C++ DevTools, C/C++ Extensions Pack, and CMake Tools). Now, all it takes is pressing the F5 key to build a debug version of the binary and start debugging. You can set breakpoints and check variable values at runtime—a huge help. For production testing, I initially continued to launch the gcc compiler manually. This wasn’t often necessary, though, since VS Code uses a terminal within WSL, so debugging takes place in the same environment.
At some point, I got it into my head to organize the source code more clearly and wanted to move parts of it into separate files, grouped by topic. I created a file for code segments that I had copied from examples and tutorials and might want to use, one for collecting system information, and one for output. The last two were simply included in main.c using an #include directive. However, the compilation command was now getting noticeably longer, and as I experimented with additional parameters, I made mistakes when rerunning it that were hard to spot. It was time to think about a reliable, repeatable build process, so I created a Makefile for the project. I can’t say whether I needed help because of the problems I’d created myself during compilation or because of the creation of the Makefile, but that was the first time I consulted an LLM for this project.
Splitting the source code into multiple files by topic certainly makes the application easier to navigate, but anyone who has ever made a series of changes in different places only to find that a project no longer compiles will appreciate being able to undo such a change with a single command or revert to an earlier version. Git has the answer—and since I’d migrated all my repositories from GitHub to Codeberg just a few days earlier, the “Hello” program became my first repository hosted exclusively on Codeberg: Linux-Hello. Up until then, I’d ignored the Git command line, so I have to admit that I wasn’t able to create the repository from there. In the middle of programming, I also lacked the motivation to figure it out and resisted the idea of asking the assistant I’d just hired. So I created the repository in Codeberg’s web GUI and then cloned it next to the existing project directory (hello). It’s okay to be a bit clueless—you just have to know how to get help (there are many ways to Rome). Now I could simply copy the most important files from the old project directory into the new repository and then fill the still nearly empty Codeberg repository with a commit and a push.
At some point, I noticed in Codeberg’s web GUI that there were files there that didn’t belong. In a local repository, artifacts generated during debugging and similar processes aren’t a problem, but in a shared repository, you don’t want to see them. I had deliberately committed the hello executable created by the Makefile because I wanted it in a release archive, but a main executable generated by VS Code debugging?! So it was time to take a close look at the .gitignore file and then clean up the origin.
As described above, the build using the Makefile generated a file named hello and placed it in its own directory (/bin), while VS Code saved its output right next to the source code in the /src directory. Although this wasn’t pushed anymore, aside from the irregular path and name of the binary, VS Code used its own toolchain. Debugging was thus performed with different settings than the production build, and that increasingly bothered me. So I asked my assistant if we could somehow convince the IDE to also use the Makefile—and thus my chosen toolchain. It turned out to be surprisingly easy, and I learned something new about my favorite IDE in the process. While I wouldn’t be able to configure these settings from memory in a new project, I now know enough that I should be able to figure it out with a quick web search (copying and modifying the two files from the /.vscode directory would, of course, be the easier option). Instead of focusing solely on debugging via make, we went ahead and made all the Makefile targets available in VS Code as well. This means it’s now possible to build a production-ready binary directly from within the IDE and run it in its terminal.
As an IT consultant, I know how important consistent documentation is in a project, and I use inline comments from the very first line. Once I’ve finished a function—or at least once it’s functionally mature—I create a DevBlock for it that clearly explains what the function does and, ideally, how and why. As the project grows, however, this soon becomes insufficient for documenting all aspects. If you try to document rules within the code itself, things quickly become confusing again. But instead of assigning a Markdown file to every file or topic, I decided to explore Codeberg’s wiki functionality, and so the project got its own wiki, which Codeberg conveniently also provides as a repository. We’re juggling more and more tasks at once. At first, I was inclined to write the wiki in my primary language, but then I thought, “Programming is done in English,” and started writing it bilingually right away. I write in German and translate it into English later. At that point, I also began cleaning up the comments in the code: there was a wild mix of English, German, and “Denglish” lines. While you’re writing them, you obviously don’t notice this to the same extent as you do during a later review.
I could already install the binary directly from the project using make install, and a small change to an IF statement allowed me to test the output on distributions other than the Ubuntu I was working on. But shouldn’t I run a test on a real Debian or Mint every now and then?! (Fortunately) I always try to do as much as possible myself instead of taking the easy route via a library, so aside from the standard C library, there are no dependencies I need to worry about. So all I have to do is copy the binary to a VM using SCP, and it runs right away—but that’s not exactly elegant. As if I didn’t have enough on my plate already, I’m now also dealing with Debian packages, looking into tools like dh_make and dpkg. Man, was that complicated — I was confronted with a whole new set of error messages. Until I asked the wizard how best to get started with something like this, and he recommended a minimal packaging setup: two files in two directories and a single-line command—easy-going—and that I should leave dh_make alone. I did end up using it, though, to create an empty default structure with templates for a package. That way, I was able to slowly expand the packaging for my project and learn something new again.
Continuous integration and continuous delivery (CI/CD) describe a process of automated integration and deployment directly from the development environment. While a project this small doesn’t need it—and I haven’t even come close to the best practices used by professional development teams—automated deployment is very appealing for creating and deploying software that’s traceable and reproducible. It eliminates the need for additional installations—which are sure to be all over the place on development machines—and can be locked down to a specific toolchain with defined versions. Surely there’s another wedding where we could go dancing?!
Okay, at this point I would have failed miserably, and I can’t take credit for most of it. Trying to integrate several tools—whose functionality I’m only just beginning to understand—and then use them to interact with an API I’m not even familiar with yet was perhaps a bit too ambitious. The idea was mine, but the assistant wrote the integration code. Thanks to my prompts, though, he’s always very considerate with me and explains why it works—not just how. I’d specified as a basic requirement for this project that I be able to follow every single step, and patience isn’t much of a challenge for an LLM. To follow the workflow yourself, take a look at /.forgejo/workflows/release.yml and the deb: target in the Makefile. The workflow is automatically triggered when a tag is created in the repository that starts with "v" and is executed entirely by Codeberg Actions. At the end of the workflow, a new release is created in the repository and the newly generated deb package is attached. This way, I can create interim releases at any time, which are then still named with just a release version and contain only the source archive.
I’ve tried out a lot of LLMs over the past few weeks, tested some of them in more depth, and I’m not done yet (you can read a bit about that here on the blog). It often starts with a simple question that can hardly be called a prompt, but which contains an implication. If I like the answer or if the implication is confirmed by following up with the LLM, I sign up for the service and, if available, also install the provider’s app to get a few more tokens with a free subscription. Claude was the first model from one of the major players on the market, and it surprised me with very targeted follow-up questions—it’s a good listener. So, for me, it was a logical decision to bring it on board as an assistant for the project. Claude supports its own project structure that not only allows you to bundle different chats but also to store files and specifications as context. For this project, one of the requirements was that I didn’t want a ready-made solution, but rather wanted to thoroughly understand every step—that this would be a project to relearn the programming language, and its goal was to ensure that. He adhered to this very well, even as the number of weddings multiplied. If it weren’t for the fact that it’s an American company—and data protection concerns often stand in the way—I would certainly use him more often.
Maybe I was using a sledgehammer to crack a nut, but it was never really just about replacing the existing Bash script. I wanted to find something meaningful to get back into C programming, and, driven by my curiosity, I got to know many other tools better or learned them from scratch. That would even help me in my profession, although the likelihood of ever getting healthy enough to return to it seems remote. If anyone ever happens to stumble upon my repository, the C program probably won’t be the reason why. It’s much more of an illustrative example of how integrated development can evolve within the project lifecycle, and each of the tools used is so well documented that the repo could serve as a starting point for a tutorial on any of these tools.
An anecdote to wrap things up: Since I’m so familiar with this project, I used it as the “first question” for another large LLM. A request for a security review of the repository (not the source code) led to a lengthy conversation, and together we came up with a task for the LLM based on that discussion. The LLM then wrote the task description—or prompt—for it itself. The goal is to conduct a review of the development process behind the repository. If it makes sense, I’ll publish it here on the blog. I’m really looking forward to it!
Feel free to chat with me on the Fediverse.