Rakefileで快適コンパイル生活

f:id:kazukitash:20160220154420p:plain

コンパイルしたいときはたった四文字打つだけ。

$ rake

Rubyをよく使うのでC言語コンパイル処理をRakefileに書いた。

require 'rake/clean'
CLEAN.include("**/*.o")
CLOBBER.include("*.out")

CC = "gcc"
GFLAGS = "-g -Wall"
OPT = "-O3"
INCDIR  = "lib"
SRCS = FileList["**/*.c"]
OBJS = SRCS.ext('.o')
PROGRAMS_DIR = FileList.new("**/*.c").exclude("lib/*").ext('.out')
PROGRAMS = PROGRAMS_DIR.map{|p| File.basename(p)}
DATA_DIR = "data"
directory DATA_DIR

task default: PROGRAMS

desc "初期化"
task init: [DATA_DIR, :reset]

desc "中間ファイルを削除してからビルド"
task refresh: [:clean, :default]

desc "実行ファイルを削除してからビルド"
task reset: [:clobber, :default]

desc "依存ファイルのビルド"
task dist: OBJS

def dep_obj program
  headers = [program.ext('.o')]
  open(program.ext('.c')){ |f| headers.push "/#{$1.ext('.o')}" if $_ =~ /#include\s+"(.+)"/ while f.gets }
  OBJS.select{|obj| obj =~ Regexp.union(headers) }
end

PROGRAMS_DIR.each do |program|
  desc "#{File.basename(program)}をビルド"
  file File.basename(program) => dep_obj(program) do |t|
    sh "#{CC} #{OPT} #{GFLAGS} -o #{t.name} #{t.prerequisites.join(' ')}"
  end
end

rule '.o' => '.c' do |t|
  sh "#{CC} #{OPT} #{GFLAGS} -c #{t.source} -o #{t.name} -I#{INCDIR}"
end

固定タスクは initrefreshreset で文字通りのことしてる。rake/cleanを require することでcleanとclobberというタスクが追加される。

CLEAN.include("**/*.o")
CLOBBER.include("*.out")

このようにして削除したいファイルをincludeしておくとタスクを実行したときに消してくれる。いちおうcleanは中間ファイル、clobberをclean + 実行ファイルを消すためのものらしいけど、違いはいまいち分からない。

PROGRAMS_DIR = FileList.new("**/*.c").exclude("lib/*").ext('.out')
PROGRAMS = PROGRAMS_DIR.map{|p| File.basename(p)}

defaultタスクは配下のすべてのプログラムをコンパイルしている。libを除いたフォルダの中のC言語をそれぞれ実行ファイル形式(.out)にコンパイルしている。FileListでCファイルのPATHを求めて実行ファイル名を作成している。ext()はFileListの関数で拡張子を任意の文字列に変換できる。

DATA_DIR = "data"
directory DATA_DIR

...ruby
task init: [DATA_DIR, :reset]

directoryはFileUtilsの関数だけど[1]タスクのように指定することでディレクトリを作成してくれる。

PROGRAMS_DIR.each do |program|
  desc "#{File.basename(program)}をビルド"
  file File.basename(program) => dep_obj(program) do |t|
    sh "#{CC} #{OPT} #{GFLAGS} -o #{t.name} #{t.prerequisites.join(' ')}"
  end
end

動的に実行ファイルのコンパイルタスクを生成している。

def dep_obj program
  headers = [program.ext('.o')]
  open(program.ext('.c')){ |f| headers.push "/#{$1.ext('.o')}" if $_ =~ /#include\s+"(.+)"/ while f.gets }
  OBJS.select{|obj| obj =~ Regexp.union(headers) }
end

Cファイルを開きincludeされているものを読んでいる。 if $ =~ /#include\s+"(.+)"/ は$が暗黙の変数なので if /#include\s+"(.+)"/ でもOK。でもwarningが出るので極力やめよう。

rule '.o' => '.c' do |t|
  sh "#{CC} #{OPT} #{GFLAGS} -c #{t.source} -o #{t.name} -I#{INCDIR}"
end

rakeのruleでOファイルが同名のCファイルから作成できることを書いた。


  1. rakefileはFileUtilsを自動でrequireしてくれている。  ↩