2015年3月26日 星期四

Custom Makefile (python to library (.so))


(屬原創! 轉載請表明出處。)


BASE := /opt/python
SHELL := /bin/bash

PYTHON := ${BASE}/bin/python
CYTHON := ${BASE}/bin/cython
CY_OPTS := -2 -D --lenient
CYBUILD := build_ext --inplace
CC := gcc
C_OPTS := -shared -pthread -fPIC -fwrapv -O2 -Wall -fno-strict-aliasing
INC := -I${BASE}/include/python2.7
LIB := -L${BASE}/lib/python2.7

ifeq ("$(wildcard $(FILE))","")
.SUFFIXS: .c .py .o .so

all: pytolib2

pytolib2: $(FILE).so
    @strip -s $(FILE).so
    @if [ -f $(FILE).so ]; \
    then \
        echo 'compile completed.'; \
    else \
        echo 'compile failure!'; \
    fi;




$(FILE).c: $(FILE).py
    @$(CYTHON) $(CY_OPTS) $(FILE).py

$(FILE).so: $(FILE).c
    @$(CC) $(C_OPTS) $(INC) $(LIB) $(FILE).c -o $@
    @rm -f $<


.PHONY: all clean

clean:
    @rm -f $(FILE).{c,so}

else
err:
    @echo ""
    @echo "Usage: make -f {specify_makefile_path+file} FILE={python_file_name(attached .py is except)}"
    @echo "  Example has an logs.py one want compile to library(.so), and Makefile in current path."
    @echo ""
    @echo "    The good command >>> make -f Makefile FILE=logs"
    @echo "    result >>> compile completed."
    @echo ""
    @echo ""
endif   

=============== 使用範例 ================

Example 1:  (這是一個靜態的使用方式)
    要編譯一個 logs.py,在當前路徑下,有 logs.py、logs.conf、logs.txt。
   ` make -f Makefile FILE=logs `

=============== 動作分析 ================

  1.    首先 [all] tag 依關聯去尋找 [pytolib2] tag。
  2.    [pytolib2] 指示它要有 $(FILE).so 檔案存在。
  3.    $(FILE).so 的存在要求須有 $(FILE).c 為前提。
  4.    $(FILE).c 存在的要求則先具備 $(FILE).py 檔案存在。
  5.    系統判斷存在 $(FILE).py ==> logs.py
              logs.c 的產生則執行 $(CYTHON) $(CY_OPTS) $(FILE).py 此動作。
  6.    有了 logs.c,則開始產生 logs.so 的命令
          
    $(CC) $(C_OPTS) $(INC) $(LIB) $(FILE).c -o $@ 
  7.    完成動作。

Sample 2: (測試 Makefile 部分)
    ...
    %.c: %.py
        @echo $<
        @echo $*
        @echo $@
        @echo $?
    ....
command: ` make logs `
result:
logs.py
logs
logs.c
logs.py

============== 分析 =============

它可以這麼理解:
$@ 取得了 Tag_Name: %.c  (為 logs.c)
$< $? 取得了相依性 %.py  (為 logs.py)
$* 取得當前第一個資訊

接下來,我要將上面的 Makefile 做調整 ...

========== 以下為調整的部分 ==========

...
# 檢閱當前路徑下 所有的 .py 檔
SOURCE = $(wildcard *.py)

# Makefile 有幾個可方便使用的命令,詳情參考
# https://www.gnu.org/software/make/manual/html_node/File-Name-Functions.html

# 其中
# $(notdir $var) 去除路徑結構 ex: '/a/b/c.py' notdir is 'c.py'

# $(suffix $var) 取附檔名稱 ex: 'c.py' suffix is '.py'
# 故以下判斷式:當取得的 FILE 變數是為 (.pyx) 時,則觸發 ...
ifeq ("$(suffix $(notdir $(wildcard $(FILE))))",".pyx")


# $(dir $var) 取得路徑結構 ex: '/a/b/c.py' dir is '/a/b'
FDIR = $(dir $(FILE))
TARGET = $(basename $(notdir $(wildcard $(FILE))))
# 運用 shell script 取得特定內容
SOURCEFILE := $(shell awk '/ext_modules/ { print gensub(/^.*\"(.*)\"\,.*$+/, "\\1", ""); exit }' $(FILE))


# 當 FILE 變數 內容不為空
# Makefile 專用的判斷式,內容的每一行需位於行首。

 ifneq ("$(FILE)","")


# TARGET 則取 FILE 變數指定的名稱 (採用 basename,附檔名將排除)
TARGET = $(basename $(FILE))


# 否則判斷 SOURCE 變數內容不為空 時
else ifneq ("$(SOURCE)","")

# TARGET 將取自 SOURCE 變數找到的名稱列表 (附檔名將排除)
TARGET = $(basename $(SOURCE))


endif


# 預設將執行 TARGET tag
all: $(TARGET)



# 設計功能 pytolib2 指定相依性
pytolib2: $(TARGET)


# 設計功能 pytolib
pytolib:
    @$(PYTHON) $(FILE) $(CYBUILD)
    @echo "SOURCE FILE: "$(SOURCEFILE)
    @if [ -f "$(SOURCEFILE).so" ]; \
    then \
        strip -s $(SOURCEFILE).so; \
        rm -rf build/ $(SOURCEFILE).c; \
        echo "Successfully compile "$(SOURCEFILE)".py to "$(SOURCEFILE)".so"; \
    else \
        echo "Compile "$(SOURCEFILE)".py failure!"; \
    fi;
   


# TARGET 變數可能是一個檔案列表,此時可指定 %
# 來針對它關聯的每一個檔案(%.c) 來進行運作。
# $^ 是顯示先決條件都符合的檔案名稱 (即 ?.c)

$(TARGET): % : %.c
    @$(CC) $(C_OPTS) $(INC) $(LIB) $^ -o $@.so
    @strip -s $@.so
    @if [ -f $@.so ]; \
    then \
        echo "Successfully compile "$^ "to "$@.so; \
    else \
        echo 'compile '$^ 'to '$@.so' failure!'; \
    fi;   

# 此段告訴 make,為每個 .py 的檔案,依底下動作產生對應檔名的 .c 檔。
%.c: %.py
    @$(CYTHON) $(CY_OPTS) $^
    @echo "Successfully compile "$< "to "$@


# 宣告此 clean 不是存在實體的環境中,為虛擬的。
.PHONY: pytolib clean help




help:
    @echo ""
    @echo "Usage: make -f {specify_makefile_path+file} {Functions} {FILE={python_file_name(attached .py is except)}}"
    @echo "  Functions: "
    @echo "         pytolib: ........ Used by cython build_ext (first be create the (.pyx) File)"
    @echo "         pytolib2: ....... This is an normal compile"
    @echo ""
    @echo "  Example has an logs.py one want compile to library(.so), and Makefile in current path."
    @echo ""
    @echo "    specify compile once file >>> make -f Makefile FILE=logs"
    @echo "    result >>> compile completed."
    @echo ""
    @echo "    compile to all >>> make -f Makefile"
    @echo "    [Notice] default is used by this pytolib2 function."
    @echo ""
    @echo "    compile function use 'pytolib' >>> make -f Makefile pytolib FILE=setup/log.pyx"
    @echo "    [Notice] Use the 'pytolib' function, must specify (.pyx) path and File."
    @echo "    [Notice] 'pytolib' function cannot compile to all!"
    @echo ""
    @echo ""
    @echo "                                    --- Author: Baron. Wan ---"
    @echo ""

# 當我們執行刪除動作時,我們可以運用 makefile 的特性
# wildcard 命令來確認當前環境下,應該刪除的檔案是否尚存在 ?

clean:
    @rm -f $(wildcard *.c *.so)  


# Makefile 內部命令都會先被預執行,所以判斷上無法即時判斷檔案是否已刪除成功,
# 此時可以採取如下的做法,當再次執行 `make msg` 時,即能正確判斷結果。

ifeq ("$(wildcard *.c *.so)","")
MSG := "Remove completed"
else
MSG := "Cannot remove: "$(wildcard *.c *.so)
endif

msg:
    @echo $(MSG)
 

=========== 執行 ===========

cmd: ` make `
會進行當前路徑下,所有 .py 的編譯動作。

cmd: ` make FILE=logs `
則只對於 logs.py 進行編譯。