Running on Apple Mac mini Dualcore i5 2.8GHz (MGEQ2FN/A) OSX Yosemite: 10.10.1
UPDATE Nov 3rd 2015: Upgraded to El Capitan caused my Jenkins to stop working. I had to apply the file permission fix for the log folder again (see known issues at the end of this gist). I also had to reinstall legacy java support and load the jenkins daemon again with launchctl. I took the time to update all the components mentioned in this gist when I did the upgrade. So the Android sdk/ndk, nodejs (0.10 -> 0.12), Titanium and Java. Beware that there is a new command-line tool for titanium (appc from the 'appcelerator' package).
I installed and setup the new Mac with 1 main administrative account. In the scripts below it is assumed you are logged in as this administrative user. Changing to the jenkins user is done using sudo su - jenkins
.
You will need a valid Apple ID and developer license to install XCode and manage your provisioning files. You also need a valid Appcelerator account to download and install the SDK.
Jenkins itself needs Java to run. The titanium tool chain needs an installed JDK to function. We also need to install the legacy Java 6 runtime for some build tools.
Goto the oracle site: http://www.oracle.com/technetwork/java/javase/downloads/index.html Download the JDK and install it (Java SE 8u31 at the time of writing)
Goto the apple download site: http://support.apple.com/kb/DL1572
Download the JavaForOSX2014-001.dmg
and install the pkg after mounting the image
I installed Jenkins using the official Mac installer from: http://jenkins-ci.org/content/thank-you-downloading-os-x-installer It ends up running as a deamon for user 'jenkins'. The jenkins user account has normal user permissions (ie. not an administrator). You won't have to be logged in for Jenkins to be available and it will automatically start when a reboot occurs.
If you want jenkins to run on another port then the default 8080 port you can change it with the following sequence of commands (change the 9999 to the port you want):
sudo launchctl unload /Library/LaunchDaemons/org.jenkins-ci.plist
sudo defaults write /Library/Preferences/org.jenkins-ci httpPort 9999
sudo launchctl load /Library/LaunchDaemons/org.jenkins-ci.plist
Setup my SSH key for the Jenkins user:
sudo su - jenkins
ssh-keygen
The generated public key (found in ~/.ssh/id_rsa.pub) is being used on Bitbucket as a deployment key (read-only access). Same could be done for github. I'm using SCM polling in my project because public access to my Jenkins setup is not available.
Installed nodeJS and NPM using official installer from http://nodejs.org/
Globally installed following modules:
npm install -g grunt-cli titanium alloy appcelerator
UPDATE Nov 3rd 2015: The 'appcelerator' module is for a new set of platform tools needed to build latest Titanium SDK based apps.
This is not how I'd like to have NodeJS setup. You want to be able to use different versions of node and globally installed modules per project. There is a NodeJS plugin for jenkins that should do this but the auto-installers don't work on Mac. I tried using manual archives but always ended up without a working npm. In the end I gave up and used a globally installed NodeJS for now.
Installed XCode
using Mac App store. Ran at least once to accept agreement and install command-line tools.
TIP: If you installed a new version of xcode you will need to accept the license again. If you only have shell (SSH) access you can just do something like: sudo xcodebuild
. This will ask you to review and accept the license if it is needed. You need to run this with administrative permissions or it wont work.
First we need to create the folder where the keychain is supposed to reside:
sudo su - jenkins
mkdir ~/Library
mkdir ~/Library/Keychains
Create a keychain with the required credentials (private/public keys and distribution certificates) on your Mac. You will need to use the Apple developer portal to create a distribution certificate, an App ID and a provisioning profile (Ad Hoc or App store depending on your needs). These need to be imported into your keychain and installed on your Mac. Easiest is to open XCode, login with your Apple ID and hit the refresh button on the account details page. Copy (don't move, use the Alt/Option key) the certificate and keys to the new jenkins keychain. Note the provisioning profiles are not in your keychain and we will handle them in the next paragraph of this document. Copy the jenkins keychain file (found in ~/Library/Keychains) to the jenkins server and place it in the Keychains directory. Then set it as the default keychain:
sudo su - jenkins
security default-keychain -d user -s ~/Library/Keychains/jenkins.keychain
We will need to use the security
command-line tool in build scripts to unlock the keychain when starting a build. Access to the keychain is timed (it automatically locks) so set the default timeout to (for example) 10 minutes. Adjust according to how long your build may take. You only have to do this once as the setting is stored in the keychain itself.
sudo su - jenkins
security unlock ~/Library/Keychains/jenkins.keychain (asks for password)
security set-keychain-settings -t 600 -l ~/Library/Keychains/jenkins.keychain
security show-keychain-info ~/Library/Keychains/jenkins.keychain (to verify the timeout value)
In your project build scripts you can unlock the keychain without user interaction by specifying the password directly:
security unlock -p $KEYCHAIN_PASS ~/Library/Keychains/jenkins.keychain
El Capitan is even more strict when it comes to keychain access and security. I ran into the issue that I wanted to change the default access to the private key for the jenkins keychain. What happened is that I was asked for the keychain password to save the changes but it wouldn't accept. Turns out any input with accessibility tools enabled or when using screen sharing is deemed 'unsecure' and it simply won't accept it. I had to directly use connected keyboard to enter the password before it would accept.
Reference info: https://support.apple.com/en-gb/HT205375
The jenkins mask password plugin can be used to set the KEYCHAIN_PASS variable and mask it from the console log output:
https://wiki.jenkins-ci.org/display/JENKINS/Mask+Passwords+Plugin
Once the plugin is installed you need to go into the main Jenkins configuration and store your password there. Use KEYCHAIN_PASS as the name and be sure to select the 'Mask passwords (and enable global passwords)' in the build environment section of your project.
We need to create the folder for the provisioning profiles on the Jenkins server and upload them from your mac. First we create the target folder on the server:
sudo su - jenkins
mkdir ~/Library
mkdir ~/Library/MobileDevice
mkdir ~/Library/MobileDevice/Provisioning\ Profiles
Then copy the needed profiles to the jenkins server using a tool like scp
. Note that the jenkins user doesn't have a login so you need to upload it to your administrators users home directory first and then login to copy them to the jenkins users home directory. On your mac:
scp ~/Library/MobileDevice/Provisioning\ Profiles/*.mobileprovision user@your_jenkins_server:~/
On the jenkins server you can then copy the uploaded profiles to the correct location. As the administrator user you can:
cp ~/*.mobileprovision /Users/Shared/Jenkins/Library/MobileDevice/Provisioning\ Profiles/
Once we reach the titanium installation phase we can verify if everything is in the right place with the titanium info
command.
Apart from the fact that Java on Mac OSX is a little different Android should be easier to setup. We will use homebrew to make installing the Android SDK's a lot easier.
Installed HomeBrew (http://brew.sh/) and added PATH
environment variable to global Jenkins configuration:
$PATH:/usr/local/bin
File permissions are an issue. Always check your setup with brew doctor
when in doubt. I changed ownership of /usr/local
to the jenkins user. Group ownership I set to admin to allow the administrator user(s) to also use brew if needed. I needed to add group write permissions recursively for this. So in the end I did (as an administrator user):
cd /usr/local
sudo chown -R jenkins:admin *
sudo chmod -R g+w *
Used brew to install Android SDK and NDK:
brew install android-sdk
brew install android-ndk
Then update the SDK and install the platform tools using Androids SDK manager (grab a coffee at this point)
android update sdk --no-ui
Added environment variables in Jenkins to match where Android SDK/NDK is installed:
ANDROID_HOME
is /usr/local/opt/android-sdk
ANDROID_SDK
is /usr/local/opt/android-sdk
ANDROID_NDK
is /usr/local/opt/android-ndk
We also need to tell titanium where the Android SDK/NDK are:
titanium config android.sdkPath /usr/local/opt/android-sdk/
titanium config android.ndkPath /usr/local/opt/android-ndk/
I did the above titanium config for both our administrator account and the jenkins user just to be sure.
Use the keytool
command as you normally would for an Android app to generate a keystore:
keytool -genkeypair -v -keystore android.keystore -alias your-app-alias -keyalg RSA -sigalg SHA1withRSA -validity 10000
We need to install and select the titanium SDK for the jenkins user. You need your Appcelerator account details for this. Note that there is a shorthand ti
available for the titanium
command. We already installed the titanium tools after installing NodeJS with the npm install -g grunt-cli titanium alloy
command.
sudo su - jenkins
mkdir ~/Library
mkdir ~/Library/Application\ Support
mkdir ~/Library/Application\ Support/Titanium
appc ti sdk install
You can verify installed and selected SDK using the command:
titanium sdk
You can verify your whole titanium setup using the info command:
titanium info
In package.json we declared the following script section:
"scripts": {
"test": "command -v grunt >/dev/null 2>&1 && grunt coverage || { echo >&2 'Grunt is not installed'; }",
"prepublish": "command -v grunt >/dev/null 2>&1 && grunt || { echo >&2 'Grunt is not installed'; }"
},
In Jenkins we create a free-from project with the following shell command:
security unlock -p $KEYCHAIN_PASS /Users/Shared/Jenkins/Library/Keychains/jenkins.keychain
npm install
npm test
security lock /Users/Shared/Jenkins/Library/Keychains/jenkins.keychain
Don't forget to setup the password masking pluging for the keychain and keystore passwords to be available. In the example below we use KEYCHAIN_PASS and KEYSTORE_PASS for Android and iOS respectively.
Below is an example combination of package.json and a Grunt build script. Note that the below example is setup to make an adhoc for iOS. There is an appstore example targetr as well. Remember that you will need different provisioning profiles for Adhoc and Appstore version of iOS apps. Some assembly is required for you own projects.
An example package.json
setup:
{
"name": "YourApp",
"version": "0.1.0",
"description": "An app",
"main": "Resources/app.js",
"dependencies": {
"grunt": "^0.4.5",
"grunt-alloy": "^0.1.0",
"grunt-titanium": "^0.2.2"
},
"devDependencies": {
"grunt": "^0.4.5",
"grunt-alloy": "^0.1.0",
"grunt-contrib-clean": "^0.6.0",
"grunt-shell": "^1.1.1",
"grunt-titanium": "^0.2.2"
},
"scripts": {
"test": "command -v grunt >/dev/null 2>&1 && grunt coverage || { echo >&2 'Grunt is not installed'; }",
"prepublish": "command -v grunt >/dev/null 2>&1 && grunt || { echo >&2 'Grunt is not installed'; }"
},
"repository": {
"type": "git",
"url": "https://bitbucket.org/your-repo/your-app.git"
},
"author": "You!",
"license": "GPL/MIT/BSD/Commercial/Whatever"
}
And a GruntFile.js
to go along with it:
module.exports = function( grunt )
{
// Project configuration.
//
grunt.initConfig(
{
pkg: grunt.file.readJSON( "package.json" ),
settings:
{
ppUuid: "Your provisioning profile UUID (iOS)",
distributionName: "Your distribtion name (iOS)"
},
clean:
{
dist:
{
src: [ "dist" ]
},
coverage:
{
src: [ "dist/coverage" ]
}
},
titanium:
{
build_ios:
{
options:
{
command: "build",
projectDir: ".",
platform: "ios",
buildOnly: true,
}
},
ios_adhoc: {
options:
{
command: "build",
projectDir: ".",
platform: "ios",
target: "dist-adhoc",
buildOnly: true,
distributionName: "<%= settings.distributionName %>",
ppUuid: "<%= settings.ppUuid %>",
outputDir: "./dist/artifacts"
}
},
ios_store: {
options:
{
command: "build",
projectDir: ".",
platform: "ios",
target: "dist-appstore",
buildOnly: true,
distributionName: "<%= settings.distributionName %>",
ppUuid: "<%= settings.ppUuid %>",
outputDir: "./dist/artifacts"
}
},
dev_ios:
{
options:
{
command: "build",
args: "--shadow",
projectDir: ".",
platform: "ios",
buildOnly: false,
}
},
build_android:
{
options:
{
command: "build",
projectDir: ".",
platform: "android",
buildOnly: true
}
},
android_apk:
{
options:
{
command: "build",
projectDir: ".",
platform: "android",
target: "dist-playstore",
buildOnly: true,
keystore: "android.keystore",
alias: "your-app-alias",
password: process.env.KEYSTORE_PASS,
outputDir: "./dist/artifacts"
}
},
dev_android:
{
options:
{
command: "build",
args: "--shadow",
projectDir: ".",
platform: "android",
buildOnly: false
}
},
clean:
{
options:
{
command: "clean",
projectDir: "."
}
}
}
} );
// Load the required plug-ins
//
grunt.loadNpmTasks( "grunt-titanium" );
grunt.loadNpmTasks( "grunt-alloy" );
grunt.loadNpmTasks( "grunt-contrib-clean" );
grunt.loadNpmTasks( "grunt-shell" );
// Default task(s)
//
grunt.registerTask( "default", [ "titanium:ios_adhoc", "titaniun:android_apk" ] );
grunt.registerTask( "coverage", [] );
grunt.registerTask( "ios-adhoc", [ "titanium:ios_adhoc" ] );
grunt.registerTask( "ios", [ "titanium:dev_ios" ] );
grunt.registerTask( "android", [ "titanium:dev_android" ] );
grunt.registerTask( "android-apk", [ "titanium:android_apk" ] );
};
Note that the coverage task is not setup so npm test
doesn't really do anything.
Due to the nature of mobile development this software setup will likely stop working at some point. XCode will need to be updated when Apple says so for instance. Also the globally installed npm modules, Appcelerator SDK and the Android SDK will need to be updated. When this time comes around you will need to rerun all your mobile projects to verify they still build correctly. Having unit tests and code coverage in place is highly recommended but beyond the scope of this document.
A notice will be shown with a download link in the main Jenkins configuration. You need to download the .war file, stop Jenkins, replace the existing .war file and start Jenkins again. The commands to use are:
sudo launchctl unload /Library/LaunchDaemons/org.jenkins-ci.plist
sudo cp ~/Downloads/jenkins.war /Applications/Jenkins/jenkins.war
sudo launchctl load /Library/LaunchDaemons/org.jenkins-ci.plist
Update using App store
Download and install latest installation package from nodejs.org
As an administrator run:
sudo npm update -g
brew update && brew upgrade
sudo su - jenkins
titanium sdk install
Should auto-select the latest SDK when installed. Otherwise run titanium sdk select
and choose the correct one. To see available SDK's simply run: ti sdk
. Note that the SDK selected in your tiApp.xml needs to be available on your CI server.
There is a bug with jenkins on Mac OSX Yosemite 10.10. When the log file for jenkins gets rotated the jenkins user looses write permission. Result is that jenkins can no longer start. To fix it I did:
sudo chown -R jenkins /var/log/jenkins
And then in /private/etc/newsyslog.d/jenkins.conf
you need to replace:
/var/log/jenkins/jenkins.log 644 3 * $D0 J
with
/var/log/jenkins/jenkins.log jenkins:jenkins 644 3 * $D0 J
You need to edit that file with root permissions (I did sudo vi /private/etc/newsyslog.d/jenkins.conf
but you may prefer another editor.
See issue for details: https://issues.jenkins-ci.org/browse/JENKINS-23543 Future versions of the Mac installer should fix this. The issue is unresolved at this time.
UPDATE Nov 3rd 2015: Issue linked above has been labeled as a duplicate of https://issues.jenkins-ci.org/browse/JENKINS-26982 which claims to have been resolved. A new related issue seems to have appeared in https://issues.jenkins-ci.org/browse/JENKINS-26983
In short, always check log permissions and defaults :(
The discard old build plugin allows you to define a post-build action to remove older builds based on a number of criteria. Disk space may become an issue with mobile build artifacts. https://wiki.jenkins-ci.org/display/JENKINS/Discard+Old+Build+plugin