注册 登录  
 加关注
查看详情
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

为着理想勇敢前进

 
 
 

日志

 
 

用 lua 代替 GNU Make  

2009-11-15 22:28:49|  分类: 默认分类 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |
我们团队一直在用 GNU Make ,我们老大云风尤其把 make 用得十分高级。但使用过程中,碰到一些问题确实比较讨厌:
  1. 斜杠的处理
  2. 参见 http://blog.codingnow.com/2009/03/gnu_make_backslash.html
  3. 递归目录
  4. 参见 http://blog.codingnow.com/2009/06/make_recursion_directory.html,这个用到了神技(eval)
  5. 自动创建目录
  6. 参见 http://blog.codingnow.com/2009/07/gnu_make_mkdir.html,这个也用到了神技。
  7. 包含空格的文件名的处理
  8. 据高人说,可以在调用 foreach 等内置函数以前先把空格替换成别的字符,再在作为文件名使用时把空格替换为 。但这个做法等于是自己定义一种转义规则了。
  9. shell的依赖性
  10. make 的命令行都是调用操作系统的 shell ,这样会带来移植性问题。就连基本的文件操作都需要定义很多环境变量来处理 cp / copy cat / type 之类的差异。不同操作系统的命令行转义规则也各不相同,如果命令行参数中出现特殊字符,要想在不同的 shell 下都正确转义难度极大。
  11. 算术运算
  12. 比如我想要从 1 循环到 10,声明 frame_1.png 依赖于 frame_1.tga,frame_2.png 依赖于 frame_2.tga,frame_3.png 依赖于 frame_3.tga …… frame_10.png 依赖于 frame_10.tga。这样的事情在 C 里面就是 for (i = 0; i < 10; ++i) 即可。在 make 中, for 倒是可以用递归来模拟,但 make 里面除非求助于 shell 就没有办法能做 ++i 这个事情。
  13. 子模块依赖关系
  14. 这个是 make 的死穴。如果一个项目中有多个模块,模块 a 依赖于模块 b 的输出文件 libb.a,那应该怎么写依赖关系呢?一般有两种做法
    • 不同的模块使用独立的 Makefile ,a 模块依赖于 libb.a,如果 libb.a 不存在,就调用 b 模块的 make 来创建 libb.a,大概代码如下:
      ../b/libb.a:
      cd ../b && $(MAKE)

      a.exe: ../b/libb.a xxx.c yyy.c xxx.h
      gcc ……
    • 这样做的问题在于如果模块 b 中的某个源文件修改了,在 a 模块下 make 时, libb.a 不能自动重新编译,因而 a 模块也不会重新编译
    • 用 include 之类的办法把所有模块的 Makefile 统一起来,这是目前我们做的方式。缺点在于:
      • 编写子模块的 Makefile 时需要知道别的模块的信息,破坏了 Makefile 的局部性
      • 为了解决这个问题,云风想的办法是自己定义了一套语法,在子模块 make 时,通通调用根一级的 Makefile ,只是把子模块的信息通过环境变量传进去。但这样带来了另一个缺点,即自定义的语法有学习成本,和直接的 make 语法相比,维护起来要稍微困难一些。
      • 即使只构建一个子模块,也需要生成整个项目的依赖图
      • 尤其当文件很多的时候,生成整个依赖图会比较慢。这个问题在 bjam 中也存在。因为我们现在的做法就相当于用 make 实现了一套 bjam ,所以我们也会面临 bjam 的一些问题。

    当一个项目构建过程比较复杂的时候,可以考虑用通用语言来管理构建过程。对于通用语言来说,处理字符串转义、算术运算、循环遍历等事情易如反掌,无需使用 make 那些神奇而又晦涩的神技。而跨平台的通用语言的文件系统库往往也比 shell 有更好的移植性。至于子模块依赖问题,对于支持 closure 、 coroutine 的语言来说,很容易就能实现依赖关系的惰性计算,只需要生成整个依赖图中于构建目标相关的那部分,速度就快多了。

    我在做的一个项目的构建过程就十分复杂,需要把一些配置文件从一种格式转成另一种格式,然后再嵌入主程序中;用到的美术素材也需要转格式。这些过程需要用到好几种转换工具,有一些转换工具就直接要用 lua 编写。我原先是用 GNU Make 来做这件事情的,把各种神奇的特性都用上了,最后碰到空格文件名的时候,还是放弃了,决心自己用 lua 实现一套构建系统,我把它叫做 lua-make

    因为编译器、转换工具都是命令行工具,所以这个构建系统碰到的第一个问题是要能启动进程。lua 标准库中启动进程有两个函数,io.popen 和 os.execute 。其中 io.popen 不支持 Windows。而 os.execute 要依赖于操作系统的 shell ,移植性不佳比如,在 bash 中,我们可以写这样的代码:

    cat 111.txt | a/b/foo 'xxxxx' | bar 'yyyyy' 'aaa' 2> out/err > out/log

    如果要用 lua 来启动,就是

    os.execute[[cat 111.txt | a/b/foo 'xxxxx' | bar 'yyyyy' 'aaa' 2> out/err > out/log]]

    可是上述代码在 Windows 中就跑不通了。这是因为 Windows 的 shell 是 cmd ,语法不同,字符串转义规则不同,目录分隔符也不同。

    归根到底, os.execute 是不可移植的。所以我用了 Lua-Ex 。Lua-Ex 提供了创建管道的函数(ex.pipe),启动进程并重定向标准输入输出的函数(ex.spawn),刚好适合做构建系统。不过,pipe 和 spawn 是比较底层的函数,要想简单的完成一串管道的命令,还需要一些语法糖。

    我做了一个 path 模块和一个 shell 模块来做实现这个语法糖。先前的 bash 表达式,用我提供的语法糖来写的话就是这样:

    "111.txt" / path["a/b/foo"]("xxxxx") / path["bar"]('yyyyy', 'aaa') % path["out/err"] / path["out/log"]

    上述表达式中,运算符/用来重定向标准输出,相当于 bash 中的|<>;运算符%用来重定向标准错误,相当于 bash 中的2>

    path["out/log"] 是一个路径对象。我重载了路径对象metatable中的 __div ,所以也可写成 path["out"] / path["log"] 或者 path.out / path.log 又或者 path.out / "log" 。如果用 setfenv 把当前执行环境设成 path ,甚至可以直接写成 out/log 。

    路径的比较也十分简单,我能保证同一个路径在内存中只有一份,所以 path["x/b"] 、 path["x"] / b 、 path["x/a/../b"] 、 path["x\\b"] 都是同一个对象。

    我也重载了路径对象metatable中的 __call ,当它被调用的时候就会被当成可执行文件来执行。这就是为什么上述代码可以直接写path["bar"]('yyyyy', 'aaa') 的缘故。

    对于一个构建系统来说,核心部分是目标的依赖关系管理。这方面,我主要是造搬 GNU Make 的模型,只是语法用 lua 的表来描述。

    这里我碰到一个问题,就是多任务的并行执行。make 有一个 -j 选项,可以并行执行多个任务。用 lua 怎么实现类似的功能呢?用 ex.spawn 可以创建进程,用 ex.wait 可以等待一个进程退出,但是如果同时启动了多个进程,怎样才能等待其中任意一个退出呢?Windows 中有一个 WaitForMultiObjects 可以做这件事情,但是一方面这不可移植,另一方面我也不想为了这一个功能而多写一个 lua C库。我用的办法是在主进程调用 ex.pipe 创建一个 pipe,为需要监视的进程创建一个辅助进程来监视,辅助进程等待被监视的进程退出,退出时向 pipe 发一个消息。所以主进程之需要读这个 pipe 就可以知道任意一个进程退出了。我把这个监视模块叫做 shell.watchdog 。

    最终,要使用这个构建系统,大概是这样来写Makefile:

    require "shell"
    require "path"
    require "shell.watchdog"
    require "make"
    require "make.filetarget"

    local watchdog = shell.watchdog()

    make(10, make.filetarget {
    path = "xxxx.exe",
    dependencies = {
    {
    run = function ()
    print("这是一个伪目标")
    end
    },
    make.filetarget {
    path = "xxxx.c"
    },
    make.filetarget {
    path = "xxxx.h"
    }
    }
    run = watchdog + path.gcc("-o", "xxxx.exe", "xxxx.c")
    })

    watchdog:wait()

    make 函数的第一个参数指最多允许多少个并行任务的意思,这里传 10,就相当于GNU Make 的 -j 10

    make.filetarget 是说这是一个文件目标,而不是一个伪目标(即 GNU Make 的 .PHONY)。文件目标和伪目标相比,要增加判断文件修改时间、自动创建父级目录等功能。在我实现的构建系统中,一个目标默认是没有这些功能的,如果需要这些功能就得写 make.filetarget {......}

    这里的watchdog +是一个语法糖,意思是要把接下来的这一段 shell 表达式加入到 watchdog 这个监视对象中去监视。

    大体上我就是这样实现 Lua 构建系统的,欢迎回帖讨论

      评论这张
     
    阅读(1549)| 评论(6)
    推荐 转载

    历史上的今天

    评论

    <#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
     
     
     
     
     
     
     
     
     
     
     
     
     
     

    页脚

    网易公司版权所有 ©1997-2018