Last active
June 3, 2016 17:26
-
-
Save rosslebeau/e5f6be6d2e6fd5f2109082b47c96cc96 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
require 'fileutils' | |
require 'optparse' | |
require 'json' | |
IPA_EXPORT_DIR = 'WFIPAExport' | |
ARCHIVE_DIR = 'WFArchive' | |
XCARCHIVE_NAME = 'WF-iOS.xcarchive' | |
IPA_NAME = 'WF-iOS.ipa' | |
PLIST_NAME = 'WF-iOS.plist' | |
class JenkinsIOSBuilder | |
# Let's pretend this has a serious type system and implement a tuple with an array | |
# returns: ([String], String, String) | |
def buildWorkspace(projName, scheme, workspacePath, buildID) | |
createOrCleanBuildArtifactDirs | |
resetKeychain | |
xcworkspacePath = "#{workspacePath}/#{projName}.xcworkspace" | |
xcarchivePath = "#{workspacePath}/#{ARCHIVE_DIR}/#{XCARCHIVE_NAME}" | |
ipaPath = "#{workspacePath}/#{IPA_EXPORT_DIR}/#{IPA_NAME}" | |
profileID = profileIDForScheme(scheme) | |
system "xcodebuild clean -workspace \"#{xcworkspacePath}\" -scheme \"#{scheme}\"" | |
system "xcodebuild -workspace \"#{xcworkspacePath}\" -scheme \"#{scheme}\" archive -archivePath \"#{xcarchivePath}\" -destination generic/platform=iOS PROVISIONING_PROFILE=\"#{profileID}\" CODE_SIGN_IDENTITY=\"iPhone Distribution: Wellframe\"" | |
system "xcodebuild -exportArchive -exportFormat IPA -archivePath \"#{xcarchivePath}\" -exportPath \"#{ipaPath}\" -exportProvisioningProfile=\"#{profileID}\" -exportSigningIdentity=\"iPhone Distribution: Wellframe\"" | |
addEntitlementsToIPAFromArchive(ipaPath, xcarchivePath) | |
infoPlistPath = "#{xcarchivePath}/Info.plist" | |
downloadPlistPath = "#{IPA_EXPORT_DIR}/#{PLIST_NAME}" | |
createDownloadPlist(infoPlistPath, downloadPlistPath, buildID) | |
return [[ipaPath, downloadPlistPath], getVersion(infoPlistPath), getBuild(infoPlistPath)] | |
end | |
def profileIDForScheme(scheme) | |
if scheme == 'Staging' | |
return '1b9f6afe-29ac-11e6-b67b-9e71128cae77' #Random one I generated, not my real profile ID! | |
elsif scheme == 'Enterprise Production' | |
return '320de450-42df-1a22-1bfc-929be1f1b5b5' #Also random | |
else | |
puts "wf-ios-build: Unknown scheme: #{scheme}. Add provisioning profile information for that scheme or try again with a supported scheme." | |
exit(false) | |
end | |
end | |
def createOrCleanBuildArtifactDirs | |
if Dir.exists?(IPA_EXPORT_DIR) | |
FileUtils.remove_dir IPA_EXPORT_DIR | |
end | |
Dir.mkdir IPA_EXPORT_DIR | |
if Dir.exists?(ARCHIVE_DIR) | |
FileUtils.remove_dir ARCHIVE_DIR | |
end | |
Dir.mkdir ARCHIVE_DIR | |
end | |
# This is needed because the keychain may be locked on our build server, e.g. after booting, after inactivity | |
# Locking first is just precaution | |
def resetKeychain | |
system 'security lock-keychain login.keychain' | |
system 'security unlock-keychain -p keychainpassword login.keychain' #'keychainpassword' should be replaced with your actual keychain password | |
system 'security lock-keychain WellframeDev.keychain' | |
system 'security unlock-keychain WellframeDev.keychain' | |
end | |
def addEntitlementsToIPAFromArchive(ipaPath, archivePath) | |
# This is needed to get the entitlements file in the IPA | |
# Due to a bug in xcodebuild. See http://www.openradar.me/21309940 | |
# And the writeup I posted here: http://rosslebeau.com/2016/xcodebuild-thinks-millenials-entitled | |
appFileName = '' | |
Dir.chdir("#{archivePath}/Products/Applications/") do | |
appFileName = Dir["*.app"][0] | |
end | |
Dir.chdir("./#{IPA_EXPORT_DIR}") do | |
system "unzip \"#{ipaPath}\" && | |
cp \"#{archivePath}/Products/Applications/#{appFileName}/archived-expanded-entitlements.xcent\" \"Payload/#{appFileName}/\" && | |
rm -rf \"#{ipaPath}\" | |
zip -r \"#{ipaPath}\" Payload/" | |
end | |
end | |
def createDownloadPlist(infoPlistPath, downloadPlistPath, buildID) | |
version = getVersion(infoPlistPath) | |
bundleID = %x(/usr/libexec/PlistBuddy -c "print :ApplicationProperties:CFBundleIdentifier" \"#{infoPlistPath}\") | |
bundleID.chomp! | |
buildNumber = getBuild(infoPlistPath) | |
# Yeah this is dumb but the plist support for Ruby is minimal and not even a gem from what I can see. | |
File.write(downloadPlistPath, | |
"<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\"> | |
<plist version=\"1.0\" mimeType=\"application/xml\"> | |
<dict> | |
<key>items</key> | |
<array> | |
<dict> | |
<key>assets</key> | |
<array> | |
<dict> | |
<key>kind</key> | |
<string>software-package</string> | |
<key>url</key> | |
<string>https://www.your-build-page.com/#{buildID}/#{IPA_NAME}</string> | |
</dict> | |
</array> | |
<key>metadata</key> | |
<dict> | |
<key>bundle-identifier</key> | |
<string>#{bundleID}</string> | |
<key>bundle-version</key> | |
<string>#{version} #{buildNumber}</string> | |
<key>kind</key> | |
<string>software</string> | |
<key>title</key> | |
<string>Wellframe iOS Staging</string> | |
</dict> | |
</dict> | |
</array> | |
</dict> | |
</plist>" | |
) | |
end | |
def getVersion(infoPlistPath) | |
version = %x(/usr/libexec/PlistBuddy -c "print :ApplicationProperties:CFBundleShortVersionString" \"#{infoPlistPath}\") | |
version.chomp! | |
return version | |
end | |
def getBuild(infoPlistPath) | |
build = %x(/usr/libexec/PlistBuddy -c "print :ApplicationProperties:CFBundleVersion" \"#{infoPlistPath}\") | |
build.chomp! | |
return build | |
end | |
end | |
class WFBuildUploader | |
BUILD_CONTENT_SERVER = 'www.your-build-distribution-webserver.com' | |
INDEX_JSON_FILENAME = 'build-index.json' | |
METADATA_JSON_FILENAME = 'metadata.json' | |
def upload(filesToUpload, wf_buildID, wf_buildTitle, version, buildNumber, commitHash) | |
updateBuildIndex(wf_buildID) | |
system "ssh -p 22222 #{BUILD_CONTENT_SERVER} 'mkdir /var/www/path-to-build-web/#{wf_buildID}'" | |
filesToUpload << makeMetadataJson(wf_buildID, wf_buildTitle, version, buildNumber, commitHash) | |
filesToUpload.each {|file| | |
system "scp -P 22222 \"#{file}\" #{BUILD_CONTENT_SERVER}:/var/www/path-to-build-web/#{wf_buildID}" | |
} | |
end | |
def updateBuildIndex(wf_buildID) | |
tempBuildIndexFilename = 'temp-build-index.json' | |
system "scp -P 22222 #{BUILD_CONTENT_SERVER}:/var/www/path-to-build-web/#{INDEX_JSON_FILENAME} #{tempBuildIndexFilename}" | |
buildIndex = JSON.parse(File.open(tempBuildIndexFilename, 'r+').read) | |
if !buildIndex.include?(wf_buildID) | |
buildIndex << wf_buildID | |
File.write(tempBuildIndexFilename, JSON.pretty_generate(buildIndex)) | |
system "scp -P 22222 #{tempBuildIndexFilename} #{BUILD_CONTENT_SERVER}:/var/www/path-to-build-web/#{INDEX_JSON_FILENAME}" | |
end | |
system "rm #{tempBuildIndexFilename}" | |
end | |
def makeMetadataJson(wf_buildID, wf_buildTitle, version, buildNumber, commitHash) | |
metadataHash = {'name' => wf_buildTitle, | |
'version' => version, | |
'jenkins-build-num' => buildNumber, | |
'commit' => commitHash, | |
'link' => "itms-services://?action=download-manifest&url=https://www.your-build-page.com/#{wf_buildID}/#{PLIST_NAME}"} | |
File.write(METADATA_JSON_FILENAME, JSON.pretty_generate(metadataHash)) | |
return METADATA_JSON_FILENAME | |
end | |
end | |
class WFIOSBuildOptions | |
attr_reader :projectName | |
attr_reader :scheme | |
attr_reader :workspacePath | |
attr_reader :title | |
attr_reader :buildID | |
def initialize | |
@optionParser = OptionParser.new do |opts| | |
opts.banner = "Usage: XCImageAssetsCleaner [options] [.xcassets folder]" | |
opts.separator "" | |
opts.separator "Options:" | |
opts.on("--project PROJNAME", | |
"The name of the project to build (name of xcworkspace).") do |projName| | |
@projectName = projName | |
end | |
opts.on("--scheme SCHEME", | |
"The name of the scheme to build.") do |scheme| | |
@scheme = scheme | |
end | |
# Currently we only support the current working directory. Can add this later but it's not important right now | |
# opts.on("--workspace WORKSPACE", | |
# "The path to the workspace with the project files in it.") do |workspace| | |
# @workspacePath = workspace | |
# end | |
opts.on("--title TITLE", | |
"The title that should display on the builds web page.") do |title| | |
@title = title | |
end | |
opts.on("--buildid ID", | |
"The identifier for this build project.") do |id| | |
@buildID = id | |
end | |
end | |
end | |
def parse! args | |
@optionParser.parse!(args) | |
end | |
def help | |
return @optionParser.help | |
end | |
end | |
# Script start | |
options = WFIOSBuildOptions.new | |
options.parse!(ARGV) | |
builder = JenkinsIOSBuilder.new | |
buildObjects = builder.buildWorkspace(options.projectName, options.scheme, Dir.pwd, options.buildID) | |
filesToUpload = buildObjects[0] | |
version = buildObjects[1] | |
buildNumber = buildObjects[2] | |
commitHash = %x(echo $GIT_COMMIT) | |
commitHash.chomp! | |
uploader = WFBuildUploader.new | |
uploader.upload(filesToUpload, options.buildID, options.title, version, buildNumber, commitHash) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment