Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
#!/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
You can’t perform that action at this time.