summaryrefslogtreecommitdiff
path: root/xmake/core/base/winos.lua
blob: 73657662edd2817f982ae7c32ce821916c80978e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
--!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        winos.lua
--

-- define module: winos
local winos = winos or {}

-- load modules
local os     = require("base/os")
local path   = require("base/path")
local semver = require("base/semver")

winos._ansi_cp         = winos._ansi_cp or winos.ansi_cp
winos._oem_cp          = winos._oem_cp  or winos.oem_cp
winos._registry_query  = winos._registry_query or winos.registry_query
winos._registry_keys   = winos._registry_keys or winos.registry_keys
winos._registry_values = winos._registry_values or winos.registry_values

function winos.ansi_cp()
    if not winos._ANSI_CP then
         winos._ANSI_CP = winos._ansi_cp()
    end
    return winos._ANSI_CP
end

function winos.oem_cp()
    if not winos._OEM_CP then
         winos._OEM_CP = winos._oem_cp()
    end
    return winos._OEM_CP
end

-- get windows version from name
function winos._version_from_name(name)
    winos._VERSIONS = winos._VERSIONS or {
        nt4      = "4.0"
    ,   win2k    = "5.0"
    ,   winxp    = "5.1"
    ,   ws03     = "5.2"
    ,   win6     = "6.0"
    ,   vista    = "6.0"
    ,   ws08     = "6.0"
    ,   longhorn = "6.0"
    ,   win7     = "6.1"
    ,   win8     = "6.2"
    ,   winblue  = "6.3"
    ,   win81    = "6.3"
    ,   win10    = "10.0"
    ,   win11    = "10.0.22000"
    }
    return winos._VERSIONS[name]
end

-- v1 == v2 with name (winxp, win10, ..)?
function winos._version_eq(self, version)
    if type(version) == "string" then
        local namever = winos._version_from_name(version)
        if namever then
            return semver.compare(self:major() .. '.' .. self:minor(), namever) == 0
        else
            return semver.compare(self:rawstr(), version) == 0
        end
    elseif type(version) == "table" then
        return semver.compare(self:rawstr(), version:rawstr()) == 0
    end
end

-- v1 < v2 with name (winxp, win10, ..)?
function winos._version_lt(self, version)
    if type(version) == "string" then
        local namever = winos._version_from_name(version)
        if namever then
            return semver.compare(self:major() .. '.' .. self:minor(), namever) < 0
        else
            return semver.compare(self:rawstr(), version) < 0
        end
    elseif type(version) == "table" then
        return semver.compare(self:rawstr(), version:rawstr()) < 0
    end
end

-- v1 <= v2 with name (winxp, win10, ..)?
function winos._version_le(self, version)
    if type(version) == "string" then
        local namever = winos._version_from_name(version)
        if namever then
            return semver.compare(self:major() .. '.' .. self:minor(), namever) <= 0
        else
            return semver.compare(self:rawstr(), version) <= 0
        end
    elseif type(version) == "table" then
        return semver.compare(self:rawstr(), version:rawstr()) <= 0
    end
end

-- get system version
function winos.version()
    local winver = winos._VERSION
    if winver == nil then

        -- get winver
        local ok, verstr = os.iorun("cmd /c ver")
        if ok and verstr then
            winver = verstr:match("%[.-([%d%.]+)]")
            if winver then
                winver = winver:trim()
                local sem_winver
                local seg = 0
                for num in winver:gmatch("%d+") do
                    if seg == 0 then
                        sem_winver = num
                    elseif seg == 3 then
                        sem_winver = sem_winver .. "+" .. num
                    else
                        sem_winver = sem_winver .. "." .. num
                    end
                    seg = seg + 1
                end
                if sem_winver then
                    winver = semver.new(sem_winver)
                end
            end
        end
        if not winver then
            winver = semver.new("0.0")
        end

        -- rewrite comparator
        winver.eq = winos._version_eq
        winver.lt = winos._version_lt
        winver.le = winos._version_le
        winos._VERSION = winver
    end
    return winver
end

-- get command arguments on windows to solve 8192 character command line length limit
function winos.cmdargv(argv, opt)

    -- too long arguments?
    local limit = 4096
    local argn = 0
    for _, arg in ipairs(argv) do
        arg = tostring(arg)
        argn = argn + #arg
        if argn > limit then
            break
        end
    end
    if argn > limit then
        opt = opt or {}
        local argsfile = os.tmpfile(opt.tmpkey or os.args(argv)) .. ".args.txt"
        local f = io.open(argsfile, 'w', {encoding = os.is_host("windows") and "ansi"})
        if f then
            -- we need to split args file to solve `fatal error LNK1170: line in command file contains 131071 or more characters`
            -- @see https://github.com/xmake-io/xmake/issues/812
            local idx = 1
            while idx <= #argv do
                arg = tostring(argv[idx])
                arg1 = argv[idx + 1]
                if arg1 then
                    arg1 = tostring(arg1)
                end
                -- we need to ensure `/name value` in same line,
                -- otherwise cl.exe will prompt that the corresponding parameter value cannot be found
                --
                -- e.g.
                --
                -- /sourceDependencies xxxx.json
                -- -Dxxx
                -- foo.obj
                --

                -- if host is not Windows, paths may start with '/', which conflicts with '/<argname>'
                -- note that checking if the next argument ends with '.json' will only work for /sourceDependencies
                -- other arguments will remain broken, perhaps we should discuss a better solution
                if idx + 1 <= #argv and arg:find("^[-/]") and (os.is_host("windows") and (not arg1:find("^[-/]")) or (arg1:endswith(".json"))) then
                    f:write(os.args(arg, {escape = opt.escape}) .. " ")
                    f:write(os.args(arg1, {escape = opt.escape}) .. "\n")
                    idx = idx + 2
                else
                    f:write(os.args(arg, {escape = opt.escape}) .. "\n")
                    idx = idx + 1
                end
            end
            f:close()
        end
        argv = {"@" .. argsfile}
    end
    return argv
end

-- query registry value
--
-- @param keypath   the key path
-- @return          the value and errors
--
-- @code
-- local value, errors = winos.registry_query("HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AeDebug")
-- local value, errors = winos.registry_query("HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AeDebug;Debugger")
-- @endcode
--
function winos.registry_query(keypath)

    -- get value name
    local splitinfo = keypath:split(';', {plain = true})
    local valuename = splitinfo[2] or ""
    keypath = splitinfo[1]

    -- get rootkey, e.g. HKEY_LOCAL_MACHINE
    local rootkey
    local p = keypath:find("\\", 1, true)
    if p then
        rootkey = keypath:sub(1, p - 1)
    end
    if not rootkey then
        return nil, "root key not found!"
    end

    -- get the root directory
    local rootdir = keypath:sub(p + 1)

    -- query value
    return winos._registry_query(rootkey, rootdir, valuename)
end

-- get registry key paths
--
-- @param keypath   the key path (support key pattern, e.g. \\a\\b;xx*yy)
--                  uses "*" to match any part of a key path,
--                  uses "**" to recurse into subkey paths.
-- @return          the result array and errors
--
-- @code
-- local keys, errors = winos.registry_keys("HKEY_LOCAL_MACHINE\\SOFTWARE\\*\\Windows NT\\*\\CurrentVersion\\AeDebug")
-- local keys, errors = winos.registry_keys("HKEY_LOCAL_MACHINE\\SOFTWARE\\**")
-- @endcode
--
function winos.registry_keys(keypath)

    -- get rootkey, e.g. HKEY_LOCAL_MACHINE
    local rootkey
    local p = keypath:find("\\", 1, true)
    if p then
        rootkey = keypath:sub(1, p - 1)
    end
    if not rootkey then
        return
    end
    keypath = keypath:sub(p + 1)

    -- get the root directory
    local pattern
    local rootdir = keypath
    p = rootdir:find("*", 1, true)
    if p then
        pattern = path.pattern(rootdir)
        rootdir = path.directory(rootdir:sub(1, p - 1))
    end

    -- compute the recursion level
    --
    -- infinite recursion: aaa\\**
    -- limit recursion level: aaa\\*\\*
    local recursion = 0
    if keypath:find("**", 1, true) then
        recursion = -1
    else
        -- "aaa\\*\\*" -> "*\\" -> recursion level: 1
        -- "aaa\\*\\xxx" -> "*\\" -> recursion level: 1
        -- "aaa\\*\\subkey\\xxx" -> "*\\\\" -> recursion level: 2
        if p then
            local _, seps = keypath:sub(p):gsub("\\", "")
            if seps > 0 then
                recursion = seps
            end
        end
    end

    -- get keys
    local keys = {}
    local count, errors = winos._registry_keys(rootkey, rootdir, recursion, function (key)
        if not pattern or key:match("^" .. pattern .. "$") then
            table.insert(keys, rootkey .. '\\' .. key)
            if #keys > 4096 then
                return false
            end
        end
        return true
    end)
    if #keys > 4096 then
        return nil, "too much registry keys: " .. keypath
    end
    if count ~= nil then
        return keys
    else
        return nil, errors
    end
end

-- get registry values from the given key path
--
-- @param keypath   the key path (support value pattern, e.g. \\a\\b;xx*yy)
--                  uses "*" to match value name,
-- @return          the values array and errors
--
-- @code
-- local values, errors = winos.registry_values("HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AeDebug")
-- local values, errors = winos.registry_values("HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AeDebug;Debug*")
-- @endcode
--
function winos.registry_values(keypath)

    -- get value pattern
    local splitinfo = keypath:split(';', {plain = true})
    local pattern = splitinfo[2]
    if pattern then
        pattern = path.pattern(pattern)
    end
    keypath = splitinfo[1]

    -- get rootkey, e.g. HKEY_LOCAL_MACHINE
    local rootkey
    local p = keypath:find("\\", 1, true)
    if p then
        rootkey = keypath:sub(1, p - 1)
    end
    if not rootkey then
        return nil, "root key not found!"
    end

    -- get the root directory
    local rootdir = keypath:sub(p + 1)

    -- get value names
    local value_names = {}
    local count, errors = winos._registry_values(rootkey, rootdir, function (value_name)
        if not pattern or value_name:match("^" .. pattern .. "$") then
            table.insert(value_names, rootkey .. "\\" .. rootdir .. ";" .. value_name)
        end
        return true
    end)
    if count ~= nil then
        return value_names
    else
        return nil, errors
    end
end

-- inherit handles in CreateProcess safely?
-- https://github.com/xmake-io/xmake/issues/2902#issuecomment-1326934902
--
function winos.inherit_handles_safely()
    local inherit_handles_safely = winos._INHERIT_HANDLES_SAFELY
    if inherit_handles_safely == nil then
        inherit_handles_safely = winos.version():ge("win7") or false
        winos._INHERIT_HANDLES_SAFELY = inherit_handles_safely
    end
    return inherit_handles_safely
end

-- return module: winos
return winos