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:**

**Jenkins job build page view:**

## 开箱即用
**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
${SCHEME}_${CONFIGURATION}_#${BUILD_NUMBER}
true
true