diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 8395b1cd9f..6bbfccd9f0 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -308,16 +308,15 @@ Then, run the tests using `pnpm run test-release`.
macOS requires apps to be code signed with an Apple certificate. To test development builds
you can ad-hoc sign the packaged app which will let you run it locally.
-1. In `package.json` remove the macOS signing script: `"sign": "./ts/scripts/sign-macos.node.js",`
-2. Build the app and ad-hoc sign the app bundle:
+1. Build the app while skipping the custom macOS signing script:
```
pnpm run generate
-pnpm run build
+SKIP_SIGNING_SCRIPT=1 pnpm run build
cd release
# Pick the desired app bundle: mac, mac-arm64, or mac-universal
cd mac-arm64
codesign --force --deep --sign - Signal.app
```
-3. Now you can run the app locally.
+2. Now you can run the app locally.
diff --git a/build/entitlements.mas-dev.inherit.plist b/build/entitlements.mas-dev.inherit.plist
new file mode 100644
index 0000000000..4692fa084b
--- /dev/null
+++ b/build/entitlements.mas-dev.inherit.plist
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+ com.apple.security.app-sandbox
+
+ com.apple.security.inherit
+
+ com.apple.security.cs.allow-jit
+
+
+
diff --git a/build/entitlements.mas-dev.plist b/build/entitlements.mas-dev.plist
new file mode 100644
index 0000000000..3792692b60
--- /dev/null
+++ b/build/entitlements.mas-dev.plist
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+ com.apple.security.app-sandbox
+
+ com.apple.security.cs.allow-jit
+
+ com.apple.security.device.audio-input
+
+ com.apple.security.device.microphone
+
+ com.apple.security.device.camera
+
+ com.apple.security.files.downloads.read-write
+
+ com.apple.security.files.user-selected.read-write
+
+ com.apple.security.personal-information.photos-library
+
+ com.apple.security.network.client
+
+ com.apple.security.application-groups
+
+ U68MSDN6DR.org.whispersystems.signal-desktop
+
+
+
diff --git a/package.json b/package.json
index 822560e717..61c917a8fb 100644
--- a/package.json
+++ b/package.json
@@ -100,6 +100,7 @@
"build:styles:tailwind": "tailwindcss -i ./stylesheets/tailwind-config.css -o ./stylesheets/tailwind.css",
"build:electron": "electron-builder --config.extraMetadata.environment=$SIGNAL_ENV",
"build:release": "cross-env SIGNAL_ENV=production pnpm run build:electron --config.directories.output=release",
+ "build:mas-dev": "bash ./scripts/build-mas-dev.sh",
"build:release-win32-all": "pnpm run build:release --arm64 --x64",
"build:preload-cache": "node ts/scripts/generate-preload-cache.node.js",
"build:emoji": "run-p build:emoji:32 build:emoji:64",
@@ -442,12 +443,12 @@
}
],
"mergeASARs": true,
+ "sign": "./ts/scripts/sign-macos.node.js",
"releaseInfo": {
"vendor": {
"minOSVersion": "21.0.1"
}
},
- "sign": "./ts/scripts/sign-macos.node.js",
"singleArchFiles": "node_modules/@signalapp/{libsignal-client/prebuilds/**,ringrtc/build/**,sqlcipher/prebuilds/**}",
"target": [
{
@@ -469,6 +470,20 @@
"NSAutoFillRequiresTextContentTypeForOneTimeCodeOnMac": true
}
},
+ "masDev": {
+ "type": "development",
+ "sign": null,
+ "hardenedRuntime": false,
+ "entitlements": "./build/entitlements.mas-dev.plist",
+ "entitlementsInherit": "./build/entitlements.mas-dev.inherit.plist",
+ "preAutoEntitlements": false,
+ "extendInfo": {
+ "ElectronTeamID": "U68MSDN6DR",
+ "NSCameraUsageDescription": "Signal uses your camera for video calling.",
+ "NSMicrophoneUsageDescription": "Signal uses your microphone for voice and video calling.",
+ "ITSAppUsesNonExemptEncryption": true
+ }
+ },
"win": {
"signtoolOptions": {
"certificateSubjectName": "Signal Messenger, LLC",
diff --git a/scripts/build-mas-dev.sh b/scripts/build-mas-dev.sh
new file mode 100755
index 0000000000..73390fe2dc
--- /dev/null
+++ b/scripts/build-mas-dev.sh
@@ -0,0 +1,25 @@
+#!/usr/bin/env bash
+# Copyright 2026 Signal Messenger, LLC
+# SPDX-License-Identifier: AGPL-3.0-only
+
+set -euo pipefail
+
+if [[ -z "${MAS_PROVISIONING_PROFILE:-}" ]]; then
+ echo "MAS_PROVISIONING_PROFILE is required" >&2
+ exit 1
+fi
+
+# Electron Builder applies `mac` config first and then `masDev`.
+# Override mac configuration during build to ensure consistency between
+# the two sets of values, otherwise the mas-dev build will crash on launch.
+SIGNAL_ENV="${SIGNAL_ENV:-production}" \
+SKIP_SIGNING_SCRIPT=1 \
+pnpm run build:electron \
+ --config.directories.output=release \
+ --mac mas-dev \
+ --arm64 \
+ --publish=never \
+ --config.mac.entitlements=./build/entitlements.mas-dev.plist \
+ --config.mac.entitlementsInherit=./build/entitlements.mas-dev.inherit.plist \
+ --config.mac.preAutoEntitlements=false \
+ --config.masDev.provisioningProfile="$MAS_PROVISIONING_PROFILE"
diff --git a/ts/scripts/copy-language-packs.node.ts b/ts/scripts/copy-language-packs.node.ts
index b87111f593..32d1b7c48b 100644
--- a/ts/scripts/copy-language-packs.node.ts
+++ b/ts/scripts/copy-language-packs.node.ts
@@ -16,7 +16,7 @@ export async function afterPack({
);
let localesPath: string;
- if (electronPlatformName === 'darwin') {
+ if (electronPlatformName === 'darwin' || electronPlatformName === 'mas') {
const { productFilename } = packager.appInfo;
// en.lproj/*
diff --git a/ts/scripts/fuse-electron.node.ts b/ts/scripts/fuse-electron.node.ts
index 7b910801b4..5288da36af 100644
--- a/ts/scripts/fuse-electron.node.ts
+++ b/ts/scripts/fuse-electron.node.ts
@@ -13,7 +13,7 @@ export async function afterPack({
const { productFilename } = packager.appInfo;
let target;
- if (electronPlatformName === 'darwin') {
+ if (electronPlatformName === 'darwin' || electronPlatformName === 'mas') {
target = `${productFilename}.app`;
} else if (electronPlatformName === 'win32') {
target = `${productFilename}.exe`;
@@ -43,9 +43,11 @@ export async function afterPack({
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
// Disables the --inspect and --inspect-brk family of CLI options
[FuseV1Options.EnableNodeCliInspectArguments]: enableInspectArguments,
- // Enables validation of the app.asar archive on macOS/Windows
+ // Enables validation of the app.asar archive on macOS/Windows.
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]:
- electronPlatformName === 'darwin' || electronPlatformName === 'win32',
+ electronPlatformName === 'darwin' ||
+ electronPlatformName === 'mas' ||
+ electronPlatformName === 'win32',
// Enforces that Electron will only load your app from "app.asar" instead of
// its normal search paths
[FuseV1Options.OnlyLoadAppFromAsar]: true,
diff --git a/ts/scripts/prune-macos-release.node.ts b/ts/scripts/prune-macos-release.node.ts
index 478ceb9210..e412f1bec6 100644
--- a/ts/scripts/prune-macos-release.node.ts
+++ b/ts/scripts/prune-macos-release.node.ts
@@ -21,7 +21,7 @@ export async function afterPack({
packager,
electronPlatformName,
}: AfterPackContext): Promise {
- if (electronPlatformName !== 'darwin') {
+ if (electronPlatformName !== 'darwin' && electronPlatformName !== 'mas') {
return;
}
diff --git a/ts/scripts/sign-macos.node.ts b/ts/scripts/sign-macos.node.ts
index ca166167d5..3bcd333ab3 100644
--- a/ts/scripts/sign-macos.node.ts
+++ b/ts/scripts/sign-macos.node.ts
@@ -10,6 +10,11 @@ const { realpath } = fsExtra;
// eslint-disable-next-line max-len
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
export async function sign(configuration: any): Promise {
+ if (process.env.SKIP_SIGNING_SCRIPT === '1') {
+ console.log('SKIP_SIGNING_SCRIPT=1, skipping custom macOS signing script');
+ return;
+ }
+
const scriptPath = process.env.SIGN_MACOS_SCRIPT;
if (!scriptPath) {
throw new Error(