Skip to main content

Deploying BusyCal via MDM or Scripted Installation

BusyCal and BusyContacts do not currently support direct MDM-based licensing for App Store subscriptions, due to limitations imposed by Apple. The Mac App Store is designed around individual iCloud-based subscriptions, which unfortunately do not support business-wide deployments under a single MDM license.

For organizations and IT admins who wish to manage multiple installations of BusyCal / BusyContacts across devices, we recommend using our direct license version—available from our website—which allows volume purchases (e.g., a 5-seat license) and supports custom deployment workflows.

Benefits of our Direct Version vs. App Store

For more details, please visit the licensing page as we have given a detailed comparison there.

Essentially, our direct version allows you to:

  • Purchase multi-seat licenses with 18 months of updates included.
  • Deploy BusyCal manually or via scripts.
  • Register and activate using a license key
  • Optional Upgrades

Managed Installation on a Single Machine

You can manually install and activate BusyCal on any Mac using the following steps:

1. Download the latest DMG

Download BusyCal

2. Mount the DMG

Open Terminal and run:

hdiutil attach /path/to/BusyCal.dmg

This command will mount the DMG file as a volume on your system. Take note of the mount point displayed in the command output (e.g., /Volumes/BusyCal).

3. Copy BusyCal to Applications

cp -R /Volumes/BusyCal/BusyCal.app /Applications/

4. Register BusyCal with a License Key

You can register BusyCal by opening a special URL with your license key:

open "busycal://register/BCL3-1111-1111-1111-1111"

Replace BCL3-1111-1111-1111-1111 with your actual license key. This will silently register the installed copy in the background.

Sample Deployment Script for Automation

For automating this across multiple machines, here’s a sample Bash script:

note

This script can be adapted for deployment tools like Jamf, Munki, or custom shell workflows across managed Macs.

#!/bin/bash

# --- Configuration ---
BASE_URL="https://www.busymac.com/download" # Base URL for downloads
DMG_NAME="BusyCal.dmg" # Base name for the downloaded file and in URL
DMG_URL="$BASE_URL/$DMG_NAME" # Construct the full download URL
DMG_PATH="/tmp/$DMG_NAME" # Full path for the downloaded file
APP_NAME="BusyCal.app" # Name of the application bundle inside the DMG
LICENSE_KEY="BCL3-1111-1111-1111-1111" # Replace with actual license key
# --- End Configuration ---

# Variable to store the determined mount point
MOUNT_POINT=""

# Function for cleanup actions
cleanup() {
echo "--- Cleaning up ---"
# Attempt to detach using the determined mount point, if available and mounted
if [ -n "$MOUNT_POINT" ] && hdiutil info | grep -q -F "$MOUNT_POINT"; then
echo "Unmounting volume at $MOUNT_POINT..."
hdiutil detach "$MOUNT_POINT" -force > /dev/null 2>&1
fi
# Remove the downloaded DMG file
if [ -f "$DMG_PATH" ]; then
echo "Removing downloaded DMG at $DMG_PATH..."
rm "$DMG_PATH"
fi
echo "Cleanup finished."
}

trap cleanup EXIT

# --- Main Script ---
echo "Starting BusyCal installation script..."

# Check for xmllint availability
if ! command -v xmllint &> /dev/null; then
echo "Error: xmllint command not found. Install via: brew install libxml2" >&2
exit 1
fi

# Download BusyCal DMG
echo "Downloading $DMG_URL to $DMG_PATH..."
if ! curl --fail -L -o "$DMG_PATH" "$DMG_URL"; then
echo "Error: Failed to download DMG from $DMG_URL." >&2
exit 1
fi
echo "Download complete."

# Mount the DMG and capture the output plist
echo "Mounting DMG: $DMG_PATH..."

MOUNT_INFO_PLIST=$(hdiutil attach "$DMG_PATH" -nobrowse -noverify -plist)
ATTACH_STATUS=$? # Capture exit status of hdiutil attach

if [ $ATTACH_STATUS -ne 0 ]; then
echo "Error: Failed to mount DMG. hdiutil exited with status $ATTACH_STATUS." >&2
exit 1
fi

# Parse the plist output using plutil and xmllint to find the mount point
echo "Parsing mount point information..."

# Convert plist to XML, pipe to xmllint, use XPath to extract the string value
# following the 'mount-point' key. Suppress stderr from plutil.
MOUNT_POINT=$(echo "$MOUNT_INFO_PLIST" | plutil -convert xml1 -o - - 2>/dev/null | \
xmllint --xpath 'string(//key[.="mount-point"]/following-sibling::string[1])' - 2>/dev/null)
PARSE_STATUS=$? # Capture exit status of the command pipeline

if [ $PARSE_STATUS -ne 0 ]; then
echo "Error: Failed to parse mount point using xmllint (exit status $PARSE_STATUS)." >&2
# Attempt to detach using the DMG path as a fallback
hdiutil detach "$DMG_PATH" -force > /dev/null 2>&1
exit 1
fi

# Validate that we found a mount point
if [ -z "$MOUNT_POINT" ]; then
echo "Error: Could not determine mount point from hdiutil output (xmllint parsing failed to find it)." >&2
# Attempt to detach using the DMG path as a fallback
hdiutil detach "$DMG_PATH" -force > /dev/null 2>&1
exit 1
fi
echo "DMG mounted successfully at: $MOUNT_POINT"

# Define the source path for the application
SOURCE_APP_PATH="$MOUNT_POINT/$APP_NAME"

# Check if the application exists at the expected path within the mounted volume
if [ ! -d "$SOURCE_APP_PATH" ]; then
echo "Error: Application '$APP_NAME' not found at '$SOURCE_APP_PATH'." >&2
exit 1
fi
echo "Located application at: $SOURCE_APP_PATH"

# Copy the app to /Applications
echo "Copying $APP_NAME to /Applications/ ..."
if ! cp -R "$SOURCE_APP_PATH" /Applications/; then
echo "Error: Failed to copy '$APP_NAME' to /Applications/." >&2
exit 1
fi
echo "Application copied successfully."

# Register the app using the URL scheme
echo "Attempting to register the application..."

# Add a small delay before trying to register, sometimes needed for the system
# to recognize the newly copied app and its URL scheme handler.
sleep 2

if ! open -g "busycal://register/$LICENSE_KEY"; then
echo "Registration failed."
exit 1
fi
echo "Registration command sent."

# Unmounting and cleanup will be handled by the EXIT trap

echo "BusyCal installation script completed successfully."

# Explicitly exit successfully, triggering the cleanup trap
exit 0