Swift Makefiles – Take 2

A couple of days I ago I started a basic Makefile for compiling Swift code. That was a pretty basic Makefile and didn't actually result in a usable executable or library at the end.

Today we'll remedy that.

Makefile

Below is a Makefile that is starting to pull in more of the options and settings that Xcode uses. My intention is to create a Makefile that can capture most of those settings and allow me to simply clone it for my other projects.

# A more complicated build file that is starting to look a lot like how
# Xcode internally structures its builds. The goal here is to show how
# one might actually go about building a full Xcode-capable build system
# without all of the requirements of Xcode, xcodebuild, and xcproj files.
#
# There will likely be limitations to this process, but lets see how far
# the wind takes us.

## USER CONFIGURABLE SETTINGS ##
CONFIG       = debug
PLATFORM     = macosx
ARCH         = x86_64
MODULE_NAME  = tool
MACH_O_TYPE  = mh_execute

## GLOBAL SETTINGS ##
ROOT_DIR            = $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
BUILD_DIR           = $(ROOT_DIR)/bin
SRC_DIR             = $(ROOT_DIR)/src
LIB_DIR             = $(ROOT_DIR)/lib

TOOLCHAIN           = Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM)
TOOLCHAIN_PATH      = $(shell xcode-select --print-path)/$(TOOLCHAIN)

SWIFT               = $(shell xcrun -f swift) -frontend -c -color-diagnostics

## COMPILER SETTINGS ##
CFLAGS       = -g -Onone
SDK_PATH     = $(shell xcrun --show-sdk-path -sdk $(PLATFORM))

## LINKER SETTINGS ##
LD           = $(shell xcrun -f ld)
LDFLAGS      = -syslibroot $(SDK_PATH) -lSystem -arch $(ARCH) \
                -macosx_version_min 10.10.0 \
                -no_objc_category_merging -L $(TOOLCHAIN_PATH) \
                -rpath $(TOOLCHAIN_PATH)
OBJ_EXT      = 
OBJ_PRE      =

ifeq (mh_dylib, $(MACH_O_TYPE))
    OBJ_EXT  = .dylib
    OBJ_PRE  = lib
    LDFLAGS += -dylib
endif

## BUILD LOCATIONS ##
PLATFORM_BUILD_DIR    = $(BUILD_DIR)/$(MODULE_NAME)/bin/$(CONFIG)/$(PLATFORM)
PLATFORM_OBJ_DIR      = $(BUILD_DIR)/$(MODULE_NAME)/obj/$(CONFIG)/$(PLATFORM)
PLATFORM_TEMP_DIR     = $(BUILD_DIR)/$(MODULE_NAME)/tmp/$(CONFIG)/$(PLATFORM)

SOURCE = $(notdir $(wildcard $(SRC_DIR)/*.swift))

## BUILD TARGETS ##
tool: setup $(SOURCE) link

## COMPILE RULES FOR FILES ##

%.swift:
    $(SWIFT) $(CFLAGS) -primary-file $(SRC_DIR)/$@ \
        $(addprefix $(SRC_DIR)/,$(filter-out $@,$(SOURCE))) -sdk $(SDK_PATH) \
        -module-name $(MODULE_NAME) -o $(PLATFORM_OBJ_DIR)/$*.o -emit-module \
        -emit-module-path $(PLATFORM_OBJ_DIR)/$*~partial.swiftmodule

main.swift:
    $(SWIFT) $(CFLAGS) -primary-file $(SRC_DIR)/main.swift \
        $(addprefix $(SRC_DIR)/,$(filter-out $@,$(SOURCE))) -sdk $(SDK_PATH) \
        -module-name $(MODULE_NAME) -o $(PLATFORM_OBJ_DIR)/main.o -emit-module \
        -emit-module-path $(PLATFORM_OBJ_DIR)/main~partial.swiftmodule

link:
    $(LD) $(LDFLAGS) $(wildcard $(PLATFORM_OBJ_DIR)/*.o) \
        -o $(PLATFORM_BUILD_DIR)/$(OBJ_PRE)$(MODULE_NAME)$(OBJ_EXT)

setup:
    $(shell mkdir -p $(PLATFORM_BUILD_DIR))
    $(shell mkdir -p $(PLATFORM_OBJ_DIR))
    $(shell mkdir -p $(PLATFORM_TEMP_DIR))

While there seems to be a lot going on in this file, it is all fairly straight forward. Basically it just does the following:

  1. Configures the settings for the build.
  2. Compiles each individual file, treating main.swift as special.
  3. Links the object files together to create a combined binary.

The Makefile doesn't support everything yet; for instance, there is no optimization flags being set for release builds. But, it is a start.

Testing Our Work

To test it out, create our go to foo.swift and main.swift files:

foo.swift

public func foo() -> String {
    let message = testing()
    return "Foo - \(message)"
}

main.swift

import Foundation

public func testing() -> String { return "testing!" }

println("Hello, World!")
println("testing: \(testing())")
println("foo: \(foo())")

As you can see, there is a bit of circular usage as both foo and testing are used in both files. This allows us a basic check that each file can see each others symbols and invoke them.

Running make gets us the following output (raw output):

$ make
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift -frontend -c -color-diagnostics -g -Onone -primary-file /Users/dowens/Projects/Playground/SwiftOptions/SwiftTool/SwiftTool/src/foo.swift \
        /Users/dowens/Projects/Playground/SwiftOptions/SwiftTool/SwiftTool/src/main.swift -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.10.sdk \
        -module-name tool -o /Users/dowens/Projects/Playground/SwiftOptions/SwiftTool/SwiftTool/bin/tool/obj/debug/macosx/foo.o -emit-module \
        -emit-module-path /Users/dowens/Projects/Playground/SwiftOptions/SwiftTool/SwiftTool/bin/tool/obj/debug/macosx/foo~partial.swiftmodule
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift -frontend -c -color-diagnostics -g -Onone -primary-file /Users/dowens/Projects/Playground/SwiftOptions/SwiftTool/SwiftTool/src/main.swift \
        /Users/dowens/Projects/Playground/SwiftOptions/SwiftTool/SwiftTool/src/foo.swift -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.10.sdk \
        -module-name tool -o /Users/dowens/Projects/Playground/SwiftOptions/SwiftTool/SwiftTool/bin/tool/obj/debug/macosx/main.o -emit-module \
        -emit-module-path /Users/dowens/Projects/Playground/SwiftOptions/SwiftTool/SwiftTool/bin/tool/obj/debug/macosx/main~partial.swiftmodule
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/ld -syslibroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.10.sdk -lSystem -arch x86_64 -macosx_version_min 10.10.0 -no_objc_category_merging -L /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx -rpath /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx /Users/dowens/Projects/Playground/SwiftOptions/SwiftTool/SwiftTool/bin/tool/obj/debug/macosx/foo.o /Users/dowens/Projects/Playground/SwiftOptions/SwiftTool/SwiftTool/bin/tool/obj/debug/macosx/main.o \
        -o /Users/dowens/Projects/Playground/SwiftOptions/SwiftTool/SwiftTool/bin/tool/bin/debug/macosx/tool

The thing to note is that a tool executable has been built as can be seen in that last line.

$ bin/tool/bin/debug/macosx/tool 
Hello, World!
testing: testing!
foo: Foo - testing!

Running it, we can see that everything seems to be in working order.

So that's it, a somewhat basic Makefile that can be used to create executables and libraries without having to rely on Xcode. Now, should we be doing this? I'll leave that question as an exercise to the reader.

Swift Makefiles – Take 2