Monday 1 July 2013

iOS Deployments over the web using PHP


Deployments over iOS are quite painful. When a developer sends you a release you can try on your phone, they send over two files – something.mobileprovision and youApp.ipa. The first file lists the devices on which your app will run and the second one is the actual binary. In iOS 4, the mobile provisioning is also included inside your .ipa file, but on the older OS it wasn’t the case. So with each release, you need to ensure that you send both the files.
Now, once you receive the files, you need to drag both of them to iTunes. If iTunes accepts them (which also depends on the version of iTunes you have installed), then you can connect your device and sync the new app so that it installs on your device.
Anyways, so this means that you need a machine everywhere to install the app. Also, you need to make sure that you sync with your own iTunes otherwise all the other apps that you have will get deleted!
Not only that, if you develop applications the way we do, you can expect a new release every 3-5 days. Coupled with the time difference of 12 hours with most of our clients, the time wastage increases manifold.
So, when Apple came out saying that they have wifi-deployment, we were really excited. Though as always turning the excitement into actual work and ensuring that you create a system to send releases to your clients takes a lot of time.
Last week, we finally managed to get the damn thing live for our clients!
This is why I thought I might go into some details about what is required to get such a system live. Here are the things that you need:
1. The concept of deployment on the web is simple. There are two files that need to be installed on the device to get your application to run. As explained above, one is the something.mobileprovision file and the other is the yourApp.ipa. Apple introduced the concept of another file called manifest.plist which will link both the files and if you click on a link to this manifest.plist, you get an option asking you if you would like to install this application. If you click yes, the app gets downloaded and installs itself.
2. Considering that iOS is just around the corner, we’ll forget about the older OS (by that I mean 3.x). Now, if we’re working with iOS 4.x (in case you aren’t, please save yourself the tyranny of supporting older devices and show this and this to your clients. Believe me, they will agree to skip OS3 if you show this.)
Okay, now that your clients have agreed to skype iOS 3, you can smile and start with the next steps.
3. Now, all you need to do, is give the developer an option to upload the .ipa file. The .ipa file contains the mobile provisioning file inside it, so you don’t need to ask them to upload anything else.
To upload the file, I used this really cool code – http://valums.com/ajax-upload/. It allows everything that you can hopefully want to upload. The steps to do it are simple, and are available on their website. Do let me know if you have any issues with it.
4. Okay, now you have a form to upload your file, and once your file reaches the server, the interesting part starts. Here is how I am handling the upload
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
private function processFile() {
// The final response to send back to the browser
$response = array();
 
// Get the post value for file (we're passing the name of the file here)
// And make sure you check your routines for checking if this is valid etc
$file = $_POST['file']
 
// DISK_ROOT is defined in the project
$base_path = DISK_ROOT . '/app/cache/uploads/';
 
// Create a variable with the hash of the project
$project_path = $base_path . $project_info[0]['hash'];
 
if(file_exists($project_path) && is_dir($project_path)) {
// The structure should look like this
// l234o23o4oiu2o3u42/ (hash for the project)
// 22978349273/ (has for the release)
// manifest.plist
// project-release.ipa
// embedded.mobileprovision
// archive.zip
// tmp/
// project-release-copy.ipa
// expanded files go here
 
$release_path = createHash($file . time(), 10);
while(file_exists($release_path)) {
$release_path = createHash($file . rand(10,99) . time(), 10);
}
mkdir($project_path . '/' . $release_path, 0777, true);
mkdir($project_path . '/' . $release_path . '/tmp', 0777, true);
// Copy the ipa to project/release/.ipa
copy($base_path . $file, $project_path . '/' . $release_path . '/' . $file);
// Copy the ipa to project/release/tmp/.ipa
copy($base_path . $file, $project_path . '/' . $release_path . '/tmp/' . $file);
// save the current directory and go to tmp
$current_working_directory = getcwd();
chdir($project_path . '/' . $release_path . '/tmp/');
// Unzip the ipa file
$message = '';
exec('unzip ' . $file, $message);
if(!is_dir($project_path . '/' . $release_path . '/tmp/Payload')) {
$response = array('error' => 'This ipa is invalid');
$this->set('response', $response);
return;
}
$r = dir($project_path . '/' . $release_path . '/tmp/Payload');
$app_path = '';
while(false !== ($entry = $r->read())) {
if( ($entry != '.') && ($entry != '..') && (strpos($entry, '.app') !== false) ) {
$app_path = $entry;
}
}
if($app_path == '') {
$response = array('error' => 'This ipa is invalid');
$this->set('response', $response);
return;
}
$info_plist_path = $project_path . '/' . $release_path . '/tmp/Payload/' . $app_path . '/Info.plist';
$provision_path = $project_path . '/' . $release_path . '/tmp/Payload/' . $app_path . '/embedded.mobileprovision';
// Copy mobile provision to release folder
copy($provision_path, $project_path . '/' . $release_path . '/embedded.mobileprovision');
// Read the info.plist and get values out of it
require_once(DISK_ROOT . '/app/external/plist/CFPropertyList.php');
$plist = new CFPropertyList( $info_plist_path, CFPropertyList::FORMAT_BINARY );
$plist_data = $plist->toArray();
$ipa_url = APPLICATION_ROOT . '/app/cache/uploads/' . $project_info[0]['hash'] . '/' . $release_path . '/' . $file;
$bundle_id = _g($plist_data, 'CFBundleIdentifier');
$bundle_version = _g($plist_data, 'CFBundleVersion');
$title = _g($plist_data, 'CFBundleDisplayName');
$xml_data = $this->createManifest($ipa_url, $bundle_id, $bundle_version, $title);
// Write the manifest.plist
$f = new File();
$f->write('/app/cache/uploads/' . $project_info[0]['hash'] . '/' . $release_path . '/manifest.plist', $xml_data);
// Save values to database
} else {
$response = array('error' => 'The project directory does not exist');
}
}
5. Now that you have the values, you need to create manifest.plist, which will look something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
private function createManifest( $ipa_url, $bundle_id, $bundle_version, $title ) {
$xml = '<?xml version="1.0" encoding="UTF-8"' . urldecode( urlencode('?') . urlencode('>') ) . '
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>items</key>
<array>
<dict>
<key>assets</key>
<array>
<dict>
<key>kind</key>
<string>software-package</string>
<key>url</key>
<string>' . $ipa_url . '</string>
</dict>
</array>
<key>metadata</key>
<dict>
<key>bundle-identifier</key>
<string>' . $bundle_id . '</string>
<key>bundle-version</key>
<string>' . $bundle_version . '</string>
<key>kind</key>
<string>software</string>
<key>title</key>
<string>' . $title . '</string>
</dict>
</dict>
</array>
</dict>
</plist>';
return $xml;
}
 
}
6. That is it!
I should point out here that the cool script which lets me read the binary plist data is by Rodney Rehm which can be found here – https://github.com/rodneyrehm/CFPropertyList
Thanks for sticking around till the end, and do let me know if you have any issues with creating your own.

No comments:

Post a Comment