android編譯系統分析(一)source build/envsetup.sh與lunch
雖然已經有很多人分析過Android的編譯系統的程式碼了,我也看過他們的部落格,也學到了不少知識,但單純的看別人分析,終究還是理解的不深入,所以,我還是要自己再認真的分析一遍。
想想我們編譯android系統的過程:
首先:source build/envsetup.sh
其次:lunch ---選擇一個特定的型別
最後:make
按著這個順序,追蹤這看似簡單的幾步,到底有哪些背後的祕密?
1. source build/envsetup.sh
這個檔案雖然很大,但暫且不需要統統看一遍。它裡面定義了很多函式,這些函式在使用的時候再具體詳細學習,現在主要看看這個指令碼做的事情。即便如此,開啟這個指令碼後第一個函式還是非常吸引人的,因為它裡面介紹了這個指令碼主要要做的事情:
- function hmm() {
- cat <<EOF
- Invoke ". build/envsetup.sh" from your shell to add the following functions to your environment:
- - lunch: lunch <product_name>-<build_variant>
- - tapas: tapas [<App1><App2> ...] [arm|x86|mips|armv5|arm64|x86_64|mips64] [eng|userdebug|user]
- - croot: Changes directory to the top of the tree.
- - m: Makes from the top of the tree.
- - mm: Builds all of the modules in the current directory, but not their dependencies.
- - mmm: Builds all of the modules in the supplied directories, but not their dependencies.
- To limit the modules being built use the syntax: mmm dir/:target1,target2.
- - mma: Builds all of the modules in the current directory, and their dependencies.
- - mmma: Builds all of the modules in the supplied directories, and their dependencies.
- - cgrep: Greps on all local C/C++ files.
- - ggrep: Greps on all local Gradle files.
- - jgrep: Greps on all local Java files.
- - resgrep: Greps on all local res/*.xml files.
- - mangrep: Greps on all local AndroidManifest.xml files.
- - sepgrep: Greps on all local sepolicy files.
- - sgrep: Greps on all local source files.
- - godir: Go to the directory containing a file.
- Environemnt options:
- - SANITIZE_HOST: Set to 'true' to use ASAN for all host modules. Note that
- ASAN_OPTIONS=detect_leaks=0 will be set by default until the
- build is leak-check clean.
- Look at the source to view more functions. The complete list is:
- EOF
- T=$(gettop)
- local A
- A=""
- for i in `cat $T/build/envsetup.sh | sed -n "/^[ \t]*function /s/function [a−z]∗.*/\1/p" | sort | uniq`; do
- A="$A $i"
- done
- echo $A
- }
函式只有在呼叫到它的時候才會執行,所以暫時統統不看,現在只看函式外面的內容:
[plain] view plain copy print?- # Clear this variable. It will be built up again when the vendorsetup.sh
- # files are included at the end of this file.
- unset LUNCH_MENU_CHOICES
- # add the default one here
- add_lunch_combo aosp_arm-eng
- add_lunch_combo aosp_arm64-eng
- add_lunch_combo aosp_mips-eng
- add_lunch_combo aosp_mips64-eng
- add_lunch_combo aosp_x86-eng
- add_lunch_combo aosp_x86_64-eng
而這個函式,就是就是重新構造LUNCH_MENU_CHOICES變數:
[html] view plain copy print?- function add_lunch_combo()
- {
- local new_combo=$1
- local c
- for c in ${LUNCH_MENU_CHOICES[@]} ; do
- if [ "$new_combo" = "$c" ] ; then
- return
- fi
- done
- LUNCH_MENU_CHOICES=(${LUNCH_MENU_CHOICES[@]} $new_combo)
- }
瞭解過shell程式設計就知道,$1表示傳入函式的第一個引數,然後又定義了一個變數c。
從for迴圈中可以看出,LUNCH_MENU_CHOICES是一個數組,在shell中,可以通過 "變數名[@]"或者“變數名[*]”的方式過得陣列的所有項。然後注意比較陣列中的每一項,如果陣列中已經有傳入的引數項,就繼續返回,否則,就把新的傳入的引數加入到LUNCH_MENU_CHOIES陣列中。
這個指令碼雖然很長,但是正真執行的程式碼沒有多少,就是說當執行source build/envsetup.sh的時候執行的程式碼沒有多少,它裡面大多數內容都是函式的定義。在該檔案的最後,又執行了一點程式碼:
[plain] view plain copy print?- if [ "x$SHELL" != "x/bin/bash" ]; then
- case `ps -o command -p $$` in
- *bash*)
- ;;
- *)
- echo "WARNING: Only bash is supported, use of other shell would lead to erroneous results"
- ;;
- esac
- fi
- # Execute the contents of any vendorsetup.sh files we can find.
- for f in `test -d device && find -L device -maxdepth 4 -name 'vendorsetup.sh' 2> /dev/null | sort` \
- `test -d vendor && find -L vendor -maxdepth 4 -name 'vendorsetup.sh' 2> /dev/null | sort`
- do
- echo "including $f"
- . $f
- done
- unset f
前面的if語句塊就是檢查支援的shell解析器。後面的for語句塊比較關鍵。
這裡使用了shell的反引號的用法,它的作用就是把反引號的內容當做shell命令來執行。然後把執行結果使用echo輸出到螢幕。反引號中,首先使用test命令測試 device目錄是否存在,存在的話就查詢vendorsetup.sh指令碼,並且設定了查詢深度為4層目錄。2> /dev/null 是shell中的重定向語法,這裡把標準錯誤重定向到/dev/null中,也就是,把錯誤統統刪掉,左後把找到的vendorsetup.sh指令碼使用sort命令進行排序。
. $f相當於source $f,也就是source /device和/vendor下找到的所有vendorsetup.sh指令碼。然後,這個指令碼就分析結束了,接下來,自然是要去分析/device和/vendor下的vendorsetup.sh指令碼了。
2./device/vendor下的vendorsetup.sh
執行一下source build/envsetup.sh,會列印一下內容:
[html] view plain copy print?- including device/generic/mini-emulator-arm64/vendorsetup.sh
- including device/generic/mini-emulator-armv7-a-neon/vendorsetup.sh
- including device/generic/mini-emulator-mips/vendorsetup.sh
- including device/generic/mini-emulator-x86_64/vendorsetup.sh
- including device/generic/mini-emulator-x86/vendorsetup.sh
- add_lunch_combo mini_emulator_arm64-userdebug
因此,總結來說,envsetup.sh指令碼做了這樣的事情:
3.lunch
編譯android的時候,執行完souce build/envsetup.sh,我們還需要執行lunch,選擇一個特定的單板。
[html] view plain copy print?- function lunch()
- {
- local answer
- if [ "$1" ] ; then
- answer=$1
- else
- print_lunch_menu
- echo -n "Which would you like? [aosp_arm-eng] "
- read answer
- fi
- local selection=
- if [ -z "$answer" ]
- then
- selection=aosp_arm-eng
- elif (echo -n $answer | grep -q -e "^[0-9][0-9]*$")
- then
- if [ $answer -le ${#LUNCH_MENU_CHOICES[@]} ]
- then
- selection=${LUNCH_MENU_CHOICES[$(($answer-1))]}
- fi
- elif (echo -n $answer | grep -q -e "^[^\-][^\-]*-[^\-][^\-]*$")
- then
- selection=$answer
- fi
- if [ -z "$selection" ]
- then
- echo
- echo "Invalid lunch combo: $answer"
- return 1
- fi
- export TARGET_BUILD_APPS=
- localproduct=$(echo -n $selection | sed -e "s/-.*$//")
- check_product $product
- if [ $? -ne 0 ]
- then
- echo
- echo "** Don't have a product spec for: '$product'"
- echo "** Do you have the right repo manifest?"
- product=
- fi
- local variant=$(echo -n $selection | sed -e "s/^[^\-]*-//")
- check_variant $variant
- if [ $? -ne 0 ]
- then
- echo
- echo "** Invalid variant: '$variant'"
- echo "** Must be one of ${VARIANT_CHOICES[@]}"
- variant=
- fi
- if [ -z "$product" -o -z "$variant" ]
- then
- echo
- return 1
- fi
- export TARGET_PRODUCT=$product
- export TARGET_BUILD_VARIANT=$variant
- export TARGET_BUILD_TYPE=release
- echo
- set_stuff_for_environment
- printconfig
- }
print_lunch_menu函式如下:
[html] view plain copy print?- function print_lunch_menu()
- {
- local uname=$(uname)
- echo
- echo "You're building on" $uname
- echo
- echo "Lunch menu... pick a combo:"
- local i=1
- local choice
- for choice in ${LUNCH_MENU_CHOICES[@]}
- do
- echo " $i. $choice"
- i=$(($i+1))
- done
- echo
- }
1.不管執行lunch的時候有沒有傳入引數,最終都會將選擇的結果存入到answer變數中,那麼接下來,當然是檢查輸入的引數的合法性了。這裡首先判斷answer變數是不是為0,如果為零的話,selection變數就會賦值為aosp_arm-eng,但是如果不為零的話,首先會輸出answer的值,使用echo -n $answer,-n選項是的輸出的時候不輸出換行符,answer變數的值並沒有輸出到螢幕上,而是通過管道傳給了後面一條命令:grep -q -e "^[0-9][0-9]*$",這條命令從answer變數中搜尋以兩位數字開頭的字串,如果找到,就認為是輸入的是數字。然後進一步對這個數字做有沒有越界的檢查。如果這個數字小於LUNCH_MENU_CHOICES的大小,就會把LUNCH_MENU_CHOICES的地$answer-1項複製給selection變數。
2.如果傳入的不是數字,就會嘗試匹配這樣的字串grep -q -e "^[^\-][^\-]*-[^\-][^\-]*$",-q標示quiet模式,也就是不列印資訊,-e表示後面跟的是一個用於匹配的模式,這裡,^標示匹配開始,[]中的^則標示排除,\是轉譯字元,因為-可能是表示範圍的符號。所以,這裡匹配的是不以連續兩個--開始,後面跟任意字元,然後必須有個-,最後又不以--結束的字串。其實,這裡就是期望得到product-varient的形式。也就是我們之前使用add_lunch_combo新增的那些字串的格式。比如:aosp_arm-eng,product就是aosp_arm,varient就是eng.中間的-是必須的。如果發現符合要求的格式的話selection變數就會被$answer賦值,也就是說,selection其實就是一個product-varient模式的字串。
3.如果既不是數字,又不是合法的字串,或者是數字,這個時候,selection應該就沒有被賦值過,這個時候-z就成立了,那麼就會提示你輸入的東西不對。並且直接返回。
如果輸入合法,通過檢測後, export TARGET_BUILD_APPS= 這行匯出了一個變數,但是它的值是空的。而之後的 local product=$(echo -n $selection | sed -e "s/-.*$//")這句則是得到了product部分,也就是把varient部分砍掉了。這裡使用了sed編輯器,-e 表示執行多條命令,但這裡只有一條,雙引號的s表示替換,這裡就是把-後面接任意字元,然後以任意字元結尾的部分替換為空,也就是砍掉-後面的了。得到produce後就開始檢查product的合法性。 check_product $produc ,這裡使用了check_product函式:
[html] view plain copy print?- # check to see if the supplied product is one we can build
- function check_product()
- {
- T=$(gettop)
- if [ ! "$T" ]; then
- echo "Couldn't locate the top of the tree. Try setting TOP." >&2
- return
- fi
- TARGET_PRODUCT=$1 \
- TARGET_BUILD_VARIANT= \
- TARGET_BUILD_TYPE= \
- TARGET_BUILD_APPS= \
- get_build_var TARGET_DEVICE > /dev/null
- # hide successful answers, but allow the errors to show
- }
函式的一開始就呼叫了gettop函式,所以,我們得先弄明白這個函式的功能:
[java] view plain copy print?- function gettop
- {
- local TOPFILE=build/core/envsetup.mk
- if [ -n "$TOP" -a -f "$TOP/$TOPFILE" ] ; then
- # The following circumlocution ensures we remove symlinks from TOP.
- (cd $TOP; PWD= /bin/pwd)
- else
- if [ -f $TOPFILE ] ; then
- # The following circumlocution (repeated below as well) ensures
- # that we record the true directory name and not one that is
- # faked up with symlink names.
- PWD= /bin/pwd
- else
- local HERE=$PWD
- T=
- while [ !\(−f$TOPFILE \) -a \( $PWD != "/" \) ]; do
- \cd ..
- T=`PWD= /bin/pwd -P`
- done
- \cd $HERE
- if [ -f "$T/$TOPFILE" ]; then
- echo $T
- fi
- fi
- fi
- }
gettop函式一開始定義了一個區域性變數TOPFILE,並且給他賦了值,然後是一個測試語句:if [ -n "$TOP" -a -f "$TOP/$TOPFILE" ] ; then,這裡-n 是判斷 $TOP是否不為空, -a 就是and的意思,和C語言中的&&相同, -f是判斷給定的變數是不是檔案,那麼,這個測試語句就是如果 $TOP不為空 切同時 $TOP/$TOPFILE檔案存在,就執行下面的程式碼:
(cd $TOP; PWD= /bin/pwd)
也就是進入到$TOP目錄下,並且給PWD變數賦一個pwd命令的返回值,也就是當前目錄的路勁。我試著在這個指令碼中搜索TOP變數,發現它並沒有出現並且賦值,所以,這裡應該執行else部分。else中,首先判斷build/core/envsetup.mk這個檔案是否存在,當在原始碼頂層目錄下的時候,這個檔案是存在的,那麼這裡為真,然後PWD變數就是android程式碼的根目錄。所以如果souce build/envsetup.sh的時候,如果處於android原始碼的頂級目錄,那麼這個函式就返回了。關於shell函式的返回值問題,還需要留意一下,當一個函式沒有返回任何內容的時候,預設返回的是最後一條命令的執行結果,也就是這裡的/bin/pwd的結果。那當然就是android原始碼的頂級目錄了。這個時候如果不在頂級目錄,build/core/envsetup.mk應該不存在,這個時候就會while迴圈不斷的進入道上層目錄,然後判斷$TOPFILE是否存在,並且判斷是否到達根目錄了,如果這個檔案不存在且沒有到達根目錄,那麼就會一個往上一級目錄查詢。最終如果找到了這個檔案,就意味著找到了android原始碼的頂層目錄,並把這個路勁返回。前面的兩次判斷如果都成立的話也沒有返回任何東西,是因為,當前目錄肯定就是原始碼的頂級目錄了。也就是說,這個函式就是找到原始碼的頂級目錄,如果當前目錄就是頂級目錄,就什麼也不返回,如果當前目錄不是頂級目錄,就返回頂級目錄的路勁。
再回過頭來看check_product函式,可以看到在獲取到android原始碼的頂級目錄以後,就會判斷這個T是不是空值,空的的話就說明沒有獲取到頂級目錄,這個時候這個函式就直接返回了。如果一切正常,那麼就會定義幾個變數。
TARGET_PRODUCT=$1 \
TARGET_BUILD_VARIANT= \
TARGET_BUILD_TYPE= \
TARGET_BUILD_APPS= \
這幾個變數只有一個變數都是全域性變數,因為沒有加local關鍵字修飾,它們中,只有第一個變數賦值為$1,也就是這個函式的第一個引數。目前,這幾個變數的作用還不得而知,所以,我們繼續向下分析。
這個函式還有最後一件事情要做:
get_build_var TARGET_DEVICE > /dev/null
這裡呼叫了get_build_var這個函式,這個函式如下:
[plain] view plain copy print?- # Get the exact value of a build variable.
- function get_build_var()
- {
- T=$(gettop)
- if [ ! "$T" ]; then
- echo "Couldn't locate the top of the tree. Try setting TOP." >&2
- return
- fi
- (\cd $T; CALLED_FROM_SETUP=true BUILD_SYSTEM=build/core \
- command make --no-print-directory -f build/core/config.mk dumpvar-$1)
- }
這裡就是執行make命令,-f給它指定了makefile,同時還傳入了一個目標。然後,config.mk就會執行。
這個函式,我們不妨感性認識一下先:在命令列中執行get_build_var TARGET_DEVICE
可以看到列印了generic這個值。這個函式雖然分析起來比較複雜,但是它做的非常簡單。config.mk會include dumpvar.mk,這個檔案中會提取我們傳入的dumpvar-TARGET_DEVICE變數中的TARGET_DEVICE,然後列印$(TARGET_DEVICE)。所以它做的事情很簡單。
這個函式執行完以後,有返回到lunch函式繼續執行:
[plain] view plain copy print?- if [ $? -ne 0 ]
- then
- echo
- echo "** Don't have a product spec for: '$product'"
- echo "** Do you have the right repo manifest?"
- product=
- fi
假定一切正常,繼續執行程式碼:
[plain] view plain copy print?- local variant=$(echo -n $selection | sed -e "s/^[^\-]*-//")
- check_variant $variant
- if [ $? -ne 0 ]
- then
- echo
- echo "** Invalid variant: '$variant'"
- echo "** Must be one of ${VARIANT_CHOICES[@]}"
- variant=
- fi
下面的程式碼是:
[plain] view plain copy print?- if [ -z "$product" -o -z "$variant" ]
- then
- echo
- return 1
- fi
- export TARGET_PRODUCT=$product
- export TARGET_BUILD_VARIANT=$variant
- export TARGET_BUILD_TYPE=release
然後呼叫了set_stuff_for_environment函式,這個函式內容如下:
[plain] view plain copy print?- function set_stuff_for_environment()
- {
- settitle
- set_java_home
- setpaths
- set_sequence_number
- export ANDROID_BUILD_TOP=$(gettop)
- # With this environment variable new GCC can apply colors to warnings/errors
- export GCC_COLORS='error=01;31:warning=01;35:note=01;36:caret=01;32:locus=01:quote=01'
- export ASAN_OPTIONS=detect_leaks=0
- }
這個函式會繼續補充一些變數。其中set_java_home會檢查匯出JAVA_HOME這個環境變數,這個環境變數就是JDK坐在的路勁,setpaths函式會給PATH環境變數補充編譯Android需要的一些路徑。
最後lunch呼叫了 printconfig函式,這個函式打印出了配置資訊。
至此,source build/envsetup.sh 和 lunch就分析完了。接下來將分析make 命令所做的事情。