Contributing
MacroSIP is GPLv2 or later (PJSIP is GPLv2+, ADR 0007); any contribution is accepted under that license, and any bundled third-party code must be GPL-compatible. Bugs and feature requests go to https://github.com/HSMM/MacroSIP/issues. Before touching code read docs/DEVELOPMENT.md (the engineering notes), docs/ROADMAP.md (what is shipped and what comes next), docs/GLOSSARY.md (use its terms in code and docs), the ADRs in docs/adr/ and the UI spec docs/design/SCREENS.md with docs/design/mockups.html.
Workflow (ADR 0008)
Work is organised by the milestones in docs/ROADMAP.md. Each task starts from a written brief: goal, boundaries, files, done criteria and the facts already known; briefs are self-contained. Every milestone gets an independent review by someone who did not write the code (bugs, races, leaks, Keychain and TLS handling), then verification: build, tests, the loopback self-test and a call on a real SIP account. UI code starts only after the mockups are approved. Settled decisions are not reopened silently: to change one, add a new ADR (docs/adr/README.md: Context, Decision, Consequences; never edited after acceptance except for a "Superseded by" line). Do not add dependencies without an ADR.
Open a pull request against main; CI must pass on both the arm64 and the Intel job (see Testing). Keep docs/design/SCREENS.md, mockups.html and docs/DEVELOPMENT.md in sync with the code you change, in the same pull request.
Coding rules
- Swift 6, strict concurrency.
MacroSIPCorehas no UI imports. PJSIP callbacks arrive on PJSIP worker threads: hop to the main actor before touching app state and never block a PJSIP thread on the main actor. Everypjsua_*call that can fail returnspj_status_t; check it and surface the text viapj_strerror(EngineError.pjsip). Errors never crash the engine. - One product, one build (ADR 0003): bundle id
com.hsmm.macrosip, no build flags for features. @Local, not@State. With the current SDK@Stateis also an attached macro whose plugin ships only with Xcode, so it does not compile under the Command Line Tools. Use the@Localwrapper fromSources/MacroSIPApp/Views/Components/Local.swift; it is a drop-in replacement.- Logging. App code logs through
AppLog.app,.recording,.presence(Platform/AppLog.swift: os_log plus the rotating file~/Library/Logs/MacroSIP/MacroSIP.log), never a bareLogger. Engine.logevents land in theengineandpjsipcategories.--log-levelsets PJSIP verbosity only. - Passwords only in the Keychain; everything else is JSON in Application Support (ADR 0004). New fields in persisted structs decode missing keys as defaults, so older files keep loading.
- Strings. Views never contain user-visible literals. Every string goes through
LinModel/Strings.swiftand exists in bothen.lprojandru.lproj; a string is not done until both languages have it. See "Add a string" below. - Tests with every change. New engine behaviour gets a Swift Testing case (loopback or stub based, no real network) and, when it is user-visible on a call, a self-test step; new models get value tests; new UI states get a demo state so they can be snapshotted. Run
scripts/test.sh,scripts/selftest-call.shandscripts/check-strings.shbefore opening the pull request.
Add a string
- Add a
static varorfunctoLwith a dotted key and an English default, like the existingstatic var tabsContacts: String { loc("tabs.contacts", "Contacts") }. Interpolations become%@for text and%lldfor integers; with two or more arguments use%1$@,%2$@in both files. - Add the key to
Resources/en.lproj/Localizable.stringsandResources/ru.lproj/Localizable.strings. - Counts (contacts, calls, entries) go to
Localizable.stringsdictinstead, withone/otherfor English andone/few/many/otherfor Russian; never the same key in both files. - Run
scripts/check-strings.sh.
Product, codec and protocol names (SIP, SRTP, TLS, STUN, ICE, DTMF, REGISTER), the diagnostics report and log lines are not translated. Check the layout in Russian: dist/MacroSIP.app/Contents/MacOS/MacroSIP --demo-state contacts -AppleLanguages "(ru)".
Add a setting
- Add the field to the right struct in
Sources/MacroSIPCore/Model/AppSettings.swift(GeneralSettings,AudioSettings,RecordingSettings,AutoAnswerSettings,NetworkSettings,CallOptions,CodecOptions,QuickKeysSettings,DirectorySettings) with a default and a tolerantinit(from:), and a value test inSettingsCompatTeststhat an oldsettings.jsonstill decodes. - Apply it: engine-side options go through
AppModel+Engine.swift(applyCallOptions,applyCodecOptions,scheduleNetworkApply; a network field that needs a restart is listed inNetworkSettings.needsRestart(from:)). - Add the control to the pane in
Views/Settings/(each pane is aPaneContainer, 680 pt wide) usingmodel.updateSettings { $0... = ... }. - Strings in
Land both languages; describe the control and its states indocs/design/SCREENS.md, and updatemockups.htmlif the pane layout changes. - A demo state in
Model/DemoState.swiftif the control has a state worth a screenshot.
Add a codec
third_party/pjsip/config_site.h: setPJMEDIA_HAS_<NAME>_CODEC 1(and, if pjproject's configure has a--disable-<name>-codecflag, make surescripts/build-pjsip.shdoes not pass it; the script already fails when configure disables one of the expected codecs or a codec archive is missing). Rerunscripts/build-pjsip.sh: PJSIP is rebuilt for both architectures, OpenSSL and Opus stay cached.Package.swift: add the archive topjsipLibraries, keeping it in sync withthird_party/pjsip/LINKFLAGS; thenrm -rf .buildandswift build.Engine.optionalCodecsinMacroSIPCore(the ids shown after G.711, left at priority 0 until enabled) andCodecCataloginViews/Settings/CodecsPane.swift(all,onByDefault; it appends ids a stored order lacks).- Tests: an id listing test and a loopback call that negotiates the codec (
Engine262Testsshows the pattern), plus the self-test "codecs" step.
No G.729 (bcg729 stays disabled); see ADR 0002 and ADR 0012 for the build.
Add a directory source kind
From docs/DIRECTORY.md:
- Add the case to
DirectorySourceKindand, if it has settings, aCodableconfig struct with a tolerantinit(from:)(every key optional) and a field onDirectorySourcedecoded withtry?, so a damaged config does not drop the settings file. - Implement
DirectoryProvider(anactorwhen it keeps a cache, astructotherwise):searchreturns at mostlimitentries,lookupusesDirectoryNumber.matches,testreturns.connected(entries:),allEntrieseverything for import. ThrowDirectoryError;.signInRequiredputs the source in "Sign in required". For a local list build aDirectoryIndex. - Return it from
DirectoryService.defaultProvider. Secrets go throughDirectoryCredentials, never intosettings.json. - Tests in a
Directory*Tests.swiftfile;StubDirectoryProviderinDirectoryServiceTests.swiftcovers the service side. - UI: the type popup and fields in
Views/Directory/DirectorySourceSheet.swift, strings, SCREENS.md section 13, and adirectory-sheet-<kind>demo state.
Commit messages
Plain English, present tense, what and why. The first line names the area and the change (Engine: retry REGISTER with backoff after a transport error, Settings: Network pane with the RTP port range); the body explains the reason when it is not obvious. No trailers. Commit each finished piece, including the docs that go with it.
See also Architecture, Building, Testing, Releasing.