Вы когда-нибудь задумывались, почему bundle install иногда работает как швейцарские часы, а иногда — как советский трактор в -30°C? Сегодня разберёмся, как устроены Ruby-гемы изнутри, почему ваш gemspec внезапно сломал production, и как Bundler решает головоломку зависимостей.
🧩 Что такое RubyGem?
RubyGem — это упакованный кусок кода, который можно:
✅ Подключить в проект одной строчкой
✅ Версионировать
✅ Распространять через rubygems.org
Гемом может быть что угодно: от утилиты для генерации QR-кодов до целого фреймворка вроде Rails.
Но под капотом — это просто архив с кодом + метаданные.
📦 Анатомия гема
Стандартная структура после bundle gem my_gem:
my_gem/
├── lib/
│ ├── my_gem.rb # Главный файл
│ └── my_gem/ # Доп. модули
├── spec/ # Тесты
├── Gemfile # Зависимости для разработки
├── my_gem.gemspec # Метаданные гема
└── README.md # Документация
Сердце гема — .gemspec. Без него ваш код останется просто папкой на GitHub.
🔧 Gemspec: инструкция по сборке
Пример минимального my_gem.gemspec:
Gem::Specification.new do |s|
s.name = "my_gem"
s.version = "0.1.0"
s.summary = "Делает магию"
s.authors = ["Вася Пупкин"]
s.files = Dir["lib/**/*.rb"] # Важно! Иначе код не попадёт в гем
s.license = "MIT"
end
Частые ошибки:
- Забыли
s.files→ гем установится, но код не загрузится - Жёстко зафиксировали зависимости (
s.add_dependency "rails", "7.0.8") → конфликты в большом проекете - Не указали
s.required_ruby_version→ гем сломается на старых Ruby
🧠 Bundler: дирижёр зависимостей
Bundler решает NP-полную задачу (да, серьёзно!) — подбирает версии гемов так, чтобы:
- Все зависимости были удовлетворены
- Не было конфликтов
- Использовались максимально свежие версии
Когда bundle install работает 5 минут — он перебирает тысячи комбинаций.
💀 Реальные боли
1. Волосатый граф зависимостей
Bundler could not find compatible versions for gem "railties":
In Gemfile:
rails (~> 7.0) was resolved to 7.0.8, which depends on
railties (= 7.0.8)
devise was resolved to 4.9.3, which depends on
railties (>= 6.1.0)
Решение:
- Используйте
bundle update --conservative devise - Или явно укажите версию в
Gemfile:gem "devise", "~> 4.9"
2. Гем-призрак
После удаления гема из Gemfile он остаётся в Gemfile.lock.
Фикс:
bundle clean --force
3. Локальная разработка
Хотите тестировать гем прямо в своём проекете?
# Gemfile
gem "my_gem", path: "../my_gem"
🛠️ Антипаттерны
- Гем-монстр
- 50 зависимостей
- 10 MB кода
- А нужен только один метод
→ Разбивайте на мелкие гемы!
-
Динамический
gemspecs.version = `git describe --tags`.strip # А если git нет в production? - Игнорирование
Gemfile.lockв геме- Храните его в git!
- Иначе сборка может сломаться в любой момент.
🎓 Продвинутые трюки
1. Условные зависимости
# gemspec
s.add_development_dependency "sqlite3" # Для тестов
if RUBY_VERSION >= "3.1"
s.add_dependency "psych", ">= 4.0"
end
2. Скрываем ненужные файлы
s.files -= Dir["test/**/*"] # Не включаем тесты в релиз
3. Собственные источники гемов
# Gemfile
source "https://gems.my-company.com" do
gem "internal_gem"
end
🧪 Тестируем гем правильно
- Используйте
appraisalдля тестов с разными версиями зависимостей:
# Appraisals
appraise "rails-7" do
gem "rails", "~> 7.0"
end
- Автоматизируйте релиз с
gem-release:
gem bump -v minor && gem release
🏁 Вывод
- Gemspec — это паспорт гема. Без него ваш код не попадёт в rubygems.org.
- Bundler — сложный, но умный. Не ругайте его за медлительность — он решает задачу уровня “собрать кубик Рубика вслепую”.
- Локальная разработка гемов требует аккуратности. Тестируйте в условиях, близких к production.
P.S. Если bundle install снова завис — попробуйте сначала выпить кофе. Иногда помогает. ☕