If you have done and OS X/macOS development, especially any that predated the Mac App Store, you are probably aware of Sparkle. Even if you haven’t done any development, you have probably used Sparkle because it was basically the de facto method of providing update functionality in Mac Apps, and even to this day is still widely used on many apps distributed outside the official App Store.
Updates are distributed to applications by means of an “appcast”, an extension of the RSS specification containing information about updates. RSS itself is based on XML, which means you can build them just like you would build any other published document.
The problem comes when you start having a lot of updates in an appcast. Maintaining a large file can become difficult. But fortunately, using Jekyll collections, we can generate a single appcast using multiple files that are much easier to maintain. And, as an added bonus, we can use that same data to generate a download and changelog page from the same data.
Collections
The first step is to set up a Jekyll Collection
for our releases. Each release will be accompanied by a separate document in the
collection specific to that release. To add this collection, edit your
_config.yml
file to add the collection.
collections_dir: collections
collections:
example_tool_releases:
permalink: /example-tool/notes/:name.html
output: true
An important piece to notice there is the output: true
part. This will render
the documents as individual files at the permalink
location.
Making Updates
Each Sparkle release needs to be accompanied by some additional data:
-
edSignature - A signature for your update. Sparkle requires you to sign your updates to protect your users against malicious updates.
-
version - The new version, which Sparkle uses to compare against the current version to determine whether an update is needed.
-
length - The side of the update in bytes.
That is the minimum you need, but you can set additional options as well. Generating these pieces of data is beyond the scope of this post, but is pretty thoroughly documented at the above link.
For each release, we are going to create a note
file in collections/_example_tool_releases
to accompany the release, with the above data set as YAML front matter and the
release notes themselves in the body.
So, for example, to create a 1.0.2 release of our example tool, we would create
the file collections/_example_tool_releases/1.0.2.md
and edit it to look
something like this:
---
name: 1.0.2
date: Fri, 29 Mar 2019 03:19:19 +0000
download: https://example.com/example-tool/example-tool-1.0.2.dmg
signature: <really long signature here>
length: 70979697
---
* Fix: no default tile size was set, resulting in no tiles being shown.
* Fix: bad data from the server could crash the client.
* Fix: changes didn't take effect until restarting.
* Fix: crash in image downloading.
* Disabled sandboxing.
You can create as many updates as you like as separate files. You could even have a build phase as part of your release that generates the markdown files from commit messages. And, as these are just standard Jekyll markdown files, you can use all the usual Jekyll and Liquid functionality to create complex representations (though be aware of the limits of the display medium inside the Sparkle update box.)
Generating the Appcast
So now that you have all these documents, we need to generate the appcast itself. Earlier, we said that the appcast is just an extension to RSS, which is itself based on XML. Knowing that, it is simply another standard Jekyll document that can use all the same liquid functionality.
So you could create a file called /example-tool/appcast.xml
(or wherever your
app is looking for its appcast at) like this:
---
baseurl: https://example.com
---
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" xmlns:dc="http://purl.org/dc/elements/1.1/">
<channel>
<title>Example Tool Changelog</title>
<link>{{ page.baseurl }}/example-tool/appcast.xml</link>
<description>Most recent changes with links to updates.</description>
<language>en</language>
{% assign sorted = site.example_tool_releases | sort: 'date' | reverse %}
{% for release in sorted %}
<item>
<title>Version {{ release.name }}</title>
<sparkle:releaseNotesLink>
{{ page.baseurl }}{{ release.url }}
</sparkle:releaseNotesLink>
<pubDate>{{ release.date }}</pubDate>
<enclosure url="{{ release.download }}"
sparkle:version="{{ release.name }}"
sparkle:edSignature="{{ release.signature }}"
length="{{ release.length }}"
type="application/octet-stream" />
</item>
{% endfor %}
</channel>
</rss>
The important thing here is to keep the ---
at the top of the file so that
Jekyll knows to parse the file for liquid tags. And, if you defined any
additional front matter in your markdown files, you can add the appropriate XML
tag to the loop here.
Generating A Changlog and Download Page
We can also take this same data and use it to generate a nice HTML changelog and download page:
---
layout: default
title: Example Tool
---
<div class="row">
<div class="col">
<h4>Download</h4>
{% assign sorted = site.example_tool_releases | sort: 'date' | reverse %}
{% for release in sorted %}
<h5><a href="{{ release.download }}">Version {{ release.name }}</a></h5>
<div>Released {{ release.date | date: '%B %d, %Y'}}, {{ release.length }} bytes</div>
{{ release }}
{% endfor %}
</div>
</div>