rmk-ruby 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md ADDED
@@ -0,0 +1,37 @@
1
+ # Rmk
2
+
3
+ A build tool like make, tup, ninja.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'rmk'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install rmk
20
+
21
+ ## Usage
22
+
23
+ TODO: Write usage instructions here
24
+
25
+ ## Development
26
+
27
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
28
+
29
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
30
+
31
+ ## Contributing
32
+
33
+ Bug reports and pull requests are welcome on GitHub at https://github.com/pbl-pw/rmk. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
34
+
35
+ ## Code of Conduct
36
+
37
+ Everyone interacting in the Rmk project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/rmk/blob/master/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "rmk"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/exe/rmk ADDED
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+ require 'iniparse'
5
+ require "rmk"
6
+
7
+ options = {}
8
+ parser = OptionParser.new{ |opts|
9
+ opts.summary_width = 48
10
+ opts.banner = 'Usage: rmk [Options] [targets]'
11
+ opts.separator ''
12
+ opts.on '-O', '--outdir=dir', 'output root dir,can be absolute or relative to pwd,default _Build' do |dir|
13
+ options[:outroot] = dir
14
+ end
15
+ opts.on '-S', '--srcdir=dir', 'source root dir,can be absolute or relative to output root(start with ..),default ..' do |dir|
16
+ options[:srcroot] = dir
17
+ end
18
+ opts.on '-V', '--variant [x,y,z]', Array, 'variant list to build, empty for build all variants' do |list|
19
+ options[:variants] = list || []
20
+ end
21
+ opts.on '-h', '--help', 'show this help' do
22
+ puts opts
23
+ exit
24
+ end
25
+ opts.on '-v', '--version', 'show version' do
26
+ puts 'rmk 0.2.0', ''
27
+ exit
28
+ end
29
+ }
30
+ targets = parser.parse(ARGV)
31
+ if options[:variants]
32
+ raise "variants config file 'variants.rmk' not exist" unless File.exist? 'variants.rmk'
33
+ inifile = IniParse.open 'variants.rmk'
34
+ targets = targets.map{|tgt| %|"#{tgt}"|}.join ' '
35
+ exe = File.join RbConfig::CONFIG['bindir'], RbConfig::CONFIG['ruby_install_name']
36
+ thrs = []
37
+ (options[:variants].empty? ? inifile : options[:variants].map!{|vn|
38
+ variant = inifile[vn]
39
+ warn "variant '#{vn}' doesn't exist" unless variant
40
+ variant
41
+ }).each do |variant|
42
+ next unless variant
43
+ thrs << Thread.new{ system %|"#{exe}" "#{__FILE__}" -O"#{variant['outdir']}" -S"#{variant['srcdir']}" #{targets}|}
44
+ end
45
+ thrs.each{|thr| thr.join}
46
+ exit
47
+ end
48
+ if File.exist? 'ini.rmk'
49
+ inifile = IniParse.open 'ini.rmk'
50
+ dir, link = inifile['Outdir']['Path'], inifile['Outdir']['LinkTo']
51
+ if link
52
+ require 'fileutils'
53
+ link = link.gsub(/\$(?:(\$)|{(.*?)})/){$1 || ENV[$2]}.gsub! ?\/, ?\\
54
+ FileUtils.mkpath link unless Dir.exist? link
55
+ if File.symlink? dir
56
+ if File.readlink(dir) != link
57
+ Dir.rmdir dir
58
+ File.symlink link, dir
59
+ end
60
+ elsif Dir.exist?(dir) || File.exist?(dir)
61
+ raise "need symlink '#{dir}' as outroot, but file(or dir) '#{dir}' already exist"
62
+ else
63
+ FileUtils.mkpath File.dirname dir
64
+ File.symlink link, dir
65
+ end
66
+ else
67
+ FileUtils.mkpath dir
68
+ end
69
+ options[:srcroot] ||= inifile['Srcdir']['Path']
70
+ options[:outroot] ||= dir
71
+ end
72
+ rmk = Rmk.new srcroot:options[:srcroot] || '..', outroot:options[:outroot] || '_Build'
73
+ rmk.parse
74
+ rmk.build *targets
data/lib/rmk/build.rb ADDED
@@ -0,0 +1,238 @@
1
+ require_relative 'rmk'
2
+ require_relative 'vdir'
3
+ require_relative 'vfile'
4
+ require 'open3'
5
+ require 'digest'
6
+
7
+ class Rmk::Build
8
+ attr_reader :dir, :vars
9
+ attr_reader :infiles, :depfiles, :orderfiles, :outfiles
10
+
11
+ # create Build
12
+ # @param dir [Rmk::VDir] build's dir
13
+ # @param rule [Rmk::Rule] build's rule
14
+ # @param vars [Rmk::Vars] upstream vars
15
+ # @param input [Array<Rmk::VFile>] input files
16
+ # @param implicit_input [String, nil] implicit input raw string
17
+ # @param order_only_input [String, nil] order-only input raw string
18
+ # @param output [String, nil] output raw string
19
+ # @param implicit_output [String, nil] implicit output raw string
20
+ # @param collection [String, nil] collection name
21
+ def initialize(dir, rule, vars, input, implicit_input, order_only_input, output, implicit_output, collection)
22
+ @mutex = Thread::Mutex.new
23
+ @updatedcnt = 0 # input file updated count
24
+ @runed = false # build has been runed
25
+ @input_modified = false # input file has modified
26
+
27
+ @dir = dir
28
+ @command = rule.command
29
+ @vars = vars.downstream_new clone_rmk:true
30
+ rmk_vars = @vars.rmk
31
+ @infiles = input
32
+
33
+ if @infiles.size == 1
34
+ if FFile === @infiles[0]
35
+ vname = @infiles[0].vname
36
+ if vname
37
+ match = /^((?:[^\/]+\/)*)([^\/]*)$/.match vname
38
+ rmk_vars['in_dir'], rmk_vars['in_nodir'] = match[1], match[2]
39
+ match = /^(.*)\.(.*)$/.match match[2]
40
+ rmk_vars['in_base'], rmk_vars['in_ext'] = match[1], match[2]
41
+ rmk_vars['in_noext'] = rmk_vars['in_dir'] + rmk_vars['in_base']
42
+ end
43
+ rmk_vars['stem'] = @infiles[0].stem if @infiles[0].stem
44
+ @infiles[0] = @infiles[0].vfile
45
+ end
46
+ end
47
+ rmk_vars['in'] = @infiles.map do |file|
48
+ file.input_ref_builds << self
49
+ next file.vpath || file.path unless file.src?
50
+ file.vpath ? @dir.rmk.join_rto_src_path(file.vpath) : file.path
51
+ end.join ' '
52
+
53
+ @vars.split_str(implicit_input).each do |fn|
54
+ files = @dir.find_inputfiles fn
55
+ raise "pattern '#{fn}' not match any file" if files.empty?
56
+ files.each{|f| f.input_ref_builds << self}
57
+ end if implicit_input
58
+
59
+ @orderfiles = []
60
+ @vars.split_str(order_only_input).each do |fn|
61
+ files = @dir.find_inputfiles fn
62
+ raise "pattern '#{fn}' not match any file" if files.empty?
63
+ files.each{|f| f.order_ref_builds << self}
64
+ end if order_only_input
65
+ raise 'no found any input file' if @infiles.empty? && @orderfiles.empty?
66
+
67
+ @outfiles = []
68
+ regout = proc do |fn|
69
+ file = @dir.add_out_file fn
70
+ file.output_ref_build = self
71
+ @outfiles << file
72
+ end
73
+ output = rule['out'] || raise('must have output') unless output
74
+ @vars.split_str(output).each &regout
75
+ collection.each{|col| col.concat @outfiles} if collection
76
+ rmk_vars['out'] = @outfiles.map {|file| file.vpath || file.path}.join ' '
77
+ rmk_vars['out_noext'] = rmk_vars['out'][/^(.*)\..*$/, 1] if @outfiles.size == 1
78
+ rule.apply_to @vars # interpolate rule's vars to self
79
+ @vars.split_str(implicit_output).each &regout if implicit_output
80
+ rmk_vars.freeze
81
+ @depfiles = []
82
+ @outfiles.each do |file|
83
+ next unless (fns = @dir.rmk.dep_storage.data![file.path])
84
+ fns.each {|fn| @dir.rmk.find_inputfiles(fn).each {|f| f.input_ref_builds << self; @depfiles << f}}
85
+ end
86
+ end
87
+
88
+ def input_updated!(modified, order:false)
89
+ Rmk::Schedule.new_thread! &method(:run) if @mutex.synchronize do
90
+ next @runed = :checkskip if @runed == :force
91
+ next if @runed
92
+ @updatedcnt += 1
93
+ @input_modified ||= order ? modified == :create : modified
94
+ needrun = @updatedcnt >= @infiles.size + @depfiles.size + @orderfiles.size
95
+ @runed = true if needrun
96
+ needrun
97
+ end
98
+ end
99
+
100
+ def order_updated!(modified) input_updated! modified, order:true end
101
+
102
+ private def get_command
103
+ cmd = @vars.interpolate_str @vars['command'] || @command
104
+ digest = Digest::SHA1.new
105
+ digest << cmd
106
+ fdproc = proc{|f| digest << f.vpath || f.path}
107
+ @infiles.each &fdproc
108
+ @orderfiles.each &fdproc
109
+ @depfiles.each {|f| digest << f.path}
110
+ digest = digest.hexdigest
111
+ storage = @dir.rmk.cml_storage
112
+ [cmd, storage.data![@outfiles[0].path] != digest]
113
+ end
114
+
115
+ private def save_digest(fns)
116
+ digest = Digest::SHA1.new
117
+ digest << @command
118
+ fdproc = proc{|f| digest << f.vpath || f.path}
119
+ @infiles.each &fdproc
120
+ @orderfiles.each &fdproc
121
+ fns.each {|f| digest << f} if fns
122
+ digest = digest.hexdigest
123
+ storage = @dir.rmk.cml_storage
124
+ storage.sync{|data| data[@outfiles[0].path] = digest}
125
+ end
126
+
127
+ @output_mutex = Thread::Mutex.new
128
+ def self.puts(*parms) @output_mutex.synchronize{$stdout.puts *parms} end
129
+ def self.err_puts(*parms) @output_mutex.synchronize{$stderr.puts *parms} end
130
+ def self.log_cmd_out(out, err)
131
+ @output_mutex.synchronize do
132
+ $stdout.puts out unless out.empty?
133
+ $stderr.puts err unless err.empty?
134
+ end
135
+ end
136
+
137
+ def parser_force_run!
138
+ return if @runed
139
+ @runed = :force
140
+ exec = nil
141
+ @outfiles.each{|file| file.state = File.exist?(file.path) ? :exist : (exec = :create)}
142
+ @command, changed = get_command
143
+ unless changed || exec
144
+ inproc = proc do |file|
145
+ next file.check_for_parse if file.src?
146
+ state = file.state
147
+ raise 'output file not updated when ref as config file build input' unless state
148
+ state != :exist
149
+ end
150
+ exec = @infiles.any? &inproc
151
+ exec ||= @depfiles.any? &inproc
152
+ return unless @orderfiles.any? do |file|
153
+ next if file.src?
154
+ state = file.state
155
+ raise 'output file not updated when ref as config file build input' unless state
156
+ state == :create
157
+ end unless exec
158
+ end
159
+ raise 'config file build fail' unless raw_exec @command
160
+ @outfiles.each{|file| file.state = :update if file.state == :exist}
161
+ end
162
+
163
+ private def run
164
+ if @runed == :checkskip
165
+ @outfiles.each {|file| file.updated! file.state != :exist && file.state}
166
+ save_digest process_depfile unless @outfiles[0].state == :exist
167
+ else
168
+ exec = nil
169
+ @outfiles.each{|file| file.state = File.exist?(file.path) ? :update : (exec = :create)}
170
+ @command, changed = get_command
171
+ return @outfiles.each{|file| file.updated! false} unless changed || @input_modified || exec
172
+ return unless raw_exec @command
173
+ @outfiles.each {|file| file.updated! file.state}
174
+ save_digest process_depfile
175
+ end
176
+ end
177
+
178
+ private def raw_exec(cmd)
179
+ @vars['depfile'] ||= (@outfiles[0].vpath || @outfiles[0].path) + '.dep' if @vars['deptype']
180
+ unless /^\s*$/.match? cmd
181
+ env = {}
182
+ env['PATH'] = @vars['PATH_prepend'] + ENV['PATH'] if @vars['PATH_prepend']
183
+ @vars['ENV_export'].split(/\s+/).each{|name| env[name] = @vars[name] if @vars.include? name} if @vars['ENV_export']
184
+ std, err, result = env.empty? ? Open3.capture3(cmd) : Open3.capture3(env, cmd)
185
+ std = std.empty? ? @vars['echo'] || "Rmk: run: #{cmd}" : (@vars['echo'] || "Rmk: run: #{cmd}") + ?\n + std
186
+ if result.exitstatus != 0
187
+ err = "execute faild: '#{cmd}'" if err.empty?
188
+ Rmk::Build.log_cmd_out std, err
189
+ @outfiles.each{|file| File.delete file.path if File.exist? file.path}
190
+ return false
191
+ end
192
+ Rmk::Build.log_cmd_out std, err
193
+ end
194
+ true
195
+ end
196
+
197
+ private def process_depfile
198
+ return unless @vars['deptype']
199
+ unless File.exist? @vars['depfile']
200
+ Rmk::Build.err_puts "Rmk: depend file '#{@vars['depfile']}' which must be created by build '#{@vars['out']}' not found"
201
+ end
202
+ if @vars['deptype'] == 'make'
203
+ files = parse_make_depfile @vars['depfile']
204
+ File.delete @vars['depfile']
205
+ return Rmk::Build.err_puts "Rmk: syntax of depend file '#{@vars['depfile']}' not support yet" unless files
206
+ @dir.rmk.dep_storage[@outfiles[0].path] = files
207
+ files.each do |file|
208
+ file = File.absolute_path Rmk.normalize_path(file), @dir.rmk.outroot
209
+ next if @dir.rmk.srcfiles.include?(file) || @dir.rmk.outfiles.include?(file)
210
+ @dir.rmk.mid_storage.sync{|data| data[file] ||= Rmk::VFile.generate_modified_id(file)}
211
+ end
212
+ files
213
+ else
214
+ Rmk::Build.err_puts "Rmk: depend type '#{@vars['deptype']}' not support"
215
+ end
216
+ end
217
+
218
+ def parse_make_depfile(path)
219
+ lines = IO.readlines path
220
+ line, lid = lines[0], 0
221
+ _, _, line = line.partition /(?<!\\)(?:\\\\)*\K:\s+/
222
+ return unless line
223
+ files = []
224
+ while lid < lines.size
225
+ joinline = line.sub! /(?<!\\)(?:\\\\)*\K\\\n\z/,''
226
+ parms = line.split /(?<!\\)(?:\\\\)*\K\s+/
227
+ unless parms.empty?
228
+ parms.delete_at 0 if parms[0].empty?
229
+ parms.map!{|parm| File.absolute_path parm.gsub(/\\(.)/, '\1')}
230
+ files.concat parms
231
+ end
232
+ break unless joinline
233
+ lid += 1
234
+ line = lines[lid]
235
+ end
236
+ files
237
+ end
238
+ end
data/lib/rmk/rmk.rb ADDED
@@ -0,0 +1,197 @@
1
+ require 'rmk/version'
2
+ require_relative 'vars'
3
+ require_relative 'storage'
4
+
5
+ class Rmk
6
+ # normalize path, drive letter upcase and path seperator set to '/'
7
+ # @param path [String]
8
+ # @return [String]
9
+ def self.normalize_path(path) path.gsub(?\\, ?/).sub(/^[a-z](?=:)/){|ch|ch.upcase} end
10
+
11
+ # create Rmk object
12
+ # @param srcroot [String] source root dir,can be absolute or relative to output root(start with ..)
13
+ # @param outroot [String] output root dir,can be absolute or relative to pwd,default pwd
14
+ def initialize(srcroot:'', outroot:'')
15
+ @files_mutex = Thread::Mutex.new # files operate mutex
16
+
17
+ srcroot = Rmk.normalize_path srcroot
18
+ @outroot = File.join Rmk.normalize_path(File.absolute_path outroot), ''
19
+ @srcroot = File.join File.absolute_path(srcroot, @outroot), ''
20
+ raise "source path '#{@srcroot}' not exist or not directory" unless ::Dir.exist?(@srcroot)
21
+ warn 'in-source build' if @outroot == @srcroot
22
+ @src_relative = srcroot.match?(/^\.\./) && File.join(srcroot, '')
23
+ Dir.mkdir @outroot unless Dir.exist? @outroot
24
+ Dir.chdir @outroot
25
+ Dir.mkdir '.rmk' unless Dir.exist? '.rmk'
26
+ @mid_storage = Rmk::Storage.new '.rmk/mid', {}
27
+ @dep_storage = Rmk::Storage.new '.rmk/dep', {}
28
+ @cml_storage = Rmk::Storage.new '.rmk/cml', {} # command line text storage
29
+ @srcfiles = {}
30
+ @outfiles = {}
31
+ @defaultfiles = []
32
+ @vars = {'srcroot'=>@srcroot[0..-2], 'outroot'=>@outroot[0..-2], 'src_rto_root'=>(@src_relative || @srcroot)[0..-2]}.freeze
33
+ @virtual_root = Rmk::VDir.new self, nil
34
+ end
35
+ attr_reader :srcroot, :outroot, :vars, :virtual_root, :srcfiles, :outfiles
36
+ attr_reader :mid_storage, :dep_storage, :cml_storage
37
+
38
+ def join_abs_src_path(path) File.join @srcroot, path end
39
+
40
+ # join src file path relative to out root, or absolute src path when not relative src
41
+ def join_rto_src_path(path) ::File.join @src_relative ? @src_relative : @srcroot, path end
42
+
43
+ # split path pattern to dir part and file match regex part
44
+ # @param pattern [String] absolute path, can include '*' to match any char at last no dir part
45
+ # when pattern include '*', return [dir part, file(or dir) match regex, post dir part, post file part]
46
+ # ;otherwise return [origin pattern, nil, nil, nil]
47
+ def split_path_pattern(pattern)
48
+ match = /^([a-zA-Z]:\/(?:[^\/*]+\/)*+)([^\/*]*+)(?:\*([^\/*]*+))?(?(3)(\/(?:[^\/*]+\/)*+[^\/*]++)?)$/.match pattern
49
+ raise "file syntax '#{pattern}' error" unless match
50
+ dir, prefix, postfix, postpath = *match[1..5]
51
+ regex = postfix && /#{Regexp.escape prefix}(.*)#{Regexp.escape postfix}$/
52
+ [regex ? dir : pattern, regex, postpath]
53
+ end
54
+
55
+ # find files which can be build's imput file
56
+ # @param pattern [String] absolute path to find src and out files which can include '*' to match any char at last no dir part
57
+ # @param ffile [Boolean] return FFile struct or not
58
+ # @return [Array<VFile, FFile>] return array of FFile when ffile, otherwise array of VFile
59
+ def find_inputfiles(pattern, ffile:false)
60
+ pattern = Rmk.normalize_path pattern
61
+ path, regex, postpath = split_path_pattern pattern
62
+ files = []
63
+ if regex
64
+ @files_mutex.synchronize do
65
+ find_outfiles_imp files, path, regex, postpath, ffile:ffile
66
+ range = postpath ? path.size..-1-postpath.size : path.size..-1
67
+ Dir[pattern].each do |fn|
68
+ next if @outfiles.include? fn
69
+ next files << (ffile ? FFile.new(@srcfiles[fn], nil, fn[range][regex, 1]) : @srcfiles[fn]) if @srcfiles.include? fn
70
+ file = (@srcfiles[fn] = VFile.new rmk:self, path:fn, is_src:true,
71
+ vpath:fn.start_with?(@srcroot) && fn[@srcroot.size .. -1])
72
+ files << (ffile ? FFile.new(file, nil, fn[range][regex, 1]) : file)
73
+ end
74
+ end
75
+ else
76
+ @files_mutex.synchronize do
77
+ next files << (ffile ? FFile.new(@outfiles[path]) : @outfiles[path]) if @outfiles.include? path
78
+ next files << (ffile ? FFile.new(@srcfiles[path]) : @srcfiles[path]) if @srcfiles.include? path
79
+ next unless File.exist? path
80
+ file = @srcfiles[path] = VFile.new rmk:self, path:path, is_src:true,
81
+ vpath:path.start_with?(@srcroot) && path[@srcroot.size .. -1]
82
+ files << (ffile ? FFile.new(file) : file)
83
+ end
84
+ end
85
+ files
86
+ end
87
+
88
+ # find files which must be build's output
89
+ # @param pattern [String] absolute path to find out files which can include '*' to match any char at last no dir part
90
+ # @return [Array<Hash>] return Array of file, and Regex when has '*' pattern
91
+ def find_outfiles(pattern)
92
+ path, regex, postpath = split_path_pattern Rmk.normalize_path pattern
93
+ files = []
94
+ @files_mutex.synchronize do
95
+ next (files << @outfiles[path] if @outfiles.include? path) unless regex
96
+ find_outfiles_imp files, path, regex, postpath
97
+ end
98
+ files
99
+ end
100
+
101
+ # find outfiles raw implement(assume all parms valid)
102
+ # @param files [Array<VFile>] array to store finded files
103
+ # @param path [String] absolute path, path must be dir(end with '/') or empty
104
+ # @param regex [Regexp] file match regexp, or dir match regexp when postpath not nil
105
+ # @param postpath [String, nil] path after dir match regexp
106
+ # @param ffile [Boolean] return FFile struct or not
107
+ # @return [Array<VFile, FFile>] return array of FFile when ffile, otherwise array of VFile
108
+ private def find_outfiles_imp(files, path, regex, postpath, ffile:false)
109
+ range = postpath ? path.size..-1-postpath.size : path.size..-1
110
+ return @outfiles.each do |k, v|
111
+ next unless k.start_with?(path) && k[range].match?(regex)
112
+ files << (ffile ? FFile.new(v, nil, k[range][regex, 1]) : v)
113
+ end unless postpath
114
+ @outfiles.each do |k, v|
115
+ next unless k.start_with?(path) && k.end_with?(postpath) && k[range].match?(regex)
116
+ files << (ffile ? FFile.new(v, nil, k[range][regex, 1]) : v)
117
+ end
118
+ end
119
+
120
+ # register a out file
121
+ # @param path [String]
122
+ # @param vpath [String]
123
+ # @return [Rmk::VFile] return file obj added
124
+ def add_out_file(path:, vpath:nil)
125
+ @files_mutex.synchronize do
126
+ raise "file '#{path}' has been defined" if @outfiles.include? path
127
+ file = @srcfiles.delete(path).change_to_out! file if @srcfiles.include? path
128
+ @outfiles[path] = VFile.new rmk:self, path:path, vpath:vpath
129
+ end
130
+ end
131
+
132
+ # register a src file
133
+ # @param path [String]
134
+ # @param vpath [String]
135
+ # @return [Rmk::VFile] return file obj added
136
+ def add_src_file(path:, vpath:nil)
137
+ @files_mutex.synchronize do
138
+ @outfiles[path] || (@srcfiles[path] ||= VFile.new(rmk:self, path:path, vpath:vpath, is_src:true))
139
+ end
140
+ end
141
+
142
+ # add default target
143
+ def add_default(*files) @files_mutex.synchronize{@defaultfiles.concat files} end
144
+
145
+ # parse project
146
+ # @return [self]
147
+ def parse
148
+ puts 'Rmk: parse start'
149
+ @mid_storage.wait_ready
150
+ @dep_storage.wait_ready
151
+ @cml_storage.wait_ready
152
+ @virtual_root.parse
153
+ puts 'Rmk: parse done'
154
+ self
155
+ end
156
+
157
+ def build(*tgts)
158
+ puts 'Rmk: build start'
159
+ if tgts.empty?
160
+ files = @defaultfiles
161
+ else
162
+ files = tgts.map do |name|
163
+ file = Rmk.normalize_path name
164
+ file = @outfiles[File.absolute_path file, @outroot] || @srcfiles[File.absolute_path file, @srcroot]
165
+ raise "build target '#{name}' not found" unless file
166
+ file = file.input_ref_builds[0].outfiles[0] if file.src? && file.input_ref_builds.size == 1
167
+ file
168
+ end
169
+ end
170
+ if files.empty?
171
+ @srcfiles.each_value{|file| file.check_for_build}
172
+ else
173
+ checklist = []
174
+ checkproc = proc do |fi|
175
+ next checklist << fi if fi.src?
176
+ fi.output_ref_build.infiles.each &checkproc
177
+ fi.output_ref_build.depfiles.each &checkproc
178
+ fi.output_ref_build.orderfiles.each &checkproc
179
+ end
180
+ files.each &checkproc
181
+ exit puts('found nothing to build') || 0 if checklist.empty?
182
+ checklist.each {|file| file.check_for_build}
183
+ end
184
+ while Thread.list.size > 1
185
+ thr = Thread.list[-1]
186
+ thr.join unless thr == Thread.current
187
+ end
188
+ puts 'Rmk: build end'
189
+ @mid_storage.save
190
+ @dep_storage.data!.each_key {|key| @dep_storage.data!.delete key unless @outfiles.include? key}
191
+ @dep_storage.save
192
+ @cml_storage.data!.each_key {|key| @cml_storage.data!.delete key unless @outfiles.include? key}
193
+ @cml_storage.save
194
+ end
195
+
196
+ class VFile; end
197
+ end
data/lib/rmk/rule.rb ADDED
@@ -0,0 +1,30 @@
1
+ require_relative 'rmk'
2
+
3
+ class Rmk::Rule
4
+ Var = Struct.new :append?, :value
5
+
6
+ # create Rule
7
+ # @param command [String] exec command template
8
+ def initialize(command)
9
+ @command = command
10
+ @vars = {}
11
+ @rmk_vars = {'out'=>nil, 'collection'=>nil}
12
+ end
13
+ attr_reader :command
14
+
15
+ def vars; self end
16
+
17
+ def [](name) @rmk_vars[name] end
18
+
19
+ # add var define template
20
+ # @return Array<Var>
21
+ def []=(name, append = false, value)
22
+ return @vars[name] = Var.new(append, value) unless @rmk_vars.include? name
23
+ raise "special var '#{name}' can't be append" if append
24
+ @rmk_vars[name] = value
25
+ end
26
+
27
+ def apply_to(tgt)
28
+ @vars.each{|name, var| var.append? ? tgt[name] += var.value : tgt[name] = var.value }
29
+ end
30
+ end
@@ -0,0 +1,15 @@
1
+ class Rmk; end
2
+
3
+ module Rmk::Schedule
4
+ Thread.abort_on_exception = true
5
+ @queue = SizedQueue.new 8
6
+
7
+ def self.new_thread!(&cmd)
8
+ Thread.new do
9
+ @queue.push nil
10
+ result = cmd.call
11
+ @queue.pop
12
+ result
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,30 @@
1
+ require_relative 'schedule'
2
+
3
+ class Rmk::Storage
4
+ # create
5
+ # @param file [String] file path
6
+ # @param iniobj [Object] init inside obj
7
+ def initialize(file, iniobj)
8
+ @mutex = Thread::Mutex.new
9
+ @file = file
10
+ @data = File.exist?(@file) ? Rmk::Schedule.new_thread!{Marshal.load IO.binread @file} : iniobj
11
+ end
12
+
13
+ # wait for storage ready to read and write
14
+ # @note before call this method storage is not ready, can't call any follow methods
15
+ def wait_ready; @mutex.synchronize{@data = @data.value if Thread === @data} end
16
+
17
+ # save data to disk file
18
+ def save; @mutex.synchronize{IO.binwrite @file, Marshal.dump(@data)} end
19
+
20
+ # run block in mutex sync protected
21
+ # @yieldparam data [Hash] inside Hash obj
22
+ # @return [Object] block's result
23
+ def sync(&cmd) @mutex.synchronize{cmd.call @data} end
24
+
25
+ # get inside Hash obj without sync protected
26
+ def data!; @data end
27
+
28
+ # redirect undef method to inside data obj with sync protected, often used for single operation
29
+ def method_missing(name, *parms, &cmd) @mutex.synchronize{@data.send name, *parms, &cmd} end
30
+ end