Skip to content

Commit 8fed8ca

Browse files
Fix the macOS quarantine hang, and say how to run an unsigned app
Downloading the zip stamps com.apple.quarantine on the bundle and on every one of the ~450 files inside it. Approving the app in System Settings clears only the outer one, so dyld stalls checking each nested library: the app bounces in the Dock and never opens a window, with the main thread parked in _dyld_start before Python starts. Right-click -> Open does not help, and it looks for all the world like a crash. macOS builds now ship a folder containing the app, a double-clickable Sortero-first-run.command that clears the flag from the whole bundle and launches it, and a READ ME FIRST explaining why. Verified against a freshly quarantined copy: 911 flagged files to 0, signature still valid, and the app reaches its run loop instead of hanging in the loader. Release notes and the README now lead with this rather than the useless right-click advice, and cover the Windows SmartScreen and Linux Tk cases too. Notarisation is the only real fix and needs a paid Apple Developer account. Also drops PyInstaller's redundant onedir output from the macOS zip. --windowed emits both dist/Sortero and dist/Sortero.app; the .app is self-contained, so shipping both put the whole program in twice and took the download from 13 MB to 27 MB. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent ed1c695 commit 8fed8ca

5 files changed

Lines changed: 137 additions & 9 deletions

File tree

.github/workflows/release.yml

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ jobs:
6060
- name: Verify macOS binary is universal
6161
if: runner.os == 'macOS'
6262
run: |
63-
BIN=build/dist/Sortero.app/Contents/MacOS/Sortero
63+
BIN=build/dist/Sortero/Sortero.app/Contents/MacOS/Sortero
6464
lipo -archs "$BIN"
6565
lipo -archs "$BIN" | grep -q x86_64 && lipo -archs "$BIN" | grep -q arm64 \
6666
|| { echo "not universal"; exit 1; }
@@ -85,3 +85,27 @@ jobs:
8585
name: Sortero ${{ github.event.inputs.tag || github.ref_name }}
8686
generate_release_notes: true
8787
files: artifacts/**/*.zip
88+
body: |
89+
## Installing
90+
91+
**macOS** — the app is signed ad-hoc, not notarised (notarising needs a
92+
paid Apple Developer account). macOS therefore quarantines every file
93+
inside the bundle on download. Approving the app in System Settings only
94+
clears the outer one, so it will **bounce in the Dock and never open a
95+
window** until the flag is cleared from the whole bundle.
96+
97+
Unzip, then either double-click `Sortero-first-run.command` (included), or
98+
run once:
99+
100+
```
101+
xattr -dr com.apple.quarantine /Applications/Sortero.app
102+
```
103+
104+
Then open it normally. Only needed once per download.
105+
106+
**Windows** — unzip and run `Sortero.exe`. SmartScreen may warn about an
107+
unknown publisher; choose More info → Run anyway.
108+
109+
**Linux** — unzip and run `./Sortero`. Needs Tk (`apt install python3-tk`).
110+
111+
---

README.md

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
[![License: GPL v3+](https://img.shields.io/badge/License-GPLv3+-blue.svg)](LICENSE)
44

5-
A macOS app for getting a DJ collection under control: one canonical copy of
6-
every track, playlists that preserve your curation, clean tags, and an intake
7-
lane for new music.
5+
A cross-platform desktop app for getting a DJ collection under control: one
6+
canonical copy of every track, playlists that preserve your curation, clean
7+
tags, and an intake lane for new music. Runs on macOS, Windows and Linux.
88

99
![tabs: Overview, Organise, Tags, Duplicates, Import, History](build/icon_1024.png)
1010

@@ -122,8 +122,27 @@ from the [Releases page](../../releases):
122122
| Windows | `Sortero-windows-x86_64.zip` |
123123
| Linux | `Sortero-linux-x86_64.zip` |
124124

125-
macOS builds are ad-hoc signed, not notarised, so the first launch needs
126-
right-click → Open.
125+
### macOS: clear the quarantine flag first
126+
127+
Builds are signed ad-hoc, not notarised — notarising requires a paid Apple
128+
Developer account. macOS stamps a quarantine flag on the app **and on every one
129+
of the ~450 files inside it** when you download the zip. Approving the app in
130+
System Settings only clears the outer bundle, so the dynamic loader stalls on
131+
the nested libraries: **the app bounces in the Dock and no window ever appears.**
132+
Right-click → Open does not fix this.
133+
134+
Unzip, then either double-click the included `Sortero-first-run.command`, or run:
135+
136+
```bash
137+
xattr -dr com.apple.quarantine /Applications/Sortero.app
138+
```
139+
140+
Then open Sortero normally. Only needed once per download. If Sortero is in
141+
`/Applications`, macOS may ask permission before another program can modify apps
142+
there — approve it, or run the app from `~/Downloads` instead.
143+
144+
**Windows** — SmartScreen may warn about an unknown publisher; More info → Run anyway.
145+
**Linux** — needs Tk (`apt install python3-tk`).
127146

128147
### Building from source
129148

build/Sortero-first-run.command

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#!/bin/bash
2+
# Sortero — macOS first run
3+
#
4+
# Sortero is signed ad-hoc rather than notarised (that needs a paid Apple
5+
# Developer account). When macOS downloads the zip it stamps a "quarantine"
6+
# flag on the app AND on every one of the ~450 files inside it. Approving the
7+
# app in System Settings only clears the flag on the outer bundle, so the
8+
# loader stalls checking each nested library — the app bounces in the Dock and
9+
# no window ever appears.
10+
#
11+
# This script clears that flag from the whole bundle and launches the app.
12+
# It changes nothing else, and does not need your password.
13+
14+
set -u
15+
DIR="$(cd "$(dirname "$0")" && pwd)"
16+
17+
for CANDIDATE in "$DIR/Sortero.app" "/Applications/Sortero.app" "$HOME/Applications/Sortero.app"; do
18+
if [ -d "$CANDIDATE" ]; then
19+
APP="$CANDIDATE"
20+
break
21+
fi
22+
done
23+
24+
if [ -z "${APP:-}" ]; then
25+
echo "Couldn't find Sortero.app next to this script or in /Applications."
26+
echo "Move this script beside Sortero.app and run it again."
27+
read -r -p "Press return to close."
28+
exit 1
29+
fi
30+
31+
echo "Clearing the quarantine flag on:"
32+
echo " $APP"
33+
echo
34+
35+
if xattr -dr com.apple.quarantine "$APP" 2>/dev/null; then
36+
echo "Done."
37+
else
38+
echo "Couldn't clear it automatically."
39+
echo
40+
echo "If Sortero is in /Applications, macOS asks permission before another"
41+
echo "program may modify apps there. Either approve the prompt and run this"
42+
echo "again, or run the app from your Downloads folder instead."
43+
read -r -p "Press return to close."
44+
exit 1
45+
fi
46+
47+
echo "Launching Sortero…"
48+
open "$APP"
49+
sleep 1

build/build_app.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,46 @@ def main():
9595
else:
9696
arch = "-".join(arches) or "unknown"
9797
zip_path = os.path.join(DIST, f"Sortero-macOS-{arch}.zip")
98+
99+
# Ship the first-run helper beside the app. Downloading the zip stamps
100+
# com.apple.quarantine on every file inside the bundle, and approving
101+
# the app in System Settings only clears the outer one - the loader
102+
# then stalls on the nested libraries and no window appears.
103+
helper_src = os.path.join(BUILD, "Sortero-first-run.command")
104+
payload = os.path.join(DIST, "Sortero")
105+
# --windowed also leaves a onedir build at dist/Sortero. The .app is
106+
# self-contained, so that folder is dead weight - drop it before reusing
107+
# the name for the zip payload, or it ships twice.
108+
if os.path.isdir(payload):
109+
shutil.rmtree(payload)
110+
os.makedirs(payload)
111+
shutil.move(app, os.path.join(payload, "Sortero.app"))
112+
if os.path.exists(helper_src):
113+
helper_dst = os.path.join(payload, "Sortero-first-run.command")
114+
shutil.copy2(helper_src, helper_dst)
115+
os.chmod(helper_dst, 0o755)
116+
readme = os.path.join(payload, "READ ME FIRST.txt")
117+
with open(readme, "w") as fh:
118+
fh.write(
119+
"Sortero for macOS\n"
120+
"=================\n\n"
121+
"Sortero is signed ad-hoc, not notarised, so macOS quarantines every\n"
122+
"file inside the app when you download it. Approving the app in System\n"
123+
"Settings only clears the outer bundle, and the app will bounce in the\n"
124+
"Dock without ever opening a window.\n\n"
125+
"To fix that, once:\n\n"
126+
" 1. Drag Sortero.app wherever you want it (Applications is fine).\n"
127+
" 2. Double-click 'Sortero-first-run.command'.\n\n"
128+
"Or run this in Terminal:\n\n"
129+
" xattr -dr com.apple.quarantine /Applications/Sortero.app\n\n"
130+
"After that, open Sortero normally. You only need to do this once per\n"
131+
"download.\n")
132+
98133
print("==> zipping (ditto preserves the bundle)")
99134
subprocess.run(["ditto", "-c", "-k", "--sequesterRsrc", "--keepParent",
100-
app, zip_path], check=True)
101-
print(f"\nbuilt {app}\nzip: {zip_path} ({os.path.getsize(zip_path)/1e6:.1f} MB)")
135+
payload, zip_path], check=True)
136+
print(f"\nbuilt {payload}/Sortero.app\nzip: {zip_path} "
137+
f"({os.path.getsize(zip_path)/1e6:.1f} MB)")
102138
else:
103139
target = os.path.join(DIST, "Sortero")
104140
plat = "windows" if IS_WIN else "linux"

sortero/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.3.0"
1+
__version__ = "0.3.1"

0 commit comments

Comments
 (0)