Swift logo
New and improved! Now even Swiftier!

In my last post, I expressed some reservations about the method I was using to generate the correct compiler target for the AST command. Well, I appealed to a higher power (the swift-users mailing list) and got back this incredibly helpful response from Jordan Rose:

The Swift target triples are the same as LLVM’s target triples. Your logic matches what Xcode’s currently doing to invoke Swift (and I assume that’s where you got it from :-) ), but there’s no guarantee that it will continue to work. If you really want something stable-ish, I’d suggest doing a -dry-run xcodebuild and extracting the swiftc lines from that.

What an awesome suggestion! And that got me thinking – this would also be a better way to find all the Swift source files that should be included in the AST! Read on for an updated solution.

A Brief Aside While I BASH My Head Against The Wall

One of my goals in writing this script was to keep it as simple as possible, and not require any additional dependencies or installed tools. In that vein, I wrote it as a Bash shell script because you can easily enter it directly into the content area of the Run Script Build Phase.

However, time and again I found myself running into Bash’s limitations. Especially it’s deficiencies in handling regular expressions. While working on the new and improved version, I finally reached a breaking point and decided to abandon the Bash script altogether. But what to replace it with?

Well, why not Swift? I know Swift and (presumably) so do you. And one of the great things about Swift is that it can be executed from the command line, like a shell script. So I forged ahead and re-wrote the entire script in Swift. Using Swift in this way still has a few rough edges, but I would definitely consider it for my future scripting needs.

The New And Improved Script

Without further ado, here is the new and improved script:

// if running directly as a script, use this shebang
// #!/usr/bin/env xcrun --sdk macosx swift

import Foundation

// executes shell commands for us
// hat-tip to http://stackoverflow.com/a/26972043
func shell(launchPath: String, arguments: [String]) -> String
{
	let task = NSTask()
	task.launchPath = launchPath
	task.arguments = arguments

	let pipe = NSPipe()
	task.standardOutput = pipe
	task.launch()

	let data = pipe.fileHandleForReading.readDataToEndOfFile()
	let output: String = NSString(data: data, encoding: NSUTF8StringEncoding)! as String

	return output
}

// removes backslash escaping from a string
func unescapedString(string: String) -> String
{
	let regex = try! NSRegularExpression(pattern: "\\\\(.)", options: [])
	let nsString = string as NSString
	return regex.stringByReplacingMatchesInString(string, options: [], range: NSMakeRange(0, nsString.length), withTemplate: "$1")
}

// wrapper for calling NSRegularExpression
func matchRegex(regex: String, text: String) throws -> [NSTextCheckingResult]
{
	let regex = try NSRegularExpression(pattern: regex, options: [])
	let nsString = text as NSString
	return regex.matchesInString(text, options: [], range: NSMakeRange(0, nsString.length))
}

// extract Xcode environment variables
let envVars = NSProcessInfo.processInfo().environment
let projectName = envVars["PROJECT_NAME"]!
let targetName = envVars["TARGET_NAME"]!
let configuration = envVars["CONFIGURATION"]!
let sdkName = envVars["SDK_NAME"]!
let frameWorkPath = envVars["FRAMEWORK_SEARCH_PATHS"]!.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet())

// do a dry run to find out how Xcode would compile these swift files
let dryRunResults = shell("/usr/bin/xcrun", arguments: ["xcodebuild", "-project", "\(projectName).xcodeproj", "-target", targetName, "-configuration", configuration, "-sdk", sdkName, "-dry-run"])

// find the actual swift compile command
let compileRegex = ".*/swiftc.*[\\s]+-target[\\s]+([^\\s]*).*"

if let compileMatch = try matchRegex(compileRegex, text: dryRunResults).first {
	let compileString = (dryRunResults as NSString).substringWithRange(compileMatch.range)

	// extract the target setting
	let target = (dryRunResults as NSString).substringWithRange(compileMatch.rangeAtIndex(1))

	// find each swift file that is included in the compilation
	let sourceRegex = "(/([^ ]|(?<=\\\\) )*\\.swift(?<!\\\\)) "

	if let sourceMatches = try? matchRegex(sourceRegex, text: compileString) {
		let sourceFiles = sourceMatches.map {unescapedString((compileString as NSString).substringWithRange($0.rangeAtIndex(1)))}

		var arguments = ["-sdk", sdkName, "swiftc", "-dump-ast", "-F", frameWorkPath, "-target", target]
		arguments.appendContentsOf(sourceFiles)

		print("arguments: \(arguments)")

		// run the command to dump the AST
		let astResult = shell("/usr/bin/xcrun", arguments: arguments)
		print(astResult)
		exit(0)
	}
}

exit(1)

This script does an xcodebuild dry run and extracts some important info from it. It then feeds that into the swiftc command as before.

To execute it from the Run Script Build Phase, you can just use this command:

/usr/bin/env xcrun --sdk macosx swift path_to/GenerateAST.swift

Now if you use the proper shebang, you can actually execute a Swift file directly. However, that requires setting the executable bit on the file, a bit of housekeeping I’d rather avoid. So I went with the above invocation. You could also type Swift code directly into the content area of your Run Script Build Phase, if you set the shell to /usr/bin/env xcrun –sdk macosx swift. But then you wouldn’t get any syntax highlighting or other niceties.

So there we go – dumping the Swift AST using Swift. Now I’m going to be able to move on to the fun stuff – code generation!