Prefs CLI Tools for Mac Admins

Recently I have been working on some… well… “stuff” that uses custom configuration profiles. Very custom, and since I am testing things, they need to be updated a lot.

The issue with defaults

When you are working with defaults/preferences/settings/property lists on macOS, you will be familiar with the defaults command line tool. But, as useful as defaults can be, it has some downsides.

One of the great advantages of macOS’ preference system is that settings can be provided on multiple levels or domains. In my book “Property Lists, Preferences and Profiles for Apple Administrators, I have identified 19 different levels where settings for a single application can originate.

You will be most familiar with plist files in /Library/Preferences (system), ~/Library/Preferences (user), and managed configuration profiles (managed). When an app or tool requests a setting, the preferences system will merge all those levels together and present only the most relevant value. When the developer uses the system APIs (correctly), they do not have to worry about all the underlying levels, domains and mechanisms very much, but automatically gain support for things like separated system and user level settings files and support for management through configuration profiles.

The macOS defaults command line tool can work with settings on different levels or domains, but will only show the settings from one at a time. By default it only works with the user domain settings stored in ~/Library/Preferences/. When you have settings in multiple levels or from configuration profiles, you may be able to point defaults directly at the files. Or in the case of managed settings from profiles, you have to use a different tool. Either way, you have to determine which setting might override another and which final value might be visible to the app or process.

A new prefs tool

Years back, I had built a python script, called prefs.py, which would not only show the coalesced set of settings but their origin level. When macOS removed Python 2 in macOS 12.3, this tool obviously broke.

While working with preferences and profiles recently, this feature would have been quite useful to debug and verify preferences. I could have adapted the existing tool to work with MacAdmins Python 3, but felt I would learn something from recreating it in Swift. I had already started down that road just a bit for my sample project in this post.

So, you can find the new Swift-based prefs command line tool on GitHub. You can also download a signed and notarized pkg which will install the binary in /usr/local/bin/.

If its most basic form, you run it with a domain or application identifier. It will then list the merged settings for that preference domain, showing the level where the final value came from.

% prefs com.apple.screensaver
moduleDict [host]: {
    moduleName = "Computer Name";
    path = "/System/Library/Frameworks/ScreenSaver.framework/PlugIns/Computer Name.appex";
    type = 0;
}
PrefsVersion [host]: 100
idleTime [host]: 0
lastDelayTime [host]: 1200
tokenRemovalAction [host]: 0
showClock [host]: 0
CleanExit [host]: 1

I find this useful when researching where services and applications store their settings and also to see if a custom configuration profile is set up and applying correctly. There is a bit of documentation in the repo’s ReadMe and you can get a description of the options with prefs --help.

plist2profile

Another tool that would have been useful to my work, but that was also written in python 2 is Tim Sutton’s mcxToProfile. Back in the day, this tool was very useful when transitioning from Workgroup Manager and mcx based management to the new MDM and configuration profile based methods. If you have a long-lived management service, you will probably find some references to mcxToProfile in the custom profiles.

Even after Workgroup Manager and mcx based settings management was retired, Tim’s tool allowed to create a custom configuration profile from a simple property list file. Configuration Profiles require a lot of metadata around the actual settings keys and values, and mcxToProfile was useful in automating that step.

Some management systems, like Jamf Pro, have this feature built in. Many other management systems, however, do not. (Looking at you Jamf School.) But even then creating a custom profile on your admin Mac or as part of an automation, can be useful.

So, you probably guessed it, I also recreated mcxToProfile in Swift. The new tool is called plist2profile and available in the same repo and pkg. I have focused on the features I need right now, so plist2profile is missing several options compared to mcxToProfile. Let me know if this is useful and I might put some more work into it.

That said, I added a new feature. There are two different formats or layouts that configuration profiles can use to provide custom setting. The ‘traditional’ layout goes back all the way to the mcx data format in Workgroup Manager. This is what mcxToProfile would create as well. There is another, flatter format which has less metadata around it. Bob Gendler has a great post about the differences.

From what I can tell, the end effect is the same between the two approaches. plist2profile uses the ‘flatter’, simpler layout by default, but you can make it create the traditional mcx format by adding the --mcx option.

Using it is simple. You just need to give it an identifier and one or more plist files from which it will build a custom configuration profile:

% plist2profile --identifier example.settings com.example.settings.plist

You can find more instructions in the ReadMe and in the commands help with plist2profile --help

Conclusion

As I had anticipated, I learned a lot putting these tools together. Not just about the preferences system, but some new (and old) Swift strategies that will be useful for the actual problems I am trying to solve.

I also learnt more about the ArgumentParser package to parse command line arguments. This is such a useful and powerful package, but their documentation fails in the common way. It describes what you can do, but not why or how. There might be posts about that coming up.

Most of all, these two tools turned out to be useful to my work right now. Hope they will be useful to you!

Using desktoppr in a managed environment

A few years back, I built and released a small tool to simplify the deployment of macOS desktop pictures (also known as “wallpapers” since macOS Ventura and on other platforms) without actually locking them down or needing privacy (PPPC) exemptions. The tool is desktoppr and is quite popular among Mac admins.

I haven’t updated desktoppr in the last three years, and even that last update was just a re-compile to make the binary universal. It was always meant to do one thing and it has done that well, so there was no need for updates.

Recently however, I have been thinking about the workflows to install and set a custom desktop picture or wallpaper in a managed environment.

Note: my thoughts here are for Macs that are owned and managed by organizations and given out to employees to work with. They do not really apply to shared devices in classrooms, carts, labs or kiosk style deployments or even 1:1 devices in an education setting. Other rules (figurative, literal, and legal) apply in these scenarios and you probably want to set and lock a desktop picture/wallpaper with a configuration profile.

Why do it at all?

This is a fair question. My general recommendation for Mac Admins regarding pre-configuring, or even locking down settings in user space is to only do it when you absolutely need to for compliance and security reasons.

I often get the argument that it makes the lives of Mac Admins and tech support easier when you pre-configure settings that “everyone wants anyway.” However, I believe, from my own experience, that “tech geeks” (and I include myself here) are poor judges for which settings should be a general preset and which are just our geeky personal habits and preferences.

This holds especially true when macOS updates change default settings. There have been examples for this throughout macOS history, such as the “natural scrolling” on trackpads, whether to use dark mode, or more recently, the “click to desktop” behavior in macOS Sonoma.

You may be able to generate the statistics from your support ticketing system that tells you which pre-configuration would cut down a significant number of incidents and then, by all means, apply those. (I’d be very interested in those stats and which settings those are.)

But generally, a more hands-off approach is less intrusive, and also less effort in management and maintenance.

So, why configure the wallpaper?

Given my lecture above, this is a fair question. The desktop picture/wallpaper is special. It gives you a chance for a great first impression.

The desktop picture/wallpaper is the first thing a user sees when they log in to a new Mac. Apple understands this, and creates new, beautiful images for each major release of macOS to emphasize its branding. Likewise, when you are managing Macs, this is a very compelling opportunity to present your organization’s branding. This will also demonstrate to end users that this Mac is different. This will identify this Mac as a managed Mac from your organization.

Of course, many users will go ahead and change the desktop picture/wallpaper immediately, or eventually. And so they should. The ability to configure and setup the device the way they like it will make it “theirs,” i.e. more personal, and also might encourage some to learn a bit more about macOS and the device they are working with. Limiting a power user in how they can configure “their” device and how they work with it will be a cause for much frustration, friction and complaints. This is why desktoppr exists: to set the desktop picture/wallpaper once, but then allow the user to change it later.

But first impressions are important. There are very few ways Apple lets Mac admins customize the first use experience. The desktop picture/wallpaper is fairly simple to manage and can leave a big impression.

Ok, but how?

That said, there a few considerations to get this first impression just right. In general, to set a customized wallpaper you need to:

  1. install the image file(s)
  2. install desktoppr
  3. run desktoppr to set the wallpaper

The challenge is to get the timing right on these steps. In this case the first two steps can (and should) happen right after enrollment. The third step configures user space, so it cannot happen before the user account is created and the user logs in. If you run it too late, the user will see the default macOS wallpaper for a while and then switch to the new one, which is not really ideal to create a good first impression.

In Jamf Pro (the management system I have most experience with) you can put the packages for the image file and desktoppr in the PreStage or enroll them with policies triggered at enrollmentComplete. Since these pkgs usually are not very large, the image files and the desktoppr binary are ready to go when the user finishes setting up their account and gets to the desktop for the first time.

Jamf Pro provides an option to run policies at login, but that also has few seconds delay before it actually runs, giving the user a “flash” of the default macOS wallpaper.

The good news is, that when you run desktoppr with a LaunchAgent, it runs early enough in the login process, that the user does not get “flashed” by Apple’s default wallpaper.

That extended our workflow to:

  1. install the image file(s)
  2. install desktoppr
  3. install LaunchAgent plist
  4. desktoppr runs as LaunchAgent at login to set the wallpaper

The LaunchAgent will run on every login, and reset the wallpaper even if the user changed a different one. This might be desirable for some deployments, but in general this goes against the notion to leave the user in control. There are workarounds to this. You can build a script that sets the wallpaper and creates a flag file somewhere in the user’s home directory. On subsequent runs, the script would check for that flag file and skip re-setting the wallpaper.

Also, since macOS Ventura, the system warns users about background items in the system. We can manage these warnings with a configuration profile. Since the desktoppr binary is signed, this works quite well. But if you insert a custom shell script as the LaunchAgent to perform all this extra logic, you need to sign this script and managing the background item profile gets a lot more messy.

This works, but now our workflow is:

  1. install the image file(s)
  2. install desktoppr
  3. install LaunchAgent plist
  4. install signed custom script
  5. configure and deploy managed Background Item profile
  6. script runs at login and sets the wallpaper, when necessary

If any of the pieces in step 1–5 change, you need to update at least one installation pkg, upload them to your management system and deploy them down to the clients. Neither of these are very complicated, but the number of moving pieces will make this very tedious quickly.

These workflow steps will not vary much from one deployment to another. So I thought it would be nice if I could integrate some of these steps in desktoppr and find some means to make the pieces less volatile.

Manage the arguments

The first moving piece I wanted to fix was the LaunchAgent plist file. This needs to contain the path to the image file as the first argument to the desktoppr binary, which will be different for every deployment. This value might even change over time for a single given deployment as the org wants to updated and “fresh” desktop pictures/wallpapers with new branding.

For a managed environment, we can also provide this information with a configuration profile. This separates the ‘data’ (the path to the image file) from the logic (the other information in the launchd plist file, that controls when it runs).

I added a new verb manage to desktoppr that tells it to get configuration from a preference domain or configuration profile instead of the arguments. Now, we can put desktoppr with the single argument manage in the LaunchAgent plist. This means the LaunchAgent will not have to change when you update image paths or other settings. Instead, the admin updates the configuration profile in the MDM server. The LaunchAgent plist file is still required, but it won’t need frequent updates any more. (Step 3 is now less volatile)

Setting once

The next step is some new logic in desktoppr to only set the wallpaper once. For that, desktoppr now “remembers” when it last set the wallpaper and to which file. When it is called again to set it to the same file, it does nothing, even if the user changed the desktop picture/wallpaper. Some deployments might still want to reset the wallpaper every time the LaunchAgent triggers, so I added a key to the configuration profile to enable or disable this behavior. This flag only takes effect when desktoppr manage is run, other invocations of desktoppr will set the desktop picture/wallpaper regardless.

This removes the requirement for a custom script that determines whether desktoppr should set the wallpaper or not. (Step 4) Since we are now running desktoppr manage directly from the LaunchAgent, and the desktoppr binary is signed, it is easy to create the PPPC profile to designate this as a managed background item. (Step 5 is less volatile)

There is a sample configuration profile in the repo which has the settings payload and the payload for the background item pre-approval.

Fetch image files

The last volatile bit is the image file itself. So far, you have to create an installation pkg for the image file and install that before desktoppr runs.

To avoid this, I taught desktoppr to download an image file from a URL and use that. So now, you can upload a file to a web server (or AWS, or some file sync service that can provide a static URL) and use that as your desktop.

Since images can be a vector for malware, we add the option to verify the downloaded file with an sha256 hash given in the configuration profile.

Since the download can take a few seconds, this re-introduces the “flash” of the default wallpaper. I don’t really see a way to avoid this and if this really upsets you, you will have to fall back to pre-installing an image file. Nevertheless, I found the option to have desktoppr download an image file to be useful and left it in there.

The new workflow

  1. create and deploy a desktoppr configuration profile
  2. install desktoppr with a LaunchAgent plist (these shouldn’t change very often)
  3. desktoppr runs, downloads the image file and sets the wallpaper, when necessary

All the custom configuration is now in the configuration profile. You will only need to update the desktoppr installation pkg when the binary is an updated.

New use cases

This shortened workflow enables some new workflows. First of all, since there is logic in desktoppr to only change when necessary, we don’t really have to wait until the next login any more. (Most users only “log in” after a reboot for a software update.) We can change the LaunchAgent plist to run in the background every few hours or so. That way, new settings in the configuration profile should be picked up by all the clients within a few hours after coming online and receiving the new profile from the MDM.

Introducing desktoppr 0.5beta

I have been playing around with this and testing for a bit now. It has been working well for me, but I know there will some edge cases and workflows out there that I am not anticipating. For this reason, I am releasing the new manage feature as a beta, so you can start testing it and reporting any improvements, issues, or challenges.

I am curious to see what you are going to do with it.

Installomator v10.5

We have released an update for installomator which brings it to v10.5. You can can see the details in its release notes page.

With an astounding 78 new labels and updates to 42 existing labels, this brings Installomator to 733 labels. The project has also reached 100 contributors who have put in at least one pull request!

Many thanks to everyone who contributed, whether through pull requests, or by filing issues or by joining in the discussion in the #installomator channel on the Mac Admins Slack.

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.

RAID and macOS

RAIDs are a strange edge case that are rarely useful outside of servers, but when they are useful, they are very important. RAID is an acronym for ‘Redundant Array of Independent/Inexpensive Disks.’ It is a technology where you combine multiple, physical disks into a single virtual drive for redundancy, speed, or both.

Often the RAID system is handled by a dedicated controller in an external enclosure, but sometimes, you want or need to work with drives directly. macOS has some basic RAID functionality built-in and there are good third party options if you want to go further.

RAID Levels

If you are unfamiliar with RAIDs, we need to get some terminology sorted out first. There are different kinds of RAIDs which are called ‘levels.’

In this post, I am going to focus on level 1 or ‘RAID 1.’ With RAID 1, the data is ‘mirrored’ between two or more drives, so that each drive carries a complete copy of the data. This protects from data loss because of drive failure. Note that RAID 1 does not protect from other common reasons for data loss, such as file system or individual file corruption, accidental or malicious file deletion. A RAID is never a replacement for a good backup strategy. Since the data is mirrored on all the devices, a RAID 0 will only have the capacity of the smallest drive in the set. It is generally recommended that drives in any RAID set should be of the same capacity and type, for best performance and efficiency.

A level 0 RAID (or ‘RAID 0’) is not actually redundant. In a RAID 0 the data is ‘striped’ across two or more drives so that writes and reads happen in parallel, which increases the data bandwidth available. Since the data is spread evenly (striped) across all drives in the RAID 0 set, failure of a single drive will result in complete data loss. The capacity of the stripe raid is the capacity of the smallest drive in the set multiplied by the number of drives in the set. Having drives of the same type and capacity is even more relevant for RAID 0 performance.

There are more RAID levels, such as 0+1, 10 and 5 and dedicated disk controllers will have more options (such as combining multiple drives of different sizes more efficiently), but we will focus RAID 1 (mirror).

Here, there be dragons

Warning: many of the commands shown here to setup and experiment with disk drives and RAIDs may or will lead to loss of the data on the drives involved, so be careful. I strongly recommend disconnecting any drives other than those you are experimenting with from the Mac you are working on.

I would also recommend to experiment with a set of drives that contain no relevant data at all. Two USB sticks will do just fine to explore and test the functionality. Drives do not have to be of the same capacity and type for testing, but I do recommend that for actual use.

Apple RAID

macOS has built-in support for software-based mirror and stripe RAID called “AppleRAID.” This also provides a third option to concatenate drives, but concatenation provides neither redundancy, nor performance, so I do not recommend using it.

You can use the Disk Utility app to setup a RAID. It has a nice assistant that you an access from the File menu called RAID Assistant. It will ask you what kind of RAID you want to setup and allow you to select the drives and create a new RAID volume. This will delete the data on the disk drives and there are few features that are not exposed in the Disk Utility UI, so I will focus on how to do it in the command line.

You can keep Disk Utility open to get a visual representation of what is going on, though the Disk Utility app often has problems keeping up with changes done from the command line. You may have to quit and restart the app to force it to update its status. You want to enable “Show all Devices” from the View menu to see the physical drives as well as the file systems and virtual drives.

First, we need to identify the disks that we want to work with. When you run diskutil list it will list all the disk on the system. Usually disk0 will be the built-in drive, and disk3 will be the (synthesized) APFS container inside (with the System and Data volume). But depending on what Mac you are using, what your configuration is, and what devices you had attached to the Mac before you started this, the numbers may be different.

Again, to be safe, unmount and disconnect any drives or file servers with data that you care about at this point, and the connect the two drives you want to use for experimentation. Their data will be erased!

Run diskutil list again and identify the device identifiers for the drives you will be working with. They should look like this:

/dev/disk4 (external, physical):

For me, the two drives where disk4 and disk5, so I will be using those numbers in my examples, but be sure to replace those with the device numbers on your system, other wise you might be working with the wrong disk or volume.

Promoting a drive to mirror RAID

One of the features you can use from the command line is to ‘promote’ an existing drive to a mirror RAID without data loss. Apple RAID promotion works (as far as I can tell) only with HFS+ formatted Volumes, so let us reformat the first disk (disk4) as such:

> diskutil eraseDisk JHFS+ DiskName disk4 
Started erase on disk4
Unmounting disk
Creating the partition map
Waiting for partitions to activate
Formatting disk4s2 as Mac OS Extended (Journaled) with name DiskName
Initialized /dev/rdisk4s2 as a 59 GB case-insensitive HFS Plus volume with a 8192k journal
Mounting disk
Finished erase on disk4

Using the command line, we will be able to promote this HFS+ drive without have to erase it (again), so copy some (un-important) files to it now.

diskutil has a sub-group of commands dedicated to the RAID functions called appleRAID or ar for short. I am going to use the short form. You can run diskutil ar to get a list of the sub-commands for working with Apple Raid. You can read the diskutil man page for details.

Next we have to enable AppleRAID on the drive. Enabling RAID on single drive seems a bit pointless, but this prepares everything on that drive, so that we can add more drives later.

> diskutil ar enable mirror DiskName
Started RAID operation on disk4s2 (DiskName)
Resizing disk
Unmounting disk
Adding a booter for the disk
Creating a RAID set
Bringing the RAID partition online
Waiting for the new RAID to spin up "8D05B6EB-DCFB-426D-885B-ED8C76DC2484"
Finished RAID operation on disk4s2 (DiskName)

The volume and the files you had copied earlier are still there. But the volume is now listed under “RAID Sets” in Disk Utility. You can see the single drive in the RAID Set in the UI there. You can also get this info in command line with

> diskutil ar list   
AppleRAID sets (1 found)
===============================================================================
Name:                 DiskName
Unique ID:            8D05B6EB-DCFB-426D-885B-ED8C76DC2484
Type:                 Mirror
Status:               Online
Size:                 63.8 GB (63816400896 Bytes)
Rebuild:              manual
Device Node:          disk6
-------------------------------------------------------------------------------
#  DevNode   UUID                                  Status     Size
-------------------------------------------------------------------------------
0  disk4s2   467826B1-BBA2-4671-99CE-5CBB04E06882  Online     63816400896
===============================================================================

To make this a real mirror RAID, we need to add the second drive:

> diskutil ar add member disk5 DiskName                
Started RAID operation on disk6 (DiskName)
Unmounting disk
Repartitioning disk5 so it can be in a RAID set
Unmounting disk
Creating the partition map
Adding disk5s2 to the RAID Set
Finished RAID operation on disk6 (DiskName)

This will add the drive to the RAID. This will delete any data that might be on disk5!

When you look at the RAID in Disk Utility (you might have to restart the app for it to pick up the new status) you will now see both drives, but one of them has the status “Rebuilding” with a percentage. The status of the entire RAID set is now “Degraded.” The RAID system is still in the process of mirroring data to the new member. You can use the volume to read and write data at this time, but it is not redundant yet. If the first drive fails during rebuilding, the data will be gone.

Once the rebuilding is done, the status of the RAID will change to “Online,” which is the “good” status. At this point the data on the RAID will be resilient to the failure or removal of either of the drives.

You can also create the RAID with both drives from the start, but this will erase all the data on both drives (this is what RAID Assistant in Disk Utility does.

Before we can try the other way of creating a mirror RAID set, we need to “break” the mirror we have built so far.

> diskutil ar delete DiskName
Started RAID operation on disk6 (DiskName)
Unmounting volume for RAID set 8D05B6EB-DCFB-426D-885B-ED8C76DC2484
Destroying the RAID set 8D05B6EB-DCFB-426D-885B-ED8C76DC2484
Finished RAID operation on disk6 (DiskName)

If you waited for the rebuilding to be complete, both individual drives will each contain the data of the former mirror RAID. If the rebuilding was not complete yet, only the first drive will contain the data, the second drive will be empty.

Create a new RAID set

Now let’s build a new empty mirror RAID with both drives included from the start:

> diskutil ar create mirror DiskName APFS disk4 disk5
Started RAID operation
Unmounting proposed new member disk4
Unmounting proposed new member disk5
Repartitioning disk4 so it can be in a RAID set
Unmounting disk
Creating the partition map
Using disk4s2 as a data slice
Repartitioning disk5 so it can be in a RAID set
Unmounting disk
Creating the partition map
Using disk5s2 as a data slice
Creating a RAID set
Bringing the RAID partitions online
Waiting for the new RAID to spin up "FE3E7A3F-E4BF-4AEE-BC3C-094A9BFB3251"
Mounting disk
Finished RAID operation

Note that we can build an APFS volume on the RAID set with the command line tool. RAID Assistant in Disk Utility will build a HFS+ volume. In this new empty RAID set both drives will be online immediately.

When you replace the mirror with stripe in this command you will get a striped RAID 0 volume. (performance instead of redundancy)

Drive failure

We can simulate a drive failure by unplugging one of the members. Sadly, macOS does not seem to have a notification for this event. Once you have removed the drive you can run disktutil ar list and see the status is “Degraded” and the which member is “Missing/Damaged.” You can keep using the volume in degraded mode.

Once you plug the drive back in it will appear as ‘failed.’ You can start the process of repairing or rebuilding the mirror with

> diskutil ar repairMirror FE3E7A3F-E4BF-4AEE-BC3C-094A9BFB3251 disk5  
Started RAID operation
Unmounting disk
Repartitioning disk5 so it can be in a RAID set
Unmounting disk
Creating the partition map
Adding disk5s2 to the RAID Set
Finished RAID operation

Note: Syncing data between mirror partitions can take a very long time.
Note: The mirror should now be repairing itself.  You can check its status using 'diskutil appleRAID list'.

The UUID is the UniqueID of the RAID set you see with diskutil ar list. The warning you get at the end is fair. The rebuilding process will take a while. How it takes depends on how full the volume is and how fast the new member drive is.

Downsides of AppleRAID

There are quite a few downsides to the built-in AppleRAID functionality. There is no notification or warning when one of the drives in a mirror goes offline and the RAID is running in degraded state. The RAID will also not automatically rebuild when a missing drive re-appears. (There is an AutoRebuild option mention in man page, but whenever I tried to enable that, the entire disk management stack froze in a way that required a reboot.)

AppleRAID can be useful to quickly stripe some random disks for performance. But generally, if the data was of so much concern that I am considering RAID 1, I would not rely on AppleRAID.

SoftRAID

There is a wonderful third party tool for managing software based RAIDs on macOS called SoftRAID (14-day free trial, then a tiered license). And, much to my delight, it also comes with a command line tool. Creating a RAID is not something you do regularly, so I went ahead and did this in the GUI app. Once that was setup, I used the command line tool to get the RAID’s status:

> softraidtool volume DiskName info

Info for "DiskName":

Mountpoint: /Volumes/DiskName
BSD disk: disk4
Total Bytes: 59.6 GB (64,016,478,208)
Free Bytes: 59.6 GB (64,016,478,208)
Volume format: unknown
Volume is DiskName
RAID level: RAID 1
DiskName ID: 09DF05C72BFFAD20
Optimized for: Workstation
Created: Jul 17, 2023 at 3:33:39 PM
Last Validated: never
Volume state: normal, 
Volume Safeguard: enabled
Total I/Os: 5,610
Total I/O Errors: 0
Total number secondary disks (including offline ones): 1

Disks used for this volume:
bsd disk:    SoftRAID ID:         Location and Size:
disk7     09DF053ECE82F980     (USB3 bus 0, id 4 - 59.8 GB)  secondary disk, 
disk6     09DF053D23386500     (USB3 bus 0, id 5 - 59.8 GB)  primary disk, 

The SoftRAID software also comes with a menubar app that shows the status of the RAID.

When you unplug one of the drives, the ‘Volume state’ changes to ‘missing disk.’ When you plug the missing drive back in, SoftRAID will automatically detect it and rebuild the RAID, when necessary. Rebuilding went so quickly that I had a hard time capturing the state from the command line. The more changes you apply to the degraded RAID the longer the rebuild takes.

> softraidtool volume SoftRAID info
SoftRAIDTool status: waiting for disk5 to finish (00:00:01)

Info for "SoftRAID":

Mountpoint: /Volumes/SoftRAID
BSD disk: disk5
Total Bytes: 59.6 GB (64,016,478,208)
Free Bytes: 59.6 GB (64,016,478,208)
Volume format: unknown
Volume is SoftRAID
RAID level: RAID 1
SoftRAID ID: 09DF06D95024CBE0
Optimized for: Workstation
Created: Jul 17, 2023 at 3:53:16 PM
Last Validated: never
Volume state: rebuiding, out of sync, 
Volume Safeguard: enabled
Volume progress: 15%
Current offset: 7,343,685,632
Time remaining: 00:07:24
Total I/Os: 22,752
Total I/O Errors: 0
Total number secondary disks (including offline ones): 1

Disks used for this volume:
bsd disk:    SoftRAID ID:         Location and Size:
disk4     09DF053ECE82F980     (USB3 bus 0, id 4 - 59.8 GB)  primary disk, 
disk7     09DF053D23386500     (USB3 bus 0, id 5 - 59.8 GB)  secondary disk, rebuiding, out of sync, 

You can parse this output using awk to get just the volume state. This is useful for reporting the state to Jamf Pro with an extension attribute:

#!/bin/sh

# reports the SoftRAID status

softraidtool="/usr/local/bin/softraidtool"

if [ ! -x "$softraidtool" ]; then
    echo "<result>SoftRAID not installed</result>"
fi

volumestate=$(softraidtool volume SoftRAID info | awk -F ': ' '/Volume state/ {print $2}')

echo "<result>$volumestate</result>"

Keep in mind that Jamf Inventory Updates (aka as recon) may run very infrequently (recommended default is once per day, and it shouldn’t run much more often than that to avoid database bloat), so the data in your Jamf Pro may be hours or sometimes longer out of date. If you want to react to changes in the RAID status more quickly, you should rely on other tools than Jamf Pro.

Conclusion

The best solution for RAIDs will always be a dedicated hardware controller. But there also good reasons (cost) to just put together a “bunch of disks” into a RAID. The built-in AppleRAID functionality works, but has limitations, especially for mirror RAIDs. SoftRAID is a great tool to overcome these limitations. For Mac admins, both can be managed and monitored with command line tools, which allows automation and integration with your management system.

Installomator v10.3 and v11.0beta1

We have released a new minor version of Installomator. Version 10.3 contains several new labels and some very important fixes to existing labels. You can see the details in the v10.3 release notes.

We have also released a first beta for the v11.0 release.

Having a beta parallel to a new minor release is a new approach for us. The way Installomator is built, changes and additions to labels do not (well, should not) affect the hundreds of other labels. This allows us to add and update labels quite easily. (We have added and updated 205 labels since v9.2, and 37 labels since 10.2) The minor updates focus on adding new labels and updating the existing labels, mostly because the names or download URLs change on the vendor side. Since not everyone is comfortable with the intricacies of git and GitHub, frequent minor releases are important to keep Installomator working for everyone that uses it.

However, we want to update the script and functionality in the script, as well. But since any change to behavior of the main script might affect all 500+ applications, we have to tread very carefully here. Last year, we had an extended beta period for v10, which was necessary to identify some problems with the changes. However, we didn’t release new minor updates during that beta phase which means that many labels in the v9.2 release broke over the beta phase.

Because of this, I have studied some new git and GitHub skills. Now, there will be a minor release with new and updated labels, as well as a new beta for v11.0 with some new features, that we are quite excited about. We will keep this up until we deem v11 to be ready for production.

Do not use v11.0beta1 in production! That is what the v10.3 release is for. But please, test the beta in your testing environments and report all issues that you find. This will help us build a better, safe, and stable Installomator v11.0.

As always, many thanks to everyone who is helping to make this project so much better than I could have ever imagined…

Swift UI Tutorial – Part 3

The big conferences expect you to provide a topic and a brief description several months in advance. This can be challenging as you have to pick something that you think will still be interesting in eight months or so, even if there is a WWDC and the larger part of the beta phase of a new macOS and iOS version in between. It also has to be able to hold my attention for about eight months, which is not an easy requirement.

I usually try to meet that challenge by choosing something that is complex enough that it has a large likelihood of remaining relevant. For this year’s JNUC, I chose “Use Swift with the Jamf API.” This would very certainly remain relevant, as both Swift and the Jamf Pro API were certain to remain in existence. I was also not expecting too great changes in either and if there were smaller changes—both Swift and the Jamf API update regularly—I was confident I would be able to handle them.

It also covered another goal I have for my presentation: it was something I didn’t really know at the time and wanted to learn more about. I had dabbled with SwiftUI before, most prominently for my 2021 MacSysAdmin Online presentation “Let’s Swift Again” and I had also tried myself with using the Jamf API from Swift. But the new concurrency features of Swift 5.5 looked like they could make the code much more interesting.

My hunch was correct. But, even though the new concurrency features simplified the code, using Swift to retrieve and work with objects from the Jamf API still remained quite a bit more complex than doing so with curl in a shell script. A JNUC session was supposed to fit into 30 minutes.

So, I hatched a crazy plan. To remain in the 30 minutes, I would just superficially introduce the most important concepts in the sessions, and then publish a tutorial and some sample code that would explain the details. Ideally, the tutorial would publish around the same time as JNUC.

Even early in putting together the session slides and sample code, I realized, there is far too much to explain for a single post. So it would become a series of posts. No problem, I have done that before. I suggested the series to Jamf marketing and they were happy to go with it, so I was committed.

I did manage to get the first part done and published in time for JNUC. Then the work that piled up over being away for the conference struck and it took a while to get part two out. Part three was published yesterday. The project is starting to take form and is diving into some really essential, but also exciting features of Swift.

I am polishing part 4 right now and will send it to the great people who run the Jamf blog for editing and more polishing soon-ish. I am working on the sequels, where we finally, actually will get into the SwiftUI part of the tutorial. I expect there to be seven parts in total, though this project may have more surprises for me yet.

I think this worked out well, even though it certainly turned out to be far more complex and far more work than I had originally anticipated. I have certainly learned a lot along the way, so that goal was achieved! I hope you will enjoy this series as much as I did writing it. (Some people like it.)

Next year, maybe I will go for something less complex… maybe…

Installomator v10.0

After three betas and a lot of testing, we have (finally) released Installomator v10.

If you haven’t been following the betas, then you should really read the release notes, as well as those of the previous betas.

Major new feature is integration with Bart Reardon’s excellent Swift Dialog. Installomator can now send download and install progress to be displayed with Swift Dialog. There are several examples made by Soren Theilgaard and myself in the repo.

This update brings Installomator to more than 500 applications. And there are several more queued up in pull requests that we need to test and merge. The feedback from the community has been amazing. Many thanks to everyone who contributed and helped!

Also, many thanks to Søren, Adam, and Isaac, who are co-managing the project with me, I couldn’t do it without them!

On viewing man pages (Ventura update)

I have written about man pages before. That post references an even older post, which is actually the second oldest post on this blog. Most of the recommendations in the posts still hold true, but there is one change relevant to macOS Ventura and one other thing that is worth adding.

Ventura’s Preview app lost the ability to render postscript or ps files. This breaks the previous, popular shell alias to open a man page in Preview. However, the amazing community in the #scripting channel of the MacAdmins Slack have figured out a replacement. They have asked me to share it.

Add this function to your shell configuration file: (bashzsh

preman() {
    mandoc -T pdf "$(/usr/bin/man -w $@)" | open -fa Preview
}

Then you can run preman <command> in Terminal and the man page will render beautifully in Preview. If you want to override the man command to actually use this function instead of the built-in command, add this alias:

alias man preman

If you then need to revert to the actual man command for a test of something, just add \ before the command: \man <command>

Update: Pico has expanded this into a full blown script which caches the pdfs.

Myself, I had not noticed this change, because I prefer opening the ‘yellow’ man pages in Terminal app. You can do so by entering a command in the Help menu, or by using the x-man-page URL scheme. In these yellow terminal windows, you can scroll and search in the text with command-F. You can also do a secondary click (right/ctrl/two-finger click) on any word and it will offer to open that man page in the context menu.

If the yellow man page windows annoy you, you can change their appearance by modifying Terminal’s “Man Page” profile. I modify it to use my favorite mono spaced font at a larger size. I like the yellow background because it stands out.

In the previous posts, I had a simple function you could add to your shell configuration files, but I have since refined this to also support man page sections (the number you sometimes see after a command, you can learn what they mean in the man page for the man command).

xmanpage() {
  if [[ -z $2 ]]; then
    open x-man-page://"$1"
  else
    open x-man-page://"$1"/"$2"
  fi
}

With this, you can open the desired section in the ‘yellow’ man window with xman 2 stat. This works already with the preman function. The mandoc command knows how to deal with the extra argument, the URL scheme needs a bit of extra work.

If you want to override the normal man command you can, again, use an alias in your shell configuration file:

alias man xmanpage

This way, I can have both functions in the configuration file and choose or change which function (if any) overrides the normal man by just changing the alias.

When you work in Terminal for a while you may accumulate a lot of yellow man page windows. You can use this AppleScript/osascript one-liner to close all Terminal windows which use the ‘Man Page’ profile at once.

osascript -e 'tell application "Terminal" to close (every window where name of current settings of every tab contains "Man Page")'

And while this one-liner is succint, it is still easier to wrap this in a function for your shell configuration file:

closeman() {
  osascript -e 'tell application "Terminal" to close (every window where name of current settings of every tab contains "Man Page")'
}

If you enjoyed this excursion into macOS Terminal and command line tricks and configuration, you might like one of my books: “macOS Terminal and shell” and “Moving to zsh”.