Repository: debugtalk/JenkinsTemplateForApp Branch: master Commit: 9b849d425229 Files: 8 Total size: 19.6 KB Directory structure: gitextract_7dav07gs/ ├── .gitignore ├── LICENSE ├── README.md ├── build_scripts/ │ ├── build.py │ ├── ios_builder.py │ ├── pgyer_uploader.py │ └── requirements.txt └── jobs/ └── job_template/ └── config.xml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # IPython Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # dotenv .env # virtualenv venv/ ENV/ # Spyder project settings .spyderproject # Rope project settings .ropeproject # Mac OS .DS_Store ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2016 Leo Lee Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ ## Overview **Jenkins job overview:** ![](images/Jenkins_Job_Overview.jpg) **Jenkins job build page view:** ![](images/Jenkins_Job_Build_View.jpg) ## 开箱即用 **1,添加构建脚本;** - 在构建脚本中配置`PROVISIONING_PROFILE`和`pgyer/fir.im`账号; - 将`build_scripts`文件夹及其文件拷贝至目标构建代码库的根目录下; - 将`build_scripts`提交到项目的仓库中。 除了与Jenkins实现持续集成,构建脚本还可单独使用,使用方式如下: ```bash $ python ${WORKSPACE}/build_scripts/build.py \ --scheme ${SCHEME} \ --workspace ${WORKSPACE}/Store.xcworkspace \ --sdk ${SDK} --configuration ${CONFIGURATION} \ --output_folder ${WORKSPACE}/${OUTPUT_FOLDER} ``` 需要特别说明的是,若要构建生成可在移动设备中运行的`.ipa`文件,则要将`${SDK}`设置为`iphoneos`;若要构建生成可在模拟器中运行的`.app`文件,则要将`${SDK}`设置为`iphonesimulator`。 **2、运行jenkins,安装必备插件;** ```bash $ nohup java -jar jenkins_located_path/jenkins.war & ``` **3、创建Jenkins Job;** - 在Jenkins中创建一个`Freestyle project`类型的Job,先不进行任何配置; - 然后将`config.xml`文件拷贝到`~/.jenkins/jobs/YourProject/`中覆盖原有配置文件,重启Jenkins; - 完成配置文件替换和重启后,刚创建好的Job就已完成了大部分配置; - 在`Job Configure`中根据项目实际情况调整配置,其中`Git Repositories`是必须修改的,其它配置项可选择性地进行调整。 **4、done!** ## Read More ... - [《使用Jenkins搭建iOS/Android持续集成打包平台》](http://debugtalk.com/post/iOS-Android-Packing-with-Jenkins) - [《关于持续集成打包平台的Jenkins配置和构建脚本实现细节》](http://debugtalk.com/post/iOS-Android-Packing-with-Jenkins-details) - 微信公众号:[DebugTalk](http://debugtalk.com/images/wechat_qrcode.png) ================================================ FILE: build_scripts/build.py ================================================ #coding=utf-8 from __future__ import print_function import os import sys import argparse from ios_builder import iOSBuilder from pgyer_uploader import uploadIpaToPgyer from pgyer_uploader import saveQRCodeImage def parse_args(): parser = argparse.ArgumentParser(description='iOS app build script.') parser.add_argument('--build_method', dest="build_method", default='xcodebuild', help="Specify build method, xctool or xcodebuild.") parser.add_argument('--workspace', dest="workspace", default=None, help="Build the workspace name.xcworkspace") parser.add_argument("--scheme", dest="scheme", default=None, help="Build the scheme specified by schemename. \ Required if building a workspace") parser.add_argument("--project", dest="project", default=None, help="Build the project name.xcodeproj") parser.add_argument("--target", dest="target", default=None, help="Build the target specified by targetname. \ Required if building a project") parser.add_argument("--sdk", dest="sdk", default='iphoneos', help="Specify build SDK, iphoneos or iphonesimulator, \ default is iphonesimulator") parser.add_argument("--build_version", dest="build_version", default='1.0.0.1', help="Specify build version number") parser.add_argument("--provisioning_profile", dest="provisioning_profile", default=None, help="specify provisioning profile") parser.add_argument("--plist_path", dest="plist_path", default=None, help="Specify build plist path") parser.add_argument("--configuration", dest="configuration", default='Release', help="Specify build configuration, Release or Debug, \ default value is Release") parser.add_argument("--output_folder", dest="output_folder", default='BuildProducts', help="specify output_folder folder name") parser.add_argument("--update_description", dest="update_description", help="specify update description") args = parser.parse_args() print("args: {}".format(args)) return args def main(): args = parse_args() if args.plist_path is None: plist_file_name = '%s-Info.plist' % args.scheme args.plist_path = os.path.abspath( os.path.join(os.path.dirname(__file__), os.path.pardir, plist_file_name ) ) ios_builder = iOSBuilder(args) if args.sdk.startswith("iphonesimulator"): app_zip_path = ios_builder.build_app() print("app_zip_path: {}".format(app_zip_path)) sys.exit(0) ipa_path = ios_builder.build_ipa() app_download_page_url = uploadIpaToPgyer(ipa_path, args.update_description) try: output_folder = os.path.dirname(ipa_path) saveQRCodeImage(app_download_page_url, output_folder) except Exception as e: print("Exception occured: {}".format(str(e))) if __name__ == '__main__': main() ================================================ FILE: build_scripts/ios_builder.py ================================================ #coding=utf-8 from __future__ import print_function import os import subprocess import shutil import plistlib class iOSBuilder(object): """docstring for iOSBuilder""" def __init__(self, options): self._build_method = options.build_method self._sdk = options.sdk self._configuration = options.configuration self._provisioning_profile = options.provisioning_profile self._output_folder = options.output_folder self._plist_path = options.plist_path self._build_version = options.build_version self._archive_path = None self._build_params = self._get_build_params( options.project, options.target, options.workspace, options.scheme) self._prepare() def _prepare(self): """ get prepared for building. """ self._change_build_version() print("Output folder for ipa ============== {}".format(self._output_folder)) try: shutil.rmtree(self._output_folder) except OSError: pass finally: os.makedirs(self._output_folder) self._udpate_pod_dependencies() self._build_clean() def _udpate_pod_dependencies(self): podfile = os.path.join(os.getcwd(), 'Podfile') podfile_lock = os.path.join(os.getcwd(), 'Podfile.lock') if os.path.isfile(podfile) or os.path.isfile(podfile_lock): print("Update pod dependencies =============") cmd_shell = 'pod repo update' self._run_shell(cmd_shell) print("Install pod dependencies =============") cmd_shell = 'pod install' self._run_shell(cmd_shell) def _change_build_version(self): """ set CFBundleVersion and CFBundleShortVersionString. """ build_version_list = self._build_version.split('.') cf_bundle_short_version_string = '.'.join(build_version_list[:3]) with open(self._plist_path, 'rb') as fp: plist_content = plistlib.load(fp) plist_content['CFBundleShortVersionString'] = cf_bundle_short_version_string plist_content['CFBundleVersion'] = self._build_version with open(self._plist_path, 'wb') as fp: plistlib.dump(plist_content, fp) def _run_shell(self, cmd_shell): process = subprocess.Popen(cmd_shell, shell=True) process.wait() return_code = process.returncode assert return_code == 0 def _get_build_params(self, project, target, workspace, scheme): if project is None and workspace is None: raise "project and workspace should not both be None." elif project is not None: build_params = '-project %s -scheme %s' % (project, scheme) # specify package name self._package_name = "{0}_{1}".format(scheme, self._configuration) self._app_name = scheme elif workspace is not None: build_params = '-workspace %s -scheme %s' % (workspace, scheme) # specify package name self._package_name = "{0}_{1}".format(scheme, self._configuration) self._app_name = scheme build_params += ' -sdk %s -configuration %s' % (self._sdk, self._configuration) return build_params def _build_clean(self): cmd_shell = '{0} {1} clean'.format(self._build_method, self._build_params) print("build clean ============= {}".format(cmd_shell)) self._run_shell(cmd_shell) def _build_archive(self): """ specify output xcarchive location """ self._archive_path = os.path.join( self._output_folder, "{}.xcarchive".format(self._package_name)) cmd_shell = '{0} {1} archive -archivePath {2}'.format( self._build_method, self._build_params, self._archive_path) print("build archive ============= {}".format(cmd_shell)) self._run_shell(cmd_shell) def _export_ipa(self): """ export archive to ipa file, return ipa location """ if self._provisioning_profile is None: raise "provisioning profile should not be None!" ipa_path = os.path.join(self._output_folder, "{}.ipa".format(self._package_name)) cmd_shell = 'xcodebuild -exportArchive -archivePath {}'.format(self._archive_path) cmd_shell += ' -exportPath {}'.format(ipa_path) cmd_shell += ' -exportFormat ipa' cmd_shell += ' -exportProvisioningProfile "{}"'.format(self._provisioning_profile) print("build archive ============= {}".format(cmd_shell)) self._run_shell(cmd_shell) return ipa_path def build_ipa(self): """ build ipa file for iOS device """ self._build_archive() ipa_path = self._export_ipa() return ipa_path def _build_archive_for_simulator(self): cmd_shell = '{0} {1} -derivedDataPath {2}'.format( self._build_method, self._build_params, self._output_folder) print("build archive for simulator ============= {}".format(cmd_shell)) self._run_shell(cmd_shell) def _archive_app_to_zip(self): app_path = os.path.join( self._output_folder, "Build", "Products", "{0}-iphonesimulator".format(self._configuration), "{0}.app".format(self._app_name) ) app_zip_filename = os.path.basename(app_path) + ".zip" app_zip_path = os.path.join(self._output_folder, app_zip_filename) cmd_shell = "zip -r {0} {1}".format(app_zip_path, app_path) self._run_shell(cmd_shell) print("app_zip_path: %s" % app_zip_path) return app_zip_path def build_app(self): """ build app file for iOS simulator """ self._build_archive_for_simulator() app_zip_path = self._archive_app_to_zip() return app_zip_path ================================================ FILE: build_scripts/pgyer_uploader.py ================================================ #coding=utf-8 from __future__ import print_function import os import requests import time import re from datetime import datetime # configuration for pgyer USER_KEY = "9667e5933d************540b83ed7c" API_KEY = "d2e517468e7************e24310b65" PGYER_UPLOAD_URL = "https://www.pgyer.com/apiv1/app/upload" def parseUploadResult(jsonResult): print('post response: %s' % jsonResult) resultCode = jsonResult['code'] if resultCode != 0: print("Upload Fail!") raise Exception("Reason: %s" % jsonResult['message']) print("Upload Success") appKey = jsonResult['data']['appKey'] app_download_page_url = "https://www.pgyer.com/%s" % appKey print("appDownloadPage: %s" % app_download_page_url) return app_download_page_url def uploadIpaToPgyer(ipaPath, updateDescription): print("Begin to upload ipa to Pgyer: %s" % ipaPath) headers = {'enctype': 'multipart/form-data'} payload = { 'uKey': USER_KEY, '_api_key': API_KEY, 'publishRange': '2', # 直接发布 'isPublishToPublic': '2', # 不发布到广场 'updateDescription': updateDescription # 版本更新描述 } try_times = 0 while try_times < 5: try: print("uploading ... %s" % datetime.now()) ipa_file = {'file': open(ipaPath, 'rb')} resp = requests.post(PGYER_UPLOAD_URL, headers=headers, files=ipa_file, data=payload) assert resp.status_code == requests.codes.ok result = resp.json() app_download_page_url = parseUploadResult(result) return app_download_page_url except requests.exceptions.ConnectionError: print("requests.exceptions.ConnectionError occured!") time.sleep(60) print("try again ... %s" % datetime.now()) try_times += 1 except Exception as e: print("Exception occured: %s" % str(e)) time.sleep(60) print("try again ... %s" % datetime.now()) try_times += 1 if try_times >= 5: raise Exception("Failed to upload ipa to Pgyer, retried 5 times.") def parseQRCodeImageUrl(app_download_page_url): try_times = 0 while try_times < 3: try: response = requests.get(app_download_page_url) regex = ' false SCHEME scheme configuration of this project StoreCI CONFIGURATION configuration of packing, Release/Debug Release OUTPUT_FOLDER output folder for build artifacts, it is located in workspace/project root dir. build_outputs BRANCH git repository branch NPED_2.6 2 https://github.com/debugtalk/XXXX 483f9c64-3560-4977-a0a7-71509f718fd4 refs/heads/${BRANCH} false false false 120 0 true false false false H H/3 * * * false false python ${WORKSPACE}/Build_scripts/build.py \ --scheme ${SCHEME} \ --workspace ${WORKSPACE}/Store.xcworkspace \ --configuration ${CONFIGURATION} \ --output ${WORKSPACE}/${OUTPUT_FOLDER} appDownloadPage: (.*)$ <img src='${BUILD_URL}artifact/build_outputs/QRCode.png'>\n<a href='\1'>Install Online</a> ${OUTPUT_FOLDER}/*.ipa,${OUTPUT_FOLDER}/QRCode.png,${OUTPUT_FOLDER}/*.xcarchive/Info.plist true true false true true true true