為了確保我們commit到GitHub上的Rust程式是可以正常工作的,我們可以替程式專案加入CI(持續整合,Continuous Integration)。Rust的crates.io線上套件庫也可以放上各種CI的狀態徽章(badge),提供開發者在選用套件時一個參考,避免用到連套件作者自己寫的測試都測不過的套件。Travis CI是對於Rust程式語言親和力比較高的CI,這篇文章將會針對Rust的Cargo程式專案來介紹Travis CI的設定檔撰寫方式。



Travis CI支援Linux、macOS和Windows作業系統,換句話說,它可以協助開發者在commit程式到GitHub上後,自動把程式拿到這三個系統上做測試。開發者就不用自己弄台實體機器或是虛擬機器來安裝這些系統來測試程式啦!Travis CI可以免費使用在GitHub的開源專案上,若要用在GitHub的閉源專案上,就得使用它的付費(pro)版本。

Travis CI的開源專案網站:

https://travis-ci.org/

Travis CI的付費版網站:

https://travis-ci.com/

註冊Travis CI的開源專案帳號

進入Travis CI的開源專案網站後,按下Sign Up即可註冊。

travis-ci

接著要確認提供GitHub帳號的權限給Travis CI。

travis-ci

如此一來Travis CI帳號就註冊成功了!以後只要使用GitHub帳號就可以登入。

travis-ci

加入GitHub上的倉庫(Repository)

在Travis CI的帳號頁面中,可以按下Sync Account來同步GitHub上的倉庫,注意按下之後頁面可能不會跟著刷新,可以手動重新整理頁面,或是直接使用搜尋功能。

travis-ci

travis-ci

在GitHub倉庫的列表中,可以藉由撥動開關來設定該GitHub倉庫是否要使用Travis CI。

travis-ci

替Cargo程式專案加上Travis CI的設定檔

Travis CI會去讀取GitHub倉庫下的.travis.yml檔案來執行測試程式專案的腳本。這個.travis.yml檔案的基本撰寫方式如下:

language: rust

rust:
  - stable
  - beta
  - nightly

os:
  - linux
  - osx
  - windows

以上的設定,會讓Travis CI在Linux、macOS和Windows作業系統上,以不同的容器(container)分別使用最新的Rust Stable、Beta和Nightly版本來測試程式專案。(所以這個設定會有9個容器。)

我們可以在Travis CI的網頁上直接看到不同腳本運行的即時情況。

travis-ci

travis-ci

travis-ci

travis-ci

替Cargo程式專案加上Travis CI的徽章

為了要在crates.io上顯示出如下的Travis CI的徽章:

Travis CI badge

我們可以在Cargo.toml設定檔中加上[badges.travis-ci]區塊,並設定其中的repositorybranch項目。舉例來說,若我要顯示GitHub上magiclen/rust-short-crypt這個倉庫的Travis CI狀態,[badges.travis-ci]區塊的撰寫方式如下:

[badges.travis-ci]
repository = "magiclen/rust-short-crypt"
branch = "master"

手動觸發測試腳本

在一般情況下,GitHub倉庫只要有新的commit,Travis CI就會被觸發而開始執行測試腳本。不過如果想要手動觸發的話,可以在Travis CI上的GitHub倉庫頁面中,或是Dashboard頁面中,按下Trigger a build

travis-ci

travis-ci

手動觸發腳本時,可以自行決定要使用的分支(branch)和commit訊息(用來辨識用,可以不輸入)。最重要的是,它可以無視.travis.yml設定檔的設定,而用臨時的設定值(與.travis.yml一樣的格式)來運行腳本。

travis-ci

進階的.travis.yml設定檔撰寫方式

指定Rust編譯器的版本

Travis CI除了可以使用最新的Rust Stable、Beta和Nightly版本外,還可以直接指定版本號碼。

例如要使用1.35.0的穩定版、2019-07-23的Beta版,和2019-07-25的Nightly版,.travis.yml設定檔的寫法如下:

language: rust

rust:
  - 1.35.0
  - beta-2019-07-23
  - nightly-2019-07-25

修改Travis CI所執行的指令

在預設的情況下,Travis CI只會使用以下指令來執行Cargo程式專案的測試:

cargo test --verbose

如果要修改Travis CI在運行腳本時會執行的指令,可以在.travis.yml設定檔加上script這個設定項目。例如要確保在運行測試時只使用一個執行緒,設定檔撰寫方式如下:

language: rust

script:
  - cargo test --verbose -- --test-threads=1

這個script設定項目所設定的指令,會以從上到下的順序來執行。當然,如果有需要的話,我們也可以使用cargo之外的指令。

指定環境變數

.travis.yml設定檔加上env這個設定項目,可以設定Travis CI容器在運行時的環境變數,而每個環境變數設定,都會建立出新的容器。

例如:

language: rust

env:
  -
  - FOO=bar BAR=foo
  - BAR=foo

如果需要指定能夠用在所有容器的環境變數,寫法如下:

language: rust

env:
  global:
    - GLOBAL=foobar
  matrix:
    -
    - FOO=bar BAR=foo
    - BAR=foo

指定多種不同的特色組合

雖然我們可以利用script這個設定項目在同一個Travis CI容器內執行多次的cargo test來測試多種不同的特色組合,例如:

language: rust

script:
  - cargo test --verbose
  - cargo test --verbose --no-default-features
  - cargo test --verbose --no-default-features --features std

但這樣我們就不太方便知道究竟是哪行指令的測試出了問題。利用env設定項目,我們可以將以上設定檔改寫為:

language: rust

env:
  -
  - NO_DEFAULT_FEATURES=1
  - NO_DEFAULT_FEATURES=1 FEATURES="std"

script:
  - if [ "$NO_DEFAULT_FEATURES" = "1" ]; then NO_DEFAULT_FEATURES="--no-default-features"; else NO_DEFAULT_FEATURES=""; fi 
  - cargo test --verbose $NO_DEFAULT_FEATURES --features "$FEATURES"

如此一來就可以區分不同的容器來運行不同特色的測試了!

在開始測試程式專案前先安裝必要的函式庫

.travis.yml設定檔加上install這個設定項目,可以讓Travis CI在測試程式專案前,先去執行某些指令。由於Rust程式時常會用到外部的C/C++函式庫,因此我們也可以利用這個install設定項目來執行安裝相依函式庫所要執行的指令。

例如以下設定,可以在Linux和macOS的環境中安裝支援HDRI和WebP的ImageMagick。

language: rust

os:
  - linux
  - osx

install:
  - if [ "$TRAVIS_OS_NAME" = "linux" ]; then sudo apt update ; fi
  - if [ "$TRAVIS_OS_NAME" = "linux" ]; then sudo apt install libwebp-dev ; fi
  - if [ "$TRAVIS_OS_NAME" = "osx" ]; then brew update ; fi
  - if [ "$TRAVIS_OS_NAME" = "osx" ]; then brew install webp ; fi
  - wget http://www.imagemagick.org/download/ImageMagick.tar.gz
  - tar xf ImageMagick.tar.gz
  - mkdir /tmp/ImageMagick-lib
  - cd ImageMagick*
  - ./configure --enable-hdri --with-webp
  - make
  - sudo make install
  - if [ "$TRAVIS_OS_NAME" = "linux" ]; then sudo ldconfig ; fi

Travis CI內建的TRAVIS_OS_NAME環境變數所儲存的值及為os設定項目所使用的值,用來表示當前容器是用哪個作業系統。

指定不同的目標

Travis CI的Linux預設的編譯目標為x86_64-unknown-linux-gnu,Windows預設的編譯目標為x86_64-pc-windows-msvc。然而在Linux上也很常用musl libc來取代gcc,在Windows上有時也會用gcc來取代微軟的Visual C++。

若要指定新的目標來測試,可以在.travis.yml設定檔加上matrix設定項目,再去設定其中的include設定項目。

完整的.travis.yml設定檔寫法如下:

language: rust

rust:
  - stable
  - beta
  - nightly

os:
  - linux
  - osx
  - windows

matrix:
  include:
    - rust: stable
      os: linux
      env: TARGET=x86_64-unknown-linux-musl
      install: rustup target add $TARGET
      script: cargo test --verbose --target $TARGET
    - rust: beta
      os: linux
      env: TARGET=x86_64-unknown-linux-musl
      install: rustup target add $TARGET
      script: cargo test --verbose --target $TARGET
    - rust: nightly
      os: linux
      env: TARGET=x86_64-unknown-linux-musl
      install: rustup target add $TARGET
      script: cargo test --verbose --target $TARGET
    - rust: stable
      os: windows
      env: TARGET=x86_64-pc-windows-gnu
      install:
        - rustup set default-host $TARGET
        - rustup default $TRAVIS_RUST_VERSION
        - rustup target add $TARGET
        - mkdir -p ~/.cargo
        - printf '\n[target.'$TARGET']\nlinker = "x86_64-w64-mingw32-gcc"\nar = "x86_64-w64-mingw32-ar"\n' >> ~/.cargo/config
      script: cargo test --verbose --target $TARGET
    - rust: beta
      os: windows
      env: TARGET=x86_64-pc-windows-gnu
      install:
        - rustup set default-host $TARGET
        - rustup default $TRAVIS_RUST_VERSION
        - rustup target add $TARGET
        - mkdir -p ~/.cargo
        - printf '\n[target.'$TARGET']\nlinker = "x86_64-w64-mingw32-gcc"\nar = "x86_64-w64-mingw32-ar"\n' >> ~/.cargo/config
      script: cargo test --verbose --target $TARGET
    - rust: nightly
      os: windows
      env: TARGET=x86_64-pc-windows-gnu
      install:
        - rustup set default-host $TARGET
        - rustup default $TRAVIS_RUST_VERSION
        - rustup target add $TARGET
        - mkdir -p ~/.cargo
        - printf '\n[target.'$TARGET']\nlinker = "x86_64-w64-mingw32-gcc"\nar = "x86_64-w64-mingw32-ar"\n' >> ~/.cargo/config
      script: cargo test --verbose --target $TARGET

不過由於使用musl時很有可能會使用到musl-gcc,所以如果需要的話還要先安裝musl-tools這個Linux套件。另外,許多套件在需要使用C編譯器來編譯程式的時候,會去讀取CC環境變數,但Travis CI的Windows環境,預設並不會去設定CC環境變數,所以很多套件就會去使用通用的cc指令來編譯C語言程式,然而,Travis CI的Windows環境也沒有cc指令可以用,也就是說,這時候我們就得透過設定CC環境變數來設定要使用的C語言編譯器。在編譯目標為x86_64-pc-windows-gnu時,建議就使用gcc編譯器作為C語言編譯器,正好Travis CI的Windows環境就有提供gcc指令。

此時的完整.travis.yml設定檔寫法如下:

language: rust

rust:
  - stable
  - beta
  - nightly

os:
  - linux
  - osx
  - windows

matrix:
  include:
    - rust: stable
      os: linux
      env: TARGET=x86_64-unknown-linux-musl
      install:
        - sudo apt update
        - sudo apt install musl-tools
        - rustup target add $TARGET
      script: cargo test --verbose --target $TARGET
    - rust: beta
      os: linux
      env: TARGET=x86_64-unknown-linux-musl
      install:
        - sudo apt update
        - sudo apt install musl-tools
        - rustup target add $TARGET
      script: cargo test --verbose --target $TARGET
    - rust: nightly
      os: linux
      env: TARGET=x86_64-unknown-linux-musl
      install:
        - sudo apt update
        - sudo apt install musl-tools
        - rustup target add $TARGET
      script: cargo test --verbose --target $TARGET
    - rust: stable
      os: windows
      env: TARGET=x86_64-pc-windows-gnu CC=gcc
      install:
        - rustup set default-host $TARGET
        - rustup default $TRAVIS_RUST_VERSION
        - rustup target add $TARGET
        - mkdir -p ~/.cargo
        - printf '\n[target.'$TARGET']\nlinker = "x86_64-w64-mingw32-gcc"\nar = "x86_64-w64-mingw32-ar"\n' >> ~/.cargo/config
      script: cargo test --verbose --target $TARGET
    - rust: beta
      os: windows
      env: TARGET=x86_64-pc-windows-gnu CC=gcc
      install:
        - rustup set default-host $TARGET
        - rustup default $TRAVIS_RUST_VERSION
        - rustup target add $TARGET
        - mkdir -p ~/.cargo
        - printf '\n[target.'$TARGET']\nlinker = "x86_64-w64-mingw32-gcc"\nar = "x86_64-w64-mingw32-ar"\n' >> ~/.cargo/config
      script: cargo test --verbose --target $TARGET
    - rust: nightly
      os: windows
      env: TARGET=x86_64-pc-windows-gnu CC=gcc
      install:
        - rustup set default-host $TARGET
        - rustup default $TRAVIS_RUST_VERSION
        - rustup target add $TARGET
        - mkdir -p ~/.cargo
        - printf '\n[target.'$TARGET']\nlinker = "x86_64-w64-mingw32-gcc"\nar = "x86_64-w64-mingw32-ar"\n' >> ~/.cargo/config
      script: cargo test --verbose --target $TARGET

允許失敗(Allow Failures)

有些執行環境可能會有高機率使程式專案測試失敗的可能,如果想要讓Travis CI依然運行這些環境的腳本,但是又不想要讓這些環境的腳本運行結果去影響到最終的GitHub倉庫的Travis CI狀態,我們可以在.travis.yml設定檔加上matrix設定項目,再去設定其中的allow_failures設定項目。

這個功能通常會用在Rust的Nightly版本上,例如:

language: rust

rust:
  - stable
  - beta
  - nightly

matrix:
  allow_failures:
    - rust: nightly

不過筆者是不建議這樣用啦,畢竟Rust的Nightly版本經常會被用到,因為Rust有太多好東西都只能在Nightly版本中啟用。所以應該要儘量讓自己的程式專案能夠永遠成功在最新的Rust Nightly版本下測試。

加快Travis CI處理一次commit的速度

快取(Cache)

Travis CI支援很多種程式語言的套件管理工具的快取功能,Cargo也不例外。在.travis.yml設定檔加上cache: cargo這項設定,來啟用Cargo的快取功能。這樣前一次commit時所下載和編譯過的套件就會被快取下來,之後就可以一直重複使用。

language: rust

cache: cargo

不過這個功能可能比較適合用在固定使用相同版本的Rust編譯器時,不然只要Rust版本一有更新,先前快取到的東西就沒什麼用了。

快速結束(Fast Finish)

Travis CI提供快速結束的機制,當這個機制啟用時,Travis CI就不會等到所有任務執行結束後才返回狀態,而是只需要等到所有不在allow_failures中的任務執行結束後即可返回狀態。在.travis.yml設定檔的matrix這個設定項目,加上fast_finish: true這項設定,來啟用快速結束機制。

language: rust

rust:
  - stable
  - beta
  - nightly

matrix:
  allow_failures:
    - rust: nightly
  fast_finish: true

交叉編譯

有關於Rust交叉編譯的方式可以參考這篇文章:

https://magiclen.org/rust-cross-compile/

當然,上面這篇文章介紹的cross也是可以用在Travis CI的。只不過要先讓Travis CI有Docker的環境,可以在.travis.yml設定檔加上services: docker這項設定,來啟用Docker服務。然後再用script設定項目來執行cargo install cross指令

如果想要在x86和x86_64環境下完成程式專案的測試,完整的.travis.yml設定檔寫法如下:

language: rust

services: docker

rust:
  - stable
  - beta
  - nightly

os:
  - linux
  - osx
  - windows

env:
  -
  - TARGET=i686-unknown-linux-gnu
  - TARGET=i686-pc-windows-gnu

script:
  - if [ -z "$TARGET" ]; then cargo test --verbose; fi 
  - if [ ! -z "$TARGET" ]; then cargo install cross && cross test --verbose --target $TARGET; fi

matrix:
  exclude:
    - os: osx
      env: TARGET=i686-unknown-linux-gnu
    - os: osx
      env: TARGET=i686-pc-windows-gnu
    - os: windows
      env: TARGET=i686-unknown-linux-gnu
    - os: windows
      env: TARGET=i686-pc-windows-gnu

注意這邊我們在matrix設定項目中加了exclude,用來排除帶有指定參數的腳本。