Skip to content

Building

HSMM edited this page Oct 13, 2026 · 1 revision

Building

MacroSIP builds with SwiftPM and the Xcode Command Line Tools alone. There is no Xcode project, and Homebrew is not involved: OpenSSL and Opus are compiled from pinned source tarballs together with PJSIP. The details live in scripts/README-build.md, scripts/build-pjsip.sh, scripts/make-app.sh and ADR 0012.

Requirements

  • macOS 14 or newer on an Apple Silicon Mac. The build host must be arm64: the x86_64 half of the universal build is cross-compiled on the same machine, and build-pjsip.sh stops on an Intel host.
  • Xcode Command Line Tools (xcode-select --install). Full Xcode is not needed; if it is installed, it opens Package.swift directly.
  • Network access to github.com and downloads.xiph.org on the first run (tarballs are cached afterwards).
  • Optional: Rosetta 2 (softwareupdate --install-rosetta --agree-to-license) to run the x86_64 slice on Apple Silicon.

Step 1: scripts/build-pjsip.sh

scripts/build-pjsip.sh            # first run about 5 minutes on an 8-core M-series Mac, then seconds
scripts/build-pjsip.sh --clean    # remove every output except the downloads and rebuild
JOBS=4 scripts/build-pjsip.sh     # limit parallelism
KEEP_BUILD=1 scripts/build-pjsip.sh   # keep the source trees under third_party/build

The script builds, for arm64 (native) and x86_64 (--host=x86_64-apple-darwin), with -mmacosx-version-min=14.0:

Component Version Options
OpenSSL 3.5.9 (LTS) no-shared no-module no-tests, only build_libs and install_dev; darwin64-arm64-cc / darwin64-x86_64-cc
Opus 1.6.1 --disable-shared --enable-static --disable-doc --disable-extra-programs
pjproject 2.17 third_party/pjsip/config_site.h installed before configure; --disable-shared --disable-pjsua2 --disable-video --disable-darwin-ssl --disable-speex-aec, no L16, AMR, SILK, bcg729 (G.729) or Lyra; --with-ssl and --with-opus pointing at third_party/deps/<arch>

Every tarball is verified against a SHA-256 pinned at the top of the script. --disable-darwin-ssl matters: configure would otherwise prefer the deprecated SecureTransport over OpenSSL. The script checks the configure log (OpenSSL and Opus found, the G.722.1, GSM, iLBC and Speex codecs not disabled), checks the architecture and minos of every archive and of pjsua, and fails if pjsua links anything from /opt/homebrew.

config_site.h turns on IPv6, SRTP (SDES, no DTLS), the WebRTC echo canceller, TLS, authorization-header caching, PJSUA_MAX_CALLS 8, and the codecs Opus, G.711, G.722, G.722.1, GSM, iLBC and Speex (G.722.1, GSM, iLBC and Speex stay disabled in the engine until a user enables them; see Contributing for adding a codec). Video is off.

Per-architecture results are merged with lipo. Headers that differ between architectures (pj/compat/m_auto.h, os_auto.h, where configure writes the machine name) become one file with an #if defined(__aarch64__) / #elif defined(__x86_64__) branch. Library names lose the host-triple suffix (libpj-aarch64-apple-darwin27.2.0.a becomes libpj.a), so both slices have the same names and the link flags survive macOS updates.

Output layout under third_party/ (all git-ignored except pjsip/config_site.h):

Path Contents
downloads/ verified tarballs, kept by --clean
deps/<arch>/{include,lib} OpenSSL and Opus for one architecture
deps/universal/lib libssl.a, libcrypto.a, libopus.a, linked by Package.swift
pjsip/arch/<arch>/ PJSIP headers, libraries and pjsua for one architecture
pjsip/include/, pjsip/lib/ merged headers and universal archives
pjsip/bin/pjsua universal pjsua CLI, the second phone in the self-test
pjsip/LINKFLAGS, pjsip/CFLAGS flags a consumer needs; pjsipLibraries in Package.swift mirrors LINKFLAGS
build/, build-logs/<arch>/ source trees (removed unless KEEP_BUILD=1) and full configure/make logs

Caching. Each component writes a stamp with its recipe (versions, checksums, flags, the SHA-256 of config_site.h, the stamps it was built against). A run skips every component whose stamp matches and rebuilds the rest from a freshly extracted tree. After editing config_site.h, rerun the script: PJSIP is rebuilt for both architectures (2 to 5 minutes) while OpenSSL and Opus stay. CI caches third_party/pjsip and third_party/deps with a key over the script and config_site.h, so a hit makes the step a no-op. To upgrade a component, change its version and checksum in the script and rerun.

Step 2: swift build

swift build                         # debug, native architecture
swift build -c release              # release, native architecture
swift build --arch arm64 --arch x86_64 -c release   # universal (what make-app.sh release runs)
.build/release/pjsip-smoke          # prints PJSIP version, codecs and audio devices; exits 0

Package.swift links the archives with absolute -L/-l flags (Context.packageDirectory), adds -lc++ because libwebrtc contains C++, and gives every Swift target that imports CPJSUA the header path through swiftSettings: pjsipSwiftSettings (-Xcc -I). The app target gets the rpath @executable_path/../Frameworks for Sparkle. Universal builds put their products in .build/out/Products/<Config>, not .build/<config>; ask with swift build ... --show-bin-path. SwiftPM does not track the prebuilt archives, so after a PJSIP rebuild run rm -rf .build if a change seems not to take effect.

Step 3: scripts/make-app.sh

scripts/make-app.sh                 # release: universal dist/MacroSIP.app (default)
scripts/make-app.sh debug           # debug: native only, faster; the only build that accepts demo options
ARCHS=arm64 scripts/make-app.sh     # single-architecture release

The script runs swift build --product MacroSIPApp (universal for release, native for debug), checks the slices with lipo, and assembles dist/MacroSIP.app: the binary, Sparkle.framework into Contents/Frameworks, the en.lproj and ru.lproj strings and the InfoPlist.strings into Contents/Resources, packaging/AppIcon.icns, and a generated Info.plist with bundle id com.hsmm.macrosip, the sip/sips/tel URL schemes, the Sparkle keys (SUFeedURL, SUPublicEDKey, daily checks) and the version. CFBundleShortVersionString is the nearest git tag (git describe --tags --abbrev=0; before any tag, Product.version), CFBundleVersion the commit count. It strips every rpath except /usr/lib/swift and @executable_path/../Frameworks and fails if the binary needs another @rpath library.

Signing is ad hoc (-) with the hardened runtime, inside out as Sparkle documents: XPC services, Autoupdate, Updater.app, the framework, then the app with packaging/Entitlements.plist; codesign --verify --deep --strict must pass. The entitlements grant microphone and Contacts access and disable library validation, which an ad hoc signature needs to load the embedded framework (ADR 0013).

A debug app accepts --mock-engine, --demo-state, --data-dir, --snapshot and --appearance (see Testing); a release app ignores them and starts the real app on the real data, Keychain prompt included. All builds accept --log-level 0-6 (or MACROSIP_LOG_LEVEL) for PJSIP verbosity.

Other scripts

Script What it does
scripts/make-dmg.sh [debug|release] runs make-app.sh, stages the app with an Applications symlink and writes dist/MacroSIP-<version>.dmg (HFS+, UDZO, volume "MacroSIP"), prints its SHA-256
swift scripts/make-icon.swift draws the icon (source of truth packaging/AppIcon.svg: a dial pad whose pressed keys trace an M on a green gradient) with CoreGraphics into an iconset and runs iconutil to produce packaging/AppIcon.icns; rerun after changing the artwork
scripts/make-appcast.sh, scripts/publish-appcast.sh, scripts/update-cask.sh release tooling, see Releasing
scripts/test.sh, scripts/selftest-call.sh, scripts/check-strings.sh see Testing

The universal build (ADR 0012)

One app and one DMG serve Apple Silicon and Intel. Release builds of the app are universal, debug builds native. Opus and OpenSSL are linked as static archives from third_party/deps/universal/lib, never as .linkedLibrary or from Homebrew, so the app has no dylib dependency outside the system (Homebrew archives would be arm64-only and built for the host macOS). Test the Intel slice on Apple Silicon with SELFTEST_ARCH=x86_64 scripts/selftest-call.sh under Rosetta 2. Intel support ends when Apple's toolchain or GitHub's Intel runners do.

Common problems

Symptom Cause and fix
Apple Silicon (arm64) host required build-pjsip.sh cross-compiles x86_64 and runs only on arm64. On an Intel Mac take the universal libraries from a CI artifact (the Intel CI job does exactly that).
SHA-256 mismatch for ... a corrupt or re-published download; delete the file under third_party/downloads/ to fetch it again, and review the pinned checksum if upstream changed the tarball
configure did not enable OpenSSL / Opus configure did not see third_party/deps/<arch>; read third_party/build-logs/<arch>/pjsip-configure.log
plugin for module SwiftUIMacros not found @State needs Xcode's macro plugin; use the @Local wrapper from Views/Components/Local.swift
swift test cannot find the Testing macro run scripts/test.sh, which passes the plugin path
the app still runs old PJSIP code after a rebuild SwiftPM does not see the archives change: rm -rf .build
Sparkle.framework not found in make-app.sh run swift package resolve or swift build first
the binary needs @rpath libraries other than Sparkle a new dependency was linked dynamically; ADR 0012 requires static archives
PJSUA_INVALID_ID does not compile in Swift it is a C enum constant: use PJSUA_INVALID_ID.rawValue
an x86_64 pjsua under Rosetta does not answer at once Rosetta start-up takes a few seconds; selftest-call.sh polls for up to about 15 s

See also Architecture, Installation for installing a built DMG, and Troubleshooting for runtime problems.

Clone this wiki locally