Шлях додому

Це історія про PATH, /usr/local/bin і трохи про Homebrew.

У нашому проекті ми використовуємо власну систему Git сабмодулів під назвою system7. Насправді історія не про s7, це могло статися з будь-яким іншим внутрішнім бінарним інструментом, який у нас є.

s7 можна встановити двома способами: використовуючи скрипти для внутрішнього використання Readdle та використовуючи Homebrew. Це означає, що немає жодного відомого шляху, куди може бути встановлений бінарник s7.

s7 використовує Git хуки для максимальної автоматизації роботи. В ідеалі, якщо кінцевому користувачеві не потрібно перезв’язувати сабрепозиторії, йому не доведеться запускати s7 вручну взагалі. Для правильної роботи Git хуків, s7 має бути видимим з контексту, де запускається хук. Існує два таких контексти: CLI та GUI.

Історично, всі внутрішні інструменти Readdle встановлюються в “$HOME/bin”. Я не знав жодних причин для цього і просто слідував традиції. Я людина консолі, тому я тестував s7 в терміналі і все працювало чітко.

З першого дня стало очевидно, що більшість людей надає перевагу GUI інструментам для роботи з Git. Моя перша реакція була,– “Тоді додайте “$HOME/bin” до PATH?”. Виявилося, що додати щось до PATH для GUI додатків не так вже й легко на сучасній macOS.

Якщо вам цікаво, дефолтний PATH для GUI додатків виглядає так /usr/bin:/bin:/usr/sbin:/sbin. Багато додатків додають щось до PATH після запуску, наприклад, SourceTree додає шляхи до свого власного git-lfs, Xcode додає SDK/bin, тощо.

.bash_profile/.zshrc – не працюють для GUI додатків*. Мене іноді обманювала моя любов запускати Xcode використовуючи xed, або SourceTree використовуючи stree. GUI додатки, запущені з Finder, Dock, Spotlight, або через launch services не консультуються з shell профілями.

/etc/paths, /etc/paths.d/* – у мене була слабенька надія що це спрацює. Ні, не спрацює. Це ВИКОРИСТОВУЄТЬСЯ Terminal-ом, але GUI не консультується з цим.

launchctl setenv PATH VALUE – не працює конкретно для змінної PATH. Не знайшов цього в документації, але мої тести це підтверджують. Працює для власних змінних, таких як MY_TESTlaunchctl setenv MY_TEST VALUE працює, PATH – ніт. Не працює навіть якщо встановлено з ~/Library/LaunchAgents/*.plist. (ПРИМІТКА: Не перевіряв це, але люди на Stack Overflow пишуть що змінні встановлені через LaunchAgents не працюють для додатків запущених через повторне відкриття останніх відкритих вікон і для Preferences > Users & Groups > Login Items. Можливо варто перевірити якщо нам знадобиться інша змінна середовища колись).

Єдиний спосіб, який знайшли мої колеги, що працює – це запустити sudo launchctl config user path VALUE. Я не хотів, щоб користувачі довіряли мені права суперкористувача (навіть просто для встановлення) для роботи з s7 (враховуючи що sudo <something> буде прихованим в нутрощах скрипта встановлення / brew формули).

Я звернув свій погляд на стандартні директорії. /usr/local/bin зокрема. Xcode пропонує встановити CLI інструменти в /usr/local/bin за замовчуванням. На той час, Homebrew використовував встановлення в /usr/local/bin за замовчуванням. Це, принаймні, пропонувало уніфікацію шляху встановлення для наших внутрішніх скриптів і Homebrew. Дефолтний /etc/paths містить /usr/local/bin, тому не було потреби модифікувати PATH користувача для роботи з Terminal. Однак, використання /usr/local/bin не звільнило мене від потреби щось робити з PATH для GUI додатків.

Базуючись на нашому досвіді, не всі люди стежать за оновленнями внутрішніх утиліт (навіть якщо ми надсилаємо email на devlist про кожне оновлення). Люди забувають, вони зайняті, вони не люблять CLI, і не хочуть паритись. В залежності від утиліти це призводило до різних проблем. Наприклад, баг в інструменті локалізації, який був виправлений місяці тому, виринав в нашому проекті, бо хтось не потурбувався оновити необхідний інструмент. Щоб прибрати людський фактор, ми автоматизували процес встановлення та оновлення необхідних інструментів. У нас є Run Phase в нашому Xcode проекті, що оновлює наші внутрішні інструменти якщо необхідно. Я хотів тримати s7 актуальним на машині кожного інженера. Щоб Run Phase міг тихо встановлювати оновлення, директорія встановлення має бути доступною для запису для звичайного не-admin/не-sudo користувача. І /usr/local/bin мав саме такі права*.

Повертаючись до оригінальної проблеми з GUI додатками. s7 “використовується” GUI додатками непрямо через Git хуки, тому я придумав рішення: ми розміщуємо s7 в /usr/local/bin і хардкодимо /usr/local/bin/s7 у всіх s7 хуках. GUI додаткам не потрібно буде шукати в PATH, для щоденного використання в CLI не буде потреби модифікувати PATH користувача. Win-win! 🎉

Ох, який це був неправильний шлях…

s7 був написаний у час, коли мало людей мали M1 Mac. Ми знали що /usr/local/bin не існує на M1. Але причина не була зрозумілою. Коли з’явився M1, Homebrew почав використовувати /opt/homebrew/ для ARM, і /usr/local/bin для Intel архітектури. Що ж, це мало сенс для них 🤷‍♂️. Але для нас, з нашим хардкодженим /usr/local/bin в Git хуках… Ну, ми збираємо s7 на машині кожного користувача, тому не було потреби розділяти архітектури. До часу коли ми отримали наші перші M1 Mac, ми не хотіли змінювати наше рішення хардкодити /usr/local/bin в Git хуках: це б означало міграцію, потребу в новому PATH, або якийсь динамічний пошук s7. Ми не хотіли всієї цієї тягомотини, тому ми проголосили, що кожен хто має M1 повинен зробити наступне:

 sudo mkdir -p /usr/local/bin
 sudo chown "$USER" /usr/local/bin

Це імітувало права на /usr/local/bin які ми мали на Intel машинах.

Минуло три роки з рішенням жити в /urs/local/bin.

Поки я не наткнувся на цю статтю.

Мене вкрило холодним потом.

Своїми власними руками я зробив команду з близько 30 інженерів вразливою до підробки sudo.

А потім мене накрило… У мене є Homebrew версія Git на моїй машині. Щоб її використовувати, я додав /opt/homebrew/bin на початку мого PATH. А які права /opt/homebrew/bin? owner = $USER, 775. Ну добре, я зробив це своїми власними руками знову. Я вирішив перевірити що інтернет має сказати за це. Звісно, ви знайдете ту саму відповідь,– “додай. спереду. /opt/homebrew/bin. до. твого. PATH.”. Ще гірше. Подивіться що робить /opt/homebrew/bin/brew shellenv – воно… додає спереду /opt/homebrew/bin до вашого PATH.

Звісно, коли я вирішив використовувати /usr/local/bin, права цієї директорії були модифіковані Homebrew. Звісно, я теж винен. Після прочитання статті, кожен шматочок пазлу, який я знав раніше, просто склав цілу картину.

Наші попередники, які обрали “$HOME/bin”, мабуть щось знали. Ми повертаємось додому.

Мої висновки (які я боюсь можу знайти неправильними в майбутньому): 0. якщо постраждали, відновіть права “/usr/local/bin” до заводських налаштувань. Ну, він не існує за замовчуванням :) Тож, якщо можливо – видаліть його. Якщо абсолютно необхідно: /usr/bin/sudo chown root:wheel /usr/local/bin і /usr/bin/sudo chmod o+rx /usr/local/bin

  1. завжди використовуйте повний шлях принаймні для “/usr/bin/sudo”
  2. ніколи не додавайте спереду будь-яку директорію доступну для запису користувачем до вашого PATH
  3. якщо ви хочете перезаписати будь-яку утиліту з /usr/bin (наприклад, git) – зробіть симлінк в /usr/local/bin. Дивіться рекомендовані права цієї директорії в (0). На всяк випадок – ви не можете перезаписати нічого в /usr/bin на сучасній macOS