electron / universal

Create Universal macOS applications from two x64 and arm64 Electron applications
MIT License
112 stars 43 forks source link

best practices workflow for using this with electron-forge especially code signing and notarizing #50

Closed marcelblum closed 2 years ago

marcelblum commented 2 years ago

Apologies for this basic question, but I'm feeling a bit in the dark as to how and when (at what point in the build/package process) to use this package especially in conjunction with the code signing, notarizing, and dmg-making steps, and which steps can only be done on one arch or the other (and the brief docs both here and at electron-packager are lacking). I use electron-forge and part of my ignorance here surely stems from how beautifully Forge handles all these steps automagically. Here's the best I can infer as to what my build process for a universal binary should be, but it feels clunky and I'd greatly appreciate it if anyone could correct me if/where this is a wrong or wildly inefficient way to go about this:

  1. On an Intel Mac, do an npm run package, skipping the signing and notarizing steps by leaving the osxSign and osxNotarize config options undefined, to create an unsigned Intel .app bundle
  2. On an Arm Mac, npm run package, again without signing/notarizing
  3. Copy the built unsigned Intel .app over to the Arm Mac
  4. On the Arm Mac, run a script that calls makeUniversalApp() from this package, pointing to the unsigned Intel & Arm builds
  5. On the Arm Mac, use the command line (or some own built script) to codesign and notarize the resulting universal .app
  6. On the Arm Mac, use the command line (or some own built script) to create a .dmg containing the universal .app
  7. On the Arm Mac, use the command line (or some own built script) to codesign and notarize the resulting .dmg

I noticed some past comments mention the possibility of including a universal value for the arch property in electron-packager, but the official docs do not mention universal as an valid option. Then there is the osxUniversal property but it's not really clear how to use it, or at what point in my steps above.

MarshallOfSound commented 2 years ago

@marcelblum I think it just hasn't been updated yet, you should just set universal as the architecture for forge / packager. You don't need to hop around between devices either, on a new enough macOS you can build for both arm or intel on the other architecture.

E.g. electron-forge package --arch=universal should work (although iirc shows some weird terminal output 😆 )

marcelblum commented 2 years ago

@MarshallOfSound thanks for the clarification, I didn't realize I could build for either target on either arch now. But I still don't get how little details like arch-specific native module files are handled if everything was built on the same machine. Would I need to maintain 2 separate project folders each with their own separate node_nodules, one for each arch? Or just add a bunch of special case code to handle modules that need it? Some native modules have their own package folder structure with different folders for different arch builds but not all do. For example a package that looks for its binaries under \node_modules\[name]\build\Release\[name].node rather than say \node_modules\[name]\builds\[archname]\build\Release\[name].node.

marcelblum commented 2 years ago

Wanted to report back in case anyone stumbles on this issue. I was able to build a universal binary with Electron Forge on an m1 mac relatively easily but there were 2 gotchas that aren't well documented, hopefully this helps someone else out there:

1) With Forge the only way to package/make for the universal target is to use the command line option AFAICT. There is no way to specify it in the forge config, any packagerConfig.arch value is ignored, and if you try to use packagerConfig.all it will throw an error.

2) Native modules: My project was depending on a native module whose package folder structure did not use arch-specific folder names for its binary builds, presenting a problem in creating a universal binary that needed to contain 2 different arch versions of the package binaries in a way that the package could find them. In my case this package used the popular node-bindings to load its binaries. node-bindings actually searches in a variety of folder name permutations for the binary, including one permutation that includes the platform and arch, see here. So I was able to simply rename the folder structure to include the different arch version binaries in folders node-bindings automatically tries, without having to fork the package or do any clunky duping of the package to get it to work. So if you run into this issue, check if your native module dependency is using node-bindings because that offers a painless solution.