diff --git a/xmake/plugins/pack/wix/main.lua b/xmake/plugins/pack/wix/main.lua index 080d4e66dcb..57bb4a2e122 100644 --- a/xmake/plugins/pack/wix/main.lua +++ b/xmake/plugins/pack/wix/main.lua @@ -19,6 +19,7 @@ -- import("lib.detect.find_tool") +import("detect.sdks.find_qt") import("private.action.require.impl.packagenv") import("private.action.require.impl.install_packages") import(".batchcmds") @@ -49,6 +50,406 @@ function _get_wix() return wix, oldenvs end +-- get windeployqt tool for Qt applications +function _get_windeployqt() + local windeployqt = find_tool("windeployqt") + if not windeployqt then + -- Try to find it in Qt installation + local qt = find_qt() + if qt and qt.bindir then + local windeployqt_path = path.join(qt.bindir, "windeployqt.exe") + if os.isfile(windeployqt_path) then + windeployqt = {program = windeployqt_path} + end + end + + if not windeployqt then + return nil + end + end + return windeployqt +end + +-- detect if this is a Qt project +function _is_qt_project(package) + -- Method 1: Check for Qt libraries in links + local links = package:get("links") + if links then + for _, link in ipairs(links) do + if link:lower():find("qt") then + return true + end + end + end + + -- Method 2: Check for Qt packages in requirements + local requires = package:get("requires") + if requires then + for _, require in ipairs(requires) do + if require:lower():find("qt") then + return true + end + end + end + + -- Method 3: Check executable for Qt dependencies (Windows-specific using dumpbin if available) + local main_executable = _find_main_executable(package) + + if main_executable and os.isfile(main_executable) then + -- Try using dumpbin if available + local dumpbin = find_tool("dumpbin") + if dumpbin then + local dumpbin_output = os.iorunv(dumpbin.program, {"/dependents", main_executable}) + if dumpbin_output then + -- Check for Qt DLLs in dependency output + if dumpbin_output:lower():find("qt%d+") or + dumpbin_output:lower():find("qt5") or + dumpbin_output:lower():find("qt6") then + return true + end + end + end + end + + -- Method 4: Check source files for Qt headers/includes + local srcfiles, _ = package:sourcefiles() + for _, srcfile in ipairs(srcfiles or {}) do + if srcfile:endswith(".cpp") or srcfile:endswith(".cc") or srcfile:endswith(".cxx") then + if os.isfile(srcfile) then + local content = io.readfile(srcfile) + if content and (content:find("#include.*[Qq][Tt]") or + content:find("#include.* " .. file_info[1]) + end + return true, qt_files +end + +-- Check if the project uses QML +function _check_qml_usage(package) + -- Method 1: Check for QML-related libraries in links + local links = package:get("links") or {} + for _, link in ipairs(links) do + if link:lower():find("qml") or link:lower():find("quick") then + return true + end + end + + -- Method 2: Check for .qml files in project + local qml_files = os.files("**.qml") + if qml_files and #qml_files > 0 then + return true + end + + -- Method 3: Check source files for QML-related includes + local srcfiles, _ = package:sourcefiles() + for _, srcfile in ipairs(srcfiles or {}) do + if srcfile:endswith(".cpp") or srcfile:endswith(".cc") or srcfile:endswith(".cxx") then + if os.isfile(srcfile) then + local content = io.readfile(srcfile) + if content and (content:find("#include.*QQml") or + content:find("#include.*QQuick") or + content:find("QQmlEngine") or + content:find("QQuickView")) then + return true + end + end + end + end + + return false +end + +-- Find project QML directory +function _find_project_qml_dir() + local possible_qml_dirs = {"qml", "src/qml", "resources/qml", "assets/qml"} + + for _, qml_dir in ipairs(possible_qml_dirs) do + if os.isdir(qml_dir) then + return path.absolute(qml_dir) + end + end + + return nil +end + +-- collect Qt files after windeployqt (legacy function for compatibility) +function _collect_qt_dlls(package, is_qt) + if not is_qt then + return {} + end + local windeployqt = _get_windeployqt() + if not windeployqt then + return _collect_qt_dlls_manual(package) + end + + local success, qt_files = _deploy_qt_dependencies(package, windeployqt) + if success then + return qt_files + else + return _collect_qt_dlls_manual(package) + end +end + +-- Manual Qt DLL collection (fallback) +function _collect_qt_dlls_manual(package) + local qt_dlls = {} + local qt = find_qt() + + if qt and qt.bindir then + local qt_version = qt.sdkver or "5.15" + local is_qt6 = qt_version:startswith("6") + + -- Core Qt DLLs that are almost always needed + local core_dlls = {} + if is_qt6 then + core_dlls = {"Qt6Core.dll", "Qt6Gui.dll", "Qt6Widgets.dll"} + else + core_dlls = {"Qt5Core.dll", "Qt5Gui.dll", "Qt5Widgets.dll"} + end + -- Check which DLLs actually exist and add them + for _, dll_name in ipairs(core_dlls) do + local dll_path = path.join(qt.bindir, dll_name) + if os.isfile(dll_path) then + table.insert(qt_dlls, {dll_path, dll_name}) + print("Found Qt DLL:", dll_path) + end + end + + -- Check for additional Qt modules based on links + local links = package:get("links") or {} + local additional_dlls = {} + + for _, link in ipairs(links) do + local link_lower = link:lower() + if is_qt6 then + if link_lower:find("qt6network") or link_lower:find("qtnetwork") then + table.insert(additional_dlls, "Qt6Network.dll") + end + if link_lower:find("qt6multimedia") or link_lower:find("qtmultimedia") then + table.insert(additional_dlls, "Qt6Multimedia.dll") + end + if link_lower:find("qt6opengl") or link_lower:find("qtopengl") then + table.insert(additional_dlls, "Qt6OpenGL.dll") + end + if link_lower:find("qt6svg") or link_lower:find("qtsvg") then + table.insert(additional_dlls, "Qt6Svg.dll") + end + if link_lower:find("qt6xml") or link_lower:find("qtxml") then + table.insert(additional_dlls, "Qt6Xml.dll") + end + if link_lower:find("qt6quick") or link_lower:find("qtquick") then + table.insert(additional_dlls, "Qt6Quick.dll") + table.insert(additional_dlls, "Qt6Qml.dll") + end + else + if link_lower:find("qt5network") or link_lower:find("qtnetwork") then + table.insert(additional_dlls, "Qt5Network.dll") + end + if link_lower:find("qt5multimedia") or link_lower:find("qtmultimedia") then + table.insert(additional_dlls, "Qt5Multimedia.dll") + end + if link_lower:find("qt5opengl") or link_lower:find("qtopengl") then + table.insert(additional_dlls, "Qt5OpenGL.dll") + end + if link_lower:find("qt5svg") or link_lower:find("qtsvg") then + table.insert(additional_dlls, "Qt5Svg.dll") + end + if link_lower:find("qt5xml") or link_lower:find("qtxml") then + table.insert(additional_dlls, "Qt5Xml.dll") + end + if link_lower:find("qt5quick") or link_lower:find("qtquick") then + table.insert(additional_dlls, "Qt5Quick.dll") + table.insert(additional_dlls, "Qt5Qml.dll") + end + end + end + + -- Add additional DLLs if they exist + for _, dll_name in ipairs(additional_dlls) do + local dll_path = path.join(qt.bindir, dll_name) + if os.isfile(dll_path) then + local already_added = false + for _, existing in ipairs(qt_dlls) do + if existing[2] == dll_name then + already_added = true + break + end + end + if not already_added then + table.insert(qt_dlls, {dll_path, dll_name}) + print("Found additional Qt DLL:", dll_path) + end + end + end + + -- CRITICAL: Add Qt platform plugin DLL (essential for Qt apps on Windows) + local plugins_dir = path.join(qt.sdkdir, "plugins") + if not os.isdir(plugins_dir) and qt.pluginsdir then + plugins_dir = qt.pluginsdir + end + + if os.isdir(plugins_dir) then + local platforms_dir = path.join(plugins_dir, "platforms") + if os.isdir(platforms_dir) then + local platform_dll = path.join(platforms_dir, "qwindows.dll") + if os.isfile(platform_dll) then + table.insert(qt_dlls, {platform_dll, "platforms/qwindows.dll"}) + print("Found Qt platform plugin:", platform_dll) + else + print("Warning: qwindows.dll not found in", platforms_dir) + end + else + print("Warning: platforms directory not found:", platforms_dir) + end + + -- Add other important plugins + local plugin_types = {"imageformats", "iconengines", "styles"} + for _, plugin_type in ipairs(plugin_types) do + local plugin_dir = path.join(plugins_dir, plugin_type) + if os.isdir(plugin_dir) then + local plugin_files = os.files(path.join(plugin_dir, "*.dll")) + for _, plugin_file in ipairs(plugin_files) do + local rel_path = plugin_type .. "/" .. path.filename(plugin_file) + table.insert(qt_dlls, {plugin_file, rel_path}) + print("Found Qt plugin:", plugin_file) + end + end + end + else + print("Warning: Qt plugins directory not found:", plugins_dir) + end + + else + print("Qt SDK not found, cannot collect Qt DLLs automatically") + end + + return qt_dlls +end + -- translate the file path function _translate_filepath(package, filepath) return path.relative(filepath, package:install_rootdir()) @@ -146,7 +547,7 @@ function _get_other_commands(package, cmd, opt) elseif kind == "mkdir" then local dir = _translate_filepath(package, cmd.dir) local subdirectory = dir ~= "." and string.format([[Subdirectory="%s"]], dir) or "" - result = string.format([[]], id, subdirectory) + result = string.format([[]], subdirectory) elseif kind == "wix" then result = cmd.rawstr end @@ -192,12 +593,12 @@ function _build_feature(package, opt) for _, file in ipairs(files) do local srcfile = file[1] local dstname = file[2] - table.insert(result, string.format([[]], srcfile, dstname, _get_id())) + table.insert(result, string.format([[]], srcfile, dstname, _get_id(srcfile .. dstname))) end table.insert(result, "") end - table.insert(result, _get_component_string(name .. "Cmds")) + table.insert(result, _get_component_string(name .. "Cmds", nil)) for _, cmd in ipairs(installcmds) do table.insert(result, _get_other_commands(package, cmd, {install = true})) end @@ -210,11 +611,57 @@ function _build_feature(package, opt) return result end +-- build Qt runtime feature +function _build_qt_feature(package, qt_files) + if #qt_files == 0 then + return {} + end + + local result = {} + table.insert(result, _get_feature_string("QtRuntime", "Qt Runtime Libraries", {default = true, force = true, description = "Qt runtime libraries and plugins required by the application"})) + + -- Group files by directory + local file_groups = {} + for _, file_info in ipairs(qt_files) do + local srcfile = file_info[1] + local dstname = file_info[2] + local dstdir = path.directory(dstname) + + if dstdir == "." or dstdir == "" then + dstdir = "bin" -- Main executable directory + end + + if not file_groups[dstdir] then + file_groups[dstdir] = {} + end + table.insert(file_groups[dstdir], {srcfile, path.filename(dstname)}) + end + + -- Create components for each directory + for dir, files in pairs(file_groups) do + local component_id = _get_id("QtRuntime" .. dir) + local subdir = (dir ~= "bin" and dir ~= ".") and dir or nil + table.insert(result, _get_component_string(component_id, subdir)) + + for _, file_info in ipairs(files) do + local srcfile = file_info[1] + local filename = file_info[2] + local file_id = _get_id("QtFile" .. filename .. dir) + table.insert(result, string.format([[]], srcfile, filename, file_id)) + end + + table.insert(result, "") + end + + table.insert(result, "") + return result +end + -- add to path feature function _add_to_path(package) local result = {} table.insert(result, _get_feature_string("PATH", "Add to PATH", {default = false, force = false, description = "Add to PATH"})) - table.insert(result, _get_component_string("PATH")) + table.insert(result, _get_component_string("PATH", nil)) table.insert(result, [[]]) table.insert(result, "") table.insert(result, "") @@ -223,11 +670,20 @@ end -- get specvars function _get_specvars(package) + local is_qt = _is_qt_project(package) + local qt_files = _collect_qt_dlls(package, is_qt) + local installcmds = batchcmds.get_installcmds(package):cmds() local specvars = table.clone(package:specvars()) local features = {} table.join2(features, _build_feature(package, {default = true, force = true, config_dir = true})) + + -- Add Qt runtime feature if this is a Qt project + if is_qt and #qt_files > 0 then + table.join2(features, _build_qt_feature(package, qt_files)) + end + table.join2(features, _add_to_path(package)) for name, component in table.orderpairs(package:components()) do table.join2(features, _build_feature(component, {name = "Install " .. name})) @@ -252,6 +708,18 @@ function _get_specvars(package) if package:get("company") == nil or package:get("company") == "" then specvars.PACKAGE_COMPANY = package:name() end + + -- Add Qt-specific variables if needed + if is_qt then + specvars.PACKAGE_IS_QT = "true" + local qt = find_qt() + if qt then + specvars.PACKAGE_QT_VERSION = qt.sdkver or "unknown" + end + else + specvars.PACKAGE_IS_QT = "false" + end + return specvars end @@ -316,13 +784,19 @@ function main(package) end cprint("packing %s", package:outputfile()) + + -- Check if this is a Qt project and inform the user + local is_qt = _is_qt_project(package) + if is_qt then + local windeployqt = _get_windeployqt() + end -- get wix local wix, oldenvs = _get_wix() - -- pack nsis package + -- pack wix package _pack_wix(wix.program, package) -- done os.setenvs(oldenvs) -end +end \ No newline at end of file