become more close to actual ninja

This commit is contained in:
kp2pml30 2024-11-25 12:31:01 +04:00
parent b44e5373ed
commit 1ece0617dc

537
ya-build
View file

@ -66,72 +66,103 @@ class OpenStruct
end end
end end
def escape_args_to(buf, args) class NinjaPiece
args.each { |a| protected def dump_ninja(buf)
raise "abstract"
end
end
class NinjaPieceRaw
def initialize(data)
@data = data
end
def dump_ninja(buf)
buf << @data << "\n\n"
end
end
class Target < NinjaPiece
attr_reader :meta, :outputs, :inputs, :implicit_inputs, :implicit_outputs, :rule
def initialize(outputs:, inputs:, rule:, implicit_inputs: [], implicit_outputs: [])
raise "invalid param" if not outputs.kind_of?(Array)
raise "invalid param" if not inputs.kind_of?(Array)
raise "invalid param" if not implicit_inputs.kind_of?(Array)
raise "invalid param" if not implicit_outputs.kind_of?(Array)
raise "rule must be a string" if not rule.kind_of?(String)
@meta = OpenStruct.new
@implicit_inputs = implicit_inputs
@implicit_outputs = implicit_outputs
@inputs = inputs.clone
@outputs = outputs.clone
@rule = rule
end
def dump_ninja(buf)
raise "no output" if outputs.size == 0
buf << "build"
DefaultTargets::escape_args_to(buf, outputs)
if implicit_outputs.size > 0
buf << " |"
DefaultTargets::escape_args_to(buf, implicit_outputs)
end
buf << " : #{rule}"
DefaultTargets::escape_args_to(buf, inputs)
if implicit_inputs.size > 0
buf << " |"
DefaultTargets::escape_args_to(buf, implicit_inputs)
end
buf << "\n"
cb = Proc.new { |k, v|
buf << " " << k << " = " << v << "\n"
}
dump_vars cb
buf << "\n"
end
protected def dump_vars(cb)
raise "abstract for #{self}"
end
end
module DefaultTargets
def self.escape_args_to(buf, args)
raise "buf can't be nil" if buf.nil?
dump_args = args.flat_map { |arg|
if arg.kind_of?(Target)
arg.outputs + arg.implicit_outputs
elsif arg.kind_of?(Pathname)
[arg.to_s]
elsif arg.kind_of?(String)
[arg]
else
raise "unsupported type #{arg.class} of #{arg}"
end
}
dump_args.each { |a|
buf << ' ' buf << ' '
buf << Shellwords.escape(a).gsub(/\\=/, '=') buf << Shellwords.escape(a).gsub(/\\=/, '=')
} }
end
class Target
attr_reader :trg_name, :meta, :outputs
def initialize(trg_name, dependencies)
@meta = OpenStruct.new
if dependencies.nil?
raise "dependencies can't be nil"
end
@trg_name = trg_name
if @trg_name.kind_of?(Pathname)
@trg_name = @trg_name.to_s
end
@outputs = [trg_name]
raise "target name is not a string (got `#{@trg_name}`)" if not @trg_name.kind_of?(String)
@dependencies = dependencies
end end
def add_deps(*deps) class Phony < Target
@dependencies.concat(deps.flatten) attr_reader :name
def initialize(name:, inputs:, implicit_inputs: [])
super(outputs: [name].freeze, inputs: inputs, implicit_inputs: implicit_inputs, implicit_outputs: [].freeze, rule: "phony")
@name = name
end end
def inspect protected def dump_vars(cb)
"<#{self.class.name}:#{@trg_name}>" end
end end
def dump_rules(buf) class Command < Target
buf << "build "
escape_args_to(buf, outputs)
buf << ": #{mode}"
@dependencies.each { |d|
buf << ' '
if d.kind_of?(Target)
buf << d.trg_name
elsif d.kind_of?(Pathname)
buf << d.to_s
elsif d.kind_of?(String)
buf << d
else
raise "Invalid dependency #{d} : #{d.class} for #{self}"
end
}
buf << "\n"
dump_rules_impl(buf)
buf << "\n\n"
end
def mode
raise "abstract"
end
protected def dump_rules_impl(buf)
raise "abstract"
end
end
class CommandTarget < Target
attr_reader :output_file attr_reader :output_file
def initialize(output_file, dependencies, cwd, commands, depfile, pool, env) def initialize(output_file, dependencies, cwd, commands, depfile, pool, env)
super(output_file, dependencies) super(outputs: [output_file], inputs: dependencies, rule: 'CUSTOM_COMMAND')
@env = env @env = env
@depfile = depfile @depfile = depfile
@output_file = output_file @output_file = output_file
@ -140,139 +171,106 @@ class CommandTarget < Target
@pool = pool @pool = pool
end end
protected def dump_rules_impl(buf) protected def dump_vars(cb)
if @env.size > 0 if @env.size > 0
buf << " ENV = env" buf = String.new
buf << "env"
@env.each { |k, v| @env.each { |k, v|
buf << ' ' << k << '=' << Shellwords.escape(v).gsub(/\\=/, '=') buf << ' ' << k << '=' << Shellwords.escape(v).gsub(/\\=/, '=')
} }
buf << "\n" cb.('ENV', buf)
end end
if not @pool.nil? if not @pool.nil?
buf << " pool = #{@pool}\n" cb.('pool', @pool)
end end
if not @cwd.nil? if not @cwd.nil?
buf << " WD = #{Shellwords.escape @cwd}\n" cb.('CWD', Shellwords.escape(@cwd))
end end
if not @depfile.nil? if not @depfile.nil?
buf << " depfile = #{@depfile}\n" cb.('depfile', Shellwords.escape(@depfile))
end end
buf << " COMMAND =" cmd = String.new
@commands.each_with_index { |c, i| @commands.each_with_index { |c, i|
if i != 0 if i != 0
buf << " &&" cmd << " &&"
end end
escape_args_to(buf, c) DefaultTargets::escape_args_to(cmd, c)
} }
buf << "\n" cb.('COMMAND', cmd)
end
end end
def mode class ConfigureGenerated < Target
"CUSTOM_COMMAND" def initialize(configurator)
end
end
class ConfigureGeneratedTarget < Target
def initialize(configurator, output_file)
@configurator = configurator @configurator = configurator
super(output_file, []) super(outputs: [], inputs: [], rule: 'RERUN_YA_BUILD')
end end
def dump_rules_impl(buf)
buf << " pool = console\n"
end
def mode
"RERUN_YA_BUILD"
end
end
class CTarget < Target def dump_vars(cb)
cb.('pool', 'console')
end
end
class C < Target
attr_reader :output_file attr_reader :output_file
def initialize(output_file, mode, dependencies, flags, cc, root_dir) def initialize(output:, inputs:, flags:, cc:, rule:, root_dir: nil)
super(output_file, dependencies) super(outputs: [output].freeze, inputs: inputs, rule: rule)
@root_dir = root_dir @root_dir = root_dir
@mode = mode @output_file = output
@output_file = output_file
@flags = flags @flags = flags
@cc = cc @cc = cc
end end
protected def dump_rules_impl(buf) protected def dump_vars(cb)
if @mode != "link"
buf << " ROOT_DIR = #{Shellwords.escape @root_dir}/\n"
end
if not @cc.nil? if not @cc.nil?
buf << " CC = #{@cc}\n" cb.('CC', Shellwords.escape(@cc))
end
if not @root_dir.nil?
cb.('ROOT_DIR', Shellwords.escape(@root_dir))
end end
if not @flags.nil? and not @flags.empty? if not @flags.nil? and not @flags.empty?
buf << " cflags =" cflags = String.new
escape_args_to(buf, @flags) DefaultTargets::escape_args_to(cflags, @flags)
buf << "\n" cb.('cflags', cflags)
end
end end
end end
def mode() class Copy < Target
case @mode
when "compile"
"COMPILE_C"
when "link"
"LINK_C"
else
raise "unknown mode #{@mode}"
end
end
end
class AliasTarget < Target
def initialize(name, dependencies)
super(name, dependencies)
end
protected def dump_rules_impl(buf)
end
def mode()
"phony"
end
end
class CopyTarget < Target
def initialize(dest, src) def initialize(dest, src)
super(dest, [src]) super(outputs: [dest].freeze, inputs: src, rule: "COPY")
end end
protected def dump_rules_impl(buf) protected def dump_vars(buf)
end end
def mode()
"COPY"
end end
end end
class Configurator class Configurator
attr_reader :root_src, :root_build, :config, :logger attr_reader :root_src, :root_build, :config, :logger, :ninja_path, :ninja_files_parts
private def get_tag_target(tag) private def get_tag_target(tag)
ret = @tags[tag] ret = @tags[tag]
return ret if not ret.nil? return ret if not ret.nil?
ret = AliasTarget.new("tags/#{tag}", []) ret = DefaultTargets::Phony.new(name: "tags/#{tag}", inputs: [])
@tags[tag] = ret @tags[tag] = ret
register_target(ret) register_target(ret)
ret ret
end end
def initialize(src, build, preloads) def initialize(src, build, preloads)
@rules = []
@root_src = Pathname.new(src).realpath @root_src = Pathname.new(src).realpath
@root_build = Pathname.new(build) @root_build = Pathname.new(build)
@root_build.mkpath() @root_build.mkpath()
@root_build = @root_build.realpath @root_build = @root_build.realpath
@trivial_targets = String.new @ninja_files_parts = Hash.new
@ninja_files_parts[''] = []
@tags = {} @tags = {}
@config_target = ConfigureGeneratedTarget.new(self, 'build.ninja') @config_target = DefaultTargets::ConfigureGenerated.new(self)
@targets = [@config_target] @named_targets = []
@stack = [] @stack = []
@ -285,18 +283,80 @@ class Configurator
end end
@config = OpenStruct.new @config = OpenStruct.new
@config.tools = OpenStruct.new
cnf = root_src.join('yabuild-default-conf.rb') cnf = root_src.join('yabuild-default-conf.rb')
if cnf.exist? if cnf.exist?
@config_target.add_deps(cnf) @config_target.inputs.push(cnf)
@config = self.instance_eval(cnf.read, cnf.to_s) self.instance_eval(cnf.read, cnf.to_s)
end end
preloads.each { |preload| preloads.each { |preload|
preload = Pathname.new(preload).realpath preload = Pathname.new(preload).realpath
@config_target.add_deps(preload) @config_target.inputs.push(preload)
contents = preload.read self.instance_eval(preload.read, preload.to_s)
self.instance_eval(contents, preload.to_s)
} }
@ninja_path = find_executable('ninja', critical: true)
fill_default_ninja
register_target(@config_target)
end
private def fill_default_ninja
default_pieces = []
@ninja_files_parts['default-rules'] = default_pieces
@ninja_files_parts[''] << NinjaPieceRaw.new(<<-EOF
# ya-build generated, do not edit
# src: #{root_src}
ninja_required_version = 1.5
ya_ninja_workdir = #{root_build}
include default-rules.ninja
build clean: CLEAN
build help: HELP
EOF
)
default_pieces << NinjaPieceRaw.new(<<-EOF
# ya-build generated, do not edit
CC = clang
rule RERUN_YA_BUILD
command = cd #{Shellwords.escape Dir.getwd} && #{SELF_COMMAND.map { |x| Shellwords.escape x }.join(' ')}
description = rerunning ya-build
rule CLEAN
command = #{Shellwords.escape ninja_path} $FILE_ARG -t clean $TARGETS
description = Cleaning all built files...
rule HELP
command = #{Shellwords.escape ninja_path} -t targets rule phony rule CLEAN rule HELP
description = All primary targets available
rule CUSTOM_COMMAND
command = cd $CWD && $ENV $COMMAND
description = $DESC
rule C_COMPILE
depfile = $out.d
command = $CC \
-MD -MF $out.d \
-c $cflags \
-o $out \
$in
rule C_LINK
command = $CC $cflags -o $out $in
rule COPY
command = cp $in $out
EOF
)
end end
private def extend_config_impl(l, r) private def extend_config_impl(l, r)
@ -347,65 +407,21 @@ class Configurator
end end
def eval_script(path) def eval_script(path)
script_path = cur_src.join(@stack[-1].path.join(path)) script_path = cur_src.join(path)
@config_target.add_deps(script_path)
contents = script_path.read contents = script_path.read
@config_target.inputs.push script_path
self.instance_eval(contents, script_path.to_s) self.instance_eval(contents, script_path.to_s)
end end
private def run_last_stack() private def run_last_stack()
path = @stack[-1].path path = @stack[-1].path
@logger.info("configuring #{path}") @logger.info("entering #{path}")
script_path = root_src.join(path, 'yabuild.rb') script_path = root_src.join(path, 'yabuild.rb')
@config_target.add_deps(script_path) @config_target.inputs.push(script_path)
contents = script_path.read contents = script_path.read
self.instance_eval(contents, script_path.to_s) self.instance_eval(contents, script_path.to_s)
end end
def add_rule(rule)
@rules.push(rule)
end
private def rules_str()
beg = [<<-EOF
# ya-build generated, do not edit
CC = clang
rule RERUN_YA_BUILD
command = cd #{Shellwords.escape Dir.getwd} && #{SELF_COMMAND.map { |x| Shellwords.escape x }.join(' ')}
description = rerunning ya-build
rule CLEAN
command = /usr/bin/ninja $FILE_ARG -t clean $TARGETS
description = Cleaning all built files...
rule HELP
command = /usr/bin/ninja -t targets rule phony rule CLEAN rule HELP
description = All primary targets available
rule CUSTOM_COMMAND
command = cd $WD && $ENV $COMMAND
description = $DESC
rule COMPILE_C
depfile = $out.d
command = cd $ROOT_DIR && \
$CC -MD -MF $out.d $cflags -o $out "$$(#{[RbConfig.ruby, Pathname.new(__FILE__).realpath, '__realpath_to_from'].map(&Shellwords.method(:escape)).join(' ')} "$in" "$ROOT_DIR")" && \
#{[RbConfig.ruby, Pathname.new(__FILE__).realpath, '__patch_dep'].map(&Shellwords.method(:escape)).join(' ')} $ROOT_DIR $out.d
rule LINK_C
command = $CC $cflags -o $out $in
rule COPY
command = cp $in $out
EOF
]
beg += @rules
beg.join("\n\n")
end
def run() def run()
@stack = [ @stack = [
OpenStruct.new( OpenStruct.new(
@ -418,44 +434,49 @@ EOF
File.write(root_build.join('config.json'), @config.to_json) File.write(root_build.join('config.json'), @config.to_json)
File.write(root_build.join('rules.ninja'), rules_str) ninja_files_parts.each { |k, v|
@config_target.outputs << "#{if k == "" then "build" else k end}.ninja"
build_str = String.new() }
build_str << <<-EOF
# ya-build generated, do not edit ninja_files_parts.each { |k, v|
# src: #{root_src} if k == ''
ninja_required_version = 1.5 k = 'build'
ya_ninja_workdir = #{root_build} end
include rules.ninja fname = root_build.join("#{k}.ninja")
buf = String.new
build clean: CLEAN buf << "# ya-build generated, do not edit\n"
v.each do |v|
build help: HELP v.dump_ninja buf
rescue => e
EOF logger.error("can't dump #{v.inspect}")
@targets.each { |t| raise
t.dump_rules(build_str) end
if k == 'build'
buf << "default tags/all\n"
end
File.write fname, buf
} }
build_str << "default tags/all\n"
File.write(root_build.join('build.ninja'), build_str)
end end
def find_target(name) def find_target(name)
suitable = @targets.filter { |trg| suitable = @named_targets.filter { |trg|
trg.trg_name.match?(name) trg.name.match?(name)
} }
raise "couldn't find target by name #{name} ; suitable: #{suitable}" if suitable.size() != 1 raise "couldn't find target by name #{name} ; suitable: #{suitable}" if suitable.size() != 1
suitable[0] suitable[0]
end end
private def register_target(trg) private def register_target(trg, to: '')
@targets.push(trg) @ninja_files_parts[to] << trg
if trg.kind_of?(DefaultTargets::Phony)
@named_targets.push(trg)
end
end end
private def return_target(trg, tags: [], &block) private def return_target(trg, tags: [], &block)
register_target(trg) register_target(trg)
tags.map { |t| get_tag_target(t) }.each { |t| tags.map { |t| get_tag_target(t) }.each { |t|
t.add_deps trg t.inputs.push trg
} }
if not block.nil? if not block.nil?
trg.instance_eval(&block) trg.instance_eval(&block)
@ -486,27 +507,68 @@ EOF
root_src.join(@stack[-1].path) root_src.join(@stack[-1].path)
end end
def find_executable(name, critical: false) def find_executable(name, critical: false, check_first: [], check_path: true, check_last: [], &blk)
chck = Proc.new { |path|
if path.nil?
next false
end
if not path.exist?
next false
end
logger.debug("executable #{name} found at #{path}")
if blk.nil?
next true
end
begin
res = blk.(path)
if not res
logger.info("executable #{name} found at #{path} does not work (check failed)")
end
res
rescue => e
logger.info("executable #{name} found at #{path} does not work\n#{e}")
false
end
}
override = config.tools.send(name)
if not override.nil?
res = chck.(override)
if res.nil? and critical
logger.error("executable #{name} not found for override #{override}")
raise "#{name} not found"
end
return res
end
check_first.each { |p|
path = Pathname.new(p).join(name)
if chck.(path)
return path
end
}
if check_path
paths = ENV['PATH'].split(':') paths = ENV['PATH'].split(':')
paths << '/usr/bin'
paths << '/bin'
paths << "#{ENV['HOME']}/.local/bin"
paths << "#{ENV['HOME']}/.cargo/bin"
paths.each { |p| paths.each { |p|
check = ['', '.elf', '.exe'] path = Pathname.new(p).join(name)
check.each { |c| if chck.(path)
cur_p = Pathname.new(p).join("#{name}#{c}") return path
if cur_p.exist?()
logger.info("checking for #{name}... located at #{cur_p}")
return cur_p
end end
} }
end
check_last.each { |p|
path = Pathname.new(p).join(name)
if chck.(path)
return path
end
} }
if critical if critical
logger.error("checking for #{name}... not found") logger.error("executable #{name} not found")
raise "#{name} not found in path" raise "#{name} not found"
end end
logger.info("checking for #{name}... not found") logger.info("executable #{name} not found")
return nil return nil
end end
@ -523,7 +585,7 @@ EOF
&blk &blk
) )
if commands.nil? == command.nil? if commands.nil? == command.nil?
raise "exectly one of command or commands must be specified" raise "exactly one of command or commands must be specified"
end end
if commands.nil? if commands.nil?
commands = [command] commands = [command]
@ -533,29 +595,24 @@ EOF
cwd = cur_src cwd = cur_src
end end
trg = CommandTarget.new(output_file, dependencies, cwd, commands, depfile, pool, env) trg = DefaultTargets::Command.new(output_file, dependencies, cwd, commands, depfile, pool, env)
return_target(trg, **kwargs, &blk) return_target(trg, **kwargs, &blk)
end end
def target_copy(dest:, src:, **kwargs, &blk) def target_copy(dest:, src:, **kwargs, &blk)
trg = CopyTarget.new(dest, src) trg = DefaultTargets::Copy.new(dest, src)
return_target(trg, **kwargs, &blk) return_target(trg, **kwargs, &blk)
end end
def target_c(output_file:, mode:, root_dir: nil, file: nil, objs: nil, flags: nil, cc: nil, **kwargs, &blk) def target_c_compile(output_file:, file:, flags: nil, cc: nil, **kwargs, &blk)
if (mode == "compile") == file.nil? real_out = output_file.relative_path_from(root_build)
raise "file must be provided only for compile" real_file = file.relative_path_from(root_build)
trg = DefaultTargets::C.new(output: real_out.to_s, inputs: [real_file.to_s], flags: flags, cc: cc, rule: "C_COMPILE")
return_target(trg, **kwargs, &blk)
end end
if (mode == "link") == objs.nil?
raise "objs must be provided only for link" def target_c_link(output_file:, objs:, flags: nil, cc: nil, **kwargs, &blk)
end trg = DefaultTargets::C.new(output: output_file, inputs: objs, flags: flags, cc: cc, rule: "C_LINK")
if root_dir.nil?
root_dir = cur_src
end
deps = if objs.nil? then [
root_dir.join(file)
] else objs end
trg = CTarget.new(output_file, mode, deps, flags, cc, root_dir)
return_target(trg, **kwargs, &blk) return_target(trg, **kwargs, &blk)
end end
@ -565,7 +622,7 @@ EOF
name_full += "/" name_full += "/"
end end
name_full += name name_full += name
trg = AliasTarget.new(name_full, dependencies) trg = DefaultTargets::Phony.new(name: name_full, inputs: dependencies)
if dependencies.size() != 1 and inherit_meta.size() != 0 if dependencies.size() != 1 and inherit_meta.size() != 0
raise "Can inherit meta only for single dependency alias" raise "Can inherit meta only for single dependency alias"
end end
@ -576,11 +633,11 @@ EOF
end end
def mark_as_config_generated(file) def mark_as_config_generated(file)
@config_target.outputs.push file @config_target.implicit_outputs.push file
end end
def reconfigure_on_change(file) def reconfigure_on_change(file)
@config_target.add_deps file @config_target.inputs.push file
end end
end end