1require 'pathname' 2require 'forwardable' 3require 'tsort' 4require 'shellwords' 5 6module MRuby 7 module Gem 8 class << self 9 attr_accessor :current 10 end 11 LinkerConfig = Struct.new(:libraries, :library_paths, :flags, :flags_before_libraries, :flags_after_libraries) 12 13 class Specification 14 include Rake::DSL 15 extend Forwardable 16 def_delegators :@build, :filename, :objfile, :libfile, :exefile 17 18 attr_accessor :name, :dir, :build 19 alias mruby build 20 attr_accessor :build_config_initializer 21 attr_accessor :mrblib_dir, :objs_dir 22 23 attr_accessor :version 24 attr_accessor :description, :summary 25 attr_accessor :homepage 26 attr_accessor :licenses, :authors 27 alias :license= :licenses= 28 alias :author= :authors= 29 30 attr_accessor :rbfiles, :objs 31 attr_accessor :test_objs, :test_rbfiles, :test_args 32 attr_accessor :test_preload 33 34 attr_accessor :bins 35 36 attr_accessor :requirements 37 attr_reader :dependencies, :conflicts 38 39 attr_accessor :export_include_paths 40 41 attr_reader :generate_functions 42 43 attr_block MRuby::Build::COMMANDS 44 45 def initialize(name, &block) 46 @name = name 47 @initializer = block 48 @version = "0.0.0" 49 @mrblib_dir = "mrblib" 50 @objs_dir = "src" 51 MRuby::Gem.current = self 52 end 53 54 def setup 55 return if defined?(@linker) # return if already set up 56 57 MRuby::Gem.current = self 58 MRuby::Build::COMMANDS.each do |command| 59 instance_variable_set("@#{command}", @build.send(command).clone) 60 end 61 @linker = LinkerConfig.new([], [], [], [], []) 62 63 @rbfiles = Dir.glob("#{@dir}/#{@mrblib_dir}/**/*.rb").sort 64 @objs = Dir.glob("#{@dir}/#{@objs_dir}/*.{c,cpp,cxx,cc,m,asm,s,S}").map do |f| 65 objfile(f.relative_path_from(@dir).to_s.pathmap("#{build_dir}/%X")) 66 end 67 68 @test_rbfiles = Dir.glob("#{dir}/test/**/*.rb").sort 69 @test_objs = Dir.glob("#{dir}/test/*.{c,cpp,cxx,cc,m,asm,s,S}").map do |f| 70 objfile(f.relative_path_from(dir).to_s.pathmap("#{build_dir}/%X")) 71 end 72 @custom_test_init = !@test_objs.empty? 73 @test_preload = nil # 'test/assert.rb' 74 @test_args = {} 75 76 @bins = [] 77 78 @requirements = [] 79 @dependencies, @conflicts = [], [] 80 @export_include_paths = [] 81 @export_include_paths << "#{dir}/include" if File.directory? "#{dir}/include" 82 83 instance_eval(&@initializer) 84 85 @generate_functions = !(@rbfiles.empty? && @objs.empty?) 86 @objs << objfile("#{build_dir}/gem_init") if @generate_functions 87 88 if !name || !licenses || !authors 89 fail "#{name || dir} required to set name, license(s) and author(s)" 90 end 91 92 build.libmruby_objs << @objs 93 94 instance_eval(&@build_config_initializer) if @build_config_initializer 95 end 96 97 def setup_compilers 98 compilers.each do |compiler| 99 compiler.define_rules build_dir, "#{dir}" 100 compiler.defines << %Q[MRBGEM_#{funcname.upcase}_VERSION=#{version}] 101 compiler.include_paths << "#{dir}/include" if File.directory? "#{dir}/include" 102 end 103 104 define_gem_init_builder if @generate_functions 105 end 106 107 def add_dependency(name, *requirements) 108 default_gem = requirements.last.kind_of?(Hash) ? requirements.pop : nil 109 requirements = ['>= 0.0.0'] if requirements.empty? 110 requirements.flatten! 111 @dependencies << {:gem => name, :requirements => requirements, :default => default_gem} 112 end 113 114 def add_test_dependency(*args) 115 add_dependency(*args) if build.test_enabled? 116 end 117 118 def add_conflict(name, *req) 119 @conflicts << {:gem => name, :requirements => req.empty? ? nil : req} 120 end 121 122 def self.bin=(bin) 123 @bins = [bin].flatten 124 end 125 126 def build_dir 127 "#{build.build_dir}/mrbgems/#{name}" 128 end 129 130 def test_rbireps 131 "#{build_dir}/gem_test.c" 132 end 133 134 def search_package(name, version_query=nil) 135 package_query = name 136 package_query += " #{version_query}" if version_query 137 _pp "PKG-CONFIG", package_query 138 escaped_package_query = Shellwords.escape(package_query) 139 if system("pkg-config --exists #{escaped_package_query}") 140 cc.flags += [`pkg-config --cflags #{escaped_package_query}`.strip] 141 cxx.flags += [`pkg-config --cflags #{escaped_package_query}`.strip] 142 linker.flags_before_libraries += [`pkg-config --libs #{escaped_package_query}`.strip] 143 true 144 else 145 false 146 end 147 end 148 149 def funcname 150 @funcname ||= @name.gsub('-', '_') 151 end 152 153 def compilers 154 MRuby::Build::COMPILERS.map do |c| 155 instance_variable_get("@#{c}") 156 end 157 end 158 159 def define_gem_init_builder 160 file objfile("#{build_dir}/gem_init") => [ "#{build_dir}/gem_init.c", File.join(dir, "mrbgem.rake") ] 161 file "#{build_dir}/gem_init.c" => [build.mrbcfile, __FILE__] + [rbfiles].flatten do |t| 162 FileUtils.mkdir_p build_dir 163 generate_gem_init("#{build_dir}/gem_init.c") 164 end 165 end 166 167 def generate_gem_init(fname) 168 open(fname, 'w') do |f| 169 print_gem_init_header f 170 build.mrbc.run f, rbfiles, "gem_mrblib_irep_#{funcname}" unless rbfiles.empty? 171 f.puts %Q[void mrb_#{funcname}_gem_init(mrb_state *mrb);] 172 f.puts %Q[void mrb_#{funcname}_gem_final(mrb_state *mrb);] 173 f.puts %Q[] 174 f.puts %Q[void GENERATED_TMP_mrb_#{funcname}_gem_init(mrb_state *mrb) {] 175 f.puts %Q[ int ai = mrb_gc_arena_save(mrb);] 176 f.puts %Q[ mrb_#{funcname}_gem_init(mrb);] if objs != [objfile("#{build_dir}/gem_init")] 177 unless rbfiles.empty? 178 f.puts %Q[ mrb_load_irep(mrb, gem_mrblib_irep_#{funcname});] 179 f.puts %Q[ if (mrb->exc) {] 180 f.puts %Q[ mrb_print_error(mrb);] 181 f.puts %Q[ mrb_close(mrb);] 182 f.puts %Q[ exit(EXIT_FAILURE);] 183 f.puts %Q[ }] 184 end 185 f.puts %Q[ mrb_gc_arena_restore(mrb, ai);] 186 f.puts %Q[}] 187 f.puts %Q[] 188 f.puts %Q[void GENERATED_TMP_mrb_#{funcname}_gem_final(mrb_state *mrb) {] 189 f.puts %Q[ mrb_#{funcname}_gem_final(mrb);] if objs != [objfile("#{build_dir}/gem_init")] 190 f.puts %Q[}] 191 end 192 end # generate_gem_init 193 194 def print_gem_comment(f) 195 f.puts %Q[/*] 196 f.puts %Q[ * This file is loading the irep] 197 f.puts %Q[ * Ruby GEM code.] 198 f.puts %Q[ *] 199 f.puts %Q[ * IMPORTANT:] 200 f.puts %Q[ * This file was generated!] 201 f.puts %Q[ * All manual changes will get lost.] 202 f.puts %Q[ */] 203 end 204 205 def print_gem_init_header(f) 206 print_gem_comment(f) 207 f.puts %Q[#include <stdlib.h>] unless rbfiles.empty? 208 f.puts %Q[#include <mruby.h>] 209 f.puts %Q[#include <mruby/irep.h>] unless rbfiles.empty? 210 end 211 212 def print_gem_test_header(f) 213 print_gem_comment(f) 214 f.puts %Q[#include <stdio.h>] 215 f.puts %Q[#include <stdlib.h>] 216 f.puts %Q[#include <mruby.h>] 217 f.puts %Q[#include <mruby/irep.h>] 218 f.puts %Q[#include <mruby/variable.h>] 219 f.puts %Q[#include <mruby/hash.h>] unless test_args.empty? 220 end 221 222 def test_dependencies 223 [@name] 224 end 225 226 def custom_test_init? 227 @custom_test_init 228 end 229 230 def version_ok?(req_versions) 231 req_versions.map do |req| 232 cmp, ver = req.split 233 cmp_result = Version.new(version) <=> Version.new(ver) 234 case cmp 235 when '=' then cmp_result == 0 236 when '!=' then cmp_result != 0 237 when '>' then cmp_result == 1 238 when '<' then cmp_result == -1 239 when '>=' then cmp_result >= 0 240 when '<=' then cmp_result <= 0 241 when '~>' 242 Version.new(version).twiddle_wakka_ok?(Version.new(ver)) 243 else 244 fail "Comparison not possible with '#{cmp}'" 245 end 246 end.all? 247 end 248 end # Specification 249 250 class Version 251 include Comparable 252 include Enumerable 253 254 def <=>(other) 255 ret = 0 256 own = to_enum 257 258 other.each do |oth| 259 begin 260 ret = own.next <=> oth 261 rescue StopIteration 262 ret = 0 <=> oth 263 end 264 265 break unless ret == 0 266 end 267 268 ret 269 end 270 271 # ~> compare algorithm 272 # 273 # Example: 274 # ~> 2.2 means >= 2.2.0 and < 3.0.0 275 # ~> 2.2.0 means >= 2.2.0 and < 2.3.0 276 def twiddle_wakka_ok?(other) 277 gr_or_eql = (self <=> other) >= 0 278 still_minor = (self <=> other.skip_minor) < 0 279 gr_or_eql and still_minor 280 end 281 282 def skip_minor 283 a = @ary.dup 284 a.slice!(-1) 285 a[-1] = a[-1].succ 286 a 287 end 288 289 def initialize(str) 290 @str = str 291 @ary = @str.split('.').map(&:to_i) 292 end 293 294 def each(&block); @ary.each(&block); end 295 def [](index); @ary[index]; end 296 def []=(index, value) 297 @ary[index] = value 298 @str = @ary.join('.') 299 end 300 def slice!(index) 301 @ary.slice!(index) 302 @str = @ary.join('.') 303 end 304 end # Version 305 306 class List 307 include Enumerable 308 309 def initialize 310 @ary = [] 311 end 312 313 def each(&b) 314 @ary.each(&b) 315 end 316 317 def <<(gem) 318 unless @ary.detect {|g| g.dir == gem.dir } 319 @ary << gem 320 else 321 # GEM was already added to this list 322 end 323 end 324 325 def empty? 326 @ary.empty? 327 end 328 329 def default_gem_params dep 330 if dep[:default]; dep 331 elsif File.exist? "#{MRUBY_ROOT}/mrbgems/#{dep[:gem]}" # check core 332 { :gem => dep[:gem], :default => { :core => dep[:gem] } } 333 else # fallback to mgem-list 334 { :gem => dep[:gem], :default => { :mgem => dep[:gem] } } 335 end 336 end 337 338 def generate_gem_table build 339 gem_table = each_with_object({}) { |spec, h| h[spec.name] = spec } 340 341 default_gems = {} 342 each do |g| 343 g.dependencies.each do |dep| 344 default_gems[dep[:gem]] ||= default_gem_params(dep) 345 end 346 end 347 348 until default_gems.empty? 349 def_name, def_gem = default_gems.shift 350 next if gem_table[def_name] 351 352 spec = gem_table[def_name] = build.gem(def_gem[:default]) 353 fail "Invalid gem name: #{spec.name} (Expected: #{def_name})" if spec.name != def_name 354 spec.setup 355 356 spec.dependencies.each do |dep| 357 default_gems[dep[:gem]] ||= default_gem_params(dep) 358 end 359 end 360 361 each do |g| 362 g.dependencies.each do |dep| 363 name = dep[:gem] 364 req_versions = dep[:requirements] 365 dep_g = gem_table[name] 366 367 # check each GEM dependency against all available GEMs 368 if dep_g.nil? 369 fail "The GEM '#{g.name}' depends on the GEM '#{name}' but it could not be found" 370 end 371 unless dep_g.version_ok? req_versions 372 fail "#{name} version should be #{req_versions.join(' and ')} but was '#{dep_g.version}'" 373 end 374 end 375 376 cfls = g.conflicts.select { |c| 377 cfl_g = gem_table[c[:gem]] 378 cfl_g and cfl_g.version_ok?(c[:requirements] || ['>= 0.0.0']) 379 }.map { |c| "#{c[:gem]}(#{gem_table[c[:gem]].version})" } 380 fail "Conflicts of gem `#{g.name}` found: #{cfls.join ', '}" unless cfls.empty? 381 end 382 383 gem_table 384 end 385 386 def tsort_dependencies ary, table, all_dependency_listed = false 387 unless all_dependency_listed 388 left = ary.dup 389 until left.empty? 390 v = left.pop 391 table[v].dependencies.each do |dep| 392 left.push dep[:gem] 393 ary.push dep[:gem] 394 end 395 end 396 end 397 398 ary.uniq! 399 table.instance_variable_set :@root_gems, ary 400 class << table 401 include TSort 402 def tsort_each_node &b 403 @root_gems.each &b 404 end 405 406 def tsort_each_child(n, &b) 407 fetch(n).dependencies.each do |v| 408 b.call v[:gem] 409 end 410 end 411 end 412 413 begin 414 table.tsort.map { |v| table[v] } 415 rescue TSort::Cyclic => e 416 fail "Circular mrbgem dependency found: #{e.message}" 417 end 418 end 419 420 def check(build) 421 gem_table = generate_gem_table build 422 423 @ary = tsort_dependencies gem_table.keys, gem_table, true 424 425 each(&:setup_compilers) 426 427 each do |g| 428 import_include_paths(g) 429 end 430 end 431 432 def import_include_paths(g) 433 gem_table = each_with_object({}) { |spec, h| h[spec.name] = spec } 434 435 g.dependencies.each do |dep| 436 dep_g = gem_table[dep[:gem]] 437 # We can do recursive call safely 438 # as circular dependency has already detected in the caller. 439 import_include_paths(dep_g) 440 441 dep_g.export_include_paths.uniq! 442 g.compilers.each do |compiler| 443 compiler.include_paths += dep_g.export_include_paths 444 g.export_include_paths += dep_g.export_include_paths 445 compiler.include_paths.uniq! 446 g.export_include_paths.uniq! 447 end 448 end 449 end 450 end # List 451 end # Gem 452 453 GemBox = Object.new 454 class << GemBox 455 attr_accessor :path 456 457 def new(&block); block.call(self); end 458 def config=(obj); @config = obj; end 459 def gem(gemdir, &block); @config.gem(gemdir, &block); end 460 def gembox(gemfile); @config.gembox(gemfile); end 461 end # GemBox 462end # MRuby 463