summaryrefslogtreecommitdiff
path: root/src/luarocks/build.lua
blob: c6b3388bd6db3f8012924c5a89e1d94a8b04165d (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

local build = {}

local path = require("luarocks.path")
local util = require("luarocks.util")
local fun = require("luarocks.fun")
local fetch = require("luarocks.fetch")
local fs = require("luarocks.fs")
local dir = require("luarocks.dir")
local deps = require("luarocks.deps")
local cfg = require("luarocks.core.cfg")
local vers = require("luarocks.core.vers")
local repos = require("luarocks.repos")
local writer = require("luarocks.manif.writer")
local deplocks = require("luarocks.deplocks")

build.opts = util.opts_table("build.opts", {
   need_to_fetch = "boolean",
   minimal_mode = "boolean",
   deps_mode = "string",
   build_only_deps = "boolean",
   namespace = "string?",
   branch = "string?",
   verify = "boolean",
   check_lua_versions = "boolean",
   pin = "boolean",
   rebuild = "boolean",
   no_install = "boolean"
})

do
   --- Write to the current directory the contents of a table,
   -- where each key is a file name and its value is the file content.
   -- @param files table: The table of files to be written.
   local function extract_from_rockspec(files)
      for name, content in pairs(files) do
         local fd = io.open(dir.path(fs.current_dir(), name), "w+")
         fd:write(content)
         fd:close()
      end
   end

   --- Applies patches inlined in the build.patches section
   -- and extracts files inlined in the build.extra_files section
   -- of a rockspec.
   -- @param rockspec table: A rockspec table.
   -- @return boolean or (nil, string): True if succeeded or
   -- nil and an error message.
   function build.apply_patches(rockspec)
      assert(rockspec:type() == "rockspec")

      if not (rockspec.build.extra_files or rockspec.build.patches) then
         return true
      end

      local fd = io.open(fs.absolute_name(".luarocks.patches.applied"), "r")
      if fd then
         fd:close()
         return true
      end

      if rockspec.build.extra_files then
         extract_from_rockspec(rockspec.build.extra_files)
      end
      if rockspec.build.patches then
         extract_from_rockspec(rockspec.build.patches)
         for patch, patchdata in util.sortedpairs(rockspec.build.patches) do
            util.printout("Applying patch "..patch.."...")
            local create_delete = rockspec:format_is_at_least("3.0")
            local ok, err = fs.apply_patch(tostring(patch), patchdata, create_delete)
            if not ok then
               return nil, "Failed applying patch "..patch
            end
         end
      end

      fd = io.open(fs.absolute_name(".luarocks.patches.applied"), "w")
      if fd then
         fd:close()
      end
      return true
   end
end

local function check_macosx_deployment_target(rockspec)
   local target = rockspec.build.macosx_deployment_target
   local function patch_variable(var)
      if rockspec.variables[var]:match("MACOSX_DEPLOYMENT_TARGET") then
         rockspec.variables[var] = (rockspec.variables[var]):gsub("MACOSX_DEPLOYMENT_TARGET=[^ ]*", "MACOSX_DEPLOYMENT_TARGET="..target)
      else
         rockspec.variables[var] = "env MACOSX_DEPLOYMENT_TARGET="..target.." "..rockspec.variables[var]
      end
   end
   if cfg.is_platform("macosx") and rockspec:format_is_at_least("3.0") and target then
      local version = util.popen_read("sw_vers -productVersion")
      if version:match("^%d+%.%d+%.%d+$") or version:match("^%d+%.%d+$") then
         if vers.compare_versions(target, version) then
            return nil, ("This rock requires Mac OSX %s, and you are running %s."):format(target, version)
         end
      end
      patch_variable("CC")
      patch_variable("LD")
   end
   return true
end

local function process_dependencies(rockspec, opts)
   if not opts.build_only_deps then
      local ok, err, errcode = deps.check_external_deps(rockspec, "build")
      if err then
         return nil, err, errcode
      end
   end

   if opts.deps_mode == "none" then
      return true
   end

   if not opts.build_only_deps then
      if next(rockspec.build_dependencies) then

         local user_lua_version = cfg.lua_version
         local running_lua_version = _VERSION:sub(5)

         if running_lua_version ~= user_lua_version then
            -- Temporarily flip the user-selected Lua version,
            -- so that we install build dependencies for the
            -- Lua version on which the LuaRocks program is running.

            -- HACK: we have to do this by flipping a bunch of
            -- global config settings, and this list may not be complete.
            cfg.lua_version = running_lua_version
            cfg.lua_modules_path = cfg.lua_modules_path:gsub(user_lua_version:gsub("%.", "%%."), running_lua_version)
            cfg.lib_modules_path = cfg.lib_modules_path:gsub(user_lua_version:gsub("%.", "%%."), running_lua_version)
            cfg.rocks_subdir = cfg.rocks_subdir:gsub(user_lua_version:gsub("%.", "%%."), running_lua_version)
            path.use_tree(cfg.root_dir)
         end

         local ok, err, errcode = deps.fulfill_dependencies(rockspec, "build_dependencies", "all", opts.verify)

         path.add_to_package_paths(cfg.root_dir)

         if running_lua_version ~= user_lua_version then
            -- flip the settings back
            cfg.lua_version = user_lua_version
            cfg.lua_modules_path = cfg.lua_modules_path:gsub(running_lua_version:gsub("%.", "%%."), user_lua_version)
            cfg.lib_modules_path = cfg.lib_modules_path:gsub(running_lua_version:gsub("%.", "%%."), user_lua_version)
            cfg.rocks_subdir = cfg.rocks_subdir:gsub(running_lua_version:gsub("%.", "%%."), user_lua_version)
            path.use_tree(cfg.root_dir)
         end

         if err then
            return nil, err, errcode
         end
      end
   end

   return deps.fulfill_dependencies(rockspec, "dependencies", opts.deps_mode, opts.verify)
end

local function fetch_and_change_to_source_dir(rockspec, opts)
   if opts.minimal_mode or opts.build_only_deps then
      return true
   end
   if opts.need_to_fetch then
      if opts.branch then
         rockspec.source.branch = opts.branch
      end
      local ok, source_dir, errcode = fetch.fetch_sources(rockspec, true)
      if not ok then
         return nil, source_dir, errcode
      end
      local err
      ok, err = fs.change_dir(source_dir)
      if not ok then
         return nil, err
      end
   else
      if rockspec.source.file then
         local ok, err = fs.unpack_archive(rockspec.source.file)
         if not ok then
            return nil, err
         end
      end
      local ok, err = fetch.find_rockspec_source_dir(rockspec, ".")
      if not ok then
         return nil, err
      end
   end
   fs.change_dir(rockspec.source.dir)
   return true
end

local function prepare_install_dirs(name, version)
   local dirs = {
      lua = { name = path.lua_dir(name, version), is_module_path = true, perms = "read" },
      lib = { name = path.lib_dir(name, version), is_module_path = true, perms = "exec" },
      bin = { name = path.bin_dir(name, version), is_module_path = false, perms = "exec" },
      conf = { name = path.conf_dir(name, version), is_module_path = false, perms = "read" },
   }

   for _, d in pairs(dirs) do
      local ok, err = fs.make_dir(d.name)
      if not ok then
         return nil, err
      end
   end

   return dirs
end

local function run_build_driver(rockspec, no_install)
   local btype = rockspec.build.type
   if btype == "none" then
      return true
   end
   -- Temporary compatibility
   if btype == "module" then
      util.printout("Do not use 'module' as a build type. Use 'builtin' instead.")
      btype = "builtin"
      rockspec.build.type = btype
   end
   if cfg.accepted_build_types and not fun.contains(cfg.accepted_build_types, btype) then
      return nil, "This rockspec uses the '"..btype.."' build type, which is blocked by the 'accepted_build_types' setting in your LuaRocks configuration."
   end
   local pok, driver = pcall(require, "luarocks.build." .. btype)
   if not pok or type(driver) ~= "table" then
      return nil, "Failed initializing build back-end for build type '"..btype.."': "..driver
   end

   if not driver.skip_lua_inc_lib_check then
      local ok, err, errcode = deps.check_lua_incdir(rockspec.variables)
      if not ok then
         return nil, err, errcode
      end

      if cfg.link_lua_explicitly then
         local ok, err, errcode = deps.check_lua_libdir(rockspec.variables)
         if not ok then
            return nil, err, errcode
         end
      end
   end

   local ok, err = driver.run(rockspec, no_install)
   if not ok then
      return nil, "Build error: " .. err
   end
   return true
end

local install_files
do
   --- Install files to a given location.
   -- Takes a table where the array part is a list of filenames to be copied.
   -- In the hash part, other keys, if is_module_path is set, are identifiers
   -- in Lua module format, to indicate which subdirectory the file should be
   -- copied to. For example, install_files({["foo.bar"] = "src/bar.lua"}, "boo")
   -- will copy src/bar.lua to boo/foo.
   -- @param files table or nil: A table containing a list of files to copy in
   -- the format described above. If nil is passed, this function is a no-op.
   -- Directories should be delimited by forward slashes as in internet URLs.
   -- @param location string: The base directory files should be copied to.
   -- @param is_module_path boolean: True if string keys in files should be
   -- interpreted as dotted module paths.
   -- @param perms string ("read" or "exec"): Permissions of the newly created
   -- files installed.
   -- Directories are always created with the default permissions.
   -- @return boolean or (nil, string): True if succeeded or
   -- nil and an error message.
   local function install_to(files, location, is_module_path, perms)
      assert(type(files) == "table" or not files)
      assert(type(location) == "string")
      if not files then
         return true
      end
      for k, file in pairs(files) do
         local dest = location
         local filename = dir.base_name(file)
         if type(k) == "string" then
            local modname = k
            if is_module_path then
               dest = dir.path(location, path.module_to_path(modname))
               local ok, err = fs.make_dir(dest)
               if not ok then return nil, err end
               if filename:match("%.lua$") then
                  local basename = modname:match("([^.]+)$")
                  filename = basename..".lua"
               end
            else
               dest = dir.path(location, dir.dir_name(modname))
               local ok, err = fs.make_dir(dest)
               if not ok then return nil, err end
               filename = dir.base_name(modname)
            end
         else
            local ok, err = fs.make_dir(dest)
            if not ok then return nil, err end
         end
         local ok = fs.copy(file, dir.path(dest, filename), perms)
         if not ok then
            return nil, "Failed copying "..file
         end
      end
      return true
   end

   local function install_default_docs(name, version)
      local patterns = { "readme", "license", "copying", ".*%.md" }
      local dest = dir.path(path.install_dir(name, version), "doc")
      local has_dir = false
      for file in fs.dir() do
         for _, pattern in ipairs(patterns) do
            if file:lower():match("^"..pattern) then
               if not has_dir then
                  fs.make_dir(dest)
                  has_dir = true
               end
               fs.copy(file, dest, "read")
               break
            end
         end
      end
   end

   install_files = function(rockspec, dirs)
      local name, version = rockspec.name, rockspec.version

      if rockspec.build.install then
         for k, d in pairs(dirs) do
            local ok, err = install_to(rockspec.build.install[k], d.name, d.is_module_path, d.perms)
            if not ok then return nil, err end
         end
      end

      local copy_directories = rockspec.build.copy_directories
      local copying_default = false
      if not copy_directories then
         copy_directories = {"doc"}
         copying_default = true
      end

      local any_docs = false
      for _, copy_dir in pairs(copy_directories) do
         if fs.is_dir(copy_dir) then
            local dest = dir.path(path.install_dir(name, version), copy_dir)
            fs.make_dir(dest)
            fs.copy_contents(copy_dir, dest)
            any_docs = true
         else
            if not copying_default then
               return nil, "Directory '"..copy_dir.."' not found"
            end
         end
      end
      if not any_docs then
         install_default_docs(name, version)
      end

      return true
   end
end

local function write_rock_dir_files(rockspec, opts)
   local name, version = rockspec.name, rockspec.version

   fs.copy(rockspec.local_abs_filename, path.rockspec_file(name, version), "read")

   local deplock_file = deplocks.get_abs_filename(rockspec.name)
   if deplock_file then
      fs.copy(deplock_file, dir.path(path.install_dir(name, version), "luarocks.lock"), "read")
   end

   local ok, err = writer.make_rock_manifest(name, version)
   if not ok then return nil, err end

   ok, err = writer.make_namespace_file(name, version, opts.namespace)
   if not ok then return nil, err end

   return true
end

--- Build and install a rock given a rockspec.
-- @param opts table: build options table
-- @return (string, string) or (nil, string, [string]): Name and version of
-- installed rock if succeeded or nil and an error message followed by an error code.
function build.build_rockspec(rockspec, opts)
   assert(rockspec:type() == "rockspec")
   assert(opts:type() == "build.opts")

   if not rockspec.build then
      if rockspec:format_is_at_least("3.0") then
         rockspec.build = {
            type = "builtin"
         }
      else
         return nil, "Rockspec error: build table not specified"
      end
   end

   if not rockspec.build.type then
      if rockspec:format_is_at_least("3.0") then
         rockspec.build.type = "builtin"
      else
         return nil, "Rockspec error: build type not specified"
      end
   end

   local ok, err = fetch_and_change_to_source_dir(rockspec, opts)
   if not ok then return nil, err end

   if opts.pin then
      deplocks.init(rockspec.name, ".")
   end

   ok, err = process_dependencies(rockspec, opts)
   if not ok then return nil, err end

   local name, version = rockspec.name, rockspec.version
   if opts.build_only_deps then
      if opts.pin then
         deplocks.write_file()
      end
      return name, version
   end

   local dirs, err
   local rollback
   if not opts.no_install then
      if repos.is_installed(name, version) then
         repos.delete_version(name, version, opts.deps_mode)
      end

      dirs, err = prepare_install_dirs(name, version)
      if not dirs then return nil, err end

      rollback = util.schedule_function(function()
         fs.delete(path.install_dir(name, version))
         fs.remove_dir_if_empty(path.versions_dir(name))
      end)
   end

   ok, err = build.apply_patches(rockspec)
   if not ok then return nil, err end

   ok, err = check_macosx_deployment_target(rockspec)
   if not ok then return nil, err end

   ok, err = run_build_driver(rockspec, opts.no_install)
   if not ok then return nil, err end

   if opts.no_install then
      fs.pop_dir()
      if opts.need_to_fetch then
         fs.pop_dir()
      end
      return name, version
   end

   ok, err = install_files(rockspec, dirs)
   if not ok then return nil, err end

   for _, d in pairs(dirs) do
      fs.remove_dir_if_empty(d.name)
   end

   fs.pop_dir()
   if opts.need_to_fetch then
      fs.pop_dir()
   end

   if opts.pin then
      deplocks.write_file()
   end

   ok, err = write_rock_dir_files(rockspec, opts)
   if not ok then return nil, err end

   ok, err = repos.deploy_files(name, version, repos.should_wrap_bin_scripts(rockspec), opts.deps_mode)
   if not ok then return nil, err end

   util.remove_scheduled_function(rollback)
   rollback = util.schedule_function(function()
      repos.delete_version(name, version, opts.deps_mode)
   end)

   ok, err = repos.run_hook(rockspec, "post_install")
   if not ok then return nil, err end

   util.announce_install(rockspec)
   util.remove_scheduled_function(rollback)
   return name, version
end

return build