FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

DSLでビルドツールを自作する (22日目 gitコマンドに埋め込む1)

この記事は「Qiita Advent Calendar 2019 DSLで自作ビルドツールを作ろう」の22日目の記事です。

22日目 gitコマンドに埋め込む1

最近のビルドツールには、gitによるバージョン管理と融合し、リポジトリの管理の補助をしてくれる機能が当たりします。Rumy-Makeにも、gitと融合して開発を支援してくれる機能が欲しいので、これらを実装してみました。

まず、最初に考えたのは、リグレッションテストなどを実行する際、バージョン管理中のファイルの中に変更途中のものが混じっており、すべてのファイルがコミット状態になっていないままリグレッションテストを実行して結果が誤ってしまうケースがあります。このことを防ぐために、設定によっては、依存関係に含まれるファイルに編集中(gitのmodified状態)に入っているものがあればそこでビルドを止める、という機能が欲しいです。

その機能はリグレッション中や、特別にcloneしたリポジトリにのみ適用されてほしいです(つまり開発用のリポジトリには適用されない)。そこで、rumy専用のgit cloneコマンドを用意して、このgit cloneコマンドを使用すると自動的にリポジトリにロックがかかり、編集状態でビルドを実行すると自動的に停止するように改造してみましょう。

RubyでGitを扱うためのパッケージruby-git

Ruby内でgitのコマンドやリポジトリを扱うために、ruby-gitパッケージを使用しました。

https://github.com/ruby-git/ruby-git/tree/master/lib/git

このパッケージを使用すれば、gitのコマンド操作をRuby上で簡単に扱うことができます。

まずは、Rumy上でgit cloneするためのrumy clone-gitコマンドを作ります。

  • rumy
  # Project Initializer for Rust Project
  desc "clone-git repo_location", "Clone Git Repository"
  def clone_git(repo_location)
    rumy_clone_git(repo_location, repo_location.split('/').last.sub('.git', ''))
  end

  # Project Initializer for Rust Project
  desc "clone-git repo_location dir", "Clone Git Repository"
  def clone_git(repo_location, dir)
    rumy_clone_git(repo_location, dir)
  end

ダウンロード先のディレクトリを指定するバージョンとそうでないバージョンを作りました。

clone-git repo_location
clone-git repo_location dir

どちらでも使用できます。実際の動作は、rumy_clone_git()に記述しています。

  • src/rumy-git.rb
def rumy_clone_git(url, dir)
  # g = Git.open(toplevel_path)
  Git.clone(url, dir, :log => Logger.new(STDOUT))
  lock_file_loc = dir + "/" + ".rumy-lock"
  if not File.exist?(lock_file_loc) then
    loc_fp = File.open(lock_file_loc, "w")
    loc_fp.write("lock = true")
    loc_fp.close
  end
end

よく見ると、clone-gitをした場合にそのリポジトリの直下に.rumy-lockというファイルを配置するようにしています。.rumy-lock内にはlock = trueと書いた設定ファイルを置いてあり、ビルド中にこのファイルを見つけると、modified状態になっているファイルを見つけた場合にビルドを中止するような処理を適用する仕組みにしました。

Modifiedなファイルを見つけた場合にビルドを中止する機能の実装

では、いよいよ本命のmodifiedな状態の依存ファイルを見つけるとそこでビルドを中止する機能を実装してみます。条件としては下記のようにします。

  • リポジトリである(.gitファイルが見つからなければこの機能は適用しない)
  • リポジトリの先頭に.rumy-lockファイルが配置してあり、lock = trueという行が存在している(この辺の実装は適当)
  • 依存関係を探索していく中で、Git管理ファイルの中にmodifiedなものが存在すればビルドを中止する。

まずは、当該ターゲットのディレクトリがGitの管理リポジトリであることを確認します。簡単に、いかのようにしてgitのコマンドで確認を行います。

  • src/rumy-git.rb
  toplevel_path = Pathname.new(`git rev-parse --show-toplevel`.sub("\n", ""))
  if toplevel_path.to_s == "" then
    return false
  end

Gitのrev-parseコマンドを使用してGitのトップディレクトリを探索します。これが見つからなければGitリポジトリではないので、この関数からFalseを返して抜けます。

次に、.rumy-lockファイルとlock = trueの行を確認します。

  git_root_relative = toplevel_path.relative_path_from(current_path).to_s
  lock_file_loc = git_root_relative + "/" + ".rumy-lock"
  if not File.exist?(lock_file_loc) then
    return false
  else
    File.open(lock_file_loc, "r"){|fp|
      fp.readlines.each{|line|
        if line.include?("lock = true")
          return true
        end
      }
    }
    return false
  end

まずはGitのリポジトリの先頭から、.rumy-lockのファイルの存在を確認し、存在していなければFalseを返して終了です。存在している場合、行を探索していき、lock = trueの行を見つければTrueを返すことにしました。.rumy-lockの探索の実装はかなり適当ですが、どうせ人が手を加えるファイルではないので、適当な実装でも良しとしました。

最後に、ビルドの依存関係探索中にmodifiedのファイルを確認するコードです。

  • src/rumy-target.rb
  if $target_list.key?(name) then
    target = $target_list[name]
    # 依存関係を探索時に、最初に.rumy-lockによる確認を行うかを調べておく
    need_check_modify = rumy_is_git_modify_lock()
...
        # 依存ファイルを探索する場合
        elsif not $target_list.key?(dep) and not Symbol.all_symbols.include?(dep) then
          # modifyのチェックを行う必要があり、当該ファイルがmodifiedであれば
          if need_check_modify and git_file_modified?(dep) then
            puts "[ERROR] : Rumy build is stop due to File #{dep} is modified. Exit."
            exit
          end

まず、上記のrumy_is_git_modify_lock()を呼んで、Modified状態ファイルの確認を行う必要があるか見ておきます。次に、依存関係ファイルを探索している間、git_file_modified?(dep)によりファイルのgitによる管理状態を確認し、modifiedであれば強制的に終了します。

def git_file_modified?(filename)
  current_path = Pathname.new(`pwd`.sub("\n", ""))
  toplevel_path = Pathname.new(`git rev-parse --show-toplevel`.sub("\n", ""))

  git_root_relative = toplevel_path.relative_path_from(current_path).to_s

  g = Git.open(toplevel_path)

  file_dir_from_root = current_path.relative_path_from(toplevel_path).to_s
  file_location = file_dir_from_root + "/" + filename
  if not File.exist?(filename) then
    puts "This file " + filename + " is not exist."
    return
  end

  return g.status.changed?(file_location)
end

無理やりな実装ですが、上記のようにしました。ruby-gitを使って、当該ファイルの状態を確認します。changed?であれば、modified状態なのでビルドを中止します。

このように、.rumy-lockファイルを先頭に配置することによりビルドツールの挙動を変える機能を実装しました。これは、手元でリグレッションなどを行う場合に使えそうです。

しかしこの機能にはまだ不十分な点があります。まず、サブリポジトリなどの複数にわたるリポジトリの階層がある場合に、一番トップのリポジトリ.rumy-lockを見なければならないはずですが、それができていません。複数階層のリポジトリを正しく扱う場合には、さらなる改造が必要そうです。