Анатомія повної деінсталяції застосунків на macOS
Усім привіт. Я Юрій Варбанець, CTO в Nektony. У попередній статті я ділився нашою методологією оцінки macOS-утиліт. Цього разу хочу розповісти про задачу, з якою наша команда працює щодня вже 15 років і яка досі не має повного автоматизованого рішення попри розвиток штучного інтелекту.
Звучить вона тривіально: «повністю видалити застосунок з Mac». Здавалося б, перетягнув бандл у кошик — і все чисто. Але кожен, хто хоч раз заглядав у ’~/Library’ після видалення Photoshop чи Office, знає: видалений застосунок лишає по системі десятки артефактів, частина з яких — невинні налаштування, частина — фонові помічники, що продовжують стартувати при логіні, а частина — те, що не можна видаляти, інакше macOS відмовиться завантажуватися.

Я хочу розповісти, що саме означає «повністю видалити Mac-застосунок» з технічної точки зору, чому стандартний підхід «Bundle ID + ім’я» неминуче ламається на реальних кейсах, як ми у Nektony підходимо до цієї задачі та чому 100% автоматизації тут не існує.
Що насправді означає «видалити застосунок»
З погляду користувача застосунок — це одна іконка в ’/Applications’. З погляду інженера — це бандл (’.app’-папка) плюс набір артефактів, які цей бандл лишає по системі під час встановлення й роботи.
На macOS ці артефакти традиційно лежать у достатньо передбачуваних місцях:
’/Applications’ — сам бандл застосунку. ’~/Library/Application Support/<App>’ — конфігурація, локальні бази, кеші конкретного користувача. ’~/Library/Preferences/<bundleID>.plist’ — налаштування: останній відкритий проєкт, стан вікон, прапорці чекбоксів. ’~/Library/Caches/<App>’ — тимчасові файли, мініатюри, скомпільовані ресурси. ’~/Library/Logs/<App>’ — логи. ’~/Library/Containers/<bundleID>’ — sandbox-контейнер для застосунків з App Store. ’~/Library/Group Containers/<groupID>’ — спільний контейнер для розширень того самого вендора. ’~/Library/LaunchAgents’ і ’/Library/LaunchDaemons’ — фонові помічники, що стартують разом із системою або сесією. ’/Library’ — те саме, але для всіх користувачів машини.
Це публічна інформація: у документації Apple вона описана прямо, і будь-який macOS-розробник з нею стикався. На цьому етапі здається, що задача видалення вирішується просто: пройдися цими директоріями, знайди файли застосунку, видали.
Але далі починається реальність.
Чому Bundle ID + ім’я застосунку — це не критерій
Найочевидніший підхід, який реалізують десятки клінерів на ринку: пошукати у вищезазначених папках усе, що містить bundle ID або ім’я застосунку. На практиці цей підхід ламається на першому ж нетривіальному кейсі.
На Reddit часто можна знайти такі історії: користувач хотів видалити, скажімо, Microsoft Office, запустив популярний клінер, той «знайшов» цілу купу сервісних файлів зі словом ’office’ в імені і видалив їх. Серед них опинилися файли від абсолютно сторонніх застосунків, які просто мали ’office’ у назві. Користувач натиснув delete — і ці сторонні застосунки перестали запускатися.

Це поверхневий пошук за рядком, і він провалюється з причин, які не вирішуються «розумнішим запитом». Ось лише кілька проблем, з якими прямолінійний підхід не справляється.
1. Абревіатури в іменах файлів. Наш власний продукт — Duplicate File Finder. Його сервісні файли часто йдуть під ім’ям ’dff’ або ’DFF’. ШІ, до речі, теж не завжди впевнено впізнає такі скорочення без додаткового контексту: трибуквена абревіатура легко може бути чим завгодно. А реальні застосунки, особливо від менших вендорів, дуже люблять економити символи в іменах. У результаті файлом ’dff.plist’ без додаткового аналізу може бути як наш застосунок, так і щось зовсім стороннє.
2. Спільні папки вендора. Класичний випадок — Adobe. Установіть Photoshop і Illustrator, і обидва покладуть свої сервісні файли в спільну папку ’Adobe’. Якщо ви видаляєте Photoshop і за «співпадінням слова» прихоплюєте всю папку ’Adobe’, ви знесли ще й Illustrator, Premiere, Lightroom, коротше, все, що там жило. Ми не раз бачили клінери, які саме так і поводяться.
3. Зміна Bundle ID між версіями. Гарний приклад — 1Password. У них 1Password 4, 5, 6 та 7 виходили з різними bundle ID, проте частину сервісних файлів між мажорними версіями вендор не перейменовував. Якщо у користувача стоять і 6, і 7 паралельно — спроба «знести все, що належить версії 7» через bundle ID просто не дасть очікуваного результату: спільні артефакти лишаться.
4. Системно захищені файли. macOS має цілий клас файлів, які не можна просто видалити з графічного інтерфейсу — система на них посилається на старті. Класичний приклад — kernel extensions (’.kext’). Вони встановлюються як системні розширення, захищаються SIP (System Integrity Protection), і щоб їх дочистити, інколи потрібен перезапуск, інколи — взагалі тимчасове відключення SIP. Маємо історії, коли видалення на вигляд звичайного файлу в звичайній папці клало систему на старті — так ми виявляли, що його шлях був жорстко прописаний у системному реєстрі лаунч-агентів. macOS чекала на нього, не отримувала, і завантаження системи припинялось.
5. Динамічні шляхи. Кеш-файли, тимчасові директорії, «сховище ресурсів» — багато застосунків генерують імена піддиректорій випадково (UUID, хеш сесії, мітка часу). Простий пошук за іменем тут не працює: треба ігнорувати кілька рівнів вкладеності й дивитися лише на ім’я бандла на потрібній глибині.
6. Sandbox vs non-sandbox. Застосунки з App Store зобов’язані тримати свої сервісні файли всередині ’~/Library/Containers/<bundleID>’ — це правило магазину. А ось застосунки, встановлені з сайту вендора (’.dmg’ чи ’.pkg’), можуть розкидати свої файли практично будь-де: під будь-яким ім’ям, у будь-якій директорії, навіть у ’/usr/local/’. Це принципово різні підходи, і єдиного підходу тут не може бути.
7. Залишки без застосунку. Окрема й часто болюча історія: користувач сам перетягнув ’.app’ у кошик. Бандла вже немає, але всі його сервісні файли у ’~/Library’ лишилися. Тепер навіть bundle ID нема де взяти — файли треба впізнавати лише за іменами та структурою. Багато клінерів цей сценарій просто ігнорують, хоча саме він — найчастіша причина «розпухлої» бібліотеки в активних користувачів macOS.
8. Спільні ресурси у Shared. ’/Users/Shared’ — традиційно ігнорована директорія, бо зазвичай машиною чи застосунком користується одна людина. Але насправді там може лежати багато сервісних файлів, особливо у мультикористувацьких сценаріях, і клінер, який її пропускає, лишає за собою хвости.
Наш підхід: правила + ручна перевірка
Тепер найголовніше — як саме ми у Nektony розвʼязуємо цю проблему.
Ми маємо набір правил — еволюційно вибудуваний за час нашого існування — які описують, як треба сканувати застосунок, а не що саме видаляти. Різниця приблизно така ж, як між алгоритмічним та евристичним підходами.
Кілька прикладів того, з чого ці правила складаються.
Існують правила за шляхом:
- у певних кореневих директоріях ми взагалі нічого не шукаємо до видалення (бо ціна помилки — система, що не вантажиться);
- у ’~/Library/Application Support’ — впевнено пропонуємо;
- усе, що знайдено на іншому фізичному диску, автоматично класифікується як користувацькі дані, а не як сервісний файл, бо застосунки самі туди свої службові файли не пишуть.
Існують правила розпізнавання приналежності: ім’я і bundle ID ми використовуємо як один із сигналів, але не єдиний. Над ним нашаровується логіка на рівні структури папок, метаданих, наявності супутніх файлів, типового формату вмісту.
Існують правила для класів застосунків: продукти Adobe, продукти Microsoft, sandbox-застосунки з App Store, фонові утиліти, KEXT-розширення — кожна категорія потребує власної поведінки. Те, що працює для VSCode, ламається на Photoshop.
Існує whitelist чутливих шляхів — клас файлів, які не можна видаляти, навіть якщо вони формально «належать» застосунку. У них є посилання з системного реєстру, або вони очищаються самою macOS при перезавантаженні, або їхнє видалення вимагає окремої системної процедури.
Існують правила для shared-папок і користувацьких даних: для ’/Users/Shared’ логіка інша, ніж для ’~/Library’; користувацькі дані в ’Documents’, ’Downloads’, ’Desktop’ ми взагалі ніколи не чіпаємо, навіть якщо в назві файлу є повний bundle ID застосунку.
Кожне з цих правил — універсальне в тому сенсі, що пишеться один раз і застосовується до всіх застосунків визначених класів. Якщо ми бачимо, що для якогось конкретного продукту правило поводиться не так, як треба, — ми не «додаємо виняток в список»; ми йдемо в правила і шукаємо, яке узагальнення треба змінити, щоб в майбутньому ми могли цей виняток піймати і в інших схожих застосунках.
Чому без людини це не працює
Поверх цих правил працює другий шар, без якого жодне з них не має цінності: екстенсивне ручне тестування.
Уявімо, що з’явився новий застосунок. Наш аналізатор перевіряє, які файли він створює під час роботи юзера, видаляє ті, що правилами визначаються як безпечні, і далі проганяє ряд тестів на стабільність системи. Однак якою б не була просунутою така автоматизація, все одно треба вручну перевірити: чи коректно зачистились лаунч-агенти? Чи правильно клінер повівся із SIP-захищеними файлами? Чи лишилися «хвости» в Containers або Group Containers? Врешті, чи система після цього нормально перезавантажується?
Якщо щось не так — фіксуємо, вертаємось у правила, доопрацьовуємо, перепроганяємо. Так ми робимо щоразу, як виходить новий реліз популярних застосунків чи нова версія macOS. Таке регресійне тестування має відбутися ще до того, як оновлення дістанеться більшості користувачів.
І так, застосунків багато, і ручного тестування багато. Це нудно, дорого за людино-годинами, не масштабується лінійно і не автоматизується — але саме це дозволяє нам гарантувати якість і безпеку наших утиліт.
Де нам допомагає ШІ — і де ні
Останні роки ми, як і вся індустрія, активно експериментуємо зі штучним інтелектом, у тому числі й для аналізу сервісних файлів. ШІ непогано порається з класифікацією на загальних патернах: за ім’ям бандла, типовими структурами директорій, типовим вмістом ’.plist’-файлів. Він — гарний помічник у формулюванні гіпотез: «Ось ці файли, ймовірно, належать ось цьому застосунку».

Але є чітка межа, де ШІ ламається. Абревіатури і нестандартні імена (той самий ’dff’) — модель про них «не знає» без додаткової інформації. Застосунки, які поводяться нетипово (генерують випадкові шляхи, маскуються під системні, ділять папки з вендорським «парасольковим» брендом), легко вводять модель в оману. І найголовніше — ситуації, де рішення «видаляти/не видаляти» має наслідки для системи, а не лише для дискового простору. У таких місцях галюцинація — це крах системи користувача (який найчастіше не турбується належним чином про бекапи).
Тому ми не вибудовуємо процес, у якому модель «вирішує сама»: використовуємо її як ще один сигнал поряд з нашими правилами, а критичні рішення лишаємо за людиною.
Чому це важливо для галузі
Деінсталяція macOS-застосунків — це не «утилітарна задача», з якою впорається ще один клінер на Swift, написаний за вечір. Це постійна робота на стику реверс-інжинірингу, файлових систем і знання конкретних застосунків. Скільки років ми над цим працюємо, і все одно регулярно знаходимо нові й ніде не задокументовані edge cases.
Якщо ви розробляєте власний macOS-продукт із подібною функціональністю, ось кілька практичних висновків з нашого досвіду.
- Не покладайтеся лише на ’bundle ID + ім’я’ — це гарантовано призводить або до false positives (видалили чуже), або до false negatives (лишили залишки).
- Перевіряйте свій підхід окремо на категоріях застосунків, де поведінка нестандартна: Adobe, Microsoft, продукти з частими ребрендингами, KEXT-розширення, продукти з App Store із складними Group Containers.
- Ніколи не видаляйте файли в системних директоріях без перевірки — ціною помилки може бути неможливість завантажити свій Mac.
- І не довіряйте ШІ як єдиному джерелу істини: він — добрий перший помічник, але без ручної перевірки на реальних застосунках ви будете знаходити помилки разом із користувачами, а не до них.
А як ви розв’язуєте подібні задачі?
Буду радий поспілкуватися в коментарях з тими, хто теж працює з macOS або системними утилітами. Цікаво почути ваш досвід:
- Як ви у своїх продуктах вирішуєте задачу пошуку «всього, що пов’язане зі застосунком», особливо для shared-ресурсів і динамічних шляхів?
- Чи стикалися ви з історіями, коли видалення на вигляд звичайного файлу клало macOS на старті? Які саме файли — лаунч-агенти, kext, щось інше?
- Чи бачите ви роль ШІ в системних утилітах саме як «асистента інженера», чи вірите, що тут можлива повна автоматизація? Якщо так, у яких саме сценаріях ви б їй довірилися?
Якщо тема вам цікава загалом, теж пишіть. Хочу розповідати більше про нашу внутрішню кухню, і ваші відгуки допомагають вибирати наступні теми.
Дякую за увагу.
11 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментаріввзяв за правило
не користуватися «клінерами», «опитмізаторами системи» і тд
користуватися менеджерами пакетів — brew для mac, сhoco або аналоги для win і тд
Цілком резонний підхід для розробницького воркфлоу, коли більшість софту ставиться через brew/choco, і Ви свідомо контролюєте, що саме потрапляє в систему.
Стаття все ж писалася з ширшою аудиторією на думці — звичайні користувачі macOS зазвичай ставлять додатки з .dmg, Mac App Store або напряму із сайтів розробників, де менеджери пакетів не задіяні. До того ж навіть brew uninstall —zap прибирає лише те, що мейнтейнер каска явно прописав у секції zap — тож і там бувають свої прогалини.
після того як вручну вичистив половину ssd поставив Pearcleaner
Pearcleaner — гідна open-source-опція, спільнота навколо нього активна. Цікаво, чи знаходить він зараз залишки від програм, які Ви вже видалили вручну до встановлення?
у нього є розділ orphans і щось знаходить. але там багато промахів.
проте основний режим — він ставить хук на видалення апок, і під час видалення дочищає хвости.
Дуже точне спостереження. Це, по суті, теза статті: повністю автоматичний пошук сирітських хвостів натикається на edge-cases, де без перевірки не обійтись.
Обидва механізми (і пошук залишків, і відстеження видалення апок) сьогодні мастхев для будь-якого нормального клінера. У App Cleaner & Uninstaller це теж є: при перетягуванні апки з Applications у кошик пропонуємо дочистити, а пошук залишків працює в межах сканування встановлених програм.
Питання в якості. Хибне визначення чогось чужого як «хвоста» є критичною помилкою, після якої довіра до інструмента губиться надовго. Тому за нашою базою правил стоять роки ручної верифікації і постійні оновлення, бо інакше тримати належну точність неможливо.
На жаль, Pearcleaner не мейнтейниться більше.
Я користуюсь Mole.
На вихідних грався з Pinokio — тула для запуску локальних ллм-ок.
Після видалення додатка, залишились копії всіх ллм-ок яких викачував 80+Гб.
mo не зміг знайти автоматично в чому проблема.
du наш найкращій друг.
Доброго дня! Дякую за коментар.
Ми свого часу розібрали саме цей нестандартний кейс і врахували його в App Cleaner & Uninstaller. Pinokio зберігає всі скрипти й моделі у спеціальних прихованих папках, і наш клінер знаходить їх разом із самою програмою.
До речі, цікаво — що саме Ви мали на увазі під «mo»?
github.com/tw93/mole
Mole, бінарник для запуску — mo.