# Copyright © 2016 Vlad Petric

# Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the “Software”), to # deal in the Software without restriction, including without limitation the # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or # sell copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE.

$MODES = $MODE_COMPILE_FLAGS.keys $COMPILER_PREFIX = $COMPILER_PREFIX.nil? ? “” : $COMPILER_PREFIX + “ ” $LINKER_PREFIX = $LINKER_PREFIX.nil? ? $COMPILER_PREFIX : $LINKER_PREFIX + “ ” $LINKER = $LINKER.nil? ? $COMPILER : $LINKER $LINK_FLAGS = $LINK_FLAGS.nil? ? $COMPILE_FLAGS : $LINK_FLAGS $MODE_LINK_FLAGS = $MODE_LINK_FLAGS.nil? ? $MODE_COMPILE_FLAGS : $MODE_LINK_FLAGS

module Util

def Util.make_relative_path(path)
  absolute_path = File.absolute_path(path)
  base_dir = File.absolute_path(Dir.pwd)
  base_dir << '/' if !base_dir.end_with?('/')
  r = absolute_path.start_with?(base_dir) ? absolute_path[base_dir.size..-1] : nil
  if !r.nil? && r.start_with?('/')
    raise "Relative path #{r} is not relative"
  end
  r
end

end

module FileMapper

# Extract build mode from path.
# E.g., debug/a/b/c.o returns debug.
def FileMapper.get_mode(path)
  rel_path = Util.make_relative_path(path)
  raise "Path #{path} does not belong to #{Dir.pwd}" if rel_path.nil?
  mode = rel_path[/^([^\/]*)/, 1]
  raise "Unknown mode #{mode} for #{path}" if !$MODES.include?(mode)
  mode
end
def FileMapper.get_mode_from_akpath(path)
  rel_path = Util.make_relative_path(path)
  raise "Path #{path} does not belong to #{Dir.pwd}" if rel_path.nil?
  mode = rel_path[/^\.akro\/([^\/]*)/, 1]
  raise "Unknown mode #{mode} for #{path}" if !$MODES.include?(mode)
  mode
end
# Strip the build mode from path.
def FileMapper.strip_mode(path)
  rel_path = Util.make_relative_path(path)
  raise "Path #{path} does not belong to #{Dir.pwd}" if rel_path.nil?
  get_mode(rel_path) # for sanity checking
  rel_path[/^[^\/]*\/(.*)$/, 1]
end
# Maps object file to its corresponding depcache file.
# E.g., release/a/b/c.o maps to .akro/release/a/b/c.depcache
def FileMapper.map_obj_to_dc(path)
  FileMapper.get_mode(path)
  raise "#{path} is not a #{$OBJ_EXTENSION} file" if !path.end_with?($OBJ_EXTENSION)
  ".akro/#{path[0..-$OBJ_EXTENSION.length-1]}.depcache"
end
# Maps object file to its corresponding cpp file, if it exists.
# E.g., release/a/b/c.o maps to a/b/c{.cpp,.cc,.cxx,.c++}
def FileMapper.map_obj_to_cpp(path)
  raise "#{path} is not a #{$OBJ_EXTENSION} file" if !path.end_with?($OBJ_EXTENSION)
  file = FileMapper.strip_mode(path)
  file = file[0..-$OBJ_EXTENSION.length-1]
  # Under windows, make_relative_path also canonicalizes the path.
  srcs = $CPP_EXTENSIONS.map{|ext| file + ext}.select{|fname| File.exist?(fname)}.map{|fname| Util.make_relative_path(fname)}.uniq
  raise "Multiple sources for base name #{file}: #{srcs.join(' ')}" if srcs.length > 1
  srcs.length == 0? nil : srcs[0]
end
def FileMapper.map_cpp_to_dc(mode, path)
  $CPP_EXTENSIONS.map do |ext|
    return ".akro/#{mode}/#{path[0..-ext.length-1]}.depcache" if path.end_with?(ext)
  end
  raise "#{path} is not one of: #{$CPP_EXTENSIONS.join(',')}"
end
def FileMapper.map_cpp_to_obj(mode, path)
  $CPP_EXTENSIONS.map do |ext|
    return "#{mode}/#{path[0..-ext.length-1]}#{$OBJ_EXTENSION}" if path.end_with?(ext)
  end
  raise "#{path} is not one of: #{$CPP_EXTENSIONS.join(',')}"
end
# Maps depcache file to its corresponding cpp file, which should exist.
# E.g., .akro/release/a/b/c.o maps to a/b/c{.cpp,.cc,.cxx,.c++}
def FileMapper.map_dc_to_cpp(path)
  raise "#{path} is not a .depcache file" if !path.end_with?('.depcache') || !path.start_with?('.akro')
  file = path[/^\.akro\/(.*)\.depcache$/, 1]
  file = FileMapper.strip_mode(file)
  srcs = $CPP_EXTENSIONS.map{|ext| file + ext}.select{|fname| File.exist?(fname)}
  raise "Multiple sources for base name #{file}: #{srcs.join(' ')}" if srcs.length > 1
  raise "No sources for base name #{file}" if srcs.length == 0
  srcs[0]
end
def FileMapper.map_dc_to_compcmd(path)
  raise "#{path} is not a .depcache file" if !path.end_with?('.depcache') || !path.start_with?('.akro')
  path.gsub(/\.depcache$/, ".compcmd" )
end
def FileMapper.map_compcmd_to_cpp(path)
  raise "#{path} is not a .compcmd file" if !path.end_with?('.compcmd') || !path.start_with?('.akro')
  file = path[/^\.akro\/(.*)\.compcmd$/, 1]
  file = FileMapper.strip_mode(file)
  srcs = $CPP_EXTENSIONS.map{|ext| file + ext}.select{|fname| File.exist?(fname)}
  raise "Multiple sources for base name #{file}: #{srcs.join(' ')}" if srcs.length > 1
  raise "No sources for base name #{file}" if srcs.length == 0
  srcs[0]
end
def FileMapper.map_exe_to_linkcmd(path)
  ".akro/#{path.gsub(/\.exe$/, ".linkcmd" )}"
end
def FileMapper.map_linkcmd_to_exe(path)
  path[/^.akro\/(.*)\.linkcmd$/, 1] + ".exe"
end
def FileMapper.map_static_lib_to_linkcmd(path)
  ".akro/#{path.gsub(/#{$STATIC_LIB_EXTENSION}$/, ".stlinkcmd" )}"
end
def FileMapper.map_linkcmd_to_static_lib(path)
  path[/^.akro\/(.*)\.stlinkcmd$/, 1] + $STATIC_LIB_EXTENSION
end
def FileMapper.map_dynamic_lib_to_linkcmd(path)
  ".akro/#{path.gsub(/#{$DYNAMIC_LIB_EXTENSION}$/, ".dynlinkcmd" )}"
end
def FileMapper.map_linkcmd_to_dynamic_lib(path)
  path[/^.akro\/(.*)\.dynlinkcmd$/, 1] + $DYNAMIC_LIB_EXTENSION
end
# Maps header file to its corresponding cpp file, if it exists
# E.g., a/b/c.h maps to a/b/c.cpp, if a/b/c.cpp exists, otherwise nil
def FileMapper.map_header_to_cpp(path)
  rel_path = Util.make_relative_path(path)
  # file is not local
  return nil if rel_path.nil?
  srcs = $HEADER_EXTENSIONS.select{|ext| rel_path.end_with?(ext)}.collect{ |ext|
    base_path = rel_path[0..-ext.length-1]
    $CPP_EXTENSIONS.map{|cppext| base_path + cppext}.select{|file| File.exist?(file)}
  }.flatten.uniq
  raise "Multiple sources for base name #{path}: #{srcs.join(' ')}" if srcs.length > 1
  srcs.length == 0? nil : srcs[0]
end

def FileMapper.map_script_to_exe(path)
  path_no_ext = path[/^(.*)[^.\/]*$/, 1]
  srcs = $CPP_EXTENSIONS.map{|cppext| path + cppext}.select{|file| File.exist?(file)}
  srcs.length == 0? nil : path + ".exe"
end

end

#Builder encapsulates the compilation/linking/dependecy checking functionality module Builder

def Builder.create_depcache(src, dc)
  success = false
  mode = FileMapper.get_mode_from_akpath(dc)
  basedir, _ = File.split(dc)
  FileUtils.mkdir_p(basedir)
  output = File.open(dc, "w")
  puts "Determining dependencies for #{dc}" if $VERBOSE_BUILD
  begin
    #Using backticks as Rake's sh outputs the command. Don't want that here.
    cmdline = CmdLine.dependency_cmdline(mode, src)
    puts cmdline if $VERBOSE_BUILD
    deps = `#{cmdline}`
    raise "Dependency determination failed for #{src}" if $?.to_i != 0
    # Replace quoted spaces with placeholders
    deps.gsub!(/\\ /, '<*%#?>') # a string that never exists in filenames
    # Get rid of endlines completeley
    deps.gsub!(/\\\n/, '')
    # also get rid of <filename>: at the beginning
    # split by spaces
    deps[/^[^:]*:(.*)$/, 1].split(' ').each do |line|
      # Output either a relative path if the file is local, or the original line.
      line.gsub!('<*%#?>', ' ')
      output << (Util.make_relative_path(line.strip) || line) << "\n"
    end
    output.close
    success = true
  ensure
    FileUtils.rm(dc) if !success
  end
end

def Builder.compile_object(src, obj)
  mode = FileMapper.get_mode(obj)
  basedir, _ = File.split(obj)
  FileUtils.mkdir_p(basedir)
  RakeFileUtils::sh(CmdLine.compile_cmdline(mode, src, obj)) do |ok, res|
    raise "Compilation failed for #{src}" if !ok
  end
end

def Builder.link_binary(objs, bin)
  mode = FileMapper.get_mode(bin)
  basedir, _ = File.split(bin)
  FileUtils.mkdir_p(basedir)
  RakeFileUtils::sh(CmdLine.link_cmdline(mode, objs, bin)) do |ok, res|
    raise "Linking failed for #{bin}" if !ok
  end
end
def Builder.archive_static_library(objs, bin)
  basedir, _ = File.split(bin)
  FileUtils.mkdir_p(basedir)
  RakeFileUtils::sh(CmdLine.static_lib_cmdline(objs, bin)) do |ok, res|
    raise "Archiving failed for #{bin}" if !ok
  end
end

def Builder.build_dynamic_library(mode, objs, additional_params, bin)
  mode = FileMapper.get_mode(bin)
  basedir, _ = File.split(bin)
  FileUtils.mkdir_p(basedir)
  RakeFileUtils::sh(CmdLine.dynamic_lib_cmdline(mode, objs, additional_params, bin)) do |ok, res|
    raise "Building dynamic library #{bin} failed" if !ok
  end
end

def Builder.depcache_object_collect(mode, top_level_srcs)
  all_covered_cpps = Set.new
  all_objects = []
  srcs = top_level_srcs
  while !srcs.empty?
    new_srcs = [] 
    dcs = srcs.map{|src| FileMapper.map_cpp_to_dc(mode, src)}
    dcs.each{|dc| Rake::Task[dc].invoke}
    dcs.each do |dc|
      cpp = FileMapper.map_dc_to_cpp(dc)
      obj = FileMapper.map_cpp_to_obj(mode, cpp)
      all_objects << obj if !all_objects.include?(obj)
      File.readlines(dc).map{|line| line.strip}.each do |header|
        new_cpp = FileMapper.map_header_to_cpp(header)
        if !new_cpp.nil? and !all_covered_cpps.include?(new_cpp)
          new_srcs << new_cpp
          all_covered_cpps << new_cpp
        end
      end
    end
    srcs = new_srcs
  end
  all_objects
end

end

#Phony task that forces anything depending on it to run task “always”

rule “.compcmd” => ->(compcmd) {

mode = FileMapper.get_mode_from_akpath(compcmd)
src = FileMapper.map_compcmd_to_cpp(compcmd)
cmd = CmdLine.compile_base_cmdline(mode, src)
if File.exists?(compcmd) && File.read(compcmd).strip == cmd.strip then
  []
else
  "always"
end

} do |task|

basedir, _ = File.split(task.name)
FileUtils.mkdir_p(basedir)
output = File.open(task.name, "w")
mode = FileMapper.get_mode_from_akpath(task.name)
src = FileMapper.map_compcmd_to_cpp(task.name)
output << CmdLine.compile_base_cmdline(mode, src) << "\n"
output.close

end

rule “.linkcmd” => ->(dc) {

binary = FileMapper.map_linkcmd_to_exe(dc)
raise "Internal error - linkcmd not mapped for #{binary}" if !$LINK_BINARY_OBJS.has_key?(binary)
mode = FileMapper.get_mode_from_akpath(dc)
cmd = CmdLine.link_cmdline(mode, $LINK_BINARY_OBJS[binary], binary)
if File.exists?(dc) && File.read(dc).strip == cmd.strip then
  []
else
  "always"
end

} do |task|

basedir, _ = File.split(task.name)
binary = FileMapper.map_linkcmd_to_exe(task.name)
FileUtils.mkdir_p(basedir)
output = File.open(task.name, "w")
mode = FileMapper.get_mode_from_akpath(task.name)
output << CmdLine.link_cmdline(mode, $LINK_BINARY_OBJS[binary], binary) << "\n"
output.close

end

rule “.dynlinkcmd” => ->(dc) {

dynlib = FileMapper.map_linkcmd_to_dynamic_lib(dc)
raise "Internal error - linkcmd not mapped for #{dynlib}" if !$LINK_BINARY_OBJS.has_key?(dynlib)
mode = FileMapper.get_mode_from_akpath(dc)
cmd = CmdLine.dynamic_lib_cmdline(mode, $LINK_BINARY_OBJS[dynlib], $LINK_LIBRARY_EXTRAFLAGS[dynlib], dynlib)
if File.exists?(dc) && File.read(dc).strip == cmd.strip then
  []
else
  "always"
end

} do |task|

basedir, _ = File.split(task.name)
dynlib = FileMapper.map_linkcmd_to_dynamic_lib(task.name)
FileUtils.mkdir_p(basedir)
output = File.open(task.name, "w")
mode = FileMapper.get_mode_from_akpath(task.name)
output << CmdLine.dynamic_lib_cmdline(mode, $LINK_BINARY_OBJS[dynlib], $LINK_LIBRARY_EXTRAFLAGS[dynlib], dynlib) << "\n"
output.close

end

rule “.stlinkcmd” => ->(dc) {

stlib = FileMapper.map_linkcmd_to_static_lib(dc)
raise "Internal error - linkcmd not mapped for #{stlib}" if !$LINK_BINARY_OBJS.has_key?(stlib)
mode = FileMapper.get_mode_from_akpath(dc)
cmd = CmdLine.static_lib_cmdline($LINK_BINARY_OBJS[stlib], stlib)
if File.exists?(dc) && File.read(dc).strip == cmd.strip then
  []
else
  "always"
end

} do |task|

basedir, _ = File.split(task.name)
stlib = FileMapper.map_linkcmd_to_static_lib(task.name)
FileUtils.mkdir_p(basedir)
output = File.open(task.name, "w")
mode = FileMapper.get_mode_from_akpath(task.name)
output << CmdLine.static_lib_cmdline($LINK_BINARY_OBJS[stlib], stlib) << "\n"
output.close

end

rule “.depcache” => ->(dc){

[FileMapper.map_dc_to_compcmd(dc), FileMapper.map_dc_to_cpp(dc)] + 
(File.exist?(dc) ? File.readlines(dc).map{|line| line.strip}.map{|file| File.exist?(file) ? file : "always"}: [])

} do |task|

src = FileMapper.map_dc_to_cpp(task.name)
Builder.create_depcache(src, task.name)

end

rule $OBJ_EXTENSION => ->(obj){

src = FileMapper.map_obj_to_cpp(obj)
raise "No source for object file #{obj}" if src.nil?
dc = FileMapper.map_obj_to_dc(obj)
[src, dc, FileMapper.map_dc_to_compcmd(dc)] +
(File.exist?(dc) ? File.readlines(dc).map{|line| line.strip}: [])

} do |task|

src = FileMapper.map_obj_to_cpp(task.name)
Builder.compile_object(src, task.name)

end

def libname(mode, lib)

"#{mode}/#{lib.path}#{if lib.static then $STATIC_LIB_EXTENSION else $DYNAMIC_LIB_EXTENSION end}"

end

$LINK_BINARY_OBJS = Hash.new $LINK_LIBRARY_EXTRAFLAGS = Hash.new

rule “.exe” => ->(binary){

obj = binary.gsub(/\.exe$/, $OBJ_EXTENSION)
mode = FileMapper.get_mode(binary)
cpp = FileMapper.map_obj_to_cpp(obj)
raise "No proper #{$CPP_EXTENSIONS.join(',')} file found for #{binary}" if cpp.nil?
Rake::Task["#{mode}/all_capturing_libs"].invoke
obj_list = []
# Two passes through the object list - the capturing libraries will
# be inserted on the position of the *last* object in the list
last_obj = Hash.new
objs = Builder.depcache_object_collect(mode, [cpp])
objs.each do |obj|
  if $LIB_CAPTURE_MAP.has_key?(obj)
    last_obj[$LIB_CAPTURE_MAP[obj]] = obj
  end
end
objs.each do |obj|
  if $LIB_CAPTURE_MAP.has_key?(obj)
    capture_lib = $LIB_CAPTURE_MAP[obj]
    if last_obj[capture_lib] == obj
      obj_list << capture_lib
    end
  else
    obj_list << obj
  end
end
$LINK_BINARY_OBJS[binary] = obj_list
[FileMapper.map_exe_to_linkcmd(binary)] + obj_list

} do |task|

Builder.link_binary(task.prerequisites[1..-1], task.name)

end

$MODES.each do |mode|

rule /^#{mode}\/all_capturing_libs$/ => $AKRO_LIBS.select{|l| l.capture_deps}.collect{|l| libname(mode, l)} do |task|
  FileUtils.mkdir_p(mode)
  FileUtils::touch(task.name)
end

end

rule $STATIC_LIB_EXTENSION => ->(library) {

mode = FileMapper.get_mode(library)
srcs = []
lib = FileMapper.strip_mode(library)[0..-$STATIC_LIB_EXTENSION.length-1]
libspec = nil
$AKRO_LIBS.each do |alib|
  if alib.path == lib and alib.static
    raise "Library #{library} declared multiple times" if !libspec.nil?
    libspec = alib
    srcs << alib.sources
  end
end
raise "Library #{library} not found!" if libspec.nil?
Rake::Task["#{mode}/all_capturing_libs"].invoke if !libspec.capture_deps
srcs.flatten!
if libspec.recurse
  objs = Builder.depcache_object_collect(mode, srcs)
else
  objs = srcs.collect{|src| FileMapper.map_cpp_to_obj(mode, src)}
end
if libspec.capture_deps && !$CAPTURING_LIBS.include?(library)
  objs.each do |obj|
    if $LIB_CAPTURE_MAP.has_key?(obj)
      raise "Object #{obj} has dependency captures for multiple libraries - #{$LIB_CAPTURE_MAP[obj]} and #{library}"
    end
    $LIB_CAPTURE_MAP[obj] = library
  end
  $CAPTURING_LIBS << library
end
$LINK_BINARY_OBJS[library] = objs
[FileMapper.map_static_lib_to_linkcmd(library)] + objs

} do |task|

Builder.archive_static_library(task.prerequisites[1..-1], task.name)

end

rule $DYNAMIC_LIB_EXTENSION => ->(library) {

mode = FileMapper.get_mode(library)
srcs = []
lib = FileMapper.strip_mode(library)[0..-$DYNAMIC_LIB_EXTENSION.length-1]
libspec = nil
$AKRO_LIBS.each do |alib|
  if alib.path == lib and not alib.static
    raise "Library #{library} declared multiple times" if !libspec.nil?
    libspec = alib
    srcs << alib.sources
  end
end
raise "Library #{library} not found!" if libspec.nil?
Rake::Task["#{mode}/all_capturing_libs"].invoke if !libspec.capture_deps

srcs.flatten!
if libspec.recurse
  objs = Builder.depcache_object_collect(mode, srcs)
else
  objs = srcs.collect{|src| FileMapper.map_cpp_to_obj(mode, src)}
end
if libspec.capture_deps && !$CAPTURING_LIBS.include?(library)
  objs.each do |obj|
    if $LIB_CAPTURE_MAP.has_key?(obj)
      raise "Object #{obj} has dependency captures for multiple libraries - #{$LIB_CAPTURE_MAP[obj]} and #{library}"
    end
    $LIB_CAPTURE_MAP[obj] = library
  end
  $CAPTURING_LIBS << library
end
$LINK_BINARY_OBJS[library] = objs
$LINK_LIBRARY_EXTRAFLAGS[library] = libspec.additional_params
[FileMapper.map_dynamic_lib_to_linkcmd(library)] + objs

} do |task|

libspec = nil
lib = FileMapper.strip_mode(task.name)[0..-$DYNAMIC_LIB_EXTENSION.length-1]
$AKRO_LIBS.each do |alib|
  if alib.path == lib and not alib.static
    libspec = alib
  end
end
mode = FileMapper.get_mode(task.name)
Builder.build_dynamic_library(mode, task.prerequisites[1..-1], libspec.additional_params, task.name)

end

task :clean do

FileUtils::rm_rf(".akro/")
$MODES.each{|mode| FileUtils::rm_rf("#{mode}/")}

end

$MODES.each do |mode|

task mode
task "test_#{mode}"
$AKRO_BINARIES.each do |bin|
  raise "Binary cannot start with mode #{bin}" if bin.start_with?(mode + "/")
  Rake::Task[mode].enhance(["#{mode}/#{bin}"])
end
# Build all non-capturing libs by default.
# Capturing libs are automatically invoked by binaries anyway.
Rake::Task[mode].enhance($AKRO_LIBS.select{|l| !l.capture_deps}.map{|l| libname(mode, l)})
$AKRO_TESTS.each do |test|
  test_dep = 
    if !test.binary.nil?
      "#{mode}/#{test.binary}"
    else
      # map_script_to_exe may return nil, which is fine
      FileMapper.map_script_to_exe(test.script)
    end
  task "#{test.name}_test_#{mode}" => test_dep do |task|
    puts "Running test #{task.name}"
    base = (if !test.script.nil? then "#{test.script}" else "#{mode}/#{test.binary}" end)
    params = (if !test.cmdline.nil? then " " + test.cmdline else "" end)
    new_ld_path = if ENV.has_key?("LD_LIBRARY_PATH") then "#{mode}/:#{ENV['LD_LIBRARY_PATH']}" else "#{mode}/" end
    raise "Test #{task.name} failed" if !silent_exec(base + params, verbose: $VERBOSE_BUILD, env: {"MODE" => mode, "LD_LIBRARY_PATH" => new_ld_path})
    puts "Test #{task.name} passed"
  end
  Rake::Task["test_#{mode}"].enhance(["#{test.name}_test_#{mode}"])
end

end