--!A cross-platform build utility based on Lua -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- -- Copyright (C) 2015-present, Xmake Open Source Community. -- -- @author ruki -- @file cmake.lua -- -- imports import("core.base.option") import("core.project.config") import("core.tool.toolchain") import("core.platform.platform") import("lib.detect.find_file") import("lib.detect.find_tool") import("private.utils.toolchain", {alias = "toolchain_utils"}) -- get build directory function _get_builddir() return config.builddir() or "build" end -- get artifacts directory function _get_artifacts_dir() return path.absolute(path.join(_get_builddir(), "artifacts")) end -- get the build environment function _get_buildenv(key) local value = config.get(key) if value == nil then value = platform.toolconfig(key, config.plat()) end if value == nil then value = platform.tool(key, config.plat()) end return value end -- get vs arch function _get_vsarch() local arch = get_config("arch") or os.arch() if arch == "x86" or arch == "i386" then return "Win32" end if arch == "x86_64" then return "x64" end if arch == "arm64ec" then return "ARM64EC" end if arch:startswith("arm64") then return "ARM64" end if arch:startswith("arm") then return "ARM" end return arch end -- get msvc function _get_msvc() local msvc = toolchain.load("msvc") assert(msvc:check(), "vs not found!") -- we need to check vs envs if it has been not checked yet return msvc end -- get msvc run environments function _get_msvc_runenvs() return os.joinenvs(_get_msvc():runenvs()) end -- translate paths function _translate_paths(paths) if is_host("windows") then if type(paths) == "string" then return (paths:gsub("\\", "/")) elseif type(paths) == "table" then local result = {} for _, p in ipairs(paths) do table.insert(result, (p:gsub("\\", "/"))) end return result end end return paths end -- translate bin path function _translate_bin_path(bin_path) if is_host("windows") and bin_path then bin_path = bin_path:gsub("\\", "/") if not bin_path:find(string.ipattern("%.exe$")) and not bin_path:find(string.ipattern("%.cmd$")) and not bin_path:find(string.ipattern("%.bat$")) then bin_path = bin_path .. ".exe" end end return bin_path end -- is cross compilation? function _is_cross_compilation() if not is_plat(os.subhost()) then return true end if is_plat("macosx") and not is_arch(os.subarch()) then return true end return false end function _get_cmake_system_processor() -- on Windows, CMAKE_SYSTEM_PROCESSOR comes from PROCESSOR_ARCHITECTURE -- on other systems it's the output of uname -m if is_plat("windows") then local archs = { x86 = "x86", x64 = "AMD64", x86_64 = "AMD64", arm = "ARM", arm64 = "ARM64", arm64ec = "ARM64EC" } return archs[os.subarch()] or os.subarch() end return os.subarch() end -- insert configs from envs function _insert_configs_from_envs(configs, envs, opt) opt = opt or {} for k, v in pairs(envs) do table.insert(configs, "-D" .. k .. "=" .. v) end end -- get configs for windows function _get_configs_for_windows(configs, opt) local envs = {} opt = opt or {} local cmake_generator = opt.cmake_generator if not cmake_generator or cmake_generator:find("Visual Studio", 1, true) then table.insert(configs, "-A") if is_arch("x86", "i386") then table.insert(configs, "Win32") elseif is_arch("arm64") then table.insert(configs, "ARM64") elseif is_arch("arm64ec") then table.insert(configs, "ARM64EC") elseif is_arch("arm.*") then table.insert(configs, "ARM") else table.insert(configs, "x64") end local vs_toolset = config.get("vs_toolset") if vs_toolset then vs_toolset = toolchain_utils.get_vs_toolset_ver(vs_toolset) if vs_toolset then envs.CMAKE_GENERATOR_TOOLSET = vs_toolset end end end -- use clang/clang-cl local cc = _get_buildenv("cc") if cc and path.basename(cc):find("clang", 1, true) then envs.CMAKE_C_COMPILER = _translate_bin_path(cc) end local cxx = _get_buildenv("cxx") if cxx and path.basename(cxx):find("clang", 1, true) then envs.CMAKE_CXX_COMPILER = _translate_bin_path(cxx) end _insert_configs_from_envs(configs, envs, opt) end -- get configs for android function _get_configs_for_android(configs) -- https://developer.android.google.cn/ndk/guides/cmake local ndk = config.get("ndk") if ndk and os.isdir(ndk) then local arch = config.arch() local ndk_sdkver = config.get("ndk_sdkver") local ndk_cxxstl = config.get("ndk_cxxstl") table.insert(configs, "-DCMAKE_TOOLCHAIN_FILE=" .. path.join(ndk, "build/cmake/android.toolchain.cmake")) if arch then table.insert(configs, "-DANDROID_ABI=" .. arch) end if ndk_sdkver then table.insert(configs, "-DANDROID_NATIVE_API_LEVEL=" .. ndk_sdkver) end if ndk_cxxstl then table.insert(configs, "-DANDROID_STL=" .. ndk_cxxstl) end -- avoid find and add system include/library path -- @see https://github.com/xmake-io/xmake/issues/2037 table.insert(configs, "-DCMAKE_FIND_ROOT_PATH_MODE_PACKAGE=BOTH") table.insert(configs, "-DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=BOTH") table.insert(configs, "-DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=BOTH") table.insert(configs, "-DCMAKE_FIND_ROOT_PATH_MODE_PROGRAM=NEVER") end end -- get configs for appleos function _get_configs_for_appleos(configs) local envs = {} local cflags = table.join(table.wrap(_get_buildenv("cxflags")), _get_buildenv("cflags")) local cxxflags = table.join(table.wrap(_get_buildenv("cxflags")), _get_buildenv("cxxflags")) envs.CMAKE_C_FLAGS = table.concat(cflags, ' ') envs.CMAKE_CXX_FLAGS = table.concat(cxxflags, ' ') envs.CMAKE_ASM_FLAGS = table.concat(table.wrap(_get_buildenv("asflags")), ' ') envs.CMAKE_STATIC_LINKER_FLAGS = table.concat(table.wrap(_get_buildenv("arflags")), ' ') envs.CMAKE_EXE_LINKER_FLAGS = table.concat(table.wrap(_get_buildenv("ldflags")), ' ') envs.CMAKE_SHARED_LINKER_FLAGS = table.concat(table.wrap(_get_buildenv("shflags")), ' ') -- https://cmake.org/cmake/help/v3.17/manual/cmake-toolchains.7.html#id25 if is_plat("watchos") then envs.CMAKE_SYSTEM_NAME = "watchOS" if is_arch("x86_64", "i386") then envs.CMAKE_OSX_SYSROOT = "watchsimulator" end elseif is_plat("iphoneos") then envs.CMAKE_SYSTEM_NAME = "iOS" if is_arch("x86_64", "i386") then envs.CMAKE_OSX_SYSROOT = "iphonesimulator" end elseif _is_cross_compilation() then envs.CMAKE_SYSTEM_NAME = "Darwin" envs.CMAKE_SYSTEM_PROCESSOR = _get_cmake_system_processor() end envs.CMAKE_FIND_ROOT_PATH_MODE_LIBRARY = "BOTH" envs.CMAKE_FIND_ROOT_PATH_MODE_INCLUDE = "BOTH" envs.CMAKE_FIND_ROOT_PATH_MODE_FRAMEWORK = "BOTH" envs.CMAKE_FIND_ROOT_PATH_MODE_PROGRAM = "NEVER" -- avoid install bundle targets envs.CMAKE_MACOSX_BUNDLE = "NO" for k, v in pairs(envs) do table.insert(configs, "-D" .. k .. "=" .. v) end end -- get configs for mingw function _get_configs_for_mingw(configs) local envs = {} local cflags = table.join(table.wrap(_get_buildenv("cxflags")), _get_buildenv("cflags")) local cxxflags = table.join(table.wrap(_get_buildenv("cxflags")), _get_buildenv("cxxflags")) local sdkdir = _get_buildenv("mingw") or _get_buildenv("sdk") envs.CMAKE_C_COMPILER = _get_buildenv("cc") envs.CMAKE_CXX_COMPILER = _get_buildenv("cxx") envs.CMAKE_ASM_COMPILER = _get_buildenv("as") envs.CMAKE_AR = _get_buildenv("ar") envs.CMAKE_LINKER = _get_buildenv("ld") envs.CMAKE_RANLIB = _get_buildenv("ranlib") envs.CMAKE_C_FLAGS = table.concat(cflags, ' ') envs.CMAKE_CXX_FLAGS = table.concat(cxxflags, ' ') envs.CMAKE_ASM_FLAGS = table.concat(table.wrap(_get_buildenv("asflags")), ' ') envs.CMAKE_STATIC_LINKER_FLAGS = table.concat(table.wrap(_get_buildenv("arflags")), ' ') envs.CMAKE_EXE_LINKER_FLAGS = table.concat(table.wrap(_get_buildenv("ldflags")), ' ') envs.CMAKE_SHARED_LINKER_FLAGS = table.concat(table.wrap(_get_buildenv("shflags")), ' ') envs.CMAKE_SYSTEM_NAME = "Windows" envs.CMAKE_SYSTEM_PROCESSOR = _get_cmake_system_processor() -- avoid find and add system include/library path envs.CMAKE_FIND_ROOT_PATH = sdkdir envs.CMAKE_SYSROOT = sdkdir envs.CMAKE_FIND_ROOT_PATH_MODE_PACKAGE = "BOTH" envs.CMAKE_FIND_ROOT_PATH_MODE_LIBRARY = "BOTH" envs.CMAKE_FIND_ROOT_PATH_MODE_INCLUDE = "BOTH" envs.CMAKE_FIND_ROOT_PATH_MODE_PROGRAM = "NEVER" -- avoid add -isysroot on macOS envs.CMAKE_OSX_SYSROOT = "" -- Avoid cmake to add the flags -search_paths_first and -headerpad_max_install_names on macOS envs.HAVE_FLAG_SEARCH_PATHS_FIRST = "0" for k, v in pairs(envs) do table.insert(configs, "-D" .. k .. "=" .. v) end end -- get configs for wasm function _get_configs_for_wasm(configs) if config.get("toolchain") == "wasi" then _get_configs_for_cross(configs) return end local emsdk = find_emsdk() assert(emsdk and emsdk.emscripten, "emscripten not found!") local emscripten_cmakefile = find_file("Emscripten.cmake", path.join(emsdk.emscripten, "cmake/Modules/Platform")) assert(emscripten_cmakefile, "Emscripten.cmake not found!") table.insert(configs, "-DCMAKE_TOOLCHAIN_FILE=" .. emscripten_cmakefile) assert(emscripten_cmakefile, "Emscripten.cmake not found!") _get_configs_for_generic(configs) end -- get configs for cross function _get_configs_for_cross(configs) local envs = {} local cflags = table.join(table.wrap(_get_buildenv("cxflags")), _get_buildenv("cflags")) local cxxflags = table.join(table.wrap(_get_buildenv("cxflags")), _get_buildenv("cxxflags")) local sdkdir = _translate_paths(_get_buildenv("sdk")) envs.CMAKE_C_COMPILER = _translate_bin_path(_get_buildenv("cc")) envs.CMAKE_CXX_COMPILER = _translate_bin_path(_get_buildenv("cxx")) envs.CMAKE_ASM_COMPILER = _translate_bin_path(_get_buildenv("as")) envs.CMAKE_AR = _translate_bin_path(_get_buildenv("ar")) -- https://github.com/xmake-io/xmake-repo/pull/1096 local cxx = envs.CMAKE_CXX_COMPILER if cxx and (cxx:find("clang", 1, true) or cxx:find("gcc", 1, true)) then local dir = path.directory(cxx) local name = path.filename(cxx) name = name:gsub("clang$", "clang++") name = name:gsub("clang%-", "clang++-") name = name:gsub("gcc$", "g++") name = name:gsub("gcc%-", "g++-") envs.CMAKE_CXX_COMPILER = _translate_bin_path(dir and path.join(dir, name) or name) end -- @note The link command line is set in Modules/CMake{C,CXX,Fortran}Information.cmake and defaults to using the compiler, not CMAKE_LINKER, -- so we need to set CMAKE_CXX_LINK_EXECUTABLE to use CMAKE_LINKER as linker. -- -- https://github.com/xmake-io/xmake-repo/pull/1039 -- https://stackoverflow.com/questions/1867745/cmake-use-a-custom-linker/25274328#25274328 envs.CMAKE_LINKER = _translate_bin_path(_get_buildenv("ld")) local ld = envs.CMAKE_LINKER if ld and (ld:find("g++", 1, true) or ld:find("clang++", 1, true)) then envs.CMAKE_CXX_LINK_EXECUTABLE = " -o " end envs.CMAKE_RANLIB = _translate_bin_path(_get_buildenv("ranlib")) envs.CMAKE_C_FLAGS = table.concat(cflags, ' ') envs.CMAKE_CXX_FLAGS = table.concat(cxxflags, ' ') envs.CMAKE_ASM_FLAGS = table.concat(table.wrap(_get_buildenv("asflags")), ' ') envs.CMAKE_STATIC_LINKER_FLAGS = table.concat(table.wrap(_get_buildenv("arflags")), ' ') envs.CMAKE_EXE_LINKER_FLAGS = table.concat(table.wrap(_get_buildenv("ldflags")), ' ') envs.CMAKE_SHARED_LINKER_FLAGS = table.concat(table.wrap(_get_buildenv("shflags")), ' ') envs.CMAKE_SYSTEM_NAME = "Linux" -- avoid find and add system include/library path envs.CMAKE_FIND_ROOT_PATH = sdkdir envs.CMAKE_SYSROOT = sdkdir envs.CMAKE_FIND_ROOT_PATH_MODE_PACKAGE = "BOTH" envs.CMAKE_FIND_ROOT_PATH_MODE_LIBRARY = "BOTH" envs.CMAKE_FIND_ROOT_PATH_MODE_INCLUDE = "BOTH" envs.CMAKE_FIND_ROOT_PATH_MODE_PROGRAM = "NEVER" -- avoid add -isysroot on macOS envs.CMAKE_OSX_SYSROOT = "" -- Avoid cmake to add the flags -search_paths_first and -headerpad_max_install_names on macOS envs.HAVE_FLAG_SEARCH_PATHS_FIRST = "0" for k, v in pairs(envs) do table.insert(configs, "-D" .. k .. "=" .. v) end end -- get configs for host toolchain function _get_configs_for_host_toolchain(configs) local envs = {} local cflags = table.join(table.wrap(_get_buildenv("cxflags")), _get_buildenv("cflags")) local cxxflags = table.join(table.wrap(_get_buildenv("cxflags")), _get_buildenv("cxxflags")) local sdkdir = _translate_paths(_get_buildenv("sdk")) envs.CMAKE_C_COMPILER = _translate_bin_path(_get_buildenv("cc")) envs.CMAKE_CXX_COMPILER = _translate_bin_path(_get_buildenv("cxx")) envs.CMAKE_ASM_COMPILER = _translate_bin_path(_get_buildenv("as")) envs.CMAKE_AR = _translate_bin_path(_get_buildenv("ar")) -- https://github.com/xmake-io/xmake-repo/pull/1096 local cxx = envs.CMAKE_CXX_COMPILER if cxx and (cxx:find("clang", 1, true) or cxx:find("gcc", 1, true)) then local dir = path.directory(cxx) local name = path.filename(cxx) name = name:gsub("clang$", "clang++") name = name:gsub("clang%-", "clang++-") name = name:gsub("gcc$", "g++") name = name:gsub("gcc%-", "g++-") envs.CMAKE_CXX_COMPILER = _translate_bin_path(dir and path.join(dir, name) or name) end -- @note The link command line is set in Modules/CMake{C,CXX,Fortran}Information.cmake and defaults to using the compiler, not CMAKE_LINKER, -- so we need to set CMAKE_CXX_LINK_EXECUTABLE to use CMAKE_LINKER as linker. -- -- https://github.com/xmake-io/xmake-repo/pull/1039 -- https://stackoverflow.com/questions/1867745/cmake-use-a-custom-linker/25274328#25274328 envs.CMAKE_LINKER = _translate_bin_path(_get_buildenv("ld")) local ld = envs.CMAKE_LINKER if ld and (ld:find("g++", 1, true) or ld:find("clang++", 1, true)) then envs.CMAKE_CXX_LINK_EXECUTABLE = " -o " end envs.CMAKE_RANLIB = _translate_bin_path(_get_buildenv("ranlib")) envs.CMAKE_C_FLAGS = table.concat(cflags, ' ') envs.CMAKE_CXX_FLAGS = table.concat(cxxflags, ' ') envs.CMAKE_ASM_FLAGS = table.concat(table.wrap(_get_buildenv("asflags")), ' ') envs.CMAKE_STATIC_LINKER_FLAGS = table.concat(table.wrap(_get_buildenv("arflags")), ' ') envs.CMAKE_EXE_LINKER_FLAGS = table.concat(table.wrap(_get_buildenv("ldflags")), ' ') envs.CMAKE_SHARED_LINKER_FLAGS = table.concat(table.wrap(_get_buildenv("shflags")), ' ') -- we don't need to set it as cross compilation if we just pass toolchain -- https://github.com/xmake-io/xmake/issues/2170 if _is_cross_compilation() then envs.CMAKE_SYSTEM_NAME = "Linux" end for k, v in pairs(envs) do table.insert(configs, "-D" .. k .. "=" .. v) end end -- get cmake generator for msvc function _get_cmake_generator_for_msvc() local vs = _get_msvc():config("vs") or config.get("vs") return "Visual Studio " .. toolchain_utils.get_vsver(vs) .. " " .. vs end -- get configs for cmake generator function _get_configs_for_generator(configs, opt) opt = opt or {} configs = configs or {} local cmake_generator = opt.cmake_generator if cmake_generator then if cmake_generator:find("Visual Studio", 1, true) then cmake_generator = _get_cmake_generator_for_msvc() end table.insert(configs, "-G") table.insert(configs, cmake_generator) elseif is_plat("mingw") and is_subhost("msys") then table.insert(configs, "-G") table.insert(configs, "MSYS Makefiles") elseif is_plat("mingw") and is_subhost("windows") then table.insert(configs, "-G") table.insert(configs, "MinGW Makefiles") elseif is_plat("windows") then table.insert(configs, "-G") table.insert(configs, _get_cmake_generator_for_msvc()) elseif is_plat("wasm") and is_subhost("windows") then table.insert(configs, "-G") table.insert(configs, "MinGW Makefiles") else table.insert(configs, "-G") table.insert(configs, "Unix Makefiles") end end -- get configs for installation function _get_configs_for_install(configs, opt) -- @see https://cmake.org/cmake/help/v3.14/module/GNUInstallDirs.html -- LIBDIR: object code libraries (lib or lib64 or lib/ on Debian) -- table.insert(configs, "-DCMAKE_INSTALL_PREFIX=" .. opt.artifacts_dir) table.insert(configs, "-DCMAKE_INSTALL_LIBDIR:PATH=lib") end -- get configs function _get_configs(opt) local configs = {} _get_configs_for_install(configs, opt) _get_configs_for_generator(configs, opt) if is_plat("windows") then _get_configs_for_windows(configs, opt) elseif is_plat("android") then _get_configs_for_android(configs) elseif is_plat("iphoneos", "watchos") or -- for cross-compilation on macOS, @see https://github.com/xmake-io/xmake/issues/2804 (is_plat("macosx") and (get_config("appledev") or not is_arch(os.subarch()))) then _get_configs_for_appleos(configs) elseif is_plat("mingw") then _get_configs_for_mingw(configs) elseif is_plat("wasm") then _get_configs_for_wasm(configs) elseif _is_cross_compilation() then _get_configs_for_cross(configs) elseif config.get("toolchain") then -- we still need find system libraries, -- it just pass toolchain environments if the toolchain is compatible with host if toolchain_utils.is_compatible_with_host(config.get("toolchain")) then _get_configs_for_host_toolchain(configs) else _get_configs_for_cross(configs) end end -- enable verbose? if option.get("verbose") then table.insert(configs, "-DCMAKE_VERBOSE_MAKEFILE=ON") end -- add extra user configs local tryconfigs = config.get("tryconfigs") if tryconfigs then for _, item in ipairs(os.argv(tryconfigs)) do table.insert(configs, tostring(item)) end end -- add build directory table.insert(configs, '..') return configs end -- build for msvc function _build_for_msvc(opt) local runenvs = _get_msvc_runenvs() local msbuild = find_tool("msbuild", {envs = runenvs}) local slnfile = assert(find_file("*.sln", os.curdir()) or find_file("*.slnx", os.curdir()), "*.sln/slnx file not found!") os.vexecv(msbuild.program, {slnfile, "-nologo", "-t:Build", "-m", "-p:Configuration=" .. (is_mode("debug") and "Debug" or "Release"), "-p:Platform=" .. _get_vsarch()}, {envs = runenvs}) local projfile = os.isfile("INSTALL.vcxproj") and "INSTALL.vcxproj" or "INSTALL.vcproj" if os.isfile(projfile) then os.vexecv(msbuild.program, {projfile, "/property:configuration=" .. (is_mode("debug") and "Debug" or "Release")}, {envs = runenvs}) end end -- build for make function _build_for_make(opt) local argv = {"-j" .. option.get("jobs")} if option.get("verbose") then table.insert(argv, "VERBOSE=1") end if is_host("bsd") then os.vexecv("gmake", argv) os.vexecv("gmake", {"install"}) else os.vexecv("make", argv) os.vexecv("make", {"install"}) end end -- build for ninja function _build_for_ninja(opt) local njob = option.get("jobs") or tostring(os.default_njob()) local ninja = assert(find_tool("ninja"), "ninja not found!") local argv = {} if option.get("diagnosis") then table.insert(argv, "-v") end table.insert(argv, "-j") table.insert(argv, njob) local envs if is_plat("windows") then envs = _get_msvc_runenvs() end os.vexecv(ninja.program, argv, {envs = envs}) end -- detect build-system and configuration file function detect() return find_file("CMakeLists.txt", os.curdir()) end -- do clean function clean() local builddir = _get_builddir() if os.isdir(builddir) then local configfile = find_file("[mM]akefile", builddir) or (is_plat("windows") and find_file("*.sln", builddir)) if configfile then local oldir = os.cd(builddir) if is_plat("windows") then local runenvs = _get_msvc_runenvs() local msbuild = find_tool("msbuild", {envs = runenvs}) os.vexecv(msbuild.program, {configfile, "-nologo", "-t:Clean", "-p:Configuration=" .. (is_mode("debug") and "Debug" or "Release"), "-p:Platform=" .. (is_arch("x64") and "x64" or "Win32")}, {envs = runenvs}) else os.vexec("make clean") end os.cd(oldir) end end end -- do build function build() -- get cmake local cmake = assert(find_tool("cmake"), "cmake not found!") -- get artifacts directory local opt = {} local artifacts_dir = _get_artifacts_dir() if not os.isdir(artifacts_dir) then os.mkdir(artifacts_dir) end os.cd(_get_builddir()) opt.artifacts_dir = artifacts_dir -- exists $CMAKE_GENERATOR? use it opt.cmake_generator = os.getenv("CMAKE_GENERATOR") -- do configure os.vexecv(cmake.program, _get_configs(opt)) -- do build local cmake_generator = opt.cmake_generator if cmake_generator then if cmake_generator:find("Visual Studio", 1, true) then _build_for_msvc(opt) elseif cmake_generator == "Ninja" then _build_for_ninja(opt) elseif cmake_generator:find("Makefiles", 1, true) then _build_for_make(opt) else raise("unknown cmake generator(%s)!", cmake_generator) end else if is_plat("windows") then _build_for_msvc(opt) else _build_for_make(opt) end end cprint("output to ${bright}%s", artifacts_dir) cprint("${color.success}build ok!") end