<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Krzysztof Tarnowski</title>
    <description>The latest articles on DEV Community by Krzysztof Tarnowski (@christarnowski).</description>
    <link>https://dev.to/christarnowski</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F372983%2F40cb5672-b1ae-4b51-8cbe-43848a3e950f.jpg</url>
      <title>DEV Community: Krzysztof Tarnowski</title>
      <link>https://dev.to/christarnowski</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/christarnowski"/>
    <language>en</language>
    <item>
      <title>Starting small and opting for “good enough” in a startup</title>
      <dc:creator>Krzysztof Tarnowski</dc:creator>
      <pubDate>Sun, 11 Jul 2021 17:33:12 +0000</pubDate>
      <link>https://dev.to/christarnowski/starting-small-and-opting-for-good-enough-in-a-startup-3c63</link>
      <guid>https://dev.to/christarnowski/starting-small-and-opting-for-good-enough-in-a-startup-3c63</guid>
      <description>&lt;p&gt;At the most recent company I co-founded, where we’re making a product for Instagram influencers 📷, we made a conscious decision to start small in terms of features, headcount, money and time. I want to share with you a short story of our product’s development and convince you that “small” and “good enough” is often the way to go. Why? Because it simply lets you focus on things that actually matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Early decisions
&lt;/h2&gt;

&lt;p&gt;We started with an idea of building a product for Instagram users that would let them devote more time to creating engaging content their followers (and brands) love. In the long term, the product would have to offer advanced features that would take a bit of time to develop. Being ambitious in this context is perfectly fine, but we knew we had to start somewhere and first build, then test in the wild, a proof-of-concept that would serve as a platform upon which we could build bigger things.&lt;/p&gt;

&lt;p&gt;To borrow naming conventions from &lt;a href="https://en.wikipedia.org/wiki/List_of_Marvel_Cinematic_Universe_films"&gt;Marvel’s MCU plan&lt;/a&gt;, our “Phase I” focused on validating key technical assumptions we’ve started with. The prototype developed at this stage was very crude, engineering-wise, and didn’t even have a UI of any sorts. It didn’t take long to develop and helped us better understand the technicalities of the problem we were trying to solve.&lt;/p&gt;

&lt;p&gt;Next was “Phase II”, where the product’s featureset was limited to only the most sought-after, low-hanging automation tasks that we could ship to get the ball rolling. On the engineering side, we cut costs and saved time wherever possible. For example, instead of developing native versions of our desktop app for macOS, Windows and Linux, we opted for the Electron ecosystem. This meant we could develop the app once and distribute it to multiple platforms from a single codebase. What’s more, instead of building our very own auto-update system for the app, we could lean on existing libraries and tools (Open Source ❤️). In conjunction with Amazon S3 for hosting binaries (installers and update files), this also meant we didn’t have to spend a dime to get off the ground and move to validation with customers.&lt;/p&gt;

&lt;p&gt;The “start small” mentality also extended to other parts of running a software company. For example, when it comes to website and email hosting, Zenbox has proven to be extremely cost effective and good enough for our use case. Even WordPress, which honestly I was not a huge fan of, later on allowed us to build the whole website, including checkout flow for subscription plans and invoicing, without too much of an effort. The website works well and looks good on the outside (ready-to-use, commercial WP themes!), and even though overall it may not be an engineering state-of-the-art, it serves its purpose really well.&lt;/p&gt;

&lt;p&gt;One thing of note here is, while we cut corners whenever and wherever possible, we made sure to lay the groundwork for the future. In particular, each technical component is reasonably maintainable and swappable. The desktop app, for example, has a modular architecture and is not directly tied to Electron or the desktop itself, giving us a lot of options for what we have laid out in our roadmap.&lt;/p&gt;

&lt;p&gt;Overall, the time saved here and there allowed us to safely focus on other challenges that come with building a product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Early adopters
&lt;/h2&gt;

&lt;p&gt;You’re in a good spot when customers tell you what they think about your product and their experience in general. Without that precious feedback, you’re essentially flying in the dark, so make sure you’re doing literally everything you can to get it. Be prepared for a good fight, as nowadays many things fight for customer’s attention, and cherish every effort on the customer’s part to talk to you about your product.&lt;/p&gt;

&lt;p&gt;In our case, we enrolled our friends and friends of friends to test out prototypes and tell us about their experience. The feedback we’ve received was not only humbling, since it tested our assumptions about the needs of our potential customers, but also encouraging (we learned something!). We were even lucky to hear people say they experienced “wow!” moments on their journey with the product. At the end of this stage, a few weeks into development, we had a piece of software we could say was good enough to give into the hands of a broader audience. Naturally, the product backlog had quite the number of items in it, including bugs to squash 🐞, but doing anything more than that would mean delaying the launch. To paraphrase Reid Hoffman, if you’re happy with your product at launch, it means you waited too long to ship.&lt;/p&gt;

&lt;p&gt;The launch was also “small”. There are plenty of software and hardware stack combinations people use, so we made the product available only to a limited number of people (private beta), with potential customers having to subscribe to a waitlist. It gave us precious time to thoroughly fix issues as they pop up. &lt;/p&gt;

&lt;h2&gt;
  
  
  Perfect is the enemy of good
&lt;/h2&gt;

&lt;p&gt;I applied the “good enough” approach to hard engineering as well. In one case, we wanted for the desktop app to be properly packaged, so OS (Windows, macOS) would identify our software as safe to run. It was important to us, since it would add to the credibility of the product and help build trust. Unfortunately, I ran into an &lt;a href="https://christarnowski.com/making-notarization-work-on-macos-for-electron-apps-built-with-electron-builder/"&gt;issue with macOS app notarization&lt;/a&gt; that took me some time to investigate. I came up with a solution that was a bit crude and involved monkey-patching one of the libraries used by our build system. At that point I faced a choice:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Spend more time on this issue and try to get to the root cause of the problem, potentially arriving at a better way to fix this, or&lt;/li&gt;
&lt;li&gt;Make it work, even if it was suboptimal, and move on to other, more pressing tasks.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I must admit, I had that urge inside me to soldier on, to fully understand the problem, to arrive at an elegant solution. But, after a moment of reflection, I knew getting it perfect wouldn’t matter to our customers. They wouldn’t care about our build system. Heck, they probably wouldn’t even know we had one! To them, the end product, the customer experience, is what matters most. That’s how they will judge ⚖️ the product and the service.&lt;/p&gt;

&lt;p&gt;There’s also another dimension to opting for “good enough” in the software world. Business requirements, libraries, frameworks – you name it – change rapidly and the software I write today might be obsolete tomorrow.  Especially feedback from the customers can (and often should) make a huge impact on a product. In our case, the first iteration of the desktop UI served its purpose, but people told us they were a bit lost when just starting out. We quickly made a lot of changes based on the feedback, creating an entirely new experience for one of the core flows. And while we used off-the-shelf UI components and reused parts of the code we have already written, some had to be thrown away.&lt;/p&gt;

&lt;p&gt;In my opinion it’s not worth spending too much time on things that will undoubtedly change. Sure, I still will go for maintainable (read: easy to change/swap), but I would rather focus on coming up with solutions that are optimal in the business context. I know it will pay dividends 💰 later on.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few words on hiring
&lt;/h2&gt;

&lt;p&gt;Try to postpone hiring for as long as possible. It’s also part of “starting small” and is an important mindset to have early on. It’s not about money and time, but first getting a firm understanding of the business beforehand. Once you’ve obtained enough knowledge and experience, hiring someone to take over your roles will be way easier and more efficient.&lt;/p&gt;

&lt;p&gt;To give you some examples, we did all the design work ourselves, in some instances leaning on ready-to-use products, in others engaging our creative side (like for UI or promo video). We also considered finding someone to fully own the marketing side of the business, but opted to take care ourselves of things like content creation and promotion on social media. All in a quest to better understand what is actually needed for the business, long term.&lt;/p&gt;

&lt;p&gt;Keep in mind, tough, that it depends on the skillset of the funding team. The broader the better, with few focuses that really stand out. We are lucky to have such depth in the founding team 🦾, despite a highly engineering background. It’s not all or nothing – when in a pinch, we can always ask friends for help or hire a contractor for a specific task.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;In a startup world (and life in general), time is a valuable currency&lt;/strong&gt;. Choose a technology that lets your company or project move as fast as reasonably possible at each stage of product development.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Customers don’t care how a developer solved a problem on the software side&lt;/strong&gt;. They want the product to solve their pain, be reliable and be easy to use.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;When building a product (startup, side-business), try to build great software, but &lt;strong&gt;don’t shy away from using shortcuts where it makes sense&lt;/strong&gt;, unless you plan to send people to space. There’s always plenty of other things to work on.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Feedback and questions are more than welcome, either in comments or on social media 🙂&lt;/p&gt;

&lt;p&gt;Thanks a ton to Piotr Tomiak (&lt;a href="https://twitter.com/PiotrTomiak"&gt;@PiotrTomiak&lt;/a&gt;) and Jakub Tomanik (&lt;a href="https://twitter.com/jakub_tomanik"&gt;@jakub_tomanik&lt;/a&gt;) for reading drafts of this article.&lt;/p&gt;

</description>
      <category>startup</category>
    </item>
    <item>
      <title>Making notarization work on macOS for Electron apps built with Electron Builder</title>
      <dc:creator>Krzysztof Tarnowski</dc:creator>
      <pubDate>Fri, 09 Jul 2021 12:45:12 +0000</pubDate>
      <link>https://dev.to/christarnowski/making-notarization-work-on-macos-for-electron-apps-built-with-electron-builder-1i9k</link>
      <guid>https://dev.to/christarnowski/making-notarization-work-on-macos-for-electron-apps-built-with-electron-builder-1i9k</guid>
      <description>&lt;p&gt;I ❤️ building things and, when it comes to software, I’ve done that for quite a few platforms and in various programming languages over the years. Recently I’ve been developing a desktop app built with Electron and I must say the whole first-timer experience has been rather pleasing. One thing that required “a bit” of attention was the build process for different platforms (Windows, macOS) and part of it was the app notarization step on macOS. What on paper looked like a really easy thing to do, took me a couple of hours and a lot of detective work to get it right 🕵️‍♀️.&lt;/p&gt;

&lt;p&gt;Below is a &lt;strong&gt;step by step guide on how to set up notarization on macOS&lt;/strong&gt; when using &lt;a href="http://electron.build"&gt;Electron Builder&lt;/a&gt; (22.7.0) and &lt;a href="https://github.com/electron/electron-notarize"&gt;Electron Notarize&lt;/a&gt; (1.0.0), including a complete workaround for an issue I’ve experienced that has to do with Apple Notarization Service. Hopefully, I will be able to help you out like a true superhero 🦸🏻‍♂️, so your time and effort can be devoted to other, more pressing matters 🦾.&lt;/p&gt;

&lt;h2&gt;
  
  
  A bit of context
&lt;/h2&gt;

&lt;p&gt;Want the solution right away 🧐? Skip to the step by step guide.&lt;/p&gt;

&lt;p&gt;Why even bother with notarization in the first place? Well, on macOS (and Windows for that matter) there are various security mechanisms built into the operating system to prevent malicious software from being installed and run on a machine. macOS and Windows both require installers and binaries to be cryptographically signed with a valid certificate. On macOS, however, there is an additional build-time notarization step that involves sending a compressed .app archive to Apple’s Notarization Service (ANS) for verification.&lt;/p&gt;

&lt;p&gt;In most instances, the whole process is painless, but in my case, i.e. an Electron app with a lot of dependencies and third-party binaries, not so much 🤕. It turns out the ANS expects the ZIP archive of .app package to be compressed using the PKZIP 2.0 scheme, while the default zip utility, shipped with macOS and used by Electron Notarize, features version 3.0 of the generic ZIP algorithm. There are some notable differences between the two and to see what I mean, try manually signing &lt;code&gt;.app&lt;/code&gt;, then compressing it using:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Command-line &lt;code&gt;zip&lt;/code&gt; utility,&lt;/li&gt;
&lt;li&gt;“Compress” option found in Finder,&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And submitting it for notarization from the command line. The Finder-created archive will pass, while zip-one will fail.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;zipinfo&lt;/code&gt; command line tool reveals that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Finder uses PKZIP 2.0 scheme, while &lt;code&gt;zip&lt;/code&gt; version 3.0 of the generic ZIP algorithm.&lt;/li&gt;
&lt;li&gt;Finder compresses all the files in .app as binaries, while “zip” treats files according to the content type (code as text, binaries as binaries).&lt;/li&gt;
&lt;li&gt;Finder includes magical &lt;code&gt;__MACOSX&lt;/code&gt; folders to embed macOS-specific attributes into the archive, especially for links to dynamic libraries (e.g. found in some Node modules).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One way of getting around the above issue is to use &lt;code&gt;ditto&lt;/code&gt; instead of &lt;code&gt;zip&lt;/code&gt; to create a compressed archive of an &lt;code&gt;.app&lt;/code&gt; package. &lt;a href="https://ss64.com/osx/ditto.htm"&gt;Ditto&lt;/a&gt; is a command line tool shipped with macOS for copying directories and creating/extracting archives. It uses the same scheme as Finder (PKZIP) and preserves metadata, thus making the output compatible with Apple’s service. The relevant options for executing &lt;code&gt;ditto&lt;/code&gt; in this context, i.e. to mimic Finder’s behavior, are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-c&lt;/code&gt; and &lt;code&gt;-k&lt;/code&gt; to create a PKZIP-compressed archive,&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;—sequesterRsrc&lt;/code&gt; to preserve metadata (&lt;code&gt;__MACOSX&lt;/code&gt;),&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;—keepParent&lt;/code&gt; to embed parent directory name source in the archive.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The complete invocation looks as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ditto -c -k —sequesterRsrc —keepParent APP_NAME.app APP_NAME.app.zip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To apply this to Electron Builder’s notarization flow, you need to monkey patch Electron Notarize’s .app and make the compress step use “ditto”. This can be done via “afterSign” hook defined in the Electron Builder’s configuration file.&lt;/p&gt;

&lt;p&gt;You can learn in an &lt;a href="https://christarnowski.com/blog/making-notarization-work-on-macos-for-electron-apps-built-with-electron-builder"&gt;follow up essay&lt;/a&gt; why I chose this particular approach. Hope you love it!&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up macOS app notarization, including workaround
&lt;/h2&gt;

&lt;p&gt;Before you start, you first need to properly configure &lt;a href="https://developer.apple.com/developer-id"&gt;code signing&lt;/a&gt;, as per the &lt;a href="https://www.electron.build/code-signing"&gt;official documentation of Electron Builder&lt;/a&gt; and various guides¹. For completeness sake I’ve included here all the steps required for making the notarization to work based on my experience and excellent work by other developers¹.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://support.apple.com/en-us/HT204397"&gt;Create an app-specific password&lt;/a&gt; to use with Apple notarization service. Preferably using your organization’s developer Apple ID.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Create an Entitlements &lt;code&gt;.plist&lt;/code&gt; file specific to your Electron apps. In our case, the following did the trick (&lt;code&gt;entitlements.mac.plist&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;plist&lt;/span&gt; &lt;span class="na"&gt;version=&lt;/span&gt;&lt;span class="s"&gt;"1.0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;dict&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- https://github.com/electron/electron-notarize#prerequisites --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;com.apple.security.cs.allow-jit&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;true/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;com.apple.security.cs.allow-unsigned-executable-memory&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;true/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;com.apple.security.cs.allow-dyld-environment-variables&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;true/&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- https://github.com/electron-userland/electron-builder/issues/3940 --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;com.apple.security.cs.disable-library-validation&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;true/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/dict&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/plist&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Set &lt;code&gt;entitlements&lt;/code&gt; and &lt;code&gt;entitlementInherit&lt;/code&gt; options for macOS build in Electron Builder’s configuration file to the &lt;code&gt;.plist&lt;/code&gt; created in the previous step.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Create a &lt;code&gt;notarize.js&lt;/code&gt; script to execute after Electron Builder signs the &lt;code&gt;.app&lt;/code&gt; and its contents. Place the file in the build directory defined in Electron Builder’s configuration file.&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;notarize&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;electron-notarize&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;notarizing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;electronPlatformName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;appOutDir&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;electronPlatformName&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;darwin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;appName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;packager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;productFilename&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;notarize&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;appBundleId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;APP_BUNDLE_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;appPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;appOutDir&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;appName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.app`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;appleId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;APPLE_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;appleIdPassword&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;APPLE_ID_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Add &lt;code&gt;"afterSign": "./PATH_TO_NOTARIZE_JS_IN_BUILD_DIRECTORY”&lt;/code&gt; to Electron Builder’s configuration file.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Monkey patch Electron Notarize. The script should run before Electron Builder’s CLI command. In our case, since we’ve taken a very modular approach to general app architecture, the build scripts (TypeScript files) include a separate &lt;code&gt;commons&lt;/code&gt; module, which is imported by Electron Notarize patcher. The &lt;code&gt;.ts&lt;/code&gt; files can be executed using &lt;code&gt;ts-node&lt;/code&gt; via&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ts-node -O {\"module\":\"CommonJS\"} scripts/patch-electron-notarize.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The patcher itself does one thing only, that is, it replaces the following piece of the code in &lt;code&gt;build/node_modules/electron-notarize/lib/index.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zip&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-r&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-y&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;zipPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;basename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appPath&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ditto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-c&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-k&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--sequesterRsrc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--keepParent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;basename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appPath&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;zipPath&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our code for the &lt;code&gt;commons&lt;/code&gt; (&lt;code&gt;patcher-commons.ts&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;promises&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;fsp&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;FileContentsTransformer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;replaceFileContents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;transformer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FileContentsTransformer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fsp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FileHandle&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;fh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fsp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;r&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nx"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;fh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fsp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;w&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;writeFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;transformer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and the patcher (&lt;code&gt;patch-electron-notarize.ts&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;FileContentsTransformer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;replaceFileContents&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./common&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ELECTRON_NOTARIZE_INDEX_PATH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;build/node_modules/electron-notarize/lib/index.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;transformer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FileContentsTransformer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;spawn('zip', ['-r', '-y', zipPath, path.basename(opts.appPath)]&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;spawn('ditto', ['-c', '-k', '--sequesterRsrc', '--keepParent', path.basename(opts.appPath), zipPath]&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;replaceFileContents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ELECTRON_NOTARIZE_INDEX_PATH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;transformer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// noinspection JSIgnoredPromiseFromCall&lt;/span&gt;
&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Set &lt;code&gt;APPLE_ID&lt;/code&gt; and &lt;code&gt;APPLE_ID_PASSWORD&lt;/code&gt; environment variables (the ones defined in Step 1) before running Electron Builder on your developer machine or in your CI environment. You can use Keychain on your local machine instead.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And that’s pretty much it. You can check out a &lt;a href="https://github.com/christarnowski/electron-builder-notarization-for-macos-example"&gt;simple, working example&lt;/a&gt; to see how to put this all together. Now you can spend the extra time on something you enjoy doing 🏖!&lt;/p&gt;

&lt;h2&gt;
  
  
  Three takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;When stuck, look for the root cause in the least expected places&lt;/strong&gt;. In the case of my project, the compression step was the unexpected culprit.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Be stubborn when a particular feature or bugfix is essential to a product’s success&lt;/strong&gt;. Here, the notarization was important and it took some time to get it right, but the end result is customers feeling safe when installing the software.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Sometimes “working” is good enough&lt;/strong&gt;. I could develop a better solution, but that would take some precious time. I opted to focus on more pressing issues instead.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Feedback and questions are more than welcome, either in comments or on social media 🙂&lt;/p&gt;

&lt;p&gt;Thanks a ton to Piotr Tomiak (&lt;a href="https://twitter.com/PiotrTomiak"&gt;@PiotrTomiak&lt;/a&gt;) and Jakub Tomanik (&lt;a href="https://twitter.com/jakub_tomanik"&gt;@jakub_tomanik&lt;/a&gt;) for reading drafts of this article.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Relevant sources: &lt;a href="https://medium.com/@TwitterArchiveEraser/notarize-electron-apps-7a5f988406db"&gt;https://medium.com/@TwitterArchiveEraser/notarize-electron-apps-7a5f988406db&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;GitHub Gists of the complete code.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>development</category>
      <category>electron</category>
      <category>node</category>
    </item>
  </channel>
</rss>
