Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

เชี่ยวชาญ Ruby Dependency Management: Rbenv, RubyGems และ Bundler ทำงานร่วมกันอย่างไร

การจัดการ dependencies ใน Ruby ปกติเกี่ยวข้องกับการระบุ Ruby และ gem versions ที่โปรเจกต์พึ่งพา จากประสบการณ์ทำงานกับ Ruby การ debug dependencies เป็นหนึ่งใน biggest challenges ของผม Failures ไม่ค่อยพบ เพราะหลายอย่าง "just work" แต่เมื่อผิดพลาด มักจะ debug และ fix ยากเกินจำเป็น ในบทความนี้ ผมจะวางชิ้นส่วนต่างๆ ที่เกี่ยวข้องกับ dependency management ใน Ruby เพื่อช่วย debug ปัญหาแปลกๆ เมื่อเกิดขึ้น

เชี่ยวชาญ Ruby Dependency Management: Rbenv, RubyGems และ Bundler ทำงานร่วมกันอย่างไร

Ruby Code Loading

โดย default Ruby language มีสอง major methods สำหรับโหลดโค้ดที่ define ที่อื่น: load & require

load 'json.rb' require 'json.rb' require_relative 'json.rb' 

ทั้งสอง loading methods ยอมรับ absolute และ relative paths เป็น arguments อย่างไรก็ตาม มี two differentiating factors:

  1. Multiple calls to load จะ re-execute file ในขณะที่ multiple calls to require จะไม่ re-execute file แต่จะ return false
  2. Calls to load resolves เฉพาะ absolute และ relative paths Calls to require ตรวจสอบ $LOAD_PATH เมื่อ path ไม่ resolve ไป absolute path

Variant ที่สามคือ require_relative ซึ่งใช้ relative paths เพื่อ require code relative ต่อ current file's location แทนที่จะเป็น Ruby process' working directory

Rbenv

Version manager คือ tool ใช้ manage และ switch ระหว่าง versions ของ interpreter (ในกรณีนี้ Ruby) และระบุ location หา gems สำหรับโปรเจกต์ Version managers เป็น tools ที่ language agnostic ส่วนใหญ่ และภาษาต่างๆ มี implementations ของตัวเอง เช่น Nvm, n สำหรับ Node.js, pyenv สำหรับ Python และ Rbenv, rvm, chruby สำหรับ Ruby ตอนนี้ลอง rbenv ดู

Install Ruby Version

เราใช้คำสั่ง rbenv install ติดตั้ง Ruby version ใดๆ:

# Install ruby 2.6.1 $ rbenv install 2.6.1 Downloading openssl-1.1.1i.tar.gz... -> https://dqw8nmjcqpjn7.cloudfront.net/e8be6a35fe41d10603c3cc635e93289ed00bf34b79671a3a4de64fcee00d5242 Installing openssl-1.1.1i... Installed openssl-1.1.1i to /home/directory/.rbenv/versions/2.6.1 Downloading ruby-2.6.1.tar.bz2... -> https://cache.ruby-lang.org/pub/ruby/2.6/ruby-2.6.1.tar.bz2 Installing ruby-2.6.1... ruby-build: using readline from homebrew Installed ruby-2.6.1 to /home/directory/.rbenv/versions/2.6.1 # Check Installation $ rbenv versions # Shows all versions installed. system 2.6.1 # Lookup versions available for installation $ rbenv install -L 1.8.5-p52 1.8.5-p113 1.8.5-p114 ... 2.7.0-rc1 2.7.0-rc2 2.7.0 ... truffleruby+graalvm-20.1.0 truffleruby+graalvm-20.2.0 truffleruby+graalvm-20.3.0 # Full list ประมาณ 500 versions scroll ทั้งหมดเยอะมาย # คำสั่งด้านล่างเป็น shortcut หา version เฉพาะด้วย fzf $ rbenv install `rbenv install -L | fzf` 

Switch Between Versions (สลับระหว่าง versions)

มีหลายวิธีระบุวิธีสลับ versions ของ Ruby rbenv ทำดังนี้:

  • Check RBENV_VERSION
  • ค้นหา file .ruby-version ใน script's directory และ parent จนถึง root directory
  • ค้นหา file .ruby-version ใน $PWD และ parent directories จนถึง root directory
  • ใช้ global file ~/.rbenv/version

Priority จากบนลงล่าง ~/.rbenv/version เป็น fallback สุดท้าย และถือว่า global version ดูด้านล่าง:

# Inside First Project Root # Select ruby version for project $ touch .ruby-version && echo "2.7.1" >> .ruby-version # Verify selected version $ ruby --version ruby 2.7.1p83 (2020-03-31 revision a0c7c23c9c) [x86_64-darwin20] # Result $ rbenv version 2.7.1 (set by /path/to/current/directory/.ruby-version) # Result # Change selected version $ : >> .ruby-version && echo "2.6.1" >> .ruby-version # Verify selection change $ ruby --version ruby 2.6.1p33 (2019-01-30 revision 66950) [x86_64-darwin20] # Result $ rbenv version 2.6.1 (set by /path/to/current/directory/.ruby-version) # Change selection with RBENV_VERSION while .ruby-version is present $ export RBENV_VERSION=2.5.1 # Verify selection change # .ruby-version is ignored. $ ruby --version ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-darwin20] # Result $ rbenv version 2.5.1 (set by RBENV_VERSION environment variable) # Result # Change to a version that is not installed & remove RBENV_VERSION $ unset RBENV_VERSION & : >> .ruby-version && echo "2.4.1" >> .ruby-version # Verify selection change $ ruby --version rbenv: version `2.4.1' is not installed (set by full/path/to/current/directory/.ruby-version) # Result 

Shims และ Rehashing

สองแนวคิดนี้ต้องเข้าใจถูกต้องเพื่อ debug rbenv ได้มีประสิทธิภาพ

Shims คือ lightweight bash scripts ที่อยู่ใน PATH เพื่อ intercept commands และ route ไป version ที่เหมาะสมสำหรับ execution ใน high level ทุก command (เช่น rspec) ถูกแปลงเป็น rbenv exec rspec ดูรายละเอียดด้านล่าง

ก่อนอื่น rbenv สร้าง shim สำหรับทุก commands (rspec, bundle ฯลฯ) ทุก installed Ruby versions เพื่อ intercept calls ไป CLI ไม่สน version shims เหล่านี้อยู่ที่ ~/.rbenv/shims ทุก shim มี bash script เหมือนกัน ดูด้านล่าง:

#!/usr/bin/env bash set -e [ -n "$RBENV_DEBUG" ] && set -x program="${0##*/}" if [ "$program" = "ruby" ]; then for arg; do case "$arg" in -e* | -- ) break ;; */* ) if [ -f "$arg" ]; then export RBENV_DIR="${arg%/*}" break fi ;; esac done fi export RBENV_ROOT="/home/directory/.rbenv" exec "/usr/local/Cellar/rbenv/1.1.2/libexec/rbenv" exec "$program" "$@" 

ต่อไป script ข้างบนแปลประมาณว่า:

  • ถ้า program name เป็น ruby พร้อม argument -e
    • แปลงเป็น rbenv exec ruby <args>
  • ถ้า program name เป็น ruby พร้อม path ไป script
    • Set RBENV_DIR เป็น script's directory นี่ enable rbenv ค้นหา .ruby-version ใน script's directory ก่อน $PWD หาก .ruby-version ระบุในทั้งสองที่ rbenv เลือก script's directory
  • ถ้า program name ไม่ใช่ Ruby
    • แปลงเป็น rbenv exec <program-name> <args>

สุดท้าย rbenv exec <command-name> <args> ระบุ version ที่ถูกต้องส่ง command โดย check RBENV_VERSION environment variable จำได้ว่า RBENV_VERSION ถูก set โดย algorithm ด้านบน

Shims บน PATH ต้อง prepended นี่ ensure พวกมันเป็น first point of contact สำหรับ Ruby executables และ intercept ได้ถูกต้อง วิธีดีที่สุดที่ผมพบเข้าใจ PATH setup และรู้ว่า shims intercept ถูกต้องคือ:

$ which -a bundle /path/to/home/.rbenv/shims/bundle /usr/bin/bundle 

which -a bundle: นี้ naive look through PATH และ print out ในลำดับที่เจอ locations ที่ bundle หาได้ หากมีอะไร print ก่อน ~/.rbenv/shims หมายถึง shims ของคุณไม่ set up ถูกต้อง rbenv which bundle จะไม่ reveal เพราะคำสั่งทำงานใน context ของ rbenv ไม่ได้ search PATH

Rehashing คือกระบวนการสร้าง shims เมื่อคุณ newly install Ruby gem ที่ให้ executable เช่น rspec คุณต้องรัน rbenv rehash เพื่อสร้าง shim เพื่อที่ subsequent calls to rspec จะถูก intercept โดย rbenv และส่งต่อไป version ของ Ruby ที่เหมาะสม

RubyGems

ต่อไปคือ RubyGems มีให้จาก official Ruby site RubyGems เป็น Ruby packaging system ออกแบบมาเพื่อ facilitate การสร้าง sharing และ installation ของ libraries ในบางแง่มุม มันคือ distribution packaging system คล้าย apt-get แต่ target Ruby software RubyGems เป็น de-facto method สำหรับ sharing gems พวกมันมักติดตั้งที่ ~/.rbenv/versions/{version-number}/lib/ruby/gems/{minor-version}/ หรือ variant ขึ้นอยู่กับ version manager ที่ใช้ Ruby's default required method Kernel.require ไม่มี mechanism ใดๆ โหลด gems จาก Gems installation directory RubyGems monkey-patches Kernel.require เพื่อ

  • ก่อนอื่น ค้นหา gems ใน $LOAD_PATH
  • หากไม่เจอ ค้นหา gems ใน GEMS INSTALLATION DIRECTORY
    • เมื่อเจอ เพิ่ม path เข้า $LOAD_PATH

นี่ทำงาน "natively" เพราะ Ruby มาพร้อม RubyGems โดย default ตั้งแต่ version 1.9 Ruby versions ก่อนหน้านี้ต้องติดตั้ง RubyGems เอง แม้ทำงาน natively แตสำคัญที่จะรู้ difference นี้เมื่อ debugging

Gem คือชุดโค้ดที่เกี่ยวข้องกันใช้แก้ปัญหาเฉพาะ Install gem และ get information เกี่ยว gem environment ดังนี้:

$ gem install gemname 
$ gem env RubyGems Environment: - RUBYGEMS VERSION: 3.1.2 - RUBY VERSION: 2.7.1 (2020-03-31 patchlevel 83) [x86_64-darwin20] - INSTALLATION DIRECTORY: /path/to/home/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0 - USER INSTALLATION DIRECTORY: /path/to/home/.gem/ruby/2.7.0 - RUBY EXECUTABLE: /path/to/home/.rbenv/versions/2.7.1/bin/ruby - GIT EXECUTABLE: /usr/bin/git - EXECUTABLE DIRECTORY: /path/to/home/.rbenv/versions/2.7.1/bin - SPEC CACHE DIRECTORY: /path/to/home/.gem/specs - SYSTEM CONFIGURATION DIRECTORY: /path/to/home/.rbenv/versions/2.7.1/etc - RUBYGEMS PLATFORMS: - ruby - x86_64-darwin-20 - GEM PATHS: - /path/to/home/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0 - /path/to/home/.gem/ruby/2.7.0 - GEM CONFIGURATION: ... - REMOTE SOURCES: - https://rubygems.org/ - SHELL PATH: - /path/to/home/.rbenv/versions/2.7.1/bin 

RubyGems แก้ปัญหานี้อย่างไร? มัน monkey patches Kernel's require system ด้วย require method ของตัวเอง ด้วยสิ่งนี้เมื่อ require honeybadger ถูกเรียก มัน search through gems folder หา honeybadger.rb และ activate gem เมื่อเจอ

ตัวอย่าง require 'honeybadger' ผลิตออกมาแบบนี้:

  • spec = Gem::Specification.find_by_path('honeybadger')
  • spec.activate

Activate gem หมายถึง put it ใน $LOAD_PATH RubyGems ยังช่วย download all of a gem's dependencies ก่อน download gem เอง

เพิ่มเติม Rubygems ship พร้อม feature ให้ open associated gem's directory ด้วย gem open <gem-name> ตัวอย่าง

เชี่ยวชาญ Ruby Dependency Management: Rbenv, RubyGems และ Bundler ทำงานร่วมกันอย่างไร

นี่ช่วยให้เราหา/trace specific version ของ gem ที่ app เรา reference ได้ง่าย

Bundler

ใน layer นี้ Bundler ช่วยให้เราระบุ project dependencies ทั้งหมดได้ง่าย และ optionally ระบุ version สำหรับแต่ละตัว จากนั้นมัน resolve gems ของเรา รวมถึง install มันและ dependencies มัน การสร้าง real-world applications pre-bundler มาพร้อม challenges หลายอย่าง เช่น:

  • Applications ของเรามี dependencies จำนวนมาก และ dependencies เหล่านี้มี dependencies อื่นๆ อีก และ versions ของพวกมัน การติดตั้ง wrong version ของ gem หนึ่งจะทำให้ app แตกง่าย และ fix ปัญหานี้เกี่ยวกับน้ำตาเยอะ
  • สอง dependencies ของเราอาจ refer ไป same third-level dependency การหา compatibility เป็น issue และถ้ามี compatibility ใดๆ ก็เป็นปัญหา
  • ที่เรามี multiple applications บนเครื่องเดียวกัน พร้อม dependencies ต่างๆ application ของเราสามารถ access gems ใดๆ ที่ติดตั้งบนเครื่อง ซึ่งขัดหลักการ least privilege และ expose application ให้ gems ทั้งหมดบนเครื่อง ไม่สนว่าจะ malicious หรือไม่

Bundler แก้ปัญหาทั้งสามและให้ sane way จัดการ app dependencies โดยทำดังนี้

Bundler resolves dependencies และ generates lockfile:

# Gemfile gem 'httparty' 

ถ้าเรารัน bundle หรือ bundle install มันจะ generate lockfile:

GEM specs: httparty (0.18.1) mime-types (~> 3.0) multi_xml (>= 0.5.2) mime-types (3.3.1) mime-types-data (~> 3.2015) mime-types-data (3.2020.1104) multi_xml (0.6.0) PLATFORMS ruby DEPENDENCIES httparty BUNDLED WITH 2.1.4 

จากข้างต้น bundler generate version ของ httparty ที่จะติดตั้ง รวมถึง dependencies ของมันใน Gemfile.lock. File นี้คือ blueprint ของ app dependencies และควร check into version control มัน ensure ว่า project dependencies consistent across environments (development, staging, production)

Bundler resolves compatibility among dependencies

มัน resolve dependencies สำหรับ httparty โดยหา suitable version สำหรับ dependencies ของมันและระบุไว้ Bundler ยังพยายาม resolve dependencies ระหว่าง gems ตัวอย่าง

# Gemfile gem 'httparty' # That relies on gem 'mime-types', '>= 3.0.1, < 4.0.1' gem 'rest-client' # That relies on gem 'mime-types', '>= 2.0.1, < 3.0' 

ตัวอย่างข้างต้น arbitrary และจะได้ error เช่น:

Bundler could not find compatible versions for gem "mime-types": In Gemfile: httparty was resolved to 0.18.1, which depends on mime-types ('>= 3.0.1, < 4.0.1') rest-client was resolved to 2.0.4, which depends on mime-types ('>= 2.0.1, < 3.0') 

เพราะสอง gems มี dependencies ที่ไม่ compatible และไม่สามารถ auto resolve ได้

Bundler restrict access ไป gems ที่ติดตั้งแต่ไม่ได้ระบุใน Gemfile

ใน sample gemfile เช่น:

# Gemfile gem 'httparty' # irb require 'rest-client' # raises LoadError (cannot load such file -- rest-client) 

มัน ensure ว่าแค่ dependencies ที่ระบุใน Gemfile เท่านั้นที่ project ของเราสามารถ require ได้

Bundle exec

ตอนรัน rspec ใน project directory มี possibility รัน version อื่นที่ไม่ได้ระบุใน Gemfile เพราะ most recent version จะถูก select รัน vs version ใน Gemfile bundle exec rspec ensure rspec รันใน context ของโปรเจกต์ (i.e. gems ที่ระบุใน Gemfile)

Bundle binstubs

บ่อยครั้งเราอ่าน articles ที่รัน commands เช่น ./bin/rails คำสั่งนี้คล้าย bundle exec rails Binstubs คือ wrappers รอบ Ruby executables เพื่อลดความยุ่งยากของ bundle exec

เพื่อ generate binstub รัน bundle binstubs gem-name นี่สร้าง binstub ใน ./bin folder แต่ configure ได้ด้วย --path directory ถ้าตั้งค่า

References (อ้างอิง)

เพื่อเรียนรู้เพิ่มเติม ดู references เหล่านี้:

  • How Do Gems Work?
  • Rbenv
  • RubyGems
  • Bundler