I'm thinking about something: theoretically, would...
# compose-desktop
u
I'm thinking about something: theoretically, would it be a problem to create an auto-update system that only detects modified JAR files and only updates those JARs? That would allow for really lightweight updates, wouldn't it? Is there something I'm missing? Maybe just managing the update of the version installed by the operating system?
not kotlin but kotlin colored 1
h
That is exactly what a smart updater does, like the one from IntelliJ that also does not update the version presented to the OS.
u
No, IntelliJ downloads everything; you might be talking about plugin updates.
h
But the patch updates are small, aren't they? 🤔
These update are only a few mb instead of a full download
u
I didn't notice, maybe, but it doesn't seem so to me.
h
Hm: the patch from 2026.2 to IntelliJ 2026.2.1 is about 200 MB, but downloading 2026.2.1 from the web is 1.5 GB (including the JVM runtime)
u
Yes, so they must have done something like that. I don't really see the difference; I have a 2.5 Gbps connection.
h
You can still check the file sizes 😜
u
Yes, I can measure it too, but I'm just saying I didn't notice it like that.
h
But I forgot the updates for the previous bundled plugins, that's another 500 MB. The plugins are distributed as a jar, so you cannot easily patch the individual files (well, technically you could).
u
To be honest, it's not very relevant to what I want to do for Compose Desktop
The advantage would be being able to push patches of only a few MB.
s
IntelliJ updates are delta updates — I am not sure they contain actual jars, I think it's pure deltas
Which is what you normally want — don't focus on JARs as units, focus on file deltas
It took Apple 15+ years to understand this on the iPhone, but maybe we can build something faster lol
😅 1
(this is also how Android updates have always worked)
u
But I wanted something fairly simple to manage and understand. A delta system requires comparing each version and generating deltas between each version, whereas here you would just need to compare the hashes of the JARs. That seems much simpler and more than sufficient, doesn't it?
m
Conveyor does this on macOS and Windows.
🦜 1
m
Conveyor already did that with deltas, update if just few MBs
m
The effectiveness is somewhat limited by the fact it doesn't currently track deltas across file renames, so e.g.
foobar-lib-1.2.jar
being renamed to
foobar-lib-1.3.jar
and changed simultaneously causes the file to be emitted into the delta as a whole. That's an easy fix, we just didn't fix it yet.
s
> whereas here you would just need to compare the hashes of the JARs Well you still need the hashes before/after, seems like you just shift the problem from storing the previous release (or rebuilding it if you have reproducible builds), to storing their hashes
u
@seb Well, it's much simpler. I can calculate the current hashes at runtime, and then I just have to publish JAR files with their hashes to the server, and there's no need for deltas between each version. I'm not sure I understand.
s
Fair, different storage requirements if you need to store the last release, but pretty simple to do... and if you don't want to, reproducible builds solve that (or, you know, just downloading your current published build from your downloads server and diff that)
u
Yes, it doesn't matter how we do it, the principle is that if I release 3 patches per week, I don't want to end up with 15 deltas to build each time. Just a folder with JARs inside, and a JSON with their hashes seems by far the simplest solution, and a comparison. The main JAR is only a few MB without the libraries, in principle, so I don't really see the point in bothering with a real delta system.
Once you remove the JRE, Skiko, Compose, and all the libraries, the size of the remaining JAR files should be very light.
m
Isn't this a problem which the electron updater has solved already? It may be more complicated but it is also more general and it already exists. I use your auto-update feature but haven't activated delta updates yet. So I don't know how reliable that is.
u
I currently have no delta update system in Nucleus
Nucleus manages auto-updates like the vast majority of Electron applications, by re-downloading the entire executable. However, on macOS, it uses a zip file, but it's the entire application that is re-downloaded.
Nucleus has fixed the fact that there was no easy way to silently update your application with Vanilla Compose; you had to do everything yourself on all three operating systems.
m
There was an easy way, you just didn't want to use it Elie 😉
😂 1
m
Now you are confusing me. Isn't that what all these generated block maps are all about? At least that's still created in the 1.x version of Nucleus.
u
@mikehearn Ah yes, I use it like 99% of desktop apps today; it's not a real problem, which is why I haven't undertaken the development of such a thing.
@Michael Paus No, it's for the current auto-update system. That's how Electron Builder works.
A delta system isn't just about maps; deltas need to be generated between each version. Since this becomes impossible because the deltas increase with each version, and we're not going to generate 100 or 1000 deltas with each update, we use a logarithmic system. I've already had to implement this for an SQLite database; I don't have very fond memories of it.
s
fwiw the solution is to offer deltas from 1–2 previous releases, and full downloads for older ones. That's how IJ/Studio do it, too
u
Yes, but if you're pushing patches like a website, every day, well, that's not enough. That's why I liked my approach with JARs.
s
if 90% of updates are from 1–3 previous versions, that is what you should worry about
u
and much cheaper in terms of ci
s
Why would you want to spam updates like that
it's bad for AV/EDR churn at a minimum
changing signatures mean enterprise users with strict monitoring systems will get a "you need approval to run this" multiple times per day
much cheaper in terms of ci
if you ship without testing on CI yes, otherwise, no
m
Normally AV/EDR cares about the signing identity, not the hash of the files. Shipping every day should be fine as long as users actually stay up to date, which is what the aggressive updates mode in Conveyor is about.
👍 2
s
That's the Chrome-like update daemon?
u
Why would you want to spam updates like that
I think this is a use case that can be necessary in certain situations; if you have a bug that you've fixed, you don't have to wait until the end of the week or month to patch it.
s
Normally AV/EDR cares about the signing identity, not the hash of the files.
You are blessed 🙂 My corp MDM would 100% flag that
I think this is a use case that can be necessary in certain situations; if you have a bug that you've fixed, you don't have to wait until the end of the week or month to patch it.
Hotfixes should be rare and there is no reason to delay them. I'm just saying that is not the norm and not a scenario you should design around as the primary one
m
Aggressive updates in Conveyor works differently on macOS and Windows. On Windows there is a Chrome-like update daemon provided by the OS itself which does keep apps up to date in the background. The 'aggressive mode' does a synchronous version check at startup and then forces an update if the OS didn't get around to doing a background update yet. So you know when you start the app that you're up to date, and there's an API that lets the app force an update on itself. On macOS there's no background daemon. The update checks and downloads are done by the app itself in the background. Startup check is the same. This is where small delta updates are really useful. Users don't want to sit staring at at progress bar when the update is synchronous and forced.
👍 1
s
Out of curiosity, on macOS do you get Gatekeeper re-checks when you do delta updates? I guess not, but curious
u
Hotfixes should be rare and there is no reason to delay them. I'm just saying that is not the norm and not a scenario you should design around as the primary one
I completely agree, and that's why I didn't do it, by the way.
m
No. Gatekeeper only checks apps that are quarantined. The kernel does enforce that code is signed on every launch, however.
👍 1
With respect to the original question, the problems are primarily around native code and protecting your code signing identity. It's not uncommon for Java JARs to include shared libraries that are extracted and loaded on the fly. If you start using such a jar, or upgrade to one, then that is really when AV engines and macOS get upset with you, because extracting unsigned or wrongly signed native code and loading it from %TEMP% looks a lot like what malware does. And it's not how you're meant to do things. Dynamically loading JARs from a user-writable location is also getting more dangerous because it lets other apps inject code into yours, meaning permissions can't be meaningfully granted to your app. On macOS this is a big no-no, on Windows they'd like to make it into a big no-no but are so far behind that they can't really do that today because not many apps use MSIX or package identity. But the core OS permissions and privacy infrastructure increasingly only works for apps that do implement package identity, meaning, app data files can't be writable by the user. Conveyor's delta updates works with all this complexity and security, but hand-rolled "just download new jars" won't.
h
Well, Square/Cash had the same problem, updating business logic in their iOS apps takes days due to the review process, so they use a completely different way: They split the business logic from the “app”. Instead of running and updating static JVM/Kotlin native bytecode that contains the business logic and the UI, they use a JS engine to execute the business code at runtime, but use the “shipped” code for the presentation. (The presentation also uses Compose, but it is a implementation detail). The business logic is also written in Compose code but using molecule, and it is written in Kotlin/JS (or multiplatform) for JUnit tests (better IntelliJ test support). The in app updater only updates the JS code, but you still need “real” updates for the native view code.
So it is basically server side configuration, but not limited to configurations/rules but the whole business logic.
m
Yes they basically write their own custom "browser". It's a not uncommon approach.
s
I know of a company in Germany that did that 10+ years ago, really not novel
In their case was mainly because they had a complex algo to run on-device on sensitive user data that a) needed updating out-of-band in some cases, and b) they did not want to maintain two separate versions of (with misalignment risks)
u
That's exactly what Jack Wharton's Zipline does.
h
Yes. Zipline is the updater and uses quickJS as runtime. Treehouse is the whole framework
u
@mikehearn That's actually what my question was from the start — what are the pitfalls I should think about, so thanks. To be clear, I'm not planning to load jars from a data directory: the idea is to patch the installed app itself so it ends up byte-identical to the CI-signed release, CodeResources included, like a Sparkle delta does. Nucleus already signs natives in place inside the jars on macOS for notarization, so that part exists — the Windows equivalent is what's missing. The apply step would be a small signed helper that runs after the JVM quits and re-verifies everything, so no locking issues. Anything else in this category ?
m
Nuceus already signs natives in place inside the jars on macOS
Do you mean the app sign itself on user computer?
u
No, nothing signs on the user's machine — keys never leave CI. It happens at build time: the Gradle plugin rewrites the dependency jars during packaging, extracts each dylib/jnilib entry, codesigns it with the Developer ID cert and writes it back into the jar. So the jars ship already signed and notarization passes.
m
In my opinion, having a fallback mechanism would also be nice for the times that advanced auto-update (incrementally) is turned off (by developer or user) or fails due to non-network reasons. I've seen many times Google Play start updating an app, and for some unknown reason, it somehow started downloading the whole app. I guessed the previous update was not successful and it fell back to re-downloading the entire app. I'm not sure about that; I'm just guessing.
u
Yes, that's why for now I'm not doing per-jar deltas at all — I'd rather have users re-download 50 MB and have it work every single time. In a generation where a single YouTube short weighs more than my entire app, I don't see it as a major problem, so it's a minor priority for me. I think a good first step would be updates that skip the JRE and Skiko when they haven't actually changed — a simple hash check, and if they were bumped in the release you download them too. Out of 50 MB they probably represent 40, so that alone would already be a very solid incremental update, without any of the delta complexity.
m
Yeah, for your use case, I get it. But most of the really useful desktop apps, like Intellij or local AI stuff, easily hit 500MB to 1GB. So for those, they'd need to build a custom in-app updater, just like you said.
u
IntelliJ is a very special case; it's indeed one of the biggest apps out there. For other apps, it's different, I think, because most of the time, this kind of update happens at runtime.
In my case too, my app downloads 4GB of database data, but I have no interest in linking it to any system installer; quite the contrary, doing this at runtime allows me to run common code on Linux, Mac, and Windows.
s
Electron uses blockmaps. it keeps a cached version of the previous installer and uses the blockmaps to download the updated chunks and patch the installer. That works between any two versions.