summaryrefslogtreecommitdiff
path: root/src/luarocks/util.lua
blob: de9157fc0190494b3c4b46dc3a2e482d3076d6c0 (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
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634

--- Assorted utilities for managing tables, plus a scheduler for rollback functions.
-- Does not requires modules directly (only as locals
-- inside specific functions) to avoid interdependencies,
-- as this is used in the bootstrapping stage of luarocks.core.cfg.

local util = {}

local core = require("luarocks.core.util")

util.cleanup_path = core.cleanup_path
util.split_string = core.split_string
util.sortedpairs = core.sortedpairs
util.deep_merge = core.deep_merge
util.deep_merge_under = core.deep_merge_under
util.popen_read = core.popen_read
util.show_table = core.show_table
util.printerr = core.printerr
util.warning = core.warning
util.keys = core.keys

local unpack = unpack or table.unpack
local pack = table.pack or function(...) return { n = select("#", ...), ... } end

local scheduled_functions = {}

--- Schedule a function to be executed upon program termination.
-- This is useful for actions such as deleting temporary directories
-- or failure rollbacks.
-- @param f function: Function to be executed.
-- @param ... arguments to be passed to function.
-- @return table: A token representing the scheduled execution,
-- which can be used to remove the item later from the list.
function util.schedule_function(f, ...)
   assert(type(f) == "function")

   local item = { fn = f, args = pack(...) }
   table.insert(scheduled_functions, item)
   return item
end

--- Unschedule a function.
-- This is useful for cancelling a rollback of a completed operation.
-- @param item table: The token representing the scheduled function that was
-- returned from the schedule_function call.
function util.remove_scheduled_function(item)
   for k, v in pairs(scheduled_functions) do
      if v == item then
         table.remove(scheduled_functions, k)
         return
      end
   end
end

--- Execute scheduled functions.
-- Some calls create temporary files and/or directories and register
-- corresponding cleanup functions. Calling this function will run
-- these function, erasing temporaries.
-- Functions are executed in the inverse order they were scheduled.
function util.run_scheduled_functions()
   local fs = require("luarocks.fs")
   if fs.change_dir_to_root then
      fs.change_dir_to_root()
   end
   for i = #scheduled_functions, 1, -1 do
      local item = scheduled_functions[i]
      item.fn(unpack(item.args, 1, item.args.n))
   end
end

--- Produce a Lua pattern that matches precisely the given string
-- (this is suitable to be concatenating to other patterns,
-- so it does not include beginning- and end-of-string markers (^$)
-- @param s string: The input string
-- @return string: The equivalent pattern
function util.matchquote(s)
   return (s:gsub("[?%-+*%[%].%%()$^]","%%%1"))
end

local var_format_pattern = "%$%((%a[%a%d_]+)%)"

-- Check if a set of needed variables are referenced
-- somewhere in a list of definitions, warning the user
-- about any unused ones. Each key in needed_set should
-- appear as a $(XYZ) variable at least once as a
-- substring of some value of var_defs.
-- @param var_defs: a table with string keys and string
-- values, containing variable definitions.
-- @param needed_set: a set where keys are the names of
-- needed variables.
-- @param msg string: the warning message to display.
function util.warn_if_not_used(var_defs, needed_set, msg)
   local seen = {}
   for _, val in pairs(var_defs) do
      for used in val:gmatch(var_format_pattern) do
         seen[used] = true
      end
   end
   for var, _ in pairs(needed_set) do
      if not seen[var] then
         util.warning(msg:format(var))
      end
   end
end

-- Output any entries that might remain in $(XYZ) format,
-- warning the user that substitutions have failed.
-- @param line string: the input string
local function warn_failed_matches(line)
   local any_failed = false
   if line:match(var_format_pattern) then
      for unmatched in line:gmatch(var_format_pattern) do
         util.warning("unmatched variable " .. unmatched)
         any_failed = true
      end
   end
   return any_failed
end

--- Perform make-style variable substitutions on string values of a table.
-- For every string value tbl.x which contains a substring of the format
-- "$(XYZ)" will have this substring replaced by vars["XYZ"], if that field
-- exists in vars. Only string values are processed; this function
-- does not scan subtables recursively.
-- @param tbl table: Table to have its string values modified.
-- @param vars table: Table containing string-string key-value pairs
-- representing variables to replace in the strings values of tbl.
function util.variable_substitutions(tbl, vars)
   assert(type(tbl) == "table")
   assert(type(vars) == "table")

   local updated = {}
   for k, v in pairs(tbl) do
      if type(v) == "string" then
         updated[k] = v:gsub(var_format_pattern, vars)
         if warn_failed_matches(updated[k]) then
            updated[k] = updated[k]:gsub(var_format_pattern, "")
         end
      end
   end
   for k, v in pairs(updated) do
      tbl[k] = v
   end
end

function util.lua_versions(sort)
   local versions = { "5.1", "5.2", "5.3", "5.4" }
   local i = 0
   if sort == "descending" then
      i = #versions + 1
      return function()
         i = i - 1
         return versions[i]
      end
   else
      return function()
         i = i + 1
         return versions[i]
      end
   end
end

function util.lua_path_variables()
   local cfg = require("luarocks.core.cfg")
   local lpath_var = "LUA_PATH"
   local lcpath_var = "LUA_CPATH"

   local lv = cfg.lua_version:gsub("%.", "_")
   if lv ~= "5_1" then
      if os.getenv("LUA_PATH_" .. lv) then
         lpath_var = "LUA_PATH_" .. lv
      end
      if os.getenv("LUA_CPATH_" .. lv) then
         lcpath_var = "LUA_CPATH_" .. lv
      end
   end
   return lpath_var, lcpath_var
end

function util.starts_with(s, prefix)
   return s:sub(1,#prefix) == prefix
end

--- Print a line to standard output
function util.printout(...)
   io.stdout:write(table.concat({...},"\t"))
   io.stdout:write("\n")
end

function util.title(msg, porcelain, underline)
   if porcelain then return end
   util.printout()
   util.printout(msg)
   util.printout((underline or "-"):rep(#msg))
   util.printout()
end

function util.this_program(default)
   local i = 1
   local last, cur = default, default
   while i do
      local dbg = debug and debug.getinfo(i,"S")
      if not dbg then break end
      last = cur
      cur = dbg.source
      i=i+1
   end
   local prog = last:sub(1,1) == "@" and last:sub(2) or last

   -- Check if we found the true path of a script that has a wrapper
   local lrdir, binpath = prog:match("^(.*)/lib/luarocks/rocks%-[0-9.]*/[^/]+/[^/]+(/bin/[^/]+)$")
   if lrdir then
      -- Return the wrapper instead
      return lrdir .. binpath
   end

   return prog
end

function util.format_rock_name(name, namespace, version)
   return (namespace and namespace.."/" or "")..name..(version and " "..version or "")
end

function util.deps_mode_option(parser, program)
   local cfg = require("luarocks.core.cfg")

   parser:option("--deps-mode", "How to handle dependencies. Four modes are supported:\n"..
      "* all - use all trees from the rocks_trees list for finding dependencies\n"..
      "* one - use only the current tree (possibly set with --tree)\n"..
      "* order - use trees based on order (use the current tree and all "..
      "trees below it on the rocks_trees list)\n"..
      "* none - ignore dependencies altogether.\n"..
      "The default mode may be set with the deps_mode entry in the configuration file.\n"..
      'The current default is "'..cfg.deps_mode..'".\n'..
      "Type '"..util.this_program(program or "luarocks").."' with no "..
      "arguments to see your list of rocks trees.")
      :argname("<mode>")
      :choices({"all", "one", "order", "none"})
   parser:flag("--nodeps"):hidden(true)
end

function util.see_help(command, program)
   return "See '"..util.this_program(program or "luarocks")..' help'..(command and " "..command or "").."'."
end

function util.see_also(text)
   local see_also = "See also:\n"
   if text then
      see_also = see_also..text.."\n"
   end
   return see_also.."   '"..util.this_program("luarocks").." help' for general options and configuration."
end

function util.announce_install(rockspec)
   local cfg = require("luarocks.core.cfg")
   local path = require("luarocks.path")

   local suffix = ""
   if rockspec.description and rockspec.description.license then
      suffix = " (license: "..rockspec.description.license..")"
   end

   util.printout(rockspec.name.." "..rockspec.version.." is now installed in "..path.root_dir(cfg.root_dir)..suffix)
   util.printout()
end

--- Collect rockspecs located in a subdirectory.
-- @param versions table: A table mapping rock names to newest rockspec versions.
-- @param paths table: A table mapping rock names to newest rockspec paths.
-- @param unnamed_paths table: An array of rockspec paths that don't contain rock
-- name and version in regular format.
-- @param subdir string: path to subdirectory.
local function collect_rockspecs(versions, paths, unnamed_paths, subdir)
   local fs = require("luarocks.fs")
   local dir = require("luarocks.dir")
   local path = require("luarocks.path")
   local vers = require("luarocks.core.vers")

   if fs.is_dir(subdir) then
      for file in fs.dir(subdir) do
         file = dir.path(subdir, file)

         if file:match("rockspec$") and fs.is_file(file) then
            local rock, version = path.parse_name(file)

            if rock then
               if not versions[rock] or vers.compare_versions(version, versions[rock]) then
                  versions[rock] = version
                  paths[rock] = file
               end
            else
               table.insert(unnamed_paths, file)
            end
         end
      end
   end
end

--- Get default rockspec name for commands that take optional rockspec name.
-- @return string or (nil, string): path to the rockspec or nil and error message.
function util.get_default_rockspec()
   local versions, paths, unnamed_paths = {}, {}, {}
   -- Look for rockspecs in some common locations.
   collect_rockspecs(versions, paths, unnamed_paths, ".")
   collect_rockspecs(versions, paths, unnamed_paths, "rockspec")
   collect_rockspecs(versions, paths, unnamed_paths, "rockspecs")

   if #unnamed_paths > 0 then
      -- There are rockspecs not following "name-version.rockspec" format.
      -- More than one are ambiguous.
      if #unnamed_paths > 1 then
         return nil, "Please specify which rockspec file to use."
      else
         return unnamed_paths[1]
      end
   else
      local fs = require("luarocks.fs")
      local dir = require("luarocks.dir")
      local basename = dir.base_name(fs.current_dir())

      if paths[basename] then
         return paths[basename]
      end

      local rock = next(versions)

      if rock then
         -- If there are rockspecs for multiple rocks it's ambiguous.
         if next(versions, rock) then
            return nil, "Please specify which rockspec file to use."
         else
            return paths[rock]
         end
      else
         return nil, "Argument missing: please specify a rockspec to use on current directory."
      end
   end
end

-- Quote Lua string, analogous to fs.Q.
-- @param s A string, such as "hello"
-- @return string: A quoted string, such as '"hello"'
function util.LQ(s)
   return ("%q"):format(s)
end

-- Split name and namespace of a package name.
-- @param ns_name a name that may be in "namespace/name" format
-- @return string, string? - name and optionally a namespace
function util.split_namespace(ns_name)
   local p1, p2 = ns_name:match("^([^/]+)/([^/]+)$")
   if p1 then
      return p2, p1
   end
   return ns_name
end

--- Argparse action callback for namespaced rock arguments.
function util.namespaced_name_action(args, target, ns_name)
   assert(type(args) == "table")
   assert(type(target) == "string")
   assert(type(ns_name) == "string" or not ns_name)

   if not ns_name then
      return
   end

   if ns_name:match("%.rockspec$") or ns_name:match("%.rock$") then
      args[target] = ns_name
   else
      local name, namespace = util.split_namespace(ns_name)
      args[target] = name:lower()
      if namespace then
         args.namespace = namespace:lower()
      end
   end
end

function util.deep_copy(tbl)
   local copy = {}
   for k, v in pairs(tbl) do
      if type(v) == "table" then
         copy[k] = util.deep_copy(v)
      else
         copy[k] = v
      end
   end
   return copy
end

-- A portable version of fs.exists that can be used at early startup,
-- before the platform has been determined and luarocks.fs has been
-- initialized.
function util.exists(file)
   local fd, _, code = io.open(file, "r")
   if code == 13 then
      -- code 13 means "Permission denied" on both Unix and Windows
      -- io.open on folders always fails with code 13 on Windows
      return true
   end
   if fd then
      fd:close()
      return true
   end
   return false
end

do
   local function Q(pathname)
      if pathname:match("^.:") then
         return pathname:sub(1, 2) .. '"' .. pathname:sub(3) .. '"'
      end
      return '"' .. pathname .. '"'
   end

   function util.check_lua_version(lua, luaver)
      if not util.exists(lua) then
         return nil
      end
      local lv, err = util.popen_read(Q(lua) .. ' -e "io.write(_VERSION:sub(5))"')
      if lv == "" then
         return nil
      end
      if luaver and luaver ~= lv then
         return nil
      end
      return lv
   end

   function util.get_luajit_version()
      local cfg = require("luarocks.core.cfg")
      if cfg.cache.luajit_version_checked then
         return cfg.cache.luajit_version
      end
      cfg.cache.luajit_version_checked = true

      if not cfg.variables.LUA then
         return nil
      end

      local ljv
      if cfg.lua_version == "5.1" then
         -- Ignores extra version info for custom builds, e.g. "LuaJIT 2.1.0-beta3 some-other-version-info"
         ljv = util.popen_read(Q(cfg.variables.LUA) .. ' -e "io.write(tostring(jit and jit.version:gsub([[^%S+ (%S+).*]], [[%1]])))"')
         if ljv == "nil" then
            ljv = nil
         end
      end
      cfg.cache.luajit_version = ljv
      return ljv
   end

   local find_lua_bindir
   do
      local exe_suffix = (package.config:sub(1, 1) == "\\" and ".exe" or "")

      local function insert_lua_variants(names, luaver)
         local variants = {
            "lua" .. luaver .. exe_suffix,
            "lua" .. luaver:gsub("%.", "") .. exe_suffix,
            "lua-" .. luaver .. exe_suffix,
            "lua-" .. luaver:gsub("%.", "") .. exe_suffix,
         }
         for _, name in ipairs(variants) do
            names[name] = luaver
            table.insert(names, name)
         end
      end

      find_lua_bindir = function(prefix, luaver, verbose)
         local names = {}
         if luaver then
            insert_lua_variants(names, luaver)
         else
            for v in util.lua_versions("descending") do
               insert_lua_variants(names, v)
            end
         end
         if luaver == "5.1" or not luaver then
            table.insert(names, "luajit" .. exe_suffix)
         end
         table.insert(names, "lua" .. exe_suffix)

         local tried = {}
         local dir_sep = package.config:sub(1, 1)
         for _, d in ipairs({ prefix .. dir_sep .. "bin", prefix }) do
            for _, name in ipairs(names) do
               local lua = d .. dir_sep .. name
               local is_wrapper, err = util.lua_is_wrapper(lua)
               if is_wrapper == false then
                  local lv = util.check_lua_version(lua, luaver)
                  if lv then
                     return lua, d, lv
                  end
               elseif is_wrapper == true or err == nil then
                  table.insert(tried, lua)
               else
                  table.insert(tried, string.format("%-13s (%s)", lua, err))
               end
            end
         end
         local interp = luaver
                        and ("Lua " .. luaver .. " interpreter")
                        or  "Lua interpreter"
         return nil, interp .. " not found at " .. prefix .. "\n" ..
                     (verbose and "Tried:\t" .. table.concat(tried, "\n\t") or "")
      end
   end

   function util.find_lua(prefix, luaver, verbose)
      local lua, bindir
      lua, bindir, luaver = find_lua_bindir(prefix, luaver, verbose)
      if not lua then
         return nil, bindir
      end

      return {
         lua_version = luaver,
         lua = lua,
         lua_dir = prefix,
         lua_bindir = bindir,
      }
   end
end

function util.lua_is_wrapper(interp)
   local fd, err = io.open(interp, "r")
   if not fd then
      return nil, err
   end
   local data, err = fd:read(1000)
   fd:close()
   if not data then
      return nil, err
   end
   return not not data:match("LUAROCKS_SYSCONFDIR")
end

function util.opts_table(type_name, valid_opts)
   local opts_mt = {}

   opts_mt.__index = opts_mt

   function opts_mt.type()
      return type_name
   end

   return function(opts)
      for k, v in pairs(opts) do
         local tv = type(v)
         if not valid_opts[k] then
            error("invalid option: "..k)
         end
         local vo, optional = valid_opts[k]:match("^(.-)(%??)$")
         if not (tv == vo or (optional == "?" and tv == nil)) then
            error("invalid type option: "..k.." - got "..tv..", expected "..vo)
         end
      end
      for k, v in pairs(valid_opts) do
         if (not v:find("?", 1, true)) and opts[k] == nil then
            error("missing option: "..k)
         end
      end
      return setmetatable(opts, opts_mt)
   end
end

--- Return a table of modules that are already provided by the VM, which
-- can be specified as dependencies without having to install an actual rock.
-- @param rockspec (optional) a rockspec table, so that rockspec format
-- version compatibility can be checked. If not given, maximum compatibility
-- is assumed.
-- @return a table with rock names as keys and versions and values,
-- specifying modules that are already provided by the VM (including
-- "lua" for the Lua version and, for format 3.0+, "luajit" if detected).
function util.get_rocks_provided(rockspec)
   local cfg = require("luarocks.core.cfg")

   if not rockspec and cfg.cache.rocks_provided then
      return cfg.cache.rocks_provided
   end

   local rocks_provided = {}

   local lv = cfg.lua_version

   rocks_provided["lua"] = lv.."-1"

   if lv == "5.2" then
      rocks_provided["bit32"] = lv.."-1"
   end

   if lv == "5.3" or lv == "5.4" then
      rocks_provided["utf8"] = lv.."-1"
   end

   if lv == "5.1" then
      local ljv = util.get_luajit_version()
      if ljv then
         rocks_provided["luabitop"] = ljv.."-1"
         if (not rockspec) or rockspec:format_is_at_least("3.0") then
            rocks_provided["luajit"] = ljv.."-1"
         end
      end
   end

   if cfg.rocks_provided then
      util.deep_merge_under(rocks_provided, cfg.rocks_provided)
   end

   if not rockspec then
      cfg.cache.rocks_provided = rocks_provided
   end

   return rocks_provided
end

function util.remove_doc_dir(name, version)
   local path = require("luarocks.path")
   local fs = require("luarocks.fs")
   local dir = require("luarocks.dir")

   local install_dir = path.install_dir(name, version)
   for _, f in ipairs(fs.list_dir(install_dir)) do
      local doc_dirs = { "doc", "docs" }
      for _, d in ipairs(doc_dirs) do
         if f == d then
            fs.delete(dir.path(install_dir, f))
         end
      end
   end
end

return util