Weekly News Summary for Admins — 2023-10-13

In my session at MacSysAdmin conference in Gothenburg last week, I announced that the name of this weekly news summary will be changing to “MacAdmins.news“.


(Sponsor: SentinelOne)

macOS MetaStealer | New Family of Obfuscated Go Infostealers Spread in Targeted Attacks

The rise of macOS infostealers continues with the latest entrant aiming to compromise business environments with targeted social engineering lures.

Continue Reading here


Why the change? This news summary has taken on a life of its own from my main Scripting OS X blog. I had been thinking it deserved a name and domain of its own, for a while, but I couldn’t think of one that I liked. When I realized that MacAdmins.news was available and I checked in with the MacAdmins Foundation if they would mind me using it.

I had already been double-posting to the new website for a few weeks to make sure that everything is working. Over the next few weeks, I will be updating everything else. Slowly and carefully, I don’t want to disrupt anything too badly.

If you are receiving this news summary by email and have mail rules by subject or sender you may have to update them, but I will announce the changes here before they take effect in the emails.

Go read this week’s news summary on MacAdmins.news!

If you would rather get the weekly newsletter by email, you can subscribe to the MacAdmins.news here!! (Same content, delivered to your Inbox once a week.)

Weekly News Summary for Admins — 2023-10-06

Just a short news summary this week, as I am busy presenting today at MacSysAdmin in Gothenburg!


(Sponsor: SentinelOne)

Bloated Binaries | How to Detect and Analyze Large macOS Malware Files

Massive malware binaries are becoming more common on macOS and can cause problems for detection and analysis. Here’s how we can successfully deal with them.

Continue Reading here


If you would rather get the weekly newsletter by email, you can subscribe to the Scripting OS X Weekly Newsletter here!! (Same content, delivered to your Inbox once a week.)

📰News and Opinion

⚙️macOS and iOS Updates

🔨Support and HowTos

🤖Scripting and Automation

🍏Apple Support

♻️Updates and Releases

🎧To Listen

🎈Just for Fun

📚Support

If you are enjoying what you are reading here, please spread the word and recommend it to another Mac Admin!

Weekly News Summary for Admins — 2023-09-29

Release week part 2! We got macOS Sonoma 14.0 release. If you were running the RC2, that is the same as the release. We also got the first round of iOS 17.1 and macOS Sonoma 14.1 betas! (It’ll be good to have the minor update numbers for iOS and macOS in sync for a change.


(Sponsor: Mosyle)

Mosyle Logo

The only Apple Unified Platform for Business

Mosyle is the only solution that fully integrates Enhanced MDM, Endpoint Security, Internet Privacy & Security, Single Sign-On, and Application Management on a single Apple-only platform.

Click here to learn why Mosyle is all you need to work with Apple.


If you would rather get the weekly newsletter by email, you can subscribe to the Scripting OS X Weekly Newsletter here!! (Same content, delivered to your Inbox once a week.)

macOS Sonoma

Reviews

MacAdmins

Security and Privacy

Support and HowTos

Scripting and Automation

Apple Support

Updates and Releases

To Watch

To Listen

Just for Fun

Support

If you are enjoying what you are reading here, please spread the word and recommend it to another Mac Admin!

If you want to support me and this website even further, then consider buying one (or all) of my books. It’s like a subscription fee, but you also get a useful book or two extra!

Weekly News Summary for Admins — 2023-09-22

Release week number one! We got iOS 17 and all the siblings. Next week, the updates will continue with the macOS Sonoma release on Sep 26, which Apple already set the stage for with security updates for iOS 16 and 17, and macOS Monterey and Ventura yesterday, as well as a second RC for macOS Sonoma.


(Sponsor: Mosyle)

Mosyle Logo

The only Apple Unified Platform for Business

Mosyle is the only solution that fully integrates Enhanced MDM, Endpoint Security, Internet Privacy & Security, Single Sign-On, and Application Management on a single Apple-only platform.

Click here to learn why Mosyle is all you need to work with Apple.


As expected, Big Sur did not get a security update. But there is a Safari update for Big Sur and Monterey. Apple’s productivity apps (Keynote, Numbers, Pages, iMovie and some of the Pro apps) also got updates.

The new iPhones 15 and Apple watches are shipping today. I am getting a new Series 9 watch, which will be an upgrade from my previous Series 5, where the battery has trouble lasting through my busier days. I am also considering upgrading from my iPhone 12 mini to the iPhone 15 Pro. My wife has the 14 Pro and I am very envious of the camera. But I want to go to the Apple Store and hold and feel the new phone first, and then decide whether to order or wait whether the mini form factor will see a revival in the next generation of the iPhone SE. (I don’t have much hope.)

Jamf Nation User Conference was this week, and from what I saw from afar, everyone had a great time. You can find a summary of the keynote on the Jamf blog.

If you would rather get the weekly newsletter by email, you can subscribe to the Scripting OS X Weekly Newsletter here!! (Same content, delivered to your Inbox once a week.)

News and Opinion

iOS and iPad OS 17

macOS

Other Platforms

Applications

Community

Security and Privacy

Support and HowTos

Scripting and Automation

Updates and Releases

To Listen

Support

If you are enjoying what you are reading here, please spread the word and recommend it to another Mac Admin!

If you want to support me and this website even further, then consider buying one (or all) of my books. It’s like a subscription fee, but you also get a useful book or two extra!

Weekly News Summary for Admins — 2023-09-15

The Apple event this week, brought the expected iPhone 15 and Apple Watch updates. You can read all about them on Apple’s event page and the usual suspects.


Mosyle Logo

(Sponsor: Mosyle)

The only Apple Unified Platform for Business

Mosyle is the only solution that fully integrates Enhanced MDM, Endpoint Security, Internet Privacy & Security, Single Sign-On, and Application Management on a single Apple-only platform.

Click here to learn why Mosyle is all you need to work with Apple.


More importantly for Mac and iOS Admins: iOS 17, iPadOS 17, and watchOS 10 will be released September 18. macOS Sonoma 14.0 will be released September 26. This is four weeks earlier than the release of macOS Ventura. There were RC releases for all beta platforms this week.

JNUC is next week, safe travel and a fun conference for everyone who is going!

If you would rather get the weekly newsletter by email, you can subscribe to the Scripting OS X Weekly Newsletter here!! (Same content, delivered to your Inbox once a week.)

Focus

News and Opinion

macOS Sonoma and iOS 17

macOS and iOS Updates

Social Media

  • David Nelson: “When reading what’s changed in the latest iPhone we tend to get comparisons to the immediate previous model. Not so helpful if you’re coming from a device that’s two or more years old. The “Compare iPhone Models” page helps. It includes devices all the way back to the iPhone 6 and the original SE. For example, I can see the weight and dimensions of my 12 Pro compared to a 15 Pro Max.

Security and Privacy

Support and HowTos

Scripting and Automation

Updates and Releases

To Listen

Support

If you are enjoying what you are reading here, please spread the word and recommend it to another Mac Admin!

If you want to support me and this website even further, then consider buying one (or all) of my books. It’s like a subscription fee, but you also get a useful book or two extra!

Weekly News Summary for Admins — 2023–09–08

The news summary is back! And right in time for the Apple event next week (Tuesday, Sep 12, 10am PDT)! This is a big heavy summary to catch up on the last eight weeks. If you believe I missed something, let me know and I will add it next week.


(Sponsor: Mosyle)

Mosyle Logo

The only Apple Unified Platform for Business

Mosyle is the only solution that fully integrates Enhanced MDMEndpoint SecurityInternet Privacy & SecuritySingle Sign-On, and Application Management on a single Apple-only platform.

Click here to learn why Mosyle is all you need to work with Apple.


We got macOS 13.5 and iOS 16.6 right at the beginning of the break and a few security updates since then. The macOS Sonoma beta is on the seventh incarnation, iOS 17 and watchOS on the eighth, and, oddly, tvOS is on the ninth.

But aside from the betas and looming system releases, there were many updates for popular MacAdmins tools and software. I myself used the break to write up a few tutorials that have been rattling around in my head for a while.

We have three more big MacAdmin conferences this year: Jamf Nation User Conference (JNUC) in Austin, Texas, USA, Sep 19–21, MacSysAdmin in Göteborg, Sweden, Oct 3–6 and Objective-by-the-Sea 6.0, in Spain, Oct 9–13.

This year, I will be presenting at MacSysAdmin in Göteborg! I have the closing session on Friday, and will be talking about “MacAdmin Tools” so do not leave early. If you are there, too, be sure to say ‘Hi!’ and you will be rewarded with with some Scripting OS X stickers.

Eight weeks of summer hiatus was a long time. There were several scheduling requirements that lead to the long break this year. I plan to organize this differently next year.

If you would rather get the weekly newsletter by email, you can subscribe to the Scripting OS X Weekly Newsletter here!! (Same content, delivered to your Inbox once a week.)

News and Opinion

macOS Sonoma and iOS 17

macOS and iOS Updates

Social Media

  • Aaron on X: “New in macOS Sonoma: You can now use one Mac to revive or restore another Mac that’s in DFU mode right from Finder. Previously you needed to install Apple Configurator to do this but now it’s built in! Accidentally stumbled upon this while playing around with some stuff.”

Security and Privacy

Support and HowTos

Scripting and Automation

Updates and Releases

To Watch

To Listen

Just for Fun

Support

If you are enjoying what you are reading here, please spread the word and recommend it to another Mac Admin!

If you want to support me and this website even further, then consider buying one (or all) of my books. It’s like a subscription fee, but you also get a useful book or two extra!

Build a notarized package with a Swift Package Manager executable

One of the most popular articles on this blog is “Notarize a Command Line Tool with notarytool.” In that post I introduced a workflow for Xcode to build and notarize a custom installer package for a command line tool. This workflow also works with apps and other projects that require a customized installer package building workflow. I use it in many of my own projects.

But Xcode is not the only way to build Swift binaries. Especially for command line tools, you can also use Swift Package Manager. This provides a mostly command line based interface to building and organizing your project, which you might prefer if you want to use an IDE that is not Xcode, or have Swift projects that need to run cross-platform.

I also have an older article on building a command line tool with Swift Package Manager. But then, I did not create an installer package or notarize the resulting binary.

Placing the binary in an installer package file is the best way to distribute a binary as you can control where in the file system the binary is installed. Notarizing the pkg file is necessary when you are distributing a command line tool, since it enables installations without scary dialogs or handling quarantine flags.

Also, some of the behavior of Swift Package Manager (SPM) and Xcode have changed since the previous posts. So, this article will introduce an updated workflow using Swift Package Manager tools and how to sign, package and notarize a command line tool for distribution.

Note on nomenclature: Swift Package Manager projects are called ‘packages.’ On macOS, installer files (with the pkg file extension) are also called ‘packages.’ We will be using SPM to build a notarized installation package (a pkg file) from a Swift package project. This is confusing. There is not much I can do about that other than using ‘installer package’ and ‘Swift package project’ to help distinguish.

Prerequisites

I wrote this article using Xcode 14.3.1 and Swift 5.8.1. It should also work with somewhat older or newer versions of Xcode and Swift, but I have not tested any explicitly.

Since I said earlier that using Swift Package Manager allows us to not use Xcode and maybe even build a cross-platform project, you may be wondering why we need Xcode. While we don’t need Xcode for our project, it is one way of installing all the tools we need, most importantly the swift and notarytool binaries. You get those from Developer Command Line tools, as well. We will also see that we can combine Xcode with the command line Swift Package Manager workflow, which I find a very useful setup.

To submit a binary to Apple’s notarization process you will need a Personal or Enterprise Apple Developer account, and access to the Developer ID Application and Developer ID Installer certificates from that account. A free Apple Developer account does not provide those certificates, but they are necessary for notarization

You can follow the instructions in the Xcode article on how to get the certificates and how to configure notarytool with an application specific password. If you had already done this previously you should be able to re-use all of that here. When you reach the ‘Preparing the Xcode Project’ section in that article, you can stop and continue here. Apple also has some documentation on how to configure notarytool.

The sample code we will be using will only work on macOS as it uses CoreFoundation functions. Installer packages and notarization are features of macOS, too, so this is not really a problem here. You can use this workflow to build macOS specific signed binaries and notarized installation pkg files from a cross-platform Swift package project. This will work as long as you keep in mind that the tools to sign, package and notarize only exist and/or work on macOS.

The sample code

We will build the same simple sample tool as in the last article. The prf command (short for ‘pref’ or ‘preference’) reads a default setting’s effective value using the CFPreferencesCopyAppValue function.

The macOS defaults command will read preferences, but only from the user level, or from a specified file path. This ignores one of the main features of macOS’ preferences system as it will not show if a value is being managed by a different preference level, such as the global domain, a file in /Library/Preferences, or (most importantly for MacAdmins) a configuration profile.

You can learn all about preferences and profiles in my book “Property Lists, Preferences and Profiles for Apple Administrators.”

We will build a really simple command line tool, named prf which shows the effective value of a setting, no matter where the value comes from. You could make this tool far more elaborate, but we will keep it simple, since the code is not the actual topic for this article.

We will also be using the Swift Argument Parser package to parse command line arguments and provide a help message. We could build this simple tool without using Argument Parser, but using an external package module is one of the strengths of using Swift Package Manager.

Create the Swift Package project

With all the preparations done, it is time to create our Swift package. We will do all the work in the shell, so open Terminal or your other favorite terminal app and navigate to the directory where you want to create the project.

> cd ~/Projects

Then create a new directory with the name swift-prf. This will contain all the files from the Swift package project. Change directory into that new directory. All following commands will assume this project directory is the current working directory.

> mkdir swift-prf
> cd swift-prf

Then run the swift tool to setup the template structure for our command line tool or ‘executable.’

> swift package init --type executable 
Creating executable package: swift-prf
Creating Package.swift
Creating .gitignore
Creating Sources/
Creating Sources/main.swift

You can inspect the hierarchy of files that the init tool created in the Finder (open .) or in your preferred editor or IDE.

.gitignore
Package.swift
Sources
    main.swift

`

You can open this package project in Xcode. In older versions of Xcode you had to run a special swift package command to generate the Xcode project, but now, Xcode can open Swift package projects directly. Use xed (the ‘Xcode text editor invocation tool’) to open the current directory in Xcode.

> xed .

There is a pre-filled .gitignore (which will be hidden in Finder and probably your IDE), a Package.swift, and a Sources directory with a single main.swift inside. If you want to use git (or another version control) system, now is the time to initialize with git init.

Build the project with swift build and/or run it with swift run. Not surprisingly, the template prints Hello, world!.

> swift build
Building for debugging...
[3/3] Linking swift-prf
Build complete! (0.92s)
> swift run  
Building for debugging...
Build complete! (0.11s)
Hello, world!

After building, there will also be a .build directory (also hidden in Finder, unless you toggle the visibility of invisible files using shift-command-.) which contains all the interim files. In the debug folder, you can find the swift-prf executable. You can run it directly:

> .build/debug/swift-prf
Hello, world!

You can clean all the generated pieces from the .build directory with swift package clean. This will leave some empty folders behind but remove all the interim and final products. This means the next build is going to take much longer, but this can be helpful after reconfiguring the Package.swift file or when the compiler gets confused.

Sidenote: when you use Xcode to edit your Swift package project, and choose Build or Run from the Xcode interface, then it will build and run in a different location (~/Library/Developer/Xcode/DerivedData/swift-prf-<random-letters>/Build). You need to be aware of this when you alternate between Xcode and the command line.

Configuring the Package

The Package.swift file contains the configuration for a Swift package project. You can see that the executable package template has a single target named swift-prf that builds from the files in Sources.

To change the name of the executable file, change the value of the name: of the .executableTarget to just prf. There is another name: earlier in the file, that sets the name of the entire project, you can leave that being swift-prf. They do not have to match.

Then build the project in the command line and run it directly:

> swift build
Building for debugging...
[3/3] Linking prf
Build complete! (0.51s)
> .build/debug/prf          
Hello, world!

We want to add the Swift Argument Parser package to our project as a dependency, so we can use its functionality in our code. For that, we will have to add a ‘dependency’ to the project and then to the target, as well. Modify the Package.swift file to match this:

// swift-tools-version: 5.8
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
  name: "swift-prf",
  products: [
    .executable(name: "prf", targets: ["prf"]),
  ],
  dependencies: [
    .package(url: "https://github.com/apple/swift-argument-parser", from: "1.2.0"),
  ],
  targets: [
    .executableTarget(
      name: "prf",
      dependencies: [.product(name: "ArgumentParser", package: "swift-argument-parser")],
      path: "Sources")
  ]
)

This means that our project uses the package available at the given URL, and our target is going to use the specific product (or module or framework) named ArgumentParser from that package. Some packages have several products combined out of several targets.

You can find more information on the format of the Package.swift file in this overview, and the full documentation.

The next time you build after this change, it will download the repository, build and link to toward your executable. That might take a while. The next build should be much faster again. Also, a Package.resolved file will appear in the project. This file caches the current versions of the included packages protecting you from unexpected changes when a package repo dependency updates. You can force Swift Package Manager to update the file with swift package update.

Sprinkle some code

Now that we have the Swift package project prepared, we can add the code to actually do something.

First, let’s keep the ‘Hello, world!’ for a moment, but put it in the right struct to use ArgumentParser. Change main.swift to:

import Foundation
import ArgumentParser
@main
struct PRF: ParsableCommand {
  func run() {
    print("Hello, world!")
  }
}

This should build and run fine from the command line with swift build and swift run. However, when you open this now in Xcode, you will see an error: 'main' attribute cannot be used in a module that contains top-level code

This comes from a long-running issue in Swift. In older versions of Swift it appears on the command line, as well. The work-around is easy though. It only seems to appear when the @main designator is the main.swift file. We can rename our main file to PRF.swift.

You may want to close the Xcode project window before you do this because this can confuse Xcode. If you manage to get Xcode into a confused state where the project in Xcode does not match what is on disk any more, quit Xcode and delete the .swiftpm/xcode directory, which is where Xcode keeps its generated files.

> mv Sources/main.swift Sources/PRF.swift

Now the project should build and run the same with the Swift Package Manager tools and in Xcode.

Now we can add the ‘full’ code for our tool. Keep in mind that the goal of this tutorial is not to learn how to write complex swift code for command line tools, but to learn the infrastructure requires to create and distribute them, so this code is intentionally simple and basic.

import Foundation
import ArgumentParser
@main
struct PRF: ParsableCommand {
  static var configuration = CommandConfiguration(
    commandName: "prf",
    abstract: "read effective preference value",
    version: "1.0"
  )
  @Argument(help: "the preference domain, e.g. 'com.apple.dock'")
  var domain: String
  @Argument(help: "the preference key, e.g. 'orientation'")
  var key: String
  func run() {
    let plist = CFPreferencesCopyAppValue(key as CFString, domain as CFString)
    print(plist?.description ?? "<no value>")
  }
}

When you compare that to the code from the last article, there are a few differences. We are using the @main attribute to designate the main entry point for the code (this was added in Swift 5.3) and I have added some help text to the tool and argument declarations.

When you use Swift Argument Parser, you should study the documentation on adding help to [commands](I have added some help text to the tool and argument declarations. ) and flags, arguments and options. (To be honest, you should read the entire documentation, a lot has changed since the last article.)

When you now run the tool:

> swift run  
Building for debugging...
[3/3] Linking prf
Build complete! (0.54s)
Error: Missing expected argument '<domain>'
OVERVIEW: read effective preference value
USAGE: prf <domain> <key>
ARGUMENTS:
  <domain>                the preference domain, e.g. 'com.apple.dock'
  <key>                   the preference key, e.g. 'orientation'
OPTIONS:
  --version               Show the version.
  -h, --help              Show help information.

We get the help text generated by Swift Argument Parser with the extra information we provided in the code.

If you want to provide the arguments to the swift run you have to add the executable name, as well:

> swift run prf com.apple.dock orientation       
Building for debugging...
Build complete! (0.11s)
left

Or you can run the executable directly from the .build/debug directory. (This will not automatically re-build the command like swift run does.

> .build/debug/prf com.apple.dock orientation
left

Since we provided a version in the CommandConfiguration, ArgumentParser automatically generates a --version option:

> .build/debug/prf --version       
1.0

Now that we have a simple but working tool, we can tackle the main part: we will package and notarize the tool for distribution.

Preparing the binary

When you run swift build or swift run it will compile the tool in a form that is optimized for debugging. This is not the form you want to distribute the binary in. Also, we want to compile the release binary as a ‘universal’ binary, which means it will contain the code for both Intel and Apple silicon, no matter which CPU architecture we are building this on.

The command to build a universal release binary is

> swift build --configuration release --arch arm64 --arch x86_64

When that command is finished, you will find the universal binary file in .build/apple/Products/Release/prf. we can check that it contains the Intel (x86_64) and Apple silicon (arm64) with the lipo tool:

> lipo -info .build/apple/Products/Release/prf
Architectures in the fat file: .build/apple/Products/Release/prf are: x86_64 arm64 

For comparison, the debug version of the binary only contains the platform you are currently on:

> lipo -info .build/debug/prf
Non-fat file: .build/debug/prf is architecture: arm64

Apple’s notarization process requires submitted binaries to fulfill a few restrictions. They need a timestamped signature with a valid Developer ID and have the ‘hardened runtime’ enabled.

Xcode will always sign code it generates, but the swift command line tool does not. We will have to sign it ourselves using the codesign tool. You will need the full name of your “Developer ID Application” certificate for this. (Don’t confuse it with the “Developer ID Installer” certificate, which we will need later.)

You can list the available certs with

> security find-identity -p basic -v

and copy the entire name (including the quotes) of your certificate. Then run codesign:

> codesign --sign "Developer ID Application: Your Name (ABCDEFGHJK)" --options runtime  --timestamp .build/apple/Products/Release/prf

You can verify the code signature with

> codesign --display --verbose .build/apple/Products/Release/prf

Build the installation package

Now that we have prepared the binary for distribution, we can wrap it in an package installer file.

To cover all deployment scenarios, we will create a signed ‘product archive.’ You can watch my MacDevOps presentation “The Encyclopedia of Packages” for all the gory details.

First, create a directory that will contain all the files we want put in the pkg. Then we copy the binary there.

> mkdir .build/pkgroot
> cp .build/apple/Products/Release/prf .build/pkgroot/

Then build a component pkg from the pkgroot:

> pkgbuild --root .build/pkgroot --identifier com.scriptingosx.prf --version 1.0 --install-location /usr/local/bin/ prf.pkg

The --identifier uses the common reverse domain notation. This is what the installer system on macOS uses to determine whether an installation is an upgrade, so you really need to pay attention to keep using the same identifier across different versions of the tool. The --version value should change on every update.

The --install-location determines where the contents of the payload (i.e. the contents of the pkgroot directory) get installed to. /usr/local/bin/ is a useful default for macOS, but you can choose other locations here.

Next, we need to wrap the component pkg inside a distribution package.

> productbuild --package prf.pkg --identifier com.scriptingosx.prf --version 1.0 --sign "Developer ID Installer: Your Name (ABCDEFGHJK)" prf-1.0.pkg

It is important that you use the “Developer ID Installer” certificate here. The --identifier and --version are optional with productbuild but this data required for some (admittedly rare) deployment scenarios, and we want to cover them all.

You can inspect the installer pkg file with a package inspection tool such as the amazing Suspicious Package. The package file should as a signed “Product Archive.”

We don’t need the component pkg anymore, and it’s presence might be confusing, so let’s remove it:

> rm prf.pkg

Note: If you want to learn more about building installation packages, check out my book “Packaging for Apple Administrators”

Notarization

We are nearly there, just two more steps.

It is important to notarize pkgs that will be installed by a user, because otherwise they will get a scary warning that Apple can’t verify the pkg for malicious software.

notarytool submits the installer package to Apple’s Notarization process and returns the results. Use the keychain profile name you set up, following the instructions in the previous article or the instructions from the Apple Developer page.

> xcrun notarytool submit prf-1.0.pkg --keychain-profile notary-example.com --wait

This will print a lot of logging, most of which is self-explanatory. The process might stall at the “waiting” step for a while, depending on how busy Apple’s servers are. You should eventually get status: Accepted.

If you got a different status, or if you are curious, you can get more detail about the process, including rejection reasons, with notarytool log. You will need the ‘Submission ID’ from the submission output:

xcrun notarytool log <submission-uuid> --keychain-profile notary-example.com

As the final step, you should ‘staple’ the notarization ticket to the pkg. This means that the (signed) notarization information is attached to the pkg-file, saving a round trip to Apple’s servers to verify the notarization status when a system evaluates the downloaded installer package file.

xcrun stapler staple prf-1.0.pkg
Processing: /Users/armin/Desktop/swift-prf/prf-1.0.pkg
Processing: /Users/armin/Desktop/swift-prf/prf-1.0.pkg
The staple and validate action worked!

And with that, we have a signed and notarized installation pkg file! You can verify this with spctl:

> spctl --assess --verbose -t install prf-1.0.pkg 
prf-1.0.pkg: accepted
source=Notarized Developer ID

Automation

While it is instructive to do this process manually, it is also quite complex and error-prone. If you have been following this blog for any time, you will know that I don’t stop at detailed step-by-step instructions with explanations.

You can find a script to automate all of these steps here. The enclosing repository includes the entire project (all three files) for your reference.

There is a section at the beginning with variables to modify with the information specific to your environment and project, such as your developer ID information and the name of the credential profile for notarytool. Then there are a few variables, such as the product name, and the installation package identifier.

Run the pkgAndNotarize.sh script from the root of the Swift package project directory.

./pkgAndNotarize.sh

The script creates the installer pkg file in the .build directory. The last line of output is the path to the final, signed, notarized and stapled pkg file.

The script mostly follows the process described above, with a few extras. For example, the script determines the version dynamically by running the tool with the --version option. It also uses the modern compression options I described in this post.

If any of the steps in the script fail, you can determine what exactly failed from the output, error message and error code.

(I belief that this could probably be a makefile, but I have no experience with that (yet). I guess I will need to ‘make’ time for this…)

Conclusion

Apple provides developers and MacAdmins with amazing platforms and tools to build all kinds of powerful apps, tools and automations. But then they don’t really document any of the processes or best practices at all. The amount of searching, guesswork, and frustrating trial and error necessary to piece all of this together for a workflow like this one is quite the shocking condemnation of Apple’s documentation.

There are glimmers of hope. The documentation for the notarization process and notarytool are exemplary.

But they only cover one piece of this puzzle. A developer building a tool has to still figure out how to

  • sign all the binaries properly
  • assemble the binaries and resources necessary into an installation package payload
  • how (and when, and when not) to use pre- and postinstall scripts
  • which kind of package installer to build and which tools to use
  • securely manage the Developer ID certificates (this is especially challenging for developer teams)
  • automate this workflow with Xcode or Swift Package Manager or a CI/CD system

MacAdmins often complain about poorly built installer pkgs, and often for good reasons. But to be fair, there are no best practices and little to no documentation for this from Apple. How are developers supposed to know all of this? Most MacAdmins can define what an installer package should do and not do, but wouldn’t be able to explain to a developer how to build such an installer package, let alone integrate that into their build automations. And most developers don’t even know a MacAdmin to ask about this.

Apple requires that developers create signed and notarized archives for software distribution. And I agree wholeheartedly with their motivations and goals here. But when impose requirements for distribution, you have to make the process of creating the installers the correct way easy, or at least well documented, whether you use Xcode or a different tool set, whether you want to distribute a simple self-contained app, a single command line tool, or a complex suite of tools and resources.

Apple has their work cut out to improve this. Official best practices and sample workflows for installer creation and distribution that consider and respect the requirements of MacAdmins for deployment, have been disgracefully lacking for a long time. The more requirements and security Apple piles on to application and tool distribution, the more desperately they need to provide documentation, best practices and guidance.

Until that happens, you have my paltry scripts.

Build a macOS Application to Run a Shell Command with Xcode and SwiftUI – Part 2

A long time ago, I wrote a post on how to build a simple App using SwiftUI that would run a shell command. Amazingly, the tutorial still works!

This is particularly useful for MacAdmins, because we sometimes want to provide a simple UI for some command or script. However, some things have changed since I wrote this tutorial three years. While the code still “works,” there are some things that can be improved. Also, when I wrote the original post, there were some features I didn’t expand upon, because the post was already very long, and, to be honest, back then, I didn’t really know how they worked, myself.

When I re-visited this earlier because of a question in the MacAdmins Slack, I was really surprised that I didn’t need to update code to make it work. That said, there are the matters I didn’t explain last time. Also Swift has changed in some ways. That means it is time for the second part.

You can get the sample code from the first part as a starting point.

Get the output of the command

In the first part, we ran the say command, which doesn’t have a text output. Often times we want to run shell commands to get information from the command’s standard output (or sometimes standard error). To grab the output of commands, we need to prepare the Process object, which means we cannot use the convenience method Process.run() any more.

The say command has an option to change the voice being used. You can list the names of voices available to the say command by running say -v '?' in Terminal. We’d like to get that output and populate a menu with the available voices in our UI.

Note: the output of the say -v '?' command does not show most of the modern voices available in macOS. Most of the voices listed are quite old, funny, and, to be honest, awful. As has been mentioned before, I am using the say command as a convenient example for a shell command. If you want proper speech synthesis in your app, you should not use the say command but the proper speech synthesis API.

We will start out experimenting with code in a macOS Playground in Xcode and later add it to the app from the first part. When you create a new Playground in Xcode (File > New > New Playground), make sure you select ‘macOS’ for the platform above the template picker and then ‘Blank’ as the template.

Start with this code:

let process = Process()
process.launchPath = "/usr/bin/say"
process.arguments = ["-v", "?"]
let outPipe = Pipe()
process.standardOutput = outPipe
process.terminationHandler = { process in
  let outData = outPipe.fileHandleForReading.readDataToEndOfFile()
  let output = String(data: outData, encoding: .utf8) ?? ""
 print(output)
}
try? process.run()
process.waitUntilExit()

You will recognize some of the code from the first part of this tutorial. We create a Process object and set its launchPath and arguments. Note that the Process.run() convenience method, which we used in the first part, takes a URL to define the executable, but the launchPath is a String containing the path to the executable. We also (this is the new part) create a Pipe and set it as the standardOutput of the process.

The process will run the say command asynchronously. Since we want to work with the output , we have to give the process some code to execute when the command is done. We set this terminationHandler to a closure. The code in this closure grabs the data from pipe we connected to standardOutput of the process, converts it to a String and prints it.

At the end of our code, we tell the process object to run(). The waitUntilExit() is necessary here, because that is all this particular playground code does and we want to be sure that the command and the termination handler get a chance to do their work, before the code ends. In an app, where many things may be happening at the same time, you usually will not use waitUntilExit(). We will see that later when we implement our solution in SwiftUI.

Many outcomes

This code grabs the standard out from the command. Unix commands have different results that can be interesting. Some commands print to standard error instead of (or together with) standard out. All unix commands also have an exit code, which should return 0 for successful runs and a non-zero value for failures. Some commands use different non-zero value to give information for different errors.

All of this is available from Process objects. However, you have to set up the pipes and connections and get the data back in the termination handler, and the code gets quite complicated and tedious to set up. We have the seen the Process type has a convenience method to create and run a command without the pipes. I don’t know why it doesn’t have a convenience method when you are interested in all the data, but the good news is, we can create an extension to build our own.

Replace the code in the playground with the following:

import Foundation

extension Process {
  @discardableResult
  static func launch(
    path: String,
    arguments: [String] = [],
    terminationHandler: @escaping (Int, Data, Data) -> Void
  ) throws -> Process {
    let process = Process()
    let outPipe = Pipe()
    let errorPipe = Pipe()
    process.standardOutput = outPipe
    process.standardError = errorPipe
    process.arguments = arguments
    process.launchPath = path
    process.terminationHandler = { process in
      let outData = outPipe.fileHandleForReading.readDataToEndOfFile()
      let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile()
      let exitCode = Int(process.terminationStatus)
      terminationHandler(exitCode, outData, errorData)
    }
    try process.run()
    return process
  }
}

let process = try? Process.launch(
  path: "/usr/bin/say", 
  arguments: ["-v", "?"]
) { exitCode, outData, errData in
  let output = String(data: outData, encoding: .utf8) ?? ""
  print(output)
}
process?.waitUntilExit()

You will recognize the code from our first example in the launch function in the extension. But here, we have added the configuration for a second Pipe for standardError, in the closure we get the Data for the standard out, standard error and the exit code and pass them in to the closure passed in a termination handler.

This method simplifies using the Process type. We just have to pass in the path to the command and the arguments array and give a closure that is called when the command completes.

async/await

But it still uses a closure for the termination handler. This breaks our code into different fragments that are not executed in the order they appear in. Since macOS Monterey 12 and iOS 15, Swift has had a concurrency feature called async/await. With this, your code appears in a more natural order.

When a function has the await marker, the system knows to suspend the code at that point until the function returns a result. While this code is “on hold” other threads or tasks can run, such as the UI handling, so your process or app isn’t blocked. When the function returns, the code continues after the function, so the code that processes the output of the function comes in the logical order, which makes it easier to read and understand.

For some reason, the Process type has not yet been updated to use this new feature. We can however, add this functionality using an extension. Add this method to the extension:

  static func launch(
    path: String,
    arguments: [String] = []
  ) async throws -> (exitCode: Int, standardOutput: Data, standardError: Data) {
    try await withCheckedThrowingContinuation { continuation in
      do {
        try launch(path: path, arguments: arguments) { exitCode, outData, errData in
          continuation.resume(returning: (exitCode, outData, errData))
        }
      } catch let error {
        continuation.resume(throwing: error)
      }
    }
  }

If you want to get more detail how async and await work, I recommend the WWDC session “Meet async/await in Swift” from WWDC 2021.

With this, we can change our code to run the say command to:

guard let (exitCode, outData, errData) = try? await Process.launch(
    path: "/usr/bin/say",
    arguments: ["-v", "?"]
) else { exit(0) }

let output = String(data: outData, encoding: .utf8) ?? ""
print(output)

The code is now in a more sensible order. If you can afford macOS Monterey as a minimum system requirement, you should consider adopting async/await.

Now that we have our output of the command, we have to parse out the names of the voices. This code will turn the output of the say into an Array of names, ignoring the language code and sample text:

func parseVoices(_ output: String) -> [String] {
  output
    .components(separatedBy: "\n")
    .map {
      $0
        .components(separatedBy: "#")
        .first?
        .trimmingCharacters(in: .whitespaces)
        .components(separatedBy: CharacterSet.whitespaces)
        .dropLast()
        .filter { !$0.isEmpty }
        .joined(separator: " ")
      ?? ""
    }
    .filter { !$0.isEmpty }
}

So we can add these lines to get an Array of `voices:

let voices = parseVoices(output)
print(voices)

You can find the stages of code for the playground in this gist.

Updating the app

Now that we have assembled all the pieces working in a playground, we can move on to putting these pieces in our SwiftUI app. The goal is to have a popup menu with all the voices above the field where you can enter text.

We will be using some features that were introduced in macOS Monterey 12.0. If you have built the project a while ago with an older version of macOS, the project may be set to build with older versions of macOS. To verify and, if necessary, update the deployment target for the project, select the blue project icon at the very top of the item sidebar, then select the gray “SayThis” icon under the “Targets” section. Then select “General” in the tab list. The second section is called “Minimum Deployments” and should show macOS 12.0 or later. When you change this from an older version, Xcode may prompt to update some other settings in the project, you can accept those.

First, we want to use the extension to the Process type we created in the playground earlier. Create a new file in the project (File > New > New File…), select “Swift File” from the templates, and name it ‘Process-launch’. Copy and paste the extension code from the playground (with both methods) to that file. You could have all the code in a single file, but it is cleaner and more maintainable to have one file per type or extension. This also allows you to copy that file to other projects to use it there.

Next we need two more state variables to track the state of the popup-menu that we are going to add. In the ContentView file, add these two lines right under the existing @State variables:

  @State var voices: [String] = []
  @State var selectedVoice: String = "Samantha"

The first is where we are going to store all the available voices that we parse from the output of say -v '?'. The second variable will store the current selection.

Next, we will insert the popup menu for the voices into the UI. In the body, insert this below the title and above the HStack:

      Picker(selection: $selectedVoice, label: Text("Voice:")) {
          ForEach(voices, id: \.self) { Text($0) }
      }

First we create a Picker view, which is not really proper terminology for macOS. But since SwiftUI is designed to share objects across all platforms, a ‘Picker’ will render as a popup menu in macOS. We attach the selectedVoice state variable to the selection of the picker. Then, we loop through all the items in the voices state variable to add them to the picker.

At this point, you should see the popup menu in the preview and when you run the project. You cannot select anything from it, though, which makes sense as the voices array is empty.

We need to populate the voices array before the UI appears. SwiftUI offers a special modifier to do this. You can attach a .task modifier to any view and its closure will be run asynchronously once, before the view appears for the first time. Add this code to the end of the VStack block, right below the line that reads .frame(maxWidth: .infinity, maxHeight: .infinity):

    .task {
      guard let (_, outData, _) = try? await Process.launch(
        path: "/usr/bin/say",
        arguments: ["-v", "?"]
      ) else { return }

      let output = String(data: outData, encoding: .utf8) ?? ""
      voices = parseVoices(output)
    }

You also need to copy the parseVoices() function from the test playground and add it to the ContentView struct.

This will populate the popup menu with all the voices. Build and run to test. It will still not actual use the selected voice. We need to update the runCommand() method to:

  func runCommand() {
    let arguments = [message, "-v", selectedVoice]
    let executableURL = URL(fileURLWithPath: "/usr/bin/say")
    self.isRunning = true
    try! Process.run(executableURL, arguments: arguments) { _ in
      self.isRunning = false
    }
  }

We have achieved what we set out to do. You can find the project with all the code for this point here.

Refinements

However, there are some refinements and improvements that we can and should still add to the code.

First, the runCommand() function still uses the old convenience handler with a termination handler. We can change it to use our new await Process.launch():

  func runCommand() async {
    let arguments = [message, "-v", selectedVoice]
    self.isRunning = true
    let _ = try? await Process.launch(path: "/usr/bin/say", arguments: arguments)
    self.isRunning = false
  }

This will now generate an error in the Button because we have changed the runCommand() function to be async. Change the code of the Button to

        Button("Say") {
          Task {
            await runCommand()
          }
        }
        .disabled(isRunning)

By wrapping the await runCommand() function in a Task { } we are telling the system that this code should run in the background and not return immediately. This is necessary to run code blocks with async functions from UI elements.

The say command will use the voice set in the system settings when no -v option is given. We want to provide a way to recreate that behavior in our UI. We can add items to the Picker by adding elements before the ForEach:

      Picker(selection: $selectedVoice, label: Text("Voice:")) {
        Text("System Default").tag("System Default")
        Divider()
        ForEach(voices, id: \.self) { Text($0) }
      }

Also change the default value of the selectedVoice variable to "System Default".

Then we have to change the runCommand() method to only use the -v and voice name arguments when their value is not “System Default”

  func runCommand() async {
    var arguments = [message]
    if selectedVoice != "System Default" {
      arguments.append(contentsOf: ["-v", selectedVoice])
    }
    self.isRunning = true
    let _ = try? await Process.launch(path: "/usr/bin/say", arguments: arguments)
    self.isRunning = false
  }

You can find the code for this here.

Out of the Sandbox

The say command provides a fun example to play around with. You will probably already have plenty of ideas that you want to try out. There is one thing that might trip you up here: sandboxed apps do not get access to all available command line tools. If you want to run such ‘prohibited’ commands, you will need to remove the sandbox configuration from the app.

Better Extension

The extension I put together here will work in most situations, but there are conditions where different parts might not work well. Quinn the Eskimo has published this code which goes further and is probably “even more” thread safe.

Launching commands is an “expensive” task. You should always do some research to see if there is some framework that can give you access to the same functionality natively in Swift. Nevertheless, sometimes it might be necessary, and sometimes it might just be easier to call out to an external command.

This tutorial should help you along the way of using these tools effectively.

Managed Xcode Deployment

Over on the Jamf Blog, I have an article on “Managed Xcode Deployment.” In it I explain what the special challenges for managed deployments of Xcode and how MacAdmins can solve them.

Most of the challenges from Xcode come from the fact that it is a very big application. But there is also the problem that Xcode doesn’t really play by the rules that all other third party applications in the Mac App Store have to abide by, because it does install extra software and requires administrator privileges on first install…

Check out the post for my solutions!

macOS Ventura 13.5 and iOS 16.6

macOS

iOS and iPadOS

Guides

Other Platforms

Applications

Community

Other updates