--!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 find_package.lua
--
-- imports
import("core.base.option")
import("core.base.hashset")
import("core.project.target")
import("lib.detect.find_tool")
-- exclude cmake internal definitions https://github.com/xmake-io/xmake/issues/5217
function _should_exclude(define)
local name = define:split("=")[1]
return table.contains({"CMAKE_INTDIR", "_DEBUG", "NDEBUG"}, name)
end
-- map xmake mode to cmake mode
function _cmake_mode(mode)
if mode == "debug" then return "Debug"
elseif mode == "releasedbg" then return "RelWithDebInfo"
elseif mode == "minsizerel" then return "MinSizeRel"
else return "Release"
end
end
-- find package
function _find_package(cmake, name, opt)
-- get work directory
local workdir = os.tmpfile() .. ".dir"
os.tryrm(workdir)
os.mkdir(workdir)
io.writefile(path.join(workdir, "test.cpp"), "")
-- generate CMakeLists.txt
local filepath = path.join(workdir, "CMakeLists.txt")
local cmakefile = io.open(filepath, "w")
if cmake.version then
cmakefile:print("cmake_minimum_required(VERSION %s)", cmake.version)
end
cmakefile:print("project(find_package)")
-- e.g. OpenCV 4.1.1, Boost COMPONENTS regex system
local requirestr = name
local configs = opt.configs or {}
if opt.require_version and opt.require_version ~= "latest" then
requirestr = requirestr .. " " .. opt.require_version
end
-- set search mode, e.g. config, module
-- it will be both mode if do not set this config.
-- e.g. https://cmake.org/cmake/help/latest/command/find_package.html#id4
if configs.search_mode then
requirestr = requirestr .. " " .. configs.search_mode:upper()
end
-- use opt.components is for backward compatibility
local componentstr = ""
local components = configs.components or opt.components
if components and #components > 0 then
componentstr = "COMPONENTS"
for _, component in ipairs(components) do
componentstr = componentstr .. " " .. component
end
end
local moduledirs = configs.moduledirs or opt.moduledirs
if moduledirs then
for _, moduledir in ipairs(moduledirs) do
cmakefile:print("list(APPEND CMAKE_MODULE_PATH \"%s\")", (moduledir:gsub("\\", "/")))
end
end
-- https://github.com/xmake-io/xmake/issues/6296
local prefixdirs = configs.prefixdirs or opt.prefixdirs
if prefixdirs then
for _, prefixdir in ipairs(prefixdirs) do
cmakefile:print("list(APPEND CMAKE_PREFIX_PATH \"%s\")", (prefixdir:gsub("\\", "/")))
end
end
-- e.g. set(Boost_USE_STATIC_LIB ON)
local presets = configs.presets or opt.presets
if presets then
for k, v in pairs(presets) do
if type(v) == "boolean" then
cmakefile:print("set(%s %s)", k, v and "ON" or "OFF")
else
cmakefile:print("set(%s %s)", k, tostring(v))
end
end
end
local testname = "test_" .. name
cmakefile:print("find_package(%s REQUIRED %s)", requirestr, componentstr)
cmakefile:print("add_executable(%s test.cpp)", testname)
-- setup include directories
local includedirs = ""
if configs.include_directories then
includedirs = table.concat(table.wrap(configs.include_directories), " ")
else
includedirs = ("${%s_INCLUDE_DIR} ${%s_INCLUDE_DIRS}"):format(name, name)
includedirs = includedirs .. (" ${%s_INCLUDE_DIR} ${%s_INCLUDE_DIRS}"):format(name:upper(), name:upper())
end
cmakefile:print("target_include_directories(%s PRIVATE %s)", testname, includedirs)
-- reserved for backword compatibility
cmakefile:print("target_include_directories(%s PRIVATE ${%s_CXX_INCLUDE_DIRS})",
testname, name)
-- setup link library/target
local linklibs = ""
if configs.link_libraries then
linklibs = table.concat(table.wrap(configs.link_libraries), " ")
else
linklibs = ("${%s_LIBRARY} ${%s_LIBRARIES} ${%s_LIBS}"):format(name, name, name)
linklibs = linklibs .. (" ${%s_LIBRARY} ${%s_LIBRARIES} ${%s_LIBS}"):format(name:upper(), name:upper(), name:upper())
end
cmakefile:print("target_link_libraries(%s PRIVATE %s)", testname, linklibs)
cmakefile:close()
if option.get("diagnosis") then
local cmakedata = io.readfile(filepath)
cprint("finding it from the generated CMakeLists.txt:")
io.write(cmakedata .. "\n")
end
-- run cmake
local envs = configs.envs or opt.envs or {}
envs.CMAKE_BUILD_TYPE = envs.CMAKE_BUILD_TYPE or _cmake_mode(opt.mode or "release")
-- If the generated CMakeLists.txt fails to find the REQUIRED package, CMake will exit
-- with code 1, os.vrunv will raise an error and the try{} block will return nil.
local ok = try {function()
os.vrunv(cmake.program, {workdir}, {curdir = workdir, envs = envs})
return true
end}
if not ok then
return
end
-- parse defines and includedirs for macosx/linux
local links
local linkdirs
local libfiles
local defines
local includedirs
local ldflags
local flagsfile = path.join(workdir, "CMakeFiles", testname .. ".dir", "flags.make")
if os.isfile(flagsfile) then
local flagsdata = io.readfile(flagsfile)
if flagsdata then
if option.get("diagnosis") then
cprint("finding includes from %s", flagsfile)
io.write(flagsdata .. "\n")
end
for _, line in ipairs(flagsdata:split("\n", {plain = true})) do
if line:find("CXX_INCLUDES =", 1, true) then
local has_include = false
local flags = os.argv(line:split("=", {plain = true})[2]:trim())
for _, flag in ipairs(flags) do
if has_include or (flag:startswith("-I") and #flag > 2) then
local includedir = has_include and flag or flag:sub(3)
if includedir and os.isdir(includedir) then
includedirs = includedirs or {}
table.insert(includedirs, includedir)
end
has_include = false
elseif flag == "-isystem" or flag == "-I" then
has_include = true
end
end
elseif line:find("CXX_DEFINES =", 1, true) then
defines = defines or {}
local flags = os.argv(line:split("=", {plain = true})[2]:trim())
for _, flag in ipairs(flags) do
if flag:startswith("-D") and #flag > 2 then
local define = flag:sub(3)
if define and not _should_exclude(define) then
table.insert(defines, define)
end
end
end
end
end
end
end
-- parse links and linkdirs for macosx/linux
local linkfile = path.join(workdir, "CMakeFiles", testname .. ".dir", "link.txt")
if os.isfile(linkfile) then
local linkdata = io.readfile(linkfile)
if linkdata then
if option.get("diagnosis") then
cprint("finding links from %s", linkfile)
io.write(linkdata .. "\n")
end
for _, line in ipairs(os.argv(linkdata)) do
local line = line
local is_ldflags = false
local is_library = false
for _, suffix in ipairs({".so", ".dylib", ".dylib", ".tbd", ".lib"}) do
if line:startswith("-Wl,") then
is_ldflags = true
break
elseif line:find(suffix, 1, true) then
is_library = true
break
end
end
if is_ldflags then
ldflags = ldflags or {}
table.insert(ldflags, line)
elseif is_library then
-- strip library version suffix, e.g. libxxx.so.1.1 -> libxxx.so
if line:find(".so", 1, true) then
line = line:gsub("lib(.-)%.so%..+$", "lib%1.so")
end
-- get libfiles
if os.isfile(line) then
libfiles = libfiles or {}
table.insert(libfiles, line)
end
-- get links and linkdirs
local linkdir = path.directory(line)
if linkdir ~= "." then
linkdirs = linkdirs or {}
table.insert(linkdirs, linkdir)
end
local link = target.linkname(path.filename(line))
if link then
links = links or {}
table.insert(links, link)
end
-- is link? e.g. -lxxx
elseif line:startswith("-l") then
local link = line:sub(3):trim()
links = links or {}
table.insert(links, link)
end
end
end
end
-- pares includedirs and links/linkdirs for windows
local vcprojfile = path.join(workdir, testname .. ".vcxproj")
if os.isfile(vcprojfile) then
local vcprojdata = io.readfile(vcprojfile)
local vs_mode = envs.CMAKE_BUILD_TYPE or _cmake_mode(opt.mode or "release")
vcprojdata = vcprojdata:match("(.-)")
if vcprojdata then
for _, line in ipairs(vcprojdata:split("\n", {plain = true})) do
local values = line:match("(.+);%%%(AdditionalIncludeDirectories%)")
if values then
includedirs = includedirs or {}
table.join2(includedirs, path.splitenv(values))
end
values = line:match("(.+)")
if values then
for _, library in ipairs(path.splitenv(values)) do
-- get libfiles
if os.isfile(library) then
libfiles = libfiles or {}
table.insert(libfiles, library)
end
-- get links and linkdirs
local linkdir = path.directory(library)
linkdir = path.translate(linkdir)
if linkdir ~= "." and not linkdir:startswith(workdir) then
linkdirs = linkdirs or {}
table.insert(linkdirs, linkdir)
local link = target.linkname(path.filename(library))
if link then
links = links or {}
table.insert(links, link)
end
end
end
end
values = line:match("%%%(PreprocessorDefinitions%);(.+)")
if values then
defines = defines or {}
values = path.splitenv(values)
for _, value in ipairs(values) do
if not _should_exclude(value) then
table.insert(defines, value)
end
end
end
end
end
end
-- remove work directory
os.tryrm(workdir)
-- get results
if configs.allow_empty_package or links or includedirs then
local results = {}
results.links = table.reverse_unique(links)
results.ldflags = table.reverse_unique(ldflags)
results.linkdirs = table.unique(linkdirs)
results.defines = table.unique(defines)
results.libfiles = table.unique(libfiles)
results.includedirs = table.unique(includedirs)
return results
end
end
-- find package using the cmake package manager
--
-- e.g.
--
-- find_package("cmake::ZLIB")
-- find_package("cmake::OpenCV", {require_version = "4.1.1"})
-- find_package("cmake::Boost", {configs = {components = {"regex", "system"}, presets = {Boost_USE_STATIC_LIB = true}}})
-- find_package("cmake::Foo", {configs = {moduledirs = "xxx"}})
--
-- we can use add_requires with {system = true}
--
-- add_requires("cmake::ZLIB", {system = true})
-- add_requires("cmake::OpenCV 4.1.1", {system = true})
-- add_requires("cmake::Boost", {configs = {components = {"regex", "system"}, presets = {Boost_USE_STATIC_LIB = true}}})
-- add_requires("cmake::Foo", {configs = {moduledirs = "xxx"}})
--
-- @param name the package name
-- @param opt the options, e.g. {verbose = true, require_version = "1.0",
-- configs = {
-- components = {"regex", "system"},
-- moduledirs = "xxx",
-- presets = {Boost_USE_STATIC_LIB = true},
-- envs = {CMAKE_PREFIX_PATH = "xxx"}})
--
function main(name, opt)
opt = opt or {}
local cmake = find_tool("cmake", {version = true})
if not cmake then
return
end
return _find_package(cmake, name, opt)
end