Use Swift Package Manager and Swift’s ArgumentParser to build a Command Line Tool

Update 2023-08-28: There is a newer post with more, updated information on this topic.

Apple recently published the Swift Package ArgumentParser. As the name implies, this provides a library of commands to parse arguments in a command line tool.

For MacAdmins, Swift can be a useful choice to build tools, especially now that built-in Python is going away in some ‘future macOS.’ While you can (and should) ‘bring your own Python’ for MacAdmin tools, using Swift is interesting alternative.

I have talked about this in detail in my 2018 MacSysAdmin presentation “Swift for Apple Admins” and towards the end of my 2019 MacSysAdmin presentation “Moving to zsh.”

A powerful and ‘official’ library to parse arguments has been a glaring hole when writing Swift command line tools. Now, ArgumentParser can fill this gap. But, to use the ArgumentParser package, you need to use Swift Package Manager (SPM). Previously you could only use SPM from the command line. With Xcode 11 there is some support for Swift Package Manager in the UI. Either way, it is still an extra hurdle for a MacAdmin before they can build their tool.

In this post I will show how to build a simple command line tool using Swift Package Manager and the ArgumentParser package. And we will do it entirely from the command line, but also be able to use Xcode.

Preparation

You will need Xcode 11 or higher, preferably the latest version that works on your version of macOS. You can get Xcode from the Mac App Store or from the Apple Developer Portal.

Once you have downloaded Xcode, launch it to accept the license agreement and finish the additional installations, then we are ready to go.

What will we build?

I will build a simple command line to read user preferences. In macOS the value of a setting can come from a number of different ‘domains.’ A preference can be set by the user, for the computer, or from an MDM server with a configuration profile.

The built-in 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. The defaults tool will not realize or show if a value is being managed by a higher level, such as a configuration profile.

This is the simplified introduction into macOS Preferences and domains. You can learn all about preferences and profiles in my book “Property Lists, Preferences and Profiles for Apple Administrators.”

We will build a simple command line tool which can show the consolidated value of a setting, no matter where the value comes from.

I have built such a tool using python and eventually I will want to re-build that with Swift. But for our demo, we will just have a very simple subset of the functionality, so we will call our simple Swift tool just prf.

Create a Project with Swift Package Manager

Open Terminal. Change Directory for the place where you store your code projects. (For me that is ~/Projects.) Then create a new empty directory for the project and change into it.

> mkdir swift-prf
> cd swift-prf

Then we run the swift command to set us up with an template SPM project.

> swift package init --type executable --name prf

The set the type as executable because we want to build a command line tool. We also set the name to just prf. When you don’t give a name, it will use the name of the enclosing directory.

This will create a bunch of files. Our main code is in Sources/prf/main.swift. Right now, that is a single line print("Hello, world!').

You can build and run the project from Terminal with swift run:

> swift run
[3/3] Linking prf
Hello, world!

Adding the ArgumentParser Package

Before we can write our own code, we need to add the ArgumentParser package to the project. The configuration for this is in the Package.swift file in the root of the project. Open that file with your favored text editor.

In the dependencies array that starts in line 8, add a line for the swift-argument-parser package like this:

    dependencies: [
        // Dependencies declare other packages that this package depends on.
        // .package(url: /* package url */, from: "1.0.0"),
        .package(url: "https://github.com/apple/swift-argument-parser", from: "0.0.5"),
    ],

Version 0.0.5 is the latest version as I write this post.

Then further down in the code there is .target named "prf" which contains another dependencies array. Add the ArgumentParser package there as well:

        .target(
            name: "prf",
            dependencies: [.product(name: "ArgumentParser", package: "swift-argument-parser")]),

Leave the rest of the Package.swift file as it is.

You can now run the tool again. You will see that swift will download and compile the package:

> swift run  
Fetching https://github.com/apple/swift-argument-parser
Cloning https://github.com/apple/swift-argument-parser
Resolving https://github.com/apple/swift-argument-parser at 0.0.5
[32/32] Linking prf
Hello, world!

We will still get the same output, because we haven’t changed the code of our tool yet. But now we can use the ArgumentParser package.

The Code for the Tool

Open Sources/prf/main.swift in you favorite text editor, delete the single print line and add the following:

// prf

// a simple tool to read preferences

import Foundation
import ArgumentParser

struct ReadPreference: ParsableCommand {
  
  @Argument()
  var domain: String
  
  @Argument()
  var key: String
  
  func run() throws {
    let plist = CFPreferencesCopyAppValue(key as CFString, domain as CFString)
    print(plist?.description ?? "<no value>")
  }
}

ReadPreference.main()

Then try to run it again:

> swift run  
[3/3] Linking prf
Error: Missing expected argument '<domain>'
Usage: read-preference <domain> <key>

At first glance this looks like an error, but when you look at it closely, you see that the tool is complaining that expected arguments are missing. Right now, the prf tool expects two positional arguments, the preference domain and the key. You can pass arguments into swift run but when you do, you need to provide the tool name as well;

> swift run prf com.apple.loginwindow lastUserName        
armin

Now, you might want to confirm this and read the same preference with defaults read com.apple.loginwindow and you will not find the lastUserName key and value there. This is because this value is not stored on the user level, but on the computer level, when you run defaults read /Library/Preferences/com.apple.loginwindow you see the value. So we see, that our simple prf tool properly finds and displays the consolidated value!

What the Code does

When we look at the code we see that the main work happens in the run() function of a ReadPreference struct that inherits most of its behavior from ParsableCommand struct from the ArgumentParser library.

In that function we use the CFPreferencesCopyAppValue function which gets a consolidated preference value for a key and identifier pair.

The two properties of this struct are labelled with Swift Property Wrappers: @Argument() this tells the ArgumentParser code that these are expected positional arguments that will be filled into these properties.

Everything else happens automatically.

Because we have two properties with the @Argument() wrapper, the command line tool will expect two arguments, otherwise it will return an error. This is what we saw earlier.

Get Xcode in the Mix

All we’ve needed so far is the swift command in Terminal and a text editor. Conceivably, this may be enough for building a decently complex tool. But, if you prefer to edit in Xcode, you can still do that. You can create an Xcode project file with:

> swift package generate-xcodeproj
generated: ./prf.xcodeproj

And then you can open that and edit, build, and run your tool in Xcode.

In Xcode you can provide arguments to the tool in the Scheme Editor. However, it is quite cumbersome to keep changing those for iterative testing. That’s when falling back to to swift run prf in the command line is very useful.

When you change the configuration in the Packages.swift files to remove or add more dependencies, you should re-generate the Xcode project file with swift package generate-xcodeproj.

Where to go from here

As you can tell from the version number, ArgumentParser is still very ‘young’ and it doesn’t have quite as deep a feature set as Python’s argparse. But there is, even now, much, much more to the ArgumentParser library than we are using in this simple example. There are verbs, options with long and short flags, and help messages and many other things. You can find the details in the package’s documentation.

There is also much more to Swift Package Manager and how to use it. You can learn more about SPM in its documentation.

Nevertheless, this should give you enough to get you started.

I am looking forward to the tools you build!

Weekly News Summary for Admins — 2020-04-17

New iPhone SE release! It looks like a nice upgrade to the iPhone 8 physical design. If you were hoping that ‘SE’ would continue to mean ‘small,’ then you will be disappointed. As John Gruber notes, this is the first time an iPhone model carries the exact same name as a previous model. To distinguish them, I’d suggest calling the new model “iPhone SE/20.”

We also got a new series of beta system releases from Apple.

Please read Babo D’s excellent summary of some of the changes to the softwareupdate management in macOS. This will affect every MacAdmin.

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.)

📈Headlines

📰News and Opinion

⚙️macOS Catalina 10.15 and iOS 13 Updates

🦠Coronavirus and Remote Work

🐦MacAdmins on Twitter

  • William Smith: “MacAdmins, Only six more updates coming for Office 2016 for Mac and then no more! It end-of-lifes in October 2020 with no plans for further support. Put together your plan now to move to Office 2019 ASAP. macadmins.software/calendar/

🐞Bugs and Security

🔨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!

Random Desktop Background Color with desktoppr

File this one under: probably useless, but fun.

I recently updated desktoppr with the feature to control the scaling of a custom desktop picture. Because a scaled desktop picture might not cover the entire screen, macOS also allows you to choose a custom color to fill the remaining area. desktoppr v0.3 can be used to control both of these settings.

So, I thought I could use this to just set a random single color as the desktop background, similar to how I did it for Terminal windows.

macOS does not allow you to set no desktop picture. So I created a PNG file that is only a transparent background. This image is basically an invisible desktop picture and all you see on screen is the fill color. Then, you can set any fill color with desktoppr color.

I have added a randombackgroundcolor script to the examples in the desktoppr repository on GitHub.

It’s a bit silly, but also kind of fun. Enjoy!

Weekly News Summary for Admins — 2020-04-10

It wouldn’t be Apple Update week without a supplemental update a week later… We got i[Pad]OS 13.4.1 and a macOS 10.15.4 Supplemental update this week. Time to re-download your installers!

Also in other news, several MacAdmin conferences are reacting to the Coronavirus pandemic. We have cancellations, but also also new approaches to virtual conferences, which is exciting as it opens to audience to a much larger audience.

Stay safe!

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 Catalina 10.15 and iOS 13 Updates

Conference Updates

Coronavirus and Remote Work

MacAdmins on Twitter

  • Nathan McNulty: “Re: NYC blocking Zoom” (Thread)
  • Mike Lynn: “There’s a lot of stuff in that Platform Security guide. I just learned today we have the 802.11w and WPA3 hardware requirements in there, for instance.”
  • Tim Perfitt: “starting a new project tomorrow codenamed SCONE. smart card over network environment. insert a smart card into a reader on one mac and use it to authenticate or sign things on any mac on the network.” (short thread)
  • Dana Keeler: “Firefox 75 released yesterday, and with it, you can use client auth certificates from your OS certificate store! Set ”security.osclientcerts.autoload“ to true in about:config to try it out! Windows and macOS (Mojave and later)”
  • Cabel: “Did you know you can share any iCloud Drive file publicly? And if you share an iCloud Drive file with someone else using iCloud Drive, when they hit save, you get their changes automatically? No, you don’t know any of this, because the menu item to do this is ‘Add People’”

Bugs and Security

Support and HowTos

Scripting and Automation

Apple Support

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!

Advanced Quoting in Shell Scripts

Quoting strings and variable substitutions is a bit of a dark art in shell scripts. It looks simple and straightforward enough, but there are lots of small devils in the details, that can come out and haunt you.

Basics: why we quote strings

In shell scripts (sh, bash, and zsh) you use the equals character = to assign a string value to a variable:

> name=John
> dirpath=/Library

As long as there are no special characters in the literal string, there is no need to quote the string.

When you use the variable, you prefix a $ symbol:

> echo $name
John
> cd $dirpath
> pwd
/Library

When the literal string contains special characters, you need to either escape the special characters with the backslash \ or quote the entire string with either single quotes ' or double quotes ". Space is proverbial ‘killer character’, especially for file paths. (More details in this post.)

name='John Doe'
dirpath="/Library/Application Support"

The difference between single quotes and double quotes is important. Single quotes escape every special character except the single quote itself. A single quoted string of '#$"\!' will represent exactly those characters.

Double quotes escape most characters, except the double quote " the backtick `, the dollar sign $, the backslash \, and the exclamation mark !. (There are slight differences between the shells on this.)

This allows us to use old-style command substitution with backticks and variable substitution (dollar sign) within double quoted strings:

> echo "Hello, $name"
Hello, John Doe
> echo "The Computer Name is `scutil --get ComputerName`"

Though you should be using the $(…) syntax for command substitution instead of backticks `. The parenthesis syntax is more readable and can be nested.

In general, it is a good rule to always quote literal strings. Whether you should use double quotes or single quotes depends on the use case.

Combining literal strings with special characters

Things can start getting complicated when you want special characters with their special functionality. For example, when you want to refer to the path ~/Library/Application Support, you should put it in quotes, because of the space. But when you put the ~ in the quotes, it will not be substituted to the user’s home directory path.

There are a few ways to solve this problem. You could escape the space with a backslash. You could use the $HOME variable instead (but be sure you are in a context where this is set). But the easiest is to move the special character out of the quotes:

dirpath=~"/Library/Application Support"

Quotes in quotes

Sometimes it is necessary to have a set of quotes within quotes. A common situation for MacAdmins is the following osascript:

osascript -e 'display dialog "Hello, World"'

The osascript command can be used to run Apple commands or scripts. Since AppleScript uses double quotes for literal strings, the entire AppleScript command is passed in single quotes. This keep the command string together and the double quotes in single quotes don’t confuse the shell.

This works fine, until you want to do something like this:

computerName=$(scutil --get ComputerName)
newName=$(osascript -e 'text returned of (display dialog "Enter Computer Name" default answer "$computerName")')

Again, we put the AppleScript command in single quotes, so we can use double quotes inside. But now, the single quotes are also blocking the variable substitution and we get the literal $computerName in the dialog.

There are a few solutions out of this, I will demonstrate three:

First, you could close the single quotes before the variable substitution and re-open them after:

osascript -e 'text returned of (display dialog "Enter Computer Name" default answer "'$computerName'")'

This will in this form as long as $computerName contains no spaces. This is unlikely as the default computer name is something like Armin's MacBook Pro. The shell will consider that space a separator before a new argument, breaking the AppleScript command into meaningless pieces and failing the osascript command. We can avoid that by putting the substitution itself in double quotes:

osascript -e 'text returned of (display dialog "Enter Computer Name" default answer "'"$computerName"'")'

This works and is entirely legal syntax, but not very legible.

Escaping the escape characters

Another solution is to use double quotes for the entire AppleScript command, we can use variable substitution inside. But then we have to deal with the double quotes required for the AppleScript string literal. The good news here is that we can escape those with the backslash:

osascript -e "text returned of (display dialog \"Enter Computer Name\" default answer \"$computerName\")"

This doesn’t win prizes for legibility either, but I consider it an improvement over the previous approach.

Here Docs

The above approaches with work in sh, bash, and zsh. But bash and zsh have another tool available that can work here. The ‘here doc’ syntax can be used to include an entire block of AppleScript code in a bash or zsh script:

#!/bin/bash

computerName=$(scutil --get ComputerName)

newName=$(osascript <<EndOfScript
    text returned of (display dialog "Enter Computer Name" default answer "$computerName")
EndOfScript
)

echo "New name: $newName"

The syntax is a bit weird. The <<EndOfScript says: take all the text until the next appearance of EndOfScript and pipe it into the preceding command, in this case osascript.

The ‘marker’ EndOfScript is entirely arbitrary. Many people choose EOF but I prefer something a little more descriptive. Whatever label you choose the ending marker has to stand alone in its line. This is why the parenthesis ) which closes the command substition $( has to stand alone in the next line.

You can still use variable substitution in a here doc, so the variable $computerName will be substituted before the here doc is piped into osascript.

Weekly News Summary for Admins — 2020-04-03

In this time where days just blur into each other: we made it to April, everyone!

The video conferencing software Zoom is enjoying new popularity, but also (deservedly) new scrutiny. MacAdmins have been complaining about their absolutely insane installer package for years. Now, the security people are on it.

I repeat my offer: I will donate a code for my book “Packaging for Apple Administrators” to any engineer at Zoom who is working on improving the installer. (The book has helped engineers at other companies.)

Stay Safe!

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

MacAdmins on Twitter

  • Bruvik: “Turns out – it is impossible to enter the | symbol on a virtualbox machine on a mac if you have both the guest and host keyboard layout set to norwegian”
  • Tim Perfitt: “triple boot successful both with boot files on efi partition and on the volume. next up, quad boot with 2 versions of windows.” (read the thread!)
  • Rich Trouton: “Need to quickly check to see if your web server’s system clock has drifted? Here’s a one-liner to check using curl to see if a web server’s system clock has the correct time by comparing it against Google’s”
  • Tim Perfitt: “Boot Camp on ARM-based Macs? Possible.”
  • Rosyna Keller: “This is a tweetstorm discussing the new features of altool 4.01 (included with Xcode 11.4), changes to the notarization documentation, and a change to notary service requirements, with the far majority coming directly from user requests We’ll start with altool 4.01.” (Thread)
  • BBEdit: “ICYMI from yesterday’s 13.1 beta change notes, there’s a little taste of home for TextWrangler customers who are making the switch.” (Image)
  • Tom Bridge: “Every. Single. Day. I have to explain how the Screen Recording, Mic and Camera permissions work. Every day. Sometimes a dozen times a day. This sucks.”

Zoom Security and Installer

Note: several MacAdmins have known and complained about this for a long time.

Coronavirus and Remote Work

Bugs and Security

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!

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 — 2020-03-27

Update week! As they had announced last week Apple released the ‘Spring Updates’ this week. As has been standard in the last two years, these updates contain new behaviors and features, many of which are relevant to MacAdmins.

Frederick Abeloos, aka ‘Traveling Tech Guy,’ has published a book with all his great research on FileVault:

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 Catalina 10.15.4 and iOS 13.4 Updates

News and Opinion

Coronavirus and Remote Work

MacAdmins on Twitter

  • William Smith: “Just learned a handy little command to query security information on macOS: sudo /usr/libexec/mdmclient QuerySecurityInfo This is a fantastic overview!”
  • Brian Stucki: “Confirmed: the updated Mac mini will still identify as “Late 2018” in software, etc. Also confirmed: We’re on pace for the biggest Mac mini signup week ever. Feels good to give people access to build servers, remote desktops, etc while working from home.”
  • Tim Perfitt: “reply with a Mac keyboard shortcut you wish everyone knew. I’ll start: Control-a jumps to start of line (a is the start of the alphabet to remember) Control-e jumps to end of line (e for ”end“ to remember)” (Read the replies, there are some real good ones.)

Support and HowTos

Scripting and Automation

Apple Support

Most of these are via Mr Macintosh – thanks!

Updates and Releases

To Watch

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!

Update: desktoppr v0.3

I have posted an update for desktoppr. You can download it from the repository’s releases page.

This update adds two new verbs; scale and color.

Image Scaling

You can use scale to control how the desktop pictures are scaled. I have matched the options to the options in the Desktop preference pane:

  • fill: scale the image up or down so that it fills the entire screen (this is the default behavior)
  • stretch: stretch the image non-proportionally along both axes to fill the screen
  • center: show the image in the center in the original size
  • fit: scale the image up or down so that all of the image is shown
> desktoppr scale center

Background color

The center and fit options for image scaling may result in the desktop picture not fully covering the entire desktop. You can then control the background color with the color verb. The color verb takes a single hex coded color (as in web colors) (no leading # required):

desktoppr color 000000      # black background
desktoppr color FFFFFF      # white background
desktoppr color FF0000      # red background

Future of desktoppr

This tool has always been meant to be a simple ‘one-trick-pony.’ The option to control the image settings has been nearly since I published the first version. I am glad I have finally gotten around to implementing it.

I have learnt a lot about Swift since I first built this tool. When I look at the code now, I want to re-write the entire thing from scratch. I’d also like build better argument parsing. However, it does what it is supposed to do and if I rewrote it now it would probably change the syntax, breaking other people’s workflows

I don’t expect the tool will need updates, other than when it has to adapt to future macOS updates, but we will see.

Weekly News Summary for Admins — 2020-03-20

Happy Equinox, everyone!

The Coronavirus pandemic seems to be affecting nearly everything this week. WWDC will be an online event this year. Apple Stores outside of China are closed, as are most of our work places and schools.

My wife’s and her home office got featured in this post from Leiden University about how their faculty are dealing with working from home.

But, we also got a new MacBook Air (new keyboard), a new iPad Pro (amazing looking, albeit expensive Magic Keyboard), bumped SSD storage on the Mac mini, and a GM release for the iOS 13.4 family (with trackpad support on iPadOS) and macOS Catalina 10.15.4, with a release date for the OS updates next week.

Stay safe!

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.)

Apple Releases

#! On Scripting OS X

📰News and Opinion

🦠Coronavirus and Remote Work

🐦MacAdmins on Twitter

  • Mike Boylan: “Mac Admin Pro Tip: The checkbox to allow an app to record the screen is able to be checked even by a standard user on the Mac. The whole pane does not need to be unlocked. This means your users don’t need to be admins to approve camera, mic, and/or screen sharing for a conf tool.”
  • Tim Perfitt: “the new ipad pro looks like a laptop. i am officially calling BS on the ARM based macs. apple strategy is clear: ios is the future. macs are trucks. Xcode on iOS is coming. The new ipad pro convinces me of that. ARM-based macs rumors are a distraction.”

🐞Bugs and Security

🔨Support and HowTos

🤖Scripting and Automation

🍏Apple Support

♻️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!

Strategies to using desktoppr

A while back I introduced desktoppr. It is a very simple tool; its singular goal is to set the desktop picture (users and admins migrating from Windows call it ‘wallpaper,’ but that is not the proper macOS nomenclature) without requiring PPPC/TCC whitelisting.

The good news is that desktoppr still works fine, nearly one-and-a-half years in! Even though Catalina brought in many changes and restrictions, desktoppr kept doing its job.

Nevertheless, as I have used desktoppr myself in several different deployments, and I have a few different approaches to deployment, depending on the requirements.

Catalina split system volume

One of the new features of Catalina is a read-only system volume, separate from the data volume. This means that the pre-installed desktop pictures are no longer in /Library/Desktop Pictures/ but can now be found in /System/Library/Desktop Pictures. This is on the read-only part of the file system.

On a new “fresh” macOS installation, the /Library/Desktop Pictures does not exist. However, when you create this folder, its contents will appear in the ‘Desktop’ preference pane, merged with the contents of the protected system folder. So, we can continue to use /Library/Desktop Pictures as a place to store and install custom desktop image files.

Note: if you do not want the custom desktop picture to appear in the Desktop preference pane, then you can install the file in a different location. /Users/Shared or /Library/MyOrganization/ are useful locations.

Packaging the custom picture

> mkdir -p BoringDesktop/payload

> cd BoringDesktop
> cp /path/to/BoringBlueDesktop.png payload

> pkgbuild --root payload --install-location "/Library/Desktop Pictures/" --identifier com.example.BoringDesktop --version 1 BoringDesktop.pkg


These command will create a payload folder, copy an image file (my example is BoringBlueDesktop.png) and build an installation pkg using the pkgbuild command.

If you want a more detailed explanation of this process, you can find it in my book: “Packaging for Apple Administrators

Lock the Desktop

In classroom, lab, and kiosk settings, MacAdmins may want to set and lock the desktop picture. In this use case, you do not need desktoppr at all.

Use the above pkg to install the image file and then use your management system to push a configuration profile that sets and locks a Desktop Picture.

Many management systems will have the desktop picture controls hidden in the ‘Restrictions’ payload among many other settings. Please consult the documentation. You can also use this custom profile that only controls the desktop setting.

Preset the desktop, but the let user change it

This is the most common way MacAdmins will want to deploy is to pre-set a custom Desktop Picture but allow the user to change it later. This is what desktoppr was created for.

There are two approaches you can take to do this. Well, to be honest, there are way more, and all of them are valid, as long as they work. I should say: I will show two different approaches.

The modular approach

In this case you use your management system to install and run all the pieces:

  • install the custom desktop picture using the above pkg
  • install desktoppr using the pkg*
  • run a script that sets the desktop

* for 10.14.3 and earlier, desktoppr v0.2 requires the Swift 5 runtime support for command line tools to be installed.

The advantage of this approach is that we already did the first part earlier, and the desktoppr pkg can be downloaded from the git repo,. So, we already have two of the three parts.

For the script, there is a sample script in the repository, as well.

Note that this script has changed slightly since the last post. Originally, the script used launchctl asuser. The behavior of launchctl asuser seems to have changed somewhat in a recent update and I have switched the script to use sudo -u instead.

This approach can be used with Munki, Jamf, outset, and many other management solutions.

All in one package

The downside of the modular approach is that you have to manage three pieces (the image file, the desktoppr binary, and a script) in your management system. This can be especially problematic when you are not the actual administrator of the management system but more active in a ‘consulting role.’

In this case, I have found it easier to build a single package that does all the work. This is easier to hand over to another admin. It is also more flexible and can be used in even more situations. It is a bit more work to assemble, though.

First, you need the ‘ingredients:’

* for 10.14.3 and earlier, desktoppr v0.2 requires the Swift 5 runtime support for command line tools to be installed.

First, create a project folder with a payload folder inside. Then copy the necessary files into the right place:

> mkdir -p BoringDesktop/payload
> cd BoringDesktop


Copy the image file to payload:

> cp /path/to/BoringBlueDesktop.png payload


Create a scripts directory and copy the postinstall script to it:

> mkdir scripts
> cp path/to/desktoppr/examples/postinstall scripts


Expand the zip archive (in Finder) and copy the desktoppr binary into the scripts folder.

> cp path/to/build/usr/local/bin/desktoppr scripts


Your project folder should now look like this:

BoringDesktop Project FolderBoringDesktop Project Folder

You can then build the pkg with:

> pkgbuild --root payload --scripts scripts --install-location "/Library/Desktop Pictures/" --identifier com.example.BoringDesktop --version 2 BoringDesktop-2.pkg


Note the different version number, so the system can recognize this as a different pkg from the one you might have built earlier.

This form of the pkg does not actually install the desktoppr binary on the target system. When the pkg is created, the entire contents of the scripts folder will be archived into the pkg file. When the pkg is being installed, the contents will be expanded into a temporary directory. That means that the postinstall script can see the binary in the same director ‘next to it.’ This happens in lines 22–27 of the postinstall script.

After the installation is complete, the temporary files will be removed, so the postinstall script and the desktoppr binary will be removed automatically. You don’t need to worry about the cleanup.

Conclusion

Which approach works best depends on your specific deployment needs, your management setup and workflows and (not the least) your comfort with building scripts and packages.

Even when you have defined your deployment needs, there are multiple solutions on how to build and deploy a custom desktop picture. As long as they achieve the desired goal, there is no “best” solution.

You can earn more details about building packages in my book: “Packaging for Apple Administrators