mirror of
https://github.com/textmate/textmate.git
synced 2026-01-21 04:38:13 -05:00
972 lines
32 KiB
Ruby
Executable File
972 lines
32 KiB
Ruby
Executable File
#!/System/Library/Frameworks/Ruby.framework/Versions/Current/usr/bin/ruby
|
||
# == Synopsis
|
||
#
|
||
# gen_build: create build.ninja based on target files
|
||
#
|
||
# == Usage
|
||
#
|
||
# --help:
|
||
# show help.
|
||
#
|
||
# --output/-o «file»:
|
||
# the «file» to write build info into.
|
||
#
|
||
# --build-directory/-C «directory»:
|
||
# where build files should go.
|
||
#
|
||
# --define/-d «name»=«value»:
|
||
# define a variable that ends up in build.ninja.
|
||
$KCODE = 'U' if RUBY_VERSION < "1.9"
|
||
require 'optparse'
|
||
require 'shellwords'
|
||
require 'set'
|
||
require 'pp'
|
||
|
||
GLOB_KEYS = %w{ CP_Resources CP_SharedSupport CP_PlugIns CP_Library/QuickLook EXPORT HTML_FOOTER HTML_HEADER MARKDOWN_HEADER MARKDOWN_FOOTER PRELUDE SOURCES TARGETS TESTS TEST_SOURCES }
|
||
STRING_KEYS = %w{ TARGET_NAME BUNDLE_EXTENSION FLAGS C_FLAGS CXX_FLAGS OBJC_FLAGS OBJCXX_FLAGS LN_FLAGS PLIST_FLAGS BUILD }
|
||
|
||
COMPILER_INFO = {
|
||
'c' => { :rule => 'build_c', :compiler => '$CC', :flags => '$C_FLAGS', },
|
||
'm' => { :rule => 'build_m', :compiler => '$CC', :flags => '$OBJC_FLAGS', },
|
||
'cc' => { :rule => 'build_cc', :compiler => '$CXX', :flags => '$CXX_FLAGS', },
|
||
'mm' => { :rule => 'build_mm', :compiler => '$CXX', :flags => '$OBJCXX_FLAGS', },
|
||
}
|
||
|
||
class Targets
|
||
include Enumerable
|
||
|
||
attr_reader :root
|
||
|
||
def initialize(path)
|
||
targets = all_targets(path)
|
||
linked = Set.new(targets.collect { |target| target['LINK'].to_a }.flatten)
|
||
|
||
@root = targets.first
|
||
@all_targets = targets
|
||
@all = [ ]
|
||
|
||
targets.each do |target|
|
||
next if target['SOURCES'].to_a.empty?
|
||
@all << target
|
||
if linked.include?(target[:name]) || target['BUILD'] == 'lib'
|
||
target[:type] = :library
|
||
else
|
||
has_info_plist = target['CP_Resources'].to_a.find { |path| path =~ /\bInfo\.plist$/ }
|
||
target[:type] = has_info_plist ? :bundle : :executable
|
||
end
|
||
end
|
||
end
|
||
|
||
def target_named(name)
|
||
res = self.find { |target| target[:name] == name }
|
||
return res unless res.nil?
|
||
abort "*** no target named ‘#{name}’. Did you update submodules?\n"
|
||
end
|
||
|
||
def each(&block)
|
||
@all.each(&block)
|
||
end
|
||
|
||
def dependencies
|
||
paths = [ ]
|
||
@all_targets.each do |target|
|
||
paths << target[:full_path] << target[:depeends_on_directories].to_a
|
||
target.each do |key, value|
|
||
paths << value.reject { |path| path =~ /^@/ }.map do |path|
|
||
File.dirname(File.join(target[:base], path))
|
||
end if GLOB_KEYS.include?(key) && key != 'PRELUDE'
|
||
end
|
||
end
|
||
|
||
paths.flatten.sort.uniq
|
||
end
|
||
|
||
private
|
||
|
||
def parse(path, root_dir, parent = { })
|
||
base = File.dirname(path)
|
||
dict = parent.merge({ :name => File.basename(base), :base => base, :path => base.sub(/^#{Regexp.escape root_dir}\//, ''), :full_path => path })
|
||
dict.delete('TARGETS')
|
||
|
||
data = File.read(path)
|
||
assignments = data.scan(/^([\w\/]+)\s*(\+=|-=|=)[ \t]*(.*)$/)
|
||
assignments.each do |arr|
|
||
key, op, value = *arr
|
||
value = Shellwords.shellwords(value) unless STRING_KEYS.include? key
|
||
|
||
if GLOB_KEYS.include?(key)
|
||
globs = value.reject { |v| v =~ /^@/ }
|
||
targets = value.reject { |v| v !~ /^@/ }
|
||
|
||
Dir.chdir(File.dirname(path)) do
|
||
value = Dir.glob(globs)
|
||
value = value.concat(targets)
|
||
end
|
||
end
|
||
|
||
oldValue = dict[key]
|
||
if STRING_KEYS.include? key
|
||
if op == '+='
|
||
value = oldValue.nil? ? value : "#{oldValue} #{value}"
|
||
elsif op == '-='
|
||
value = oldValue.to_s.gsub(/(^| )#{Regexp.escape value}( |$)/, ' ')
|
||
end
|
||
else
|
||
if op == '+='
|
||
value = oldValue.dup.concat(value) unless oldValue.nil?
|
||
elsif op == '-='
|
||
abort "Operator -= not implemented for non-string keys"
|
||
end
|
||
end
|
||
|
||
dict[key] = value
|
||
end
|
||
|
||
if dict.has_key?('TARGET_NAME')
|
||
dict[:name] = dict['TARGET_NAME']
|
||
dict.delete('TARGET_NAME')
|
||
end
|
||
|
||
dict
|
||
end
|
||
|
||
def recursive_parse(target, root_dir)
|
||
res = [ target ]
|
||
target["TARGETS"].to_a.each do |subtarget|
|
||
res << recursive_parse(parse(File.join(target[:base], subtarget), root_dir, target), root_dir)
|
||
end
|
||
res
|
||
end
|
||
|
||
def all_targets(path)
|
||
recursive_parse(parse(path, File.dirname(path)), File.dirname(path)).flatten
|
||
end
|
||
end
|
||
|
||
def esc_path(path)
|
||
if path.class == String
|
||
return path if path =~ /^\$builddir/
|
||
path.gsub(/[ :$]/, '$\\&')
|
||
else
|
||
path.map { |obj| esc_path(obj) }
|
||
end
|
||
end
|
||
|
||
def build_path(path)
|
||
"$builddir/#{path.gsub(/[ :$]/, '$\\&')}"
|
||
end
|
||
|
||
def prepend(prefix, array, sep = ' ')
|
||
array.map do |item|
|
||
item =~ %r{/} ? item : esc_path("#{prefix}#{item}")
|
||
end.join(sep)
|
||
end
|
||
|
||
def flags_for_target(target)
|
||
res = ""
|
||
target.each do |key, value|
|
||
res << " #{key} = #{value}\n" unless key.class != String || key !~ /FLAGS$/ || value == TARGETS.root[key]
|
||
end
|
||
res
|
||
end
|
||
|
||
def source_type(src)
|
||
srcExt = src[/\b[a-z\+]+$/]
|
||
srcExt == 'c++' ? 'cc' : srcExt
|
||
end
|
||
|
||
def compiler_for(src)
|
||
COMPILER_INFO[source_type(src)][:rule]
|
||
end
|
||
|
||
def pch_for(src, target)
|
||
srcExt = source_type(src)
|
||
target['PRELUDE'].each do |pch|
|
||
if srcExt == source_type(pch)
|
||
res = build_path(pch)
|
||
res << "-no-arc" if srcExt == 'm' && target['OBJC_FLAGS'] =~ /-fno-objc-arc/
|
||
res << "-no-arc" if srcExt == 'mm' && target['OBJCXX_FLAGS'] =~ /-fno-objc-arc/
|
||
return res
|
||
end
|
||
end
|
||
abort "*** no pre-compiled header for #{src} (target: #{target[:name]})"
|
||
end
|
||
|
||
def all_values_for_key(target, key)
|
||
all_targets = { }
|
||
new_targets = [ target ]
|
||
until new_targets.empty?
|
||
target = new_targets.pop
|
||
unless all_targets.include? target[:base]
|
||
all_targets[target[:base]] = target
|
||
target['LINK'].to_a.each do |name|
|
||
TARGETS.each { |target| new_targets << target if name == target[:name] }
|
||
end
|
||
end
|
||
end
|
||
|
||
all_targets.map { |path, target| target[key].to_a }.flatten.sort.uniq
|
||
end
|
||
|
||
def headers(target)
|
||
res = ''
|
||
return res if target['EXPORT'].to_s.empty?
|
||
headers = [ ]
|
||
target['EXPORT'].to_a.each do |path|
|
||
src = File.join(target[:path], path)
|
||
dst = build_path("include/#{target[:name]}/#{File.basename(path)}")
|
||
res << "build #{dst}: cp #{esc_path src}\n"
|
||
headers << dst
|
||
end
|
||
headers = headers.concat(target['LINK'].map { |lib| "#{esc_path lib}/headers" }) unless target['LINK'].nil?
|
||
res << "build #{target[:name]}/headers: phony | #{headers.join(' ')}\n"
|
||
res
|
||
end
|
||
|
||
def sources(target)
|
||
res = ''
|
||
objects = [ ]
|
||
headers = target['LINK'].to_a.map { |goal| "#{goal}/headers" }
|
||
target['SOURCES'].each do |path|
|
||
src = File.join(target[:path], path)
|
||
case src
|
||
when /^(.*)\.(c|cc|c\+\+|m|mm)$/ then
|
||
base, ext = $1, $2
|
||
cc = compiler_for(src)
|
||
dst = build_path("#{base}.o")
|
||
res << "build #{dst}: #{cc} #{esc_path src} | #{pch_for(src, target)}.gch || #{esc_path(headers).join(' ')}\n"
|
||
res << " PCH_FLAGS = -D#{target[:name].gsub(/[^A-Za-z_]/, '_')}_EXPORTS -include #{pch_for(src, target)}\n"
|
||
res << flags_for_target(target)
|
||
|
||
objects << dst
|
||
|
||
when /^(.*)\.(rl)$/ then
|
||
base, ext = $1, $2
|
||
dst = build_path("#{base}.cc")
|
||
res << "build #{dst}: gen_ragel #{esc_path src} || #{esc_path(headers).join(' ')}\n"
|
||
|
||
ext = 'cc'
|
||
cc = compiler_for(dst)
|
||
obj = build_path("#{base}.o")
|
||
res << "build #{obj}: #{cc} #{dst} | #{pch_for(dst, target)}.gch || #{esc_path(headers).join(' ')}\n"
|
||
res << " PCH_FLAGS = -D#{target[:name].gsub(/[^A-Za-z_]/, '_')}_EXPORTS -include #{pch_for(dst, target)} -iquote#{esc_path File.dirname(src)}\n"
|
||
res << flags_for_target(target)
|
||
|
||
objects << obj
|
||
|
||
when /^(.*)\.capnp$/ then
|
||
base = $1
|
||
res << "build #{src}.c++ #{src}.h: gen_capnp #{esc_path src} || #{esc_path(headers).join(' ')}\n"
|
||
|
||
src = "#{src}.c++"
|
||
cc = compiler_for(src)
|
||
dst = build_path("#{base}.o")
|
||
res << "build #{dst}: #{cc} #{esc_path src} | #{pch_for(src, target)}.gch || #{esc_path(headers).join(' ')}\n"
|
||
res << " PCH_FLAGS = -D#{target[:name].gsub(/[^A-Za-z_]/, '_')}_EXPORTS -include #{pch_for(src, target)}\n"
|
||
res << flags_for_target(target)
|
||
|
||
objects << dst
|
||
|
||
else
|
||
STDERR << "*** unhandled source type ‘#{path}’\n"
|
||
end
|
||
end
|
||
target[:objects] = objects
|
||
|
||
res
|
||
end
|
||
|
||
def clean(target)
|
||
res = ''
|
||
dst = build_path(File.join(target[:path]))
|
||
res << "build #{dst}/#{target[:name]}.clean: clean\n"
|
||
res << " path = #{dst}\n"
|
||
res << "build #{target[:name]}/clean: phony #{dst}/#{target[:name]}.clean\n"
|
||
end
|
||
|
||
def tests(target)
|
||
headers = target['LINK'].to_a.map { |goal| "#{goal}/headers" }
|
||
fws = prepend('-framework ', all_values_for_key(target, 'FRAMEWORKS'))
|
||
libs = prepend('-l', all_values_for_key(target, 'LIBS'))
|
||
|
||
res = ''
|
||
|
||
if !target['TESTS'].to_s.empty?
|
||
dst = build_path("#{target[:path]}/test_#{target[:name]}")
|
||
|
||
src = target['TESTS'].map { |path| esc_path(File.join(target[:path], path)) }.join(' ')
|
||
ext = target['TESTS'].any? { |path| path =~ /\.mm$/ } ? 'mm' : 'cc'
|
||
test_src = "#{dst}.#{ext}"
|
||
|
||
res << "build #{test_src}: gen_oak_test #{src} | bin/gen_test\n"
|
||
res << "build #{dst}.o: #{compiler_for(test_src)} #{test_src} | #{pch_for(test_src, target)}.gch || #{target[:name]}/headers\n"
|
||
res << " PCH_FLAGS = -include #{pch_for(test_src, target)}\n"
|
||
res << flags_for_target(target)
|
||
res << "build #{dst}: link_oak_test #{all_values_for_key(target, :objects).join(' ')} #{dst}.o\n"
|
||
res << flags_for_target(target)
|
||
res << " RAVE_FLAGS = #{fws} #{libs}\n"
|
||
|
||
res << "build #{dst}.run: run_test #{dst}\n"
|
||
res << "build #{dst}.always-run: always_run_test #{dst}\n"
|
||
res << "build #{dst}.coerce: skip_test #{dst}\n"
|
||
res << " seal = #{dst}.run\n"
|
||
|
||
res << "build #{target[:name]}: phony #{dst}.run\n"
|
||
res << "build #{target[:name]}/test: phony #{dst}.always-run\n"
|
||
res << "build #{target[:name]}/coerce: phony #{dst}.coerce\n"
|
||
|
||
target[:test] = "#{dst}.run"
|
||
end
|
||
|
||
if !target['TEST_SOURCES'].to_s.empty?
|
||
dst = build_path("#{target[:path]}/cxx_test_#{target[:name]}")
|
||
|
||
src = target['TEST_SOURCES'].map { |path| esc_path(File.join(target[:path], path)) }.join(' ')
|
||
ext = target['TEST_SOURCES'].any? { |path| path =~ /\.mm$/ } ? 'mm' : 'cc'
|
||
test_src = "#{dst}.#{ext}"
|
||
|
||
res << "build #{test_src}: gen_cxx_test #{src}\n"
|
||
res << "build #{dst}.o: #{compiler_for(test_src)} #{test_src} | #{pch_for(test_src, target)}.gch || #{target[:name]}/headers\n"
|
||
res << " PCH_FLAGS = -Ibin/CxxTest -include #{pch_for(test_src, target)}\n"
|
||
res << flags_for_target(target)
|
||
res << "build #{dst}: link_cxx_test #{all_values_for_key(target, :objects).join(' ')} #{dst}.o\n"
|
||
res << flags_for_target(target)
|
||
res << " RAVE_FLAGS = #{fws} #{libs}\n"
|
||
|
||
res << "build #{dst}.run: run_test #{dst}\n"
|
||
res << "build #{dst}.always-run: always_run_test #{dst}\n"
|
||
res << "build #{target[:name]}/cxx_test: phony #{dst}.always-run\n"
|
||
end
|
||
|
||
res
|
||
end
|
||
|
||
def resources_helper(target, key, symbol)
|
||
res = ''
|
||
rsrc = [ ]
|
||
all_resources = [ ]
|
||
|
||
depeends_on_directories = Array.new(target[:depeends_on_directories].to_a)
|
||
|
||
target[key].to_a.each do |path|
|
||
if path =~ /^@/
|
||
src = TARGETS.target_named($')
|
||
abort "*** no resources in target: ‘#{path}’ (from target: #{target[:name]}).\n" unless src.has_key?(:rsrc_info)
|
||
src[:rsrc_info].each do |info|
|
||
rsrc << { :src => info[:src], :dst => esc_path(info[:install_name]) }
|
||
end
|
||
else
|
||
src = File.join(target[:path], path)
|
||
abs = File.join(target[:base], path)
|
||
if File.directory?(abs) && !File.symlink?(abs) && abs !~ /\.xcdatamodeld$/
|
||
depeends_on_directories << abs
|
||
Dir.chdir(abs) { Dir.glob("**/*") }.each do |file|
|
||
if File.directory?(File.join(abs, file))
|
||
depeends_on_directories << File.join(abs, file)
|
||
else
|
||
all_resources << { :path => File.dirname(src), :file => File.join(File.basename(src), file) }
|
||
end
|
||
end
|
||
else
|
||
all_resources << { :path => File.dirname(src), :file => File.basename(src) }
|
||
end
|
||
end
|
||
end
|
||
|
||
target[:depeends_on_directories] = depeends_on_directories.sort.uniq
|
||
|
||
all_resources.each do |dict|
|
||
path, file = dict[:path], dict[:file]
|
||
src = File.join(path, file)
|
||
|
||
case file
|
||
when /(.+)\.xib$/ then
|
||
dst = build_path(File.join(path, "#$1.nib"))
|
||
res << "build #{dst}: compile_xib #{esc_path src}\n"
|
||
rsrc << { :src => dst, :dst => esc_path("#$1.nib") }
|
||
when /(.+)\.strings$/ then
|
||
dst = build_path(File.join(path, file))
|
||
res << "build #{dst}: cp_as_utf16 #{esc_path src}\n"
|
||
rsrc << { :src => dst, :dst => esc_path(file) }
|
||
when /(.+)\.xcdatamodeld$/ then
|
||
dst = build_path(File.join(path, "#$1.mom"))
|
||
res << "build #{dst}: compile_mom #{esc_path src}\n"
|
||
rsrc << { :src => dst, :dst => esc_path("#$1.mom") }
|
||
when /\bInfo\.plist$/ then
|
||
dst = build_path(File.join(path, file))
|
||
res << "build #{dst}: process_plist #{esc_path src} | bin/process_plist\n"
|
||
res << " RAVE_FLAGS = -dTARGET_NAME='#{target[:name]}'\n"
|
||
res << flags_for_target(target)
|
||
target[:info_plist] = dst
|
||
when /(.+)\.md(own)?$/ then
|
||
dst = build_path(File.join(path, "#$1.html"))
|
||
prefix, suffix = target['MARKDOWN_HEADER'].to_a, target['MARKDOWN_FOOTER'].to_a
|
||
res << "build #{dst}: markdown #{prefix.map { |path| esc_path(File.join(target[:path], path))}.join(' ')} #{esc_path src} #{suffix.map { |path| esc_path(File.join(target[:path], path))}.join(' ')} | bin/gen_html\n"
|
||
header, footer = target['HTML_HEADER'], target['HTML_FOOTER']
|
||
flags = ''
|
||
flags << " -h #{esc_path(File.join(target[:path], header.first))}" if header.to_a.size == 1
|
||
flags << " -f #{esc_path(File.join(target[:path], footer.first))}" if footer.to_a.size == 1
|
||
res << " md_flags =#{flags}\n"
|
||
rsrc << { :src => dst, :dst => esc_path("#$1.html") }
|
||
else
|
||
rsrc << { :src => esc_path(src), :dst => esc_path(file) }
|
||
end
|
||
end
|
||
|
||
help_files = rsrc.find_all { |dict| dict[:src] =~ %r{[^/]+ Help/.+\.html$} }
|
||
unless help_files.empty?
|
||
src = "#$&/#{target[:name]}.helpindex" if help_files.first[:src] =~ /.* Help(?=\/)/
|
||
dst = "#$&/#{target[:name]}.helpindex" if help_files.first[:dst] =~ /.* Help(?=\/)/
|
||
|
||
res << "build #{src}: index_help | #{help_files.map { |dict| dict[:src] }.join(' ')}\n"
|
||
res << " HELP_BOOK = #{File.dirname(src)}\n"
|
||
|
||
rsrc << { :src => src, :dst => dst }
|
||
end
|
||
|
||
target[symbol] = rsrc
|
||
|
||
res
|
||
end
|
||
|
||
def resources(target)
|
||
res = ''
|
||
target.keys.each do |key|
|
||
res << resources_helper(target, key, key.to_sym) if key.class == String && key =~ /^CP_/
|
||
end
|
||
res
|
||
end
|
||
|
||
def object_archive(target)
|
||
dst = build_path("#{target[:path]}/lib#{target[:name]}.a")
|
||
target[:lib] = dst
|
||
res = ''
|
||
res << "build #{dst}: link_static #{target[:objects].join(' ')} | #{target[:test]}\n"
|
||
res << flags_for_target(target)
|
||
end
|
||
|
||
def dynamic_lib(target)
|
||
dst = target[:dylib] = build_path("#{target[:path]}/#{target[:name]}.dylib")
|
||
|
||
if target[:CP_Resources].to_a.empty?
|
||
install_name = "@rpath/#{target[:name]}.dylib"
|
||
else
|
||
install_name = "@rpath/#{target[:name]}.framework/Versions/A/#{target[:name]}"
|
||
end
|
||
|
||
link_with = all_values_for_key(target, 'LINK').map { |name| TARGETS.target_named(name)[:dylib] }
|
||
|
||
res = ''
|
||
res << "build #{dst}: link_dynamic #{target[:objects].join(' ')} | #{target[:test]} #{link_with.join(' ')}\n"
|
||
res << flags_for_target(target)
|
||
fws = prepend('-framework ', all_values_for_key(target, 'FRAMEWORKS'))
|
||
libs = prepend('-l', all_values_for_key(target, 'LIBS'))
|
||
res << " RAVE_FLAGS = -install_name #{esc_path install_name} #{link_with.join(' ')} #{fws} #{libs}\n"
|
||
end
|
||
|
||
def framework_bundle(target, dst_dir = nil)
|
||
res = ''
|
||
dst = File.join(dst_dir || build_path("#{target[:path]}"), "#{target[:name]}.framework")
|
||
|
||
deps = [ ]
|
||
|
||
target.each do |key, files|
|
||
if key.class == Symbol && key.to_s =~ /^CP_(.+)$/
|
||
install_dir = $1
|
||
files.each do |rsrc|
|
||
deps << "#{dst}/Versions/A/#{install_dir}/#{rsrc[:dst]}"
|
||
res << "build #{deps.last}: cp #{rsrc[:src]}\n"
|
||
end
|
||
end
|
||
end
|
||
|
||
deps << "#{dst}/Versions/A/Resources/Info.plist"
|
||
res << "build #{deps.last}: cp #{target[:info_plist]}\n"
|
||
|
||
deps << "#{dst}/Versions/A/#{target[:name]}"
|
||
res << "build #{deps.last}: cp #{target[:dylib]}\n"
|
||
|
||
deps << "#{dst}/Versions/Current"
|
||
res << "build #{deps.last}: ln\n link = A\n"
|
||
deps << "#{dst}/Resources"
|
||
res << "build #{deps.last}: ln\n link = Versions/Current/Resources\n"
|
||
deps << "#{dst}/#{target[:name]}"
|
||
res << "build #{deps.last}: ln\n link = Versions/Current/#{target[:name]}\n"
|
||
|
||
res << "build #{dst}: phony | #{deps.join(' ')}\n"
|
||
end
|
||
|
||
def static_executable(target)
|
||
objects = [ ]
|
||
all_values_for_key(target, 'LINK').each do |name|
|
||
objects << TARGETS.target_named(name)[:lib]
|
||
end
|
||
objects << target[:objects]
|
||
|
||
res = ''
|
||
|
||
dst = build_path("#{target[:path]}/#{target[:name]}")
|
||
target[:static_executable] = dst
|
||
target[:rsrc_info] = [ { :install_name => "#{target[:name]}", :src => dst } ]
|
||
|
||
res << "build #{dst}: link_executable #{objects.flatten.join(' ')}\n"
|
||
res << flags_for_target(target)
|
||
fws = prepend('-framework ', all_values_for_key(target, 'FRAMEWORKS'))
|
||
libs = prepend('-l', all_values_for_key(target, 'LIBS'))
|
||
res << " RAVE_FLAGS = #{fws} #{libs}\n"
|
||
|
||
res << "build #{dst}.run: run_executable #{dst}\n"
|
||
res << "build #{dst}.debug: debug_executable #{dst}\n"
|
||
|
||
res << "build #{target[:name]}: phony #{dst}\n"
|
||
res << "build #{target[:name]}/run: phony #{dst}.run\n"
|
||
res << "build #{target[:name]}/debug: phony #{dst}.debug\n"
|
||
|
||
res
|
||
end
|
||
|
||
def dynamic_executable(target)
|
||
res = ''
|
||
|
||
link_with = all_values_for_key(target, 'LINK').map { |name| TARGETS.target_named(name)[:dylib] }
|
||
|
||
dst = build_path("#{target[:path]}/#{target[:name]}.dyn")
|
||
target[:dynamic_executable] = dst
|
||
|
||
res << "build #{dst}: link_executable #{target[:objects].join(' ')} | #{link_with.join(' ')}\n"
|
||
res << flags_for_target(target)
|
||
fws = prepend('-framework ', all_values_for_key(target, 'FRAMEWORKS'))
|
||
libs = prepend('-l', all_values_for_key(target, 'LIBS'))
|
||
res << " RAVE_FLAGS = #{link_with.join(' ')} #{fws} #{libs}\n"
|
||
|
||
res
|
||
end
|
||
|
||
def app_bundle(target)
|
||
res = ''
|
||
deps = [ ]
|
||
all_files = [ ]
|
||
ext = target['BUNDLE_EXTENSION'] || 'app'
|
||
dst = build_path("#{target[:path]}/#{target[:name]}.#{ext}")
|
||
target[:bundle] = dst
|
||
|
||
fw_dst = File.join(dst, "Contents/Frameworks")
|
||
all_values_for_key(target, 'LINK').each do |name|
|
||
tmp = TARGETS.target_named(name)
|
||
if tmp[:CP_Resources].to_a.empty?
|
||
res << "build #{fw_dst}/#{tmp[:name]}.dylib: cp #{tmp[:dylib]}\n"
|
||
deps << "#{fw_dst}/#{tmp[:name]}.dylib"
|
||
else
|
||
res << framework_bundle(tmp, fw_dst)
|
||
deps << "#{fw_dst}/#{tmp[:name]}.framework"
|
||
end
|
||
end
|
||
|
||
target.each do |key, files|
|
||
if key.class == Symbol && key.to_s =~ /^CP_(.+)$/
|
||
install_dir = $1
|
||
files.each do |rsrc|
|
||
deps << "#{dst}/Contents/#{install_dir}/#{rsrc[:dst]}"
|
||
res << "build #{deps.last}: cp #{rsrc[:src]}\n"
|
||
all_files << { :install_name => "#{target[:name]}.#{ext}/Contents/#{install_dir}/#{rsrc[:dst]}", :src => rsrc[:src] }
|
||
end
|
||
end
|
||
end
|
||
|
||
deps << "#{dst}/Contents/Info.plist"
|
||
res << "build #{deps.last}: cp #{target[:info_plist]}\n"
|
||
all_files << { :install_name => "#{target[:name]}.#{ext}/Contents/Info.plist", :src => target[:info_plist] }
|
||
|
||
deps << "#{dst}/Contents/MacOS/#{target[:name]}"
|
||
res << "build #{deps.last}: cp #{target[:dynamic_executable]}\n"
|
||
all_files << { :install_name => "#{target[:name]}.#{ext}/Contents/MacOS/#{target[:name]}", :src => target[:dynamic_executable] }
|
||
|
||
res << "build #{dst}: phony | #{deps.join(' ')}\n"
|
||
|
||
res << "build #{dst}.sign: sign_executable #{dst}\n"
|
||
res << "build #{dst}.run: run_application #{dst} | #{dst}.sign\n"
|
||
res << " appname = #{target[:name]}\n"
|
||
res << "build #{dst}.debug: debug_executable #{dst}/Contents/MacOS/#{target[:name]} | #{dst}.sign\n"
|
||
|
||
res << "build #{target[:name]}: phony #{dst}.sign\n"
|
||
res << "build #{target[:name]}/run: phony #{dst}.run\n"
|
||
res << "build #{target[:name]}/debug: phony #{dst}.debug\n"
|
||
|
||
target[:rsrc_info] = all_files
|
||
|
||
res
|
||
end
|
||
|
||
def upload(target)
|
||
res = ''
|
||
exe = target[:static_executable]
|
||
dir, _ = File.split(exe)
|
||
|
||
name = "#{target[:name]}_${APP_VERSION}"
|
||
tbz = "#{dir}/archive/#{name}.tbz"
|
||
meta = "#{dir}/meta/#{name}"
|
||
|
||
res << "build #{exe}.sign: sign_executable #{exe}\n"
|
||
res << "build #{tbz}: tbz_archive #{exe} | #{exe}.sign\n"
|
||
res << "build #{meta}.upload: upload #{tbz}\n"
|
||
|
||
res << "build #{target[:name]}/deploy: phony #{meta}.upload\n"
|
||
end
|
||
|
||
def deploy(target)
|
||
res = ''
|
||
dir, _ = File.split(target[:bundle])
|
||
|
||
name = "#{target[:name]}_${APP_VERSION}"
|
||
dsym = "#{dir}/dsym/#{name}.tbz"
|
||
tbz = "#{dir}/archive/#{name}.tbz"
|
||
meta = "#{dir}/meta/#{name}"
|
||
|
||
res << "build #{dsym}: dsym #{target[:bundle]}\n"
|
||
res << "build #{tbz}: tbz_archive #{target[:bundle]} | #{target[:bundle]}.sign\n"
|
||
res << "build #{meta}.upload: upload #{tbz}\n"
|
||
res << "build #{meta}.deploy: deploy #{meta}.upload | #{dsym}\n"
|
||
|
||
res << "build #{target[:name]}/dsym: phony #{dsym}\n"
|
||
res << "build #{target[:name]}/tbz: phony #{tbz}\n"
|
||
res << "build #{target[:name]}/upload: phony #{meta}.upload\n"
|
||
res << "build #{target[:name]}/deploy: phony #{meta}.deploy\n"
|
||
end
|
||
|
||
def all_targets(buildfile, builddir)
|
||
res = Set.new
|
||
return res unless File.exists? buildfile
|
||
|
||
Dir.chdir(File.dirname(buildfile)) do
|
||
targets = %x{ ${TM_NINJA:-ninja} -f #{buildfile.shellescape} -t targets all }
|
||
return nil if $? != 0
|
||
targets.each_line do |line|
|
||
if line =~ /.*(?=:(?! phony))/
|
||
path = $&
|
||
res << path if path =~ /^#{Regexp.escape(builddir)}/ # ignore targets outside builddir
|
||
end
|
||
end
|
||
end
|
||
|
||
res
|
||
end
|
||
|
||
# =======================
|
||
# = Program Starts Here =
|
||
# =======================
|
||
|
||
outfile, builddir = '/dev/stdout', File.expand_path('~/build')
|
||
variables = [ ]
|
||
|
||
OptionParser.new do |opts|
|
||
opts.banner = "Usage: gen_build [options] "
|
||
opts.separator "Synopsis"
|
||
opts.separator "gen_build: create build.ninja based on target files"
|
||
opts.separator "Options:"
|
||
|
||
opts.on("-h","--help", "show help.") do |v|
|
||
puts opts
|
||
exit
|
||
end
|
||
|
||
opts.on("-o","--output FILE", "the «file» to write build info into.") do |v|
|
||
outfile = v
|
||
end
|
||
|
||
opts.on("-C", "--build-directory DIRECTORY", "where build files should go.") do |v|
|
||
builddir = v
|
||
end
|
||
|
||
opts.on("-d", "--define NAME=VALUE", "define a variable that ends up in build.ninja") do |v|
|
||
variables << [ $1, $2 ] if v =~ /^(\w+)\s*=\s*(.*)$/
|
||
end
|
||
end.parse!
|
||
|
||
abort "No root target file specified" if ARGV.empty?
|
||
manifest = ARGV[0]
|
||
TARGETS = Targets.new(manifest)
|
||
|
||
all_old_targets = all_targets(outfile, builddir)
|
||
|
||
open(outfile, "w") do |io|
|
||
userfile = "#{ENV['USER']}.ninja"
|
||
io << "ninja_required_version = 1.5\n"
|
||
io << "\n"
|
||
io << "###########################################\n"
|
||
io << "# AUTOMATICALLY GENERATED -- DO NOT EDIT! #\n"
|
||
io << "###########################################\n"
|
||
io << "\n"
|
||
io << "builddir = #{builddir}\n"
|
||
io << "include $builddir/build.ninja\n"
|
||
io << "include #{userfile}\n" if userfile != 'build.ninja' && File.exists?(File.join(File.dirname(outfile), userfile))
|
||
end
|
||
|
||
open("#{builddir}/build.ninja", "w") do |io|
|
||
user_variables = []
|
||
TARGETS.root.each do |key, value|
|
||
user_variables << [ key, value.kind_of?(Array) ? value.join(' ') : value ] unless key.class != String || GLOB_KEYS.include?(key)
|
||
end
|
||
user_variables.sort! { |lhs, rhs| lhs[0] <=> rhs[0] }
|
||
|
||
tmp = variables.dup.concat(user_variables)
|
||
width = tmp.map { |arr| arr[0].length }.max { |lhs, rhs| lhs <=> rhs }
|
||
|
||
variables.each { |arr| io << format("%-#{width}s = %s\n", *arr) }
|
||
io << "\n"
|
||
|
||
user_variables.each { |arr| io << format("%-#{width}s = %s\n", *arr) }
|
||
io << "\n"
|
||
|
||
args = variables.map { |key, value| "-d'#{key}=#{value =~ /\$/ ? value.gsub(/\$/, '$$') : "$#{key}"}'" }.join(' ')
|
||
io << "rule configure\n"
|
||
io << " command = bin/gen_build -C \"$builddir\" #{args} -o $out $in\n"
|
||
io << " depfile = $builddir/$out.d\n"
|
||
io << " generator = true\n"
|
||
io << " description = Generate ‘$out’…\n"
|
||
io << "\n"
|
||
io << "build build.ninja: configure #{manifest} | bin/gen_build\n"
|
||
io << "\n"
|
||
|
||
io << DATA.read << "\n"
|
||
|
||
COMPILER_INFO.each do |key, value|
|
||
io << "rule #{value[:rule]}\n"
|
||
io << " command = '#{value[:compiler]}' $PCH_FLAGS $FLAGS #{value[:flags]} $RAVE_FLAGS -o $out -MMD -MF $out.d -I$builddir/include $in\n"
|
||
io << " depfile = $out.d\n"
|
||
io << " deps = gcc\n"
|
||
io << " description = Compile ‘$in’…\n\n"
|
||
end
|
||
|
||
TARGETS.root['PRELUDE'].each do |src|
|
||
type = { 'c' => '-x c-header', 'm' => '-x objective-c-header', 'cc' => '-x c++-header', 'mm' => '-x objective-c++-header' }
|
||
ext = source_type(src)
|
||
cc = COMPILER_INFO[ext][:compiler]
|
||
|
||
io << "build #{pch_for(src, TARGETS.root)}.gch: #{compiler_for(src)} #{src} | #{cc}\n"
|
||
io << " RAVE_FLAGS = #{type[ext]}\n\n"
|
||
|
||
if ext =~ /^mm?$/
|
||
io << "build #{pch_for(src, TARGETS.root)}-no-arc.gch: #{compiler_for(src)} #{src} | #{cc}\n"
|
||
io << " RAVE_FLAGS = #{type[ext]} -fno-objc-arc\n\n"
|
||
end
|
||
end
|
||
|
||
# =======================================================
|
||
# = Do a topological sort based on the dependency graph =
|
||
# =======================================================
|
||
|
||
dag = ''
|
||
TARGETS.each do |target|
|
||
dag << "#{target[:name]}\t#{target[:name]}\n" if target['LINK'].nil?
|
||
target['LINK'].each { |dep| dag << "#{target[:name]}\t#{dep}\n" } unless target['LINK'].nil?
|
||
target.each do |key, value|
|
||
value.each { |rsrc| dag << "#{target[:name]}\t#$'\n" if rsrc =~ /^@/ } if key =~ /^CP_/
|
||
end
|
||
end
|
||
dag = open('|/usr/bin/tsort -q', 'r+') { |tsort| tsort << dag; tsort.close_write; tsort.read }
|
||
|
||
# =========================================================================
|
||
# = Now iterate the targets (in the sorted order) to generate build rules =
|
||
# =========================================================================
|
||
|
||
dag.scan(/.+/).reverse_each do |name|
|
||
target = TARGETS.target_named(name)
|
||
|
||
io << clean(target)
|
||
io << sources(target)
|
||
io << resources(target)
|
||
|
||
if target[:type] == :library
|
||
io << headers(target)
|
||
io << object_archive(target)
|
||
io << tests(target)
|
||
io << dynamic_lib(target)
|
||
elsif target[:type] == :executable
|
||
io << static_executable(target)
|
||
io << upload(target)
|
||
else
|
||
io << dynamic_executable(target)
|
||
io << app_bundle(target)
|
||
io << deploy(target)
|
||
end
|
||
end
|
||
|
||
io << "\n"
|
||
io << "build run: phony #{variables.find { |k, _| k == 'APP_NAME' }.last}/run\n"
|
||
io << "build deploy: phony #{variables.find { |k, _| k == 'APP_NAME' }.last}/deploy\n"
|
||
io << "build debug: phony #{variables.find { |k, _| k == 'APP_NAME' }.last}/debug\n"
|
||
io << "default run\n"
|
||
end
|
||
|
||
open("#{builddir}/build.ninja.d", "w") do |io|
|
||
deps = TARGETS.dependencies.map { |path| path.gsub(/ /, '\\ ') }
|
||
io << outfile << ": " << deps.join(" \\\n ") << "\n"
|
||
end
|
||
|
||
all_new_targets = all_targets(outfile, builddir)
|
||
|
||
if all_old_targets && all_new_targets
|
||
targets_lost = all_old_targets - all_new_targets
|
||
targets_lost.each do |path|
|
||
if File.exists?(path)
|
||
STDERR << "Remove old target ‘#{path.sub(/#{Regexp.escape(builddir)}/, '$builddir')}’…\n"
|
||
File.unlink(path)
|
||
end
|
||
end
|
||
end
|
||
|
||
# =======================
|
||
|
||
__END__
|
||
rule touch
|
||
command = touch $out
|
||
generator = true
|
||
description = Touch ‘$out’…
|
||
|
||
rule cp
|
||
command = cp -p $in $out
|
||
description = Copy ‘$in’…
|
||
|
||
rule cp_as_utf16
|
||
command = if [[ $$(head -c2 $in) == $$'\xFF\xFE' || $$(head -c2 $in) == $$'\xFE\xFF' ]]; then cp -p $in $out; else iconv -f utf-8 -t utf-16 < $in > $out~ && mv $out~ $out; fi
|
||
description = Copy ‘$in’ as UTF-16…
|
||
|
||
rule ln
|
||
command = ln -fs $link $out
|
||
description = Link ‘$out’…
|
||
|
||
rule gen_ragel
|
||
command = ragel -o $out $in
|
||
description = Generate source from ‘$in’…
|
||
|
||
rule gen_capnp
|
||
command = PATH="$capnp_prefix/bin:$PATH" capnp compile -oc++ $in
|
||
description = Generate source from ‘$in’…
|
||
|
||
rule gen_cxx_test
|
||
command = bin/CxxTest/bin/cxxtestgen --have-std -o $out --runner=unix $in
|
||
description = Generate test ‘$out’…
|
||
|
||
rule link_cxx_test
|
||
command = '$CXX' -o $out $in $LN_FLAGS $RAVE_FLAGS
|
||
description = Link test ‘$out’…
|
||
|
||
rule gen_oak_test
|
||
command = bin/gen_test $in > $out~ && mv $out~ $out
|
||
description = Generate test ‘$out’…
|
||
|
||
rule link_oak_test
|
||
command = '$CXX' -o $out $in $LN_FLAGS $RAVE_FLAGS
|
||
description = Link test ‘$out’…
|
||
|
||
rule run_test
|
||
command = $in && touch $out
|
||
description = Run test ‘$in’…
|
||
|
||
rule always_run_test
|
||
command = $in
|
||
description = Run test ‘$in’…
|
||
|
||
rule skip_test
|
||
command = touch "$seal"
|
||
description = Skip test ‘$in’…
|
||
|
||
rule link_static
|
||
command = rm -f $out && ar -cqs $out $in
|
||
description = Archive objects ‘$out’…
|
||
|
||
rule link_dynamic
|
||
command = '$CXX' -o $out $in $LN_FLAGS $RAVE_FLAGS -dynamiclib -current_version 1.0.1 -compatibility_version 1.0.0
|
||
description = Link dynamic library ‘$out’…
|
||
|
||
rule link_executable
|
||
command = '$CXX' -o $out $in $LN_FLAGS $RAVE_FLAGS
|
||
description = Link executable ‘$out’…
|
||
|
||
rule run_executable
|
||
command = $in
|
||
description = Run ‘$in’…
|
||
|
||
rule debug_executable
|
||
command = lldb $in
|
||
pool = console
|
||
description = Debug ‘$in’…
|
||
|
||
rule run_application
|
||
command = { $
|
||
if pgrep >/dev/null "$appname"; then $
|
||
if [[ -x "$$DIALOG" && $$("$$DIALOG" < /dev/null alert --title "Relaunch $appname?" --body "Would you like to quit $appname and start the newly built version?" --button1 Relaunch --button2 Cancel|pl) != *"buttonClicked = 0"* ]]; $
|
||
then exit; $
|
||
fi; $
|
||
pkill "$appname"; $
|
||
while pgrep >/dev/null "$appname"; do $
|
||
if (( ++n == 10 )); then $
|
||
test -x "$$DIALOG" && "$$DIALOG" </dev/null alert --title "Relaunch Timed Out" --body "Unable to exit $appname." --button1 OK; $
|
||
exit; $
|
||
fi; $
|
||
sleep .2; $
|
||
done; $
|
||
fi; $
|
||
open $in; $
|
||
} &>/dev/null &
|
||
description = Run ‘$in’…
|
||
|
||
rule sign_executable
|
||
command = codesign --timestamp=none --deep -fs "$identity" $in && touch $out
|
||
description = Sign ‘$in’…
|
||
|
||
rule tbz_archive
|
||
command = tar $bzip2_flag -cf $out~ -C "$$(dirname $in)" "$$(basename $in)" && mv $out~ $out
|
||
generator = true
|
||
description = Archive ‘$in’…
|
||
|
||
rule upload
|
||
command = bin/upload -k$upload_keyfile -d$upload_destination -t'v$APP_VERSION' -m'{"version":"$APP_VERSION","revision":"$APP_REVISION"}' $in > $out~ && mv $out~ $out
|
||
pool = console
|
||
generator = true
|
||
description = Upload ‘$in’…
|
||
|
||
rule deploy
|
||
command = curl -sfnd @$in '${rest_api}/releases/nightly' && touch $out
|
||
generator = true
|
||
description = Deploy…
|
||
|
||
rule bump_revision
|
||
command = ruby -pe '$$_.gsub!(/(APP_REVISION\s*=\s*)(\d+)/) { newRev = $$2.to_i + 1; STDERR << "r#$$2 → r#{newRev}\n"; "#$$1#{newRev}#$$3" }' $in > $in~ && mv $in~ $in && touch $builddir/bumped
|
||
description = Increment version number…
|
||
|
||
build $builddir/always_bump_revision: bump_revision $builddir/build.ninja
|
||
build bump: phony $builddir/always_bump_revision
|
||
|
||
rule clean
|
||
command = rm -r '$path'
|
||
|
||
rule dsym
|
||
command = $
|
||
DST=$$(/usr/bin/mktemp -dt dsym); $
|
||
find $in -name rmate -or -type f -perm +0111 -print|while read line; $
|
||
do dsymutil --flat --out "$$DST/$$(basename "$$line" .dylib).dSYM" "$$line"; $
|
||
done && tar $bzip2_flag -cf $out -C "$$DST" . && rm -rf "$$DST"
|
||
generator = true
|
||
description = Archive dSYM info for ‘$in’…
|
||
|
||
rule compile_xib
|
||
command = "$xcodedir/usr/bin/ibtool" --errors --warnings --notices --output-format human-readable-text --compile $out $in
|
||
description = Compile xib ‘$in’…
|
||
|
||
rule process_plist
|
||
command = bin/process_plist > $out~ $in $PLIST_FLAGS $RAVE_FLAGS && mv $out~ $out
|
||
description = Process plist ‘$in’…
|
||
|
||
rule markdown
|
||
command = bin/gen_html > $out~ $md_flags $in && mv $out~ $out
|
||
description = Generate ‘$out’…
|
||
|
||
rule index_help
|
||
command = /usr/bin/hiutil -Cvaf $out "$HELP_BOOK"
|
||
description = Index help book ‘$HELP_BOOK’…
|
||
|
||
rule compile_mom
|
||
command = "$xcodedir/usr/bin/momc" $in $out
|
||
description = Generate ‘$out’…
|