Skip to content

Contributing

HSMM edited this page Oct 13, 2026 · 1 revision

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. MacroSIPCore has 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. Every pjsua_* call that can fail returns pj_status_t; check it and surface the text via pj_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 @State is also an attached macro whose plugin ships only with Xcode, so it does not compile under the Command Line Tools. Use the @Local wrapper from Sources/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 bare Logger. Engine .log events land in the engine and pjsip categories. --log-level sets 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 L in Model/Strings.swift and exists in both en.lproj and ru.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.sh and scripts/check-strings.sh before opening the pull request.

Add a string

  1. Add a static var or func to L with a dotted key and an English default, like the existing static var tabsContacts: String { loc("tabs.contacts", "Contacts") }. Interpolations become %@ for text and %lld for integers; with two or more arguments use %1$@, %2$@ in both files.
  2. Add the key to Resources/en.lproj/Localizable.strings and Resources/ru.lproj/Localizable.strings.
  3. Counts (contacts, calls, entries) go to Localizable.stringsdict instead, with one/other for English and one/few/many/other for Russian; never the same key in both files.
  4. 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

  1. 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 tolerant init(from:), and a value test in SettingsCompatTests that an old settings.json still decodes.
  2. Apply it: engine-side options go through AppModel+Engine.swift (applyCallOptions, applyCodecOptions, scheduleNetworkApply; a network field that needs a restart is listed in NetworkSettings.needsRestart(from:)).
  3. Add the control to the pane in Views/Settings/ (each pane is a PaneContainer, 680 pt wide) using model.updateSettings { $0... = ... }.
  4. Strings in L and both languages; describe the control and its states in docs/design/SCREENS.md, and update mockups.html if the pane layout changes.
  5. A demo state in Model/DemoState.swift if the control has a state worth a screenshot.

Add a codec

  1. third_party/pjsip/config_site.h: set PJMEDIA_HAS_<NAME>_CODEC 1 (and, if pjproject's configure has a --disable-<name>-codec flag, make sure scripts/build-pjsip.sh does not pass it; the script already fails when configure disables one of the expected codecs or a codec archive is missing). Rerun scripts/build-pjsip.sh: PJSIP is rebuilt for both architectures, OpenSSL and Opus stay cached.
  2. Package.swift: add the archive to pjsipLibraries, keeping it in sync with third_party/pjsip/LINKFLAGS; then rm -rf .build and swift build.
  3. Engine.optionalCodecs in MacroSIPCore (the ids shown after G.711, left at priority 0 until enabled) and CodecCatalog in Views/Settings/CodecsPane.swift (all, onByDefault; it appends ids a stored order lacks).
  4. Tests: an id listing test and a loopback call that negotiates the codec (Engine262Tests shows 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:

  1. Add the case to DirectorySourceKind and, if it has settings, a Codable config struct with a tolerant init(from:) (every key optional) and a field on DirectorySource decoded with try?, so a damaged config does not drop the settings file.
  2. Implement DirectoryProvider (an actor when it keeps a cache, a struct otherwise): search returns at most limit entries, lookup uses DirectoryNumber.matches, test returns .connected(entries:), allEntries everything for import. Throw DirectoryError; .signInRequired puts the source in "Sign in required". For a local list build a DirectoryIndex.
  3. Return it from DirectoryService.defaultProvider. Secrets go through DirectoryCredentials, never into settings.json.
  4. Tests in a Directory*Tests.swift file; StubDirectoryProvider in DirectoryServiceTests.swift covers the service side.
  5. UI: the type popup and fields in Views/Directory/DirectorySourceSheet.swift, strings, SCREENS.md section 13, and a directory-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.

Clone this wiki locally