--!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 main.lua -- -- imports import("core.base.option") import("core.base.semver") import("lib.detect.find_tool") import("private.action.require.impl.packagenv") import("private.action.require.impl.install_packages") import(".filter") import(".batchcmds") -- check makensis, we need check some plugins function _check_makensis(program) local tmpdir = os.tmpfile() .. ".dir" io.writefile(path.join(tmpdir, "test.nsis"), [[ !include "MUI2.nsh" !include "WordFunc.nsh" !include "WinMessages.nsh" !include "FileFunc.nsh" !include "UAC.nsh" Name "test" OutFile "test.exe" Function .onInit FunctionEnd Section "test" InstallExeutable SectionEnd Function un.onInit FunctionEnd Section "Uninstall" SectionEnd]]) os.runv(program, {"test.nsis"}, {curdir = tmpdir}) os.tryrm(tmpdir) end -- get the makensis function _get_makensis() -- enter the environments of nsis local oldenvs = packagenv.enter("nsis") -- find makensis local packages = {} local makensis = find_tool("makensis", {check = _check_makensis}) if not makensis then table.join2(packages, install_packages("nsis")) end -- enter the environments of installed packages for _, instance in ipairs(packages) do instance:envs_enter() end -- we need to force detect and flush detect cache after loading all environments if not makensis then makensis = find_tool("makensis", {check = _check_makensis, force = true}) end assert(makensis, "makensis not found!") return makensis, oldenvs end -- translate the file path function _translate_filepath(package, filepath) return filepath:replace(package:install_rootdir(), "$InstDir", {plain = true}) end -- get command string function _get_command_strings(package, cmd, opt) opt = table.join(cmd.opt or {}, opt) local result = {} local kind = cmd.kind if kind == "cp" then -- https://nsis.sourceforge.io/Reference/File local srcpath = cmd.srcpath local dstpath = _translate_filepath(package, cmd.dstpath) -- match files and directories local srcitems = os.filedirs(srcpath) for _, srcitem in ipairs(srcitems) do if os.isdir(srcitem) then -- copy directory recursively srcitem = path.normalize(srcitem) local dstdir = dstpath if opt.rootdir then dstdir = path.join(dstdir, path.relative(srcitem, opt.rootdir)) else dstdir = path.join(dstdir, path.filename(srcitem)) end dstdir = path.normalize(dstdir) table.insert(result, string.format("SetOutPath \"%s\"", dstdir)) table.insert(result, string.format("File /r \"%s\\*\"", srcitem)) else -- copy file local dstfile = dstpath if #srcitems > 1 or path.islastsep(dstfile) then if opt.rootdir then dstfile = path.join(dstfile, path.relative(srcitem, opt.rootdir)) else dstfile = path.join(dstfile, path.filename(srcitem)) end end srcitem = path.normalize(srcitem) local dstname = path.filename(dstfile) local dstdir = path.normalize(path.directory(dstfile)) table.insert(result, string.format("SetOutPath \"%s\"", dstdir)) table.insert(result, string.format("File \"/oname=%s\" \"%s\"", dstname, srcitem)) end end elseif kind == "rm" then local filepath = _translate_filepath(package, cmd.filepath) table.insert(result, string.format("${%s} \"%s\"", opt.install and "RMFileIfExists" or "unRMFileIfExists", filepath)) if opt.emptydirs then table.insert(result, string.format("${%s} \"%s\"", opt.install and "RMEmptyParentDirs" or "unRMEmptyParentDirs", filepath)) end elseif kind == "rmdir" then local dir = _translate_filepath(package, cmd.dir) table.insert(result, string.format("${%s} \"%s\"", opt.install and "RMDirIfExists" or "unRMDirIfExists", dir)) if opt.emptydirs then table.insert(result, string.format("${%s} \"%s\"", opt.install and "RMEmptyParentDirs" or "unRMEmptyParentDirs", dir)) end elseif kind == "mv" then local srcpath = _translate_filepath(package, cmd.srcpath) local dstpath = _translate_filepath(package, cmd.dstpath) table.insert(result, string.format("Rename \"%s\" \"%s\"", srcpath, dstpath)) elseif kind == "cd" then local dir = _translate_filepath(package, cmd.dir) table.insert(result, string.format("SetOutPath \"%s\"", dir)) elseif kind == "mkdir" then local dir = _translate_filepath(package, cmd.dir) table.insert(result, string.format("CreateDirectory \"%s\"", dir)) elseif kind == "nsis" then table.insert(result, cmd.rawstr) end return result end -- get commands string function _get_commands_string(package, cmds, opt) local cmdstrs = {} for _, cmd in ipairs(cmds) do table.join2(cmdstrs, _get_command_strings(package, cmd, opt)) end return table.concat(cmdstrs, "\n ") end -- get install commands of component function _get_component_installcmds(component) return _get_commands_string(component, batchcmds.get_installcmds(component):cmds(), {install = true}) end -- get uninstall commands of component function _get_component_uninstallcmds(component) return _get_commands_string(component, batchcmds.get_uninstallcmds(component):cmds(), {install = false}) end -- get install commands function _get_installcmds(package) return _get_commands_string(package, batchcmds.get_installcmds(package):cmds(), {install = true}) end -- get uninstall commands function _get_uninstallcmds(package) return _get_commands_string(package, batchcmds.get_uninstallcmds(package):cmds(), {install = false}) end -- get value and filter it function _get_filter_value(package, name) local value = package:get(name) if type(value) == "string" then value = filter.handle(value, package) end return value end -- get target file path function _get_target_filepath(package) local targetfile for _, target in ipairs(package:targets()) do if target:is_binary() then targetfile = target:targetfile() break end end if targetfile then return _translate_filepath(package, path.join(package:bindir(), path.filename(targetfile))) end end -- get specvars function _get_specvars(package) local specvars = table.clone(package:specvars()) specvars.PACKAGE_WORKDIR = path.absolute(os.projectdir()) specvars.PACKAGE_BINDIR = _translate_filepath(package, package:bindir()) specvars.PACKAGE_OUTPUTFILE = path.absolute(package:outputfile()) if specvars.PACKAGE_VERSION_BUILD then -- @see https://github.com/xmake-io/xmake/issues/5306 specvars.PACKAGE_VERSION_BUILD = specvars.PACKAGE_VERSION_BUILD:gsub(" ", "_") end specvars.PACKAGE_INSTALLCMDS = function () return _get_installcmds(package) end specvars.PACKAGE_UNINSTALLCMDS = function () return _get_uninstallcmds(package) end specvars.PACKAGE_NSIS_DISPLAY_ICON = function () local iconpath = _get_filter_value(package, "nsis_displayicon") if iconpath then iconpath = path.join(package:installdir(), iconpath) end if not iconpath then iconpath = _get_target_filepath(package) or "" end return _translate_filepath(package, iconpath) end -- install sections local install_sections = {} local install_descs = {} local install_description_texts = {} for name, component in table.orderpairs(package:components()) do local installcmds = _get_component_installcmds(component) if installcmds and #installcmds > 0 then local tag = "Install" .. name table.insert(install_sections, string.format('Section%s "%s" %s', component:get("default") == false and " /o" or "", component:title(), tag)) table.insert(install_sections, installcmds) table.insert(install_sections, "SectionEnd") table.insert(install_descs, string.format('LangString DESC_%s ${LANG_ENGLISH} "%s"', tag, component:description() or "")) table.insert(install_description_texts, string.format('!insertmacro MUI_DESCRIPTION_TEXT ${%s} $(DESC_%s)', tag, tag)) end local uninstallcmds = _get_component_uninstallcmds(component) if uninstallcmds and #uninstallcmds > 0 then local tag = "Uninstall" .. name table.insert(install_sections, string.format('Section "un.%s" %s', component:title(), tag)) table.insert(install_sections, uninstallcmds) table.insert(install_sections, "SectionEnd") end end specvars.PACKAGE_NSIS_INSTALL_SECTIONS = table.concat(install_sections, "\n ") specvars.PACKAGE_NSIS_INSTALL_DESCS = table.concat(install_descs, "\n ") specvars.PACKAGE_NSIS_INSTALL_DESCRIPTION_TEXTS = table.concat(install_description_texts, "\n ") return specvars end -- pack nsis package function _pack_nsis(makensis, package) -- install the initial specfile local specfile = path.join(package:builddir(), package:basename() .. ".nsi") if not os.isfile(specfile) then local specfile_template = package:get("specfile") or path.join(os.programdir(), "scripts", "xpack", "nsis", "makensis.nsi") os.cp(specfile_template, specfile) end -- replace variables in specfile, -- and we need to avoid `attempt to yield across a C-call boundary` in io.gsub local specvars = _get_specvars(package) local pattern = package:extraconf("specfile", "pattern") or "%${([^\n]-)}" local specvars_names = {} local specvars_values = {} io.gsub(specfile, "(" .. pattern .. ")", function(_, name) table.insert(specvars_names, name) end, {encoding = "ansi"}) for _, name in ipairs(specvars_names) do name = name:trim() if specvars_values[name] == nil then local value = specvars[name] if type(value) == "function" then value = value() end if value ~= nil then dprint(" > replace %s -> %s", name, value) end if type(value) == "table" then dprint("invalid variable value", value) end specvars_values[name] = value end end io.gsub(specfile, "(" .. pattern .. ")", function(_, name) name = name:trim() return specvars_values[name] end, {encoding = "ansi"}) -- make package os.vrunv(makensis, {specfile}) end function main(package) -- only for windows if not is_host("windows") then return end cprint("packing %s", package:outputfile()) -- get makensis local makensis, oldenvs = _get_makensis() -- pack nsis package _pack_nsis(makensis.program, package) -- done os.setenvs(oldenvs) end