mirror of
https://github.com/textmate/textmate.git
synced 2026-04-28 03:00:34 -04:00
Previously we would automatically pick up an Info.plist file copied using any of the CP_* keys, and both move it to the correct location (when belonging to target built) or ignore it, if we were copying it from an imported target. To simplify the logic in the build system, it is however better to be explicit about this, also because we could actually want an Info.plist file among our copied files.
995 lines
32 KiB
Ruby
Executable File
995 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.
|
||
require 'optparse'
|
||
require 'shellwords'
|
||
require 'fileutils'
|
||
require 'set'
|
||
|
||
BIN = File.dirname(__FILE__)
|
||
|
||
class Context
|
||
def initialize(options = { }, dir = nil, parent = nil)
|
||
@options, @dir, @parent = options, dir, parent
|
||
end
|
||
|
||
def derive(options, dir = nil)
|
||
Context.new(options, dir, self)
|
||
end
|
||
|
||
def to_h
|
||
chain.inject(Hash.new) do |hash, obj|
|
||
obj.options.each do |key, value|
|
||
hash[key] = hash.key?(key) ? value + "\n" + hash[key] : value
|
||
end
|
||
hash
|
||
end
|
||
end
|
||
|
||
def keys
|
||
chain.flat_map { |obj| obj.options.keys }.uniq
|
||
end
|
||
|
||
def [](key)
|
||
res = chain.map { |obj| obj.options[key] }.reject { |value| value.nil? }
|
||
res.empty? ? nil : res.join("\n")
|
||
end
|
||
|
||
def array_for(key)
|
||
chain.flat_map do |obj|
|
||
value = obj.options[key]
|
||
value.is_a?(Array) ? value : Shellwords.shellwords(value.to_s)
|
||
end
|
||
end
|
||
|
||
def path_for(key)
|
||
if instance = chain.find { |obj| obj.options.key?(key) }
|
||
instance.dir ? File.join(instance.dir, instance.options[key]) : instance.options[key]
|
||
end
|
||
end
|
||
|
||
def paths_for(key)
|
||
chain.select { |obj| obj.options.key?(key) }.flat_map do |instance|
|
||
files = Dir.chdir(instance.dir || '.') do
|
||
globs = instance.array_for(key).reject { |glob| glob =~ /^@(\w+)/ }
|
||
globs.flat_map { |glob| Dir.glob(glob) }
|
||
end
|
||
files.map { |file| instance.dir ? File.join(instance.dir, file) : file }
|
||
end
|
||
end
|
||
|
||
def target_names_for(key)
|
||
self.array_for(key).map { |name| name =~ /^@(\w+)/ ? $1 : nil }.reject { |name| name.nil? }
|
||
end
|
||
|
||
protected
|
||
|
||
def chain
|
||
res = [ self ]
|
||
res << res.last.parent until res.last.parent.nil?
|
||
res
|
||
end
|
||
|
||
attr_reader :options, :dir, :parent
|
||
end
|
||
|
||
class Asset
|
||
attr_accessor :file, :context
|
||
|
||
def initialize(file, context = Context.new)
|
||
@file, @context = file, context
|
||
end
|
||
end
|
||
|
||
class Target
|
||
attr_reader :dir, :context
|
||
|
||
def Target.read(file)
|
||
res = { }
|
||
File.read(file).scan(/^([\w\/]+)\s*(\+=|-=|=)[ \t]*(.*)$/).each do |key, op, value|
|
||
if op == '+=' && res.key?(key)
|
||
res[key] << ' ' << value
|
||
else
|
||
res[key] = value
|
||
end
|
||
end
|
||
res
|
||
end
|
||
|
||
def initialize(dir, context)
|
||
@dir, @context = dir, context
|
||
@files = [ ]
|
||
end
|
||
|
||
def setup_build(buildfile, all_targets)
|
||
abort "*** setup_build already called" unless @files.empty?
|
||
|
||
buildfile.assign("#{self.name}_dir", File.join(buildfile.dir, self.dir))
|
||
|
||
targets = [ self.name, *link_dependencies(all_targets) ].map { |name| all_targets[name] }
|
||
objects = targets.flat_map { |target| target.objects(buildfile, all_targets) }
|
||
tests = targets.flat_map { |target| target.test_results(buildfile, all_targets) }
|
||
|
||
sign_flags, sign_deps = [], []
|
||
if entitlements = @context.path_for('CS_ENTITLEMENTS')
|
||
sign_flags << "--entitlements #{entitlements.shellescape}"
|
||
sign_deps << entitlements
|
||
end
|
||
if codesign_flags = @context['CODESIGN_FLAGS']
|
||
sign_flags << codesign_flags
|
||
end
|
||
|
||
is_bundle = targets.any? { |target| target.context.keys.any? { |key| key =~ /^CP_/ } }
|
||
|
||
if is_bundle
|
||
bundle = "#{self.name}.#{@context['BUNDLE_EXTENSION'] || 'app'}"
|
||
bundle_path = File.join(buildfile.dir, self.dir, bundle)
|
||
|
||
resources = [ ]
|
||
|
||
targets.each do |target|
|
||
target.resources(buildfile, all_targets).each do |file, path, deps|
|
||
resources << buildfile.build('copy', file, File.join(buildfile.dir, self.dir, bundle, path), nil, deps)
|
||
@files << [ file, File.join(bundle, path), deps ]
|
||
end
|
||
end
|
||
|
||
executable = "#{bundle_path}/Contents/MacOS/#{self.name}"
|
||
signature = "#{bundle_path}/Contents/_CodeSignature/CodeResources"
|
||
|
||
buildfile.build('link', objects, executable, link_flags(all_targets))
|
||
buildfile.meta(bundle_path, executable, *resources)
|
||
buildfile.build('sign', bundle_path, signature, sign_flags, sign_deps)
|
||
buildfile.meta(self.name, signature)
|
||
|
||
@files << [ executable, "#{bundle}/Contents/MacOS/#{self.name}", signature ]
|
||
@files << [ signature, "#{bundle}/Contents/_CodeSignature/CodeResources" ]
|
||
|
||
if File.extname(bundle) == '.app'
|
||
buildfile.build('run_application', bundle_path, "#{bundle_path}.run", nil, signature)
|
||
buildfile.meta("#{self.name}/run", "#{bundle_path}.run")
|
||
buildfile.assign("#{self.name}_app", bundle_path)
|
||
buildfile.assign("#{self.name}_sign", signature)
|
||
end
|
||
else
|
||
signature = File.join(buildfile.dir, self.dir, "#{self.name}.sign")
|
||
executable = File.join(buildfile.dir, self.dir, self.name)
|
||
|
||
buildfile.build('link', objects, executable, link_flags(all_targets))
|
||
buildfile.build('sign', executable, signature, sign_flags, sign_deps)
|
||
buildfile.build('run', executable, "#{executable}.run", nil, signature)
|
||
buildfile.meta(self.name, signature)
|
||
buildfile.meta("#{self.name}/run", "#{executable}.run")
|
||
|
||
buildfile.assign("#{self.name}_exe", executable)
|
||
buildfile.assign("#{self.name}_sign", signature)
|
||
|
||
@files << [ executable, self.name, signature ]
|
||
end
|
||
|
||
buildfile.meta("#{self.name}/tests", *tests)
|
||
end
|
||
|
||
protected
|
||
|
||
attr_reader :files
|
||
|
||
def name
|
||
@context['TARGET_NAME']
|
||
end
|
||
|
||
def link_dependencies(all_targets)
|
||
if @link_dependencies.nil?
|
||
queue = [ self.name ]
|
||
seen = Set.new(queue)
|
||
while name = queue.shift
|
||
new_names = Set.new(all_targets[name].context.array_for('LINK'))
|
||
queue += (new_names - seen).to_a
|
||
seen += new_names
|
||
end
|
||
@link_dependencies = seen.delete(self.name).to_a.sort
|
||
end
|
||
@link_dependencies
|
||
end
|
||
|
||
def include_directory(buildfile)
|
||
if @include_directory.nil?
|
||
@include_directory = File.join(buildfile.dir, self.dir, 'include')
|
||
|
||
headers = @context.paths_for('EXPORT').map do |file|
|
||
buildfile.build('copy', file, File.join(@include_directory, self.name, File.basename(file)))
|
||
end
|
||
|
||
buildfile.meta(@include_directory, *headers)
|
||
end
|
||
@include_directory
|
||
end
|
||
|
||
def objects_for_sources(buildfile, all_targets, sources, include_dirs: nil)
|
||
dirs = (link_dependencies(all_targets) + @context.array_for('IMPORT')).uniq.map do |name|
|
||
all_targets[name].include_directory(buildfile)
|
||
end
|
||
|
||
dirs += include_dirs unless include_dirs.nil?
|
||
|
||
context = @context.derive({ 'include_dirs' => dirs })
|
||
|
||
sources.flat_map do |file|
|
||
buildfile.transform(Asset.new(file, context)).map do |asset|
|
||
abort "*** No rule to make object file from: #{file}" unless File.extname(asset.file) == '.o'
|
||
asset.file
|
||
end
|
||
end
|
||
end
|
||
|
||
def objects(buildfile, all_targets)
|
||
@objects ||= objects_for_sources(buildfile, all_targets, @context.paths_for('SOURCES'))
|
||
end
|
||
|
||
def resources(buildfile, all_targets)
|
||
if @resources.nil?
|
||
@resources = [ ]
|
||
|
||
@context.keys.each do |key|
|
||
if key =~ /^CP_(.+)/
|
||
asset_section = $1 == "InfoPlist" ? "Contents" : "Contents/#$1"
|
||
queue = @context.paths_for(key).map { |file| [ file, asset_section ] }
|
||
|
||
while info = queue.shift do
|
||
file, dest = *info
|
||
buildfile.transform(Asset.new(file, @context)).each do |new_asset|
|
||
if File.directory?(new_asset.file)
|
||
queue += Dir.entries(new_asset.file).reject { |path| path =~ /^\./ }.map do |entry|
|
||
[ File.join(new_asset.file, entry), File.join(dest, File.basename(file)) ]
|
||
end
|
||
else
|
||
@resources << [ new_asset.file, File.join(dest, File.basename(new_asset.file)) ]
|
||
end
|
||
end
|
||
end
|
||
|
||
@context.target_names_for(key).each do |name|
|
||
all_targets[name].files.each do |src, dst, deps|
|
||
@resources << [ src, File.join(asset_section, dst), deps ]
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
@resources
|
||
end
|
||
|
||
def link_flags(all_targets)
|
||
targets = [ self.name, *link_dependencies(all_targets) ].map { |name| all_targets[name] }
|
||
|
||
mixed_libs = targets.inject(Set.new) { |set, target| set += target.context.array_for('LIBS') }
|
||
frameworks = targets.inject(Set.new) { |set, target| set += target.context.array_for('FRAMEWORKS') }
|
||
ln_flags = targets.inject(Set.new) { |set, target| set += target.context.array_for('LN_FLAGS') }
|
||
|
||
static, dynamic = mixed_libs.partition { |lib| lib =~ /\.a$/ }
|
||
|
||
hash = { '' => [ *ln_flags, *static ], '-l' => dynamic, '-framework ' => frameworks }
|
||
hash.flat_map do |prefix, args|
|
||
args.map { |arg| prefix + arg.sub(/^(?=.*?\$).*$/, '"\&"') }
|
||
end
|
||
end
|
||
|
||
def test_results(buildfile, all_targets)
|
||
if @test_results.nil?
|
||
@test_results = []
|
||
|
||
cxx_test_sources = @context.paths_for('TESTS')
|
||
unless cxx_test_sources.empty?
|
||
ext = cxx_test_sources.any? { |file| File.extname(file) == '.mm' } ? 'mm' : 'cc'
|
||
|
||
test_source = File.join(buildfile.dir, self.dir, "test_#{self.name}.#{ext}")
|
||
test_executable = File.join(buildfile.dir, self.dir, "test_#{self.name}")
|
||
test_passed = File.join(buildfile.dir, self.dir, "test_#{self.name}.passed")
|
||
test_ignore_passed = File.join(buildfile.dir, self.dir, "test_#{self.name}.ignore")
|
||
|
||
targets = [ self.name, *link_dependencies(all_targets) ].map { |name| all_targets[name] }
|
||
test_objects = targets.inject(Array.new) { |arr, target| arr += target.objects(buildfile, all_targets) }
|
||
test_objects += objects_for_sources(buildfile, all_targets, [ test_source ], include_dirs: [ self.include_directory(buildfile) ])
|
||
|
||
buildfile.build('gen_test', cxx_test_sources, test_source)
|
||
buildfile.build('link', test_objects, test_executable, link_flags(all_targets))
|
||
buildfile.build('run_test', test_executable, test_passed)
|
||
buildfile.build('always_run_test', test_executable, test_ignore_passed)
|
||
|
||
buildfile.meta("#{self.name}/test", test_ignore_passed)
|
||
@test_results << test_passed
|
||
end
|
||
|
||
objc_test_sources = @context.paths_for('TEST_SOURCES')
|
||
unless objc_test_sources.empty?
|
||
ext = objc_test_sources.any? { |file| File.extname(file) == '.mm' } ? 'mm' : 'cc'
|
||
|
||
test_source = File.join(buildfile.dir, self.dir, "cxx_test_#{self.name}.#{ext}")
|
||
test_executable = File.join(buildfile.dir, self.dir, "cxx_test_#{self.name}")
|
||
test_passed = File.join(buildfile.dir, self.dir, "cxx_test_#{self.name}.passed")
|
||
test_ignore_passed = File.join(buildfile.dir, self.dir, "cxx_test_#{self.name}.ignore")
|
||
|
||
targets = [ self.name, *link_dependencies(all_targets) ].map { |name| all_targets[name] }
|
||
test_objects = targets.inject(Array.new) { |arr, target| arr += target.objects(buildfile, all_targets) }
|
||
test_objects += objects_for_sources(buildfile, all_targets, [ test_source ], include_dirs: [ "#{BIN}/CxxTest", self.include_directory(buildfile) ])
|
||
|
||
buildfile.build('gen_cxx_test', objc_test_sources, test_source)
|
||
buildfile.build('link', test_objects, test_executable, link_flags(all_targets))
|
||
buildfile.build('run_test', test_executable, test_passed)
|
||
buildfile.build('always_run_test', test_executable, test_ignore_passed)
|
||
|
||
buildfile.meta("#{self.name}/cxx_test", test_ignore_passed)
|
||
@test_results << test_passed
|
||
end
|
||
end
|
||
@test_results
|
||
end
|
||
end
|
||
|
||
class Buildfile
|
||
attr_reader :dir
|
||
|
||
def initialize(context, dir, variables = {})
|
||
@context, @dir, @variables = context, dir, variables
|
||
@rules, @build, @default_targets = [], [], []
|
||
end
|
||
|
||
def add_basic_rules
|
||
self.rule('copy', {
|
||
'command' => '/bin/cp -Xp $in $out && touch $out',
|
||
'description' => 'Copy ‘$in’…',
|
||
})
|
||
|
||
self.rule('gen_test', {
|
||
'command' => "#{BIN}/gen_test > $out~ $in && mv $out~ $out",
|
||
'description' => 'Generate test ‘$out’…',
|
||
})
|
||
|
||
self.rule('gen_cxx_test', {
|
||
'command' => "#{BIN}/CxxTest/bin/cxxtestgen --have-std -o $out --runner=unix $in",
|
||
'description' => 'Generate test ‘$out’…',
|
||
})
|
||
|
||
self.rule('run_test', {
|
||
'command' => '$in $test_runner_flags && touch $out',
|
||
'description' => 'Run ‘$in’…',
|
||
'pool' => 'console',
|
||
})
|
||
|
||
self.rule('always_run_test', {
|
||
'command' => '$in $test_runner_flags',
|
||
'description' => 'Run ‘$in’…',
|
||
'pool' => 'console',
|
||
})
|
||
|
||
self.rule('link', {
|
||
'command' => 'xcrun clang++ $args -o $out $in',
|
||
'description' => 'Link executable ‘$out’…',
|
||
})
|
||
|
||
self.rule('sign', {
|
||
'command' => 'xcrun codesign $args -fs "$identity" $in && touch $out',
|
||
'description' => 'Sign ‘$in’…',
|
||
})
|
||
|
||
self.rule('index_help', {
|
||
'command' => '/usr/bin/hiutil -Cvaf $out "$HELP_BOOK"',
|
||
'description' => 'Index help book ‘$HELP_BOOK’…',
|
||
})
|
||
|
||
self.rule('run', {
|
||
'command' => '$in',
|
||
'description' => 'Run ‘$in’…',
|
||
'pool' => 'console',
|
||
})
|
||
|
||
self.rule('run_application', {
|
||
'command' => <<~SHELL.gsub(/\s+/, ' '),
|
||
{
|
||
app_name=$$(basename $in .app);
|
||
if pgrep "$$app_name"; then
|
||
if [[ -x "$$DIALOG" && $$("$$DIALOG" alert --title "Relaunch $$app_name?" --body "Would you like to quit $$app_name and start the newly built version?" --button1 Relaunch --button2 Cancel|pl) != *"buttonClicked = 0"* ]];
|
||
then exit;
|
||
fi;
|
||
pkill "$$app_name";
|
||
while pgrep "$$app_name"; do
|
||
if (( ++n == 10 )); then
|
||
test -x "$$DIALOG" && "$$DIALOG" alert --title "Relaunch Timed Out" --body "Unable to exit $$app_name." --button1 OK;
|
||
exit;
|
||
fi;
|
||
sleep .2;
|
||
done;
|
||
fi;
|
||
open $in --args -disableSessionRestore NO;
|
||
} </dev/null &>/dev/null &
|
||
SHELL
|
||
'description' => 'Run ‘$in’…',
|
||
})
|
||
end
|
||
|
||
def default(*names)
|
||
@default_targets += names
|
||
end
|
||
|
||
def rule(name, values)
|
||
@rules << [ name, values ]
|
||
end
|
||
|
||
def build(rule, src, dst, args = nil, deps = nil, outs = nil, order_only = nil)
|
||
@build << [ rule, src, dst, args, deps, outs, order_only ]
|
||
dst
|
||
end
|
||
|
||
def meta(name, *deps)
|
||
build('phony', deps, name)
|
||
end
|
||
|
||
def transform(initial_asset)
|
||
derived_assets = [ ]
|
||
|
||
exclude = Set.new
|
||
queue = [ initial_asset ]
|
||
while asset = queue.shift
|
||
if compiler = compiler_for(asset.file, exclude)
|
||
exclude.add(compiler.class)
|
||
queue += compiler.transform(asset, self)
|
||
else
|
||
derived_assets << asset
|
||
end
|
||
end
|
||
|
||
derived_assets
|
||
end
|
||
|
||
def assign(key, value)
|
||
@variables[key] = value
|
||
end
|
||
|
||
def to_s
|
||
variables = @context ? @variables.merge(@context.to_h) : @variables
|
||
|
||
width = variables.map { |key, value| key.length }.max { |lhs, rhs| lhs <=> rhs }
|
||
res = variables.map { |key, value| format("%-#{width}s = %s\n", key, value) }.join
|
||
res << "\n" unless variables.empty?
|
||
|
||
@rules.map do |name, values|
|
||
flags = values.map { |k, v| " #{k} = #{v}" }.join("\n")
|
||
res << "rule #{name}\n#{flags}\n\n"
|
||
end
|
||
|
||
esc = lambda { |x| Array(x).map { |file| file.gsub(/[ :$]/, '$\&') }.join(' ') }
|
||
@build.each do |rule, src, dst, args, deps, outs, order_only|
|
||
res << "build #{esc.call(dst)}"
|
||
res << " | #{esc.call(outs)}" unless outs.to_a.empty?
|
||
res << ": #{rule} #{esc.call(src)}"
|
||
res << " | #{esc.call(deps)}" unless deps.to_s.empty?
|
||
res << " || #{esc.call(order_only)}" unless order_only.to_a.empty?
|
||
res << "\n"
|
||
args = Array(args).join("\n").gsub(/\s+/, ' ').strip
|
||
res << " args = #{args}\n" unless args.empty?
|
||
res << "\n"
|
||
end
|
||
|
||
unless @default_targets.empty?
|
||
res << "default #{@default_targets.join(' ')}\n\n"
|
||
end
|
||
|
||
res
|
||
end
|
||
|
||
private
|
||
|
||
def compiler_for(file, exclude)
|
||
candidates = []
|
||
|
||
Plugin.plugins_of_type(Compiler).each do |klass|
|
||
klass.transforms.each do |ext, dest_ext|
|
||
if file =~ /\b#{Regexp.escape ext}$/
|
||
candidates << { class: klass, ext: ext, dest_ext: dest_ext }
|
||
end
|
||
end unless klass.transforms.nil?
|
||
|
||
klass.extensions.each do |canonical_ext, extensions|
|
||
extensions.each do |ext|
|
||
if file =~ /\b#{Regexp.escape ext}$/
|
||
candidates << { class: klass, ext: ext, dest_ext: klass.transforms[canonical_ext], canonical_ext: canonical_ext }
|
||
end
|
||
end
|
||
end unless klass.extensions.nil?
|
||
end
|
||
|
||
candidates.sort! { |lhs, rhs| lhs[:ext].length <=> rhs[:ext].length }
|
||
while match = candidates.pop
|
||
res = match[:class].new(self, @context, match[:ext], match[:canonical_ext] || match[:ext], match[:dest_ext])
|
||
return res unless exclude.include?(match[:class])
|
||
end
|
||
end
|
||
end
|
||
|
||
class CleanupTargets
|
||
def initialize(ninja_file, builddir)
|
||
@ninja_file, @builddir = ninja_file, builddir
|
||
@old_targets = self.targets(ninja_file, builddir)
|
||
end
|
||
|
||
def targets(ninja_file, builddir)
|
||
return nil if ninja_file == '/dev/stdout' || !File.exists?(ninja_file)
|
||
|
||
targets = %x{ ${TM_NINJA:-ninja} -C #{File.dirname(ninja_file).shellescape} -f #{ninja_file.shellescape} -t targets all }
|
||
return nil if $? != 0
|
||
|
||
res = Set.new
|
||
targets.each_line do |line|
|
||
res << $& if line =~ /^#{Regexp.escape(builddir)}.*(?=:(?! (phony|(tbz|zip)_archive)))/
|
||
end
|
||
res
|
||
end
|
||
|
||
def run(dry_run: false)
|
||
new_targets = self.targets(@ninja_file, @builddir)
|
||
|
||
if @old_targets && new_targets
|
||
targets_lost = @old_targets - new_targets
|
||
targets_lost.each do |path|
|
||
if File.exists?(path)
|
||
STDERR << "Remove old target ‘#{path.sub(/#{Regexp.escape(@builddir)}/, '$builddir')}’…\n"
|
||
# FIXME Does not seem to work with symbolic links (Proxy.png → Settings.png)
|
||
File.unlink(path) unless dry_run == true
|
||
end
|
||
rescue Exception => e
|
||
STDERR << "File.unlink: #{e}\n"
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
class Plugin
|
||
@@plugins = []
|
||
|
||
def self.inherited(subclass)
|
||
@@plugins << subclass
|
||
end
|
||
|
||
def self.plugins_of_type(klass)
|
||
@@plugins.select { |candidate| candidate < klass }
|
||
end
|
||
end
|
||
|
||
class Compiler < Plugin
|
||
class << self
|
||
def transforms(hash = nil)
|
||
@transforms = hash || @transforms
|
||
end
|
||
|
||
def extensions(hash = nil)
|
||
@extensions = hash || @extensions
|
||
end
|
||
end
|
||
|
||
@@did_setup = []
|
||
|
||
def initialize(buildfile, context, ext, canonical_ext, dest_ext)
|
||
@ext, @canonical_ext, @dest_ext = ext, canonical_ext, dest_ext
|
||
unless @@did_setup.include?(self.class)
|
||
@@did_setup << self.class
|
||
self.setup(buildfile, context)
|
||
end
|
||
end
|
||
|
||
def filter?
|
||
@canonical_ext == @dest_ext
|
||
end
|
||
|
||
def derive_asset(asset, dest, options = nil)
|
||
new_path = asset.file.chomp(@ext) + @dest_ext
|
||
if filter?
|
||
dirname, basename = File.dirname(new_path), File.basename(new_path)
|
||
dirname = File.dirname(dirname) if File.basename(dirname) =~ /^_[A-Z]\w+/
|
||
new_path = File.join(dirname, "_#{self.class}", basename)
|
||
end
|
||
Asset.new(File.expand_path(new_path, dest), options ? asset.context.derive(options) : asset.context)
|
||
end
|
||
end
|
||
|
||
class CompileRagel < Compiler
|
||
transforms '.rl' => '.cc', '.mm.rl' => '.mm', '.cc.rl' => '.cc'
|
||
|
||
def setup(buildfile, context)
|
||
buildfile.rule('gen_ragel', {
|
||
'command' => 'ragel $args -o $out $in',
|
||
'description' => 'Generate source from ‘$in’…',
|
||
})
|
||
end
|
||
|
||
def transform(asset, buildfile)
|
||
config = { 'FLAGS' => Shellwords.shelljoin([ '-iquote', File.dirname(asset.file) ]) }
|
||
new_asset = self.derive_asset(asset, buildfile.dir, config)
|
||
buildfile.build('gen_ragel', asset.file, new_asset.file, asset.context['RAGEL_FLAGS'])
|
||
[ new_asset ]
|
||
end
|
||
end
|
||
|
||
class CompileCapnp < Compiler
|
||
transforms '.capnp' => '.capnp.c++'
|
||
|
||
def setup(buildfile, context)
|
||
buildfile.rule('gen_capnp', {
|
||
'command' => 'PATH="$capnp_prefix/bin:$$PATH" capnp compile -oc++ $in',
|
||
'description' => 'Generate source from ‘$in’…',
|
||
})
|
||
end
|
||
|
||
def transform(asset, buildfile)
|
||
new_asset_source = Asset.new(asset.file.chomp(@ext) + '.capnp.c++', asset.context)
|
||
new_asset_header = Asset.new(asset.file.chomp(@ext) + '.capnp.h', asset.context)
|
||
buildfile.build('gen_capnp', asset.file, [ new_asset_source.file, new_asset_header.file ])
|
||
[ new_asset_source ]
|
||
end
|
||
end
|
||
|
||
class CompileClang < Compiler
|
||
transforms '.c' => '.o', '.m' => '.o', '.cc' => '.o', '.mm' => '.o'
|
||
extensions '.cc' => [ '.c++', '.cxx', '.cpp' ]
|
||
|
||
COMPILER_INFO = {
|
||
'.c' => { :rule => 'build_c', :compiler => 'xcrun clang', :flags => 'C_FLAGS', :pch => '-x c-header' },
|
||
'.m' => { :rule => 'build_m', :compiler => 'xcrun clang', :flags => 'OBJC_FLAGS', :pch => '-x objective-c-header' },
|
||
'.cc' => { :rule => 'build_cc', :compiler => 'xcrun clang++', :flags => 'CXX_FLAGS', :pch => '-x c++-header' },
|
||
'.mm' => { :rule => 'build_mm', :compiler => 'xcrun clang++', :flags => 'OBJCXX_FLAGS', :pch => '-x objective-c++-header' },
|
||
}
|
||
|
||
@@prelude = { }
|
||
|
||
def setup(buildfile, context)
|
||
COMPILER_INFO.each do |_, info|
|
||
buildfile.rule(info[:rule], {
|
||
'command' => "#{info[:compiler]} $args -o $out -MMD -MF $out.d $in",
|
||
'depfile' => "$out.d",
|
||
'deps' => "gcc",
|
||
'description' => "Compile ‘$in’…",
|
||
})
|
||
end
|
||
|
||
context.paths_for('PRELUDE').each do |file|
|
||
ext = File.extname(file)
|
||
if info = COMPILER_INFO[ext]
|
||
flags = [ info[:pch] ]
|
||
flags << context[info[:flags]]
|
||
flags << context['FLAGS']
|
||
|
||
outfile = "#{File.expand_path(file, buildfile.dir)}.gch"
|
||
buildfile.build(info[:rule], file, outfile, flags)
|
||
@@prelude[ext] = { :outfile => outfile, :include => outfile.chomp('.gch') }
|
||
end
|
||
end
|
||
end
|
||
|
||
def transform(asset, buildfile)
|
||
info = COMPILER_INFO[@canonical_ext]
|
||
|
||
flags = [ "-include #{@@prelude[@canonical_ext][:include]}" ]
|
||
deps = [ @@prelude[@canonical_ext][:outfile] ]
|
||
|
||
order_only = nil
|
||
|
||
if dirs = asset.context.array_for('include_dirs')
|
||
flags << dirs.map { |dir| "-I#{dir}" }.shelljoin
|
||
order_only = dirs
|
||
end
|
||
|
||
flags << asset.context[info[:flags]]
|
||
flags << asset.context['FLAGS']
|
||
|
||
new_asset = self.derive_asset(asset, buildfile.dir)
|
||
buildfile.build(info[:rule], asset.file, new_asset.file, flags, deps, nil, order_only)
|
||
[ new_asset ]
|
||
end
|
||
end
|
||
|
||
class CompileXib < Compiler
|
||
transforms '.xib' => '.nib'
|
||
|
||
def setup(buildfile, context)
|
||
buildfile.rule('compile_xib', {
|
||
'command' => 'xcrun ibtool --errors --warnings --notices --output-format human-readable-text --minimum-deployment-target $APP_MIN_OS --compile $out $in',
|
||
'description' => 'Compile xib ‘$in’…',
|
||
})
|
||
end
|
||
|
||
def transform(asset, buildfile)
|
||
new_asset = self.derive_asset(asset, buildfile.dir)
|
||
buildfile.build('compile_xib', asset.file, new_asset.file)
|
||
[ new_asset ]
|
||
end
|
||
end
|
||
|
||
class CompileXCAssets < Compiler
|
||
transforms '.xcassets' => '.car'
|
||
|
||
def setup(buildfile, context)
|
||
buildfile.rule('xcassets', {
|
||
'command' => 'xcrun actool --errors --warnings --notices --output-format human-readable-text --platform macosx --minimum-deployment-target $APP_MIN_OS --compile "$$(dirname $out)" $in',
|
||
'description' => 'Compile xcassets ‘$in’…',
|
||
})
|
||
end
|
||
|
||
def transform(asset, buildfile)
|
||
deps = Dir.glob("#{asset.file}/**/*")
|
||
|
||
new_asset = Asset.new(File.join(buildfile.dir, File.dirname(asset.file), 'Assets.car'))
|
||
buildfile.build('xcassets', asset.file, new_asset.file, nil, deps)
|
||
[ new_asset ]
|
||
end
|
||
end
|
||
|
||
class CopyAsUTF16Strings < Compiler
|
||
transforms '.strings' => '.strings'
|
||
|
||
def setup(buildfile, context)
|
||
buildfile.rule('copy_as_utf16', {
|
||
'command' => 'if [[ $$(head -c2 $in) == $$\'\\xFF\\xFE\' || $$(head -c2 $in) == $$\'\\xFE\\xFF\' ]]; then /bin/cp -Xp $in $out && touch $out; else iconv -f utf-8 -t utf-16 < $in > $out~ && mv $out~ $out; fi',
|
||
'description' => 'Copy ‘$in’ as UTF-16…',
|
||
})
|
||
end
|
||
|
||
def transform(asset, buildfile)
|
||
new_asset = self.derive_asset(asset, buildfile.dir)
|
||
buildfile.build('copy_as_utf16', asset.file, new_asset.file)
|
||
[ new_asset ]
|
||
end
|
||
end
|
||
|
||
class ExpandVariables < Compiler
|
||
transforms 'Info.plist' => 'Info.plist', 'InfoPlist.strings' => 'InfoPlist.strings'
|
||
|
||
def setup(buildfile, context)
|
||
buildfile.rule('expand_variables', {
|
||
'command' => "#{BIN}/expand_variables -o $out $args $in",
|
||
'description' => 'Expand variables ‘$in’…',
|
||
})
|
||
end
|
||
|
||
def transform(asset, buildfile)
|
||
flags = [ "-dTARGET_NAME='#{asset.context['TARGET_NAME']}'", "-dYEAR='#{Time.now.year}'" ]
|
||
flags << asset.context['PLIST_FLAGS']
|
||
|
||
new_asset = self.derive_asset(asset, buildfile.dir)
|
||
buildfile.build('expand_variables', asset.file, new_asset.file, flags, [ "#{BIN}/expand_variables" ])
|
||
[ new_asset ]
|
||
end
|
||
end
|
||
|
||
class CompileMarkdown < Compiler
|
||
transforms '.md' => '.html'
|
||
extensions '.md' => [ '.mdown' ]
|
||
|
||
def setup(buildfile, context)
|
||
buildfile.rule('markdown', {
|
||
'command' => "#{BIN}/gen_html > $out~ $args $in && mv $out~ $out",
|
||
'description' => 'Generate ‘$out’…',
|
||
})
|
||
end
|
||
|
||
def transform(asset, buildfile)
|
||
header = asset.context.path_for('HTML_HEADER')
|
||
footer = asset.context.path_for('HTML_FOOTER')
|
||
|
||
deps = [ "#{BIN}/gen_html" ]
|
||
deps << header unless header.nil?
|
||
deps << footer unless footer.nil?
|
||
|
||
flags = []
|
||
flags << "-h #{header.shellescape}" unless header.nil?
|
||
flags << "-f #{footer.shellescape}" unless footer.nil?
|
||
|
||
md_header = asset.context.path_for('MARKDOWN_HEADER')
|
||
md_footer = asset.context.path_for('MARKDOWN_FOOTER')
|
||
infiles = [ md_header, asset.file, md_footer ].reject { |file| file.nil? }
|
||
|
||
new_asset = self.derive_asset(asset, buildfile.dir)
|
||
buildfile.build('markdown', infiles, new_asset.file, flags, deps)
|
||
[ new_asset ]
|
||
end
|
||
end
|
||
|
||
class CreateBundlesArchive < Compiler
|
||
transforms '.tbz.bl' => '.tbz'
|
||
|
||
def setup(buildfile, context)
|
||
buildfile.rule('download_bundles', {
|
||
'command' => "#{buildfile.dir}/Applications/bl/bl -C $out install $$(<$in)",
|
||
'description' => 'Download bundles…',
|
||
'pool' => 'console',
|
||
})
|
||
|
||
buildfile.rule('archive_bundles', {
|
||
'command' => '/usr/bin/tar --disable-copyfile $bzip2_flag -cf $out~ -C "$$(dirname $in)" "$$(basename $in)" && mv $out~ $out',
|
||
'description' => 'Create tbz archive for ‘$in’…',
|
||
'generator' => 'true',
|
||
})
|
||
end
|
||
|
||
def transform(asset, buildfile)
|
||
tmp_dir_asset = Asset.new(File.expand_path("#{asset.file}/Managed", buildfile.dir), asset.context)
|
||
new_asset = self.derive_asset(asset, buildfile.dir)
|
||
buildfile.build('download_bundles', asset.file, tmp_dir_asset.file, nil, [ "#{buildfile.dir}/Applications/bl/bl" ])
|
||
buildfile.build('archive_bundles', tmp_dir_asset.file, new_asset.file)
|
||
[ new_asset ]
|
||
end
|
||
end
|
||
|
||
# ========
|
||
# = Main =
|
||
# ========
|
||
|
||
if __FILE__ == $PROGRAM_NAME
|
||
outfile = '/dev/stdout'
|
||
builddir = 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?
|
||
ravefile = ARGV.shift
|
||
|
||
Dir.chdir(File.dirname(ravefile)) do
|
||
# ================
|
||
# = Load Targets =
|
||
# ================
|
||
|
||
config = Context.new(Target.read(ravefile))
|
||
files = []
|
||
|
||
all_targets = config.paths_for('TARGETS').map do |file|
|
||
dir, options = File.dirname(file), Target.read(file)
|
||
name, target = (options['TARGET_NAME'] ||= File.basename(dir)), Target.new(dir, config.derive(options, dir))
|
||
files << [ file, target ]
|
||
[ name, target ]
|
||
end.to_h
|
||
|
||
# ====================================
|
||
# = Check Existence of Named Targets =
|
||
# ====================================
|
||
|
||
files.each do |file, target|
|
||
target.context.array_for('LINK').each do |name|
|
||
unless all_targets.key?(name =~ /^@(.*)/ ? $1 : name)
|
||
abort "#{file}: no such target: #{name} (required by LINK)"
|
||
end
|
||
end
|
||
|
||
target.context.keys.select { |key| key =~ /^CP_/ }.each do |key|
|
||
target.context.array_for(key).each do |name|
|
||
if name =~ /^@(.*)/ && !all_targets.key?($1)
|
||
abort "#{file}: no such target: #{name} (required by #{key})"
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
# =================================
|
||
# = Topological Sort Root Targets =
|
||
# =================================
|
||
|
||
link_targets = Set.new(all_targets.flat_map { |_, target| target.context.array_for('LINK') })
|
||
root_targets = all_targets.reject { |name, _| link_targets.include?(name) }
|
||
|
||
nodes = root_targets.map do |name, target|
|
||
keys = target.context.keys.select { |key| key =~ /^CP_/ }
|
||
deps = keys.flat_map { |key| target.context.target_names_for(key) }
|
||
[ name, deps ]
|
||
end.to_h
|
||
|
||
edge_count = nodes.map { |name, _| [ name, 0 ] }.to_h
|
||
nodes.values.flatten.each { |target| edge_count[target] += 1 }
|
||
|
||
ordered = [ ]
|
||
|
||
until edge_count.empty?
|
||
roots = edge_count.select { |_, deps| deps.zero? }
|
||
abort "*** Fatal: Dependency cycle" if roots.empty?
|
||
|
||
roots.each do |root, _|
|
||
ordered << root
|
||
edge_count.delete(root)
|
||
nodes[root].each { |target| edge_count[target] -= 1 }
|
||
end
|
||
end
|
||
|
||
# =====================
|
||
# = Create build file =
|
||
# =====================
|
||
|
||
buildfile = Buildfile.new(config, builddir)
|
||
buildfile.add_basic_rules
|
||
|
||
ordered.reverse.each do |name|
|
||
root_targets[name].setup_build(buildfile, all_targets)
|
||
end
|
||
|
||
# =====================================
|
||
# = Calculate build file dependencies =
|
||
# =====================================
|
||
|
||
dependencies = all_targets.flat_map do |name, target|
|
||
target.context.keys.select { |key| key =~ /^(SOURCES|TESTS|TEST_SOURCES|CP_.*)$/ }.flat_map do |key|
|
||
all_targets[name].context.paths_for(key).map { |file| File.dirname(file) }
|
||
end
|
||
end.uniq
|
||
dependencies += [ '.', ravefile, *config.paths_for('TARGETS') ]
|
||
|
||
# ============================
|
||
# = Write build file to disk =
|
||
# ============================
|
||
|
||
cleaner = CleanupTargets.new(outfile, builddir)
|
||
|
||
FileUtils.mkdir_p(builddir)
|
||
File.write("#{builddir}/build.ninja", buildfile.to_s)
|
||
File.write("#{builddir}/build.ninja.d", "build.ninja: #{dependencies.sort.map { |dep| dep.gsub(/ /, '\\ ') }.join(" \\\n ")}\n")
|
||
|
||
bootstrap = Buildfile.new(nil, builddir, variables)
|
||
bootstrap.assign('ninja_required_version', '1.5')
|
||
bootstrap.assign('builddir', builddir)
|
||
|
||
strip_variables = Set.new(config.keys) << 'ninja_required_version' << 'builddir'
|
||
args = variables.reject { |key, value| strip_variables.include?(key) }
|
||
args = args.map { |key, value| "-d'#{key}=#{value =~ /\$/ ? value.gsub(/\$/, '$$') : "$#{key}"}'" }.join(' ')
|
||
|
||
bootstrap.rule('gen_build', {
|
||
'command' => "#{__FILE__} -C \"$builddir\" #{args} -o $out $in",
|
||
'depfile' => '$builddir/$out.d',
|
||
'generator' => 'true',
|
||
'description' => 'Generate ‘$out’…',
|
||
})
|
||
|
||
bootstrap.build('gen_build', 'target', 'build.ninja', nil, __FILE__)
|
||
|
||
open("#{outfile}", "w") do |io|
|
||
userfile = "#{ENV['USER']}.ninja"
|
||
io << bootstrap.to_s
|
||
io << "include $builddir/build.ninja\n"
|
||
io << "include #{userfile}\n" if userfile != 'build.ninja' && File.exists?(File.join(File.dirname(outfile), userfile))
|
||
end
|
||
|
||
cleaner.run(dry_run: false)
|
||
end
|
||
end
|