iOS中 為 iOS 建立 Travis CI 韓俊強的博客

分類:IT技術 時間:2016-10-07

每日更新關註:http://weibo.com/hanjunqiang  新浪微博!

你是否曾經試著為 iOS 項目搭建一臺支持持續集成的服務器,從我的個人經驗而言,這可不是一個輕松的活。首先需要準備一臺 Mac 電腦,並安裝好全部所需的軟件和插件。你要負責管理所有的用戶賬戶,並提供安全保護。你需要授予訪問倉庫的權限,並配置所有的編譯步驟和證書。在項目運行時期,你需要保持服務器的穩健和最新。

最後,原本你想節省的時間,會發現你花費了大量的時間去維護這臺服務器。不過如果你的項目托管在 GitHub) 上,現在有了新的希望:Travis CI。該服務可以為你的項目提供持續集成的支持,也就意味著它會負責好托管一個項目的所有細節。在Ruby 的世界中,Travis CI 已久負盛名。從 2013 年 4 月起,Travis 也開始支持 iOS 和 Mac 平臺。

在這篇文章中,我將向你展示如何一步步的在項目中集成 Travis。不僅包括項目的編譯和單元測試的運行,還包括將應用部署到你所有的測試設備上。為了演示,我在 GitHub 上放了一個示例項目。在這篇文章的最後,我會教你一些提示:如何用 Travis 去定位程序中的錯誤。

GitHub 集成

我最喜歡 Travis 的一點就是它與 GitHub 的 Web UI 集成的非常好。例如 pull 請求。Travis 會為每次請求都執行編譯操作。如果一切正常,pull 請求在 GitHub 上看起來就像這樣:

萬一編譯不成功,GitHub 頁面會修改相應的顏色,給予提醒:

每日更新關註:http://weibo.com/hanjunqiang  新浪微博!

鏈接 Travis 和 GitHub

讓我們看一下如何將 GitHub 項目與 Travis 鏈接上。使用 GitHub 賬號登錄 Travis 站點。對於私有倉庫,需要註冊一個 Travis 專業版賬號。

登錄成功後,需要為項目開啟 Travis 支持。導航到屬性頁面,該頁面列出了所有 GitHub 項目。不過要註意,如果你此後創建了一個新的倉庫,要使用

Sync now
按鈕進行同步。Travis 只會偶爾更新你的項目列表。

現在只需要打開這個開關就可以為你的項目添加 Travis 服務。之後你會看到 Travis 會和 GitHub 項目設置相關聯。下一步就是告訴 Travis, 當它收到項目改動通知之後該做什麽。

最簡單的項目配置

Travis CI 需要項目的一些基本信息。在項目的根目錄創建一個名叫

.travis.yml
的文件,文件中的內容如下:

language: objective-c

Travis 編譯器運行在虛擬機環境下。該編譯器已經利用 Ruby,Homebrew,CocoaPods 和一些默認的編譯腳本進行過預配置。上述的配置項已經足夠編譯你的項目了。

預裝的編譯腳本會分析你的 Xcode 項目,並對每個 target 進行編譯。如果所有文件都沒有編譯錯誤,並且測試也沒有被打斷,那麽項目就編譯成功了。現在可以將相關改動 Push 到 GitHub 中看看能否成功編譯。

雖然上述配置過程真的很簡單,不過對你的項目不一定適用。這裏幾乎沒有什麽文檔來指導用戶如何配置默認的編譯行為。例如,有一次我沒有用

iPhonesimulator
SDK 導致代碼簽名錯誤。如果剛剛那個最簡單的配置對你的項目不適用的話,讓我們來看一下如何對 Travis 使用自定義的編譯命令。

自定義編譯命令

Travis 使用命令行對項目進行編譯。因此,第一步就是使項目能夠在本地編譯。作為 Xcode 命令行工具的一部分,Apple 提供了

xcodebuild
命令。

打開終端並輸入:

xcodebuild --help

上述命令會列出

xcodebuild
所有可用的參數。如果命令執行失敗了,確保命令行工具已經成功安裝。一個常見的編譯命令看起來是這樣的:

xcodebuild -project {project}.xcodeproj -target {target} -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO

使用

iphonesimulator
SDK 是為了避免簽名錯誤。直到我們稍後引入證書之前,這一步是必須的。通過設置
ONLY_ACTIVE_ARCH=NO
我們可以確保利用模擬器架構編譯工程。你也可以設置額外的屬性,例如
configuration
,輸入
man xcodebuild
查看相關文檔。

對於使用

CocoaPods
的項目,需要用下面的命令來指定
workspace
scheme

xcodebuild -workspace {workspace}.xcworkspace -scheme {scheme} -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO

schemes 是由 Xcode 自動生成的,但這在服務器上不會發生。確保所有的 scheme 都被設為

shared
並加入到倉庫中。否則它只會在本地工作而不會被 Travis CI 識別。

我們示例項目下的

.travis.yml
文件現在看起來應該像這樣:

language: objective-c
script: xcodebuild -workspace TravisExample.xcworkspace -scheme TravisExample -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO

運行測試

對於測試來說,通常使用如下這個命令 (註意

test
屬性):

xcodebuild test -workspace {workspace}.xcworkspace -scheme {test_scheme} -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO

不幸的是,

xcodebuild
對於 iOS 來說,並不能正確支持 target 和應用程序的測試。這裏有一些解決方案,不過我建議使用 Xctool。

Xctool

Xctool 是來自 Facebook 的命令行工具,它可以簡化程序的編譯和測試。它的彩色輸出信息比

xcodebuild
更加簡潔直觀。同時還添加了對邏輯測試,應用測試的支持。

Travis 中已經預裝了 xctool。要在本地測試的話,需要用 Homebrew 安裝 xctool:

brew update
brew install xctool

xctool 用法非常簡單,它使用的參數跟

xcodebuild
相同:

xctool test -workspace TravisExample.xcworkspace -scheme TravisExampleTests -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO

一旦相關命令在本地能正常工作,那麽就是時候把它們添加到

.travis.yml
中了:

language: objective-c
script:
  - xctool -workspace TravisExample.xcworkspace -scheme TravisExample -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO
  - xctool test -workspace TravisExample.xcworkspace -scheme TravisExampleTests -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO

到此為止,介紹的內容對於使用 Travis 的 library 工程來說,已經足夠了。我們可以確保項目正常編譯並測試通過。但對於 iOS 應用來說,我們希望能在真實的物理設備上進行測試。也就是說我們需要將應用部署到我們的所有測試設備上。當然,我們希望 Travis 能自動完成這項任務。首先,我們需要給程序簽名。

應用程序的簽名

為了在 Travis 中能給程序簽名,我們需要準備好所有必須的證書和配置文件。就像每個 iOS 開發人員知道的那樣,這可能是最困難的一步。後面,我將寫一些腳本在服務器上給應用程序簽名。

每日更新關註:http://weibo.com/hanjunqiang  新浪微博!

證書和配置文件

1. 蘋果全球開發者關系認證

從蘋果官網下載證書,或者從鑰匙串中導出。並將其保存到項目的目錄

scripts/certs/apple.cer
中。

2. iPhone 發布證書 + 私鑰

如果還沒有發布證書的話,先創建一個。登錄蘋果開發者賬號,按照步驟,創建一個新的生產環境證書 (

Certificates
>
Production
>
Add
>
App Store and Ad Hoc
)。然後下載並安裝證書。之後,可以在鑰匙串中找到它。打開 Mac 中的
鑰匙串
應用程序:

右鍵單擊證書,選擇

Export...
將證書導出至
scripts/certs/dist.cer
。然後導出私鑰並保存至
scripts/certs/dist.p12
。記得輸入私鑰的密碼。

由於 Travis 需要知道私鑰密碼,因此我們要把這個密碼存儲在某個地方。當然,我們不希望以明文的形式存儲。我們可以用 Travis 的安全環境變量。打開終端,並定位到包含

.travis.yml
文件所在目錄。首先用
gem install travis
命令安裝 Travis gem。之後,用下面的命令添加密鑰密碼:

travis encrypt "KEY_PASSWORD={password}" --add

上面的命令會安裝一個叫做

KEY_PASSWORD
的加密環境變量到
.travis.yml
配置文件中。這樣就可以在被 Travis CI 執行的腳本中使用這個變量。

3. iOS 配置文件 (發布)

如果還沒有用於發布的配置文件,那麽也創建一個新的。根據開發者賬號類型,可以選擇創建 Ad Hoc 或 In House 配置文件 (

Provisioning Profiles
>
Distribution
>
Add
>
Ad Hoc
or
In House
)。然後將其下載保存至
scripts/profile/
目錄。

由於 Travis 需要訪問這個配置文件,所以我們需要將這個文件的名字存儲為一個全局環境變量。並將其添加至

.travis.yml
文件的全局環境變量 section 中。例如,如果配置文件的名字是
TravisExample_Ad_Hoc.mobileprovision
,那麽按照如下進行添加:

env:
  global:
  - APP_NAME="TravisExample"
  - 'DEVELOPER_NAME="iPhone Distribution: {your_name} ({code})"'
  - PROFILE_NAME="TravisExample_Ad_Hoc"

上面還聲明了兩個環境變量。第三行中的

APP_NAME
通常為項目默認 target 的名字。第四行的
DEVELOPER_NAME
是 Xcode 中,默認 target 裏面
Build Settings
Code Signing Identity
>
Release
對應的名字。然後搜索程序的
Ad Hoc
In House
配置文件,將其中黑體文字取出。根據設置的不同,括弧中可能不會有任何信息。

加密證書和配置文件

如果你的 GitHub 倉庫是公開的,你可能希望對證書和配置文件 (裏面包含了敏感數據) 進行加密。如果你用的是私有倉庫,可以跳至下一節。

首先,我們需要一個密碼來對所有的文件進行加密。在我們的示例中,密碼為 “foo”,記住在你的工程中設置的密碼應該更加復雜。在命令行中,我們使用

openssl
加密所有的敏感文件:

openssl aes-256-cbc -k "foo" -in scripts/profile/TravisExample_Ad_Hoc.mobileprovision -out scripts/profile/TravisExample_Ad_Hoc.mobileprovision.enc -a
openssl aes-256-cbc -k "foo" -in scripts/certs/dist.cer -out scripts/certs/dist.cer.enc -a
openssl aes-256-cbc -k "foo" -in scripts/certs/dist.p12 -out scripts/certs/dist.cer.p12 -a

通過上面的命令,可以創建出以

.enc
結尾的加密文件。之後可以把原始文件忽略或者移除掉。至少不要把原始文件提交到 GitHub 中,否則原始文件會顯示在 GitHub 中。如果你不小心把原始文件提交上去了,那麽請看這裏如何解決。

現在,我們的文件已經被加密了,接下來需要告訴 Travis 對文件進行解密。解密過程,需要用到密碼。具體使用方法跟之前創建的

KEY_PASSWORD
變量一樣:

travis encrypt "ENCRYPTION_SECRET=foo" --add

最後,我們需要告訴 Travis 哪些文件需要進行解密。將下面的命令添加到

.travis.yml
文件中的
before-script
部分:

before_script:
- openssl aes-256-cbc -k "$ENCRYPTION_SECRET" -in scripts/profile/TravisExample_Ad_Hoc.mobileprovision.enc -d -a -out scripts/profile/TravisExample_Ad_Hoc.mobileprovision
- openssl aes-256-cbc -k "$ENCRYPTION_SECRET" -in scripts/certs/dist.p12.enc -d -a -out scripts/certs/dist.p12
- openssl aes-256-cbc -k "$ENCRYPTION_SECRET" -in scripts/certs/dist.p12.enc -d -a -out scripts/certs/dist.p12

就這樣,在 GitHub 上面的文件就安全了,並且 Travis 依舊能讀取並使用這些加密後的文件。但是有一個安全問題你需要知道:在 Travis 的編譯日誌中可能會顯示出解密環境變量。不過對 pull 請求來說不會出現。

添加腳本

現在我們需要確保證書都導入至 Travis CI 的鑰匙串中。為此,我們需要在

scripts
文件夾中添加一個名為
add-key.sh
的文件:

#!/bin/sh
security create-keychain -p travis ios-build.keychain
security import ./scripts/certs/apple.cer -k ~/Library/Keychains/ios-build.keychain -T /usr/bin/codesign
security import ./scripts/certs/dist.cer -k ~/Library/Keychains/ios-build.keychain -T /usr/bin/codesign
security import ./scripts/certs/dist.p12 -k ~/Library/Keychains/ios-build.keychain -P $KEY_PASSWORD -T /usr/bin/codesign
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp ./scripts/profile/$PROFILE_NAME.mobileprovision ~/Library/MobileDevice/Provisioning\ Profiles/

通過上面的命令創建了一個名為

ios-build
的臨時鑰匙串,裏面包含了所有證書。註意,這裏我們使用了
$KEY_PASSWORD
來導入私鑰。最後一步是將配置文件拷貝至
Library
文件夾。

創建好文件之後,確保給其授予了可執行的權限:在命令行輸入:

chmod a+x scripts/add-key.sh
即可。為了正常使用腳本,必須要這樣處理一下。

至此,已經導入了所有的證書和配置文件,我們可以開始給應用程序簽名了。註意,在給程序簽名之前必須對程序進行編譯。由於我們需要知道編譯結果存儲在磁盤的具體位置,我建議在編譯命令中使用

OBJROOT
SYMROOT
來指定輸出目錄。另外,為了創建 release 版本,還需要把 SDK 設置為
iphoneos
,以及將 configuration 修改為
Release

xctool -workspace TravisExample.xcworkspace -scheme TravisExample -sdk iphoneos -configuration Release OBJROOT=$PWD/build SYMROOT=$PWD/build ONLY_ACTIVE_ARCH=NO 'CODE_SIGN_RESOURCE_RULES_PATH=$(SDKROOT)/ResourceRules.plist'

如果運行了上面的命令,那麽編譯完成之後,可以在

build/Release-iphoneos
目錄找到應用程序的二進制文件。接下來,就可以對其簽名,並創建
IPA
文件了。為此,我們創建一個新的腳本:

#!/bin/sh
if [[ "$TRAVIS_PULL_REQUEST" != "false" ]]; then
  echo "This is a pull request. No deployment will be done."
  exit 0
fi
if [[ "$TRAVIS_BRANCH" != "master" ]]; then
  echo "Testing on a branch other than master. No deployment will be done."
  exit 0
fi

PROVISIONING_PROFILE="$HOME/Library/MobileDevice/Provisioning Profiles/$PROFILE_NAME.mobileprovision"
OUTPUTDIR="$PWD/build/Release-iphoneos"

xcrun -log -sdk iphoneos packageApplication "$OUTPUTDIR/$APPNAME.app" -o "$OUTPUTDIR/$APPNAME.ipa" -sign "$DEVELOPER_NAME" -embed "$PROVISIONING_PROFILE"

第二行至第九行非常重要。我們並不希望在某個特性分支上創建新的 release。對 pull 請求也一樣的。由於安全環境變量被禁用,所以 pull 請求也不會編譯。

第十四行,才是真正的簽名操作。這個命令會在

build/Release-iphoneos
目錄生成 2 個文件:
TravisExample.ipa
TravisExample.app.dsym
。第一個文件包含了分發至手機上的應用程序。
dsym
文件包含了二進制文件的調試信息。這個文件對於記錄設備上的 crash 信息非常重要。之後當我們部署應用程序的時候,會用到這兩個文件。

最後一個腳本是移除之前創建的臨時鑰匙串,並刪除配置文件。雖然這不是必須的,不過這有助於進行本地測試。

#!/bin/sh
security delete-keychain ios-build.keychain
rm -f ~/Library/MobileDevice/Provisioning\ Profiles/$PROFILE_NAME.mobileprovision

最後一步,我們必須告訴 Travis 什麽時候執行這三個腳本。在應用程序編譯、簽名和清除等之前,需要先添加私鑰。在

.travis.yml
文件中添加如下內容:

before_script:
- ./scripts/add-key.sh
- ./scripts/update-bundle.sh
script:
- xctool -workspace TravisExample.xcworkspace -scheme TravisExample -sdk iphoneos -configuration Release OBJROOT=$PWD/build SYMROOT=$PWD/build ONLY_ACTIVE_ARCH=NO
after_success:
- ./scripts/sign-and-upload.sh
after_script:
- ./scripts/remove-key.sh

完成上面的所有操作之後,我們就可以將所有內容 push 到 GitHub 上,等待 Travis 對應用程序進行簽名。我們可以在工程頁面下的 Travis 控制臺驗證是否一切正常。如果一切正常的話,下面來看看如何將簽好名的應用程序部署給測試人員。

每日更新關註:http://weibo.com/hanjunqiang  新浪微博!

部署應用程序

這裏有兩個知名的服務可以幫助你發布應用程序:TestFlight 和HockeyApp。不管選擇哪個都能夠滿足需求。就我個人來說,推薦使用 HockeyApp,不過這裏我會對這兩個服務都做介紹。

首先我們對

sign-and-build.sh
腳本做一個擴充 -- 在裏面添加一些 release 記錄:

RELEASE_DATE=`date '+%Y-%m-%d %H:%M:%S'`
RELEASE_NOTES="Build: $TRAVIS_BUILD_NUMBER\nUploaded: $RELEASE_DATE"

註意這裏使用了一個 Travis 的全局變量

TRAVIS_BUILD_NUMBER

TestFlight

創建一個 TestFlight 賬號,並配置好應用程序。為了使用 TestFlight 的 API,首先需要獲得apitoken 和teamtoken。再強調一下,我們需要確保它們是加密的。在命令行中執行如下命令:

travis encrypt "TESTFLIGHT_API_TOKEN={api_token}" --add
travis encrypt "TESTFLIGHT_TEAM_TOKEN={team_token}" --add

現在我們可以調用相應的 API 了。並將下面的內容添加到

sign-and-build.sh
:

curl http://testflightapp.com/api/builds.json \
  -F file="@$OUTPUTDIR/$APPNAME.ipa" \
  -F dsym="@$OUTPUTDIR/$APPNAME.app.dSYM.zip" \
  -F api_token="$TESTFLIGHT_API_TOKEN" \
  -F team_token="$TESTFLIGHT_TEAM_TOKEN" \
  -F distribution_lists='Internal' \
  -F notes="$RELEASE_NOTES"

千萬不要使用 verbose 標記 (

-v
) -- 這會暴露加密 tokens。

HockeyApp

註冊一個 HockeyApp 賬號,並創建一個新的應用程序。然後在概述頁面獲取一個

App ID
。接下來,我們必須創建一個 API token。打開這個頁面,並創建一個。如果你希望自動的將新版本部署給所有的測試人員,那麽請選擇
Full
 Access
版本。

對 App ID 和 token 進行加密:

travis encrypt "HOCKEY_APP_ID={app_id}" --add
travis encrypt "HOCKEY_APP_TOKEN={api_token}" --add

然後在

sign-and-build.sh
文件中調用相關的 API:

curl https://rink.hockeyapp.net/api/2/apps/$HOCKEY_APP_ID/app_versions \
  -F status="2" \
  -F notify="0" \
  -F notes="$RELEASE_NOTES" \
  -F notes_type="0" \
  -F ipa="@$OUTPUTDIR/$APPNAME.ipa" \
  -F dsym="@$OUTPUTDIR/$APPNAME.app.dSYM.zip" \
  -H "X-HockeyAppToken: $HOCKEY_APP_TOKEN"

註意我們還上傳了

dsym
文件。如果集成了 TestFlight 或 HockeyApp SDK,可以立即收集到易讀的 crash 報告。

Travis 故障排除

使用 Travis 一個月以來,並不總是那麽順暢。知道如何不通過直接訪問編譯環境就能找出問題是非常重要的。

在寫本文的時候,還沒有可以下載的虛擬機映像 (VM images) 。如果 Travis 不能正常編譯,首先試著在本地重現問題。在本地執行跟 Travis 相同的編譯命令:

xctool ...

為了調試 shell 腳本,首先需要定義環境變量。我的做法是創建一個新的 shell 腳本來設置所有的環境變量。記得將這個腳本添加到

.gitignore
文件中 -- 因為我們並不希望將該文件公開暴露出去。針對示例工程來說,
config.sh
腳本文件看起來是這樣的:

#!/bin/bash

# Standard app config
export APP_NAME=TravisExample
export DEVELOPER_NAME=iPhone Distribution: Mattes Groeger
export PROFILE_NAME=TravisExample_Ad_Hoc
export INFO_PLIST=TravisExample/TravisExample-Info.plist
export BUNDLE_DISPLAY_NAME=Travis Example CI

# Edit this for local testing only, DON'T COMMIT it:
export ENCRYPTION_SECRET=...
export KEY_PASSWORD=...
export TESTFLIGHT_API_TOKEN=...
export TESTFLIGHT_TEAM_TOKEN=...
export HOCKEY_APP_ID=...
export HOCKEY_APP_TOKEN=...

# This just emulates Travis vars locally
export TRAVIS_PULL_REQUEST=false
export TRAVIS_BRANCH=master
export TRAVIS_BUILD_NUMBER=0

為了暴露出所有的環境變量,執行如下命令(確保

config.sh
是可執行的):

. ./config.sh

然後試著運行

echo $APP_NAME
,以此檢查腳本是否正確。如果正確的話,那麽現在我們不用做任何修改,就能在本地運行所有的 shell 腳本了。

如果在本地得到的是不同的編譯信息,那麽可能是使用了不同的庫和 gems。盡量試著將配置信息設置為與 Travis VM 相同的信息。Travis 在這裏列出了其所有安裝的軟件版本。你也可以在 Travis 的配置文件中添加調試信息得到所有庫文件的版本:

gem cocoapod --version
brew --version
xctool -version
xcodebuild -version -sdk

在本地安裝好與服務器完全相同的軟件之後,再重新編譯項目。

如果獲取到的編譯信息仍然不一樣,試著將項目 check out 到一個新的目錄。並確保所有的緩存都已清空。每次編譯程序時,Travis 都會創建一個全新的虛擬機,所以不存在緩存的問題,但在你的本地機器上可能會出現。

一旦在本地重現出和服務器上相同的錯誤,就可以開始調查具體問題了。當然導致問題的原因取決於具體問題。一般來說,通過 Google 都能找到引起問題的根源。

如果一個問題影響到了 Travis 上其它的項目,那麽可能是 Travis 環境配置的原因。我曾經遇到過幾次這樣的問題 (特別是剛開始時)。如果發生這樣的情況試著聯系 Travis,取得支持,以我的經驗來說,他們的響應非常迅速。

每日更新關註:http://weibo.com/hanjunqiang  新浪微博!

點評

Travis CI 跟市面上同類產品相比還是有一些限制。因為 Travis 運行在一個預先配置好的虛擬機上,因此必須為每次編譯都安裝一遍所有的依賴。這會花費一些額外的時間。不過 Travis 團隊已經在著手提供一種緩存機制解決這個問題了。

在一定程度上,你會依賴於 Travis 所提供的配置。比如你只能使用 Travis 內置的 Xcode 版本進行編譯。如果你本地使用的 Xcode 版本較新,你的項目在服務器上可能無法編譯通過。如果 Travis 能夠為不同的 Xcode 版本都分別設置一個對應虛擬機會就好了。

對於復雜的項目來說,你可能希望把整個編譯任務分為編譯應用,運行集成測試等等。這樣你可以快速獲得編譯信息而不用等所有的測試都完成。目前 Travis 還沒有直接支持有依賴的編譯。

當項目被 push 到 GitHub 上時,Travis 會自動觸發。不過編譯動作不會立即觸發,你的項目會被放到一個根據項目所用語言不同而不同的一個全局編譯隊列,不過專業版允許並發編譯。

總結

Travis CI 提供了一個功能完整的持續集成環境,以進行應用程序的編譯、測試和部署。對於開源項目來說,這項服務是完全免費的。很多社區項目都得益於 GitHub 強大的持續集成能力。你可能已經看過如下這樣的按鈕:

對於商業項目,Travis 專業版也能為私有倉庫提供快捷、簡便的持續集成支持。

如果你還沒有用過 Travis,趕緊去試試吧,它棒極了!

更多鏈接

  • 示例工程
  • Travis CI
  • Travis CI 專業版
  • Xctool
  • HockeyApp
  • TestFlight

  • 每日更新關註:http://weibo.com/hanjunqiang  新浪微博!
  •  

Tags: 新浪微博 服務器 文章 博客 倉庫

文章來源:


ads
ads

相關文章
ads

相關文章

ad