1--- Windows implementation of filesystem and platform abstractions. 2-- Download http://unxutils.sourceforge.net/ for Windows GNU utilities 3-- used by this module. 4local win32 = {} 5 6local fs = require("luarocks.fs") 7 8local cfg = require("luarocks.core.cfg") 9local dir = require("luarocks.dir") 10local path = require("luarocks.path") 11local util = require("luarocks.util") 12 13-- Monkey patch io.popen and os.execute to make sure quoting 14-- works as expected. 15-- See http://lua-users.org/lists/lua-l/2013-11/msg00367.html 16local _prefix = "type NUL && " 17local _popen, _execute = io.popen, os.execute 18 19-- luacheck: push globals io os 20io.popen = function(cmd, ...) return _popen(_prefix..cmd, ...) end 21os.execute = function(cmd, ...) return _execute(_prefix..cmd, ...) end 22-- luacheck: pop 23 24--- Annotate command string for quiet execution. 25-- @param cmd string: A command-line string. 26-- @return string: The command-line, with silencing annotation. 27function win32.quiet(cmd) 28 return cmd.." 2> NUL 1> NUL" 29end 30 31--- Annotate command string for execution with quiet stderr. 32-- @param cmd string: A command-line string. 33-- @return string: The command-line, with stderr silencing annotation. 34function win32.quiet_stderr(cmd) 35 return cmd.." 2> NUL" 36end 37 38-- Split path into drive, root and the rest. 39-- Example: "c:\\hello\\world" becomes "c:" "\\" "hello\\world" 40-- if any part is missing from input, it becomes an empty string. 41local function split_root(pathname) 42 local drive = "" 43 local root = "" 44 local rest 45 46 local unquoted = pathname:match("^['\"](.*)['\"]$") 47 if unquoted then 48 pathname = unquoted 49 end 50 51 if pathname:match("^.:") then 52 drive = pathname:sub(1, 2) 53 pathname = pathname:sub(3) 54 end 55 56 if pathname:match("^[\\/]") then 57 root = pathname:sub(1, 1) 58 rest = pathname:sub(2) 59 else 60 rest = pathname 61 end 62 63 return drive, root, rest 64end 65 66--- Quote argument for shell processing. Fixes paths on Windows. 67-- Adds double quotes and escapes. 68-- @param arg string: Unquoted argument. 69-- @return string: Quoted argument. 70function win32.Q(arg) 71 assert(type(arg) == "string") 72 -- Use Windows-specific directory separator for paths. 73 -- Paths should be converted to absolute by now. 74 local drive, root, rest = split_root(arg) 75 if root ~= "" then 76 arg = arg:gsub("/", "\\") 77 end 78 if arg == "\\" then 79 return '\\' -- CHDIR needs special handling for root dir 80 end 81 -- URLs and anything else 82 arg = arg:gsub('\\(\\*)"', '\\%1%1"') 83 arg = arg:gsub('\\+$', '%0%0') 84 arg = arg:gsub('"', '\\"') 85 arg = arg:gsub('(\\*)%%', '%1%1"%%"') 86 return '"' .. arg .. '"' 87end 88 89--- Quote argument for shell processing in batch files. 90-- Adds double quotes and escapes. 91-- @param arg string: Unquoted argument. 92-- @return string: Quoted argument. 93function win32.Qb(arg) 94 assert(type(arg) == "string") 95 -- Use Windows-specific directory separator for paths. 96 -- Paths should be converted to absolute by now. 97 local drive, root, rest = split_root(arg) 98 if root ~= "" then 99 arg = arg:gsub("/", "\\") 100 end 101 if arg == "\\" then 102 return '\\' -- CHDIR needs special handling for root dir 103 end 104 -- URLs and anything else 105 arg = arg:gsub('\\(\\*)"', '\\%1%1"') 106 arg = arg:gsub('\\+$', '%0%0') 107 arg = arg:gsub('"', '\\"') 108 arg = arg:gsub('%%', '%%%%') 109 return '"' .. arg .. '"' 110end 111 112--- Return an absolute pathname from a potentially relative one. 113-- @param pathname string: pathname to convert. 114-- @param relative_to string or nil: path to prepend when making 115-- pathname absolute, or the current dir in the dir stack if 116-- not given. 117-- @return string: The pathname converted to absolute. 118function win32.absolute_name(pathname, relative_to) 119 assert(type(pathname) == "string") 120 assert(type(relative_to) == "string" or not relative_to) 121 122 relative_to = (relative_to or fs.current_dir()):gsub("[\\/]*$", "") 123 local drive, root, rest = split_root(pathname) 124 if root:match("[\\/]$") then 125 -- It's an absolute path already. Ensure is not quoted. 126 return drive .. root .. rest 127 else 128 -- It's a relative path, join it with base path. 129 -- This drops drive letter from paths like "C:foo". 130 return relative_to .. "/" .. rest 131 end 132end 133 134--- Return the root directory for the given path. 135-- For example, for "c:\hello", returns "c:\" 136-- @param pathname string: pathname to use. 137-- @return string: The root of the given pathname. 138function win32.root_of(pathname) 139 local drive, root, rest = split_root(fs.absolute_name(pathname)) 140 return drive .. root 141end 142 143--- Create a wrapper to make a script executable from the command-line. 144-- @param script string: Pathname of script to be made executable. 145-- @param target string: wrapper target pathname (without wrapper suffix). 146-- @param name string: rock name to be used in loader context. 147-- @param version string: rock version to be used in loader context. 148-- @return boolean or (nil, string): True if succeeded, or nil and 149-- an error message. 150function win32.wrap_script(script, target, deps_mode, name, version, ...) 151 assert(type(script) == "string" or not script) 152 assert(type(target) == "string") 153 assert(type(deps_mode) == "string") 154 assert(type(name) == "string" or not name) 155 assert(type(version) == "string" or not version) 156 157 local batname = target .. ".bat" 158 local wrapper = io.open(batname, "wb") 159 if not wrapper then 160 return nil, "Could not open "..batname.." for writing." 161 end 162 163 local lpath, lcpath = path.package_paths(deps_mode) 164 165 local luainit = { 166 "package.path="..util.LQ(lpath..";").."..package.path", 167 "package.cpath="..util.LQ(lcpath..";").."..package.cpath", 168 } 169 170 local remove_interpreter = false 171 if target == "luarocks" or target == "luarocks-admin" then 172 if cfg.is_binary then 173 remove_interpreter = true 174 end 175 luainit = { 176 "package.path="..util.LQ(package.path), 177 "package.cpath="..util.LQ(package.cpath), 178 } 179 end 180 181 if name and version then 182 local addctx = "local k,l,_=pcall(require,'luarocks.loader') _=k " .. 183 "and l.add_context('"..name.."','"..version.."')" 184 table.insert(luainit, addctx) 185 end 186 187 local argv = { 188 fs.Qb(dir.path(cfg.variables["LUA_BINDIR"], cfg.lua_interpreter)), 189 "-e", 190 fs.Qb(table.concat(luainit, ";")), 191 script and fs.Qb(script) or "%I%", 192 ... 193 } 194 if remove_interpreter then 195 table.remove(argv, 1) 196 table.remove(argv, 1) 197 table.remove(argv, 1) 198 end 199 200 wrapper:write("@echo off\r\n") 201 wrapper:write("setlocal\r\n") 202 if not script then 203 wrapper:write([[IF "%*"=="" (set I=-i) ELSE (set I=)]] .. "\r\n") 204 end 205 wrapper:write("set "..fs.Qb("LUAROCKS_SYSCONFDIR="..cfg.sysconfdir) .. "\r\n") 206 wrapper:write(table.concat(argv, " ") .. " %*\r\n") 207 wrapper:write("exit /b %ERRORLEVEL%\r\n") 208 wrapper:close() 209 return true 210end 211 212function win32.is_actual_binary(name) 213 name = name:lower() 214 if name:match("%.bat$") or name:match("%.exe$") then 215 return true 216 end 217 return false 218end 219 220function win32.copy_binary(filename, dest) 221 local ok, err = fs.copy(filename, dest) 222 if not ok then 223 return nil, err 224 end 225 local exe_pattern = "%.[Ee][Xx][Ee]$" 226 local base = dir.base_name(filename) 227 dest = dir.dir_name(dest) 228 if base:match(exe_pattern) then 229 base = base:gsub(exe_pattern, ".lua") 230 local helpname = dest.."/"..base 231 local helper = io.open(helpname, "w") 232 if not helper then 233 return nil, "Could not open "..helpname.." for writing." 234 end 235 helper:write('package.path=\"'..package.path:gsub("\\","\\\\")..';\"..package.path\n') 236 helper:write('package.cpath=\"'..package.path:gsub("\\","\\\\")..';\"..package.cpath\n') 237 helper:close() 238 end 239 return true 240end 241 242--- Move a file on top of the other. 243-- The new file ceases to exist under its original name, 244-- and takes over the name of the old file. 245-- On Windows this is done by removing the original file and 246-- renaming the new file to its original name. 247-- @param old_file The name of the original file, 248-- which will be the new name of new_file. 249-- @param new_file The name of the new file, 250-- which will replace old_file. 251-- @return boolean or (nil, string): True if succeeded, or nil and 252-- an error message. 253function win32.replace_file(old_file, new_file) 254 os.remove(old_file) 255 return os.rename(new_file, old_file) 256end 257 258function win32.is_dir(file) 259 file = fs.absolute_name(file) 260 file = dir.normalize(file) 261 local fd, _, code = io.open(file, "r") 262 if code == 13 then -- directories return "Permission denied" 263 fd, _, code = io.open(file .. "\\", "r") 264 if code == 2 then -- directories return 2, files return 22 265 return true 266 end 267 end 268 if fd then 269 fd:close() 270 end 271 return false 272end 273 274function win32.is_file(file) 275 file = fs.absolute_name(file) 276 file = dir.normalize(file) 277 local fd, _, code = io.open(file, "r") 278 if code == 13 then -- if "Permission denied" 279 fd, _, code = io.open(file .. "\\", "r") 280 if code == 2 then -- directories return 2, files return 22 281 return false 282 elseif code == 22 then 283 return true 284 end 285 end 286 if fd then 287 fd:close() 288 return true 289 end 290 return false 291end 292 293--- Test is file/dir is writable. 294-- Warning: testing if a file/dir is writable does not guarantee 295-- that it will remain writable and therefore it is no replacement 296-- for checking the result of subsequent operations. 297-- @param file string: filename to test 298-- @return boolean: true if file exists, false otherwise. 299function win32.is_writable(file) 300 assert(file) 301 file = dir.normalize(file) 302 local result 303 local tmpname = 'tmpluarockstestwritable.deleteme' 304 if fs.is_dir(file) then 305 local file2 = dir.path(file, tmpname) 306 local fh = io.open(file2, 'wb') 307 result = fh ~= nil 308 if fh then fh:close() end 309 if result then 310 -- the above test might give a false positive when writing to 311 -- c:\program files\ because of VirtualStore redirection on Vista and up 312 -- So check whether it's really there 313 result = fs.exists(file2) 314 end 315 os.remove(file2) 316 else 317 local fh = io.open(file, 'r+b') 318 result = fh ~= nil 319 if fh then fh:close() end 320 end 321 return result 322end 323 324--- Create a temporary directory. 325-- @param name_pattern string: name pattern to use for avoiding conflicts 326-- when creating temporary directory. 327-- @return string or (nil, string): name of temporary directory or (nil, error message) on failure. 328function win32.make_temp_dir(name_pattern) 329 assert(type(name_pattern) == "string") 330 name_pattern = dir.normalize(name_pattern) 331 332 local temp_dir = os.getenv("TMP") .. "/luarocks_" .. name_pattern:gsub("/", "_") .. "-" .. tostring(math.floor(math.random() * 10000)) 333 local ok, err = fs.make_dir(temp_dir) 334 if ok then 335 return temp_dir 336 else 337 return nil, err 338 end 339end 340 341function win32.tmpname() 342 local name = os.tmpname() 343 local tmp = os.getenv("TMP") 344 if tmp and name:sub(1, #tmp) ~= tmp then 345 name = (tmp .. "\\" .. name):gsub("\\+", "\\") 346 end 347 return name 348end 349 350function win32.current_user() 351 return os.getenv("USERNAME") 352end 353 354function win32.is_superuser() 355 return false 356end 357 358function win32.export_cmd(var, val) 359 return ("SET %s=%s"):format(var, val) 360end 361 362function win32.system_cache_dir() 363 return dir.path(fs.system_temp_dir(), "cache") 364end 365 366return win32 367