Repository navigation
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.
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.
-
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
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 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.
-
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.
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.
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.
User guide
- Installation
- Accounts
- Calls
- Incoming calls
- Contacts and call log
- Messages
- Directory
- Quick keys
- Audio
- Settings
- Menu bar
- Troubleshooting
For developers
Русский
中文