
Note for english-speaking readers: Since I already had created this tutorial for a school project, this article is written in German. If you would like me to translate it to English, just drop me a line or give Google Translate a shot.
Dieser Blog Eintrag Beschreibt den Ablauf des Server Setups für die Continuous Integration sowie die Versionsverwaltung von iOS Projekten. Zu diesem Zweck muss ein OS X Betriebssysytem eingesetzt werden welches Objective-C bzw. Cocoa Applikationen kompilieren kann. Für dieses Tutorial wurde eine Mac OS X Server 10.7 Installation inklusive Xcode 4.2 mit iOS 5 verwendet.
Mac Ports Installation
Der einfachste Weg Software unter OS X zu installieren, ist es ein Paket Management System zu verwenden. Unter OS X empfiehlt es sich das Paket Managment System «Mac Ports» zu installieren. Einen Installer dazu finden Sie unter der folgenden URL: http://www.macports.org/install.php. Danach können Software Pakete mit folgendem Befehl installiert werden:
SSH Setup
Da wir für die Authentifizierung unter Git SSH einsetzen muss dies ebenfalls eingerichtet werden. Unter OS X gestaltet sich dies sehr einfach. Es muss lediglich in den Systemeinstellungen unter dem Punkt “Freigaben” ein Haken bei “Entfernte Anmeldung” gesetzt werden.
Git
Für die Versionsverwaltung wird Git eingesetzt. Diese muss auf dem Server erst einmal installiert werden. Da es keine gesonderte Server Version von git gibt kann im Prinzip jeder Computer welcher den Git Client installiert hat auch als Server dienen.
“Es gibt keine spezielle Version von Git für den Server (oder den Client), da jeder pull aus einem Repository das komplette Repository dupliziert, kann jedes Team-Mitglieder einen Server bereit stellen.”[
1]
Authentifizierung
“Im folgenden wird die Authentifizierung über SSH beschrieben: Alle Teammitglieder werden auf ein bzw. mehrere Repositories mit Schreibrechten Zugriff haben, alle anderen haben keinen Zugriff.
Wir setzen das Python Script gitosis für die Verwaltung der SSH Zugänge ein. Das besondere an gitosis ist, dass alle Zugänge nur einen einzigen Systemaccount benötigen. Ein Shell-Login über diesen globalen Account wird den Team-Mitgliedern auf dem Server selbst nicht möglich sein, sondern nur der Zugriff auf das GIT-Repository (dies wird über die command= Anweisung in der authorized_keys erreicht). Um dies zu bewerkstelligen verwendet gitosis einen genialen Trick: die Authentifizierung findet über den Namen des öffentlichen SSH Schlüssels statt. Jeder Benutzer sendet seinen öffentlichen Schlüssel an Verwalter des gemeinsamen Repositories. Die öffentlichen Schlüssel werden dann von gitosis automatisch in die Datei authorized_keys übernommen, welche den SSH Zugriff so für mehrere Benutzer möglich macht. Die Unterscheidung zwischen den einzelnen Mitgliedern wird über den Dateinamen des öffentlichen Schlüssels ermöglicht.” [
1]
Installation
Aus Sicherheitsgründen werden wir Git unter einem separaten Benutzer laufen lassen. Dazu wird ein neuer Benutzer “git” mit Hilfe der folgenden Anweisungen erstellt:
#Auf dem Server
mkdir -p /Users/Shared/Git/Home
sudo dscl . create /Users/git
sudo dscl . create /Users/git PrimaryGroupID 1
sudo dscl . create /Users/git UniqueID 301
sudo dscl . create /Users/git UserShell /bin/bash
sudo dscl . passwd /Users/git $PASSWORD
sudo dscl . create /Users/git home /Users/Shared/Git/Home/
$PASSWORD kann dabei mit dem gewünschten Passwort ersetzt werden, oder aber auch leer gelassen werden.
Nun installieren wir Git und Python, welches für die Installation Gitosis notwendig ist:
#Auf dem Server
sudo port install git-core python27
“Um gitosis zu initialisieren, wird ein SSH Schlüssel benötigt, den man auf seinen lokalen Arbeitsplatz mittels ssh-keygen anlegt. Dieser Befehl legt die Dateien ./ssh/id_rsa (privater Schlüssel) und ./ssh/id_rsa.pub (öffentlicher Schlüssel) an. Falls schon Schlüssel vorhanden sind, können selbstverständliche auch diese verwendet werden. Die Übertragung des Schlüssels kann z.B. mit scp erfolgen.” [
1]
#Auf einem Client
scp ~/.ssh/id_rsa.pub mein-server.ch:/tmp/benutzername.pub
Dabei wird “mein-server.ch” mit der URL des gewünschten Servers und «benutzername” mit dem gewünschten Benutzernamen ersetzt.
Darauf kann man sich wieder beim Server via SSH anmelden und Gitosis aus dem offiziellen Repository laden und installieren:
#Auf dem Server
cd /tmp
git clone git://eagain.net/gitosis.git
cd gitosis
sudo python setup.py install
Falls gewünscht kann das Repository auf einem anderen Volumen erstellt werden. In diesem Fall muss das entsprechende Verzeichnis mittels eines Symlinks verknüpft werden:
#Auf dem Server
sudo mkdir /Volumes/PartitionName/repositories
sudo ln -s /Volumes/PartitionName/repositories /Users/Shared/Git/Home/repositories
sudo chown git /Volumes/PartitionName/repositories
sudo chown git /Users/Shared/Git/Home/repositories
Damit der Benutzer “git” die git Binaries, speziell “git-serve” findet, müssen wir noch die entsprechenden Pfade in sein Bash Profil speichern:
#Auf dem Server
sudo su git
echo "PATH=/opt/local/bin:/usr/local/bin:$PATH" > ~/.bashrc
chown git ~/.bashrc
Der letzte Schritt ist es Gitosis zu initialisieren und sich selber zugriff auf das Admin Repository zu geben, indem der Public Key welcher zuvor auf den Server geladen wurde, hinzugefügt wird:
#Auf dem Server
sudo -H -u git gitosis-init < /tmp/benutzername.pub
sudo chmod +x /srv/git/repositories/gitosis-admin.git/hooks/post-update # muss ausführbar sein!
Wenn alles glatt läuft, werden folgende Meldungen ausgegeben:
Initialized empty Git repository in ./
Initialized empty Git repository in ./
Falls der Fehler “gitosis.repository.GitReadTreeError: git read-tree failed: exit status 128 error” auftritt, muss folgende Code Zeile in den Gitosis Dateien gesucht werden: shutil.rmtree
Dann muss vor dieser Zeile current_dir = os.getcwd() und danach os.chdir(current_dir) eingefügt werden.
“Es empfiehlt sich die SSH Konfiguration für den Git Server (mit Git Server meine ich immer den Server, der als zentraler Teamserver verwendet werden soll) in die lokale ./ssh/config Datei aufzunehmen. Dadurch erspart man sich den Benutzer bei jeder Kommunikation mit dem GIT Server mit anzugeben. Auch kann man durch diese Konfigurationsdatei den Ort des privaten SSH Schlüssels angeben (falls dieser von dem Standard Schlüssel abweicht).” [
1]
#Auf dem Client
Host mein-git-server.ch
User git
# falls man einen anderen Port verwendet, muss dieser hier gesetzt werden
# Port 22
IdentityFile ~/.ssh/id_rsa
“Die weiteren Arbeitsschritte werden auf dem lokalen Rechner durchgeführt (nicht auf dem Server). Zuerst holt man sich das Repository gitosis-admin mit:
cd /www # in das Verzeichnis wechseln, in dem man das Repository führen will
git clone git@mein-git-server.ch:gitosis-admin.git
Hierbei ist zu beachten, dass der Benutzername git immer verwendet wird, für alle Benutzer! gitosis unterscheidet die Benutzer durch den Dateinamen des öffentlichen SSH Schlüssels, welcher verwendet wird.”[
1]
Testen des SSH Zugangs
“Mit folgendem Test kannst Du verifizieren, ob der SSH Zugang für den Benutzer git richtig konfiguriert ist. Der Befehl: ssh mein-git-server.ch sollte folgendes ausgeben: PTY allocation request failed on channel 0. Erklärung: Die SSH Authentifizierung war erfolgreich. Ein Login ist allerdings nicht möglich, da dieser für den “git” Benutzer nicht gestattet ist. Diese Meldung bedeutet also, dass alles in bester Ordnung ist. Falls ein Login möglich ist, stimmt etwas nicht! Sieht man folgende Fehlermeldung, stimmt etwas mit dem SSH Zugang nicht: Permission denied (publickey).” [
6][
1]
Verwaltung der Git Repositories
“Die Verwaltung der Repositories erfolgt über das Repository gitosis-admin. Im Unterverzeichnis keydir werden alle öffentlichen Schlüssel der Benutzer abgelegt, die auf Repositories Zugriff haben. Der Dateiname stellt den Benutzernamen dar.” Hier ein Beispiel:
“Die Konfiguration von Benutzergruppen und wer auf welches Repository zugreifen darf, erfolgt in der Datei gitosis.conf.” [
1]
# ls -1 keydir/
alex.pub
andi.pub
claudi.pub
flobruit.pub
reinhard.pub
Neues Repository anlegen
“In der Datei gitosis.conf wird das neue Repository zu den gewünschten Benutzergruppen hinzugefügt. Sofort nach dem Commit dieser, kann von den Benutzern der Benutzergruppen mit Schreibrechten das neue Repository angelegt werden. Hierzu werden folgende Kommandos ausgeführt (auf dem Arbeitsplatzrechnern, nicht auf dem Server!):”
mkdir mein_projekt
cd mein_projekt
git init
touch dummy.txt && git add dummy.txt && git commit -m 'Initial commit with dummy file'
git remote add origin git@mein-git-server.ch:mein_projekt.git
git push origin master:refs/heads/master
“Wichtig ist dabei, dass mindestens ein Commit durchgeführt wurde, bevor man den push ausführt (anderfalls wird eine Fehlermeldung erzeugt). Anstelle der dummy.txt Datei können natürlich auch bestehende Dateien in das Verzeichnis kopiert werden, die ins Repository übernommen werden sollen. Hier im Beispiel wird als Repository-Name mein_projekt verwendet, welches mit dem gleichen Namen in der gitosis.conf als writable für den Benutzer eingetragen sein muss. Nach dem Push ist auf dem Git Server das Verzeichnis /srv/git/repositories/mein_projekt vorhanden.
Das Repository kann jetzt von allen Team Mitglieder mit folgenden Befehl verwendet werden:”
git clone git@mein-git-server.de:mein_projekt
“Wichtig: alle Team-Mitglieder müssen als Benutzername “git” verwenden (und nicht den Benutzernamen, der für den öffentlichen Schlüssel verwendet wurde).” [
1]
Repository entfernen
“Will man ein Repository entfernen, müssen alle Verweise auf dieses Repository aus der Datei gitosis.conf enfernt werden. Das Repository selbst kann nur auf dem Git Server gelöscht werden. Dazu löscht man das komplette Verzeichnis /Users⁄Shared⁄Git⁄Home⁄repositories⁄MEIN-REPOSITORY.git” [
1]
Neues Team-Mitglied aufnehmen
“In der Datei gitosis.conf den Benutzer in die gewünschten Gruppen hinzufügen, auf der dieser Zugriff bekommen soll. Weiterhin ist es erforderlich einen öffentlichen SSH Schlüssel im Verzeichnis keydir abzulegen, dessen Dateiname mit dem Benutzernamen übereinstimmt. Beispiel: für Benutzer alex muss eine Key mit dem Namen keydir/alex.pub existieren.” [
1]
Team-Mitglied entfernen
“In der Datei gitosis.conf den Benutzer aus der gewünschten Gruppe entfernen. Ist der Benutzer in keiner Gruppe mehr vorhanden, kann auch sein öffentlicher Schlüssel aus dem Verzeichnis keydir gelöscht werden.” [
1]
Jenkins Installation
Da Git nun Installiert ist, kann der Continuous Integration server Jenkins http://jenkins-ci.org/ installiert werden, welcher dann beim Build die Daten aus dem Git Repository auscheckt. Jenkins ist ein Fork bzw. eine Weiterentwicklung von Hudson. Die meisten Hudson Plugins sind kompatibel mit Jenkins und können ohne anpassungen verwendet werden.
Standard Installation
Die Standard Installation von Jenkins unter Mac OS X (zum Zeitpunkt des Verfassens dieses Dokuments) installiert eine Launch Agent plist in /Library⁄LaunchDaemons⁄org.jenkins-ci.plist. Somit wird Jenkins unter dem Benutzer “daemon” gestartet. Das Problem ist, dass dieser das gleiche Home Directory wie root, nämlich /var/root verwendet. Wenn wir aber möchten, dass sich Jenkins beim Git Server über SSH authentifiziert, muss auf private key im Verzeichnis .ssh zugegriffen werden. Das .ssh Verzeichnis wird aber niemals die richtigen Zugriffsrechte besitzen sodass der private key genutzt werden könnte. Deshalb wird ein separater Benutzer “jenkins” erstellt unter welchem dann der Jenkins Server laufen wird. Das Home directory wird auf /Users/Shared/Jenkins/Home gesetzt. $PASSWORD kann dabei wiederum mit dem gewünschten Passwort ersetzt werden, oder aber auch leer gelassen werden.
sudo dscl . create /Users/jenkins
#Jenkins wird der Gruppe wheel hinzugefügt
sudo dscl . create /Users/jenkins PrimaryGroupID 0
sudo dscl . create /Users/jenkins UniqueID 300
sudo dscl . create /Users/jenkins UserShell /bin/bash
sudo dscl . passwd /Users/jenkins $PASSWORD
sudo dscl . create /Users/jenkins home /Users/Shared/Jenkins/Home/
mkdir -p /User/Shared/Jenkins/Home
chown -R jenkins: /Users/Shared/Jenkins/Home
Falls man die primäre Benutzergruppe später ändern möchte, kann das mit folgenden Befehl gemacht werden: dscl . -change /Users/rod PrimaryGroupID ID_ALT ID_NEU. Darauf wird der Jenkins Server gestoppt: sudo launchctl unload -w /Library⁄LaunchDaemons⁄org.jenkins-ci.plist. Darauf wird die plist Datei angepasst, sodass der Server fortan unter dem Benutzer “jenkins” gestartet wird:
<plist version="1.0">
<dict>
<key>EnvironmentVariables</key>
<dict>
<key>JENKINS_HOME</key>
<string>/Users/Shared/Jenkins/Home</string>
</dict>
<key>GroupName</key>
<string>wheel</string>
<key>KeepAlive</key>
<true/>
<key>Label</key>
<string>org.jenkins-ci</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>/Library/Application Support/Jenkins/jenkins-runner.sh</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>UserName</key>
<string>jenkins</string>
</dict>
</plist>
Mittels sudo launchctl load -w /Library/LaunchDaemons/org.jenkins-ci.plist kann Jenkins darauf wieder gestartet werden. Damit sich der jenkins Benutzer mit dem Git Repository verbinden kann muss noch dessen öffentlicher SSH Schlüssel erstellt und bereitgestellt werden. Für den Schlüssel sollte keine “Passphrase” verwendet werden, da der Benutzer ja als Daemon läuft.
#Auf dem Server
sudo su jenkins
ssh-keygen
#Standard Verzeichnis verwenden, leerer Passphrase verwenden
cat ~/.ssh/id_rsa.pub
Der ausgegebene Inhalt sieht in etwa so aus:
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC8n6utRXYaKMa4SOBYsc jVx1TPFgxMS77HjkUfJWz5bbch3CPRvUOqpQQyxj5dUedZe/cw7WU305 OyAmR7klXAebJRfDph+CPGkGOjmNsDZXQ/e0OC1RzcPb8C9nl jenkins@macbookpro
Nun wird auf dem Client welcher zuvor das Gitosis Repository initialisiert hatte eine pub Datei erstellt welche gleich wie der Benutzer heisst. In diesem Fall ist das jenkins@macbookpro.pub. Diese wird in das keydir Verzeichnis im gitosis-admin Repository gelegt. Zudem müssen in der gitosis.conf die Berechtigungen für die Repositories auf welche Jenkins zugreifen soll, korrekt gesetzt werden. Dies könnte folgendermassen aussehen:
[gitosis]
[group gitosis-admin]
members = admin@lightforce.local admin@macbookpro jenkins@macbookpro
writable = gitosis-admin documentation graphics server TravelMate
Danach werden die geänderten Gitosis Einstellungen an den Server gesendet. Nun sollte der jenkins Benutzer die Git Repositories klonen können. Am besten man testet manuell ob der Zugriff funktioniert:
#Auf dem Server
sudo su jenkins
mkdir /tmp/gittest
cd /tmp/gittest
git clone git@mein-git-server.ch:mein-repo.git
Dabei sollte kein Fehler auftreten. Damit alle Zugriffsberechtigungen für die Jenkins Build Jobs richtig gesetzt werden, sollte man sich auf dem Server einmal mit dem Benutzer jenkins anmelden.
Build Jobs
Sobald Jenkins installiert und gestartet ist können Build Jobs über das Webinterface konfiguriert werden. Standardmässig läuft Jenkins auf Port 8080. Somit kann auf dem Server über http://localhost:8080 auf Jenkins zugegriffen werden. Zu beachten ist, dass die Buildjobs keine Leerzeichen enthalten sollten. Es könnte zu unerwünschten Effekten führen wenn in den Skipts die Umgebungsvariable $JOB_NAME verwendet wird und jene Leerzeichen enthält.
API Documentation
Die API Dokumentation eines iOS Projekts kann sehr komfortabel mit Hilfe des appledoc Tools erstellt werden. Dabei wird das Look & Feel der Apple eigenen Dokumentation verwendet. Das Tool bietet zudem die Möglichkeit docset Bundles zu erstellen und zu installieren. Die appledoc Software kann von folgendem Repository heruntergeladen werden https://github.com/tomaz/appledoc. Das Verzeichnis beinhaltet ein Installationsskript welches als root Benutzer aufgerufen werden muss: sudo install-appledoc.sh. Um die Dokumentation mittels Jenkins zu erzeugen, muss ein neues Build Projekt angelegt werden. Dieses soll das Git Repository des iOS Source Codes abfragen. Als Buildverfahren wird “Shell ausführen” gewählt mit folgendem Inhalt:
rm -rf /Users/Shared/WebserverData/TravelMate/API-Documentation/*
/usr/local/bin/appledoc -p TravelMate -c "My Name" –create-html –no-create-docset –logformat 1 –verbose 3 –exit-threshold 2 -o /Users/Shared/WebserverData/TravelMate/API-Documentation $WORKSPACE/TravelMate
Der exit-treshold Parameter muss auf 2 gesetzt werden, damit Warnungen welche beim Erzeugen der Dokumentation ausgegeben werden nicht zu einem Exit Code ungleich 0 führen. Das Skript erzeugt die API Dokumentation im HTML Format und legt sie im Webserver Verzeichnis ab.
iPhone Applikation
Auch iOS Projekte können mit Hilfe von Jenkins automatisch erzeugt und überprüft werden. Sind die Build Jobs einmal konfiguriert können deren Stati auf der Dashboardseite begutachtet werden (siehe Abbildung 3).
Zertifikate
Beim Erzeugen der iOS Applikation wird diese signiert. Deshalb muss auf dem Buildsystem die Entwicklerzertifikate sowie die Provisioning Profiles installiert werden. Die Zertifikate (Devlopment/Distribution) können über das Apple Entwicklerportal bezogen werden. Eine Anleitung ist unter
http://developer.apple.com/ios/manage/certificates/team/ zu finden. Entweder werden die Zertifikate direkt auf dem Server erstellt, oder auf dem Rechner, werlcher zur Verwaltung des Servers verwendet wird (siehe Abbildung 4). In letzterem Fall müssen die Zertifikate als .p12 Dateien exportiert werden und danach auf dem Server installiert werden. Wichtig ist, dass die Zertifikate auf dem Server im
System Schlüsselbund installiert werden [
12]. Der Jenkins Benutzer läuft als Daemon und somit muss der Schlüsselbund im headless modus freigegeben werden können, um den privaten Schlüssel des Zertifikats zu verwenden. Die Befehle welche im Hintergrund verwendet werden um den Schlüsselbund freizugeben sind folgende:
security unlock-keychain -p <passwort>
#oder ohne ein Passwort zu verwenden
security unlock-keychain -u
Ebenso müssen die Provisioning Profiles auf dem Server installiert werden. Diese legen fest, welches Gerät die Applikation starten bzw. testen darf. Am einfachsten können sie über den Organizer der Xcode IDE installiert werden (siehe Abbildung 5). Nach der Installation sind sie unter ~/Library/MobileDevice/Provisioning Profiles/ zu finden. Eine Anleitung, wie die Provisioning Profiles erzeugt werden können befindet sich unter http://developer.apple.com/ios/manage/provisioningprofiles/.
Build Job
Um Build Jobs für iOS Projekte einrichten zu können, empfiehlt es sich ein Jenkins Plugin für Xcode Projekte zu installieren. Hier wird das Plugin von Ray Hilton verwendet. Die neuste Version dieses Plugins kann direkt vom Git Repository geladen und mittels Maven installiert werden. Dazu muss in den Maven Settings die Plugin Group für das Hudson Maven Plugin eingefügt werden. Dazu folgendes in der ~/.m2/settings.xml Datei einfügen:
<settings>
<plugingroups>
<plugingroup>org.jvnet.hudson.tools</plugingroup>
</plugingroups>
</settings>
Danach kann das Plugin geladen und installiert werden:
Zum Schluss befindet sich die xcode.hpi Datei im target/ Verzeichnis. Diese kann dann über das Webinterface von Jenkins installiert werden (Hudson>Manage Plugins>Advanced>Upload Plugin).
# Clone the repository
$ git clone git://github.com/rayh/xcode-hudson-plugin.git
# Go into the cloned repository
$ cd xcode-hudson-plugin
# Build the project
$ mvn install
Darauf Kann ein neues “Free Style” Projekt angelegt werden. Da die Daten das Projekts vo Git Repository bezogen werden sollen, welches auf dem gleichen System betrieben wird, wie jenkins, kann beim Source-Code Management ein lokales Repository angegeben werden (z.B. git@localhost:TravelMate.git).
Als Build-Auslöser wählen wir “Source Code Management System abfragen”. Als Zeitplan geben wir z.B. @hourly oder @daily an. Beim Xcode Abschnitt wird das gewünschte Target z.B. TravelMate angegeben und die Konfiguration Release ausgewählt. Für die Release Konfiguration sollte zuvor in Xcode konfiguriert werden, dass das AdHoc Provisioning Profile verwendet wird. Es wird ein Haken bei “Update CFBundleVersion with build number?”, “Clean before build?” und “Build IPA?” gesetzt. Danach sollte das Projekt erfolgreich erzeugt werden können.[
15]
Deployment zu TestFlightApp
TestFlightApp.com bietet eine Plattform um Beta Apps OTA (Over The Air) zu testen. Die IPA Dateien können automatisiert auf das Portal geladen werden. Die Plattform notifiziert darauf die Tester, dass eine neue Version der App zum Testen bereitsteht. Damit ein fertiger Build automatisch auf TestFlightApp geladen wird, muss dem bestehenden Jenkins Projekt ein “Shell ausführen” Build-Schritt mit folgendem Inhalt hinzugefügt werden [
3]:
curl http://testflightapp.com/api/builds.json
-F file=@$WORKSPACE/build/Release-iphoneos/TravelMate-Release-${JOB_NAME}-\"${BUILD_NUMBER}\".ipa
-F api_token='e1619135...'
-F team_token='52aca8b...'
-F notes='This is an autodeploy build from Jenkins!'
-F distribution_lists='Internal'
Da bei der aktuellen Version des Jenkins Xcode Plugins ein Bug besteht, welcher Anführungszeichen vor und nach die Build-Nummer stellt, müssen diese im obigen Skript escaped werden. Sobald der Bug behoben wurde kann auch file=@$WORKSPACE⁄build⁄Release-iphoneos⁄TravelMate-Release-${BUILD_TAG}.ipa als Pfad verwendet werden.
Beim Build wird dann die IPA Datei hochgeladen und ein Resultat im JSON Format wird ausgegeben. Dies sollte etwa wie folgt aussehen:
{
"bundle_version": "1.0 (\"13\")",
"install_url": "https://testflightapp.com/install/0c01fa526108f4691004383cd446d455-NDE1Mzgy/",
"config_url": "https://testflightapp.com/dashboard/builds/complete/415382/",
"created_at": "2011-09-27 09:32:07",
"device_family": "iPhone",
"notify": false,
"team": "TravelMate",
"minimum_os_version": "5.0",
"release_notes": "This is an autodeploy build from Jenkins!",
"binary_size": 32634
}Finished: SUCCESS
Über https://testflightapp.com/dashboard/builds/ können die Tester direkt über ihr iOS Gerät auf den neuen Build zugreifen (siehe Abbildung 6). Ausserdem können über diese Plattform Testergebnisse, Fragen, Feedback etc. für die einzelnen Builds abgewickelt werden.
Build Job für den App-Store Release
Für den Release in den App Store empfiehlt es sich einen separaten Build Job einzurichten. Zuerst muss eine neue Build Konfiguration in Xcode angelegt werden welche auf der Relaese Konfiguration basiert. Diese kann zu diesem Zweck dupliziert werden (siehe Abbildung 7).
Die Neue Konfiguration die wir z.B. Publish nennen muss so konfiguriert werden, dass diese das Release Provisioning Profile für den App Store verwendet.
Bei diesem Job wird ebenfalls ein Haken bei “Update CFBundleVersion with build number?”, “Clean before build?” und “Build IPA?” gesetzt. Es muss hier jedoch kein Build-Auslöser angegeben werden, da der Build eine Release Version typischerweise manuell gestartet wird. Die dSYM files sollten zudem archiviert werden, damit ein späterer Crash Log eines Benutzers auch ausgewertet werden kann. Es muss lediglich ein Haken bei “Artefakte archivieren?” gesetzt werden und im Textfeld folgendes eingegeben werden: “**/**.dSYM”. Falls gewünscht kann die IPA Datei gleich noch in das Webserver Verzeichnis kopiert werden, sodass diese leicht zugänglich ist. Dazu muss ein ein weiterer Build Schritt “Shell ausführen” mit folgendem Inhalt hinzugefügt werden:
source="$WORKSPACE/build/Release-iphoneos/*.ipa"
target="/Users/Shared/WebserverData/TravelMate/$JOB_NAME"
rm -rf $target
mkdir $target
cp -R $source $target
Logic & Application Tests
Um die Unit und Application Test laufen lassen zu können muss noch einiges angepasst werden. Da Apple seit Xcode 4 nicht mehr zwischen Logic Tests und Application Tests unterscheidet, werden beim Test Vorgang beide ausgeführt. Im headless Modus will Xcode (Version 4.2 zum Zeitpunkt des Verfassens dieses Textes) die Application Tests aber perse nicht ausführen, da offenbar keine hosted Tests vom iOS Simulator unterstützt werden. Deshalb muss das RunPlatformUnitTests Skript angepasst werden. Ändern Sie Zeile 95 der Datei /Developer⁄Platforms⁄iPhoneSimulator.platform⁄Developer⁄Tools⁄RunPlatformUnitTests von
Warning ${LINENO} "Skipping tests; the iPhoneSimulator platform does not currently support application-hosted tests (TEST_HOST set)."
zu
export OTHER_TEST_FLAGS="-RegisterForSystemEvents"
RunTestsForApplication "${TEST_HOST}" "${TEST_BUNDLE_PATH}"
Dann werden die Application Test trotzdem ausgeführt [
16]. Falls Core Data beim Projekt eingesetzt wird, muss zusätzlich im Programm eine Anpassung gemacht werden, da der iOS Simulator im Headless Modus einen Bug hat und das Dokument Verzeichnis für eine laufende Applikation falsch angibt [
8]. Die Methode applicationDocumentsDirectory muss angepasst werden, damit im Headless Modus der Pfad anders gesetzt wird:
-(BOOL)isApplicationDirectoryCorrect:(NSString*)directory{
NSRegularExpression* regex = [[NSRegularExpression alloc] initWithPattern:@"/[0-9A-F]+-[0-9A-F]+-[0-9A-F]+-[0-9A-F]+-[0-9A-F]+/" options:0 error:nil];
NSUInteger numberOfMatches = [regex numberOfMatchesInString:directory options:0 range:NSMakeRange(0, [directory length])];
return numberOfMatches>=1;
}
/**
Returns the URL to the application's Documents directory.
*/
- (NSURL *)applicationDocumentsDirectory
{
/*Fix to make application tests working in headless (command line) mode
The problem is that in headless mode the Document directory (for core data) is not set correctly:
~/Library/Application Support/iPhone Simulator/Documents instead of
~/Library/Application Support/iPhone Simulator/5.0/Applications/E1941BAA-8C4D-4AD4-8381-F1D128A1C05E/Documents/
*/
//See http://www.gorillalogic.com/forumpost/1393
//See http://denizdemir.com/2011/03/24/ios-unit-testing-with-xcode-4-and-core-data/
if(![self isApplicationDirectoryCorrect:[[NSBundle mainBundle] description]])
return [NSURL fileURLWithPath:[[NSFileManager defaultManager] currentDirectoryPath]];
return [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
}
Nun kann in Jenkins ein neues “Free-Style” Projekt angelegt werden. Für dieses Projekt spezifizieren wir wiederum einen Xcode Build Step. Als Target konfigurieren wir das Test Target welches in diesem Fall “TravelMateTests” lautet. Das SDK muss dieses mal angegeben werden, da die Unit Tests nur ausgeführt werden können, wenn der Build für das SDK des Simulators erzeugt wird. Als Konfiguration wählen wir Debug. Bei “Update CFBundleVersion with build number?” setzen wir keinen Haken und bei “Build IPA?” ebenfalls nicht. Jedoch müssen die Unit Test Ergebnisse von Jenkins gesammelt werden. Dazu setzten wir bei “Veröffentliche JUnit-Testergebnisse.” einen Haken und setzten “**/test-reports/**.xml” als Suchpfad. Ausserdem sollte ein Clean vor dem Ausführen der Tests ausgeführt werden, da sonst unter Umständen Inkonsistenzen im Core Data Store auftreten können. Zu diesem Zweck muss ein Haken bei «Clean before build?” gesetzt werden.
Code Coverage
Falls die Testabdeckung der iOS Applikation berechnet werden soll, kann dies ebenfalls automatisiert mittels Jenkins erfolgen. Dazu muss das Build Projekt für die Unit Tests angepasst werden. Zunächst muss noch eine neue Buildkonfiguration in Xcode erstellt werden. Dabei kann die Debug Konfiguration dupliziert werden (analog Abbildung 7). Die neue Konfiguration wird Libprofile genannt. Für diese Konfiguration müssen noch einige Flags gesetzt werden [
13].
Zudem muss noch der Suchpfad “$(DEVELOPER_DIR)/usr/lib” für die libprofile Library hinzugefügt werden. Ausserdem muss noch das Linker Flag “-lprofile_rt” beim Parameter “Other Linker Flags” gesetzt werden [
14].
Darauf werden die gcov kompatiblen Programmfluss Analyse Dateien erzeugt. Da es zurzeit kein gcov Plugin für jenkins gibt, verwenden wir das Python Skript gcovr um die Analysedateien in Cobertura XML Dateien umzuwandeln. Die Version 2.2 kann über folgende URL heruntergeladen werden: https://software.sandia.gov/trac/fast/export/2575/gcovr/trunk/scripts/gcovr. Das Skript legen wir in einen Unterordner des Projekts welcher “Automation” getauft wird.
Dann wird das Jenkins Build projekt angepasst. Als Konfiguration im Abschnitt Xcode unter Buildverfahren wird nun Libprofile statt Debug verwendet. Es muss ein zusätzlicher “Shell ausführen” Build-Schritt mit folgendem Inhalt hinzugefügt werden [
5,
4]:
python $WORKSPACE/Automation/gcovr.py -x -o $WORKSPACE/coverage.xml -r $WORKSPACE -e ".*/ThirdParty/.*" -e ".*/TravelMateTests/.*"
Damit die erzeugten Testabdeckungsergebnisse gesammelt werden, muss das Cobertura Plugin für Jenkins installiert sein. Dies kann über die Plugin-Verwaltungsseite
http://localhost:8080/pluginManager/available vorgenommen werden. Darauf muss ein Haken bei der Option “Veröffentliche die Cobertura Testabdeckung” gesetzt werden und als Suchpfad “**/coverage.xml” definiert werden [
2]. Folglich werden beim Buildvorgang die Testergebnisse auf Jenkins veröffentlicht.
Automatischer Build-Start nach einem Commit
Falls gewünscht, kann für den Git Server ein post-receive hook eingerichtet werden, welcher einen Build startet sobald ein neuer Commit erfolgt ist. Das folgende Skript kann zu diesem Zweck als post-receive hook konfigueriert werden[
9]:
#!/bin/sh
USER=hudson
PASS=password
HUDSON_URL=http://$USER:$PASS@<my-build-server>:8080
JOB=$1
TOKEN=$2
CAUSE=$2
set -x
lwp-request -ds "$HUDSON_URL/job/$JOB/build?token=$TOKEN"
Webserver Installation
Um auf die Daten welche von Jenkins erzeugt werden über den Browser zugreifen zu können, wird nun noch der Apache Webserver konfiguriert. Dieser wird mit dem OS X Betriebssystem mitgeliefert.
Es wird ein Directory Listing auf dem Webserver Ordner /User/Shared/WebserverData eingerichtet. Dieses wird durch eine Authentifizierung abgesichert. Dies einfachsten zwei Möglichkeiten sind Basic Authentication oder Digest Authentication. Basic Authentication hat den Nachteil, dass die Zugangsdaten unverschlüsselt an den Webserver geschickt und somit gesnifft werden können. Der Vorteil ist, dass sie nahezu von allen Browsern unterstützt wird. Auf Grund des genannten Nachteils empfiehlt es sich jedoch die Digest Authentication zu verwenden. Der Vollständigkeit halber sei hier dennoch kurz erwähnt wie die Basic Authentication eingerichtet würde.
Konfiguration des Virtual Hosts
Damit der Webserver weiss, welches Verzeichnis er auslesen soll, wenn er eine Anfrage erhält, muss ein virtueller Host konfiguriert werden. Dies kann in der Server Administrations Applikation gemacht werden. Es muss ein neuer virtueller Host mit dem gewünschten Domainnamen hinzugefügt werden (siehe Abbildung 9). Sofern der DNS Server nicht entsprechend konfiguriert ist, muss hier die IP Adresse des lokalen Hosts ausgewählt werden (hier 192.168.0.5). Zudem wird noch der Document Root auf /Users/Shared/WebserverData gesetzt.
sudo nano /etc/apache2/sites/0000_192.168.0.5_80_my-host.com.conf
Dort muss innerhalb des Directory Tags das Attribut AllowOverride (Zeile 19) auf den Wert “All” gesetzt werden.
<VirtualHost 192.168.0.5:80>
ServerName my-host.com
ServerAdmin admin@example.com
DocumentRoot "/Users/admin/WebserverData"
DirectoryIndex index.html index.php /wiki/ default.html
CustomLog "/var/log/apache2/access_log" combinedvhost
ErrorLog "/var/log/apache2/error_log"
<IfModule mod_ssl.c>
SSLEngine Off
SSLCipherSuite "ALL:!aNULL:!ADH:!eNULL:!LOW:!EXP:RC4+RSA:+HIGH:+MEDIUM"
SSLProtocol -ALL +SSLv3 +TLSv1
SSLProxyEngine On
SSLProxyProtocol -ALL +SSLv3 +TLSv1
</IfModule>
<Directory "/Users/admin/WebserverData">
Options All +MultiViews -ExecCGI -Indexes
AllowOverride All
<IfModule mod_dav.c>
DAV Off
</IfModule>
</Directory>
</VirtualHost>
Danach empfiehlt es sich den Webserver mittels sudo apachectl restart neu zu starten. Sollte etwas schief gehen, kann die Konfiguration jederzeit mittels folgendem Befehl zurück gesetzt werden:
sudo serveradmin command web:command=restoreFactorySettings
Basic Authentication
Die Basic Authentication braucht eine Datei in welcher die Passwort Hashes für die Benutzer speichert. Um eine solche Datei zu erstellen kann der Befehl htpasswd -c FILE USER verwendet werden, wobei FILE mit dem gewünschten Dateinamen und USER mit dem Benutzernamen ersetzt werden müssen.
Darauf wird im Webserver Verzeichnis /User/Shared/WebserverData folgende .htaccess Datei erzeugt:
Options +Indexes
IndexOptions +FancyIndexing
AuthType Basic
AuthName "Protected directory"
AuthUserFile $PATH_TO_PWD_FILE
Require valid-user
Wenn das Verzeichnis im Browser nun abgefragt wird, erscheint ein Authentisierungsdialog.
Digest Authentication
Die Digest Authentication überträgt im Gegensatz zur Basic Authentication die Daten verschlüsselt. Damit diese verwendet werden kann, muss das Apache Modul mod_auth_digest aktiviert sein. Dies sollte standardmässig bereits so konfiguriert sein. Andernfalls muss in der datei /etc/apache2/httpd.conf folgender Eintrag hinzugefügt werden “LoadModule auth_digest_module libexec/apache2/mod_auth_digest.so”.
Darauf wird mit Hilfe des htdigest Tools eine Datei mit dem Passwort Hash des gewünschten Benutzers erzeugt:
htdigest -c ~/digest Private lightforce
Bei diesem Beispiel wird der Hash des Passwortes für den Benutzer “lightforce” in die Datei “digest” gespeichert. Anschliessend wird eine .htaccess Datei im Webserver Verzeichnis /User/Shared/WebserverData mit folgendem Inhalt erzeugt:
Options +Indexes
IndexOptions +FancyIndexing
AuthType Digest
AuthName "Private"
AuthDigestFile ~/digest
Require user lightforce
Darauf sollte bei einem Zugriff via Browser ein Authentisierungsdialog erscheinen wie auf Abbildung 10 zu sehen ist.

Abbildung 10: Authentisierungsdialog im Browser
Nachdem die Authentisierung erfolgt ist, sollte im Browser der Inhalt des Verzeichnisses angezeigt werden (siehe Abbildung 11).

Abbildung 11: Directory Listing
Referenzen