Porting NEXTSPACE to FreeBSD: A Seven-Phase Journey
- 10 minutes read - 1981 wordsThis is Part 2 of a three-part series on my 2025 obsession with NeXTSTEP, OpenStep, and NEXTSPACE. In Part 1, I introduced the project and what drove me to port NEXTSPACE to FreeBSD. Here, I’ll walk through the technical journey itself.
The Slow-Boiled Frog
We do these things not because they are easy, but because we thought they would be easy.
Attributed to Maciej Ceglowski
Starting Off with Claude
As of this summer, I had been trying various new laptops and putting them through their paces as far as being my new “Just Focus” machine. While testing various hardware platforms for FreeBSD support, I started leaning on Claude’s chatbot to get information about how to get diagnostics or correct bugs as I was getting oriented in the platform. Claude’s responses were largely correct and allowed me, in my small sessions to make progress.
While I was comfortable with my Wayland + Sway desktop environment, I started to want a lightweight, but also helpful desktop—something that could set my clock, change system volume, pick a screen saver. But I also wanted the interfaces to be unobtrusive and easily customized (eventually) for FreeBSD. What I really wanted was that NeXT experience.
While I was looking for that, I came across Sergeii’s NEXTSPACE…for Linux. I was struck by the quality and clarity of Sergeii’s codebase and figured that it couldn’t be that hard to port an awesome Linux application to FreeBSD ("because we thought they would be easy").
Phase I: Installer shell scripts
In this initial phase, I was using my knowledge of shell scripting and Claude’s web chat to migrate and debug NEXTSPACE’s installer shell scripts. This was very much the style of development I’ve done for years. I edit something, I keep my wiki and reference documentation handy. Effectively, the locus of knowledge is my head (or notes). But with the help of AI, I was able to remember long-forgotten patterns of string testing, return status checking, etc. I didn’t have to keep documentation open to make tiny changes.
Additionally, Sergeii had written the scripts in bash shell and FreeBSD
prefers the portable sh shell. Again, Claude chat came to the rescue here to
port the idioms of one standard to another. That was pretty great, it was very
empowering, and it was work that I could accomplish in the duration of a
toddler’s nap.
Phase II: Embrace the hoosegow: jail-based development
As I’ve written several installers for Unix systems, I found that one critical problem gets in the way like none other: given a pristine machine, your first (failed) attempt to install will mean you can’t truly test your code on a clean machine again. You spend as much time trying to get back to guaranteed clean state as you spent trying to change the system state to a new direction.
Fortunately, FreeBSD provides jails. These are basically isolated file trees on
your “host” FreeBSD that look like a complete Unix system. Think of them as
very lightweight virtual machines that boot instantly, cost nothing to destroy,
and can be duplicated with simple commands like cp (watch this). If my
installer script bricked the jail, I merely deleted the jail and started fresh.
No reimaging, no multi-hour OS reinstalls, no “well, it’s almost clean”
compromises.
Phase III: Snapshots
Remember how I said to keep an eye on cp a paragraph ago? Well, here’s where we
streamline jail-based development and swap the crude cp for snapshots.
The most common file-system used in FreeBSD is zfs (“zed-eff-ess”), which has
a superpower: you can snapshot a file system tree at any point and name it.
Think of it as a save-game checkpoint in a video game. For example, a snapshot
called after-gnustep-install takes a fraction of a second to create and
stores the exact state of the jail with a meaningful name. If the next script
run fails, crashes, detonates halfway through, or halts due to an unmet
dependency, one can roll back to the moment of the snapshot. It’s like
keeping a finger on the page of the Choose Your Own Adventure book when you’re
not sure whether the next narrative node will be a good step forward. I leaned
on this heavily: I had snapshots named things like after-script-3,
before-font-fix, pre-threading-experiment. The names tell the story.
Phase IV: Peanut butter meets jelly
Hopefully you can see where this is going.
The workflow I landed on was: edit code on my host operating system, share that
file tree into the jail via a mount, snapshot the jail, then edit / debug /
test inside the jail. If things went sideways, roll back to the pristine
snapshot and try again. When a script finally worked correctly, I’d snapshot
the new good state—after-script-2, say—and keep iterating forward from there.
This meant I could experiment aggressively. There’s a psychological difference between “if I get this wrong I lose an afternoon” and “if I get this wrong I lose ten seconds.” The jail-plus-ZFS harness collapsed that cost to nearly zero, and it’s a workflow I’d recommend to anyone doing this kind of systems work—AI-assisted or not.
Phase V: Chase the Bugs
Once the development harness was in place, I could dive into the real work of making NEXTSPACE actually build and run on FreeBSD. And here’s where Claude really started to show value. You see, the NEXTSPACE codebase is divided into a number of applications written in Objective-C. It’s the language I chased many years ago while learning to program. I’ve never shipped anything of size in it.
It’s also the case that the window manager was written in C. This is a language that I have an understanding of, but no real comfort. I’ve never shipped anything of size in it.
And it just so happens that both of these codebases inside NEXTSPACE were
assembled by GNU Make. Now I’m not even that comfortable with regular BSD
Make that I’ve been, uh, using but not really understanding for decades. So
here’s a different dialect (say, Italian to Spanish) that I had never even
worked with.
And recall, all that is on top of shell scripting which I, by chance, happen to be decent at.
These areas of familiarity shortfall should have cost me lots of time. It
should have made this project impossible. But it didn’t. Why? Because I
had search results better than Google; I had a peer that could help me parse
dense man pages and generate instructive examples; I had something that could
take GNU documentation out of their bat-shit preferred documentation format
.info; I had Claude.
The bugs were concrete and sometimes humbling. A broken image path could break startup. There were issues around multithreading and the event loop. There were issues with font pickers not applying changes as I expected from the docs.
Slowly, by chat, by edit, by commit, by rollback, by roll-forward, I kept falling forward to success.
Phase VI: Integration is the Hard Part
While this human “stayed in the loop” the whole time, once all the pieces were installed, there were some real snags. Many of them had to do with library dependence order, installing fonts, and handling the eventing queue. By mid-August 2025, I had a working proof of concept.
First flickers of success
It took another 10 weeks or so, but eventually I got some of the hardest bugs worked out and got to this milestone.
All the applications running on a single thread
I thought I was pretty much home free at this point. But then I realized I’d made an error: I had all the work happening on a single thread. This meant that certain applications weren’t going to work. That is, if I dragged a file into a recycling bin on the main user interface thread, the background thread that actually does the removal wouldn’t run. I found that I’d disabled this in development to keep moving forward. Whoops. So turning it back on shouldn’t be a big deal…but, oh, I was wrong. So wrong.
Interlude: Claude Code
Before I describe Phase VII, I need to introduce the tool that made it survivable.
In May 2025, Anthropic released Claude Code: a text-based interface (yay) that could process output and introspect into files directly. It eliminated the non-ergonomic cycle of running code, seeing errors, copy-pasting into a browser, reading results, implementing them, and retrying. Instead of shuttling between terminal and browser, I had a single agent that could read the source files, understand the build system, and make edits—all in one place.
I started working with Claude Code and quickly realized it could be a force
multiplier. Running it in my source code root, I could say: “I think I have a
multithreading bug in this file, add print messages everywhere so I can see
what’s going on.” The result was a commit I simply called
Recycler regression: more logging—not glamorous, but exactly what I needed.
Claude made the execution path chatty like a preschool 5 minutes before lunch.
It made it non-painful for me to start accumulating data.
Phase VII: The Demons of Multithreading
And that’s when I started having multithreading bugs. As a class of bugs, these
are some of the hardest to battle because news of what failed and when it
failed not order may in the correct arrive — er, may not arrive in the
correct order 😄. Honestly, I was completely despairing at this point.
Under the old model of development and support, I was going to have to browse a thousand condescending forums and email lists. I was going to have to, I figured, go back and learn those three giant areas of knowledge (Objective-C, C, and GNU Make) so that I could have enough context and kudos to be able to ask for help. It was really dispiriting. I think my mental state comes through in this update:
WindowMaker [the underlying desktop package NEXTSPACE rests on] is from a single-threaded era and, in a number of points it tries to flush changes via X (Xlib, XCB, whatever). It doesn’t know that another process in another thread may have done something that it doesn’t know how to deal with.
I don’t know how this wasn’t a problem on Linux. And for whatever reason, in my prior hack-it-together approach, I somehow didn’t hit thread errors. Maybe I installed something wrong.
Having accumulated the data from all that logging, I realized this was a really hard problem. That update was a cry for help—but also the moment before I started leaning even more heavily on Claude Code. I was so close, but I was not going to have the time to level up in all three required disciplines before the new year.
But helped by Claude Code, having it instrument all the code paths and build
solutions (a step toward “agentic” or “autonomous” code patterns), I started
to find the seams. The critical commit was Workspace: Uncork thread displaying desktop/menus—a single targeted change that re-enabled the background thread
I’d been suppressing, paired with enough synchronization fixes to keep
WindowMaker from tearing itself apart. There was also a Spin fix that
addressed a busy-wait loop burning CPU while the desktop was idle. Neither of
these would have been obvious to me without Claude Code helping me read the
call graph and trace what was actually happening across threads.
Roughly a week after that cry-for-help update, I had a developer release ready.
NEXTSPACE running on FreeBSD
Before the month was out, Sergeii chimed in to recognize my effort and even used my codebase to try running his creation on FreeBSD as well. He’s been exceedingly gracious and we’re going to see if we can merge my work with his architecture to have Linux and FreeBSD as supported platforms!
In Part 3, I reflect on the broader implications of AI-assisted development and make the positive case for this new way of working.