Skip to content

Lab Software Update

Software reaches the lab Macs in three hops. An installer is imported into a Munki repository on the admin Mac; publish.sh copies that repository to the lab server; each lab Mac notices the change on its next hourly check and installs it. The whole point is that updating Fusion for the entire lab is a download, one munkiimport, and one ./publish.sh, instead of a trip to every desk.

This page covers two things: rebuilding the admin Mac if it dies, and the routine for adding or updating a title. If you're here for the routine, skip to Add a New Title.

How the Repository Is Organized

Everything is a folder of plain files. Munki has no database.

Folder Contents Who writes it
pkgs/ The installers themselves, named <name>-<version>.pkg munkiimport
pkgsinfo/ One plist per installer describing it: name, version, how to tell if it's installed munkiimport, then you touch it up
catalogs/ Generated index of everything in pkgsinfo/; production is the one clients read makecatalogs (automatic in publish.sh)
manifests/ Which titles go on which Macs; site_default is the one every lab Mac uses manifestutil
icons/ <name>.png, shown in Managed Software Center munkiimport, optional

A title is one piece of software across all its versions, identified by its name (for example fusion). A new version of a title is a new pkg and a new pkginfo with the same name and a higher version; Munki works out the upgrade from there.

Set Up the Admin Mac

Steps are numbered continuously through this page.

  1. Install the Munki tools from the current release. The one .pkg installs the admin tools and the client together. Restart when asked.

  2. Put the tools on your path.

    echo 'export PATH="$PATH:/usr/local/munki"' >> ~/.zshrc && source ~/.zshrc
    munkiimport --version
    
  3. Recover the repository. If the lab server is alive, it has a complete copy of everything except publish.sh:

    mkdir -p ~/munki_repo
    rsync -rltv pi@engineering.local:/srv/munki_repo/ ~/munki_repo/
    

    If the server is also gone, start empty and rebuild it from the installers:

    mkdir -p ~/munki_repo/{catalogs,manifests,pkgs,pkgsinfo,icons,client_resources}
    
  4. Tell munkiimport where the repository is.

    munkiimport --configure
    
    Prompt Response
    Repo URL file:///Users/<your username>/munki_repo (type the real path; ~ is not expanded here)
    pkginfo extension leave blank
    pkginfo editor nano (or your editor's command-line name)
    Default catalog production
    Repo access plugin FileRepo
  5. Give this Mac passwordless SSH to the server.

    ssh-keygen -t ed25519      # skip if ~/.ssh/id_ed25519 already exists
    ssh-copy-id pi@engineering.local
    ssh pi@engineering.local 'echo ok'
    
  6. Create the publish script. It rebuilds the catalogs, mirrors the repository to the server, and sets permissions nginx can read.

    cat > ~/munki_repo/publish.sh <<'SCRIPT'
    #!/bin/zsh
    set -e
    /usr/local/munki/makecatalogs ~/munki_repo
    rsync -rltv --delete \
      --exclude '.DS_Store' --exclude 'publish.sh' \
      ~/munki_repo/ pi@engineering.local:/srv/munki_repo/
    ssh pi@engineering.local 'chmod -R u=rwX,go=rX /srv/munki_repo'
    SCRIPT
    chmod +x ~/munki_repo/publish.sh
    

    This Is Not GNU rsync

    macOS ships openrsync under the name rsync. It does not understand --chmod, and -a would copy each file's mode from the Mac, where a downloaded installer is sometimes 600. The -rltv flags plus the trailing chmod over SSH are what make the files readable by nginx. --delete is intentional: this Mac is the single source of truth.

  7. If you started empty in step 3, create the lab manifest; otherwise skip.

    manifestutil new-manifest site_default
    manifestutil add-catalog production --manifest site_default
    
  8. Publish once to prove the pipeline.

    ~/munki_repo/publish.sh
    curl -sI http://engineering.local/catalogs/production | head -1
    

    Your terminal should look like this: the rsync file list, then HTTP/1.1 200 OK.

Add a New Title

Every title goes through the same five moves: import, touch up the pkginfo, add it to the manifest, publish, confirm on one lab Mac. Updating an existing title is the same minus the manifest step; see Update an Existing Title.

  1. Download the installer. Munki wants a .pkg. Most vendors offer one, and Autodesk's lab install for Fusion is one. A .dmg that contains a .pkg also imports directly.

  2. Import it. The prompts below are in the order munkiimport asks them. Fields marked optional can be left blank by pressing Enter.

    munkiimport ~/Downloads/<installer>.pkg
    
    Prompt Response Notes
    Item name The bare product name, lowercase, no spaces or version: thonny, fusion, prusaslicer Overwrite the suggestion. munkiimport guesses from the filename and usually includes a version or architecture. This value must stay identical across versions; it is how manifests and icons find the title
    Display name The product's proper name: Thonny, Autodesk Fusion What students see in Managed Software Center
    Description One plain sentence: 3D CAD used in the engineering labs Optional. Shown under the display name
    Version Accept the detected value Only correct it if you know the pkg's version metadata is wrong
    Category CAD, Programming, or blank Optional. Only groups items in Managed Software Center; has no effect on what installs where. Avoid class or teacher names here; that's the manifest's job
    Developer Autodesk, or blank Optional. Cosmetic
    Unattended install True Installs without a student clicking anything. Munki still waits if the app is running or a restart is needed
    Unattended uninstall True if the pkg has a clean uninstaller, False for large suites like Fusion Matters only if you later remove the title
    Catalogs production The only catalog clients read
    Import this item? y
    Upload item to subdirectory blank Keeps pkgs/ flat
    Use this directory? (first import only) y Appears once, on an empty repo
    Attempt to create a product icon? y Optional. Harmless if it finds nothing
    Edit pkginfo before upload? N The touch-ups in step 11 are easier afterward
    Rebuild catalogs? y

    Expected Noise on an Empty Repo

    The very first import prints Could not read existing catalogs: Could not read 'all' catalog. That catalog doesn't exist until the first rebuild. Carry on.

    Worked Example: Autodesk Fusion
    Prompt Response
    Item name fusion
    Display name Autodesk Fusion
    Description 3D CAD used in the engineering labs
    Version 2705.1.15 (accepted)
    Category CAD
    Developer Autodesk
    Unattended install True
    Unattended uninstall False
    Catalogs production
    Upload item to subdirectory blank
    Attempt to create a product icon? y
    Edit pkginfo before upload? N
    Rebuild catalogs? y
  3. Touch up the pkginfo. Three things, every time. Set the two variables first; they're used by all three.

    NAME=fusion                                  # the Item name from step 10
    VER=2705.1.15                                # the Version from step 10
    cd ~/munki_repo
    

    Rename the pkg to <name>-<version>.pkg and record the new location. munkiimport keeps the vendor's filename, which for Autodesk contains spaces that break plain URLs.

    OLD=$(ls pkgs | grep -i "$NAME" | grep -v "^$NAME-$VER.pkg$")
    [ -n "$OLD" ] && mv "pkgs/$OLD" "pkgs/$NAME-$VER.pkg"
    /usr/libexec/PlistBuddy -c "Set :installer_item_location $NAME-$VER.pkg" "pkgsinfo/$NAME-$VER"
    

    Add installs if munkiimport didn't. This tells Munki to decide "installed or not" by looking for the application itself, rather than by the package receipt. Students drag apps to the Trash; receipts survive that, and a receipts-only item would never be reinstalled.

    grep -c '<key>installs</key>' "pkgsinfo/$NAME-$VER"      # 0 means add it
    APP="Autodesk Fusion"                                     # app name without .app
    /usr/libexec/PlistBuddy \
      -c 'Add :installs array' \
      -c 'Add :installs:0 dict' \
      -c 'Add :installs:0:type string application' \
      -c "Add :installs:0:path string /Applications/$APP.app" \
      -c "Add :installs:0:CFBundleShortVersionString string $VER" \
      -c 'Add :installs:0:version_comparison_key string CFBundleShortVersionString' \
      "pkgsinfo/$NAME-$VER"
    

    Check the Version Key First

    CFBundleShortVersionString is the usual key, but confirm on a Mac that already has the app: defaults read "/Applications/$APP.app/Contents/Info.plist" CFBundleShortVersionString should print a version in the same style as $VER. If only CFBundleVersion does, use that key in both places above. Comparing the wrong key gives either a perpetual reinstall or a perpetual "already installed."

    Add blocking_applications so Munki won't replace the app while a student has it open; it waits and retries next cycle.

    grep -c '<key>blocking_applications</key>' "pkgsinfo/$NAME-$VER"   # 0 means add it
    /usr/libexec/PlistBuddy \
      -c 'Add :blocking_applications array' \
      -c "Add :blocking_applications:0 string $APP" \
      "pkgsinfo/$NAME-$VER"
    
  4. Add the title to the lab manifest. New titles only; an updated version of an existing title is already listed.

    manifestutil add-pkg $NAME --manifest site_default
    manifestutil display-manifest site_default
    
  5. Publish, and confirm the server has the whole file.

    ./publish.sh
    curl -sI "http://engineering.local/pkgs/$NAME-$VER.pkg" | grep -i -E 'HTTP|content-length'
    ls -l "pkgs/$NAME-$VER.pkg"
    

    Your terminal should look like this: 200 OK, and the two byte counts match. A 3.8 GB publish over Wi-Fi takes a while; that's the one slow leg.

  6. Confirm on one lab Mac before trusting the hourly cycle. Quit the app first if it's open.

    sudo managedsoftwareupdate -vv --checkonly 2>&1 | grep -i -E "$NAME|install"
    sudo managedsoftwareupdate --installonly
    

    The check should list + <name>-<version>. The install runs quietly; Fusion takes several minutes. Afterward the app's About box, or defaults read "/Applications/$APP.app/Contents/Info.plist" CFBundleShortVersionString, shows the new version.

Done

Every other lab Mac picks it up within the hour, provided the app isn't running there at that moment.

Update an Existing Title

Same as adding, with two differences.

  1. Run steps 9 through 11 with the same Item name as before (fusion stays fusion). The higher version is what makes it an upgrade.

  2. Skip step 12; the manifest already lists the title. Do steps 13 and 14.

  3. Every few updates, prune superseded versions so the server doesn't fill up. repoclean keeps the two newest versions of each title and shows you what it intends to delete before doing it.

    repoclean ~/munki_repo
    ./publish.sh
    

Remove a Title

  1. Take it out of the manifest and publish. Existing installs stay on the Macs; Munki simply stops managing them.

    manifestutil remove-pkg $NAME --manifest site_default
    ./publish.sh
    

    To actively uninstall it from the lab as well, use manifestutil add-pkg $NAME --section managed_uninstalls --manifest site_default instead of removing it. That only works for titles imported with Unattended uninstall True and a working uninstall method.

Troubleshooting

Symptom Cause Fix
curl -I on a pkg returns a body of about 146 bytes nginx 404: the filename doesn't match installer_item_location (spaces, vendor name) Redo the rename in step 11, publish
curl -I returns 403 Forbidden File mode 600 reached the server ./publish.sh again; its chmod line fixes it
Client says No changes to managed software but the app is missing Receipts-only pkginfo; the receipt survived a drag to the Trash Add installs (step 11), publish
Client never upgrades, or reinstalls every hour Wrong version_comparison_key See the warning in step 11
-vv --checkonly shows 404s for manifests/<serial> and manifests/<hostname> Normal: Munki tries those before site_default Nothing. A 404 on site_default or catalogs/production is the one that matters
Install skipped with blocking application running The app is open on that Mac Nothing; Munki retries next cycle
rsync: --chmod=...: invalid argument Someone restored a GNU-style publish.sh Use the script in step 6