Skip to content

Instantly share code, notes, and snippets.

@jon-cotton
Last active July 5, 2016 11:38
Show Gist options
  • Save jon-cotton/21bd64017efe063e3253d51c69eb7dd8 to your computer and use it in GitHub Desktop.
Save jon-cotton/21bd64017efe063e3253d51c69eb7dd8 to your computer and use it in GitHub Desktop.
#!/usr/bin/swift
import Foundation
// MARK:- Functions
func runShellCommand(cmd: String, env: [String : String]? = nil) -> (output: [String], error: [String], exitCode: Int32) {
var output : [String] = []
var error : [String] = []
let task = NSTask()
task.launchPath = "/bin/bash"
task.arguments = ["-c", "\(cmd)"]
if let env = env {
task.environment = env
}
let outpipe = NSPipe()
task.standardOutput = outpipe
let errpipe = NSPipe()
task.standardError = errpipe
task.launch()
let outdata = outpipe.fileHandleForReading.readDataToEndOfFile()
if var string = String(data: outdata, encoding: NSUTF8StringEncoding) {
string = string.stringByTrimmingCharactersInSet(NSCharacterSet.newlineCharacterSet())
output = string.componentsSeparatedByString("\n")
}
let errdata = errpipe.fileHandleForReading.readDataToEndOfFile()
if var string = String(data: errdata, encoding: NSUTF8StringEncoding) {
string = string.stringByTrimmingCharactersInSet(NSCharacterSet.newlineCharacterSet())
error = string.componentsSeparatedByString("\n")
}
task.waitUntilExit()
let status = task.terminationStatus
return (output, error, status)
}
typealias CommandRunner = (cmd: String) -> ()
func pathExists(path: String) -> Bool {
let fileManager = NSFileManager.defaultManager()
return fileManager.fileExistsAtPath(path)
}
struct BuildError: ErrorType {
let description: String
init(description: String = "Build Failed") {
self.description = description
}
}
enum Project: String {
case ios
case android
case service
func installDependencies(run: CommandRunner) {
switch self {
case .ios:
print("iOS dependencies")
case .android:
print("Android dependencies")
run(cmd: "brew tap caskroom/cask")
run(cmd: "brew install brew-cask")
run(cmd: "brew tap caskroom/versions")
run(cmd: "brew cask install java7")
// only update the sdks & tools if android-sdk isn't already installed
// if these change, you will need to rebuild without cache on circle
if !pathExists("/usr/local/Cellar/android-sdk") {
run(cmd: "brew install android-sdk")
run(cmd: "echo y | android --silent update sdk --no-ui --all --filter tools,platform-tools")
run(cmd: "echo y | android --silent update sdk --no-ui --all --filter build-tools-23.0.3,android-23")
run(cmd: "echo y | android --silent update sdk --no-ui --all --filter extra-android-m2repository,extra-google-m2repository")
} else {
print("Skipping Android SDK and tools updates")
}
case .service:
print("Ruby dependencies")
run(cmd: "cd ./service && bundle")
}
}
func build(run: CommandRunner) {
switch self {
case .ios:
print("iOS build")
run(cmd: "xcodebuild test -workspace ios/Sky/Sky.xcworkspace -scheme Sky -destination 'platform=iOS Simulator,name=iPhone 6,OS=9.3'")
case .android:
print("Android build")
run(cmd: "export ANDROID_HOME=/usr/local/Cellar/android-sdk && ./android/gradlew -p ./android test")
case .service:
print("Ruby build")
run(cmd: "cd ./service && bundle exec rspec")
}
}
}
func changedProjects(firstCommit firstCommit: String?, lastCommit: String?) -> Set<Project> {
guard let firstCommit = firstCommit
else {
print("No commits found to build")
return []
}
var command = "git --no-pager show --pretty=\"format:\" --name-only \(firstCommit)"
if let lastCommit = lastCommit {
command = "git diff --name-only \(firstCommit)..\(lastCommit)"
}
let changes = runShellCommand(command)
guard changes.exitCode == 0
else {
print(changes.error.reduce("", combine: { "\($0)\n\($1)" }))
return []
}
let allChangedFolders = changes.output.map({ String($0).componentsSeparatedByString("/")[0] })
let uniqueChangedFolders = Set(allChangedFolders)
var projects = Set<Project>()
for folder in uniqueChangedFolders {
guard let project = Project(rawValue: folder)
else { continue }
projects.insert(project)
}
return projects
}
// MARK:- Run
let env = NSProcessInfo.processInfo().environment as [String : String]
let args = Process.arguments
var shouldInstallDependencies = false
var shouldBuild = false
var shouldPrintVerboseOutput = false
// not configurable
let shouldStopOnErrors = true
if args.contains("setup") {
shouldInstallDependencies = true
}
if args.contains("build") {
shouldBuild = true
}
if args.contains("--verbose") {
shouldPrintVerboseOutput = true
}
if !shouldInstallDependencies && !shouldBuild {
print("Usage: circle-build-helper.swift setup | build [--verbose]")
exit(1)
}
let runner: CommandRunner = { (cmd: String) -> () in
print(">>> Running '\(cmd)'")
let cmdReturn = runShellCommand(cmd)
if cmdReturn.error.count > 1 {
print(">>> Errors from '\(cmd)'")
print(cmdReturn.error.reduce("", combine: { "\($0)\n\($1)" }))
}
if shouldPrintVerboseOutput {
print(">>> Output from \(cmd)")
print(cmdReturn.output.reduce("", combine: { "\($0)\n\($1)" }))
}
if shouldStopOnErrors {
guard cmdReturn.exitCode == 0
else {
print("!!! '\(cmd)' returned non-zero exit code")
exit(1)
}
}
}
// work out which projects need working on
var firstCommit: String?
var lastCommit: String?
let rangeSeparator = "..."
if let
compareURLString = env["CIRCLE_COMPARE_URL"],
range = compareURLString.componentsSeparatedByString("/").last
{
if range.rangeOfString(rangeSeparator) != nil {
firstCommit = range.componentsSeparatedByString(rangeSeparator)[0]
lastCommit = range.componentsSeparatedByString(rangeSeparator)[1]
}
} else {
if let commitSHA = env["CIRCLE_SHA1"] {
firstCommit = commitSHA
} else {
print("Neither of CIRCLE_SHA1 or CIRCLE_COMPARE_URL env vars defined")
}
}
var projectsToBuild = changedProjects(firstCommit: firstCommit, lastCommit: lastCommit)
if env["SKY_FORCE_ANDROID_BUILD"] != nil {
print("Forcing an android build")
projectsToBuild.insert(Project.android)
}
if env["SKY_FORCE_IOS_BUILD"] != nil {
print("Forcing an ios build")
projectsToBuild.insert(Project.ios)
}
if env["SKY_FORCE_SERVICE_BUILD"] != nil {
print("Forcing a service build")
projectsToBuild.insert(Project.service)
}
if projectsToBuild.isEmpty {
print("No projects to build")
exit(0)
}
// setup/build
for project in projectsToBuild {
if shouldInstallDependencies {
project.installDependencies(runner)
}
if shouldBuild {
project.build(runner)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment