Makefile技巧三则

Three Tricks to use Makefile Efficiently

最近读Protocol Buffer for C,发现一些Makefile的技巧

1. -MMD flag
Makefile比较麻烦的是写dependency。常用技巧是通过g++ -MM和sed脚本从.cpp源文件生成.d文件,这种方式可以解决自动生成依赖关系的问题,但其实有更好的方法:
g++ -MMD -c XXX.c
之后在Makefile里添加:
-include XXX.d

g++的文档提到-MMD:

-MMD
    Like -MD except mention only user header files, not system header files. 

除了-MMD外,有意思的g++参数,可以参见gcc flags 。另外这篇详细讲解Makefile dependency的文章Makefile Autodependency

2. Order-only prerequisites
order-only-prerequisites
用Makefile里的例子来说:

     OBJDIR := objdir
     OBJS := $(addprefix $(OBJDIR)/,foo.o bar.o baz.o)
     
     $(OBJDIR)/%.o : %.c
             $(COMPILE.c) $(OUTPUT_OPTION) $<
     
     all: $(OBJS)
     
     $(OBJS): | $(OBJDIR)
     
     $(OBJDIR):
             mkdir $(OBJDIR)

$(OBJS)需要$(OBJDIR),所以需要把$(OBJS)放在$(OBJDIR)前面;但在建立$(OBJDIR)之后,$(OBJ)就不在需要根据$(OBJDIR)的时间调整了。

3. Multiple line variables (Eval function)
Makefile里的变量可以有多行,这种变量要用define来定义。
使用这种变量有两种场合:(1)Canned Recipes; (2)Eval-Function

第一种就是把常用的一组命令包装在一起,比如:

     define run-yacc =
     yacc $(firstword $^)
     mv y.tab.c $@
     endef

之后调用:

     foo.c : foo.y
             $(run-yacc)

第二种更加常用,比如下面的例子:

     PROGRAMS    = server client
     
     server_OBJS = server.o server_priv.o server_access.o
     server_LIBS = priv protocol
     
     client_OBJS = client.o client_api.o client_mem.o
     client_LIBS = protocol
     
     # Everything after this is generic
     
     .PHONY: all
     all: $(PROGRAMS)
     
     define PROGRAM_template =
      $(1): $$($(1)_OBJS) $$($(1)_LIBS:%=-l%)
      ALL_OBJS   += $$($(1)_OBJS)
     endef
     
     $(foreach prog,$(PROGRAMS),$(eval $(call PROGRAM_template,$(prog))))
     
     $(PROGRAMS):
             $(LINK.o) $^ $(LDLIBS) -o $@
     
     clean:
             rm -f $(ALL_OBJS) $(PROGRAMS)

首先上面注释行以下都是通用(generic)的。
其次eval会两次展开(expand)变量名,所以$(1)会被扩展成prog(比如server),$$($(1)_OBJS)会被扩展成$(server_OBJS),之后会再次被扩展成server.o server_priv.o server_access.o

最精彩的地方是如何把上面3个技巧联合起来。下面看一下cloudwu的Protocol Buffer for C项目的Makefile片段:

BUILD = build

LIBSRCS = context.c varint.c array.c pattern.c register.c proto.c map.c alloc.c rmessage.c wmessage.c bootstrap.c stringpool.c
LIBNAME = libpbc.$(SO)

TESTSRCS = addressbook.c pattern.c
PROTOSRCS = addressbook.proto descriptor.proto

BUILD_O = $(BUILD)/o

all : lib test

lib : $(LIBNAME)

clean :
	rm -rf $(BUILD)

$(BUILD) : $(BUILD_O)

$(BUILD_O) :
	mkdir -p $@

LIB_O :=

define BUILD_temp
  TAR :=  $(BUILD_O)/$(notdir $(basename $(1)))
  LIB_O := $(LIB_O) $$(TAR).o
  $$(TAR).o : | $(BUILD_O)
  -include $$(TAR).d
  $$(TAR).o : src/$(1)
	$(CC) $(CFLAGS) -c -Isrc -I. -fPIC -o $$@ -MMD $$<
endef

$(foreach s,$(LIBSRCS),$(eval $(call BUILD_temp,$(s))))

$(LIBNAME) : $(LIB_O)
	cd $(BUILD) && $(CC) --shared -o $(LIBNAME) $(addprefix ../,$^)

有意思的地方是BUILD_temp,将每一个$(1)的值赋给TAR,之后$(TAR)在$(BUILD_O)目录下编译,编译时的依赖关系被-include到Makefile内部(include前的减号,-,表示要include的文件即使不存在也不报错),同时把编译好的$(TAR).o加入到LIB_O变量中(+=赋值)。

使用Varnish绕过学校防火墙

Use varnish to bypass school proxy (rewrite URL to bypass port blocking).

学校的防火墙规则很严格,除了80,443等常用端口外,不允许特殊端口的通信(就连git使用的9418端口也被block过)。
可以我的网站2812端口有monit服务,我常常想从学校的电脑访问 ,看看自己网站的是不是工作正常。为了解决这个问题,我发现可以用Varnish的rewrite 功能。

原理上讲,Varnish收到一个URL request,会调用vcl_recv函数 (可能是callback机制),然后根据header中的URL来分析如何具体处理,比如可以去掉header,对mobile的browser agent用mobile版的web server,还可以用不同的backend服务器,这个就是用这个功能。

先把相应的配置放上来:
default.vcl中先添加一个后台服务器:

backend monit {
.host = "localhost";
.port = "2812";
}

在sub vcl_recv函数中增加对/monit请求的重写(rewrite),就是说每次访问xxx.com/monit自动翻译成xxx.com/, 然后用backend monit(就是上面的localhost:2812服务器)来服务:


if (req.url ~ "^/monit"){
set req.url = regsub(req.url, "^/monit", "/");
set req.url = regsub(req.url, "^//", "/");
set req.backend = monit;
}

通过上面的设置,就可以在学校里,用80端口来访问服务器端2812端口的内容了。
注意,默认monit会使用这样的链接: xxx.com/nginx
这种情况下,varnish不会用backend monit来服务,我们需要手动访问:xxx.com/monit/nginx
现在还没有想到如何解决这种不方便。

后话1:

Varnish是一个非常强大的proxy,用它内置的VCL语言,可以很灵活、高效(Varnish内部把VCL编译成C语言)处理各种类型的HTTP request。比如round-robin式的负载均衡。最常用的两个函数是vcl_recv (表示varnish接收到browser的请求) 和vcl_fetch(表示varnish已经从后台服务器取得数据)。更多的用法可以见:

后话2:

Varnish不会像Apache/Nginx把log写到磁盘。如需要检查log,可以用Varnish自带的varnishlog。
比如下面的:

   12 SessionOpen  c 76.235.186.40 34862 :80
   12 ReqStart     c 76.235.186.40 34862 911966818
   12 RxRequest    c GET
   12 RxURL        c /monit
   12 RxProtocol   c HTTP/1.1
   12 RxHeader     c Host: zhanxw.com
   12 RxHeader     c User-Agent: Mozilla/5.0 (Ubuntu; X11; Linux x86_64; rv:8.0) Gecko/20100101 Firefox/8.0
   12 RxHeader     c Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
   12 RxHeader     c Accept-Language: en-us,en;q=0.5
   12 RxHeader     c Accept-Encoding: gzip, deflate
   12 RxHeader     c Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
   12 RxHeader     c Connection: keep-alive
   12 RxHeader     c Cookie: __utma=134065221.936534807.1323323714.1323329816.1323533189.3; __utmz=134065221.1323323714.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); __utmc=134065221
   12 RxHeader     c Authorization: Basic YWRtaW46eGlhb3dlaXhpYW95aQ==
   12 RxHeader     c Cache-Control: max-age=0
   12 VCL_call     c recv
   12 VCL_return   c pass
   12 VCL_call     c hash
   12 VCL_return   c hash
   12 VCL_call     c pass
   12 VCL_return   c pass
   14 BackendOpen  b monit 127.0.0.1 37258 127.0.0.1 2812
   12 Backend      c 14 monit monit
   14 TxRequest    b GET
   14 TxURL        b 
   14 TxProtocol   b HTTP/1.1
   14 TxHeader     b Host: zhanxw.com
   14 TxHeader     b User-Agent: Mozilla/5.0 (Ubuntu; X11; Linux x86_64; rv:8.0) Gecko/20100101 Firefox/8.0
   14 TxHeader     b Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
   14 TxHeader     b Accept-Language: en-us,en;q=0.5
   14 TxHeader     b Accept-Encoding: gzip, deflate
   14 TxHeader     b Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
   14 TxHeader     b Cookie: __utma=134065221.936534807.1323323714.1323329816.1323533189.3; __utmz=134065221.1323323714.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); __utmc=134065221
   14 TxHeader     b Authorization: Basic YWRtaW46eGlhb3dlaXhpYW95aQ==
   14 TxHeader     b X-Forwarded-For: 76.235.186.40
   14 TxHeader     b X-Varnish: 911966818
   14 RxProtocol   b HTTP/1.0
   14 RxStatus     b 400
   14 RxResponse   b Bad Request
   14 RxHeader     b Date: Sat, 10 Dec 2011 17:58:50 GMT
   14 RxHeader     b Server: monit 5.2.1
   14 RxHeader     b Content-Type: text/html
   14 RxHeader     b Connection: close
   12 TTL          c 911966818 RFC 120 1323539930 0 0 0 0
   12 VCL_call     c fetch
   12 VCL_return   c pass
   12 ObjProtocol  c HTTP/1.1
   12 ObjStatus    c 400
   12 ObjResponse  c Bad Request
   12 ObjHeader    c Date: Sat, 10 Dec 2011 17:58:50 GMT
   12 ObjHeader    c Server: monit 5.2.1
   12 ObjHeader    c Content-Type: text/html
   14 Length       b 201
   14 BackendClose b monit
   12 VCL_call     c deliver
   12 VCL_return   c deliver
   12 TxProtocol   c HTTP/1.1
   12 TxStatus     c 400
   12 TxResponse   c Bad Request
   12 TxHeader     c Server: monit 5.2.1
   12 TxHeader     c Content-Type: text/html
   12 TxHeader     c Content-Length: 201
   12 TxHeader     c Date: Sat, 10 Dec 2011 17:58:50 GMT
   12 TxHeader     c X-Varnish: 911966818
   12 TxHeader     c Age: 0
   12 TxHeader     c Via: 1.1 varnish
   12 TxHeader     c Connection: keep-alive
   12 Length       c 201
   12 ReqEnd       c 911966818 1323539930.287971973 1323539930.288527966 0.000089884 0.000468969 0.000087023
   12 Debug        c "herding"

其中最重要的是第3列: b 表示 “backend”,就是varnish和backend端的通信; c 表示 “client”,就是varnish和client端(www browser)之间的通信。

第2列中, VCL_call 表示varnish内部哪个函数被调用,VCL_return则是调用结果是什么。比如:VCL_call retch ,表示VCL配置文件中vcl_fetch函数被调用; VCL_return pass,表示return pass,同样也是VCL配置文件中的语句。