Оновлення: цей допис викликав жваві дискусії. Спершу я написав окремий текст із продовженням, але згодом вирішив зібрати все в одному місці. Тож я додав до цієї статті розділ, де розглядаю аргументи, що з'явилися в мережі.
Нещодавно я натрапив на блог про те, що package managers — це зло. Я не зовсім згоден із таким радикальним твердженням, адже вони вирішують проблеми, з якими важко впоратися вручну. Проте мені стало цікаво провести невеликий експеримент.
У роботі я зустрів проект, що потребував paragonie/sodium_compat. Це логічно: пакет додає підтримку функцій sodium, які з’явилися «з коробки» лише в PHP 7.2. Але був нюанс: мінімальною вимогою самого проекту вже був PHP 7.3! Отже, хоча paragonie/sodium_compat встановлювався щоразу, він фактично був непотрібним. Я створив issue, і зрештою розробники видалили цю залежність.
Сама по собі зайва залежність не є критичною проблемою. Але з огляду на останні інциденти в NPM виникає питання: наскільки ми усвідомлюємо, від якого коду залежимо? Чи не занадто сліпо ми довіряємо всьому, що підтягує менеджер пакетів?
Я написав скрипт для сканування проектів, що використовують популярні polyfills та compatibility packages:
paragonie/sodium_compatparagonie/random_compatsymfony/polyfill-php70symfony/polyfill-php70symfony/polyfill-php71symfony/polyfill-php72symfony/polyfill-php73symfony/polyfill-php80symfony/polyfill-php81symfony/polyfill-php82symfony/polyfill-php83symfony/polyfill-php84
Кожного разу, коли проект вимагав один із цих пакетів без реальної потреби (згідно з даними Packagist та вимогами до версії PHP), я це фіксував.
Із 1554 просканованих проектів 229 мали зайві залежності. Це близько 15%. Я надіслав PR кожному з них, щоб виправити це.
Звісно, це лише невелика частина екосистеми PHP. Проте цифра достатньо значуща, щоб замислитися: чи достатньо ми перевіряємо код, на який покладаємося? Я не вважаю, що package managers — це зло, але вони точно роблять нас дещо… ледачими. Можливо, нам варто приділяти більше часу розумінню того, що саме ми встановлюємо?
Інший приклад — symfony/polyfill-mbstring. Він є чи не в кожному PHP проекті. Я навіть не знаю, звідки він береться, поки не запущу composer why (до речі, дуже корисна команда). Ми просто звикли: якщо ти працюєш із PHP, symfony/polyfill-mbstring десь та й випливе. Я не зміг додати його у свій скрипт, оскільки він замінює опціональне розширення PHP і не прив’язаний до конкретної версії мови (хоча, чесно кажучи, mbstring зазвичай встановлено за замовчуванням у більшості середовищ).
Утім, є спосіб відмовитися від таких пакетів: додати конфігурацію replace у ваш composer.json:
{ "replace": { "symfony/polyfill-php54": "*", "paragonie/sodium_compat": "*", "symfony/polyfill-mbstring": "*" } }
Це вказує composer, що ваш проект уже містить ці пакети, тому їх не потрібно завантажувати. Це трохи незручно, і більшість розробників не переймаються такими налаштуваннями. Але без цього трюку ви майже гарантовано отримаєте ці залежності, бо в дереві проектів завжди знайдеться щось, що посилається на symfony/polyfill-mbstring.
Чи є в цьому проблема? Поки що ні. Хіба що одного дня станеться supply-chain attack, які ми вже не раз бачили в екосистемі NPM. Зручно мати polyfill про запас, але чи це вагома причина для ризику?
Можливо, нам час почати дбати про «гігієну залежностей»? Діліться думками в коментарях або в Discord!
Продовження
Після публікації допису я отримав багато коментарів. Хочу відповісти на основні тези тут.
Щодо sodium зокрема
Більшість дискусій зосередилася саме на paragonie/sodium_compat. Шкода, що це відволікло від основної ідеї, і частково це моя провина. Я спочатку не врахував, що хоча ext-sodium є вбудованим розширенням з версії PHP 7.2, він не завжди увімкнений за замовчуванням у Linux та Windows. Чому саме так — питання відкрите.
Хоча мій аргумент про гігієну залежностей залишається чинним, із paragonie/sodium_compat ситуація складніша. Як розробник open source, я все ж вважаю, що краще дати користувачеві можливість самому вирішувати на рівні системи, чи використовувати ext-sodium, чи paragonie/sodium_compat, замість того, щоб бібліотека примусово завантажувала сторонній пакет. Трюк із replace працює, але він не є обов’язковим, і на практиці мало хто ним користується.
Можливості composer
Багато хто запропонував розширити схему composer.json, щоб пакет міг сам оголошувати себе зайвим при виконанні певних системних вимог. Це могло б виглядати так:
{ "name": "paragonie/sodium_compat", # … "replaced-by": { "ext-sodium": "*", "php": "^7.2" } }
Зауважу: зараз у composer такої можливості немає. Ідея цікава, хоча можуть бути причини, чому її важко реалізувати. Я планую розпитати про це Jordi (автора Composer).
Про гігієну пакетів
Моє головне побоювання — це надлишковий код на мільйонах серверів, де він не потрібен (paragonie/sodium_compat має близько 140 тисяч завантажень щодня). Сьогодні це не проблема, але так само було і з пакетами NPM до того, як вони стали векторами атак. Ми не повинні ставитися до цього легковажно.
На мою думку, проблема поглиблюється тим, що бібліотеки додають ці пакети в залежності не замислюючись. Потім ці бібліотеки стають частиною інших проектів, і в результаті «залежність залежності» опиняється там, де їй не місце. «Хороша гігієна» — це не нав'язувати polyfills на рівні бібліотек, а дозволяти кінцевому розробнику обирати найкращий підхід для його системи.