Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Apple Silicon macOS test/builds #3077

Merged
merged 30 commits into from
May 25, 2024
Merged

Apple Silicon macOS test/builds #3077

merged 30 commits into from
May 25, 2024

Conversation

philrz
Copy link
Contributor

@philrz philrz commented May 22, 2024

tl;dr

Now that GitHub offers free Actions runners based on Apple Silicon, it becomes feasible for us to run CI and create builds for this platform. While getting this working, I managed to simplify the build process a little (was able to drop the separate electron-builder-notarize dependency) but also had to add some stuff to work around rough edges in the tooling. Details are below and in-line PR comments.

Actions Runners

Per GitHub docs, the Runner macos-14 is on Apple Silicon while the macos-12 we've been using continues to run on Intel. While I don't anticipate platform-specific issues, I've gone ahead here and added macos-14 to the OS matrix for our CI so we can continue to test on the same platforms we build for.

Dropping electron-builder-notarize

Early in my research I could tell that there were still some lingering growing pains in the tooling related to Apple Silicon. Rather than burn time bumping into what might be known problems that were already fixed, my first step was therefore to advance to the newest release of electron-builder. It's at this point I was reminded that electron-builder can now do notarization on its own as an alternative to relying on the separate electron-builder-notarize tool as we've done in the past. Indeed, I think I recall @jameskerr having once spotted this and we'd made a mental note to look into it, but I used "if it ain't broke" logic to justify putting it off. In this case I knew I was likely to break and re-fix our build system anyway, so I figured I'd bite the bullet and maybe spare myself from having to work around problems in yet another tool.

This admittedly did send me briefly down a rabbit hole when I saw in the electron-builder Mac docs that they recommended a different set of credentials (based on API keys) over the password-based approach we'd been using with electron-builder-notarize (explanation in electron-userland/electron-builder#7859). I spent a little time looking at what it would take to start using this alternate approach but ended up backing off because API keys seemed linked to "App Store Connect", hence the assumption we were building something to be published into the App Store, so first steps had Apple asking me to sign new agreements, entering details on payments/taxes related to all the revenue we'd surely make from our App Store sales, etc. Maybe it would have been possible to take these steps only as far as necessary to just get the API keys and not actually publish anything to the App Store. However, just as this was making me nervous, I found that I could extend the password-based approach we've been using all this time in electron-builder by making two small changes:

  1. Changed one environment variable (APPLE_ID_PASSWORD becomes APPLE_APP_SPECIFIC_PASSWORD).
  2. Added a teamId value in the electron-builder.json (which is redundant with the APPLE_TEAM_ID env var, but needed to work around open issue Notarize failed with 24.13.3 after upgrade from 24.12.0 with app specific password electron-userland/electron-builder#8103)

Entitlements, App Sandbox, and Hardened Runtime

My first attempts built successfully but then when @mattnibs would try to run them they'd fail with a crash like:

$ /Applications/Zui.app/Contents/MacOS/Zui 

#
# Fatal process out of memory: Failed to reserve virtual memory for CodeRange
#
----- Native stack trace -----

1: 0x1151c002c node::OnFatalError(char const*, char const*) [/Applications/Zui.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework]
2: 0x1129b3248 _$LT$object..xcoff..Symbol64$u20$as$u20$object..read..xcoff..symbol..Symbol$GT$::n_value::he0fe3918a79f4509 [/Applications/Zui.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework]
3: 0x10f628db4 _$LT$font_types..tag..Tag$u20$as$u20$core..cmp..PartialEq$LT$$u5b$u8$u3b$$u20$4$u5d$$GT$$GT$::eq::h1cf26754fa042eb7 [/Applications/Zui.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework]
4: 0x10f628cf8 _$LT$font_types..tag..Tag$u20$as$u20$core..cmp..PartialEq$LT$$u5b$u8$u3b$$u20$4$u5d$$GT$$GT$::eq::h1cf26754fa042eb7 [/Applications/Zui.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework]
5: 0x10f77f7a0 v8::Unlocker::~Unlocker() [/Applications/Zui.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework]
6: 0x1129b7898 _$LT$object..xcoff..Symbol64$u20$as$u20$object..read..xcoff..symbol..Symbol$GT$::n_value::he0fe3918a79f4509 [/Applications/Zui.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework]
7: 0x10f77f694 v8::Unlocker::~Unlocker() [/Applications/Zui.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework]
8: 0x10f7f198c v8::CppHeap::wrapper_descriptor() const [/Applications/Zui.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework]
9: 0x10f757b48 v8::Unwinder::PCIsInV8(unsigned long, v8::MemoryRange const*, void*) [/Applications/Zui.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework]
10: 0x10f7586b0 v8::Unwinder::PCIsInV8(unsigned long, v8::MemoryRange const*, void*) [/Applications/Zui.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework]
11: 0x10fbea454 v8::internal::TickSample::print() const [/Applications/Zui.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework]
12: 0x10f64cd6c v8::Isolate::Initialize(v8::Isolate*, v8::Isolate::CreateParams const&) [/Applications/Zui.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework]
13: 0x112b53d70 _$LT$object..xcoff..Symbol64$u20$as$u20$object..read..xcoff..symbol..Symbol$GT$::n_value::he0fe3918a79f4509 [/Applications/Zui.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework]
14: 0x10e52ea84 v8::Signature::New(v8::Isolate*, v8::Local<v8::FunctionTemplate>) [/Applications/Zui.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework]
15: 0x10e5158d8 v8::BackingStore::MaxByteLength() const [/Applications/Zui.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework]
16: 0x1108c9dac v8::internal::compiler::CompilationDependencies::FieldTypeDependencyOffTheRecord(v8::internal::compiler::MapRef, v8::internal::compiler::MapRef, v8::internal::InternalIndex, v8::internal::compiler::ObjectRef) const [/Applications/Zui.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework]
17: 0x1108cd700 v8::internal::compiler::CompilationDependencies::FieldTypeDependencyOffTheRecord(v8::internal::compiler::MapRef, v8::internal::compiler::MapRef, v8::internal::InternalIndex, v8::internal::compiler::ObjectRef) const [/Applications/Zui.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework]
18: 0x1108c9680 v8::internal::compiler::CompilationDependencies::FieldTypeDependencyOffTheRecord(v8::internal::compiler::MapRef, v8::internal::compiler::MapRef, v8::internal::InternalIndex, v8::internal::compiler::ObjectRef) const [/Applications/Zui.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework]
19: 0x10e751a18 v8::CodeEvent::GetScriptLine() [/Applications/Zui.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework]
20: 0x10e752a78 v8::CodeEvent::GetScriptLine() [/Applications/Zui.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework]
21: 0x10e7528f0 v8::CodeEvent::GetScriptLine() [/Applications/Zui.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework]
22: 0x10e751230 v8::CodeEvent::GetScriptLine() [/Applications/Zui.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework]
23: 0x10e7513f4 v8::CodeEvent::GetScriptLine() [/Applications/Zui.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework]
24: 0x10e42c51c ElectronMain [/Applications/Zui.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework]
25: 0x1828020e0 start [/usr/lib/dyld]
Trace/BPT trap: 5

After doings some web searches to see if others were seeing a similar crash, I eventually found my way back to the electron-builder Mac docs that include this passing comment:

Be aware that your app may crash if the right entitlements are not set like com.apple.security.cs.allow-jit for example on arm64 builds with Electron 20+.

Sure enough, once I learned how to make that change, the Apple Silicon builds no longer crashed. However, since the concept of "entitlements" was unfamiliar to me, I felt the need to go on a detour to learn a bit about them to understand what they've been set for on our Intel builds all this time, if we should be setting others, etc. The docs were a bit confusing in spots since most seem to be written as if to an audience that have already committed their whole lives to developing in the Apple universe and hence provide limited context. But I'll summarize what I came to understand, with the caveat that someone "more developer than me" could surely explain it better and/or find mistakes in my understanding.

This post taught me how to check an app's entitlements. For example, here's what our latest GA Zui Intel build v1.7.0 shows:

$ codesign -d --entitlements - --xml /Applications/Zui.app
Executable=/Applications/Zui.app/Contents/MacOS/Zui
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict><key>com.apple.security.cs.allow-unsigned-executable-memory</key><true/><key>com.apple.security.cs.disable-library-validation</key><true/></dict></plist>

So, picking that apart, we see com.apple.security.cs.allow-unsigned-executable-memory and com.apple.security.cs.disable-library-validation. We never took explicit steps to ask that these be included in our builds, but I found this file in electron-builder that references them, so that seems to explain why they're there by default. Strangely, that also shows an entry for com.apple.security.cs.allow-jit and it's been in there since January, 2020 (electron-userland/electron-builder#4491) including in the electron-builder version I started using in this PR, so I'm still not 100% sure why I had to ask for it explicitly. But now that I was aware that the Apple docs effectively discourage the use of those two entitlements, I became more interested in knowing if we could live without them going forward.

I did ultimately find evidence that we can leave them behind.

  1. This comment linked from that plist file explains that the com.apple.security.cs.allow-unsigned-executable-memory entitlement is only needed for Electron 11, and that its use in Electron 12+ is discouraged due to the same security exposure cited by Apple.
  2. This issue also linked from that plist file ultimately concludes with this comment that happens to be a fix I already put in Zui as part of Prevent packing of native modules #2917, indicating we can drop this entitlement as well.

Finally, using the config in this PR that dropped those two entitlements and left us with only the JIT one, I did smoke testing with an Intel build doing all the typical user stuff (loading pcaps, opening slices in Wireshark, opening whois, etc.) and it all went fine.

It also should be noted that most mentions of entitlements are in the context of discussing App Sandbox, so I had to learn a bit about that as well. Consider the entitlements shown for a common app like Slack:

$ codesign -d --entitlements - --xml /Applications/Slack.app/Executable=/Applications/Slack.app/Contents/MacOS/Slack
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict><key>com.apple.security.app-sandbox</key><true/><key>com.apple.security.application-groups</key><array><string>BQR82RBBHL.com.tinyspeck.slackmacgap</string><string>BQR82RBBHL.slack</string></array><key>com.apple.security.cs.allow-jit</key><true/><key>com.apple.security.device.audio-input</key><true/><key>com.apple.security.device.camera</key><true/><key>com.apple.security.device.microphone</key><true/><key>com.apple.security.device.usb</key><true/><key>com.apple.security.files.bookmarks.app-scope</key><true/><key>com.apple.security.files.downloads.read-write</key><true/><key>com.apple.security.files.user-selected.read-write</key><true/><key>com.apple.security.network.client</key><true/><key>com.apple.security.network.server</key><true/><key>com.apple.security.print</key><true/></dict></plist>

You'll notice entitlements in here like com.apple.security.network.client. Zui has acted as a "network client" just fine all these years but we don't list this entitlement, so what's up? Well, what I've learned is how Apple offers this "App Sandbox" wrapper as a way to run apps more safely, and one requirement of this is that the app has to list all of its entitlements for the things it might potentially do, hence things like hitting the network, accessing your microphone, etc. To be distributed through the App Store, an app must run in Sandbox, so that explains why we saw what we did with Slack. Likewise, since Zui isn't distributed through App Store, we've never had to.

If you're curious, you can add a column to Activity Monitor that'll show which apps are running in the Sandbox. While many are (e.g., Slack), many I use every day are not (e.g., Terminal, Google Chrome, Camtasia), so by no means is it a black eye that we're non-Sandbox.

image

To tie this all back to Entitlements, I ultimately recognized that dealing with them is necessary for Sandbox and being in the App Store, hence why I saw these topics mentioned together so often. However, there are also non-Sandbox/non-App-Store contexts when Entitlements manner, such as this JIT setting. But now we know a bit more should the day come that we want to start jumping through hoops to publish Zui in the App Store.

Yet another topic I encountered in my travels was Hardened Runtime, since that mentions how it disables JIT compilation and why the entitlement is therefore needed in our case (see also: Porting just-in-time compilers to Apple silicon for why it's needed with Apple Silicon but wasn't with Intel). An EntitlementCheck tool was ultimately useful to confirm that we've been using Hardened Runtime on our Zui builds all along, so once I confirmed we were in line with best practices there and the JIT entitlement got the Apple Silicon builds working, I was satisfied. Below is with the last GA Zui release v1.7.0:

$ ./Hardened_Runtime_Check.py | grep -i zui
[+] Hardened runtime enabled in: /Applications/Zui.app/Contents/MacOS/Zui

Per its name, that same repo has a tool that flags undesirable entitlements, and indeed this also flagged the two that we'd had in the past and won't anymore with the new builds that use only the JIT entitlement:

$ ./Entitlements_Check.py | grep -i zui
[-] Binary can load arbitrary unsigned plugins/frameworks (has com.apple.security.cs.disable-library-validation entitlement): /Applications/Zui.app/Contents/MacOS/Zui
[-] Binary allows c code patching, NSCreateObjectFileImageFromMemory, or dvdplayback framework (has com.apple.security.cs.allow-unsigned-executable-memory entitlement): /Applications/Zui.app/Contents/MacOS/Zui

Merging latest-mac.yml

While the changes above got the Apple Silicon builds working, a problem remained with the auto-updates. The tl;dr is the update process relies on the file latest-mac.yml that's generated during the build process. Here's an example of the one from the last GA Zui release v1.7.0:

version: 1.7.0
files:
  - url: Zui-1.7.0-mac.zip
    sha512: 4IO2UaJrfX97593l9oVdq3IyuPoouFyloN8on51HFTZQlRoZF62UKrSQmAj0zzDBVjD4botyw4bInXJsJKOrJw==
    size: 251289997
  - url: Zui-1.7.0.dmg
    sha512: N0csX2QLLWBV9I9Hd0IgVWoeLYb0h63wjq5reVoVE114QzYMmhJwn0imN5mnqMoukXgxywi6ZpVg9dVZif/tZg==
    size: 259715184
path: Zui-1.7.0-mac.zip
sha512: 4IO2UaJrfX97593l9oVdq3IyuPoouFyloN8on51HFTZQlRoZF62UKrSQmAj0zzDBVjD4botyw4bInXJsJKOrJw==
releaseDate: '2024-04-03T15:52:00.673Z'

An Apple Silicon build would create a similar file but with entries for Zui-1.7.4-arm64.dmg and Zui-1.7.4-arm64-mac.zip. When running separate Intel and Apple Silicon builds converged on the same GitHub Releases destination like I'm doing in this PR, what ends up happening is that the build that finishes last pushes its latest-mac.yml to GitHub, overwriting the one generated by the first build.

As mentioned in a comment in the merge-mac-release-files.mjs script added in this PR, I found an open electron-builder issue where other users were struggling with the same problem and so I borrowed the code from this comment to create a workaround script to do the same for our build process. In brief, the builds for each macOS platform push their own separate latest files temporarily to GitHub, then the build that finishes second downloads both files, merges them to a single latest-mac.yml that mentions both the Intel and Apple Silicon offerings, then pushes that file back up to GitHub and removes the temporary files. It's a little hacky, but it works.

Upgrade Path

@mattnibs helped test all this using releases in personal forks I created of Zui and Zui Insiders where I could publish real releases without risk of impacting our existing user base. From what we found, here's what I expect we'll need to communicate to macOS users.

  1. Having been running Intel builds all this time, if they continue to rely only on auto-update, they'll continue to be updated to Intel builds even as the Apple Silicon ones become available and referenced in the latest-mac.yml.

  2. When a user wants to switch to Apple Silicon, they should do a one-time manual download/install of a Zui-*-arm64.dmg build. During installation, they can accept the prompt to Replace the existing (Intel-based) Zui. All their settings and Zed lake data will remain intact.

  3. As newer releases come out, the users that did the one-time manual install of an Apple Silicon build will auto-update to the newer Apple Silicon builds.


Closes #1266

@philrz philrz requested review from mattnibs, nwt and jameskerr May 22, 2024 20:19
@philrz philrz self-assigned this May 22, 2024
@@ -69,7 +69,7 @@ runs:
env:
GH_TOKEN: ${{ inputs.gh_token }}
APPLE_ID: ${{ inputs.apple_id }}
APPLE_ID_PASSWORD: ${{ inputs.apple_id_password }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ inputs.apple_id_password }}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we transition from the separate electron-builder-notarize to leveraging the notarization support that's in electron-builder, the name of this environment variable changes but everything else stays the same.

@@ -87,5 +87,5 @@ runs:
- name: Check notorization with gatekeeper
if: runner.os == 'macOS'
run: |
spctl --assess --type execute --verbose --ignore-cache --no-cache dist/apps/zui/mac/*.app
spctl --assess --type execute --verbose --ignore-cache --no-cache dist/apps/zui/mac*/*.app
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The wildcard is needed here because the Apple Silicon builds end up in a subdirectory mac-arm64/ whereas the Intel ones were in mac/.

- name: Install Node
uses: actions/setup-node@v3
with:
cache: yarn
# cache: yarn
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In an early test iteration, @mattnibs spotted that the zed binaries bundled with Zui were still showing up as Intel. I ultimately traced it to a side effect of this caching, then found open issue actions/setup-node#1008 that seems to indicate this is a known problem. I briefly went down the path of trying to find another way to keep caching in play via some other config, but I also noticed that disabling caching wasn't slowing down the build process much at all. In the interest of simplicity I figured we could just live without it for now. I'm Subscribed to that issue so if/when it gets addressed I could revisit this. In the meantime I've included this comment so someone doesn't turn it on again without recognizing the effect it'll have on the macOS builds.

@@ -10,11 +10,11 @@
"sign": "./scripts/sign.js"
},
"linux": {"target": ["deb", "rpm"]},
"mac": {"entitlements": "darwin.plist", "notarize": {"teamId": "2DBXHXV7KJ"}},
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The teamId value here is redundant with the value in the APPLE_TEAM_ID environment variable, but it's needed here due to open issue electron-userland/electron-builder#8103. I'm Subscribed to that issue so I can revisit this if/when it gets fixed. Also, found consensus validated my guess that this team ID does not seem to be sensitive info (e.g., similar to a company/domain name), so I've gone ahead and put the value here rather than adding yet more code to populate it from a Secret.

// both regular Zui and Zui Insiders.
const OWNER = pkg.repository.split('/')[3];
const REPO = pkg.repository.split('/')[4];
const PRODUCT_NAME= pkg.productName.replaceAll(' ', '-');
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This replaceAll was needed because the build filenames that start out with space characters in them (e.g., Zui - Insiders-1.7.1-19.dmg) have those spaces turned into hyphens by the time they're uploaded to GItHub. I briefly explored using this as an opportunity drop those space characters entirely, but post-update that ends up putting the new app state & saved data in a fresh, new directory ~/Library/Application Support/Zui-Insiders hence losing the prior state/data from the traditional ~/Library/Application Support/Zui - Insiders, hence that would require a new migration logic for users to have a seamless update. Yuck! Hence the replaceAll to just keep going with what we've got.

const REPO = 'zui';
const URL = `/repos/${OWNER}/${REPO}/releases`;
const VERSION = pkg.version;
const RELEASE_NAME = (PRODUCT_NAME == 'Zui') ? 'v' + VERSION : VERSION;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's another coping mechanism. For all the time they've existed, Zui Insiders builds have names without the leading v (e.g., 1.7.1-19) while regular Zui have it (e.g., v1.7.0). Here I rationalized leaving things as they are because GitHub already does some annoying things with how it sorts releases, so I didn't want to make things even worse by also changing the naming convention.

@philrz philrz marked this pull request as ready for review May 22, 2024 20:49
Comment on lines 162 to 172
try {
const originalAsset = currentRelease.assets.find(asset => {
return asset.name === FILE_NAME;
})

if (!originalAsset) {
console.log(`[remote] ${FILE_NAME} not found. Skipping merge`);
return;
} else {
console.log(`[remote] ${FILE_NAME} found`)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would move this stuff out of the try block since that it was you are doing elsewhere in this file. Also you can drop the else and just console log.

"mac": {
"entitlements": "darwin.plist",
"notarize": {"teamId": "2DBXHXV7KJ"},
"artifactName": "${productName}-${version}-${arch}.${ext}"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At first when using electron-builder at its defaults, we'd get a a DMG for Intel like Zui-1.7.4.dmg and for Apple Silicon like Zui-1.7.4-arm64.dmg. This use of artifactName gives us symmetry by having the Intel one now have the arch included as well and hence Zui-1.7.4-x64.dmg.

philrz and others added 19 commits May 23, 2024 15:41
Co-authored-by: Matthew Nibecker <[email protected]>
Co-authored-by: Matthew Nibecker <[email protected]>
@philrz philrz requested a review from mattnibs May 24, 2024 17:08
.github/actions/setup-zui/action.yml Outdated Show resolved Hide resolved
@philrz philrz merged commit cde7ad8 into main May 25, 2024
4 checks passed
@philrz philrz deleted the mac-silicon branch May 25, 2024 00:19
This was referenced Jun 6, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Build/test for Mac M1/Silicon models
3 participants