Cyanote is written on a Mac. A Windows build exists, and every line of Windows-specific code in it was written on a machine that cannot execute it.
That is a normal situation for anyone shipping cross-platform software without a second desk, and it changes the work in ways I did not expect. Not "it is slower" — it is slower, obviously. It changes what you are allowed to believe, and where the verification has to live instead.
The compile error that sat for a week
The clearest example is a RunEvent::Opened handler. That event is the Apple Event the Finder sends for Open With. The variant does not exist on Windows, so the Rust source did not compile there at all.
Not "behaved differently". Did not compile. For a week.
The reason it survived a week is the part worth telling. The same commit that introduced the handler had also set the bundle targets to a macOS-only value. So the Windows CI job dutifully built, produced no installer, found nothing to complain about, and reported success. The compile error underneath had nowhere to surface, because the job that would have hit it had been quietly configured to stop before it got there.
This is the green tick that means nothing in its purest form: a job that runs, passes, and exercises none of the thing it is named after. On a platform you can build locally you would have caught it in thirty seconds. On a platform you cannot, CI is not a safety net — it is your only eyes, and an eye that reports success while looking at nothing is worse than a blindfold, because you plan around it.
The wrong guess, repeated in four places
The second one taught me something about how a mistaken assumption spreads.
The Windows release build produced exactly what it should: an installer, and a signature file beside it. The verify step failed it anyway, for missing a .nsis.zip and its signature.
That pair is the Tauri v1 layout. Version 2 signs the installer itself, so the .exe is the update payload — there is no zip and there was never going to be one.
The interesting part is that the wrong guess appeared in four places: the verification step, the artifact upload, a flag in the script that writes the update manifest, and the build documentation. Not four independent mistakes. One assumption, made once, then copied by each subsequent piece of work that needed to know what the file was called — because each of those pieces was written by looking at the previous one, which is how anyone works.
If you cannot run the thing, every downstream step gets its facts from an upstream description of the thing rather than from the thing. Get the description wrong and it propagates at the speed of copy-paste, and no single site of the error looks wrong when you read it.
The fix included a small durable improvement: on failure, the step now lists what the bundle directory actually contains. The next mismatch names itself instead of only saying what was absent. When you are working blind, an error that describes reality is worth ten that describe expectations.
Where verification has to live instead
If you cannot run the code, the checks have to be things a compiler or a test can do without a Windows machine.
The clearest case is clipboard privacy. Windows has its own convention for marking a copied secret as not-for-recording — two registered clipboard formats, one whose mere presence is the signal and one carrying a number where zero means keep it out.
None of that logic can be executed on a Mac. So the rules live in a separate module with the classification written as pure functions over the marks, and those functions are unit-tested. The tests cannot prove the formats are registered correctly on a real Windows machine. They can prove that, given a set of marks, the decision is the one intended — including the case that matters most, which is what happens when the marks cannot be read at all.
That case is fail-closed: anything unclassifiable counts as private and is dropped. Losing one clipboard entry costs a user one Ctrl+C. Storing one password they thought was protected costs them the password. When you cannot observe the running system, you choose the default that is wrong in the cheap direction.
The transparency that could never be seen
Not everything was a bug. One was pure waste, and it is the kind you only find by reading.
The main window was configured transparent, with Mica — Windows' backdrop material — layered on. Neither could ever be seen, because the app paints a fully opaque background outside macOS. Two visual effects, cancelled by a variable somewhere else, invisible in every screenshot anyone had ever taken.
The cost was not invisible. A transparent window puts the webview on a composited path, and Mica adds desktop compositor work on top. That is the likeliest reason animations felt less smooth on Windows than on macOS — a performance cost paid continuously for an appearance nobody could observe.
Two related discoveries came out of the same read. The configuration listed four window effects in sequence, and the framework applies only the first one it recognises — so two of the four were unreachable, and one of the others is a macOS material that means nothing on Windows. A list that looks like belt and braces can be a list where only the first item was ever running.
Two windows do stay transparent, deliberately. One of them is the fullscreen focus overlay, whose page background is transparent so that if the interface fails to render, the user is left with nothing covering their display rather than an opaque sheet they cannot get rid of. That is a failure mode worth designing for, and it is the reason "make every window opaque" was the wrong instruction and "make the main window opaque" was the right one.
What I would tell someone starting the same port
Make the platform job fail for the right reason. A build job that produces no artifact is not a check. Assert on the output — that a file exists, that it has a signature, that its name matches what the updater expects — because the compiler error you actually care about lives behind the step that got skipped.
Write platform rules as pure functions. Anything you can express as data in, decision out, can be tested on the machine you have. What is left is the thin layer that talks to the system, and that layer should be as boring as you can make it, because it is the part nobody can check until it is in front of a user.
Choose fail-closed defaults everywhere you are blind. You will get something wrong on a platform you cannot run. Decide in advance which direction the wrongness should point.
Expect assumptions to have children. When something is wrong in one place and you cannot verify it locally, go and look for the other three places that learned it from the first.
None of this makes the port cheap. It makes the parts you cannot see behave like parts you can reason about, which is the most that is available. The rest — signing, notarising, the whole apparatus of making an app openable on a machine that has never heard of you — is its own separate expense, and on Windows it is still waiting on a certificate.