我們開發嵌入式系統時總會需要修修改改. 不管是個人獨自創作, 多人小組協同開發, 或者是多家公司聯合團隊, 甚至是跨國合作, 理所當然的必需要有一套好用的版本控制軟體, 才能事半功倍. 而 git 正是這樣一套好用的分散式版控軟體, 重點是它還是個免費的.

這一篇記的是如何讓 ARM 的官方開發工具 Keil µVision IDE 在編譯程式的時候自動git 取得版本資訊, 省去人工修改的麻煩.

發想



因為 Keil µVision IDE 可以讓使用者自訂在編譯 (compile) 程式之前和編譯完成之後呼叫指令或者是批次檔. 所以一開始我的想法是:

  1. 寫一個批次檔去讀取 git 版本庫裡的版本資訊, 並產生一個 C 的標頭檔 (header file), 把版本資訊塞放在正確的位置上.
  2. 再修改需要用到版本資訊的程式, 去引入 (include) 這個標頭檔.
  3. 最後再讓專案在編譯之前, 先呼叫一下這個批次檔.

這樣子, 應該每次修改了程式要執行編譯工作的時候, 都會重新產生標頭檔, 取得相關的版本資訊:

  • 如果我們 commit 了修改: 標頭檔內容自然就是新的版本資訊. 因為內容更動了, 所以有用到這個資訊的程式自然就會一起重新編譯.
  • 如果修改沒有 commit: 標頭檔雖然是重新產生的, 但版本資訊沒有變化, 標頭檔內容當然也不變. 所以不會因此 (重新產生標頭檔) 而需要重新編譯許多不相關的檔案.(註一)

註一: 很可惜, Keil µVision IDE 只看檔案的修改時間戳記, 目前暫時我們只能透過 '限制不相干的程式檔案不要引入這個版本資訊的標頭檔' 來限縮重新編譯的範圍.
已經解決了 2018/02/26

實作



這裡的實作次序上和前面提的想法次序上稍微有一點不同. 其實是因為進行第一項工作寫 DOS 批次檔產生一個 C 的標頭檔時, 我需要一個明確的標頭檔樣本. 所以也就先從修改程式開始.

STEP 1-1


首先是製造一個 C 的標頭檔 (header file), 名稱可以取為 version.h. 內容大概是下面這個樣子:

#ifndef __VERSION_H__
#define __VERSION_H__
#define FW_VERSION "v2.0.10"
#endif

這個 FW_VERSIONdefine 原本可能是放在某個標頭檔中 (就叫 config.h 吧), 裡面放的應該都是和專案相關的巨集 (macro) 定義. 例如:

#ifndef __CONFIG_H__
#define __CONFIG_H__

#define FW_VERSION	"v2.0.10"
#define FW_VERDATE	BUILD_DATE

#define DEFAULT_IP	"192.168.1.100"
...
#endif

這裡我們把 FW_VERSIONconfig.h 中獨立出來, 理由很簡單: 只是要讓後面自動產生的工作簡單一些. 其實呢, 還有另外一個理由. 就是因為只有它和整套程式版本資訊相關, 不會因為只是這個標頭檔內的版號改了就引發一些不相干的程式重新編譯. 當然, 如果你的專案真的還有其他和版本資訊相關的定義, 那就試 OK 了之後自己擴充唄!

STEP 1-2


FW_VERSIONconfig.h 中獨立出來, 理所當然 config.h 需要稍作修改. 改法有下列幾種:

  1. 直接把這一行刪掉, 然後換成引入 version.h 這個新加的標頭檔.
  2. config.h 直接把這一行刪掉. 然後修改其他會用到它的程式, 加上引入 version.h.
  3. ...(應該還有其他作法吧? 例如: 不直接刪除原先這一行, 而是加一些防呆的定義檢查及設定)

最簡單的, 當然是第一種方法. 這樣改法的好處是: 整套原始程式修改最小, 就只有 config.h 需要修改, 其他的完全不動. 不過有利就有弊, 它的缺點是: 因版本資訊異動所引發的重新編譯的範圍, 可能會很大: 說不定會是整個專案. 因為 config.h 這麼重要的引入標頭檔, 說不定你就把它直接放在最底層的引入標頭檔中. 原本它是不會輕易就更動的, 但是現在卻是每次提交程式異動就會變動. 遇到這種情形, 那就麻煩你換成別種改法囉!

第二種方式看起來好像需要改很多檔案, 其實並不會多花你多少時間. 因為把 FW_VERSIONconfig.h 中刪除之後重新編譯一輪, 你就會知道需要修改的程式到底有多少支. 但是你一開始多花的時間, 很快就會回收回來, 因為這樣修改的好處是: 可以把因為版本資訊異動所引發的重新編譯控制到最小.

附帶一提, 不要小看重新編譯引發的問題. 我有聽說過有些專案全部重新編譯一遍要好幾個小時, 真的非常誇張. 不知道是不是那個人故意誇大了.

STEP 2


程式的部份準備好了, 接下來輪到寫批次檔了. 我們的目標就是自動產生最前面提到的 version.h 那 4 行內容.

開始之前我們需要知道如何取得目前的版本資訊. 如果你習慣用 git bash, 這簡直是簡單到不像話.

git describe --abbrev=6 --dirty --always --tags

這個指令會有幾種型式的結果:

  1. 目前版本的 <hash_code> 前 6 個字母: 之所以是 6 個字母, 是因為我們下指令時 --abbrev=6 指定的, 你可以依照自己的需求加長或改短. 但是不建議短於 4 個字母.
  2. 目前版本的 <hash_code> 前 6 個字母再加上 -dirty: 這是最常見的型式. 它的意思是目前的程式和版本庫中的版本有些差異, 也就是你 checkout 之後有修改了一些東西. 另外你可以把指令中的參數 --dirty 改成 --dirty=ABC. 這樣, 多出來的那個 -dirty 就可以換成你指定的 ABC.
  3. 前面二種的 <hash_code> 換成了 <tag_name>. 因為我們在版本樹上, 把這個版本標上了 tag. 這種型式通常是有一些特殊意義的版本: 已經通過內部測試, 可以釋出作 bata 測試; 或者是完成所有測試, 正式釋出的版本. 反正是只要我們有標上 tag 的版本, 就會是換成以 <tag_name> 顯示.

有了這個成果, 接下來的工作應該不會太難. 不過因為太久沒寫 DOS 批次檔了, 結果卡了一上午, 真就無言啊...

OK, 我們的批次檔名字就叫 version.cmd, 內容如下:

@echo off
for /F "usebackq" %%v in (`"git describe --abbrev=6 --dirty=_wk --always --tags"`) do (
  echo #ifndef __VERSION_H__ > %1
  echo #define __VERSION_H__ >> %1
  echo #define FW_VERSION "%%v" >> %1
  echo #endif >> %1
  echo version:%%v
)

其中讓我卡了一上午的就是第二行. DOS 批次檔不像 linux bash 只要指令前後加個倒引號 (back quote) 就可以把別的指令的執行結果抓進來用, 唯一有類似功能的就是 for /F ... in (...) do ... 這個指令. 這個指令要取得執行結果必需把指令用單引號括住然後放在 in (...) 的括弧中.
最常見的例子如: for /F %%i in ('dir /s /b') do ...

到此看似離完成目標不遠. 可是呢, 把上面的 git 指令一放進去, 結果出了奇怪的錯誤訊息.

fatal: --dirty is incompatible with commit-ishes

於是想到換用倒引號

for /F "usebackq" %%v in (`git describe --abbrev=6 --dirty=wk --always --tags`) do ...

結果還是出現一樣的錯誤訊息. 一上午谷歌大神不管怎麼拜就是拜不出答案來, 最後看到一個長檔名必需再用雙引號括住的例子, 才想到不如把整個指令再用雙引號括住. 結果神奇的, 問題就解決了, 也就是像我們上面列出來的那樣. 原來, 壞就壞在我們用的 git 這一行指令裡包含了等號 (後來才發現單引號和倒引號二者都是括不住等號的).

再來的 5 行 echo 指令, 前面 4 個就是把要產生的標頭內容輸出到指定的檔案裡. 這 4 行的內容你必需依據自己的專案需求自己增刪修改. 最後一個 echo 指令只是顯示一下抓到的版本資訊.

之後我又增加了參數檢查及使用方法的訊息, 所以最終的版本如下:

@echo off
if "%1"=="" (
  echo Usage: %0 ^<header_file_name^>
  goto :eof
)
for /F "usebackq" %%v in (`"git describe --abbrev=6 --dirty=_wk --always --tags"`) do (
  echo #ifndef __VERSION_H__ > %1
  echo #define __VERSION_H__ >> %1
  echo #define FW_VERSION "%%v" >> %1
  echo #endif >> %1
  echo version:%%v
)

STEP 3


最後一個步驟, 就是把專案設定改一下, 讓我們每一次編譯程式的時候自動產生一個 version.h. 如下圖:

Keil PreCompile Run

設定編譯前執行 version.cmd

輸入這一行時有幾點需要注意:

  • 批次檔 version.cmd 放在專案目錄中的哪一個位置其實都 OK, 但是它的位置會影響我們在這裡輸入的指令字串. 請注意一定要使用相對路徑的寫法, 不然你會很容易就需要重改一遍. IDE 要呼叫外部指令時是以專案檔 .uvprojx 所在位置為當前目錄位置. 所以如果你把 version.cmd 和專案檔 .uvprojx 擺放在同一目錄中就可以直接輸入 version.cmd 就 OK 了. 但如果是擺放在專案檔 .uvprojx 上一層目錄中, 就必需改成輸入 ..\version.cmd. 如果是擺放在專案檔 .uvprojx 同一個層級的另外一個目錄中 (例如: utils), 就必需改成輸入 ..\utils\version.cmd.
  • 另外, 我們的批次檔 version.cmd 需要一個參數作為產生標頭檔的路徑及檔名. 不過請你換用批次檔所在的目錄作為當前目錄位置, 來計算標頭檔的路徑. 還有建議你用雙引號把這個參數括住, 免得哪天你用了不相容的路徑及檔名.
  • 建議你直接把 version.cmd 和你的專案檔 .uvprojx 放在一起. 這樣就只要注意到底產生標頭檔要放在哪裡就好.

其他



還有二點要注意:

  • 請把 version.cmd 加入版本追蹤.
  • 但是不要追蹤 version.h 的版本變化. 請把它列入 .gitignore 中.

SVN


如果你用的是 SVN, 也可以有類似的功能: 只要把 version.cmd 中的 git describe指令換成 SVN 指令就可以了.

svn info --show-item revision

不過這個版本資訊, 只是一組簡單的數字. 如果你覺得太單調, 可以自己在要輸出到標頭檔時作一點小加工.

如何取得其他資訊


取得版本原始作者 (author name)

git show -s --format=format:'%an'

取得版本的原始日期 (author date)

git show -s --format=format:'%aI'

取得版本提交作者 (commiter name)

git show -s --format=format:'%cn'

取得版本提交日期 (commit date)

git show -s --format=format:'%cI'

2018/02/26 更新


花了一些時間把註一所提的問題解決了, 順便也把重新 clone git 檔案庫時找不到 version.h 的問題也修正了. version.cmd內容更新如下:

@echo off
if "%1"=="" (
  echo Usage: %0 ^<header_file_name^>
  goto :eof
)
for /F "usebackq" %%v in (`"git describe --abbrev=6 --dirty=_wk --always --tags"`) do (
  if exist %1.new del %1.new
  echo #ifndef __VERSION_H__ > %1.new
  echo #define __VERSION_H__ >> %1.new
  echo #define FW_VERSION "%%v" >> %1.new
  echo #endif >> %1.new
  echo version:%%v
)
if not exist %1 (
  move %1.new %1
) else (
  for /F "usebackq" %%c in (`"diff %1 %1.new | wc -l"`) do (
    if "0" == "%%c" (
      echo Use original %1
      del %1.new
    ) else (
      echo Use new %1
      del %1
      move %1.new %1
    )
  )
)

2018/08/22 更新


今天花了一點時間把整個 version.cmd 改寫成 bash 版本的 version.sh, 並改用 git for windows 的 bash 指令來執行. 另外還利用 date 指令抓取目前時間來設定另一個 macro FW_RELDATE. 原先這個 FW_RELDATE 是利用另一個標頭檔 "build_def.h" 裡面的一大堆 #define 來把 C 編譯器的編譯日期 __DATE__ 轉換成純數字日期的字串. 如果你想改用這個 bash 版本的 version.sh, 還需要把先前設定的 "專案的編譯前執行指令" 由原本的

version.cmd "..\src\version.h"
修改為
bash version.sh "..\src\version.h"(註二).

註二: git for windows 的 bash 會自動轉換檔案路徑中的 \/, 所以不必擔心路徑路相容性的問題.

version.sh的內容如下:

#!/bin/bash
if [ $# -eq 0 ]
then
  echo Usage: $0 \<header_file_name\>
else
  if [ -e $1.new ] 
  then
    rm $1.new
  fi
  echo \#ifndef __VERSION_H__ > $1.new
  echo \#define __VERSION_H__ >> $1.new
  echo \#define FW_VERSION \"`git describe --abbrev=6 --dirty=_wk --always --tags`\" >> $1.new
  echo \#define FW_RELDATE \"`date +%Y%m%d`\" >> $1.new
  echo \#endif >> $1.new
  echo version: `git describe --abbrev=6 --dirty=_wk --always --tags`

  if [ -e $1 ]
  then
    if [ "0" = `diff $1 $1.new | wc -l` ]
    then
      echo Use original $1
      rm $1.new
    else
      echo Use new $1
      rm $1
      mv $1.new $1
    fi
  else
    mv $1.new $1
  fi
fi

另外如果你需要 "build_def.h" 的話, 可以在這個 stackoverflow 網站的連結找到它的內容.

arrow
arrow
    創作者介紹
    創作者 MagicJackTing 的頭像
    MagicJackTing

    傑克! 真是太神奇了!

    MagicJackTing 發表在 痞客邦 留言(1) 人氣()