home / tils / til

Menu
  • GraphQL API

til: electron_sign-notarize-electron-macos.md

This data as json

path topic title url body html shot created created_utc updated updated_utc shot_hash slug
electron_sign-notarize-electron-macos.md electron Signing and notarizing an Electron app for distribution using GitHub Actions https://github.com/simonw/til/blob/main/electron/sign-notarize-electron-macos.md I had to figure this out for [Datasette Desktop](https://github.com/simonw/datasette-app). ## Pay for an Apple Developer account First step is to pay $99/year for an [Apple Developer](https://developer.apple.com/) account. I had a previous (expired) account with a UK address, and changing to a USA address required a support ticket - so instead I created a brand new Apple ID specifically for the developer account. Since a later stage here involves storing the account password in a GitHub repository secret, I think this is a better way to go: I don't like the idea of my personal Apple ID account password being needed by anyone else who should be able to sign my application. ## Generate a Certificate Signing Request First you need to generate a Certificate Signing Request using Keychain Access on a Mac - I was unable to figure out how to do this on the command-line. Quoting https://help.apple.com/developer-account/#/devbfa00fef7: > 1. Launch Keychain Access located in `/Applications/Utilities`. > 2. Choose Keychain Access > Certificate Assistant > Request a Certificate from a Certificate Authority. > 3. In the Certificate Assistant dialog, enter an email address in the User Email Address field. > 4. In the Common Name field, enter a name for the key (for example, Gita Kumar Dev Key). > 5. Leave the CA Email Address field empty. > 6. Choose "Saved to disk", and click Continue. This produces a `CertificateSigningRequest.certSigningRequest` file. Save that somewhere sensible. ## Creating a Developer ID Application certificate The certificate needed is for a "Developer ID Application" - so select that option from the list of options on https://developer.apple.com/account/resources/certificates/add Upload the `CertificateSigningRequest.certSigningRequest` file, and Apple should provide you a `developerID_application.cer` to download. ## Export it as a .p12 file The final signing step requires a `.p12` file. It took me quite a while to figure out how to create this - in the end what worked for me was this: 1. Double-click the `developerID_application.cer` file and import it into my login keychain 2. In Keychain Access open the "My Certificates" pane 3. Select the "Developer ID Application: ..." certificate and the Private Key below it (created when generating the certificate signing request) 4. Right click and select "Export 2 items..." ![Screenshot of the Keynote export interface](https://user-images.githubusercontent.com/9599/132558174-c90410a7-8548-4642-a717-0b470788a5ea.png) I saved the resulting file as `Developer-ID-Application-Certificates.p12`. It asked me to set a password, so I generated and saved a random one in 1Password. ## Building a signed copy of the application At this point I turned to [electron-builder](https://www.electron.build/) to do the rest of the work. I installed it with: npm install electron-builder --save-dev I added `"dist": "electron-builder --publish never"` to my `"scripts"` block in `package.json`. Then I ran the following: CSC_KEY_PASSWORD=... \ CSC_LINK=$(openssl base64 -in Developer-ID-Application-Certificates.p12) \ npm run dist The `CSC_KEY_PASSWORD` was the password I set earlier when I exported the certificate. That `CSC_LINK` variable is set to the base64 encoded version of the certificate file. You can also pass the file itself, but I would need the base64 option later to work with GitHub actions. This worked! It generated a signed `Datasette.app` package. ... which wasn't quite enough. It still wouldn't open without complaints on another machine until I had got it notarized. ## Notarizing the application Notarizing involves uploading the application bundle to Apple's servers, where they run some automatic scans against it before returning a notarization ticket that can be "stapled" to the binary. Thankfully [electron-notarize](https://github.com/electron/electron-notarize) does most of the work here, so I installed that: npm install electron-notarize --save-dev I then went through an iteration cycle of trying out different combinations of settings until it finally worked. I'll describe my finished configuration. I have a file in `build/entitlements.mac.plist` containing the following: ```xml <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>com.apple.security.cs.allow-dyld-environment-variables</key> <true/> <key>com.apple.security.cs.disable-library-validation</key> <true/> <key>com.apple.security.cs.allow-jit</key> <true/> <key>com.apple.security.cs.allow-unsigned-executable-memory</key> <true/> <key>com.apple.security.cs.debugger</key> <true/> <key>com.apple.security.network.client</key> <true/> <key>com.apple.security.network.server</key> <true/> <key>com.apple.security.files.user-selected.read-only</key> <true/> <key>com.apple.security.inherit</key> <true/> <key>com.apple.security.automation.apple-events</key> <true/> </dict> </plist> ``` The possible entitlements are [documented here]( https://developer.apple.com/library/archive/documentation/Miscellaneous/Reference/EntitlementKeyReference/Chapters/EnablingAppSandbox.html). I don't fully understand these ones, but they are what I got to after multiple rounds of experimentation. I have a `scripts/notarize.js` file containing this (based on [Notarizing your Electron application](https://kilianvalkhof.com/2019/electron/notarizing-your-electron-application/) by Kilian Valkhof): ```javascript /* Based on https://kilianvalkhof.com/2019/electron/notarizing-your-electron-application/ */ const { notarize } = require("electron-notarize"); exports.default = async function notarizing(context) { const { electronPlatformName, appOutDir } = context; if (electronPlatformName !== "darwin") { return; } const appName = context.packager.appInfo.productFilename; return await notarize({ appBundleId: "io.datasette.app", appPath: `${appOutDir}/${appName}.app`, appleId: process.env.APPLEID, appleIdPassword: process.env.APPLEIDPASS, }); }; ``` The `"build"` section of my `package.json` looks like this: ```json "build": { "appId": "io.datasette.app", "mac": { "category": "public.app-category.developer-tools", "hardenedRuntime" : true, "gatekeeperAssess": false, "entitlements": "build/entitlements.mac.plist", "entitlementsInherit": "build/entitlements.mac.plist", "binaries": [ "./dist/mac/Datasette.app/Contents/Resources/python/bin/python3.9", "./dist/mac/Datasette.app/Contents/Resources/python/lib/python3.9/lib-dynload/xxlimited.cpython-39-darwin.so", "./dist/mac/Datasette.app/Contents/Resources/python/lib/python3.9/lib-dynload/_testcapi.cpython-39-darwin.so" ] }, "afterSign": "scripts/notarize.js", "extraResources": [ { "from": "python", "to": "python", "filter": [ "**/*" ] } ] } ``` Again, I got here through a process of iteration - in particular, my application bundles a full copy of Python so I had to specify some additional binaries and `extraResources` - most applications will not need to do that. Note that the `scripts/notarize.js` file uses two extra environment variables: `APPLEID` and `APPLEIDPASS`. These are the account credentials for my Apple Developer account's Apple ID. (I also encountered an error `xcrun: error: unable to find utility "altool", not a developer tool or in PATH` - I resolved that by running `sudo xcode-select --reset`.) ## Creating an app-specific password Another error I encountered was this one: > Please sign in with an app-specific password. You can create one at appleid.apple.com These can be created in the "Security" section of https://appleid.apple.com/account/home - I created one called "Notarize Apps" which I set as the `APPLEIDPASS` environment variable. ## Creating a signed and notarized build With all of the above in place, creating a build on my laptop looked like this: ``` APPLEID=my-dedicated-appleid \ APPLEIDPASS=app-specific-password \ CSC_KEY_PASSWORD=key-password \ CSC_LINK=$(openssl base64 -in Developer-ID-Application-Certificates.p12) \ npm run dist ``` This worked! It produced a `Datasette.app` package which I could zip up, distribute to another machine, unzip and install - and it then opened without the terrifying security warning. ## Automating it all with GitHub Actions I decided to build and notarize on _every push_ to my repository, so I could save the resulting build as an artifact and install any in-progress work on a computer to test it. Apple [limit you to 75 notarizations a day](https://developer.apple.com/documentation/security/notarizing_macos_software_before_distribution/customizing_the_notarization_workflow#3561440) so I think this is OK for my projects. My full [test.yml](https://github.com/simonw/datasette-app/blob/0.1.0/.github/workflows/test.yml) looks like this: ```yaml name: Test on: push jobs: test: runs-on: macos-latest steps: - uses: actions/checkout@v2 - name: Configure Node caching uses: actions/cache@v2 env: cache-name: cache-node-modules with: path: ~/.npm key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} restore-keys: | ${{ runner.os }}-build-${{ env.cache-name }}- ${{ runner.os }}-build- ${{ runner.os }}- - uses: actions/cache@v2 name: Configure pip caching with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} restore-keys: | ${{ runner.os }}-pip- - name: Install Node dependencies run: npm install - name: Download standalone Python run: | ./download-python.sh - name: Run tests run: npm test timeout-minutes: 5 - name: Build distribution env: CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} CSC_LINK: ${{ secrets.CSC_LINK }} APPLEID: ${{ secrets.APPLEID }} APPLEIDPASS: ${{ secrets.APPLEIDPASS }} run: npm run dist - name: Create zip file run: | cd dist/mac ditto -c -k --keepParent Datasette.app Datasette.app.zip - name: And a README (to work around GitHub double-zips) run: | echo "More information: https://datasette.io" > dist/mac/README.txt - name: Upload artifact uses: actions/upload-artifact@v2 with: name: Datasette-macOS path: | dist/mac/Datasette.app.zip dist/mac/README.txt ``` The key stuff here is the "Build distribution" step. It sets four values that I have saved on the repository as secrets: `CSC_KEY_PASSWORD`, `CSC_LINK`, `APPLEID` and `APPLEIDPASS`. The `CSC_LINK` variable is the base64-encoded contents of my `Developer-ID-Application-Certificates.p12` file. I generated that like so: openssl base64 -in developerID_application.cer I have [a separate release.yml](https://github.com/simonw/datasette-app/blob/0.1.0/.github/workflows/release.yml) for building tagged releases, described in [this TIL](https://til.simonwillison.net/github-actions/attach-generated-file-to-release). ## The finished configuration You can browse the code in [my 0.1.0 tag](https://github.com/simonw/datasette-app/tree/0.1.0) to see all of these parts in their final configuration, as used to create the 0.1.0 initial release of my application. The original issue threads in which I figured this stuff out are: - [Get an Apple developer certificate #45](https://github.com/simonw/datasette-app/issues/45) - [Work out how to notarize the macOS application #50](https://github.com/simonw/datasette-app/issues/50) - [GitHub Actions workflow for creating packages for releases #51](https://github.com/simonw/datasette-app/issues/51) <p>I had to figure this out for <a href="https://github.com/simonw/datasette-app">Datasette Desktop</a>.</p> <h2> <a id="user-content-pay-for-an-apple-developer-account" class="anchor" href="#pay-for-an-apple-developer-account" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Pay for an Apple Developer account</h2> <p>First step is to pay $99/year for an <a href="https://developer.apple.com/" rel="nofollow">Apple Developer</a> account.</p> <p>I had a previous (expired) account with a UK address, and changing to a USA address required a support ticket - so instead I created a brand new Apple ID specifically for the developer account.</p> <p>Since a later stage here involves storing the account password in a GitHub repository secret, I think this is a better way to go: I don't like the idea of my personal Apple ID account password being needed by anyone else who should be able to sign my application.</p> <h2> <a id="user-content-generate-a-certificate-signing-request" class="anchor" href="#generate-a-certificate-signing-request" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Generate a Certificate Signing Request</h2> <p>First you need to generate a Certificate Signing Request using Keychain Access on a Mac - I was unable to figure out how to do this on the command-line.</p> <p>Quoting <a href="https://help.apple.com/developer-account/#/devbfa00fef7" rel="nofollow">https://help.apple.com/developer-account/#/devbfa00fef7</a>:</p> <blockquote> <ol> <li>Launch Keychain Access located in <code>/Applications/Utilities</code>.</li> <li>Choose Keychain Access &gt; Certificate Assistant &gt; Request a Certificate from a Certificate Authority.</li> <li>In the Certificate Assistant dialog, enter an email address in the User Email Address field.</li> <li>In the Common Name field, enter a name for the key (for example, Gita Kumar Dev Key).</li> <li>Leave the CA Email Address field empty.</li> <li>Choose "Saved to disk", and click Continue.</li> </ol> </blockquote> <p>This produces a <code>CertificateSigningRequest.certSigningRequest</code> file. Save that somewhere sensible.</p> <h2> <a id="user-content-creating-a-developer-id-application-certificate" class="anchor" href="#creating-a-developer-id-application-certificate" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Creating a Developer ID Application certificate</h2> <p>The certificate needed is for a "Developer ID Application" - so select that option from the list of options on <a href="https://developer.apple.com/account/resources/certificates/add" rel="nofollow">https://developer.apple.com/account/resources/certificates/add</a></p> <p>Upload the <code>CertificateSigningRequest.certSigningRequest</code> file, and Apple should provide you a <code>developerID_application.cer</code> to download.</p> <h2> <a id="user-content-export-it-as-a-p12-file" class="anchor" href="#export-it-as-a-p12-file" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Export it as a .p12 file</h2> <p>The final signing step requires a <code>.p12</code> file. It took me quite a while to figure out how to create this - in the end what worked for me was this:</p> <ol> <li>Double-click the <code>developerID_application.cer</code> file and import it into my login keychain</li> <li>In Keychain Access open the "My Certificates" pane</li> <li>Select the "Developer ID Application: ..." certificate and the Private Key below it (created when generating the certificate signing request)</li> <li>Right click and select "Export 2 items..."</li> </ol> <p><a href="https://user-images.githubusercontent.com/9599/132558174-c90410a7-8548-4642-a717-0b470788a5ea.png" target="_blank" rel="nofollow"><img src="https://user-images.githubusercontent.com/9599/132558174-c90410a7-8548-4642-a717-0b470788a5ea.png" alt="Screenshot of the Keynote export interface" style="max-width:100%;"></a></p> <p>I saved the resulting file as <code>Developer-ID-Application-Certificates.p12</code>. It asked me to set a password, so I generated and saved a random one in 1Password.</p> <h2> <a id="user-content-building-a-signed-copy-of-the-application" class="anchor" href="#building-a-signed-copy-of-the-application" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Building a signed copy of the application</h2> <p>At this point I turned to <a href="https://www.electron.build/" rel="nofollow">electron-builder</a> to do the rest of the work. I installed it with:</p> <pre><code>npm install electron-builder --save-dev </code></pre> <p>I added <code>"dist": "electron-builder --publish never"</code> to my <code>"scripts"</code> block in <code>package.json</code>.</p> <p>Then I ran the following:</p> <pre><code>CSC_KEY_PASSWORD=... \ CSC_LINK=$(openssl base64 -in Developer-ID-Application-Certificates.p12) \ npm run dist </code></pre> <p>The <code>CSC_KEY_PASSWORD</code> was the password I set earlier when I exported the certificate.</p> <p>That <code>CSC_LINK</code> variable is set to the base64 encoded version of the certificate file. You can also pass the file itself, but I would need the base64 option later to work with GitHub actions.</p> <p>This worked! It generated a signed <code>Datasette.app</code> package.</p> <p>... which wasn't quite enough. It still wouldn't open without complaints on another machine until I had got it notarized.</p> <h2> <a id="user-content-notarizing-the-application" class="anchor" href="#notarizing-the-application" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Notarizing the application</h2> <p>Notarizing involves uploading the application bundle to Apple's servers, where they run some automatic scans against it before returning a notarization ticket that can be "stapled" to the binary.</p> <p>Thankfully <a href="https://github.com/electron/electron-notarize">electron-notarize</a> does most of the work here, so I installed that:</p> <pre><code>npm install electron-notarize --save-dev </code></pre> <p>I then went through an iteration cycle of trying out different combinations of settings until it finally worked.</p> <p>I'll describe my finished configuration.</p> <p>I have a file in <code>build/entitlements.mac.plist</code> containing the following:</p> <div class="highlight highlight-text-xml"><pre>&lt;?<span class="pl-ent">xml</span><span class="pl-e"> version</span>=<span class="pl-s"><span class="pl-pds">"</span>1.0<span class="pl-pds">"</span></span><span class="pl-e"> encoding</span>=<span class="pl-s"><span class="pl-pds">"</span>UTF-8<span class="pl-pds">"</span></span>?&gt; &lt;!<span class="pl-ent">DOCTYPE</span> <span class="pl-e">plist</span> PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"&gt; &lt;<span class="pl-ent">plist</span> <span class="pl-e">version</span>=<span class="pl-s"><span class="pl-pds">"</span>1.0<span class="pl-pds">"</span></span>&gt; &lt;<span class="pl-ent">dict</span>&gt; &lt;<span class="pl-ent">key</span>&gt;com.apple.security.cs.allow-dyld-environment-variables&lt;/<span class="pl-ent">key</span>&gt; &lt;<span class="pl-ent">true</span>/&gt; &lt;<span class="pl-ent">key</span>&gt;com.apple.security.cs.disable-library-validation&lt;/<span class="pl-ent">key</span>&gt; &lt;<span class="pl-ent">true</span>/&gt; &lt;<span class="pl-ent">key</span>&gt;com.apple.security.cs.allow-jit&lt;/<span class="pl-ent">key</span>&gt; &lt;<span class="pl-ent">true</span>/&gt; &lt;<span class="pl-ent">key</span>&gt;com.apple.security.cs.allow-unsigned-executable-memory&lt;/<span class="pl-ent">key</span>&gt; &lt;<span class="pl-ent">true</span>/&gt; &lt;<span class="pl-ent">key</span>&gt;com.apple.security.cs.debugger&lt;/<span class="pl-ent">key</span>&gt; &lt;<span class="pl-ent">true</span>/&gt; &lt;<span class="pl-ent">key</span>&gt;com.apple.security.network.client&lt;/<span class="pl-ent">key</span>&gt; &lt;<span class="pl-ent">true</span>/&gt; &lt;<span class="pl-ent">key</span>&gt;com.apple.security.network.server&lt;/<span class="pl-ent">key</span>&gt; &lt;<span class="pl-ent">true</span>/&gt; &lt;<span class="pl-ent">key</span>&gt;com.apple.security.files.user-selected.read-only&lt;/<span class="pl-ent">key</span>&gt; &lt;<span class="pl-ent">true</span>/&gt; &lt;<span class="pl-ent">key</span>&gt;com.apple.security.inherit&lt;/<span class="pl-ent">key</span>&gt; &lt;<span class="pl-ent">true</span>/&gt; &lt;<span class="pl-ent">key</span>&gt;com.apple.security.automation.apple-events&lt;/<span class="pl-ent">key</span>&gt; &lt;<span class="pl-ent">true</span>/&gt; &lt;/<span class="pl-ent">dict</span>&gt; &lt;/<span class="pl-ent">plist</span>&gt;</pre></div> <p>The possible entitlements are <a href="https://developer.apple.com/library/archive/documentation/Miscellaneous/Reference/EntitlementKeyReference/Chapters/EnablingAppSandbox.html" rel="nofollow">documented here</a>. I don't fully understand these ones, but they are what I got to after multiple rounds of experimentation.</p> <p>I have a <code>scripts/notarize.js</code> file containing this (based on <a href="https://kilianvalkhof.com/2019/electron/notarizing-your-electron-application/" rel="nofollow">Notarizing your Electron application</a> by Kilian Valkhof):</p> <div class="highlight highlight-source-js"><pre><span class="pl-c">/* Based on https://kilianvalkhof.com/2019/electron/notarizing-your-electron-application/ */</span> <span class="pl-k">const</span> <span class="pl-kos">{</span> notarize <span class="pl-kos">}</span> <span class="pl-c1">=</span> <span class="pl-en">require</span><span class="pl-kos">(</span><span class="pl-s">"electron-notarize"</span><span class="pl-kos">)</span><span class="pl-kos">;</span> <span class="pl-s1">exports</span><span class="pl-kos">.</span><span class="pl-en">default</span> <span class="pl-c1">=</span> <span class="pl-k">async</span> <span class="pl-k">function</span> <span class="pl-en">notarizing</span><span class="pl-kos">(</span><span class="pl-s1">context</span><span class="pl-kos">)</span> <span class="pl-kos">{</span> <span class="pl-k">const</span> <span class="pl-kos">{</span> electronPlatformName<span class="pl-kos">,</span> appOutDir <span class="pl-kos">}</span> <span class="pl-c1">=</span> <span class="pl-s1">context</span><span class="pl-kos">;</span> <span class="pl-k">if</span> <span class="pl-kos">(</span><span class="pl-s1">electronPlatformName</span> <span class="pl-c1">!==</span> <span class="pl-s">"darwin"</span><span class="pl-kos">)</span> <span class="pl-kos">{</span> <span class="pl-k">return</span><span class="pl-kos">;</span> <span class="pl-kos">}</span> <span class="pl-k">const</span> <span class="pl-s1">appName</span> <span class="pl-c1">=</span> <span class="pl-s1">context</span><span class="pl-kos">.</span><span class="pl-c1">packager</span><span class="pl-kos">.</span><span class="pl-c1">appInfo</span><span class="pl-kos">.</span><span class="pl-c1">productFilename</span><span class="pl-kos">;</span> <span class="pl-k">return</span> <span class="pl-k">await</span> <span class="pl-en">notarize</span><span class="pl-kos">(</span><span class="pl-kos">{</span> <span class="pl-c1">appBundleId</span>: <span class="pl-s">"io.datasette.app"</span><span class="pl-kos">,</span> <span class="pl-c1">appPath</span>: <span class="pl-s">`<span class="pl-s1"><span class="pl-kos">${</span><span class="pl-s1">appOutDir</span><span class="pl-kos">}</span></span>/<span class="pl-s1"><span class="pl-kos">${</span><span class="pl-s1">appName</span><span class="pl-kos">}</span></span>.app`</span><span class="pl-kos">,</span> <span class="pl-c1">appleId</span>: <span class="pl-s1">process</span><span class="pl-kos">.</span><span class="pl-c1">env</span><span class="pl-kos">.</span><span class="pl-c1">APPLEID</span><span class="pl-kos">,</span> <span class="pl-c1">appleIdPassword</span>: <span class="pl-s1">process</span><span class="pl-kos">.</span><span class="pl-c1">env</span><span class="pl-kos">.</span><span class="pl-c1">APPLEIDPASS</span><span class="pl-kos">,</span> <span class="pl-kos">}</span><span class="pl-kos">)</span><span class="pl-kos">;</span> <span class="pl-kos">}</span><span class="pl-kos">;</span></pre></div> <p>The <code>"build"</code> section of my <code>package.json</code> looks like this:</p> <div class="highlight highlight-source-json"><pre> <span class="pl-s"><span class="pl-pds">"</span>build<span class="pl-pds">"</span></span>: { <span class="pl-s"><span class="pl-pds">"</span>appId<span class="pl-pds">"</span></span>: <span class="pl-s"><span class="pl-pds">"</span>io.datasette.app<span class="pl-pds">"</span></span>, <span class="pl-s"><span class="pl-pds">"</span>mac<span class="pl-pds">"</span></span>: { <span class="pl-s"><span class="pl-pds">"</span>category<span class="pl-pds">"</span></span>: <span class="pl-s"><span class="pl-pds">"</span>public.app-category.developer-tools<span class="pl-pds">"</span></span>, <span class="pl-s"><span class="pl-pds">"</span>hardenedRuntime<span class="pl-pds">"</span></span> : <span class="pl-c1">true</span>, <span class="pl-s"><span class="pl-pds">"</span>gatekeeperAssess<span class="pl-pds">"</span></span>: <span class="pl-c1">false</span>, <span class="pl-s"><span class="pl-pds">"</span>entitlements<span class="pl-pds">"</span></span>: <span class="pl-s"><span class="pl-pds">"</span>build/entitlements.mac.plist<span class="pl-pds">"</span></span>, <span class="pl-s"><span class="pl-pds">"</span>entitlementsInherit<span class="pl-pds">"</span></span>: <span class="pl-s"><span class="pl-pds">"</span>build/entitlements.mac.plist<span class="pl-pds">"</span></span>, <span class="pl-s"><span class="pl-pds">"</span>binaries<span class="pl-pds">"</span></span>: [ <span class="pl-s"><span class="pl-pds">"</span>./dist/mac/Datasette.app/Contents/Resources/python/bin/python3.9<span class="pl-pds">"</span></span>, <span class="pl-s"><span class="pl-pds">"</span>./dist/mac/Datasette.app/Contents/Resources/python/lib/python3.9/lib-dynload/xxlimited.cpython-39-darwin.so<span class="pl-pds">"</span></span>, <span class="pl-s"><span class="pl-pds">"</span>./dist/mac/Datasette.app/Contents/Resources/python/lib/python3.9/lib-dynload/_testcapi.cpython-39-darwin.so<span class="pl-pds">"</span></span> ] }, <span class="pl-s"><span class="pl-pds">"</span>afterSign<span class="pl-pds">"</span></span>: <span class="pl-s"><span class="pl-pds">"</span>scripts/notarize.js<span class="pl-pds">"</span></span>, <span class="pl-s"><span class="pl-pds">"</span>extraResources<span class="pl-pds">"</span></span>: [ { <span class="pl-s"><span class="pl-pds">"</span>from<span class="pl-pds">"</span></span>: <span class="pl-s"><span class="pl-pds">"</span>python<span class="pl-pds">"</span></span>, <span class="pl-s"><span class="pl-pds">"</span>to<span class="pl-pds">"</span></span>: <span class="pl-s"><span class="pl-pds">"</span>python<span class="pl-pds">"</span></span>, <span class="pl-s"><span class="pl-pds">"</span>filter<span class="pl-pds">"</span></span>: [ <span class="pl-s"><span class="pl-pds">"</span>**/*<span class="pl-pds">"</span></span> ] } ] }</pre></div> <p>Again, I got here through a process of iteration - in particular, my application bundles a full copy of Python so I had to specify some additional binaries and <code>extraResources</code> - most applications will not need to do that.</p> <p>Note that the <code>scripts/notarize.js</code> file uses two extra environment variables: <code>APPLEID</code> and <code>APPLEIDPASS</code>. These are the account credentials for my Apple Developer account's Apple ID.</p> <p>(I also encountered an error <code>xcrun: error: unable to find utility "altool", not a developer tool or in PATH</code> - I resolved that by running <code>sudo xcode-select --reset</code>.)</p> <h2> <a id="user-content-creating-an-app-specific-password" class="anchor" href="#creating-an-app-specific-password" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Creating an app-specific password</h2> <p>Another error I encountered was this one:</p> <blockquote> <p>Please sign in with an app-specific password. You can create one at appleid.apple.com</p> </blockquote> <p>These can be created in the "Security" section of <a href="https://appleid.apple.com/account/home" rel="nofollow">https://appleid.apple.com/account/home</a> - I created one called "Notarize Apps" which I set as the <code>APPLEIDPASS</code> environment variable.</p> <h2> <a id="user-content-creating-a-signed-and-notarized-build" class="anchor" href="#creating-a-signed-and-notarized-build" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Creating a signed and notarized build</h2> <p>With all of the above in place, creating a build on my laptop looked like this:</p> <pre><code>APPLEID=my-dedicated-appleid \ APPLEIDPASS=app-specific-password \ CSC_KEY_PASSWORD=key-password \ CSC_LINK=$(openssl base64 -in Developer-ID-Application-Certificates.p12) \ npm run dist </code></pre> <p>This worked! It produced a <code>Datasette.app</code> package which I could zip up, distribute to another machine, unzip and install - and it then opened without the terrifying security warning.</p> <h2> <a id="user-content-automating-it-all-with-github-actions" class="anchor" href="#automating-it-all-with-github-actions" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>Automating it all with GitHub Actions</h2> <p>I decided to build and notarize on <em>every push</em> to my repository, so I could save the resulting build as an artifact and install any in-progress work on a computer to test it.</p> <p>Apple <a href="https://developer.apple.com/documentation/security/notarizing_macos_software_before_distribution/customizing_the_notarization_workflow#3561440" rel="nofollow">limit you to 75 notarizations a day</a> so I think this is OK for my projects.</p> <p>My full <a href="https://github.com/simonw/datasette-app/blob/0.1.0/.github/workflows/test.yml">test.yml</a> looks like this:</p> <div class="highlight highlight-source-yaml"><pre><span class="pl-ent">name</span>: <span class="pl-s">Test</span> <span class="pl-ent">on</span>: <span class="pl-s">push</span> <span class="pl-ent">jobs</span>: <span class="pl-ent">test</span>: <span class="pl-ent">runs-on</span>: <span class="pl-s">macos-latest</span> <span class="pl-ent">steps</span>: - <span class="pl-ent">uses</span>: <span class="pl-s">actions/checkout@v2</span> - <span class="pl-ent">name</span>: <span class="pl-s">Configure Node caching</span> <span class="pl-ent">uses</span>: <span class="pl-s">actions/cache@v2</span> <span class="pl-ent">env</span>: <span class="pl-ent">cache-name</span>: <span class="pl-s">cache-node-modules</span> <span class="pl-ent">with</span>: <span class="pl-ent">path</span>: <span class="pl-s">~/.npm</span> <span class="pl-ent">key</span>: <span class="pl-s">${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}</span> <span class="pl-ent">restore-keys</span>: <span class="pl-s">|</span> <span class="pl-s"> ${{ runner.os }}-build-${{ env.cache-name }}-</span> <span class="pl-s"> ${{ runner.os }}-build-</span> <span class="pl-s"> ${{ runner.os }}-</span> <span class="pl-s"></span> - <span class="pl-ent">uses</span>: <span class="pl-s">actions/cache@v2</span> <span class="pl-ent">name</span>: <span class="pl-s">Configure pip caching</span> <span class="pl-ent">with</span>: <span class="pl-ent">path</span>: <span class="pl-s">~/.cache/pip</span> <span class="pl-ent">key</span>: <span class="pl-s">${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}</span> <span class="pl-ent">restore-keys</span>: <span class="pl-s">|</span> <span class="pl-s"> ${{ runner.os }}-pip-</span> <span class="pl-s"></span> - <span class="pl-ent">name</span>: <span class="pl-s">Install Node dependencies</span> <span class="pl-ent">run</span>: <span class="pl-s">npm install</span> - <span class="pl-ent">name</span>: <span class="pl-s">Download standalone Python</span> <span class="pl-ent">run</span>: <span class="pl-s">|</span> <span class="pl-s"> ./download-python.sh</span> <span class="pl-s"></span> - <span class="pl-ent">name</span>: <span class="pl-s">Run tests</span> <span class="pl-ent">run</span>: <span class="pl-s">npm test</span> <span class="pl-ent">timeout-minutes</span>: <span class="pl-c1">5</span> - <span class="pl-ent">name</span>: <span class="pl-s">Build distribution</span> <span class="pl-ent">env</span>: <span class="pl-ent">CSC_KEY_PASSWORD</span>: <span class="pl-s">${{ secrets.CSC_KEY_PASSWORD }}</span> <span class="pl-ent">CSC_LINK</span>: <span class="pl-s">${{ secrets.CSC_LINK }}</span> <span class="pl-ent">APPLEID</span>: <span class="pl-s">${{ secrets.APPLEID }}</span> <span class="pl-ent">APPLEIDPASS</span>: <span class="pl-s">${{ secrets.APPLEIDPASS }}</span> <span class="pl-ent">run</span>: <span class="pl-s">npm run dist</span> - <span class="pl-ent">name</span>: <span class="pl-s">Create zip file</span> <span class="pl-ent">run</span>: <span class="pl-s">|</span> <span class="pl-s"> cd dist/mac</span> <span class="pl-s"> ditto -c -k --keepParent Datasette.app Datasette.app.zip</span> <span class="pl-s"></span> - <span class="pl-ent">name</span>: <span class="pl-s">And a README (to work around GitHub double-zips)</span> <span class="pl-ent">run</span>: <span class="pl-s">|</span> <span class="pl-s"> echo "More information: https://datasette.io" &gt; dist/mac/README.txt</span> <span class="pl-s"></span> - <span class="pl-ent">name</span>: <span class="pl-s">Upload artifact</span> <span class="pl-ent">uses</span>: <span class="pl-s">actions/upload-artifact@v2</span> <span class="pl-ent">with</span>: <span class="pl-ent">name</span>: <span class="pl-s">Datasette-macOS</span> <span class="pl-ent">path</span>: <span class="pl-s">|</span> <span class="pl-s"> dist/mac/Datasette.app.zip</span> <span class="pl-s"> dist/mac/README.txt</span></pre></div> <p>The key stuff here is the "Build distribution" step. It sets four values that I have saved on the repository as secrets: <code>CSC_KEY_PASSWORD</code>, <code>CSC_LINK</code>, <code>APPLEID</code> and <code>APPLEIDPASS</code>.</p> <p>The <code>CSC_LINK</code> variable is the base64-encoded contents of my <code>Developer-ID-Application-Certificates.p12</code> file. I generated that like so:</p> <pre><code>openssl base64 -in developerID_application.cer </code></pre> <p>I have <a href="https://github.com/simonw/datasette-app/blob/0.1.0/.github/workflows/release.yml">a separate release.yml</a> for building tagged releases, described in <a href="https://til.simonwillison.net/github-actions/attach-generated-file-to-release" rel="nofollow">this TIL</a>.</p> <h2> <a id="user-content-the-finished-configuration" class="anchor" href="#the-finished-configuration" aria-hidden="true"><span aria-hidden="true" class="octicon octicon-link"></span></a>The finished configuration</h2> <p>You can browse the code in <a href="https://github.com/simonw/datasette-app/tree/0.1.0">my 0.1.0 tag</a> to see all of these parts in their final configuration, as used to create the 0.1.0 initial release of my application.</p> <p>The original issue threads in which I figured this stuff out are:</p> <ul> <li><a href="https://github.com/simonw/datasette-app/issues/45">Get an Apple developer certificate #45</a></li> <li><a href="https://github.com/simonw/datasette-app/issues/50">Work out how to notarize the macOS application #50</a></li> <li><a href="https://github.com/simonw/datasette-app/issues/51">GitHub Actions workflow for creating packages for releases #51</a></li> </ul> <Binary: 74,298 bytes> 2021-09-08T10:41:46-07:00 2021-09-08T17:41:46+00:00 2021-09-08T10:41:46-07:00 2021-09-08T17:41:46+00:00 6882184d2acaa5b137e3e52a7f9feda2 sign-notarize-electron-macos
Powered by Datasette · How this site works · Code of conduct