macOS
=====

.. contents::

What this covers
----------------

ZoneMinder builds, installs and runs on macOS from source. There is no package
and no Homebrew formula yet, so everything here is manual, and there is no
launchd job — you start the daemons yourself or write your own plist.

Two prefixes are in play and it is worth keeping them apart. Homebrew's prefix
is where the dependencies come from — ``/opt/homebrew`` on Apple Silicon,
``/usr/local`` on Intel — and the commands below write it as ``$(brew --prefix)``
so they work on either. ZoneMinder's own install prefix is separate and defaults
to ``/usr/local`` on both, which is what the paths in this guide assume. Pass
``-DCMAKE_INSTALL_PREFIX=`` at configure time to put it somewhere else, and
adjust the paths here to match.

Local USB cameras are not supported. V4L2 is a Linux interface and has no macOS
equivalent in ZoneMinder, so configure reports ``Could NOT find V4L2`` and that
is expected. Network cameras — RTSP, HTTP, ONVIF — work normally, and that is
what ZoneMinder is mostly used with anyway.

Prerequisites
-------------

Xcode command line tools and Homebrew:

::

    xcode-select --install
    /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

Dependencies
------------

To build:

::

    brew install \
      catch2 ffmpeg gsoap jpeg-turbo mosquitto mysql-client \
      nlohmann-json openssl@3 pcre2

``cmake`` and ``pkg-config`` come with the command line tools, and ``curl`` comes
from the SDK, so none of those need installing.

To run, you also need a database, PHP and a web server. macOS still ships Apache
at ``/usr/sbin/httpd``, but it has not shipped PHP since Monterey, so PHP comes
from Homebrew either way:

::

    brew install mariadb php

Perl modules
------------

Some modules ZoneMinder needs are not present in either Perl, and configure only
*warns* about them rather than failing, so it is easy to end up with a clean
build and a system whose Perl daemons do not work.

- ``Sys::Mmap`` backs ``ZoneMinder::Memory::Mapped``. Every Perl daemon reads
  monitor state through it, so without it ``zmdc.pl``, ``zmwatch.pl`` and
  ``zmpkg.pl`` cannot see your monitors at all.
- ``Date::Manip`` is used by ``zmfilter.pl``, ``Event.pm`` and ``Filter.pm``.
- A database driver. ZoneMinder prefers ``DBD::MariaDB``, which drives both
  MariaDB and MySQL.

.. note::
   ``-DZM_NO_MMAP=ON`` is not a way around ``Sys::Mmap``. It switches ZoneMinder
   to SysV shared memory, and macOS caps that at 4 MiB per segment
   (``kern.sysv.shmmax``) across at most 8 segments. A single 1080p RGB frame is
   larger than one segment. Memory mapping is the only workable mode here.

Using a Homebrew Perl
~~~~~~~~~~~~~~~~~~~~~

Recommended. Apple has deprecated the Perl it bundles and it will eventually be
removed, and this keeps ZoneMinder off the system directories entirely.

::

    brew install perl cpanminus
    cpanm --notest Sys::Mmap Date::Manip DBI LWP::UserAgent

A Homebrew Perl has none of the extras Apple bundles, so it needs ``DBI`` and
``LWP::UserAgent`` as well.

``DBD::MariaDB`` needs help. Its configure step asks ``mysql_config`` for link
flags and gets ``-lzstd -lssl -lcrypto`` back, but a Homebrew Perl's ``ldflags``
carry no ``-L$(brew --prefix)/lib``, so the check fails with ``Can't
link/include C library 'zstd', 'ssl', 'crypto', aborting``. Pass the paths in:

::

    export PATH="$(brew --prefix mysql-client)/bin:$PATH"
    cpanm --notest \
      --configure-args="--libs=\"-L$(brew --prefix mysql-client)/lib -L$(brew --prefix)/lib -lmysqlclient -lz -lzstd -lssl -lcrypto -lresolv\" --cflags=\"-I$(brew --prefix mysql-client)/include/mysql\"" \
      DBD::MariaDB

Then configure ZoneMinder against that Perl:

::

    -DPERL_EXECUTABLE=$(brew --prefix)/bin/perl

ZoneMinder's own modules install to that Perl's ``vendorlib``, under
``$(brew --prefix)/lib/perl5/vendor_perl/<version>``, which sits outside the
Cellar and survives Perl upgrades. The CPAN modules above do not: ``cpanm``
writes them to ``sitelib``, which resolves into the versioned Cellar directory
and is erased whenever Homebrew upgrades Perl. Reinstall them afterwards, or set
up ``local::lib`` as ``brew info perl`` describes.

Using the system Perl
~~~~~~~~~~~~~~~~~~~~~

Fewer modules are needed, because Apple bundles ``DBI`` and ``LWP::UserAgent``:

::

    sudo cpan Sys::Mmap Date::Manip

You still need a database driver, and it needs the same link flags as above.
Modules install to ``/Library/Perl/<version>``, which needs ``sudo``.

Build
-----

::

    cmake -S . -B build \
      -DCMAKE_BUILD_TYPE=Release \
      -DBUILD_TEST_SUITE=ON \
      -DCMAKE_PREFIX_PATH="$(brew --prefix mysql-client);$(brew --prefix)" \
      -DOPENSSL_ROOT_DIR="$(brew --prefix openssl@3)" \
      -DCMAKE_C_FLAGS="-I$(brew --prefix mysql-client)/include" \
      -DCMAKE_CXX_FLAGS="-I$(brew --prefix mysql-client)/include"
    cmake --build build -j"$(sysctl -n hw.ncpu)"

The ``mysql-client`` flags are not optional. Homebrew keeps that formula
keg-only, so it is not on the default include path and the bare ``find_library``
call in ``CMakeLists.txt`` will not find its headers. Leave them out and the
build stops at ``'mysql/mysql.h' file not found``.

To check the build, from the ``tests`` directory — the font tests load fixtures
by relative path, so the working directory matters:

::

    cd build/tests && ./tests "~[notCI]"

Install
-------

::

    sudo cmake --install build

With the default prefix this puts binaries in ``/usr/local/bin``, the web files
in ``/usr/local/share/zoneminder/www``, CGI in
``/usr/local/libexec/zoneminder/cgi-bin``, configuration in
``/usr/local/etc/zm`` and the Perl modules in ``/Library/Perl/<version>``.

Create the runtime directories
------------------------------

Nothing creates these for you. On Linux the distribution package does it; here
you do it yourself, once:

::

    sudo mkdir -p /usr/local/var/run/zm \
                  /usr/local/var/log/zm \
                  /usr/local/var/cache/zoneminder/temp \
                  /usr/local/var/lib/zoneminder/events
    sudo chown -R _www:_www /usr/local/var/run/zm \
                            /usr/local/var/log/zm \
                            /usr/local/var/cache/zoneminder \
                            /usr/local/var/lib/zoneminder

``_www`` is the account macOS runs its web server as, and is what configure
detects. Mapped memory files live in ``/usr/local/var/run/zm`` rather than
``/dev/shm``, which macOS does not have. That directory is on disk, not a RAM
filesystem — macOS mounts no tmpfs — so expect more disk traffic than the same
setup on Linux.

Database
--------

::

    brew services start mariadb
    mysql -u root < /usr/local/share/zoneminder/db/zm_create.sql
    mysql -u root -e "CREATE USER IF NOT EXISTS 'zmuser'@localhost IDENTIFIED BY 'zmpass';"
    mysql -u root -e "GRANT LOCK TABLES, ALTER, SELECT, INSERT, UPDATE, DELETE, CREATE, INDEX ON zm.* TO 'zmuser'@localhost;"
    sudo zmupdate.pl --nointeractive

Change the user and password from the defaults, in the grant above and in
``/usr/local/etc/zm/zm.conf``, before putting this anywhere reachable.

Web server
----------

The build generates a starting point for both Apache and nginx at
``build/misc/apache.conf`` and ``build/misc/nginx.conf``, with your configured
paths already substituted. They are guidance, not drop-in configuration —
neither is installed, and both need adapting to how your web server is set up.

Whichever you choose, it needs PHP, CGI enabled for ``nph-zms``, and rewrite
rules for the API. The sample files show all three.

Starting ZoneMinder
-------------------

``zmpkg.pl`` falls back to ``zmdc.pl`` when systemd is absent, so you can start
and stop ZoneMinder by hand:

::

    sudo zmpkg.pl start
    sudo zmpkg.pl status
    sudo zmpkg.pl stop

To start it at boot, the build generates a launchd job at
``build/misc/com.zoneminder.zoneminder.plist`` with your paths and web user
already filled in. Copy it into place and load it:

::

    sudo install -o root -g wheel -m 644 \
      build/misc/com.zoneminder.zoneminder.plist /Library/LaunchDaemons/
    sudo launchctl load -w /Library/LaunchDaemons/com.zoneminder.zoneminder.plist

The job starts ZoneMinder; it does not supervise it. ``zmpkg.pl`` forks
``zmdc.pl`` and returns — the same shape systemd calls ``Type=forking`` — and
from there ``zmdc.pl`` and ``zmwatch.pl`` restart the capture and analysis
daemons themselves. launchd's job is to run that once at boot.

Unloading does not stop ZoneMinder, because launchd has no equivalent of
``ExecStop``. Stop it first:

::

    sudo zmpkg.pl stop
    sudo launchctl unload -w /Library/LaunchDaemons/com.zoneminder.zoneminder.plist

There is also no equivalent of the systemd unit's ``After=`` and ``Requires=``.
launchd only orders jobs it manages itself, and a Homebrew MariaDB is not one of
them, so at boot ZoneMinder may start before the database is listening.
``zmdc.pl`` retries, but ``zmdc.log`` is the place to look if monitors come up
unexpectedly idle.

Known gaps
----------

- No Homebrew formula, so no ``brew services`` integration; the launchd job
  above is loaded by hand.
- Installation is from source only.
- No log rotation. ``misc/logrotate.conf`` is written for logrotate, which macOS
  does not use — it uses ``newsyslog``, and no configuration is provided.
- No local camera support, as described at the top.
- ``libunwind``, ``libVLC`` and ``libVNC`` are not found by default. All three
  are optional; the first only affects backtrace detail in crash logs.
- Configure warns that it cannot find ``arp-scan`` and ``ip``. Neither is fatal:
  ``brew install arp-scan`` covers the first, and ``ip`` is a Linux tool that
  macOS has no equivalent of, so monitor probing is a little less capable.
