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
0 commit comments