--!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_qt.lua -- -- imports import("lib.detect.find_file") import("lib.detect.find_tool") import("core.base.semver") import("core.base.option") import("core.base.global") import("core.project.config") import("core.cache.detectcache") -- find qt sdk directory function _find_sdkdir(sdkdir, sdkver) -- append target sub-directory local subdirs = {} if is_plat("linux") then table.insert(subdirs, path.join(sdkver or "*", is_arch("x86_64") and "gcc_64" or "gcc_32", "bin")) table.insert(subdirs, path.join(sdkver or "*", is_arch("x86_64") and "clang_64" or "clang_32", "bin")) elseif is_plat("macosx") then table.insert(subdirs, path.join(sdkver or "*", "macos", "bin")) -- for Qt 6.2 table.insert(subdirs, path.join(sdkver or "*", is_arch("x86_64") and "clang_64" or "clang_32", "bin")) elseif is_plat("iphoneos") then table.insert(subdirs, path.join(sdkver or "*", "ios", "bin")) elseif is_plat("windows") then local vs = config.get("vs") if vs then table.insert(subdirs, path.join(sdkver or "*", is_arch("x64") and "msvc" .. vs .. "_64" or "msvc" .. vs .. "_32", "bin")) table.insert(subdirs, path.join(sdkver or "*", "msvc" .. vs, "bin")) end table.insert(subdirs, path.join(sdkver or "*", is_arch("x64") and "msvc*_64" or "msvc*_32", "bin")) table.insert(subdirs, path.join(sdkver or "*", "msvc*", "bin")) elseif is_plat("mingw") then table.insert(subdirs, path.join(sdkver or "*", is_arch("x86_64") and "mingw*_64" or "mingw*_32", "bin")) elseif is_plat("android") then local subdir if is_arch("arm64-v8a") then subdir = "android_arm64_v8a" elseif is_arch("armeabi-v7a", "armeabi", "armv7-a", "armv5te") then -- armv7-a/armv5te are deprecated subdir = "android_armv7" elseif is_arch("x86", "i386") then -- i386 is deprecated subdir = "android_x86" elseif is_arch("x86_64") then subdir = "android_x86_64" end if subdir then table.insert(subdirs, path.join(sdkver or "*", subdir, "bin")) end table.insert(subdirs, path.join(sdkver or "*", "android", "bin")) elseif is_plat("wasm") then table.insert(subdirs, path.join(sdkver or "*", "wasm_*", "bin")) else table.insert(subdirs, path.join(sdkver or "*", "*", "bin")) end table.insert(subdirs, path.join("*", "bin")) table.insert(subdirs, "bin") -- init the search directories local paths = {} if sdkdir then table.insert(paths, sdkdir) end if is_host("windows") then -- we find it from /mingw64 first if is_subhost("msys") then local mingw_prefix = os.getenv("MINGW_PREFIX") if mingw_prefix and os.isdir(mingw_prefix) then table.insert(paths, mingw_prefix) end end -- add paths from registry local regs = { "HKEY_CLASSES_ROOT\\Applications\\QtProject.QtCreator.c\\shell\\Open\\Command", "HKEY_CLASSES_ROOT\\Applications\\QtProject.QtCreator.cpp\\shell\\Open\\Command", "HKEY_CLASSES_ROOT\\Applications\\QtProject.QtCreator.pro\\shell\\Open\\Command", "HKEY_CURRENT_USER\\SOFTWARE\\Classes\\Applications\\QtProject.QtCreator.c\\shell\\Open\\Command", "HKEY_CURRENT_USER\\SOFTWARE\\Classes\\Applications\\QtProject.QtCreator.cpp\\shell\\Open\\Command", "HKEY_CURRENT_USER\\SOFTWARE\\Classes\\Applications\\QtProject.QtCreator.pro\\shell\\Open\\Command" } for _, reg in ipairs(regs) do table.insert(paths, function () local value = val("reg " .. reg) if value then local p = value:find("\\Tools\\QtCreator", 1, true) if p then return path.translate(value:sub(1, p - 1)) end end end) end -- add root logical drive pates, e.g. C:/Qt/Qtx.x.x, D:/Qtx.x.x .. for idx, drive in ipairs(winos.logical_drives()) do if idx < 5 then table.insert(paths, path.join(drive, "Qt", "Qt*")) else break end end else for _, dir in ipairs(os.dirs("~/Qt*")) do table.insert(paths, dir) end end -- special case for android on windows, where qmake is a .bat from version 6.3 -- this case also applys to wasm if is_host("windows") and is_plat("android", "wasm") then local qmake = find_file("qmake.bat", paths, {suffixes = subdirs}) if qmake then return path.directory(path.directory(qmake)), qmake end end -- attempt to find qmake local qmake if is_host("windows") then qmake = find_file("qmake.exe", paths, {suffixes = subdirs}) else -- @see https://github.com/xmake-io/xmake/issues/4881 if sdkver then local major = sdkver:sub(1, 1) local suffixes = {major, "-" .. major, "-qt" .. major, ""} for _, suffix in ipairs(suffixes) do qmake = find_file("qmake" .. suffix, paths, {suffixes = subdirs}) if qmake then break end end end if not qmake then qmake = find_file("qmake", paths, {suffixes = subdirs}) end end if qmake then return path.directory(path.directory(qmake)), qmake end end -- find qmake function _find_qmake(sdkdir, sdkver) -- we attempt to find qmake from qt sdkdir first local sdkdir, qmakefile = _find_sdkdir(sdkdir, sdkver) if qmakefile then return qmakefile end -- try finding qmake with the specific version, e.g. /usr/bin/qmake6 -- https://github.com/xmake-io/xmake/pull/3555 local qmake if sdkver then sdkver = semver.try_parse(sdkver) if sdkver then local major = sdkver:major() local suffixes = {major, "-" .. major, "-qt" .. major} for _, suffix in ipairs(suffixes) do local cachekey = "qmake" .. suffix qmake = find_tool("qmake", {program = cachekey, cachekey = cachekey, paths = sdkdir and path.join(sdkdir, "bin")}) if qmake then break end end end end -- we need to find the default qmake in current system -- maybe we only installed qmake6 if not qmake then local suffixes = {"", "6", "-6", "-qt6", "5", "-5", "-qt5"} for _, suffix in ipairs(suffixes) do local cachekey = "qmake" .. suffix qmake = find_tool("qmake", {program = cachekey, cachekey = cachekey, paths = sdkdir and path.join(sdkdir, "bin")}) if qmake then break end end end if qmake then return qmake.program end end -- get qt environment function _get_qtenvs(qmake, sdkdir) local envs = {} local results -- Try with -qtconf first if sdkdir is provided if sdkdir then local conf_paths = {path.join(sdkdir, "bin", "target_qt.conf"), path.join(sdkdir, "bin", "qt.conf"), path.join(sdkdir, "qt.conf")} for _, conf_path in ipairs(conf_paths) do if os.isfile(conf_path) then results = try {function () return os.iorunv(qmake, {"-query", "-qtconf", conf_path}) end} break end end end -- Fallback to normal query if sdkdir is not specified or -qtconf not applicable results = results or try { function () return os.iorunv(qmake, {"-query"}) end, catch { function (errors) if errors then dprint(tostring(errors)) end end } } if results then for _, qtenv in ipairs(results:split('\n', {plain = true})) do local kv = qtenv:split(':', {plain = true, limit = 2}) -- @note set limit = 2 for supporting value with win-style path, e.g. `key:C:\xxx` if #kv == 2 then envs[kv[1]] = kv[2]:trim() end end return envs end end -- Verify and correct the Qt SDK version for cross-compiling. -- qmake reports its own version (QT_VERSION), not the version specified in the SDK's configuration files. function _tryfix_sdkver_for_cross(sdkdir, sdkver) local qconfig_path = sdkdir and path.join(sdkdir, "mkspecs", "qconfig.pri") if not sdkver or not os.isfile(qconfig_path) then return sdkver end -- Extract the actual SDK version from qconfig.pri local qconfig = io.readfile(qconfig_path) local actual_sdkver = qconfig and qconfig:match("QT_VERSION%s*=%s*(%S+)") -- Expected format: QT_VERSION = x.y.z if not actual_sdkver then return sdkver end if sdkver ~= actual_sdkver then wprint("Host Qt SDK version (%s) differs from Target Qt SDK version (%s). To prevent build issues, please ensure both use the same version.", sdkver, actual_sdkver); end return actual_sdkver end -- find qt sdk toolchains function _find_qt(sdkdir, sdkver, sdkdir_host) -- find qmake local qmake = _find_qmake(sdkdir_host or sdkdir, sdkver) if not qmake then return end -- get qt environments local located_sdkdir = sdkdir and _find_sdkdir(sdkdir, sdkver) local qtenvs = _get_qtenvs(qmake, located_sdkdir or sdkdir) if not qtenvs then return end -- get qt toolchains sdkdir = qtenvs.QT_INSTALL_PREFIX local sdkver = _tryfix_sdkver_for_cross(sdkdir, qtenvs.QT_VERSION) local bindir = qtenvs.QT_INSTALL_BINS local libexecdir = qtenvs.QT_INSTALL_LIBEXECS local qmldir = qtenvs.QT_INSTALL_QML local libdir = qtenvs.QT_INSTALL_LIBS local pluginsdir = qtenvs.QT_INSTALL_PLUGINS local includedir = qtenvs.QT_INSTALL_HEADERS local mkspecsdir = qtenvs.QMAKE_MKSPECS or path.join(qtenvs.QT_HOST_DATA, "mkspecs") local mkspec = qtenvs.QMAKE_XSPEC or qtenvs.QMAKE_SPEC -- handle qt sysroot local qt_sysroot = qtenvs.QT_SYSROOT or "" if #qt_sysroot > 0 and os.isdir(qt_sysroot) then sdkdir = path.join(qt_sysroot, sdkdir) bindir = path.join(qt_sysroot, bindir) libexecdir = path.join(qt_sysroot, libexecdir) qmldir = path.join(qt_sysroot, qmldir) libdir = path.join(qt_sysroot, libdir) pluginsdir = path.join(qt_sysroot, pluginsdir) includedir = path.join(qt_sysroot, includedir) end -- for 6.2 local bindir_host = qtenvs.QT_HOST_BINS if not bindir_host and libexecdir and is_plat("android", "iphoneos") then local rootdir = path.directory(path.directory(bindir)) if is_host("macosx") then bindir_host = path.join(rootdir, "macos", "bin") else -- TODO end end local libexecdir_host = qtenvs.QT_HOST_LIBEXECS if not libexecdir_host and libexecdir and is_plat("android", "iphoneos") then local rootdir = path.directory(path.directory(libexecdir)) if is_host("macosx") then libexecdir_host = path.join(rootdir, "macos", "libexec") else -- TODO end end if sdkdir_host then local located_sdkdir_host = _find_sdkdir(sdkdir_host, sdkver) local qtenvs_host = _get_qtenvs(qmake, located_sdkdir_host or sdkdir_host) if qtenvs_host then bindir_host = qtenvs_host.QT_HOST_BINS or qtenvs_host.QT_INSTALL_BINS or bindir_host libexecdir_host = qtenvs_host.QT_HOST_LIBEXECS or qtenvs_host.QT_INSTALL_LIBEXECS or libexecdir_host end end return {sdkdir = sdkdir, bindir = bindir, bindir_host = bindir_host, libexecdir = libexecdir, libexecdir_host = libexecdir_host, libdir = libdir, includedir = includedir, qmldir = qmldir, pluginsdir = pluginsdir, mkspecsdir = mkspecsdir, mkspec = mkspec, sdkver = sdkver} end -- find qt sdk toolchains -- -- @param sdkdir the qt sdk directory -- @param opt the argument options, e.g. {verbose = true, force = false, version = "5.9.1"} -- -- @return the qt sdk toolchains. e.g. {sdkver = ..., sdkdir = ..., bindir = .., linkdirs = ..., includedirs = ..., .. } -- -- @code -- -- local toolchains = find_qt("~/Qt") -- -- @endcode -- -- find Qt SDK -- -- @param sdkdir the Qt SDK directory (optional) -- @param opt the options, e.g. {verbose = true, force = false} -- @return the SDK info table {sdkdir, bindir, libdir, includedir, ...} -- function main(sdkdir, opt) -- init arguments opt = opt or {} -- attempt to load cache first local key = "detect.sdks.find_qt" local cacheinfo = (sdkdir and detectcache:get2(key, sdkdir)) or detectcache:get(key) or {} if not opt.force and cacheinfo.qt and cacheinfo.qt.sdkdir and os.isdir(cacheinfo.qt.sdkdir) then return cacheinfo.qt end -- find qt local real_sdkdir = sdkdir or config.get("qt") or global.get("qt") or config.get("sdk") local sdkver = opt.version or config.get("qt_sdkver") local sdkdir_host = opt.sdkdir_host or config.get("qt_host") or global.get("qt_host") local qt = _find_qt(real_sdkdir, sdkver, sdkdir_host) if qt then config.set("qt", qt.sdkdir, {force = true, readonly = true}) config.set("qt_sdkver", qt.sdkver, {force = true, readonly = true}) if opt.verbose or option.get("verbose") then local extra = "" if qt.sdkver then extra = " (" .. qt.sdkver .. ")" end cprint("checking for Qt SDK ... ${color.success}%s%s", qt.sdkdir, extra) end else if opt.verbose or option.get("verbose") then cprint("checking for Qt SDK ... ${color.nothing}${text.nothing}") end end -- save to cache cacheinfo.qt = qt or false if sdkdir then detectcache:set2(key, sdkdir, cacheinfo) else detectcache:set(key, cacheinfo) end return qt end