5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

タイムスタンプを見ず、中身が変わっていたらビルドする

Last updated at Posted at 2022-06-26

これは何?

先日のツイート

の実装。

状況

タイムスタンプが信用できないファイルが絡むビルドをしたいことがある。

  • 手元で生成できるファイルが、 git でも管理されている
  • 自動更新されるデータ(スクレイピングの結果からの抽出物とか)をソースにする

など。

具体的には、たとえばとこんな rakefile(抜粋)があったとしよう。

rakefile
file "generated/cooked.json" => "hoge.json" do |a|
  cook_json( a.sources, a.name )
end

hoge.json をなんか処理して、 cooked.json を作っている。
cooked.json 自体はそんなに大きくはない。

しかし、なにかわからない事情によって hoge.json のタイムスタンプは信用できないので、上記の rakefile はうまく機能しない。

touch hoge.json などとした上で rake するとビルドされるが、 cooked.json を生成する処理は割と重いので、hoge.json が変わっていないのであればそっとしておきたい。

解決方法

rakefile
file digest_filename_of("generated/cooked.json") => digest_filename_of("hoge.json") do |a|
  cook_json( "hoge.json", a.name )
end

まずは、こんな風にして、解決を狙う。
依存ファイルにタイムスタンプが信用できないファイルがある場合、 digest_filename_of でくるむと解決するように digest_filename_of を定義しておく。
中身はこんな感じ。

ruby
def digest_filename_of(fn)
  FileUtils.mkdir_p( INTERMEDIATE_DIR )
  digest = ->(x){ Digest::SHA256.hexdigest(x).strip }
  fread = ->(x){ File.open(x,&:read) rescue "" }
  dfn = File.join(INTERMEDIATE_DIR, digest[fn])
  d = digest[fread[fn]]
  if ! File.exist?(dfn) || fread[dfn] != d
    File.open( dfn, "w" ) do |f|
      f.write(d)
    end
  end
  dfn
end

まあ見ての通り。

雑に説明すると、ダイジェスト(ハッシュ)をおぼえておいて、前回と同じかどうかを利用する。
ダイジェストが一致していればファイルを更新しない。

この作戦の欠点

見ての通り、

rakefile
file "generated/cooked.json" => "hoge.json" do |a|
  cook_json( a.sources, a.name )
end

でやっていたような a.sources が使えないのが弱点。

あと、生成されるファイルのタイムスタンプが信用できない場合はうまく行かない。
この記事を公開した当初はうまくいくと思っていたんだけど、駄目っぽい。

もっといい解決案

Qiita に書いたおかげで地味に広まり

と、task というツールを教えていただいた。

このツール、タイムスタンプではなくチェックサム(「サム」と単に加算していそうな名前だけど、おそらくダイジェスト)を見るようになっているので、なにもしなくてもこの記事のタイトルのような動作になる。
素晴らしい。

amasawa_seiji 様、ありがとうございます。

rake に手を入れる

rake に手を入れれば解決できるので、手を入れてみた。

ユーザーコード側

rakefile(抜粋)
require_relative "./contents_task.rb"

contents "generated/cooked.json" => "hoge.json" do |a|
  cook_json( a.sources, a.name )
end

file の代わりに contents と書くとタイムスタンプではなく中身の比較でビルドするかどうかを判断する。

a.sourcesa.name も普通に使える。

実装側

問題の contents_task.rb は、以下の通り:

contents_task.rb
# frozen_string_literal: true
require "rake/task"
require "digest/sha2"
require "fileutils"
require "json"

module Rake
  class ContentsTask < Task
    def initialize(*args)
      super
      @intermediate_dir = ".intermediate"
    end

    attr_accessor :intermediate_dir

    def execute(args=nil)
      super
      update_state
    end

    def needed?
      should_build?(name) || @application.options.build_all
    end

    private

    def state_fn(files)
      File.join(intermediate_dir, Digest::SHA256.hexdigest(files.sort.to_json))
    end

    def diget_files(files)
      a=Digest::SHA256.new
      files.sort.each do |fn|
        return nil unless File.exist?(fn)
        a.file(fn)
      end
      a.hexdigest
    end

    def should_build?(name)
      files = ([name]+prerequisites).flatten
      dfn = state_fn(files)
      return true unless File.exist?(dfn)
      digest_val = diget_files(files)
      return true unless digest_val
      return digest_val != File.open(dfn, &:read).strip
    end

    def update_state
      files = ([name]+prerequisites).flatten
      dfn = state_fn(files)
      FileUtils.mkdir_p( File.split(dfn)[0])
      File.open(dfn, "w") do |f|
        f.write(diget_files(files))
      end
    end

    class << self
      def scope_name(scope, task_name)
        Rake.from_pathname(task_name)
      end
    end
  end
end

module Rake
  module DSL
    def contents(*args, &block)
      Rake::ContentsTask.define_task(*args, &block)
    end
  end
end

七十余行。

そんなに真剣にテストしてなけど、うまく行っている気がする。
どうだろう。

5
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?