SnapClip, Homebrew, and sysctl

In my last post, I said that I made a change the SnapClip script because Homebrew had changed where the OptiPNG utility was installed. That was partly true, but not entirely. Yesterday, I got bit by the part that wasn’t true and had to change the script again. In the process, I learned a couple of things that will be useful to me and might be useful to you.

First, Homebrew’s installation directory has changed, but only on Apple Silicon Macs. From the FAQ:

Homebrew’s pre-built binary packages (known as bottles) of many packages can only be used if you install in the default installation prefix, otherwise they have to be built from source. Building from source takes a long time, is prone to fail, and is not supported. Do yourself a favour and install to the default prefix so that you can use our pre-built binary packages. The default prefix is /usr/local for macOS on Intel, /opt/homebrew for macOS on ARM, and /home/linuxbrew/.linuxbrew for Linux. Pick another prefix at your peril!

I was doing the update to SnapClip on my new M1 MacBook Air (which I intend to write about soon) when I noticed that the optipng binary was in /opt/homebrew/bin instead of /usr/local/bin, where it was on my iMac. I thought this was a universal change, one that would propagate to my iMac after a Homebrew update (or reinstallation), so I changed the opting path. That led to an error the next time I tried to use SnapClip on my iMac. It was then that I learned how the Homebrew people have decided that Apple Silicon Macs are a different beast entirely and need their own directory distinct from that of Intel Macs.

My problem is that my Keyboard Maestro macros are shared among my computers. How was I going to set

optipng = '/opt/homebrew/bin/optipng'

on one computer and

optipng = '/usr/local/bin/optipng'

on another?

There are at least a few ways.

  1. I could access the HOMEBREW_PREFIX environment variable and extend it to get the full path to optipng. This would be the cleanest solution, but unfortunately, HOMEBREW_PREFIX isn’t available when a script is running under Keyboard Maestro. The environment there isn’t the same as it is when you’re working in a shell. And although I could set the environment variable in Keyboard Maestro’s Variables preference pane, I’d have to do that for every computer I have now and every one I get in the future. I’d prefer a solution that doesn’t force me to remember these kinds of setup details.
  2. I could use Keyboard Maestro’s %MacName% token to distinguish between computers. This turns out to be more complicated than it ought to be because although Keyboard Maestro’s variables can be accessed in shell/Perl/Python/Ruby scripts, Keyboard Maestro’s tokens cannot. I’d have to add a step to SnapClip that uses the token to set a variable and then access that variable in the Python script. Seemed too messy.
  3. I could get the computer’s name directly within the Python script by running scutil as a subprocess:

    cname = subprocess.check_output(['scutil', '--get', 'ComputerName']).strip()
    if cname == '2020Air':
      optipng = '/opt/homebrew/bin/optipng'
    else:
      optipng = '/usr/local/bin/optipng'
    

    This is fine, but the name of my computer reflects its architecture only by coincidence. Also, I’d have to adjust this the next time I buy an Apple Silicon Mac.

  4. I could get the CPU by running sysctl as a subprocess:

    optipng = {'Intel(R)': '/usr/local/bin/optipng', 'Apple': '/opt/homebrew/bin/optipng'}
    chipmaker = subprocess.check_output(['sysctl', '-n', 'machdep.cpu.brand_string']).decode().split()[0]
    

    Later in the script, when it was time to run optipng, I’d do it this way:

    subprocess.call([optipng[chipmaker], '-quiet', tfname])
    

    This was the one I liked. No extra macro steps, and the path to the optipng was set by the processor, not by the computer’s name. This logic will keep working on my next M1 Mac.1

I’m not going to pretend I knew about sysctl before yesterday. I found it by Googling around for a solution to my problem and hitting on this old OSXDaily page. On my 2020 MacBook Air, the output of

sysctl -n machdep.cpu.brand_string

is

Apple M1

which is short and sweet, but not very informative if you’re looking for seekret details on Apple Silicon.

On my 2017 iMac, the output is

Intel(R) Core(TM) i7-7700K CPU @ 4.20GHz

which is filled with both technical and legal detail.

If you go to the man page, you’ll find that sysctl should have lots of goodies inside, but I found most of the variables empty. Oh, well.

The upshot of this newfound knowledge is this slightly changed script as the only step in SnapClip:

 1: #!/usr/bin/python3
 2: 
 3: import Pashua
 4: import tempfile
 5: from PIL import Image
 6: import sys, os
 7: import subprocess
 8: import shutil
 9: from datetime import datetime
10: 
11: # Local parameters
12: type = "png"
13: localdir = os.environ['HOME'] + "/Pictures/Screenshots"
14: tf, tfname = tempfile.mkstemp(suffix='.'+type, dir=localdir)
15: bgcolor = (85, 111, 137)
16: border = 16
17: desktop = os.environ['HOME'] + "/Desktop/"
18: fname = desktop + datetime.now().strftime("%Y%m%d-%H%M%S." + type)
19: impbcopy = os.environ['HOME'] + '/Dropbox/bin/impbcopy'
20: optipng = {'Intel(R)': '/usr/local/bin/optipng', 'Apple': '/opt/homebrew/bin/optipng'}
21: chipmaker = subprocess.check_output(['sysctl', '-n', 'machdep.cpu.brand_string']).decode().split()[0]
22: 
23: # Dialog box configuration
24: conf = b'''
25: # Window properties
26: *.title = Snapshot
27: 
28: # Border checkbox properties
29: bd.type = checkbox
30: bd.label = Add background
31: bd.x = 10
32: bd.y = 60
33: 
34: # Save file checkbox properties
35: sf.type = checkbox
36: sf.label = Save file to Desktop
37: sf.x = 10
38: sf.y = 35
39: 
40: # Default button
41: db.type = defaultbutton
42: db.label = Clipboard
43: 
44: # Cancel button
45: cb.type = cancelbutton
46: '''
47: 
48: # Capture a portion of the screen and save it to a temporary file.
49: status = subprocess.call(["screencapture", "-io", "-t", type, tfname])
50: 
51: # Exit if the user canceled the screencapture.
52: if not status == 0:
53:   os.remove(tfname)
54:   sys.exit()
55: 
56: # Open the dialog box and get the input.
57: dialog = Pashua.run(conf)
58: if dialog['cb'] == '1':
59:   os.remove(tfname)
60:   sys.exit()
61: 
62: # Add a desktop background border if asked for.
63: snap = Image.open(tfname)
64: if dialog['bd'] == '1':
65:   # Make a solid-colored background bigger than the screenshot.
66:   snapsize = tuple([ x + 2*border for x in snap.size ])
67:   bg = Image.new('RGBA', snapsize, bgcolor)
68:   bg.alpha_composite(snap, dest=(border, border))
69:   bg.save(tfname)
70: 
71: # Optimize the file.
72: subprocess.call([optipng[chipmaker], '-quiet', tfname])
73: 
74: # Put the image on the clipboard.
75: subprocess.call([impbcopy, tfname])
76: 
77: # Save to Desktop if asked for.
78: if dialog['sf'] == '1':
79:   shutil.copyfile(tfname, fname)
80: 
81: # Delete the temporary file
82: os.remove(tfname)

I suspect I’ll have to use the sysctl trick on several macros, as I’ve never been shy about using utilities from Homebrew.


  1. Assuming Homebrew doesn’t decide to change the location of its binaries in the meantime.