<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
  <id>https://jumplink.eu/en/blog/tags/macos/feed.xml</id>
  <title>Pascal Garber – macOS</title>
  <updated>2026-11-01T00:00:00Z</updated>
  <link rel="self" type="application/atom+xml" href="https://jumplink.eu/en/blog/tags/macos/feed.xml"/>
  <link rel="alternate" type="text/html" href="https://jumplink.eu/en/blog/tags/macos/"/>
  <author>
    <name>Pascal Garber</name>
    <uri>https://jumplink.eu/</uri>
  </author>
  <entry>
    <id>https://jumplink.eu/en/blog/macos-signing/</id>
    <title>Shipping GNOME apps for Linux, macOS and Windows with gjsify</title>
    <link rel="alternate" href="https://jumplink.eu/en/blog/macos-signing/"/>
    <published>2026-11-01T00:00:00Z</published>
    <updated>2026-11-01T00:00:00Z</updated>
    <summary>gjsify ship builds packages for Linux, macOS and Windows from your app. For the Mac it now also signs and notarises, with one command and no packaging files in your repo.</summary>
    <content type="html">&lt;p&gt;Writing an app is one half of the job, getting it to run on other people&amp;#39;s machines is the other. With &lt;code&gt;gjsify ship&lt;/code&gt; you build installable packages from your project, with no packaging files in the repo and no hand-driven &lt;code&gt;dpkg-deb&lt;/code&gt;, &lt;code&gt;rpmbuild&lt;/code&gt; or &lt;code&gt;hdiutil&lt;/code&gt;. What is new: for macOS, &lt;code&gt;ship&lt;/code&gt; now also &lt;strong&gt;signs and has Apple notarise&lt;/strong&gt; the result. I used it for three apps: &lt;a href=&quot;https://jumplink.eu/Learn6502/&quot;&gt;Learn 6502 Assembly&lt;/a&gt;, &lt;a href=&quot;https://jumplink.eu/steuererklaerung/&quot;&gt;Steuererklärung&lt;/a&gt; and &lt;a href=&quot;https://jumplink.eu/beifahrer/&quot;&gt;Beifahrer&lt;/a&gt;. All three now ship as a dmg for Apple Silicon and for Intel, and macOS opens them without a warning.&lt;/p&gt;
&lt;h3&gt;One command per operating system&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;ship&lt;/code&gt; runs your project&amp;#39;s &lt;code&gt;build&lt;/code&gt; script, assembles one payload and wraps it per format. For Linux it works out which libraries a distribution needs by reading your bundle. The settings live in a &lt;code&gt;gjsify.ship&lt;/code&gt; block in your &lt;code&gt;package.json&lt;/code&gt;.&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;gjsify&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; ship&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; linux&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; --target&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; deb,rpm,appimage&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;gjsify&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; ship&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; darwin&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; --arch&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; arm64&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; --target&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; macos-app-zip,macos-app-dmg&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Linux gets &lt;code&gt;.deb&lt;/code&gt;, &lt;code&gt;.rpm&lt;/code&gt; and AppImage. For the Mac you get an &lt;code&gt;.app&lt;/code&gt; as a &lt;code&gt;.zip&lt;/code&gt; and as a &lt;code&gt;.dmg&lt;/code&gt;, one architecture per run. The apps carry their own runtime, so whoever downloads one needs neither GJS nor GTK installed. Windows gets an MSI installer, which I do not sign yet; more on that below.&lt;/p&gt;
&lt;h3&gt;Signing and notarising&lt;/h3&gt;
&lt;p&gt;Without a signature macOS will not open a downloaded app, because Gatekeeper rejects anything carrying the browser&amp;#39;s quarantine flag. You need three things:&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;a membership in the Apple Developer Program,&lt;/li&gt;&lt;li&gt;a certificate of the type &lt;em&gt;Developer ID Application&lt;/em&gt; in your keychain,&lt;/li&gt;&lt;li&gt;a credential for &lt;code&gt;notarytool&lt;/code&gt;, for example an App Store Connect API key, stored as a profile with &lt;code&gt;xcrun notarytool store-credentials&lt;/code&gt;.&lt;/li&gt;&lt;/ul&gt;
&lt;p&gt;Then two flags come on top:&lt;/p&gt;
&lt;pre class=&quot;shiki shiki-themes github-light github-dark&quot; style=&quot;--shiki-light:#24292e;--shiki-dark:#e1e4e8;--shiki-light-bg:#fff;--shiki-dark-bg:#24292e&quot; tabindex=&quot;0&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#6F42C1;--shiki-dark:#B392F0&quot;&gt;gjsify&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; ship&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; darwin&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; --arch&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; arm64&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; --target&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; macos-app-dmg&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;  --sign&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; &quot;Developer ID Application: Your Name (TEAMID)&quot;&lt;/span&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;--shiki-light:#005CC5;--shiki-dark:#79B8FF&quot;&gt;  --notarize&lt;/span&gt;&lt;span style=&quot;--shiki-light:#032F62;--shiki-dark:#9ECBFF&quot;&gt; my-notary-profile&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;--sign&lt;/code&gt; signs every executable image in the app, and the dmg image as well. &lt;code&gt;--notarize&lt;/code&gt; sends the result to Apple, waits for the verdict and staples the ticket to the file. Without an identity &lt;code&gt;ship&lt;/code&gt; packs unsigned and says so loudly. There is no silent &amp;quot;signed&amp;quot;.&lt;/p&gt;
&lt;p&gt;In CI this runs on a Mac runner. Certificate and API key sit in the repository as secrets, and the job imports them into a throwaway keychain. Each of my apps carries five secrets, and CI builds the releases. Afterwards the job checks every architecture with &lt;code&gt;codesign --verify&lt;/code&gt;, &lt;code&gt;spctl&lt;/code&gt; with the quarantine flag set, and &lt;code&gt;stapler validate&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;What I learned along the way&lt;/h3&gt;
&lt;p&gt;I found the stumbling block with Steuererklärung, and it was in the tooling, not in the app: the umlaut in the name invalidated the signature. &lt;code&gt;codesign&lt;/code&gt; derives the identifier of the bundled interpreter from its file name, and the dmg stores names in a different decomposed form than the one the seal was computed with. The &lt;code&gt;.zip&lt;/code&gt; was fine, the same app inside the mounted dmg was not. Notarisation and &lt;code&gt;stapler validate&lt;/code&gt; still pass, and only Gatekeeper rejects the app at launch. &lt;code&gt;ship&lt;/code&gt; now handles both (&lt;a href=&quot;https://github.com/gjsify/gjsify/pull/2466&quot;&gt;#2466&lt;/a&gt;, &lt;a href=&quot;https://github.com/gjsify/gjsify/pull/2480&quot;&gt;#2480&lt;/a&gt;, included since gjsify 0.64.14), and a test mounts a dmg with an umlaut in the file names and verifies the signature inside it. You need to do nothing about it, not even for a name like &amp;quot;Steuererklärung&amp;quot;.&lt;/p&gt;
&lt;p&gt;The lesson holds for every release: check the finished dmg on a real Mac. I use the MacBook for arm64 and a macOS VM for Intel. Both get the quarantine flag as after a browser download, then &lt;code&gt;spctl&lt;/code&gt; and &lt;code&gt;codesign --verify --deep --strict&lt;/code&gt; on the app inside the mounted image. A green CI alone would not have shown the bug.&lt;/p&gt;
&lt;h3&gt;And Windows?&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;ship&lt;/code&gt; already builds the MSI, and CI installs and starts it as a test. It is unsigned, though, and Windows warns about it. The certificate from Microsoft is still pending, and the CI wiring follows.&lt;/p&gt;
&lt;p&gt;More on the formats is in the guide &lt;a href=&quot;https://gjsify.org/gjsify/ship/&quot;&gt;Ship your app&lt;/a&gt;.&lt;/p&gt;</content>
    <category term="gjsify"/>
    <category term="macOS"/>
  </entry>
</feed>