BitBake使用攻略--BitBake的語法知識二

chegxy發表於2023-03-11


寫在前面

這是《BitBake使用攻略》系列文章的第三篇,主要講解BitBake的基本語法。由於此篇的實驗依賴於第一篇的專案,建議先將HelloWorld專案完成之後再食用此篇為好。
第一篇的連結在這:BitBake使用攻略--從HelloWorld講起


1. BitBake中的任務

對於BitBake,執行一個菜譜(recipe)其實執行的就是這個菜譜中的任務。任務可以說是BitBake的執行單元,它僅僅出現在菜譜和類檔案中,同時其名稱通常會以do_開頭。
為了在菜譜中新增任務,我們可以使用addtask將一個shell函式或者BitBake風格的Python函式定義為任務。與此同時,由於一個菜譜中可能存在多個任務,因此BitBake提供了afterbefore定義了這些任務的執行順序,也就是依賴關係,這主要是為了避免並行構建導致的順序錯誤,下面有一個定義任務的例子(透過修改前面的printhello.bb)檔案得到的。

DESCRIPTION = "Prints Hello World"
PN = 'printhello'
PV = '1'

python do_printdate() {
    import time
    bb.plain("Date: " + time.strftime('%Y%m%d', time.gmtime()))
}
addtask printdate

python do_printendmsg() {
    bb.plain("Build End")
}
addtask printendmsg

python do_build() {
    bb.plain("********************");
    bb.plain("*                  *");
    bb.plain("*  Hello, World!   *");
    bb.plain("*                  *");
    bb.plain("********************");
}

addtask build before do_printendmsg after do_printdate

在這個例子中,我們使用addtask將三個函式新增到了任務列表中,並在最後定義了三個任務的執行順序,即build任務需要在printdate任務前和printendmsg任務後完成。另外,可以看到我們在新增任務時省略掉了do_,這是合法的,因為BitBake會自動地為其新增。此時切換到主目錄執行bitbake printhello -c printendmsg(bitbake不指定任務的情況下會預設執行build任務,-c可以指定我們要執行的任務)會看到如下內容:

Date: 20230219
********************
*                  *
*  Hello, World!   *
*                  *
********************
Build End

顯然,我們執行printendmsg直接導致了buildprintdate任務的執行,這是依賴關係造成的。對於afterbefore,它們並不是僅限於定義序列關係,其還可以定義樹形關係,也就是說關鍵字後面可以有一個或多個任務,但要注意不能出現圈形關係。由於每個菜譜中可能會有很多工,因此它通常會提供do_listtasks去檢視當前菜譜中存在的任務。
BitBake可以新增任務,同樣地就可以刪除任務。在菜譜中,透過使用deltask可以刪除任務,同時還會刪除任務相關的依賴。例如,我們刪除上面的build任務(不是很恰當的例子,build任務不該刪除),其會造成printdateprintendmsg任務沒有任何關聯,也就是說,執行printendmsg任務不會造成printdate任務的執行。為了保留依賴關係,我們應該使用下面的方法:

do_build[noexec] = "1"

這樣,在執行printendmsg的時候,仍然會執行printdate,但不再執行build任務。另外,在執行任務的時候會發現bitbake報告沒有需要執行的任務,這是因為每次bitbake執行完一個任務後會生成stamp檔案,下次再執行相同任務時會比對stamp,如果發現任務內容無變化將不再執行該任務。因此,為了再次執行執行過的任務,可以刪除xx/tmp/stamps.do_xx檔案或者在命令列中加-f選項。
如果想要在bitbake構建過程中使用外部環境變數,有一種方法可以實現:

  • 先將該環境變數的名稱新增到BB_ENV_PASSTHROUGH_ADDITIONS,此時該環境變數被允許新增到BitBake的資料倉儲中,例如:export BB_ENV_PASSTHROUGH_ADDITIONS="$BB_ENV_PASSTHROUGH_ADDITIONS TEST_ENV"
  • 然後匯出該環境變數,例如:export TEST_ENV="123"

另外值得注意的是,每次修改該環境變數都會導致與該變數相關的任務校驗和不一致,因此這些任務每次都會重新構建。
BitBake為了保證每次能夠用乾淨的環境去執行任務,通常會清除掉外部環境匯出的或者PassThrough列表(BB_ENV_PASSTHROUGHBB_ENV_PASSTHROUGH_ADDITIONS)中列出的變數,但我們能夠透過設定BB_PRESERVE_ENV來阻止這種清除,例如我們在執行bitbake命令前先在命令列執行如下命令:

export BB_PRESERVE_ENV="1"

這樣,在本次bitbake執行時就會使用和上一次一樣的環境。因此,如果我們將TEST_ENV傳進了資料倉儲,同時設定了BB_PRESERVE_ENV,那麼我們在下次執行bitbake前,即使清除了BB_ENV_PASSTHROUGH_ADDITIONS中的TEST_ENV,在環境中也仍然能用TEST_ENV。但如果我們此時又想要原始的環境變數,那麼我們可以透過BB_ORIGENV(原始資料倉儲)得到,例如我們想獲得BAR的原始值:

origenv = d.getVar("BB_ORIGENV", False)
bar = origenv.getVar("BAR", False)

2. 任務配置

除了上一節中提及的依賴關係,任務還有其他的一些屬性,這些都可以透過標誌(Flag,語法詳見前一節)來控制。下面介紹一組BitBake提供的任務屬性,其用法與之前的do_build[noexec] = "1"一樣:

  • [cleandirs] : 指定任務執行前需要建立的空目錄,如果這些目錄原本存在將會被刪除重建
  • [depends] : 控制任務間的依賴關係
  • [deptask] : 控制任務間構建時的依賴關係
  • [dirs] : 指定任務執行前需要建立的目錄,如果這些目錄原本存在將保留,另外列出的最後一個目錄將作為本任務執行的當前工作目錄
  • [lockfiles] : 指定一個或多個鎖檔案(lockfile),只有拿到指定鎖檔案的許可權後才能執行該任務,否則會阻塞,這是BitBake提供的一種互斥機制
  • [noexec] : 當設定為"1"時,該任務不再執行,但仍作為類似佔位符一樣存在
  • [nostamp] : 當設定為"1"時,不再生成該任務的stamp檔案,該任務(包括依賴該任務的任務)將每次都會執行
  • [number_threads] : 指定同時可以執行該任務的最大執行緒數,以限制某些資源的使用。另外需要注意,該屬性需要在全域性設定,而不能單獨的設定到某個菜譜檔案中,例如可以在base.bbclass檔案中定義do_fetch[number_threads] = "2",同時,當number_threads超過BB_NUMBER_THREADS時,該屬性將無效
  • [postfuncs] : 任務完成後要執行的函式列表
  • [prefuncs] : 任務執行前要執行的函式列表
  • [rdepends] : 控制內部任務執行時的依賴關係
  • [rdeptask] : 控制任務執行時的依賴關係
  • [recideptask] : 當與[recrdeptask]一起設定時,為額外的依賴指定一個要被檢查的任務
  • [recrdeptask] : 控制任務遞迴執行時的依賴
  • [stamp-extra-info] : 追加到任務stamp的額外stamp資訊
  • [umask] : 執行任務的umask

2.1 依賴

由於BitBake支援多執行緒構建程式碼,所以依賴對於它來說也是必須的,因為所有任務之間都有先後順序,例如執行緒1在執行一個軟體包的配置任務,執行緒2在執行一個軟體包的編譯任務,由於配置必須在編譯之前(否則編譯會報錯),因此我們必須宣告兩個任務的依賴關係,即編譯任務依賴於配置任務。對於BitBake,你可以宣告一個菜譜中兩個任務的依賴關係,也可以宣告不同菜譜中的依賴關係,前提是這些任務是存在的(即addtask宣告過的)。
定義依賴的方法比較多,接下來我們一個一個的進行講解,並儘可能的提供一些例子幫助理解。

2.1.1 內部任務間的依賴

這個在第一節中已經進行了說明,它是透過afterbefore指定的,其作用範圍僅限於同一個菜譜中的任務。由於在先前已經有相關的例子了,這裡就不再進行贅述。

2.1.2 不同菜譜下的任務間依賴

對於兩個不同菜譜檔案中的任務,如果要指定它們間的依賴關係,其需要先使用DEPENDS指定依賴的菜譜,然後透過任務的[deptask]指定要依賴的DEPENDS列出全部菜譜的該依賴任務。這可能有點不太容易理解,舉個例子,現在有個菜譜A,裡面有一個build任務,還有兩個菜譜BC,它們裡面都有一個configure任務,對於A的build任務來講,其需要在完成其他菜譜檔案中的configure任務後才能執行,因此此時我們可以有以下方法來指定該依賴關係:

  1. A中定義DEPENDS變數,其內容為DEPENDS = "B C"
  2. A中定義依賴:do_build[deptask] = "do_configure"

此時,執行Abuild任務時將會先執行BC中的configure任務。下面有兩個菜譜檔案:

printhello.bb: 

DESCRIPTION = "Prints Hello World"
PN = 'printhello'
PV = '1'
DEPENDS = "printelse"

do_build[deptask] = "do_configure"

python do_build() {
    bb.plain("********************");
    bb.plain("*                  *");
    bb.plain("*  Hello, World!   *");
    bb.plain("*                  *");
    bb.plain("********************");
}

printelse.bb:

DESCRIPTION = "Prints Else Info"
PN = 'printelse'
PV = '1'

python do_configure() {
    bb.plain("********************");
    bb.plain("*                  *");
    bb.plain("*   Pre Configure! *");
    bb.plain("*                  *");
    bb.plain("********************");
}

addtask do_configure

此時,執行bitbake -f printhello,你可以得到如下內容:

********************
*                  *
*   Pre Configure! *
*                  *
********************
********************
*                  *
*  Hello, World!   *
*                  *
********************
2.1.3 執行時態下的依賴

前面描述的幾種依賴定義方法僅限於構建時態下,相反,如果想要定義執行時態下的依賴就需要藉助[rdeptask]屬性。在BitBake構建程式碼的過程中會生成許多包,這些都列在PACKAGES變數中。對於每個包,BitBake都提供了RDEPENDS變數用於表示對應包依賴的其他包,這類似於DEPENDS變數。另外,類似[deptask]屬性,對於每個任務同樣有[rdeptask]屬性用於表示包內任務依賴的其他任務,一旦指定該依賴關係,那麼在執行該包內的此任務時,一定會先執行其RDEPENDS列出的全部包中的[rdeptask]依賴任務。
這裡,你可能有點混淆,但你只需要知道一點,這些歸根結底也只是任務A依賴任務B這種簡單關係。

2.1.4 遞迴依賴

BitBake還提供了一種任務屬性[recrdeptask],其提供了遞迴依賴方式,執行機制如下:

  1. 尋找當前任務的構建時依賴和執行時依賴
  2. 新增這些依賴到該任務的依賴列表中
  3. 遞迴到依賴列表中繼續1,直到不存在依賴關係

我的理解是,[recrdeptask]提供了一種更為普遍的方法去管理不同菜譜間任務的依賴,其本質上是構建依賴(2.1.2)和執行時依賴(2.1.3)的結合,因此,這個屬性通常用在高階別的菜譜中。
另外,BitBake會忽略掉迴圈依賴,例如do_a[recrdeptask] = "do_a"

2.1.5 任務間的依賴

對於2.1.2和2.1.3中提到的依賴關係定義,其都由DEPNEDS或者RDEPENDS和任務屬性共同決定,為了能夠用更為一般的定義依賴關係的方法,BitBake提供了[depends]屬性。對於該屬性,其指定了一個任務依賴的任務列表,這些依賴任務不同於[deptask]或者[rdeptask],它需要在依賴任務前指定任務所屬的目標,格式如下:

do_a = "target1:do_b target2:do_b"

在這個例子中,執行任務a之前必須要先執行target1的任務b和target2的任務b。你可以嘗試下面的例子:

printhello.bb:

DESCRIPTION = "Prints Hello World"
PN = 'printhello'
PV = '1'

python do_configure() {
    bb.plain("************************");
    bb.plain("*                      *");
    bb.plain("* Pre Configure self ! *");
    bb.plain("*                      *");
    bb.plain("************************");
}
do_configure[nostamp] = "1"
addtask do_configure

do_build[depends] = "printhello:do_configure printbeef:do_configure printbird:do_configure"

python do_build() {
    bb.plain("********************");
    bb.plain("*                  *");
    bb.plain("*  Hello, World!   *");
    bb.plain("*                  *");
    bb.plain("********************");
}

printbird.bb

DESCRIPTION = "Prints Bird Info"
PN = 'printbird'
PV = '1'

python do_configure() {
    bb.plain("************************");
    bb.plain("*                      *");
    bb.plain("* Pre Configure bird ! *");
    bb.plain("*                      *");
    bb.plain("************************");
}

do_configure[nostamp] = "1"
addtask do_configure

printbeef.bb:

DESCRIPTION = "Prints Beef Info"
PN = 'printbeef'
PV = '1'

python do_configure() {
    bb.plain("************************");
    bb.plain("*                      *");
    bb.plain("* Pre Configure Beef ! *");
    bb.plain("*                      *");
    bb.plain("************************");
}

do_configure[nostamp] = "1"
addtask do_configure

執行結果如下:

************************
*                      *
* Pre Configure bird ! *
*                      *
************************
************************
*                      *
* Pre Configure Beef ! *
*                      *
************************
************************
*                      *
* Pre Configure self ! *
*                      *
************************
********************
*                  *
*  Hello, World!   *
*                  *
********************

可見,透過這種方法,我們可以實現前面2.1.1,2.1.2,2.1.3提到的所有依賴定義方法。

2.2 事件

BitBake提供了事件處理函式機制用於在一些特定場景下執行某個指定函式,例如在某個任務失敗後觸發事件處理函式,然後傳送郵件,另外,事件只能定義在菜譜檔案或者類檔案中,下面是一個事件的例子,在printhello.bb檔案中新增如下內容:

addhandler myclass_eventhandler
python myclass_eventhandler() {
    from bb.event import getName
    bb.plain("The name of the Event is %s" % getName(e))
    bb.plain("The file we run for is %s" % d.getVar('FILE'))
}
myclass_eventhandler[eventmask] = "bb.event.RecipeParsed"

在這個例子中,我們使用addhandler將一個python函式定義為事件處理函式,然後透過[eventmask]屬性指定了哪些事件可以觸發該函式(若不指定,任何事件都將觸發該函式),例子中指定了在解析完菜譜後觸發myclass_eventhandler函式。另外在這個例子中,e是一個全域性變數,指代的是當前發生的事件,透過getName函式我們可以得到該事件的名稱。
下面列出了在標準構建過程中,最經常使用的一些事件:

  • bb.event.ConfigParsed(): 基本配置(bitbake.conf,base.bbclass以及繼承的其他全域性變數)解析後觸發。透過在該事件中設定BB_INVALIDCONF可以重解析基本配置。
  • bb.event.HeartbeatEvent(): 定時觸發,預設一秒,可以透過配置BB_HEARTBEAT_EVENT配置觸發間隔。
  • bb.event.ParseStarted(): 開始解析菜譜時觸發,事件有total屬性表示計劃解析菜譜的數量。
  • bb.event.ParseProgress(): 解析菜譜觸發,事件有current屬性表示已經解析菜譜的數量。
  • bb.event.ParseCompleted(): 解析菜譜完成時觸發,事件有cached, parsed, skipped, virtuals, masked, errors屬性,可以用來統計解析情況。
  • bb.event.BuildStarted(): 一個新的構建開始時觸發。
  • bb.build.TaskStarted(): 一個任務開始時觸發。taskfile屬性表示任務是哪個配方發起的,taskname表示任務名,logfile表示任務的輸出路徑,time表示任務開始時間。
  • bb.build.TaskInvalid(): 執行不存在的任務時觸發。
  • bb.build.TaskFailedSilent(): setscene任務失敗,輸出過於冗長不給使用者呈現時觸發。
  • bb.build.TaskFailed(): 正常任務失敗時觸發。
  • bb.build.TaskSucceeded(): 一個任務成功完成時觸發。
  • bb.event.BuildCompleted(): 一個構建完成時觸發。
  • bb.cooker.CookerExit(): 構建伺服器關機時觸發。

2.3 校驗和

BitBake在執行每個任務時都會預設生成一個stamp檔案,其儲存了該任務輸入的校驗和資料,透過它,BitBake可以避免去重複執行一些任務(輸入未改變)。另外,BitBake提供了bitbake-dumpsigs命令去讀取任務生成的簽名資料。

3. Class Extension Mechanism

BitBake提供了一種在單個recipe檔案下定義多個版本的機制,其透過BBCLASSEXTENDBBVERSIONS變數實現。


至此,基本的BitBake語法知識就算是學完了,在之後的時間裡,我將繼續介紹命令用法以及怎樣使用它完成一個複雜的工程構建任務。當然啦,也希望大家能多多支援一下博主,碼字不易,還望一鍵三連,再次謝謝大家能看到這個地方。
我是chegxy,歡迎關注!!!

相關文章