How to configure Xcode external build system to build and clean using standard short-cuts?
[Xcode 10.1, MacOS 10.14.1]
I have a project that uses bmake
(could be any make
though) and the Makefile provides a number of targets. I would like to use Xcode to build host
and clean the build folder, but I'm having trouble working out how configure Xcode to allow me to this.
From the command line, I would build using bmake host
and clean using bmake clean
. The reason I'm using Xcode for this is because I like to use an IDE for debugging.
In Project -> Info (External Build Tool Configuration)
, I have:
Build Tool : /usr/local/bin/bmake
Arguments : host
Directory : None <- I'm using the current path
With these settings, Product -> Build
builds my target, but Product -> Clean Build Folder
does nothing even though Xcode reports that the clean succeeded.
In order to actually do a clean, I either need to define another target with the Arguments
field set to clean
and then switch between targets when building/cleaning, or, use a single target and change the argument field depending on whether I'm building or cleaning. (A really clumsy way of going about it.)
If I leave Arguments
with it's default value $(ACTION)
all targets get built (except clean), and cleaning does nothing useful.
I've read https://stackoverflow.com/questions/15652316/setup-xcode-for-using-external-compiler but that question does not address this problem.
Is there a better way of doing this?
Solution 1:
The approach we are using:
- Run external build system (i.e. Make or CMake) via custom script from "External Build System" target or just as a "Run script" build phase.
- At some point of custom script execution save a text file (say
CMakeStatus.txt
) toBUILT_PRODUCTS_DIR
, which will be used as a flag indicating that Xcode did clean. - In your custom script check that
CMakeStatus.txt
is present or not. If not, then rebuild your external build system.
Here is example of using Cmake and rebuilding it if Xcode did a clean.
#!/usr/bin/env ruby -w
require 'fileutils'
# system("printenv | sort")
AppProjectName = ENV['PROJECT_NAME']
AppBuildRootDir = ENV['AWL_CMAKE_BUILDS']
AppBuildDirPath = ENV['BUILT_PRODUCTS_DIR']
AppPlatform = ENV['PLATFORM_NAME']
AppBuildConfig = ENV['CONFIGURATION']
AppTargetDeviceId = ENV['TARGET_DEVICE_IDENTIFIER']
if AppProjectName.nil?
raise "❌ Variable \"PROJECT_NAME\" is not passed to script."
end
if AppBuildRootDir.nil?
raise "❌ Variable \"AWL_CMAKE_BUILDS\" is not passed to script."
end
if AppBuildDirPath.nil?
raise "❌ Variable \"BUILT_PRODUCTS_DIR\" is not passed to script."
end
if AppPlatform.nil?
raise "❌ Variable \"PLATFORM_NAME\" is not passed to script."
end
if AppBuildConfig.nil?
raise "❌ Variable \"CONFIGURATION\" is not passed to script."
end
WhiteListedVars = ["PATH", "HOME"]
ENV.keys.each { |key|
if !WhiteListedVars.include? key
ENV[key] = nil
end
}
AppNewEnvVars = `bash -l -c printenv`.strip.split("\n")
AppNewEnvVars.each { |var|
components = var.split("=", 2)
key = components[0]
value = components[1]
ENV[key] = value
}
# system("printenv | sort")
AppCmakeBuildDirPath = "#{AppBuildRootDir}/#{AppProjectName}"
AppCmakeCacheFilePath = "#{AppCmakeBuildDirPath}/CMakeCache.txt"
AppCmakeStatusFilePath = "#{AppBuildDirPath}/CMakeStatus.txt"
if !File.exist?(AppCmakeStatusFilePath) && Dir.exist?(AppCmakeBuildDirPath)
puts "✅ Deleting previous build at \"#{AppCmakeBuildDirPath}\"..."
FileUtils.rm_rf(AppCmakeBuildDirPath)
end
if !File.exist?(AppCmakeCacheFilePath)
puts "✅ Generating Cmake for Platform \"#{AppPlatform}\"..."
cmd = "cmake -G Xcode -B \"#{AppCmakeBuildDirPath}\" -DCMAKE_Swift_COMPILER_FORCED=true -DCMAKE_BUILD_TYPE=#{AppBuildConfig} "
if AppPlatform == "macosx"
cmd += "-DCMAKE_OSX_DEPLOYMENT_TARGET=10.13"
elsif
cmd += "-DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 -DCMAKE_SYSTEM_NAME=iOS"
end
puts cmd
system cmd
File.write(AppCmakeStatusFilePath, 'This file used just as a status flag to clean Cmake.')
end
puts "✅ Building Cmake for Platform \"#{AppPlatform}\" ..."
cmd = "set -euf -o pipefail && cd \"#{AppCmakeBuildDirPath}\" && cmake --build \"#{AppCmakeBuildDirPath}\" -- -quiet CONFIGURATION_BUILD_DIR=\"#{AppBuildDirPath}\" "
if AppPlatform == "macosx"
cmd += "-destination 'platform=OS X,arch=x86_64'"
elsif AppPlatform == "iphonesimulator"
if AppTargetDeviceId.nil?
raise "❌ Variable \"TARGET_DEVICE_IDENTIFIER\" is not passed to script."
end
cmd += "-sdk iphonesimulator -destination 'platform=iOS Simulator,id=#{AppTargetDeviceId}'"
else
if AppTargetDeviceId.nil?
cmd += "-sdk iphoneos -destination generic/platform=iOS"
else
cmd += "-sdk iphoneos -destination 'platform=iOS,id=#{AppTargetDeviceId}'"
end
end
# cmd += " | xcpretty"
puts cmd
system(cmd) or exit 1