Skip to content

Instantly share code, notes, and snippets.

@rosslebeau
Last active June 3, 2016 17:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rosslebeau/e5f6be6d2e6fd5f2109082b47c96cc96 to your computer and use it in GitHub Desktop.
Save rosslebeau/e5f6be6d2e6fd5f2109082b47c96cc96 to your computer and use it in GitHub Desktop.
#!/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