2. Pour quoi faire ?
Développer depuis une autre machine
Tester sur iDevice
Fournir une version récente aux testeurs
3. Java
Jenkins a besoin de java pour tourner
Java n’est plus fourni par Apple pour OSX 10.7
☞ Il faut installer Java
4. Installation de Java
curl -O http://supportdownload.apple.com/
download.info.apple.com/Apple_Support_Area/
Apple_Software_Updates/Mac_OS_X/downloads/
041-1941.20111108.6wtg7/
JavaForMacOSX10.7.dmg
hdiutil attach JavaForMacOSX10.7.dmg
sudo installer -pkg /Volumes/Java For Mac OS X
10.7/JavaForMacOSX10.7.pkg -target /
5. Installation de Jenkins
Téléchargement et installation du dernier paquet
sur la machine cible
curl -O http://jenkins.mirror.isppower.de/
osx/jenkins-1.451.pkg
sudo installer -pkg jenkins-1.451.pkg -
target /
7. Modification du fichier
de configuration
/Library/LaunchDaemons/org.jenkins-ci.plist
☞ Changer <UserName> <daemon> par
<UserName> <jenkins>
Changement de propriétaire pour le home de Jenkins
☞ sudo chown -R jenkins /Users/Shared/Jenkins
8. Lancement de Jenkins
Jenkins est déjà lancé, il faut l’arrêter
☞ sudo launchctl unload -w /Library/
LaunchDaemons/org.jenkins-ci.plist
Puis le relancer
☞ sudo launchctl load -w /Library/
LaunchDaemons/org.jenkins-ci.plist
11. Jenkins
J’ai réussi à faire un job de compilation pour un
projet purement OSX
J’ai réussi à faire un job de compilation d’un
projet iOS pour simulateur
12. Jenkins
La compilation d’un projet iPhoneOS...
Le processus de la JVM est lancé par launchd
et est fils direct de launchd
l’ouverture de la keychain utilisateur n’est pas
compatible avec cette approche
Même en mettant les certificats et clefs privés
dans la keychain system
15. Utiliser l’existant
Puisque nous avons créé un utilisateur jenkins
pour le serveur de build Jenkins, autant
utiliser ce compte pour la suite de nos
aventures
17. Schéma
Jenkins Serveur Git
Échange de clef SSH
Serveur
de build Serveur de
distribution
La clef SSH de jenkins sur la machine de build ne comporte pas de passphrase.
Sur le serveur de distribution jenkins n’a AUCUN rôle administratif.
Il est aussi possible de n’autoriser les connexions sur le serveur de distribution
que par échange de clef (authentification par mot de passe interdite)
18. Schéma
1
2
Jenkins 3 Serveur Git
Serveur 4
de build Serveur de
distribution
1.récupération des sources (ou mises à jour) depuis le dépôt
2.compilation
3.préparation du .ipa, du manifest, du html
4.copie sur le serveur et mise à disposition
19. Étudions tout cela
Je débute en Python, il y a surement
plus simple ou plus «Pythonnesque» de
faire ce script.
En particulier, je n’utilise quasiment pas
les caractéristiques Objet du langage
Nous n’allons pas étudier le script dans l’ordre d’entrée
en action des étapes 1,2,3,4. Nous laisserons 1 et 4
pour fignoler notre script
20. Étape 2 : la compilation
La compilation, c’est le boulot de xcodebuild :
Il a besoin de trouver :
le répertoire .xcodeproj
la target
le SDK
la configuration (Debug, Release, AdHoc...)
21. Appel de xcodebuild
# /usr/bin/xcodebuild -sdk iphone5.0 -project /mon/projet.xcodeproj -
configuration release -target MaTarget
Fastoche non !
22. Sauf que...
xcodebuild a besoin de trouver le certificat de
développement signé par Apple et la clef privée de ce
certificat
Ces deux éléments doivent être placés dans une
keychain
J’ai décidé de mettre cela dans une keychain séparée
Le provisioning profile doit être dans ~/Library/
MobileDevices/Provisioning Profiles
23. Sur la machine de dev
Export du certificat et de la clef privée dans
un fichier .p12
Copie de ce fichier sur la machine de build
Copie du Provisioning Profile sur la machine
de build
24. Sur la machine de build
Pour créer une keychain à la ligne de commande
on utilise security
security create-keychain jenkins.keychain
On donne un mot de passe à cette keychain
Import du fichier .p12 venant de la machine de dev
security import -t cert -f pkcs12 -T /usr/bin/codesign
-T /usr/bin/xcodebuild -P MotDePasse
25. Security
Il y a plein d’options amusantes dans security,
je vous laisse la surprise
26. Retour au script
Pour faire marcher xcodebuild, nous allons
avoir besoin :
d’ouvrir la keychain
de la passer keychain par défaut
27. Security (bis)
C’est toujours avec security que nous allons jouer
security unlock-keychain jenkins.keychain -p MotDePasse
security default-keychain -d user -s jenkins.keychain
J’ai mis cela dans une fonction
Soyez conscient que le mot de passe peut être
intercepté par un simple ps au moment opportun
28. Modèle de fonction
Toutes les fonctions sont bâties sur le même
modèle afin de pouvoir tracer facilement ce
qui se passe
C’est mon côté sysadmin qui cause...
29. openKeychain
def openKeychain(password, keychain):
cmd_array=(["/usr/bin/security","unlock-keychain", "-p", password, keychain])
try:
subprocess.check_call(cmd_array)
except OSError as e:
logger.debug("Error in %s, exiting" % " ".join(cmd_string))
logger.info( "ErrorString == %s ErrorNum == %d" % (e.strerror,e.errno)
writeToSTDERR("Error, please see the log file")
sys.exit(1)
cmd_array=(["/usr/bin/security", "default-keychain","-d","user", "-s", keychain])
try:
subprocess.check_call(cmd_array)
except OSError as e:
logger.debug("Error in %s, exiting" % " ".join(cmd_array))
logger.info( "ErrorString == %s ErrorNum == %d" % (e.strerror,e.errno)
En python l’indentation fait partie de la syntaxe
30. Une fonction pour
xcodebuild
Sur le modèle de la précédente nous pouvons
faire une fonction qui appelle xcodebuild
32. Transformation de .app
en .ipa
L’application générée par xcodebuild n’est
pas exploitable sur un iPhone
Il faut la transformer en .ipa
C’est xcrun avec les bons paramètres qui va
faire cette transformation
(PackageApplication)
33. createIPA
def createIPA(GSDK, workspace, configuration, target, DeveloperName, ProvisioningProfile,targetFolder):
cmd_array=(["/usr/bin/xcrun","-sdk", GSDK, "PackageApplication", "-v"," %s/%s.app" % (APPPath,target),
"-o","%s/%s.ipa"%(targetFolder,target), "-sign", DeveloperName, "-embed",ProvisioningProfile])
try:
subprocess.check_call(cmd_array)
except OSError as e:
logger.debug("Error in %s. Exiting" % " ".join(cmd_array))
logger.info(" Error String == %s Error Num == %d" % (e.strerror, e.errno))
writeToSTDERR("Error, please see the log file")
sys.exit(1)
SDK c’est le nom du SDK nécessaire à la compilation, GSDK c’est la même chose sans le
numéro de release (iphoneos5.0 -> iphoneos)
34. Manifest.plist
Le fichier manifest.plist est nécessaire à la
distribution OTA
Les informations nécessaires sont déjà dans
l’application .ipa, autant aller les chercher là.
Les .ipa sont des fichiers zip
Les informations sont dans le fichier Info.plist
Le fichier Info.plist est une plist binaire
35. Manifest.plist
Nous n’allons pas détailler les fonctions mais
retrieveInfo extrait le fichier Info.plist de
l’application .ipa
createManifest transforme ce Info.plist binaire
en plist texte, récolte les informations
nécessaires (CFBundleName,
CFBundleVersion, CFBundleIdentifier) et
génère le fichier manifest grâce au module
plistlib
36. createIndexHTML &
fillHTML
Ces deux fonctions permettent de créer le
fichier index.html qui va accompagner vos
fichiers manifest.plist et MonApplication.ipa
pour la distribution OTA
Ce code et celui de la génération du manifest sont largement inspirés voire presque
totalement copiés de celui-ci <https://github.com/baz/ios-build-scripts/blob/master/
generate_manifest.py>.
37. La suite
Votre utilisateur jenkins va faire tourner le
script sur la machine de build
Déposez sur le serveur de distribution la clef
publique de cet utilisateur afin de permettre
l’envoi des éléments
Pour faciliter l’échange, la clef de l’utilisateur n’a pas de mot de passe. En revanche,
sur la machine de distribution, il n’a aucun pouvoir. Pas de sudoers entre autre
38. Récupération des
sources
Python dispose d’un module de gestion git
Il vous permet de faire le clone initial et
ensuite à chaque lancement de faire une mise
à jour des sources
39. Et ensuite ?
Les méthodes d’échange et de gestion de git
sont présentes dans le script final
Dans ce même script, grâce au module
argparse, nous pouvons gérer les arguments
passés au script
40. xcodebuild-wrapper.py
./xcodebuild-wrapper.py -h
usage: xcodebuild-wrapper.py [-h] -k KEYCHAIN -K KEYCHAINPASSWORD -p PROJECT
-P PROVISIONINGPROFILE -s SDK -c CONFIG -n DEVNAME
-t TARGET -d DEPLOY [-r REMOTEHOST] [-u USERNAME]
[-w REMOTEPASSWORD] [-f REMOTEFOLDER]
[--log LOGLEVEL]
xcodebuild wrapper parameters
optional arguments:
-h, --help show this help message and exit
-k KEYCHAIN, --keychain KEYCHAIN
Path to the keychain file
-K KEYCHAINPASSWORD, --keychainPassword KEYCHAINPASSWORD
keychain's password
-p PROJECT, --projectPath PROJECT
Path to project file
-P PROVISIONPROFILE, --provisioningProfile PROVISIONPROFILE
Provisioning Profile path
-s SDK, --sdk SDK SDK
-c CONFIG, --configuration CONFIG
Configuration
-n DEVNAME, --developerName DEVNAME
Developer name. This information is in the
provisioning profile
-t TARGET, --target TARGET
Path to projet's file
-d DEPLOY, --deploymentAddress DEPLOY
Deployment Address, used in manifest.plist file
-r REMOTEHOST, --remoteHost REMOTEHOST
Remote host, used to distribute IPA
-u USERNAME, --username USERNAME
Username on the remote host
-w REMOTEPASSWORD, --remotePassword REMOTEPASSWORD
Password of the username on the remote host
-f REMOTEFOLDER, --remoteFolder REMOTEFOLDER
Destination folder on remote host
--log LOGLEVEL LogLevel, could be DEBUG | INFO | WARNING | ERROR |
CRITICAL. Default value is INFO
41. Améliorations
Pour éviter de passer le mot de passe de la
keychain, le script pourrait utiliser un fichier de
paramètres
Lancer le script automatiquement
Envoi de mail aux testeurs pour signaler une
nouvelle version
…
45. Question
Et la version du build ?
J’ai trouvé une solution à cette URL et je l’ai
adaptée dans mon script
http://blog.jayway.com/2011/05/31/auto-
incrementing-build-numbers-in-xcode/
46. Info.plist
Ajoutons dans le ficher MonApplication-
Info.plist une entrée CWBuildNumber
Incrémentons cette valeur au moment des
build OTA (dans le script Python)
Lisons cette information au moment de
l’affichage du viewController info/about
47. Le code Objective-C
int CWBuildNumber = [[[NSBundle mainBundle]
objectForInfoDictionaryKey:@"CWBuildNumber"] intValue];
NSString *CFBundleVersion = [[NSBundle mainBundle]
objectForInfoDictionaryKey:@"CFBundleVersion"];
NSString *CFBundleName = [[NSBundle mainBundle]
objectForInfoDictionaryKey:@"CFBundleName"];
NSString *aboutString = [NSString stringWithFormat:@"%@, version %@, (Build Number
%d)", CFBundleName, CFBundleVersion, CWBuildNumber];
aboutLibelle.text = aboutString;
48. L’ajout de la clef dans
MyAPP-info.plist
/usr/libexec/PlistBuddy -c "Set :CWBuildNumber $buildNumber"
${PROJECT_DIR}/MyApp-Info.plist
49. La fonction dans
xcodebuild-wrapper.py
def increaseBuildNumber(project,subdir,plistFile):
! cmd_array=["/usr/libexec/PlistBuddy", "-c", "Print CWBuildNumber", "%s/%s/%s" %
(project,subdir,plistFile)]
! s = subprocess.Popen(cmd_array, stdout=subprocess.PIPE)
! ret = s.stdout.readline()
! buildNumber = int(ret)
! buildNumber+=1
! cmd_array=["/usr/libexec/PlistBuddy", "-c", "Set :CWBuildNumber %s" %
(buildNumber),"%s/%s/%s" % (project,subdir,plistFile)]
! try:
! ! subprocess.check_call(cmd_array)
! except OSError as e:
! ! logger.debug("Error in %s. Exiting"% " ".join(cmd_array))
! ! logger.debug("ErrorString == %s ErrorNum == %d" % (e.strerror,e.errno))
! ! writeToSTDERR("Error during increase BuildNumber")
50. Conclusion
J’ai mis deux semaines à écrire ce script
Il est dispo avec beaucoup plus d’explications
sur http://adminblog.foucry.net
Merci à Frank, Dominique et Benoît pour leur
aide et leur relecture
51. Le script est en creative commons
et il sera sur github
adminblog.foucry.net
jacques@foucry.net