Basit işlerin otomasyonu: Rakefile
Makefile değil Rakefile
Özellikle C
ile uğraşanların yakından tanıdığı Makefile
, zamanının en çok
işe yarayan araçlarındandı. Halen de çok kullanılır. Peki daha basit ve
rahat bir alternatifi var mı?
Evet, bence var! Büyük usta, toprağı bol olsun Jim Weirich
tarafından bize armağan edilen bir Ruby gem’idir rake
. Eğer macOS
kullanıyorsanız, ruby
ve rake
içinde birlikte geldiği için hiçbir ilave
kuruluma ihtiyaç duymuyorsunuz.
Ben şanslıyım, genelde çalıştığım ekiplerde hep macOS kullandığımız için,
hiçbir ilave iş yapmadan Rakefile
ile kolay otomasyon yapabiliyoruz.
Eğer sisteminizde ruby
yoksa, önce ruby
kurmalı, sonrasında da gem install rake
ile ek bir kurulum daha yapmanız lazım:
# ubuntu kullanıcıları için herşey dahil kurulum
$ sudo apt-get update -y
$ sudo apt-get install -y ruby-full
İlk Task’imiz
rake
bir task çalıştırıcı. Rakefile
da bu işleri yani task’leri
tanımladığımız yer. Şimdi aşağıdaki kodu Rakefile
adıyla kaydedip çalıştıralım:
desc "Merhaba komutu"
task :merhaba do
puts "Merhaba"
end
Şimdi task’leri listeleyelim:
$ rake -T
rake merhaba # Merhaba komutu
Çalıştırmak için:
$ rake merhaba
Merhaba
Bu kadar basit… Peki daha başka neler var?
Aynı Makefile
daki gibi, bir task’i çalıştırmadan önce başka task’leri
çağırabiliriz. Örneğin, environment variable GO_ENV
set edilmemişse,
hatta bu değişkenin değeri development
değilse bu task çalışmasın:
task :check_env do
abort "lütfen GO_ENV i tanımlayın" unless ENV['GO_ENV']
abort "GO_ENV değeri development olmalı" unless ENV['GO_ENV'] == 'development'
end
desc "Merhaba komutu"
task :merhaba => [:check_env] do
puts "Merhaba"
end
Haydi çalıştıralım:
$ rake merhaba
lütfen GO_ENV i tanımlayın
$ GO_ENV=1 rake merhaba
GO_ENV değeri development olmalı
$ GO_ENV=development rake merhaba
Merhaba
Peki acaba parametre geçsek ve bize “Merhaba <İSİM>” şeklinde çıktı verse:
task :check_env do
abort "lütfen GO_ENV i tanımlayın" unless ENV['GO_ENV']
abort "GO_ENV değeri development olmalı" unless ENV['GO_ENV'] == 'development'
end
desc "Merhaba komutu"
task :merhaba, [:isim] => [:check_env] do |t, args|
puts "Merhaba #{args.isim}"
end
çalıştıralım:
$ GO_ENV=development rake merhaba["vigo"]
Merhaba vigo
Burada küçük bir hatırlatma. Eğer shell olarak zsh
kullanıyorsanız, köşeli
parantezleri escape etmeniz gerekiyor, yani komutu:
$ GO_ENV=development rake merhaba\["vigo"\]
şeklinde çağırmanız lazım. Ben çok uzun yıllardan beri bash
kullandığım için
böyle bir sıkıntım yok :)
Peki, eğer isim geçilmemişse hata verelim:
task :check_env do
abort "lütfen GO_ENV i tanımlayın" unless ENV['GO_ENV']
abort "GO_ENV değeri development olmalı" unless ENV['GO_ENV'] == 'development'
end
desc "Merhaba komutu"
task :merhaba, [:isim] => [:check_env] do |t, args|
abort "lütfen isim girin" unless args.isim
puts "Merhaba #{args.isim}"
end
çalıştıralım:
$ GO_ENV=development rake merhaba
lütfen isim girin
Peki, abort
yerine, varsayılan bir isim versek?
task :check_env do
abort "lütfen GO_ENV i tanımlayın" unless ENV['GO_ENV']
abort "GO_ENV değeri development olmalı" unless ENV['GO_ENV'] == 'development'
end
desc "Merhaba komutu"
task :merhaba, [:isim] => [:check_env] do |t, args|
# abort "lütfen isim girin" unless args.isim
args.with_defaults(isim: "isimsiz")
puts "Merhaba #{args.isim}"
end
çalıştıralım:
$ GO_ENV=development rake merhaba
Merhaba isimsiz
Şimdi işin esas güzel yanı, task’in içinden shell komutları çağırmak. Yeni
bir task ekleyelim, bize directory list yani ls -al
versin ?
task :check_env do
abort "lütfen GO_ENV i tanımlayın" unless ENV['GO_ENV']
abort "GO_ENV değeri development olmalı" unless ENV['GO_ENV'] == 'development'
end
desc "Merhaba komutu"
task :merhaba, [:isim] => [:check_env] do |t, args|
# abort "lütfen isim girin" unless args.isim
args.with_defaults(isim: "isimsiz")
puts "Merhaba #{args.isim}"
end
desc "Directory listele"
task :ls do
system "ls -al"
end
Önce neler var listeleyelim;
$ rake -T
rake ls # Directory listele
rake merhaba[isim] # Merhaba komutu
şimdi;
$ rake ls
Gerçek Hayat Örneği
Bir Python/Django projesinde sıkça tekrarladığım işler var. Örneğin yeni bir
model eklediğimde bu modelin migration dosyasının oluşturulması. Bazen migration
içinde geri dönmem gerekiyor, yani sürekli uzun uzun python manage.py ...
bir
süre şey yazmam lazım.
Peki ben ne yapıyorum?
task :check_development_environment do
abort "Set DJANGO_SECRET_KEY variable! via export DJANGO_SECRET_KEY=..." unless ENV['DJANGO_SECRET_KEY']
abort "Set DJANGO_ENV variable! via export DJANGO_ENV=..." unless ENV['DJANGO_ENV']
abort "Set DATABASE_URL variable! via export DJANGO_ENV=..." unless ENV['DATABASE_URL']
end
namespace :db do
desc "Run migration for given database (default: 'default')"
task :migrate, [:database] => [:check_development_environment] do |_, args|
args.with_defaults(:database => "default")
puts "Running migration for: #{args.database} database..."
system "python manage.py migrate --database=#{args.database}"
end
desc "run database shell ..."
task :shell => [:check_development_environment] do
system "python manage.py dbshell"
end
desc "show migrations for an application (default: 'all')"
task :show, [:name_of_application] => [:check_development_environment] do |_, args|
args.with_defaults(:name_of_application => "all")
single_application_or_all = " #{args.name_of_application}"
single_application_or_all = "" if args.name_of_application == "all"
system "python manage.py showmigrations#{single_application_or_all}"
end
desc "update migration (name of application, name of migration?, is empty?)"
task :update, [:name_of_application, :name_of_migration, :is_empty] => [:check_development_environment] do |_, args|
abort "Please provide: 'name_of_application'" unless args.name_of_application
args.with_defaults(:name_of_migration => "auto_#{Time.now.strftime('%Y%m%d_%H%M')}")
args.with_defaults(:is_empty => "no")
name_param = "--name #{args.name_of_migration}"
empty_param = ""
unless args.is_empty == "no"
empty_param = "--empty #{args.name_of_application} "
end
system "python manage.py makemigrations #{empty_param}#{name_param}"
end
desc "roll-back (name of application, name of migration)"
task :roll_back, [:name_of_application, :name_of_migration] => [:check_development_environment] do |_, args|
abort "Please provide: 'name_of_application'" unless args.name_of_application
args.with_defaults(:name_of_migration => nil)
which_application = args.name_of_application
which_application = "" if args.name_of_application == "all"
if args.name_of_migration.nil?
puts "Please select your migration:"
system "python manage.py showmigrations #{which_application}"
else
system "python manage.py migrate #{which_application} #{sprintf("%04d", args.name_of_migration)}"
end
end
desc "fill database"
task :fill => [:check_development_environment] do
system "python manage.py fill"
end
desc "drop database"
task :drop, [:db] => [:check_development_environment] do |_, args|
args.with_defaults(:db => "YYYY")
system "dropdb --if-exists #{args.db} && echo #{args.db} dropped..."
end
desc "create database"
task :create, [:db, :username] => [:check_development_environment] do |_, args|
args.with_defaults(:db => "XXXX")
args.with_defaults(:username => "XXXX")
pg_user_exists = `psql postgres -XtAc "SELECT 1 FROM pg_roles WHERE rolname='#{args.username}'"`.chomp.empty? ? false : true
system "createuser #{args.username} && echo user #{args.username} has been created successfully" unless pg_user_exists
system %{
createdb #{args.db} -O #{args.username} &&
echo #{args.db} created and owner is set to #{args.username} &&
psql #{args.db} -c "ALTER USER XXXX CREATEDB; ALTER ROLE #{args.username} SET timezone TO 'UTC';" &&
echo timezone is set to UTC
}
end
end
Hemen bakalım hangi task’ler var:
$ rake -T
rake db:create[db,username] # create database
rake db:drop[db] # drop database
rake db:fill # fill database
rake db:migrate[database] # Run migration for given database (default: 'default')
rake db:roll_back[name_of_application,name_of_migration] # roll-back (name of application, name of migration)
rake db:shell # run database shell ..
rake db:show[name_of_application] # show migrations for an application (default: 'all')
rake db:update[name_of_application,name_of_migration,is_empty] # update migration (name of application, name of migration?, is empty?)
Örnekte namespace kullanımı var, yani db adında bir namespace’im var,
komutu çağırırken rake db:XXXXXX
şeklinde kullanıyorum.
Yeni bir model için migration dosyası oluşturmam şu kadar kolay:
$ rake db:update[core,add_new_fields_to_user]
Bu komut ile adı core
olan app’i gösteriyorum ve migration’a isim veriyorum.
Dosya adı 00XX_add_new_fields_to_user.py
şeklinde oluyor…
Peki yaklaşık 20 tane migration var ve ben birşey denemek için 0014_
ile
başlayan duruma dönmek istiyorum:
$ rake db:roll_back[core,14]
yapmam yeterli…
Veritabanını uçurup yeniden oluşturacağım:
$ rake db:drop && rake db:create && rake db:migrate
Peki işin içine Docker operasyonları girerse?
namespace :docker do
namespace :compose do
task :build do
system %{
docker-compose build --build-arg django_requirements=development
}
end
desc "run application"
task :up => ["docker:compose:build"] do
system "docker-compose up"
end
desc "down"
task :down do
system "docker-compose down"
end
desc "down and kill all"
task :down_and_kill do
system "docker-compose down --rmi all --volumes"
end
desc "remove all volumes (any volume exists)"
task :rm_volumes do
system "docker volume rm $(docker volume ls -q)"
end
end
end
Hemen task listesi:
$ rake -T
rake docker:compose:down # down
rake docker:compose:down_and_kill # down and kill all
rake docker:compose:rm_volumes # remove all volumes (any volume exists)
rake docker:compose:up # run application
Haydi uygulama ayağa kalksın:
$ rake docker:compose:up
Tabi rake
’in gücü bunlarla sınırlı değil. Geçtiğimiz yıllarda bu konu ile
ilgili sunum yapmıştım, bu link’ten erişebilirsiniz.
Tabi şunu da unutmamak lazım, Rakefile
içinde ruby
kodu yazıyoruz. Başımıza
bir de Ruby programlama çıktı diye bana kızabilirsiniz. İnanın, gerçekten
çok basit aslında. Sevgiyle yaklaşırsanız yapabilirsiniz.
Eğer ruby
öğrenmek isterseniz, biraz eski de olsa şöyle bir kaynak tavsiye
edebilirim