summaryrefslogtreecommitdiff
path: root/src/luarocks/fs/tools.lua
blob: 23f2561e8c448ed00475fd438220e93db2b8604b (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

--- Common fs operations implemented with third-party tools.
local tools = {}

local fs = require("luarocks.fs")
local dir = require("luarocks.dir")
local cfg = require("luarocks.core.cfg")

local vars = setmetatable({}, { __index = function(_,k) return cfg.variables[k] end })

local dir_stack = {}

do
   local tool_cache = {}

   local tool_options = {
      downloader = {
         desc = "downloader",
         { var = "WGET", name = "wget" },
         { var = "CURL", name = "curl" },
      },
      md5checker = {
         desc = "MD5 checker",
         { var = "MD5SUM", name = "md5sum" },
         { var = "OPENSSL", name = "openssl", cmdarg = "md5" },
         { var = "MD5", name = "md5" },
      },
   }

   function tools.which_tool(tooltype)
      local tool = tool_cache[tooltype]
      local names = {}
      if not tool then
         for _, opt in ipairs(tool_options[tooltype]) do
            table.insert(names, opt.name)
            if fs.is_tool_available(vars[opt.var], opt.name) then
               tool = opt
               tool_cache[tooltype] = opt
               break
            end
         end
      end
      if not tool then
         local tool_names = table.concat(names, ", ", 1, #names - 1) .. " or " .. names[#names]
         return nil, "no " .. tool_options[tooltype].desc .. " tool available," .. " please install " .. tool_names .. " in your system"
      end
      return tool.name, vars[tool.var] .. (tool.cmdarg and " "..tool.cmdarg or "")
   end
end

local current_dir_with_cache
do
   local cache_pwd

   current_dir_with_cache = function()
      local current = cache_pwd
      if not current then
         local pipe = io.popen(fs.quiet_stderr(vars.PWD))
         current = pipe:read("*a"):gsub("^%s*", ""):gsub("%s*$", "")
         pipe:close()
         cache_pwd = current
      end
      for _, directory in ipairs(dir_stack) do
         current = fs.absolute_name(directory, current)
      end
      return current, cache_pwd
   end

   --- Obtain current directory.
   -- Uses the module's internal directory stack.
   -- @return string: the absolute pathname of the current directory.
   function tools.current_dir()
      return (current_dir_with_cache()) -- drop second return
   end
end

--- Change the current directory.
-- Uses the module's internal directory stack. This does not have exact
-- semantics of chdir, as it does not handle errors the same way,
-- but works well for our purposes for now.
-- @param directory string: The directory to switch to.
-- @return boolean or (nil, string): true if successful, (nil, error message) if failed.
function tools.change_dir(directory)
   assert(type(directory) == "string")
   if fs.is_dir(directory) then
      table.insert(dir_stack, directory)
      return true
   end
   return nil, "directory not found: "..directory
end

--- Change directory to root.
-- Allows leaving a directory (e.g. for deleting it) in
-- a crossplatform way.
function tools.change_dir_to_root()
   local curr_dir = fs.current_dir()
   if not curr_dir or not fs.is_dir(curr_dir) then
      return false
   end
   table.insert(dir_stack, "/")
   return true
end

--- Change working directory to the previous in the directory stack.
function tools.pop_dir()
   local directory = table.remove(dir_stack)
   return directory ~= nil
end

--- Run the given command.
-- The command is executed in the current directory in the directory stack.
-- @param cmd string: No quoting/escaping is applied to the command.
-- @return boolean: true if command succeeds (status code 0), false
-- otherwise.
function tools.execute_string(cmd)
   local current, cache_pwd = current_dir_with_cache()
   if not current then return false end
   if current ~= cache_pwd then
      cmd = fs.command_at(current, cmd)
   end
   local code = os.execute(cmd)
   if code == 0 or code == true then
      return true
   else
      return false
   end
end

--- Internal implementation function for fs.dir.
-- Yields a filename on each iteration.
-- @param at string: directory to list
-- @return nil
function tools.dir_iterator(at)
   local pipe = io.popen(fs.command_at(at, vars.LS, true))
   for file in pipe:lines() do
      if file ~= "." and file ~= ".." then
         coroutine.yield(file)
      end
   end
   pipe:close()
end

--- Download a remote file.
-- @param url string: URL to be fetched.
-- @param filename string or nil: this function attempts to detect the
-- resulting local filename of the remote file as the basename of the URL;
-- if that is not correct (due to a redirection, for example), the local
-- filename can be given explicitly as this second argument.
-- @param cache boolean: compare remote timestamps via HTTP HEAD prior to
-- re-downloading the file.
-- @return (boolean, string, string): true and the filename on success,
-- false and the error message and code on failure.
function tools.use_downloader(url, filename, cache)
   assert(type(url) == "string")
   assert(type(filename) == "string" or not filename)

   filename = fs.absolute_name(filename or dir.base_name(url))

   local downloader, err = fs.which_tool("downloader")
   if not downloader then
      return nil, err, "downloader"
   end

   local ok = false
   if downloader == "wget" then
      local wget_cmd = vars.WGET.." "..vars.WGETNOCERTFLAG.." --no-cache --user-agent=\""..cfg.user_agent.." via wget\" --quiet "
      if cfg.connection_timeout and cfg.connection_timeout > 0 then
        wget_cmd = wget_cmd .. "--timeout="..tostring(cfg.connection_timeout).." --tries=1 "
      end
      if cache then
         -- --timestamping is incompatible with --output-document,
         -- but that's not a problem for our use cases.
         fs.delete(filename .. ".unixtime")
         fs.change_dir(dir.dir_name(filename))
         ok = fs.execute_quiet(wget_cmd.." --timestamping ", url)
         fs.pop_dir()
      elseif filename then
         ok = fs.execute_quiet(wget_cmd.." --output-document ", filename, url)
      else
         ok = fs.execute_quiet(wget_cmd, url)
      end
   elseif downloader == "curl" then
      local curl_cmd = vars.CURL.." "..vars.CURLNOCERTFLAG.." -f -L --user-agent \""..cfg.user_agent.." via curl\" "
      if cfg.connection_timeout and cfg.connection_timeout > 0 then
        curl_cmd = curl_cmd .. "--connect-timeout "..tostring(cfg.connection_timeout).." "
      end
      if cache then
         curl_cmd = curl_cmd .. " -R -z \"" .. filename .. "\" "
      end
      ok = fs.execute_string(fs.quiet_stderr(curl_cmd..fs.Q(url).." --output "..fs.Q(filename)))
   end
   if ok then
      return true, filename
   else
      os.remove(filename)
      return false, "failed downloading " .. url, "network"
   end
end

--- Get the MD5 checksum for a file.
-- @param file string: The file to be computed.
-- @return string: The MD5 checksum or nil + message
function tools.get_md5(file)
   local ok, md5checker = fs.which_tool("md5checker")
   if not ok then
      return false, md5checker
   end

   local pipe = io.popen(md5checker.." "..fs.Q(fs.absolute_name(file)))
   local computed = pipe:read("*l")
   pipe:close()
   if computed then
      computed = computed:match("("..("%x"):rep(32)..")")
   end
   if computed then
      return computed
   else
      return nil, "Failed to compute MD5 hash for file "..tostring(fs.absolute_name(file))
   end
end

return tools