以out-of-project方式替换cmake式构建工程中个别源文件

发布时间 2023-06-23 07:42:45作者: zwlwf

引言

现假设你在走读某个以cmake方式构建的大工程,如llvm中clang。突然看到某段代码时,突然脑中冒出一个小idea:这里若不用A,而是用B会如何。你会怎样去测试这个小想法呢?

  1. 在当前代码库中直接修改,构建,运行新生成可执行程序
  2. 使用git的分支管理,先新建一个分支切换过程,再使用方式 1
  3. 将整个项目拷贝到新的地方,在新的地方修改代码、构建、运行
    方式1直接污染了原始代码,且测试代码经常为临时性修改,版本管理混乱。
    方式2一定程度上避免了方式1的版本管理混乱问题,但分支间切换可能会造成项目的build重新重新进行,大工程构建很耗时,一通操作+等待之后,可能都忘了为神马要做这个修改。
    方式3,比方式2更耗时,更繁琐,且很占用磁盘。

怎么办?我就是想简简单单做的小测试啊!!!小idea组成和触发大idea,小idea实现受挫,直接扼杀大idea。所以小idea实现的便利性、时间和空间成本是完成大idea至关重要的因素。

设计

这里提出一种out-of-project的解决方式。

  1. 新建一个empty目录,然后cd to it,让我们从0开始。开始前明确我们要修改的源码名src_name、和最终源码会编入的target(exe/so文件),
  2. 原始cmake构建时输出compile_commands.json。可通过CMakeLists.txt中加set(CMAKE_EXPORT_COMPILE_COMMANDS on), 或者cmake命令调用时命令参数中加-DCMAKE_EXPORT_COMPILE_COMMANDS=on
  3. src_name源文件从大项目中拷贝到PWD,按照你的小idea进行随意修改
  4. compile_commands.json中选出我们想修改的c/cpp的源文件编译命令,修改其中输入文件为PWD/src_name, 输出的object文件也修改为PWD的源文件名src_name对应的对象文件名object_name,这里取object_name=src_name+'.o'
  5. 从项目的目录中搜索target对应的link.txt文件,文件中指定了该target的链接命令。修改其中链接命令中用到对象文件名为PWD的object_name,输出为PWD的target,保持链接过程中用到的其他输入不变。这样就做到了仅仅替换target链接用到的指定src_name文件,完成偷梁换柱的目的。若src_name首先被编入某个静态库,然后被链接到target,则直接在链接命令的前面加上PWD的object_name,静态库中的src_name中所有实现链接时将不会被使用。

上述out-of-project的解决方案,带来的构建耗时、磁盘占用基本都做到了最小,且不会污染源代码。

实现

这里写个简单的python脚本,i_want_to_replace.py, 使用方法

python i_want_to_replace.py src_name.cpp target_name /path/to/project/build

结果生成一个makefile模板,Makefile.in。同时打印中会提醒拷贝src_name的操作。

#i_want_to_replace.py
import sys
import os 
import json 

def usage():
  print('%s src_name target build_path' % sys.argv[0])

def find_cdb_in_build_path(build_path):
  ans = os.path.join(build_path, 'compile_commands.json')
  if os.path.exists(ans):
    return ans
  print('compile_commands not found!')
  return None

def find_command_in_compile_db(cbd_name, src_name):
  cbd_data = json.load(open(cbd_name))
  for entry in cbd_data:
    if src_name in entry['file']:
      print('you can use the old code as a model by type:')
      print('cp ' + entry['file'] + ' .')
      return entry['command']

def replace_compile_cmd_with_pwd_src(cmd, src_name):
  cmd_list = cmd.split()
  idx = 0
  out_obj = src_name+'.o'
  new_compile_cmd = []
  found_output_obj = False
  while idx < len(cmd_list):
    if cmd_list[idx] == '-o': #found output
      new_compile_cmd.append('-o')
      new_compile_cmd.append(out_obj)
      found_output_obj = True
      idx += 2
      continue
    if cmd_list[idx].endswith(src_name): #found input
      new_compile_cmd.append(src_name)
      idx += 1
      continue
    new_compile_cmd.append(cmd_list[idx])
    idx += 1
  if not found_output_obj:
    new_compile_cmd.append('-o')
    new_compile_cmd.append(out_obj)
  return ' '.join(new_compile_cmd)

def find_all_link_txt_file(path, ans):
  if os.path.isdir(path):
    for sub_file in os.listdir(path):
      find_all_link_txt_file(os.path.join(path, sub_file), ans)
  else:
    if path.endswith('link.txt'):
      ans.append(path)

def find_link_cmd_of_target(build_path, target_name):
  link_txts = []
  find_all_link_txt_file(build_path, link_txts)
  ans = ['cd']
  for link_file in link_txts:
    if target_name in link_file:
      ans.append('/'.join(link_file.split('/')[:-3]))
      ans.append(' && ' +  open(link_file).read())
  return ' '.join(ans)

def replace_link_cmd_with_pwd_object(link_cmd, obj_name):
  cmd_list = link_cmd.split()
  idx = 0
  ans = []
  found_direct_object_input = False
  while idx < len(cmd_list):
    if cmd_list[idx].endswith(obj_name):
      ans.append('${PWD}/'+obj_name)
      found_direct_object_input = True
      idx += 1
    elif cmd_list[idx] == '-o':
      ans.append('-o')
      ans.append('${PWD}/app')
      idx += 2
    else:
      ans.append(cmd_list[idx])
      idx += 1
  if not found_direct_object_input:
      ans.append('${PWD}/'+obj_name)
  return ' '.join(ans)

def main():
  if len(sys.argv) < 4:
    usage()
    return
  src_name = sys.argv[1]
  target_name = sys.argv[2]
  build_path = sys.argv[3]
  cbd_name = find_cdb_in_build_path(build_path)
  if not cbd_name:
    return
  cmd = find_command_in_compile_db(cbd_name, src_name)
  new_compile_cmd = replace_compile_cmd_with_pwd_src(cmd, src_name)
  link_cmd = find_link_cmd_of_target(build_path, target_name)
  new_link_cmd = replace_link_cmd_with_pwd_object(link_cmd, src_name+'.o')
  with open('Makefile.in','w') as f:
    f.write('app:\n')
    f.write('\t'+new_compile_cmd+'\n')
    f.write('\t'+new_link_cmd+'\n')

if __name__ == '__main__':
  main()
  

One more word

很享受从0开始的搞事,如在一个空旷的练武场上随意挥洒拳脚,如在一张白纸随意书写,如一颗糖在纯净水中迅速化开。
反之,像糖在高渗透压水化不开,在山一样的工程代码中修改和测试,手和脑都像被定住了一样,久久无法动弹。