Skip to content

Commit 1dfee59

Browse files
authored
Merge pull request #4788 from A2va/wixtoolset
Xpack: wix toolset support
2 parents d1c2095 + 1f81152 commit 1dfee59

File tree

5 files changed

+362
-2
lines changed

5 files changed

+362
-2
lines changed

tests/plugins/pack/xmake.lua

+6-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ target("foo")
1919
add_packages("zlib")
2020

2121
xpack("test")
22-
set_formats("nsis", "srpm", "rpm", "zip", "targz", "srczip", "srctargz", "runself")
22+
set_formats("nsis", "srpm", "rpm", "zip", "targz", "srczip", "srctargz", "runself", "wix")
2323
set_title("hello")
2424
set_author("ruki")
2525
set_description("A test installer.")
@@ -61,6 +61,11 @@ xpack_component("LongPath")
6161
set_title("Enable Long Path")
6262
set_description("Increases the maximum path length limit, up to 32,767 characters (before 256).")
6363
on_installcmd(function (component, batchcmds)
64+
batchcmds:rawcmd("wix", [[
65+
<RegistryKey Root="HKLM" Key="SYSTEM\CurrentControlSet\Control\FileSystem">
66+
<RegistryValue Type="integer" Name="LongPathsEnabled" Value="1" KeyPath="yes"/>
67+
</RegistryKey>
68+
]])
6469
batchcmds:rawcmd("nsis", [[
6570
${If} $NoAdmin == "false"
6671
; Enable long path

xmake/plugins/pack/wix/main.lua

+313
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
1+
--!A cross-platform build utility based on Lua
2+
--
3+
-- Licensed under the Apache License, Version 2.0 (the "License");
4+
-- you may not use this file except in compliance with the License.
5+
-- You may obtain a copy of the License at
6+
--
7+
-- http://www.apache.org/licenses/LICENSE-2.0
8+
--
9+
-- Unless required by applicable law or agreed to in writing, software
10+
-- distributed under the License is distributed on an "AS IS" BASIS,
11+
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
-- See the License for the specific language governing permissions and
13+
-- limitations under the License.
14+
--
15+
-- Copyright (C) 2015-present, TBOOX Open Source Group.
16+
--
17+
-- @author A2va
18+
-- @file main.lua
19+
--
20+
21+
import("lib.detect.find_tool")
22+
import("private.action.require.impl.packagenv")
23+
import("private.action.require.impl.install_packages")
24+
25+
import(".batchcmds")
26+
27+
-- get the wixtoolset
28+
function _get_wix()
29+
30+
-- enter the environments of wix
31+
local oldenvs = packagenv.enter("wixtoolset")
32+
33+
-- find makensis
34+
local packages = {}
35+
local wix = find_tool("wix", {require_version = ">=4.0.0"})
36+
if not wix then
37+
table.join2(packages, install_packages("wixtoolset"))
38+
end
39+
40+
-- enter the environments of installed packages
41+
for _, instance in ipairs(packages) do
42+
instance:envs_enter()
43+
end
44+
45+
-- we need to force detect and flush detect cache after loading all environments
46+
if not wix then
47+
wix = find_tool("wix", {force = true})
48+
end
49+
assert(wix, "wix not found (ensure that wix is up to date)!")
50+
return wix, oldenvs
51+
end
52+
53+
-- translate the file path
54+
function _translate_filepath(package, filepath)
55+
return path.relative(filepath, package:install_rootdir())
56+
end
57+
58+
function _to_rtf_string(str)
59+
if str == "" then
60+
return str
61+
end
62+
63+
local escape_text = str:gsub("\\", "\\\\")
64+
escape_text = escape_text:gsub("{", "\\{")
65+
escape_text = escape_text:gsub("}", "\\}")
66+
67+
local rtf = "{\\rtf1\\ansi{\\fonttbl\\f0\\fswiss Helvetica;}\\f0\\pard ";
68+
rtf = rtf .. escape_text:gsub("\r\n", " \\par ") .. "}"
69+
return rtf
70+
end
71+
72+
-- get a table where the key is a directory and the value a list of files
73+
-- used to regroup all files that are placed in the same directory under the same component.
74+
function _get_cp_kind_table(package, cmds, opt)
75+
76+
local result = {}
77+
for _, cmd in ipairs(cmds) do
78+
if cmd.kind ~= "cp" then
79+
goto continue
80+
end
81+
82+
local option = table.join(cmd.opt or {}, opt)
83+
local srcfiles = os.files(cmd.srcpath)
84+
for _, srcfile in ipairs(srcfiles) do
85+
-- the destination is directory? append the filename
86+
local dstfile = cmd.dstpath
87+
if #srcfiles > 1 or path.islastsep(dstfile) then
88+
if option.rootdir then
89+
dstfile = path.join(dstfile, path.relative(srcfile, option.rootdir))
90+
else
91+
dstfile = path.join(dstfile, path.filename(srcfile))
92+
end
93+
end
94+
srcfile = path.normalize(srcfile)
95+
local dstname = path.filename(dstfile)
96+
local dstdir = path.normalize(path.directory(dstfile))
97+
dstdir = _translate_filepath(package, dstdir)
98+
99+
if result[dstdir] then
100+
table.insert(result[dstdir], {srcfile, dstname})
101+
else
102+
result[dstdir] = {{srcfile, dstname}}
103+
end
104+
end
105+
::continue::
106+
end
107+
return result
108+
end
109+
110+
function _get_other_commands(package, cmd, opt)
111+
opt = table.join(cmd.opt or {}, opt)
112+
local result = ""
113+
local kind = cmd.kind
114+
115+
if kind == "rm" then
116+
local subdirectory = _translate_filepath(package, path.directory(cmd.filepath))
117+
subdirectory = subdirectory ~= "." and string.format([[Subdirectory="%s"]], subdirectory) or ""
118+
local on = opt.install and [[On="install"]] or [[On="uninstall"]]
119+
local filename = path.filename(cmd.filepath)
120+
121+
result = string.format([[<RemoveFile Directory="INSTALLFOLDER" Name="%s" %s %s/>]], filename, subdirectory, on)
122+
elseif kind == "rmdir" then
123+
local dir = _translate_filepath(package, cmd.dir)
124+
local subdirectory = dir ~= "." and string.format([[Subdirectory="%s"]], dir) or ""
125+
local on = opt.install and [[On="install"]] or [[On="uninstall"]]
126+
127+
result = string.format([[<RemoveFolder Directory="INSTALLFOLDER" %s %s/>]], subdirectory, on)
128+
elseif kind == "mkdir" then
129+
local dir = _translate_filepath(package, cmd.dir)
130+
local subdirectory = dir ~= "." and string.format([[Subdirectory="%s"]], dir) or ""
131+
result = string.format([[<CreateFolder Directory="INSTALLFOLDER" %s/>]], subdirectory)
132+
elseif kind == "wix" then
133+
result = cmd.rawstr
134+
end
135+
return result
136+
end
137+
138+
-- get the string of a wix feature
139+
function _get_feature_string(name, title, opt)
140+
local level = opt.default and 1 or 2
141+
local description = opt.description or ""
142+
local allow_absent = opt.force and "false" or "true"
143+
local allow_advertise = opt.force and "false" or "true"
144+
local typical_default = [[TypicalDefault="install"]]
145+
local directory = opt.config_dir and [[ConfigurableDirectory="INSTALLFOLDER"]] or ""
146+
local feature = string.format([[<Feature Id="%s" Title="%s" Description="%s" Level="%d" AllowAdvertise="%s" AllowAbsent="%s" %s %s>]], name:gsub(" ", ""), title, description, level, allow_advertise, allow_absent, typical_default, directory)
147+
return feature
148+
end
149+
150+
function _get_component_string(id, subdirectory)
151+
local subdirectory = (subdirectory ~= "." and subdirectory ~= nil) and string.format([[Subdirectory="%s"]], subdirectory) or ""
152+
return string.format([[<Component Id="%s" Guid="%s" Directory="INSTALLFOLDER" %s>]], id:gsub(" ", ""), hash.uuid(id), subdirectory)
153+
end
154+
155+
-- for each id/guid in the file wix want them to be unique
156+
-- so compute a hash for each directory based on the file that are inside
157+
function _get_dir_id(cp_table)
158+
local hashes = {}
159+
for dir, files in pairs(cp_table) do
160+
local s = ""
161+
for _, file in ipairs(files) do
162+
s = s .. table.concat(file, "")
163+
end
164+
-- wix required id to start with a letter and without any hyphen
165+
hashes[dir] = "A".. hash.uuid(s):gsub("-", ".")
166+
end
167+
return hashes
168+
end
169+
170+
-- build a feature from batchcmds
171+
function _build_feature(package, opt)
172+
opt = opt or {}
173+
local default = opt.default or package:get("default")
174+
175+
local result = {}
176+
local name = opt.name or package:title()
177+
table.insert(result, _get_feature_string(name, package:title(), table.join(opt, {default = default, description = package:description()})))
178+
179+
local installcmds = batchcmds.get_installcmds(package):cmds()
180+
local uninstallcmds = batchcmds.get_uninstallcmds(package):cmds()
181+
182+
local cp_table = _get_cp_kind_table(package, installcmds, opt)
183+
table.remove_if(installcmds, function (_, cmd) return cmd.kind == "cp" end)
184+
185+
local dir_id = _get_dir_id(cp_table)
186+
187+
for dir, files in pairs(cp_table) do
188+
table.insert(result, _get_component_string(dir_id[dir], dir))
189+
for _, file in ipairs(files) do
190+
local srcfile = file[1]
191+
local dstname = file[2]
192+
table.insert(result, string.format([[<File Source="%s" Name="%s"/>]], srcfile, dstname))
193+
end
194+
table.insert(result, "</Component>")
195+
end
196+
197+
table.insert(result, _get_component_string(name.. "Cmds"))
198+
for _, cmd in ipairs(installcmds) do
199+
table.insert(result, _get_other_commands(package, cmd, {install = true}))
200+
end
201+
for _, cmd in ipairs(uninstallcmds) do
202+
table.insert(result, _get_other_commands(package, cmd, {install = false}))
203+
end
204+
205+
table.insert(result, "</Component>")
206+
table.insert(result, "</Feature>")
207+
return result
208+
end
209+
210+
-- add to path feature
211+
function _add_to_path(package)
212+
local result = {}
213+
table.insert(result, _get_feature_string("PATH", "Add to PATH", {default = false, force = false, description = "Add to PATH"}))
214+
table.insert(result, _get_component_string("PATH"))
215+
table.insert(result, [[<Environment Id="PATH" Name="PATH" Value="[INSTALLFOLDER]bin" Permanent="false" Part="last" Action="set" System="true" />]])
216+
table.insert(result, "</Component>")
217+
table.insert(result, "</Feature>")
218+
return result
219+
end
220+
221+
-- get specvars
222+
function _get_specvars(package)
223+
224+
local installcmds = batchcmds.get_installcmds(package):cmds()
225+
local specvars = table.clone(package:specvars())
226+
227+
local features = {}
228+
table.join2(features, _build_feature(package, {default = true, force = true, config_dir = true}))
229+
table.join2(features, _add_to_path(package))
230+
231+
for name, component in table.orderpairs(package:components()) do
232+
table.join2(features, _build_feature(component, {name = "Install " .. name}))
233+
end
234+
235+
specvars.PACKAGE_LICENSEFILE = function ()
236+
local rtf_string = ""
237+
local licensefile = package:get("licensefile")
238+
if licensefile then
239+
rtf_string = _to_rtf_string(io.readfile(licensefile))
240+
end
241+
242+
local rtf_file = path.join(package:buildir(), "license.rtf")
243+
io.writefile(rtf_file, rtf_string)
244+
return rtf_file
245+
end
246+
247+
specvars.PACKAGE_WIX_CMDS = table.concat(features, "\n ")
248+
specvars.PACKAGE_WIX_UPGRADECODE = hash.uuid(package:name())
249+
250+
-- company cannot be empty with wix
251+
if package:get("company") == nil or package:get("company") == "" then
252+
specvars.PACKAGE_COMPANY = package:name()
253+
end
254+
return specvars
255+
end
256+
257+
function _pack_wix(wix, package)
258+
259+
-- install the initial specfile
260+
local specfile = package:specfile()
261+
if not os.isfile(specfile) then
262+
local specfile_template = path.join(os.programdir(), "scripts", "xpack", "wix", "msi.wxs")
263+
os.cp(specfile_template, specfile)
264+
end
265+
266+
-- replace variables in specfile
267+
local specvars = _get_specvars(package)
268+
local pattern = package:extraconf("specfile", "pattern") or "%${([^\n]-)}"
269+
io.gsub(specfile, "(" .. pattern .. ")", function(_, name)
270+
name = name:trim()
271+
local value = specvars[name]
272+
if type(value) == "function" then
273+
value = value()
274+
end
275+
if value ~= nil then
276+
dprint(" > replace %s -> %s", name, value)
277+
end
278+
if type(value) == "table" then
279+
dprint("invalid variable value", value)
280+
end
281+
return value
282+
end)
283+
284+
local argv = {"build", specfile}
285+
table.join2(argv, {"-ext", "WixToolset.UI.wixext"})
286+
table.join2(argv, {"-o", package:outputfile()})
287+
288+
if package:arch() == "x64" then
289+
table.join2(argv, {"-arch", "x64"})
290+
elseif package:arch() == "x86" then
291+
table.join2(argv, {"-arch", "x86"})
292+
end
293+
294+
-- make package
295+
os.vrunv(wix, argv)
296+
end
297+
298+
function main(package)
299+
-- only for windows
300+
if not is_host("windows") then
301+
return
302+
end
303+
304+
cprint("packing %s", package:outputfile())
305+
-- get wix
306+
local wix, oldenvs = _get_wix()
307+
308+
-- pack nsis package
309+
_pack_wix(wix.program, package)
310+
311+
-- done
312+
os.setenvs(oldenvs)
313+
end

xmake/plugins/pack/xmake.lua

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ task("pack")
3232
"e.g.",
3333
" - xmake pack -f nsis,deb,rpm",
3434
"values:",
35-
values = {"nsis", "deb", "srpm", "rpm", "runself", "targz", "zip", "srctargz", "srczip"}},
35+
values = {"nsis", "wix", "deb", "srpm", "rpm", "runself", "targz", "zip", "srctargz", "srczip"}},
3636
{},
3737
{nil, "packages", "vs", nil, "The package names."}
3838
}

xmake/plugins/pack/xpack.lua

+3
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ function xpack:inputkind()
190190
local inputkind = self:get("inputkind")
191191
if inputkind == nil then
192192
local inputkinds = {
193+
wix = "binary",
193194
nsis = "binary",
194195
zip = "binary",
195196
targz = "binary",
@@ -321,6 +322,7 @@ end
321322
-- get the specfile path
322323
function xpack:specfile()
323324
local extensions = {
325+
wix = ".wxs",
324326
nsis = ".nsi",
325327
srpm = ".spec",
326328
rpm = ".spec",
@@ -335,6 +337,7 @@ function xpack:extension()
335337
local extension = self:get("extension")
336338
if extension == nil then
337339
local extensions = {
340+
wix = ".msi",
338341
nsis = ".exe",
339342
zip = ".zip",
340343
targz = ".tar.gz",

0 commit comments

Comments
 (0)