Skip to content

Latest commit

 

History

History
109 lines (96 loc) · 23.1 KB

File metadata and controls

109 lines (96 loc) · 23.1 KB

Contributing to MacTools

[中文] [English]

Thanks for your interest in MacTools. Please keep each contribution small and clear: explain the problem, provide verifiable changes, and avoid mixing unrelated refactors into the same pull request.

Unless a file is clearly identified as third-party material under separate terms, contributions accepted into MacTools are licensed under GPL-3.0-only, consistent with the repository's licensing policy. By submitting a contribution, you confirm that you have the right to provide it under those terms.

Ways to Contribute

  • Bug reports should include reproduction steps, expected behavior, actual behavior, macOS version, and relevant logs or screenshots.
  • Feature suggestions should describe the use case, target users, and expected interaction. For large plugins or interaction changes, open an issue first to align on scope.
  • Changes involving file deletion, system permissions, global shortcuts, display control, signing, or update flows should explain risks, safeguards, and rollback options.

Development Environment

  • Xcode and xcodegen are required. The project supports macOS 14.0 and later.
  • First-time setup: run make setup, then edit LocalConfig.xcconfig and fill in DEVELOPMENT_TEAM and a stable, non-placeholder BUNDLE_IDENTIFIER_PREFIX. Debug builds fail early when either value is missing so macOS cannot register a malformed duplicate app identity.
  • Use make run for local app testing. It installs the canonical Debug app at ~/Applications/MacTools Dev.app and unregisters other MacTools Dev build copies from LaunchServices.
  • Common commands: make generate generates the Xcode project, make build validates compilation, and make run installs the verified Debug bundle at ~/Applications/MacTools Dev.app before running it locally.
  • Plugin development: make run incrementally builds the app and plugins, then syncs the latest Debug plugin packages to the local development marketplace. A full sync moves packages absent from the current checkout into the recoverable Debug quarantine; a filtered PLUGIN=... sync leaves unrelated packages untouched. make sync-debug-plugins only syncs already built plugins. make build-plugin is reserved for validating dynamic plugin packages or release flows; to build one plugin, run make build-plugin PLUGIN=calendar.
  • Do not commit local or generated files: MacTools.xcodeproj, MacTools.xcworkspace, LocalConfig.xcconfig, build/, or scripts/release.local.env.

Project Structure

  • Sources/App/: app entry point, menu bar status item, settings pages, and window routing.
  • Sources/Core/: shared infrastructure such as the plugin host, dynamic plugin loading, shortcuts, permissions, logging, and updates.
  • Sources/MacToolsPluginKit/: plugin APIs, declarative UI models, and runtime context.
  • Plugins/<PluginName>/: plugin manifest, source code, bundle entry point, resources, and adjacent tests.
  • Tests/: XCTest coverage for shared App/Core logic. Plugin tests should live inside the corresponding plugin directory when possible.
  • project.yml: root XcodeGen project source, only for the App, PluginKit, and shared aggregate entry points. Plugin targets are generated automatically.
  • Plugins/<PluginName>/project.yml: optional per-plugin build overrides, only when a plugin needs extra frameworks, include paths, bundle resources, helper/tool targets, or target overrides.
  • docs/plugins/: plugin packages, catalogs, local debugging, and release flow documentation.
  • docs/icon-gallery/: checked-in menu-bar icon catalog, previews, animation frames, and archives; rendering-mode rules are documented in docs/icon-gallery.md.
  • docs/superpowers/: larger product, interaction, or implementation design documents.

Development Guidelines

  • Add new plugins under Plugins/<PluginName>/ with at least plugin.json, Sources/, and Bundle/.
  • Ordinary plugins only need to define plugin.json, source code, and a bundle entry point. make generate scans Plugins/*/plugin.json and generates local Configs/GeneratedPlugins.yml; do not edit generated files manually.
  • Features that require macOS app extensions, such as Finder Sync, must add the extension target to the root project.yml and embed it in the main app. Use the dynamic plugin only for the MacTools panel/settings surface.
  • Command workflows for adding and updating plugins are documented in the Development Steps section of docs/plugins/local-native-plugins.md.
  • Keep documentation short and task-focused. User-visible behavior changes should update README.md or the relevant file under docs/; plugin package, catalog, or release flow changes should update docs/plugins/.
  • Keep CHANGELOG.md as the canonical release history. Do not edit Sources/Resources/ReleaseHistory.json by hand; release preparation regenerates it, and python3 scripts/changelog.py export-history repairs it after intentional historical edits.
  • Icon gallery assets must explicitly declare renderingMode; use template only for black artwork on transparency, and use original for color or grayscale detail. Third-party static assets must pin their upstream revision and catalog mapping in docs/icon-gallery/sources/manifest.json, with the corresponding license under Sources/Resources/ThirdPartyNotices/. Run the gallery generation and related tests after catalog changes.
  • Plugins implement MacToolsPlugin; menu panel plugins implement PluginPrimaryPanel, and component panel plugins implement PluginComponentPanel.
  • plugin.json.id must be stable, readable, and exactly match the runtime PluginMetadata.id; each plugin package should return exactly one plugin instance.
  • Plugin data is preserved by default on uninstall. A plugin that stores sensitive payloads and must crypto-shred them should declare uninstallDataPolicy: removePrivateData, use PluginPrivateDataKeychainIdentity for its encryption key, and leave cleanup to the host; failed cleanup must finish before the same plugin can be reinstalled, and lifecycle changes must test both recovery failures and successful cleanup.
  • Register newly consumed PluginKit APIs in scripts/tests/test_plugin_minimum_host_compatibility.py, including optional protocols. The inventory retains API introduction versions for older ABI checks; current PluginKit v6 packages require MacTools 1.3.0 or later. Keeping an older protocol's witness table unchanged does not make new symbols loadable by older hosts.
  • Plugin display state should be expressed through PluginPanelState, PluginPanelDetail, PluginPanelControl, and related models. Do not bypass the existing panel framework.
  • Prefer PluginSettingsPage.form with declarative sections and typed controls. Use a custom form section for a complex region and PluginSettingsPage.workspace only for task-oriented managers or editors that need the full content area. Permissions, shortcuts, page chrome, search, validation, and backgrounds remain host-owned.
  • Publish executable plugin capabilities through stable PluginActionProviding definitions. Keep action discovery and execution in the host-owned registry/executor, and keep ordinary global bindings in ShortcutAssignmentService; workflows, Run Links, and Action Grid must reference those actions instead of defining parallel command, shortcut, or URL paths. The architecture and automated test matrix are documented in docs/actions-automation.md.
  • A plugin that composes an existing provider's canonical action may adopt PluginActionExecutionHostContextConsuming; do not call the provider implementation or duplicate its system write directly. Use its live lookup, guarded execution, and explicit provider-settings navigation so host availability, safety, confirmation, and diagnostics remain authoritative. Verify against fresh provider snapshots rather than debounced UI catalog state, and keep navigation user-initiated. Mac Settings is the reference implementation in docs/plugins/mac-settings.md.
  • The action-execution bridge follows the same ABI and minimum-host checks. Include every public bridge type in scripts/tests/test_plugin_minimum_host_compatibility.py when extending it, and keep consuming manifests on the first compatible host release.
  • Keep composed providers such as Mac Settings in the shared manifest/runtime action snapshot tests. Disclose setting-specific permissions in setup guidance when they are not a requirement for every action.
  • Preserve selected policies separately from their current effect: Appearance's set-mode action models Auto / Light / Dark, while the quick toggle describes the rendered dark state. Composed settings, profiles, and Undo must use the policy action. Optional System Settings links must use known pane/anchor pairs, not a generic app-opening fallback.
  • Mac Settings release scope is enforced in its catalog factory. Before restoring a deferred setting, complete its criteria in the release scope and backlog, then update the scope tests and documentation together. Keep deferred definitions available for profile validation without exposing their adapters to execution.
  • A plugin may implement PluginActionExposureProviding to veto a canonical action on a host-owned system surface such as App Intents. Treat .automatic as delegation to the host's conservative eligibility policy, never as an allowlist override. Unknown surfaces and provider failures fail closed, and the executor rechecks the live policy immediately before starting the action. Run Link policy remains a separate contract.
  • When a plugin adopts an action-surface or other newly exported PluginKit type, set its plugin.json.minHostVersion to the first compatible app release. Compatibility metadata belongs in the source manifest. When local plugin validation requires that unreleased host version, MARKETING_VERSION may predeclare it; the app release helper treats a source version ahead of the latest app tag as the default release target and remains responsible for advancing CURRENT_PROJECT_VERSION. Release tooling owns plugin package version bumps.
  • If ordinary plugin resources rarely change, prefer bundling them into the executable. If extra bundle resources are needed, declare the smallest necessary differences in the plugin's own project.yml.
  • Custom plugin settings views must reuse MacToolsPluginKit.PluginSettingsTheme and .pluginSettingsCardBackground(.standard/.recessed). Do not copy private plugin settings styles, and do not make plugins depend on Sources/App/SettingsStyle.swift.
  • Call onStateChange?() after plugin state changes. Long-running scans, file system work, and system calls should not block the main thread for extended periods.
  • User-facing copy is primarily Chinese. Keep it concise, clear, and close to native macOS wording.
  • Localize user-facing copy with .xcstrings. App/Core copy belongs under Sources/Resources/Localization, PluginKit copy under Sources/MacToolsPluginKit/Resources, and plugin copy under Plugins/<PluginName>/Resources. Plugin plugin.json files should keep displayName/summary as fallbacks and add localizedMetadata for marketplace and unloaded-plugin presentation. Pre-install product, capability, privacy, setup, and relationship metadata belongs in the same plugin.json; follow docs/plugins/plugin-manifest.schema.json. Declare localized product copy once under the source-only productStrings table, using @displayName, @summary, @localizable.<key>, @standardAction.<key>, @standardSetup.requirements.<key>, or all 11 locale values, and make every localized product field reference @productStrings.<key>. Keep referenced screenshots under MarketplaceAssets/, and never add a parallel marketplace manifest or machine-local dynamic action entries.
  • Keep current plugin.json runtime envelopes complete. Generated package manifests must contain expanded localization values and match their source metadata; do not edit package copies independently. Legacy manifests must still include runtime-decodable capabilities and permissions; omitting newer product fields is supported only for PluginKit versions below 5 through the explicit local-debug compatibility flag and must never be used for release catalog generation.
  • When merging new plugins with product-metadata changes, register their factories in PluginRuntimeActionSnapshotTests and preserve independent runtime policies such as uninstallDataPolicy alongside the product fields.
  • New plugins should provide localization whenever practical, at minimum for panel copy, settings copy, permission text, and plugin metadata.
  • Prefer Apple native frameworks. When adding system frameworks, private include paths, or helper executables inside a plugin bundle, declare the smallest necessary differences in the plugin's own project.yml. Bundle resource executables that need separate signing should be listed in plugin.json.package.signPaths.
  • System power and session-ending actions must use native macOS confirmation flows, remain foreground-only, and avoid immediate restart or shutdown events that can discard unsaved work.
  • Plugins that use private Apple frameworks must load them dynamically at runtime and validate the required classes and selectors. Do not statically link private frameworks, and surface unsupported-system errors instead of crashing.
  • Plugins that intercept pointer events must declare Accessibility permission, stop their event tap on deactivation, and re-enable a tap disabled by macOS.
  • Plugins that move or resize windows must use public Accessibility APIs for ordinary position and size writes, revalidate the focused window immediately before writing, calculate against the current display visible frame, and keep pure multi-display geometry independently testable. Capabilities unavailable through Accessibility may use a narrowly scoped, dynamically loaded, version-gated private API after review and must fail closed when unsupported.

Testing

  • Behavioral changes should add or update adjacent XCTest coverage. Test files should be named <TypeName>Tests.swift.
  • Full test command: xcodebuild -project MacTools.xcodeproj -scheme MacTools -configuration Debug -derivedDataPath build/DerivedData test -quiet.
  • Single test class: append -only-testing:MacToolsTests/<TestClassName> to the full test command.
  • File system tests should use temporary directories or fake stores. Disk cleanup tests must not delete real user directories.

Pull Request Checklist

  • Keep the PR focused, and explain the purpose, verification, and user impact.
  • Prefer English for commit messages, pull request titles/descriptions, and issues.
  • Build or tests have passed. If they could not be run, explain why in the PR.
  • User-visible behavior changes are reflected in README.md or the relevant design documentation.
  • User-visible app or plugin changes include a concise English changelog fragment in changes/unreleased/*.md.
  • Plugin manifest capabilities.settings (none, form, or workspace) matches the runtime settingsPage layout.
  • Rich manifest static and dynamic action descriptors match the runtime provider/action identity, risk, permissions, external policy, automation eligibility, and parameter portability.
  • High-risk features cover safety checks, error states, and missing-permission cases.
  • The PR does not include unrelated formatting, generated files, local configuration, certificates, or release credentials.
  • New or updated third-party material is recorded in Sources/Resources/ThirdPartyNotices/manifest.json with an exact upstream revision, affected products, source paths, and retained license text.

Release

Nightly isolation also covers Activity Bar sockets/hook registrations and CLI/broker service identities. Keep the stable identifiers unchanged, keep the hardware listener lock shared, and add cross-channel coexistence tests when introducing another process-global namespace.

  • Releases are handled by maintainers. Do not create tags, publish GitHub Releases, or commit release artifacts in ordinary contributions.
  • For GitHub-based releases, prefer Actions -> Prepare Release. Enter type, target version, and whether to release; when release is enabled, the workflow continues to the actual release workflow after bumping, committing, and creating the tag.
  • For quick releases, prefer make release. The command interactively chooses app or plugin, analyzes the next patch/minor/major version, previews the bump, then only after confirmation runs git pull --rebase, lightweight checks, version updates, commit, tag creation, and tag push.
  • App releases update MARKETING_VERSION and CURRENT_PROJECT_VERSION in Configs/AppVersion.xcconfig. The app and embedded extensions inherit this shared version config. After pushing a v*.*.* tag, the Release workflow builds, signs, notarizes, uploads the DMG, marks the stable App release as GitHub Latest, and updates the Appcast plus website download metadata.
  • App signing must sign the embedded MacToolsCLIBroker executable before the outer app. For an inline workflow-only fix, retry from Actions -> Release -> Run workflow on main with the existing tag; see App release recovery. Re-running an old job does not pick up workflow fixes.
  • Public Nightly releases are maintainer-operated snapshots of main. They use a separate app identity, storage scope, privileged helper paths/signing identifiers, Keychain services, update feed, and complete same-commit plugin catalog. The optional Apple silicon CLI is signed, notarized, and published separately from the app with the same version and build; the release verifier requires the exact Nightly CLI identifier, signing team, arm64 architecture, system-only dynamic dependencies, executable mode, archive contents including the unchanged root GPL license, checksum, and version --json metadata. Nightly plugin versions are generated as source-major.run.attempt; feature contributions must not bump source manifests for Nightly. After two successful manual update runs, maintainers may enable the daily schedule with ENABLE_NIGHTLY_RELEASES=true. The build job skips unchanged scheduled runs by comparing against the deployed appcast's source, excluding generated docs/nightly/**; manual runs always publish. Publishable runs then pass one immutable artifact through a credential-free CLI verification job before the separate publication job receives repository write access. Partial retries reuse the producing build's artifact ID and version metadata; see Nightly recovery for when a full rebuild is required. Rollback refs must be ancestors of main and support the workflow's current Nightly release interface and CLI testing guide. Missing or indeterminate previous metadata does not prevent publication. Failed draft releases are removed by the creating run, and the next successful run also removes abandoned drafts in the workflow-owned nightly-<run>-<attempt> namespace. Preserve existing stable names when adding channel-specific storage or helpers.
  • Plugin releases push a plugins-* batch tag. The default auto mode uses the production catalog to find new plugins, already bumped plugins, and package-related plugin changes; it updates plugin.json.version when needed, then the Plugin Release workflow builds plugins and merges the signed catalog. Plugin batch releases are never marked as GitHub Latest. Catalog signing uses the same Foundation canonicalization as runtime verification, and the workflow validates the configured Ed25519 key pair before building. When a batch raises individual plugins' minHostVersion, publish and verify the signed plugin catalog before releasing that host version; the catalog keeps its oldest schema-compatible host floor and older apps leave newer entries unavailable.
  • On first launch, a new app version checks installed plugins and automatically updates them from the signed production catalog. It does not normally install plugins the user has not installed. The only exception is a host-declared feature-extraction migration: when an installed source plugin still owns a legacy preference, the host may runtime-validate the replacement package and update the source as one rollback-protected operation. Manually installing that replacement also coordinates retirement of an older source package, even before the legacy preference has been written.
  • Non-interactive examples: make release ARGS="--type app --version 1.0.7 --yes" or make release ARGS="--type plugin --version 1.0.10 --plugin-mode selected --plugin calendar --yes".
  • Add --dry-run to preview the steps. The working tree must be clean before a real release.
  • Before local release builds, copy scripts/release.local.env.sample to scripts/release.local.env and fill in at least DEVELOPER_ID_APPLICATION.
  • If Apple notarization is needed, store credentials first with xcrun notarytool store-credentials.
  • Version numbers default to MARKETING_VERSION and CURRENT_PROJECT_VERSION in Configs/AppVersion.xcconfig.
  • Local production builds can still use the lower-level script: ./scripts/release-local.sh; before publishing to GitHub Releases, run gh auth login, then ./scripts/release-local.sh --publish.
  • Plugin library releases are triggered by plugins-* batch tags through the Plugin Release workflow. Within one PluginKit ABI and catalog-schema compatibility line, plugins with bumped versions are built and uploaded, then merged into that line's catalog. Changes under Sources/MacToolsPluginKit/ require rebuilding and bumping every plugin so the catalog cannot retain binaries linked against an older shared framework. The standard make release flow performs these manifest bumps in the release commit; feature PRs should not pre-bump unrelated plugins. The first release of a new ABI or schema line also rebuilds every plugin and writes a separate catalog. MacTools through 1.1.6 keeps reading the immutable PluginKit v4 catalog at docs/plugins/v4/catalog.json; MacTools 1.2.0 keeps reading PluginKit v5 schema 2 at docs/plugins/v5/catalog.json; MacTools 1.3.0 and later use PluginKit v6/schema 3 at docs/plugins/v6/catalog.json. Publish the compatible plugin batch and catalog first, wait for Pages to serve the committed signed catalog, and only then prepare or publish the corresponding app. The app release helper and final release workflow fail closed unless that deployed catalog exactly matches the committed catalog and has a valid signature. The catalog private key, Developer ID certificate, and GitHub token must come from CI secrets or local environment variables.
  • GitHub Actions build and release configuration is documented in docs/github-actions.md; plugin catalog, package structure, and batch release flows are documented in docs/plugins/plugin-catalog.md.

For the PluginKit v6 migration, source manifests declare pluginKitVersion: 6 and minHostVersion: "1.3.0". Leave plugin package versions, Configs/AppVersion.xcconfig, signed catalogs, and compiled release notes to make release; do not pre-bump them in the ABI migration change. Run make release for plugins first (auto selects all plugins), wait for the v6 catalog commit and Pages deployment, then run the app release. CI and make ci check the frozen v6 client, including settings row, option, and control layouts.