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
|
--- Commonly used utilities and initialisation code
-- @module bliss.utils
local libgen = require "posix.libgen"
local pwd = require "posix.pwd"
local sys_stat = require "posix.sys.stat"
local sys_wait = require "posix.sys.wait"
local unistd = require "posix.unistd"
local stdlib = require "posix.stdlib"
local signal = require "posix.signal"
local colors = {"", "", ""}
local setup, setup_colors, check_execute, get_available, get_pkg_clean, trap_on, trap_off, split, mkdirp, mkcd, rm_rf, log, warn, die, prompt, run_shell, run, run_quiet, capture, shallowcopy, am_not_owner, as_user
--- Setup the environment.
-- @treturn env The environment table containing the parsed KISS_* variables, atexit handler, and temporary directory names.
function setup()
colors = setup_colors()
check_execute()
local env = {
_LVL = 1 + (os.getenv("_KISS_LVL") or 0),
CHK = os.getenv("KISS_CHK")
or get_available("openssl", "sha256sum", "sha256", "shasum", "digest")
or warn("No sha256 utility found"),
CHOICE = tonumber(os.getenv("KISS_CHOICE")) or 1,
COLOR = tonumber(os.getenv("KISS_COLOR")) or 1,
COMPRESS= os.getenv("KISS_COMPRESS") or "gz",
DEBUG = tonumber(os.getenv("KISS_DEBUG")) or 0,
FORCE = tonumber(os.getenv("KISS_FORCE")) or 0,
GET = os.getenv("KISS_GET")
or get_available("aria2c", "axel", "curl", "wget", "wget2")
or warn("No download utility found (aria2c, axel, curl, wget, wget2"),
HOOK = split(os.getenv("KISS_HOOK"), ":"),
KEEPLOG = tonumber(os.getenv("KISS_KEEPLOG")) or 0,
PATH = split(os.getenv("KISS_PATH"), ":"),
PID = os.getenv("KISS_PID") or unistd.getpid(),
PROMPT = tonumber(os.getenv("KISS_PROMPT")) or 1,
ROOT = os.getenv("KISS_ROOT") or "",
SU = os.getenv("KISS_SU") or get_available("ssu", "sudo", "doas", "su"),
TMPDIR = os.getenv("KISS_TMPDIR"),
time = os.date("%Y-%m-%d-%H:%M"),
}
local permitted_compress = {bz2 = true, gz = true, lzma = true, lz = true, xz = true, zst = true}
if not permitted_compress[env.COMPRESS] then
die("KISS_COMPRESS='"..env.COMPRESS.."' is not permitted (bz2, gz, lzma, lz, xz, zst)")
end
-- sys_db depends on ROOT so must be set after env is constructed
env.pkg_db = "var/db/kiss/installed"
env.cho_db = "var/db/kiss/choices"
env.sys_db = env.ROOT .. "/" .. env.pkg_db
env.sys_ch = env.ROOT .. "/" .. env.cho_db
local xdg = os.getenv("XDG_CACHE_HOME")
env.cac_dir = (xdg and #xdg > 0 and xdg or (os.getenv("HOME") .. "/.cache")) .. "/kiss"
env.src_dir = env.cac_dir .. "/sources"
env.log_dir = env.cac_dir .. "/logs/" .. string.sub(env.time, 1, 10)
env.bin_dir = env.cac_dir .. "/bin"
env.TMPDIR = env.TMPDIR or (env.cac_dir .. "/proc")
env.proc = env.TMPDIR .. "/" .. env.PID
env.mak_dir = env.proc .. "/build"
env.pkg_dir = env.proc .. "/pkg"
env.tar_dir = env.proc .. "/extract"
env.tmp_dir = env.proc .. "/tmp"
mkdirp(env.ROOT .. "/")
mkdirp(env.src_dir, env.log_dir, env.bin_dir,
env.mak_dir, env.pkg_dir, env.tar_dir, env.tmp_dir)
local atexit = get_pkg_clean(env)
env.atexit = atexit
-- make sure os.exit always closes the Lua state
local o = os.exit
os.exit = function(code, close) atexit(); o(code, true) end
trap_on(env)
return env
end
function setup_colors()
local t = {}
if os.getenv("KISS_COLOR") ~= "0" then
t[1] = "\x1B[1;33m"
t[2] = "\x1B[1;34m"
t[3] = "\x1B[m"
end
return t
end
function check_execute()
if not os.execute() then die("cannot execute shell commands") end
end
--- Find the path of the first command which exists.
-- @param ... list of commands to try
-- @treturn[1] string the path to the first command in the list which exists
-- @treturn[2] nil if none found
function get_available(...)
local x, res
for i = 1, select("#", ...) do
x = select(i, ...)
res = capture("command -v " .. x)
if res and res[1] then return res[1] end
end
return nil
end
-- makes a closure with cached env so that it can be called without args or globals.
function get_pkg_clean(env)
return function ()
if env.DEBUG ~= 0 then return end
if env._LVL == 1 then
rm_rf(env.proc)
else
rm_rf(env.tar_dir)
end
end
end
--- Turn on the cleanup trap.
-- @tparam env env
-- @treturn table env.atexit
function trap_on(env)
signal.signal(signal.SIGINT, function () os.exit(false) end)
return env.atexit
end
--- Turn off the cleanup trap.
-- @tparam env env
function trap_off(env)
signal.signal(signal.SIGINT, signal.SIG_IGN)
end
--- Split a string.
-- @tparam string s string to split
-- @tparam string sep delimiter
-- @treturn table array of substrings
function split(s, sep)
if not s then return {} end
local c = {}
for a in string.gmatch(s, "[^"..sep.."]+") do
table.insert(c, a)
end
return c
end
--- Make directories recursively.
-- The equivalent of running `mkdir -p ...`.
-- @{die}s if an element of the path already exists and is not a directory, or if making a directory fails.
-- Does not fail for directories which exist.
-- @param ... list of directory names
function mkdirp(...)
for i = 1, select("#", ...) do
local path = select(i, ...)
local sb = sys_stat.stat(path)
if sb then
if sys_stat.S_ISDIR(sb.st_mode) == 0 then
die("'" .. path .. "' already exists and is not a directory")
end
goto continue
end
assert(string.sub(path, 1, 1) == "/")
local t = split(path, "/")
local p = ""
for _, v in ipairs(t) do
p = p .. "/" .. v
local sb = sys_stat.stat(p)
if not sb then
local c, msg = sys_stat.mkdir(p)
if not c then die("mkdir " .. msg) end
end
end
::continue::
end
end
--- Make directories and chdir into the first one.
-- @param ... list of directories
function mkcd(...)
mkdirp(...)
local first = select(1, ...)
unistd.chdir(first)
end
--- Recursively remove files and directories.
-- This simply executes `rm -rf "path"`.
-- @tparam string path path to remove
function rm_rf(path)
return os.execute("rm -rf \"" .. path .. "\"")
end
--- Print a formatted log message.
-- Follows the same convention as kiss.
-- @tparam string name
-- @tparam[opt] string msg
-- @tparam[opt] string category
function log(name, msg, category)
-- This is a direct translation of kiss's log(). Quite hacky.
io.stderr:write(string.format("%s%s %s%s%s %s\n",
colors[1],
category or "->",
colors[3] .. (msg and colors[2] or ""),
name,
colors[3],
msg or ""))
end
--- Print a warning.
function warn(name, msg)
log(name, msg, "WARNING")
end
--- Print an error and exit.
function die(name, msg)
log(name, msg, "ERROR")
os.exit(false)
end
--- Prompt the user to continue or quit.
-- @tparam env env
-- @tparam[opt] string msg
function prompt(env, msg)
if msg then log(msg) end
log("Continue? Press Enter to continue or Ctrl+C to abort")
if env.PROMPT ~= 0 then
local s, err = pcall(io.stdin.read, io.stdin)
if not s then os.exit(false) end
end
end
--- Run a command in the system shell.
-- Also prints the command.
-- @see run
-- @see capture
-- @tparam string cmd
function run_shell(cmd)
io.stderr:write(cmd.."\n")
return os.execute(cmd)
end
--- Run an executable directly.
-- @tparam string path file to run. Doesn't have to be an absolute path.
-- @tparam table cmd array of arguments
-- @tparam[opt] table env table of environment variables for the new process
-- @tparam[opt] string logfile if provided, the output is copied to this file.
function run(path, cmd, env, logfile)
io.stderr:write(path .. " " .. table.concat(cmd, " ", 1) .. "\n")
return run_quiet(path, cmd, env, logfile)
end
--- Run without printing the command.
-- @see run
-- @tparam string path
-- @tparam table cmd
-- @tparam[opt] table env
-- @tparam[opt] string logfile
function run_quiet(path, cmd, env, logfile)
local f,r,w
if logfile then
local err
f,err = io.open(logfile, "w")
if not f then
die("could not create " .. err)
end
r,w = unistd.pipe()
if not r then
die("could not pipe: " .. w)
end
end
local pid = unistd.fork()
if not pid then
die("fork failed")
elseif pid == 0 then
if logfile then
unistd.close(r)
unistd.dup2(w, unistd.STDOUT_FILENO)
unistd.dup2(w, unistd.STDERR_FILENO)
unistd.close(w)
end
if env then
for k,v in pairs(env) do
stdlib.setenv(k, v)
end
end
unistd.execp(path, cmd)
else
if logfile then
unistd.close(w)
while true do
local out = unistd.read(r, 1024)
if not out or #out == 0 then break end
io.write(out)
if logfile then
f:write(out)
end
end
unistd.close(r)
f:close()
end
local _, msg, code = sys_wait.wait(pid)
if msg ~= "exited" then
die("run failed: " .. msg)
end
return code == 0
end
end
--- Run a command in the shell and capture its output.
-- @see run
-- @tparam string cmd command to run
-- @treturn[1] table an array of lines printed by cmd
-- @treturn[2] nil If cmd fails
function capture(cmd)
local p = io.popen(cmd, "r")
local res = {}
for line in p:lines() do
table.insert(res, line)
end
if not p:close() then return nil end
return res
end
--- Make a shallow copy of a table.
-- @tparam table t
-- @treturn table a table with the first-level keys of t copied
function shallowcopy(t)
local u = {}
for k,v in pairs(t) do u[k] = v end
return u
end
--- Check if the process is not the owner of a file.
-- @tparam string file
-- @treturn[1] string username of file owner
-- @treturn[2] nil if process owns file
function am_not_owner(file)
local sb = sys_stat.stat(file)
if not sb then die("Failed to stat '"..file.."'") end
if sb.st_uid ~= unistd.getuid() then
return pwd.getpwuid(sb.st_uid).pw_name
end
return nil
end
--- Run a command as a different user.
-- @tparam env env containing the KISS_SU to use
-- @tparam string user user to become
-- @tparam table arg array of command arguments
function as_user(env, user, arg)
print("Using '".. env.SU .. "' (to become "..user..")")
local flags
if libgen.basename(env.SU) == "su" then
-- TODO: stdin problem?
flags = {"-c", '"'..table.concat(arg, '" "')..'"', user}
else
flags = {"-u", user, "--", table.unpack(arg)}
end
run_quiet(env.SU, flags)
end
--- @export
local M = {
setup = setup,
trap_on = trap_on,
trap_off = trap_off,
split = split,
mkdirp = mkdirp,
mkcd = mkcd,
rm_rf = rm_rf,
log = log,
warn = warn,
die = die,
prompt = prompt,
run_shell = run_shell,
run = run,
run_quiet = run_quiet,
capture = capture,
shallowcopy = shallowcopy,
am_not_owner= am_not_owner,
as_user = as_user,
}
return M
|