class HotReload

Public Class Methods

new() click to toggle source
# File lib/hotreload.rb, line 18
def initialize
  @project_injected = Hash.new
  @pending = []
  @project_path = ""
  @project_file = ""
  @server = nil
  @last_injected = ""
  @hot_reload_id = 0
  @hot_reload_tmp_path = ""
  @compile_class = Hash.new
  @xcode_dev_path = "/Applications/Xcode.app/Contents/Developer"
  @task_queue = Queue.new

end
test(path) click to toggle source
# File lib/hotreload.rb, line 14
def self.test(path)
  puts "success, path is #{path}"
end

Public Instance Methods

build(file) click to toggle source

rebuild file

# File lib/hotreload.rb, line 170
def build(file)
  # project_model : project_file , build_Log
  project_model = get_project_model(file)
  project_file = project_model["project_file"]
  build_log_dir = project_model["build_log"]
  puts "project_file=#{project_file}, build_log=#{build_log_dir}"

  @hot_reload_id += 1
  file_tmp = "#{@hot_reload_tmp_path}/hotreload#{@hot_reload_id}"
  log_file =  "#{file_tmp}.log"

  compile_model = @compile_class[file]
  if !compile_model
    compile_model = get_compile_model(build_log_dir, file, file_tmp)
    if !compile_model
      puts
      "Could not locate compile command for #{file} (HotReload does not work with Whole Module Optimization. There are also restrictions on characters allowed in paths. All paths are also case sensitive is another thing to check.)"
      return
    end
  end

  puts "Compiling #{file}"
  project_dir_temp = Tool::escaping(@project_path, "$", "\\$0")
  compile_command = compile_model["compile_command"]
  command = "(cd #{project_dir_temp}) && #{compile_command} -o #{file_tmp}.o >#{log_file} 2>&1"
  ret = system(command)
  if !ret
    @compile_class.delete(file)
    puts "Re-compilation failed (#{file_tmp}.sh)\n#{File.read(log_file)})"
    return
  end

  # save in memory
  @compile_class[file] = compile_model

  # link result object file to create dylib
  puts "Creating dylib"
  regex = "\\s*(\\S+?\\.xctoolchain)"
  tool_chain = nil
  regex_result = compile_command.match(regex)
  if regex_result
    tool_chain = regex_result.captures[0]
  end
  if !tool_chain
    tool_chain = "#{@xcode_dev_path}/Toolchains/XcodeDefault.xctoolchain"
  end

  os_specific = ""
  cpu_arch = "arm64"
  is_real_device = true
  if compile_command.include?("iPhoneSimulator.platform")
    os_specific = "-isysroot #{@xcode_dev_path}/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk -mios-simulator-version-min=9.0 -L#{tool_chain}/usr/lib/swift/iphonesimulator -undefined dynamic_lookup"
    cpu_arch = "x86_64"
    is_real_device = false
  elsif compile_command.include?("iPhoneOS.platform")
    os_specific = "-isysroot #{@xcode_dev_path}/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk -mios-simulator-version-min=9.0 -L#{tool_chain}/usr/lib/swift/iphonesimulator -undefined dynamic_lookup"
    cpu_arch = "arm64"
  end

  link_command = "#{@xcode_dev_path}/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang -arch \"#{cpu_arch}\" -bundle #{os_specific} -dead_strip -Xlinker -objc_abi_version -Xlinker 2 -fobjc-arc #{file_tmp}.o -o #{file_tmp}.dylib >>#{log_file} 2>&1"
  ret = system(link_command)
  if !ret
    puts "Link failed, check #{@hot_reload_tmp_path}/command.sh \n #{log_file}"
    return
  end

  puts "codesign dylib"
  codesigner = Signer.new()
  codesigner.pre_codesign(build_log_dir, is_real_device)
  ret = codesigner.codeSignDylib("#{file_tmp}.dylib")
  if !ret
    puts "codesign failed"
    return
  else
    puts "codesign success \n #{file_tmp}.dylib"
    return "#{file_tmp}.dylib"
  end
end
build_log_path(project_file, derive_data_path) click to toggle source

get xcode build log

# File lib/hotreload.rb, line 264
def build_log_path(project_file, derive_data_path)
  # file = "/path/to/xyz.mp4"
  ext = File.extname(project_file)        # => ".mp4"
  name = File.basename(project_file, ext)  # => "xyz"
  prefix = "#{name}"+"-"

  build_log_array = []
  Dir.entries(derive_data_path).each do |sub|
    if sub.length <= prefix.length
      next
    end

    if sub.start_with?(prefix)
      buildLogDir = derive_data_path + "/" + sub + "/Logs/Build"
      if Dir.exist?(buildLogDir)
        build_log_array.push(buildLogDir)
      end
    end
  end

  log_sort = build_log_array.sort!{ |x,y| File.mtime(y) <=> File.mtime(x) }
  log_sort[0]
end
changeFiles(changeFiles) click to toggle source
# File lib/hotreload.rb, line 119
def changeFiles(changeFiles)
  now = Time.now.to_i
  changeFiles.each do |file|
    unless @pending.include?(file)
      time = @last_injected[file]
      if time == nil || now > time
        @last_injected[file] = now
        @pending.push(file)
      end
    end
  end

  @pending.each do |file|
    inject(file)
  end
  @pending.clear

end
get_compile_model(build_log_dir, file, tmp_file) click to toggle source

get compile command and changed file

# File lib/hotreload.rb, line 289
def get_compile_model(build_log_dir, file, tmp_file)
  if !File.file?(file)
    return
  end

#   find build command
  CompileCommand.new().getCompileCommand(build_log_dir, file, tmp_file)

  compile_command_file = "#{tmp_file}.sh"
  if !File.exist?(compile_command_file)
    puts "compile command parse error"
    return
  end

  compile_command = File.read("#{tmp_file}.sh")
  compile_command = compile_command.split(" -o ")[0] + " "

  compile_model = Hash["compile_command" => compile_command, "file" => file]
end
get_project_model(file) click to toggle source

get project file and xcode build log path

# File lib/hotreload.rb, line 250
def get_project_model(file)
  if !file.start_with?('/')
    puts "file is not validate: #{file}"
    return
  end

  # default deriveData path
  derive_data_path = File.expand_path("~/Library/Developer/Xcode/DerivedData")
  build_log = build_log_path(@project_file, derive_data_path)

  project_model = Hash["project_file" => @project_file, "build_log" => build_log]
end
get_symbol_file() click to toggle source

get class symbols

# File lib/hotreload.rb, line 310
def get_symbol_file
  file_tmp = "#{@hot_reload_tmp_path}/hotreload#{@hot_reload_id}"

  symbol_command = "#{@xcode_dev_path}/Toolchains/XcodeDefault.xctoolchain/usr/bin/nm #{file_tmp}.o | grep -E ' S _OBJC_CLASS_\\$_| _(_T0|\\$S|\\$s).*CN$' | awk '{print $3}' >#{file_tmp}.classes"
  ret = system(symbol_command)
  if !ret
    puts "Could not list class symbols"
  end

  class_file = "#{file_tmp}.classes"
end
inject(source) click to toggle source
# File lib/hotreload.rb, line 115
def inject(source)
  @task_queue << source
end
read_int() click to toggle source
# File lib/hotreload.rb, line 79
def read_int
  @server.read_int
end
read_message() click to toggle source
# File lib/hotreload.rb, line 83
def read_message
  @server.read_message
end
runTask() click to toggle source
# File lib/hotreload.rb, line 87
def runTask
  Thread.new do
    # rebuild
    while true do
      source = @task_queue.pop
      @server.send_command(Command::HRCommandBuilding, source)
      dylib = build(source)
      if !dylib
        puts "build failed"
        @server.send_command(Command::HRCommandBuildFailed, source)
        next
      end

      symbol_file = get_symbol_file

      @server.send_command(Command::HRCommandTransferFile, nil)

      puts "start transfer file"
      file_tmp = "#{@hot_reload_tmp_path}/hotreload#{@hot_reload_id}"
      @server.send_file(dylib)
      @server.send_file("#{file_tmp}.o")
      @server.send_file(symbol_file)

      puts "finish transfer file"
    end
  end
end
set_project(project_file) click to toggle source
# File lib/hotreload.rb, line 138
def set_project(project_file)
  @server.send_command(Command::HRCommandSetProject, project_file)
  @watcher = FileWatcher.new(@project_path)
  @watcher.startWatcher  do |file|
    changeFiles(file)
  end
end
startServer(projectpath) click to toggle source
# File lib/hotreload.rb, line 33
def startServer(projectpath)
  @project_path = File.expand_path("..", projectpath)
  @server = Server.new
  @server.run

  # create dir
  @hot_reload_tmp_path = "#{@project_path}/HotReload-tmp"
  if !Dir.exist?(@hot_reload_tmp_path)
    Dir.mkdir(@hot_reload_tmp_path)
  end

  # tell client the project being watched
  @project_file = "#{@project_path}/*.xcworkspace"
  files_sorted_by_time = Dir[@project_file].sort!{ |x,y| File.mtime(y) <=> File.mtime(x) }
  @project_file = files_sorted_by_time[0]
  if @project_file == nil
    @project_file = "#{@project_path}/*.xcodeproj"
    files_sorted_by_time = Dir[@project_file].sort!{ |x,y| File.mtime(y) <=> File.mtime(x) }
    @project_file = files_sorted_by_time[0]
  end

  if read_message != Config::HOTRELOAD_KEY
    puts "not the validate client"
    return
  end

  # client spcific data for building
  # frameworkPath = readMessage
  # arch = readMessage

  @last_injected = @project_injected[@project_file]
  if @last_injected == nil
    @last_injected = Hash.new
    @project_injected[@project_file] = @last_injected
  end

  # listen_command

  runTask

  set_project(@project_file)

  @watcher = nil

end