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:
- Configures the settings for the build.
- Compiles each individual file, treating
main.swift
as special. - 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.