--!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 A2va
-- @file main.lua
--
import("lib.detect.find_tool")
import("private.action.require.impl.packagenv")
import("private.action.require.impl.install_packages")
import(".batchcmds")
-- get the wixtoolset
function _get_wix()
-- enter the environments of wix
local oldenvs = packagenv.enter("wixtoolset")
-- find makensis
local packages = {}
local wix = find_tool("wix")
if not wix then
table.join2(packages, install_packages("wixtoolset"))
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 wix then
wix = find_tool("wix", {force = true})
end
assert(wix, "wix not found (ensure that wix is up to date)!")
return wix, oldenvs
end
-- translate the file path
function _translate_filepath(package, filepath)
return path.relative(filepath, package:install_rootdir())
end
function _to_rtf_string(str)
if str == "" then
return str
end
local escape_text = str:gsub("\\", "\\\\")
escape_text = escape_text:gsub("{", "\\{")
escape_text = escape_text:gsub("}", "\\}")
local rtf = "{\\rtf1\\ansi{\\fonttbl\\f0\\fswiss Helvetica;}\\f0\\pard ";
rtf = rtf .. escape_text:gsub("\r\n", " \\par ") .. "}"
return rtf
end
-- get a table where the key is a directory and the value a list of files
-- used to regroup all files that are placed in the same directory under the same component.
function _get_cp_kind_table(package, cmds, opt)
local result = {}
for _, cmd in ipairs(cmds) do
if cmd.kind ~= "cp" then
goto continue
end
local option = table.join(cmd.opt or {}, opt)
-- match files and directories
local srcitems = os.filedirs(cmd.srcpath)
for _, srcitem in ipairs(srcitems) do
local srcitem = srcitem
if os.isdir(srcitem) then
-- for directory, recursively collect all files in it
local rootdir = option.rootdir or srcitem
local files = os.files(path.join(srcitem, "**"))
for _, srcfile in ipairs(files) do
local srcfile = srcfile
-- the destination is directory? append the relative path
local dstfile = cmd.dstpath
if option.rootdir then
dstfile = path.join(dstfile, path.relative(srcfile, option.rootdir))
else
dstfile = path.join(dstfile, path.relative(srcfile, rootdir))
end
srcfile = path.normalize(srcfile)
local dstname = path.filename(dstfile)
local dstdir = path.normalize(path.directory(dstfile))
dstdir = _translate_filepath(package, dstdir)
if result[dstdir] then
table.insert(result[dstdir], {srcfile, dstname})
else
result[dstdir] = {{srcfile, dstname}}
end
end
else
-- for file, use the original logic
local dstfile = cmd.dstpath
if #srcitems > 1 or path.islastsep(dstfile) then
if option.rootdir then
dstfile = path.join(dstfile, path.relative(srcitem, option.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))
dstdir = _translate_filepath(package, dstdir)
if result[dstdir] then
table.insert(result[dstdir], {srcitem, dstname})
else
result[dstdir] = {{srcitem, dstname}}
end
end
end
::continue::
end
return result
end
-- get id
function _get_id(name)
return "A" .. hash.uuid(name):gsub("-", ".")
end
-- for each id/guid in the file wix want them to be unique
-- so compute a hash for each directory based on the file that are inside
function _get_dir_id(cp_table)
local hashes = {}
for dir, files in pairs(cp_table) do
local s = ""
for _, file in ipairs(files) do
s = s .. table.concat(file, "")
end
-- wix required id to start with a letter and without any hyphen
hashes[dir] = _get_id(s)
end
return hashes
end
-- get custom commands
function _get_other_commands(package, cmd, opt)
opt = table.join(cmd.opt or {}, opt)
local result = ""
local kind = cmd.kind
local id = _get_id()
if kind == "rm" then
local subdirectory = _translate_filepath(package, path.directory(cmd.filepath))
subdirectory = subdirectory ~= "." and string.format([[Subdirectory="%s"]], subdirectory) or ""
local on = opt.install and [[On="install"]] or [[On="uninstall"]]
local filename = path.filename(cmd.filepath)
result = string.format([[]], id, filename, subdirectory, on)
elseif kind == "rmdir" then
local dir = _translate_filepath(package, cmd.dir)
local subdirectory = dir ~= "." and string.format([[Subdirectory="%s"]], dir) or ""
local on = opt.install and [[On="install"]] or [[On="uninstall"]]
result = string.format([[]], id, subdirectory, on)
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)
elseif kind == "wix" then
result = cmd.rawstr
end
return result
end
-- get the string of a wix feature
function _get_feature_string(name, title, opt)
local level = opt.default and 1 or 2
local description = opt.description or ""
local allow_absent = opt.force and "false" or "true"
local allow_advertise = opt.force and "false" or "true"
local typical_default = [[TypicalDefault="install"]]
local directory = opt.config_dir and [[ConfigurableDirectory="INSTALLFOLDER"]] or ""
local feature = string.format([[]],
name:gsub("[ ()]", ""), title, description, level, allow_advertise, allow_absent, typical_default, directory)
return feature
end
function _get_component_string(id, subdirectory)
local subdirectory = (subdirectory ~= "." and subdirectory ~= nil) and string.format([[Subdirectory="%s"]], subdirectory) or ""
return string.format([[]], id:gsub("[ ()]", ""), hash.uuid(id), subdirectory)
end
-- build a feature from batchcmds
function _build_feature(package, opt)
opt = opt or {}
local default = opt.default or package:get("default")
local result = {}
local name = opt.name or package:title()
table.insert(result, _get_feature_string(name, package:title(), table.join(opt, {default = default, description = package:description()})))
local installcmds = batchcmds.get_installcmds(package):cmds()
local uninstallcmds = batchcmds.get_uninstallcmds(package):cmds()
local cp_table = _get_cp_kind_table(package, installcmds, opt)
table.remove_if(installcmds, function (_, cmd) return cmd.kind == "cp" end)
local dir_id = _get_dir_id(cp_table)
for dir, files in pairs(cp_table) do
table.insert(result, _get_component_string(dir_id[dir], dir))
for _, file in ipairs(files) do
local srcfile = file[1]
local dstname = file[2]
table.insert(result, string.format([[]], srcfile, dstname, _get_id()))
end
table.insert(result, "")
end
table.insert(result, _get_component_string(name .. "Cmds"))
for _, cmd in ipairs(installcmds) do
table.insert(result, _get_other_commands(package, cmd, {install = true}))
end
for _, cmd in ipairs(uninstallcmds) do
table.insert(result, _get_other_commands(package, cmd, {install = false}))
end
table.insert(result, "")
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, [[]])
table.insert(result, "")
table.insert(result, "")
return result
end
-- get specvars
function _get_specvars(package)
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}))
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}))
end
specvars.PACKAGE_LICENSEFILE = function ()
local rtf_string = ""
local licensefile = package:get("licensefile")
if licensefile then
rtf_string = _to_rtf_string(io.readfile(licensefile))
end
local rtf_file = path.join(package:builddir(), "license.rtf")
io.writefile(rtf_file, rtf_string)
return rtf_file
end
specvars.PACKAGE_WIX_CMDS = table.concat(features, "\n ")
specvars.PACKAGE_WIX_UPGRADECODE = hash.uuid(package:name())
-- company cannot be empty with wix
if package:get("company") == nil or package:get("company") == "" then
specvars.PACKAGE_COMPANY = package:name()
end
return specvars
end
function _pack_wix(wix, package)
-- install the initial specfile
local specfile = path.join(package:builddir(), package:basename() .. ".wxs")
if not os.isfile(specfile) then
local specfile_template = package:get("specfile") or path.join(os.programdir(), "scripts", "xpack", "wix", "msi.wxs")
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)
for _, name in ipairs(specvars_names) do
local 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)
local argv = {"build", specfile}
table.join2(argv, {"-ext", "WixToolset.UI.wixext"})
table.join2(argv, {"-o", package:outputfile()})
if package:arch() == "x64" then
table.join2(argv, {"-arch", "x64"})
elseif package:arch() == "x86" then
table.join2(argv, {"-arch", "x86"})
end
-- make package
os.vrunv(wix, argv)
end
function main(package)
-- only for windows
if not is_host("windows") then
return
end
cprint("packing %s", package:outputfile())
-- get wix
local wix, oldenvs = _get_wix()
-- pack nsis package
_pack_wix(wix.program, package)
-- done
os.setenvs(oldenvs)
end