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