mirror of
https://github.com/textmate/textmate.git
synced 2026-01-21 04:38:13 -05:00
Variables which are not referencing other variables are no longer provided with their literal value. Variables which do reference other variables have the reference escaped, so that ninja won’t expand the reference when rebuilding build.ninja. There is still some redundancy in that APP_VERSION has the major + tag part stated both at the top and as a command line argument, haven’t yet figured out how to best deal with this.
913 lines
30 KiB
Ruby
Executable File
913 lines
30 KiB
Ruby
Executable File
#!/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby -wKU
|
||
# == 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.
|
||
|
||
require 'getoptlong'
|
||
require 'rdoc/usage'
|
||
require 'shellwords'
|
||
require 'set'
|
||
require 'pp'
|
||
|
||
GLOB_KEYS = %w{ CP_Resources CP_SharedSupport CP_PlugIns 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
|
||
|
||
unless op != '+=' || dict[key].nil?
|
||
if STRING_KEYS.include? key
|
||
value = "#{dict[key]} #{value}"
|
||
else
|
||
value = dict[key].dup.concat(value)
|
||
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 compiler_for(source)
|
||
COMPILER_INFO[source[/\b[a-z]+$/]][:rule]
|
||
end
|
||
|
||
def pch_for(src, target)
|
||
srcExt = src[/\b[a-z]+$/]
|
||
target['PRELUDE'].each do |pch|
|
||
if srcExt == pch[/\b[a-z]+$/]
|
||
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|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 << " depfile = #{dst}.d\n"
|
||
res << " RAVE_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 << " depfile = #{obj}.d\n"
|
||
res << " RAVE_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
|
||
|
||
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)
|
||
return '' if target['TEST_SOURCES'].to_s.empty? && target['TESTS'].to_s.empty?
|
||
|
||
headers = target['LINK'].to_a.map { |goal| "#{goal}/headers" }
|
||
dst = build_path("#{target[:path]}/test_#{target[:name]}")
|
||
res = ''
|
||
|
||
if !target['TESTS'].to_s.empty?
|
||
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 << " depfile = #{dst}.o.d\n"
|
||
res << " RAVE_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)
|
||
else
|
||
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_test #{src}\n"
|
||
res << "build #{dst}.o: #{compiler_for(test_src)} #{test_src} | #{pch_for(test_src, target)}.gch || #{target[:name]}/headers\n"
|
||
res << " depfile = #{dst}.o.d\n"
|
||
res << " RAVE_FLAGS = -Ibin/CxxTest -include #{pch_for(test_src, target)}\n"
|
||
res << flags_for_target(target)
|
||
res << "build #{dst}: link_test #{all_values_for_key(target, :objects).join(' ')} #{dst}.o\n"
|
||
res << flags_for_target(target)
|
||
end
|
||
|
||
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_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"
|
||
|
||
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 = "@executable_path/../Frameworks/#{target[:name]}.dylib"
|
||
else
|
||
install_name = "@executable_path/../Frameworks/#{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]
|
||
base = "#{exe}_alpha-${APP_REVISION}"
|
||
|
||
res << "build #{exe}.sign: sign_executable #{exe}\n"
|
||
res << "build #{base}.tbz: tbz_archive #{exe} | #{exe}.sign\n"
|
||
res << "build #{base}.upload: upload #{base}.tbz\n"
|
||
|
||
res << "build #{target[:name]}/deploy: phony #{base}.upload\n"
|
||
end
|
||
|
||
def deploy(target)
|
||
res = ''
|
||
base = target[:bundle].sub(/\.[^.]+$/, '_alpha-${APP_REVISION}')
|
||
|
||
res << "build #{base}-dSYM.tbz: dsym #{target[:bundle]}\n"
|
||
res << "build #{base}.tbz: tbz_archive #{target[:bundle]} | #{target[:bundle]}.sign\n"
|
||
res << "build #{base}.upload: upload #{base}.tbz\n"
|
||
res << "build #{base}.deploy: deploy #{base}.upload | #{base}-dSYM.tbz\n"
|
||
|
||
res << "build #{target[:name]}/dsym: phony #{base}-dSYM.tbz\n"
|
||
res << "build #{target[:name]}/tbz: phony #{base}.tbz\n"
|
||
res << "build #{target[:name]}/deploy: phony #{base}.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 ].shelljoin } -t targets all }
|
||
return nil if $? != 0
|
||
targets.each 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 =
|
||
# =======================
|
||
|
||
opts = GetoptLong.new(
|
||
[ '--help', '-h', GetoptLong::NO_ARGUMENT ],
|
||
[ '--output', '-o', GetoptLong::REQUIRED_ARGUMENT ],
|
||
[ '--build-directory', '-C', GetoptLong::REQUIRED_ARGUMENT ],
|
||
[ '--define', '-d', GetoptLong::REQUIRED_ARGUMENT ]
|
||
)
|
||
|
||
outfile, builddir = '/dev/stdout', File.expand_path('~/build')
|
||
variables = [ ]
|
||
|
||
opts.each do |opt, arg|
|
||
case opt
|
||
when '--help'
|
||
RDoc::usage
|
||
when '--output'
|
||
outfile = arg
|
||
when '--build-directory'
|
||
builddir = arg
|
||
when '--define'
|
||
variables << [ $1, $2 ] if arg =~ /^(\w+)\s*=\s*(.*)$/
|
||
end
|
||
end
|
||
|
||
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 << "###########################################\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.to_a.reject { |arr| arr[0].class != String || GLOB_KEYS.include?(arr[0]) }.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 }
|
||
|
||
io << variables.map { |arr| format("%-#{width}s = %s\n", *arr) }
|
||
io << "\n"
|
||
|
||
io << user_variables.map { |arr| 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 = 1\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]}' $FLAGS #{value[:flags]} $RAVE_FLAGS -o $out -MMD -MF $out.d -I$builddir/include $in\n"
|
||
io << " depfile = $depfile\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 = src[/\b[a-z]+$/]
|
||
cc = COMPILER_INFO[ext][:compiler]
|
||
|
||
io << "build #{pch_for(src, TARGETS.root)}.gch: #{compiler_for(src)} #{src} | #{cc}\n"
|
||
io << " depfile = #{pch_for(src, TARGETS.root)}.gch.d\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 << " depfile = #{pch_for(src, TARGETS.root)}-no-arc.gch.d\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
|
||
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_test
|
||
command = bin/CxxTest/bin/cxxtestgen --have-std -o $out --runner=unix $in
|
||
description = Generate test ‘$out’…
|
||
|
||
rule link_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 = osascript >/dev/null -e $$'tell app "Terminal" \n activate \n do script "gdb $in" \n end tell'
|
||
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 -fs "$identity" $in && touch $out
|
||
description = Sign ‘$in’…
|
||
|
||
rule tbz_archive
|
||
command = tar $bzip2_flag -cf $out -C "$$(dirname $in)" "$$(basename $in)"
|
||
description = Archive ‘$in’…
|
||
|
||
rule upload
|
||
command = bin/upload -k$upload_keyfile -d$upload_destination -m '{"version":"$APP_VERSION","revision":"$APP_REVISION"}' $in > $out~ && mv $out~ $out
|
||
description = Upload ‘$in’…
|
||
|
||
rule deploy
|
||
command = curl -sfnd @$in '${rest_api}/releases/nightly' && touch $out
|
||
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
|
||
description = Increment version number…
|
||
build $builddir/revision.next: bump_revision $builddir/build.ninja
|
||
build bump: phony $builddir/revision.next
|
||
build $builddir/revision.$APP_REVISION: touch
|
||
|
||
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"
|
||
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’…
|