This post is a complement to introductory post Components: taking a step back from Dependency Management about the Components: bare-bones dependency management system that was recently introduced to our iOS community by AlexDenisov. Components go even further than Carthage by providing you with a complete control over how you manage your dependencies.
It is strongly recommended to read that introductory post first as this post targets Components and shows how a maintainer of a static iOS framework can easily build his Framework and distribute it as a Component using Make.
Make is the worst build system ever created except for every other build system created after Make.
Those who don’t know Make are doomed to repeat it.
@fogus treat make as a parallel, pure-functional dataflow programming language and it remains pretty manageable in my experience.
According to Wikipedia initial release of Make was made in “1977, 38 years ago”. Without a doubt it is the most widespread, stable and still very simple system so actually it is not really that many of choices that we have for our bare-bones style of dependency management with Components.
I personally have been using Bash scripts to manage my build scripts but thanks to Alex Denisov I learned about caching capabitilies of Make which shine when used inside Components infrastructure: if you already downloaded the zip file of third-party library you do not download it again as longer it is cached, if you already unzipped that file you will not unzip it again, because Make knows that it is there, if you copied that library’s final artefacts to your project you will not be copying them over and over again and all that results, to say it using marketing words, in blazing fast performance of your build scripts given Components were cached once. Aside from this caching feature, Make itself is very fast – even if you run
./components.sh install the first time, it works much faster than for example
pod install as those all .make files are just direct bare-bone instructions on how to fetch, compile and copy your third-party libraries in a way that those library require. This becomes especially impressive when you run your project on CI where cached Components are installed just immediately.
Instead of crafting artifical example I have rather decided to show how I use Make to maintain my own library: CompositeOperations.
CompositeOperations is distributed as static framework so I also recommend reading the following topic on StackOverflow where Petr Korolev and I explain how to create iOS Static Framework: How to export “fat” Cocoa Touch Framework (for Simulator and Device)?
Building a static iOS framework
Here is working version of Makefile that I currently use to create iOS Static Framework from CompositeOperations. As it is always with build scripts they are subject to change but still this version is stable enough and works for us during latest releases of CompositeOperations: CompositeOperations/Makefile.iOS.
# Makefile.iOS NAME=CompositeOperations XCODEPROJ=DevelopmentApp/DevelopmentApp.xcodeproj CONFIGURATION=Release SCHEME=CompositeOperations-iOS SIMULATOR='platform=iOS Simulator,name=iPhone 6s Plus' FRAMEWORK_FOLDER=$(NAME).framework ### Paths BUILD_PATH=$(PWD)/Build BUILD_PATH_SIMULATOR=$(BUILD_PATH)/$(CONFIGURATION)-iphonesimulator BUILD_PATH_IPHONE=$(BUILD_PATH)/$(CONFIGURATION)-iphoneos BUILD_PATH_UNIVERSAL=$(BUILD_PATH)/$(CONFIGURATION)-universal BUILD_PATH_UNIVERSAL_FRAMEWORK_FOLDER=$(BUILD_PATH_UNIVERSAL)/$(FRAMEWORK_FOLDER) BUILD_PATH_UNIVERSAL_FRAMEWORK_BINARY=$(BUILD_PATH_UNIVERSAL_FRAMEWORK_FOLDER)/$(NAME) DISTRIBUTION_PATH=$(PWD)/Distribution ZIPBALL_NAME=$(NAME)-iOS.zip ZIPBALL_PATH=$(DISTRIBUTION_PATH)/$(ZIPBALL_NAME) ### Colors RESET=\033[0;39m RED=\033[0;31m GREEN=\033[0;32m ### Actions .PHONY: all archive clean test build validate zip default: test archive: test build validate zip test: xcodebuild -project $(XCODEPROJ) \ -scheme $(SCHEME) \ -sdk iphonesimulator \ -destination $(SIMULATOR) \ clean test build: xcodebuild -project $(XCODEPROJ) \ -scheme $(SCHEME) \ -sdk iphonesimulator \ -destination $(SIMULATOR) \ -configuration $(CONFIGURATION) \ CONFIGURATION_BUILD_DIR=$(BUILD_PATH_SIMULATOR) \ clean build xcodebuild -project $(XCODEPROJ) \ -scheme $(SCHEME) \ -sdk iphoneos \ -configuration $(CONFIGURATION) \ CONFIGURATION_BUILD_DIR=$(BUILD_PATH_IPHONE) \ clean build rm -rf $(BUILD_PATH_UNIVERSAL) mkdir -p $(BUILD_PATH_UNIVERSAL) cp -Rv $(BUILD_PATH_IPHONE)/$(FRAMEWORK_FOLDER) $(BUILD_PATH_UNIVERSAL) lipo $(BUILD_PATH_SIMULATOR)/$(FRAMEWORK_FOLDER)/$(NAME) $(BUILD_PATH_IPHONE)/$(FRAMEWORK_FOLDER)/$(NAME) -create -output $(BUILD_PATH_UNIVERSAL_FRAMEWORK_BINARY) validate: validate.i386 validate.x86_64 validate.armv7 validate.arm64 validate.%: @printf "Validating $*... " @lipo -info $(BUILD_PATH_UNIVERSAL_FRAMEWORK_BINARY) | grep -q '$*' && echo "$(GREEN)Passed$(RESET)" || (echo "$(RED)Failed$(RESET)"; exit 1) zip: mkdir -p $(DISTRIBUTION_PATH) cd $(BUILD_PATH_UNIVERSAL) && zip -r -FS $(DISTRIBUTION_PATH)/$(ZIPBALL_NAME) $(FRAMEWORK_FOLDER) clean: rm -rf $(BUILD_PATH) rm -rf $(DISTRIBUTION_PATH)
Creating a Component
Here is corresponding
.make file that is intended to be used by a consumer of ComponentOperations as a Component: CompositeOperations/Components.make/CompositeOperations.make. Give it a look before reading further.
Components.make/CompositeOperations.make is not clear I recommend re-reading introductory post about Components one more time: Components: taking a step back from Dependency Management.
Most of the following should be self explanatory, I will just comment on things that are relevant to Components infrastructure:
# Components.make/CompositeOperations.make NAME=CompositeOperations VERSION=0.8.5 GH_REPO=stanislaw/CompositeOperations ZIPBALL_URL=https://github.com/$(GH_REPO)/releases/download/$(VERSION)/CompositeOperations-iOS.zip ### Paths COMPONENTS_BUILD_CACHE_PATH ?= $(HOME)/Library/Caches/Components COMPONENTS_INSTALL_PATH ?= ./Components
These two “global” variables COMPONENTS_BUILD_CACHE_PATH and COMPONENTS_INSTALL_PATH are to be inherited from parent
components.sh script who
exports them, but “?=” is used to still allow running this
.make file in isolation from parent Components infrastructure.
COMPONENT_BUILD_PATH=$(COMPONENTS_BUILD_CACHE_PATH)/$(NAME) COMPONENT_SOURCE_PATH=$(COMPONENT_BUILD_PATH)/$(NAME)-$(VERSION) COMPONENT_FRAMEWORK_PATH=$(COMPONENT_SOURCE_PATH)/$(NAME).framework COMPONENT_INSTALL_PATH=$(COMPONENTS_INSTALL_PATH)/$(NAME) ZIPBALL_PATH=$(COMPONENT_BUILD_PATH)/$(NAME)-$(VERSION).zip ### Targets .PHONY: install update uninstall clean prepare purge install: $(COMPONENT_INSTALL_PATH) uninstall: rm -rf $(COMPONENT_INSTALL_PATH) update: uninstall install clean: rm -rf $(COMPONENT_SOURCE_PATH) rm -rf $(ZIPBALL_PATH) purge: uninstall clean ### Artefacts $(COMPONENT_INSTALL_PATH): $(COMPONENT_SOURCE_PATH) mkdir -p $(COMPONENT_INSTALL_PATH) cp -Rv $(COMPONENT_FRAMEWORK_PATH) $(COMPONENT_INSTALL_PATH) $(COMPONENT_SOURCE_PATH): $(ZIPBALL_PATH) unzip $(ZIPBALL_PATH) -d $(COMPONENT_SOURCE_PATH) $(ZIPBALL_PATH): $(COMPONENT_BUILD_PATH) wget --no-use-server-timestamps $(ZIPBALL_URL) -O $(ZIPBALL_PATH) $(COMPONENT_BUILD_PATH): mkdir -p $(COMPONENT_BUILD_PATH)
In the lines that follow the “### Targets” comment all targets rely on “caching capability” of Make - when we run
make install targets are evaluated in reverse order like:
COMPONENT_INSTALL_PATH (depends on) -> COMPONENT_SOURCE_PATH (depends on) -> ZIPBALL_PATH (depends on) -> COMPONENT_BUILD_PATH.
When Make finds that a particular target’s dependency’s target’s path exists and its modification time is not greater than that target’s path modification time, it does skip that step by not performing any real action. So for example if we already downloaded CompositeOperations-iOS.zip file which corresponds to
ZIPBALL_PATH, Make will not try to download it again. This is why we use
wget --no-use-server-timestamps to make sure that
ZIPBALL_PATH will always be newer than modification date of its dependency:
COMPONENT_BUILD_PATH target which is created on preceding step. Or another example: if $(COMPONENT_INSTALL_PATH) folder already exists, then Make will not perform any action at all. This caching capability of Make allows Components.sh script from introductory article to work very fast given we have all our Components build cache in place in
This post is one of the very first examples of how Make can be used inside Components infrastructure. Now we are looking forward to hear back from the community if Make will work for all of us at massive scale. Try creating your own Component.make and see it in action.