-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinstaller.lua
More file actions
executable file
·590 lines (504 loc) · 17.5 KB
/
installer.lua
File metadata and controls
executable file
·590 lines (504 loc) · 17.5 KB
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
#!/usr/bin/env luajit
-- ensures functionality if you've run this from somewhere else while its in $PATH
package.path = (arg[0]:match("@?(.*/)") or arg[0]:match("@?(.*\\)")) .. "?.lua;" .. package.path
local utility = require "lib.utility"
local argparse = require "lib.argparse"
local is_browser_installed = require "lib.is_browser_installed"
local json = require "lib.dkjson"
local parser = argparse()
parser:argument("package", "Select specific package(s). If specified, --default-choice and --interactive options will be ignored."):args("*")
parser:flag("--mark-installed", "Only marks package(s) installed, rather than actually installing them."):overwrite(false)
parser:flag("--unprivileged", "Only try to install package(s) that don't require sudo / install for the current user only."):overwrite(false)
parser:option("--default-choice", "Default answer to prompts.", "N"):choices{"Y", "N"}:args(1):overwrite(false)
parser:flag("--dry-run", "Output the commands that would be run instead of running them."):overwrite(false)
parser:option("--interactive", "Wait for user input.", "true"):choices{"true", "false"}:overwrite(false)
-- commands are done via flags instead of command option
parser:mutex(
parser:flag("--show-priority", "List all packages ordered by priority."):overwrite(false),
parser:flag("--list-packages", "List all packages (presented as a Markdown task list)."):overwrite(false),
parser:flag("--detect-installed-packages", "Detect binaries in system path that indicate installed packages, and mark them as installed."):overwrite(false)
)
local options = parser:parse()
local original_error = error
function error(message)
original_error("\n\n" .. message .. "\n\n")
end
local logging_file
local function log(...)
if not logging_file then
logging_file = io.open(os.date("%Y-%m-%d %H-%M") .. ".log", "a")
end
logging_file:write(table.concat({...}, "\t"))
logging_file:write("\n")
-- we don't bother to close because we only want it closed on exit, and it will be automatically closed on exit
end
local function printlog(...) log(...) print(...) end
local function check_binary(package, success_func, failure_func)
if package.binary then
if os.execute(utility.commands.which .. package.binary .. utility.commands.silence_output) == 0 then
if success_func then return success_func() end
else
if failure_func then return failure_func() end
end
end
end
local installed_list
if utility.is_file("installed-packages.json") then
utility.open("installed-packages.json", "r", function(file)
installed_list = json.decode(file:read("*all"))
end)
else
installed_list = { packages = {}, }
end
local function save_installed_packages()
if options.dry_run then
print("Dry run: Not saving installed packages.")
return
end
utility.open("installed-packages.json", "w", function(file)
file:write(json.encode(installed_list, { indent = true }))
file:write("\n")
end)
end
-- TODO reorganize into a load_packages() function to call immediately
-- TODO load anything in packages instead of just a pre-assigned list of file names
local packages = {}
for _, name in ipairs({ "system", "games", "media", "utility", "developer", "internet", }) do
local _packages = require("packages." .. name)
for name, package in pairs(_packages) do
packages[name] = package
end
end
local states = utility.enumerate({ "IGNORED", "TO_ASK", "TO_INSTALL", "INSTALLED", "INSTALL_FAILED", })
local function sanitize_packages() -- and check for errors
local detected_hardware = require "lib.detected_hardware"
local function config_error(name, reason)
error("Package '" .. name .. "' " .. reason .. ".\nPlease report this issue at https://github.com/TangentFoxy/os-setup/issues")
end
for name, package in pairs(packages) do
package.name = name
if type(package.prerequisites) == "string" then
package.prerequisites = { package.prerequisites }
end
if type(package.optional_prerequisites) == "string" then
package.optional_prerequisites = { package.optional_prerequisites }
end
if not package.prerequisites then
package.prerequisites = {}
end
if not package.optional_prerequisites then
package.optional_prerequisites = {}
end
for _, pkg in ipairs(package.prerequisites) do
if not packages[pkg] then
printlog("WARNING: " .. name:enquote() .. " lists nonexistant dependency " .. pkg:enquote() .. " and will be ignored.")
package.ignore = true
-- break -- no, so ALL unmet dependencies are warned against
end
end
for _, pkg in ipairs(package.optional_prerequisites) do
if not packages[pkg] then
printlog("WARNING: " .. name:enquote() .. " lists nonexistant optional dependency " .. pkg:enquote() .. ".")
end
end
if package.ask or package.ask == nil then
package.status = states.TO_ASK
end
if options.unprivileged and not package.unprivileged then
package.status = states.IGNORED
end
if package.ignore then
package.status = states.IGNORED
end
if package.hardware then
if not detected_hardware[package.hardware] then
package.status = states.IGNORED
end
end
if package.hardware_exclude then
if detected_hardware[package.hardware_exclude] then
package.status = states.IGNORED
end
end
if package.description then
if package.prompt then config_error(name, "has a prompt and a description (incompatible)") end
package.prompt = "Install " .. package.description
end
if package.condition then
package.conditions = package.condition
end
if not package.priority then
package.priority = 0
end
if type(package.apt) == "string" then
package.apt = { package.apt }
end
if type(package.flatpak) == "string" then
package.flatpak = { package.flatpak }
end
if type(package.brew) == "string" then -- TODO auto-insert brew dependency?
package.brew = { package.brew }
end
if type(package.conditions) ~= "table" then
package.conditions = { package.conditions }
end
if type(package.browse_to) == "string" then
package.browse_to = { package.browse_to, "Debian (.deb)" }
end
if package.desktop then
if type(package.desktop.categories) == "string" then
package.desktop.categories = { package.desktop.categories }
end
end
if package.binary then
if package.binary == true then
package.binary = name
end
end
if not package.conditions then
package.conditions = {}
end
if not package.cronjobs then
package.cronjobs = {}
end
if package.execute and package.execute:sub(1, 1) ~= " " then -- pretty formatting for dry_run
package.execute = " " .. package.execute .. "\n"
end
if not package.prompt then
printlog("WARNING: " .. name:enquote() .. "lacks a prompt or description.\nPlease report this issue at https://github.com/TangentFoxy/os-setup/issues")
end
-- TODO check for other types of error
if #package.cronjobs % 3 ~= 0 then
config_error(name, "has invalid cronjob definition(s)")
end
-- mark already installed packages as INSTALLED (and make sure they are)
if installed_list.packages[name] then
package.status = states.INSTALLED
check_binary(package, nil, function()
printlog("WARNING: Package " .. name:enquote() .. " is marked as installed, but its binary is not in the system path.")
end)
end
end
end
sanitize_packages()
local package_order = {}
for name, package in pairs(packages) do
table.insert(package_order, {
name = name,
priority = package.priority,
})
end
table.sort(package_order, function(a, b) return a.priority > b.priority end)
if options.interactive == "false" then
options.interactive = false
-- don't need to bother converting "true" to true because its truthy
end
if options.show_priority then
for _, package in ipairs(package_order) do
print(package.priority, package.name)
end
return true
end
if options.list_packages then
for _, package in ipairs(package_order) do
local output = "- [ ] " .. package.name
if installed_list.packages[package.name] then
output = output .. " (installed)"
end
if packages[package.name].ignore then
output = output .. " (ignored)"
end
if packages[package.name].ask == false then
output = output .. " (not prompted: dependency only)"
end
print(output)
end
return true
end
if options.detect_installed_packages then
for name, package in pairs(packages) do
check_binary(package, function()
installed_list.packages[name] = true
printlog(name:enquote() .. " marked as installed.")
end, function()
print(name:enquote() .. " is not installed.")
end)
end
save_installed_packages()
end
local function prompt(text, hide_default_entry)
io.write(text)
if options.interactive then
return io.read("*line")
else
if not hide_default_entry then io.write(options.default_choice) end
io.write("\n")
return ""
end
end
local function ask(text, default_choice)
local choice = prompt(text)
if #choice < 1 then choice = default_choice or options.default_choice end
if choice:sub(1):lower() == "y" then
return true
end
end
local select_package
select_package = function(name)
local package = packages[name]
if package.status == states.INSTALLED then
if not ask(name:enquote() .. " is already installed. Do you want to reinstall it (y/n, default: n)?", "N") then
return -- skip this and its dependencies :D
end
end
if options.unprivileged and not package.unprivileged then
if not ask(name:enquote() .. " cannot be installed unprivileged. Do you want to try to install it anyhow (y/n, default: n)?", "N") then
return
end
log("Attempting to install " .. name:enquote() .. " while --unprivileged flag is set.")
end
package.status = states.TO_INSTALL
for _, name in ipairs(package.prerequisites) do
select_package(name)
end
end
local function system_upgrade()
printlog("Making sure system is up-to-date...")
if options.dry_run then
print(packages["system-upgrade"].execute)
else
os.execute(packages["system-upgrade"].execute)
end
end
local function ask_packages()
for _, package in ipairs(package_order) do
local name = package.name
package = packages[name]
local function _ask(name, package)
if not (package.status == states.TO_ASK) then
return
end
for _, name in ipairs(package.prerequisites) do
if packages[name].status == states.IGNORED then
return
end
end
if package.notes then
print("\n" .. package.notes)
end
if ask(package.prompt .. " (y/n, default: " .. options.default_choice .. ")? ") then
select_package(name)
end
end
_ask(name, package)
end
io.write("\n") -- formatting
end
-- choose what to run
if #options.package > 0 then
for _, name in ipairs(options.package) do
select_package(name)
end
else
ask_packages()
end
local function execute(...)
if options.dry_run then
print(...)
return 0 -- always say it worked
else
return os.execute(...)
end
end
local function make_accumulator()
local exit_code = 0
return function(action)
local action_type = type(action)
if action_type == "string" then
exit_code = exit_code + execute(action)
elseif action_type == "number" then
exit_code = exit_code + action
end
return exit_code
end
end
local function create_menu_entry(desktop)
local accumulator = make_accumulator()
local desktop_file_name = desktop.path .. "/" .. desktop.name .. ".desktop"
local lines = {
"[Desktop Entry]",
"Name=" .. desktop.name,
"Path=" .. desktop.path,
"Exec=" .. desktop.exec,
"Icon=" .. desktop.icon,
"Terminal=false",
"Type=Application",
"Categories=" .. table.concat(desktop.categories, "\\;"),
}
for _, line in ipairs(lines) do
accumulator(" echo " .. line .. " >> " .. desktop_file_name)
end
accumulator(" chmod +x " .. desktop_file_name)
accumulator(" desktop-file-validate " .. desktop_file_name)
accumulator(" desktop-file-install --dir=$HOME/.local/share/applications " .. desktop_file_name)
accumulator(" update-desktop-database ~/.local/share/applications") -- appears to be unnecessary on Mint
if options.dry_run then
io.write("\n")
end
return accumulator()
end
local function create_cronjob(schedule, script, sudo)
local script_name = script:split(" ")[1]
local lines = {
" uuid=tangent-os-setup", -- no longer $(uuidgen) because creating duplicate entries is baaad
" sudo cp ./scripts/" .. script_name .. " /opt/$uuid-" .. script_name,
" croncmd=\"/opt/$uuid-" .. script .. "\"",
" cronjob=\"" .. schedule .. " $croncmd\"",
}
if sudo then
lines[#lines + 1] = " (sudo crontab -l | grep -v -F \"$croncmd\" || : ; echo \"$cronjob\" ) | sudo crontab -"
else
lines[#lines + 1] = " (crontab -l | grep -v -F \"$croncmd\" || : ; echo \"$cronjob\" ) | crontab -"
end
local exit_code = execute(table.concat(lines, "\n"))
if options.dry_run then
io.write("\n")
end
return exit_code
end
system_upgrade()
local done = false
local times_run = 0
repeat
if times_run > #packages + 10 then
printlog("The following packages have failed to install:")
for name, package in pairs(packages) do
if package.status == states.TO_INSTALL then
printlog(" " .. name)
end
end
error("installer.lua (os-setup) script was detected to be looping infinitely.")
end
local function _install(name, package)
if package.status ~= states.TO_INSTALL then
return
end
if options.mark_installed then
package.status = states.INSTALLED
return
end
for _, name in ipairs(package.prerequisites) do
if packages[name].status ~= states.INSTALLED then
return
end
end
for _, name in ipairs(package.optional_prerequisites) do
if packages[name].status ~= states.TO_INSTALL then
return
end
end
for _, condition in ipairs(package.conditions) do
if type(condition) == "function" then
if not condition() then
return
end
elseif type(condition) == "string" then
if not execute(condition) == 0 then
return
end
end
end
if package.browse_to then
if not is_browser_installed() then
return
end
end
-- mark installed and skip installing if a binary is present
check_binary(package, function()
if not ask(name:enquote() .. " appears to already be installed. Do you want to reinstall it (y/n, default: n)?", "N") then
package.status = states.INSTALLED
installed_list.packages[name] = true
log("Marked " .. name:enquote() .. " as installed.")
else
log("Reinstalling " .. name:enquote())
end
end)
if package.status == states.INSTALLED then
return
end
if options.dry_run then
print("Simulating '" .. name .. "'...")
else
log("Installing '" .. name .. "'...")
end
local accumulator = make_accumulator()
if package.browse_to then
local download_url = package.browse_to[1]
local file_description = package.browse_to[2]
print("Opening your browser to a download page.")
print("Make sure you choose the " .. file_description .. " file and that it is saved to ~/Downloads")
accumulator(" open " .. download_url)
prompt("Press enter when the download is finished.", true)
end
if package.ppa then
accumulator(" sudo add-apt-repository -y " .. package.ppa .. " && sudo apt-get update")
end
if package.apt then
accumulator(" sudo apt-get install -y " .. table.concat(package.apt, " "))
end
if package.flatpak then
local flatpak_command = " flatpak install -y "
if package.unprivileged then
flatpak_command = flatpak_command .. "--user "
end
for _, name in ipairs(package.flatpak) do
accumulator(flatpak_command .. name)
end
end
if package.brew then -- TODO make sure brew is actually available
for _, name in ipairs(package.brew) do
accumulator(" eval \"$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)\"\n brew install " .. name)
end
end
if package.execute then
accumulator(package.execute)
elseif options.dry_run then
io.write("\n")
end
if package.desktop then
accumulator(create_menu_entry(package.desktop))
end
for i = 1, #package.cronjobs, 3 do
local schedule = package.cronjobs[i]
local script = package.cronjobs[i + 1]
local sudo = package.cronjobs[i + 2]
accumulator(create_cronjob(schedule, script, sudo))
end
if accumulator() == 0 then
package.status = states.INSTALLED
installed_list.packages[name] = true
check_binary(package, nil, function()
printlog("WARNING: Package " .. name:enquote() .. " appears to have failed to install.")
end)
else
package.status = states.INSTALL_FAILED
printlog("Failed to install " .. name:enquote() .. " (accumulated exit code: " .. accumulator() .. ")")
end
end
for _, package in ipairs(package_order) do
local name = package.name
package = packages[name]
_install(name, package)
end
done = true
for _, package in pairs(packages) do
if package.status == states.TO_INSTALL then
done = false
break
end
end
times_run = times_run + 1
until done
system_upgrade()
save_installed_packages()
printlog("Looped " .. times_run .. " times to run all scripts.")