Advertisement
wolfe_br

Lunar Installer

Aug 2nd, 2022 (edited)
227
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 12.32 KB | None | 0 0
  1. --[[
  2.   Installer script for CC: Tweaked (and maybe CC?)
  3.   Source: https://github.com/wolfe-labs/CC-LunarInstaller
  4.   Paste: pastebin run Mt7h3gfz install/uninstall [source] [package] [options]
  5.   Local: lunar install/uninstall [source] [package] [options]
  6. ]]
  7.  
  8. -- The installation location
  9. local apps_dir = '/apps'
  10.  
  11. -- The location where we'll drop the bin shortcuts
  12. local bin_dir = '/'
  13.  
  14. -- Extra options
  15. local extra_options = {}
  16.  
  17. ----------------------------------------------
  18. -- Helpers
  19. ----------------------------------------------
  20.  
  21. -- Printing and error with formatting
  22. function printf (format, ...) return print(string.format(format, ...)) end
  23. function errorf (format, ...) return error(string.format(format, ...)) end
  24.  
  25. -- Better array length
  26. function count (table)
  27.   local num = 0
  28.   for _ in pairs(table) do
  29.     num = num + 1
  30.   end
  31.   return num
  32. end
  33.  
  34. -- Split strings
  35. function str_split (str, sep)
  36.   local pieces = {}
  37.   for piece in str:gmatch('([^' .. sep .. ']+)') do
  38.     table.insert(pieces, piece)
  39.   end
  40.   return pieces
  41. end
  42.  
  43. -- Table merge
  44. local function table_merge (t1, t2, deep)
  45.   -- Creates new table output
  46.   local _ = {}
  47.   for k, v in pairs(t1 or {}) do
  48.     _[k] = v
  49.   end
  50.  
  51.   -- Prevents bugs
  52.   if not (type(t1) == 'table') then
  53.     return t2
  54.   end
  55.  
  56.   -- Merges with t2
  57.   for k, v in pairs(t2 or {}) do
  58.     if deep then
  59.       _[k] = table_merge(_[k], t2[k])
  60.     else
  61.       _[k] = t2[k]
  62.     end
  63.   end
  64.  
  65.   -- Done
  66.   return _
  67. end
  68.  
  69. -- Simple file reader
  70. function file_read (file)
  71.   local handle = fs.open(file, 'r')
  72.   local data = handle.readAll()
  73.   handle.close()
  74.   return data
  75. end
  76.  
  77. -- Simple file writer
  78. function file_write (file, data)
  79.   local handle = fs.open(file, 'wb')
  80.   if 'table' == type(data) then
  81.     for _, byte in ipairs(data) do
  82.       handle.write(byte)
  83.     end
  84.   else
  85.     handle.write(data)
  86.   end
  87.   handle.close()
  88. end
  89.  
  90. -- Path handling
  91. function joinpath (p1, p2)
  92.   local result = (p1 .. '/' .. p2):gsub('//+', '/')
  93.   return result -- This is necessary otherwise we'll get extra stuff from the above regex
  94. end
  95.  
  96. -- Extracts directory path from file path
  97. function dirname (path)
  98.   local pieces = str_split(path, '/')
  99.  
  100.   -- Removes last element
  101.   table.remove(pieces, #pieces)
  102.  
  103.   -- Checks if we need a prefix
  104.   local prefix = ''
  105.   if '/' == path:sub(1, 1) then
  106.     prefix = '/'
  107.   end
  108.  
  109.   -- Returns final path
  110.   return prefix .. table.concat(pieces, '/')
  111. end
  112.  
  113. -- Returns data on a shortcut
  114. function shortcut (bin, obj, install_dir)
  115.   local info = {
  116.     name = bin,
  117.     path = obj,
  118.   }
  119.  
  120.   -- Gets path, description, etc
  121.   if 'table' == type(obj) then
  122.     info.path = obj.path
  123.     info.text = obj.text
  124.   end
  125.  
  126.   -- Gets the shortcut file
  127.   info.bin = joinpath(bin_dir, bin .. '.lua')
  128.  
  129.   -- Gets the destination path
  130.   info.target = joinpath(install_dir, info.path)
  131.   info.target_dir = dirname(info.target)
  132.  
  133.   -- Done
  134.   return info
  135. end
  136.  
  137. -- Flattens table
  138. function flatten (table, prefix)
  139.   local flat = {}
  140.   for k, v in pairs(table) do
  141.     -- Adds prefix if needed
  142.     if prefix then k = prefix .. '.' .. k end
  143.  
  144.     -- Parses tree
  145.     if 'table' == type(v) then
  146.       -- Flattens sub-tree
  147.       v = flatten(v, k)
  148.  
  149.       -- Appends prefixed items
  150.       for _k, _v in pairs(v) do flat[_k] = _v end
  151.     else
  152.       -- Appends single item
  153.       flat[k] = v
  154.     end
  155.   end
  156.   return flat
  157. end
  158.  
  159. -- Inflates table
  160. function inflate (table)
  161.   local result = {}
  162.   for k, v in pairs(table) do
  163.     -- Parent table
  164.     local ref = result
  165.     local key = k
  166.  
  167.     -- Parses key
  168.     local keys = str_split(k, '.')
  169.    
  170.     -- Adds items
  171.     for i = 1, #keys do
  172.       local _ = keys[i]
  173.       if i < #keys then
  174.         ref[_] = ref[_] or {}
  175.         ref = ref[_]
  176.       end
  177.       key = _
  178.     end
  179.  
  180.     -- Updates final value
  181.     ref[key] = v
  182.   end
  183.   return result
  184. end
  185.  
  186. -- Reads package from string
  187. function str2pkg (str)
  188.     -- Parses data
  189.     local pkg, err = textutils.unserializeJSON(str)
  190.  
  191.     -- Handles invalid data
  192.     if not pkg then
  193.       errorf('Error reading package file: %s', err)
  194.     end
  195.  
  196.     -- Valid file, returns metadata
  197.     return pkg
  198. end
  199.  
  200. ----------------------------------------------
  201. -- This is our data source setup
  202. ----------------------------------------------
  203.  
  204. -- List of valid sources for package metadata
  205. local pkg_sources = {
  206.   -- Read package from GitHub repo
  207.   github = function (pkg)
  208.     local req, err = http.get(string.format('https://raw.githubusercontent.com/%s/main/ccpkg.json', pkg))
  209.  
  210.     -- Handles not found
  211.     if not req then
  212.       errorf('Error fetching package "%s" from GitHub: %s', pkg, err)
  213.     end
  214.  
  215.     -- Parses response
  216.     return str2pkg(req.readAll())
  217.   end,
  218.  
  219.   -- Read package from Pastebin
  220.   pastebin = function (pkg)
  221.     local req, err = http.get('https://pastebin.com/raw/' .. pkg)
  222.  
  223.     -- Handles not found
  224.     if not req then
  225.       errorf('Error fetching package "%s" from Pastebin: %s', pkg, err)
  226.     end
  227.  
  228.     -- Parses response
  229.     return str2pkg(req.readAll())
  230.   end,
  231. }
  232.  
  233. local pkg_installers = {
  234.   github = function (package, destination)
  235.     -- Helper to call GitHub
  236.     local function gh (path, ...)
  237.       return http.get(string.format('https://api.github.com/' .. path, ...), {
  238.         Accept = 'application/vnd.github+json',
  239.       })
  240.     end
  241.  
  242.     -- Our repo and branch
  243.     local repo = package.source.repo
  244.     local branch = package.source.branch or 'main'
  245.  
  246.     -- Sanity check
  247.     if not repo then error('Package has no repo set!') end
  248.  
  249.     -- Gets initial GitHub content
  250.     print('Reading GitHub.')
  251.     print('Repository: ' .. repo)
  252.     print('Branch: ' .. branch)
  253.     local req, err = gh('repos/%s/branches/%s', repo, branch)
  254.     if not req then errorf('Failed to fetch from GitHub: %s', err) end
  255.  
  256.     -- Reads repo data
  257.     local repo_data = textutils.unserializeJSON(req.readAll())
  258.  
  259.     -- Loads GitHub file tree
  260.     print('Reading file tree...')
  261.     local req, err = gh('repos/%s/git/trees/%s?recursive=true', repo, repo_data.commit.commit.tree.sha)
  262.     if not req then errorf('Failed to fetch GitHub files: %s', err) end
  263.  
  264.     -- Reads file data
  265.     local files = textutils.unserializeJSON(req.readAll())
  266.  
  267.     -- Downloads each of the files
  268.     local file_num = 0
  269.     local file_max = count(files.tree)
  270.     for _, file in pairs(files.tree) do
  271.       file_num = file_num + 1
  272.       local path = joinpath(destination, file.path)
  273.       if 'tree' == file.type then
  274.         fs.makeDir(path)
  275.       elseif 'blob' == file.type then
  276.         printf('Copy [%d/%d]: %s', file_num, file_max, file.path)
  277.         local req, err = http.get(string.format('https://raw.githubusercontent.com/%s/%s/%s', repo, branch, file.path))
  278.         if not req then errorf('Failed to fetch file: %s', err) end
  279.         file_write(path, req.readAll())
  280.       end
  281.     end
  282.   end,
  283.  
  284.   pastebin = function (package, destination)
  285.     -- Helper to fetch paste
  286.     local function pb (path)
  287.       return http.get('https://pastebin.com/raw/' .. path)
  288.     end
  289.  
  290.     -- Checks if there's pastes
  291.     if not package.source.files then
  292.       error('No paste file IDs found!')
  293.     end
  294.  
  295.     -- Goes through each of the paste IDs
  296.     local files = flatten(package.source.files) -- This is necessary to prevent extensions from breaking, it should not be a deep array anyways
  297.     local file_num = 0
  298.     local file_max = count(files)
  299.     for path, id in pairs(files) do
  300.       file_num = file_num + 1
  301.       printf('Copy [%d/%d]: %s', file_num, file_max, path)
  302.       path = joinpath(destination, path)
  303.       local req, err = pb(id)
  304.       if not req then errorf('Failed to fetch file: %s', err) end
  305.       file_write(path, req.readAll())
  306.     end
  307.   end,
  308. }
  309.  
  310. ----------------------------------------------
  311. -- This is what actually (un)installs things
  312. ----------------------------------------------
  313.  
  314. function uninstall_package (install_dir)
  315.   -- Reads package
  316.   local install_pkg = install_dir .. '.json'
  317.   local package = textutils.unserializeJSON(file_read(install_pkg))
  318.  
  319.   -- Removes old binaries
  320.   for bin, bin_data in pairs(package.bin or {}) do
  321.     bin = shortcut(bin, bin_data, install_dir)
  322.     if fs.exists(bin.bin) then
  323.       printf('Unlinking "%s" -> %s', bin.name, bin.target)
  324.       fs.delete(bin.bin)
  325.     end
  326.   end
  327.  
  328.   -- Removes old directory
  329.   print('Removing files...')
  330.   if fs.exists(install_dir) then
  331.     fs.delete(install_dir)
  332.   end
  333.  
  334.   -- Removes old metadata
  335.   print('Removing package metadata...')
  336.   fs.delete(install_pkg)
  337.  
  338.   -- Done
  339.   print('Uninstall successful!')
  340. end
  341.  
  342. function install_package (package, extra_options)
  343.   -- Merges with extra_options
  344.   package = inflate(table_merge(flatten(package), extra_options))
  345.  
  346.   -- Some validation rules
  347.   if not package.id then error('Package ID missing!') end
  348.   if (not package.source) or (not package.source.type) then error('Package source missing!') end
  349.  
  350.   -- Makes app dir if needed
  351.   if not fs.exists(apps_dir) then
  352.     fs.makeDir(apps_dir)
  353.   elseif not fs.isDir(apps_dir) then
  354.     errorf('Location "%s" already exists, should be a directory.', apps_dir)
  355.   end
  356.  
  357.   -- Gets our installer ready
  358.   local installer = pkg_installers[package.source.type]
  359.   if not installer then
  360.     errorf('Invalid installer: %s', package.source.type)
  361.   end
  362.  
  363.   -- Setup our install locations
  364.   local install_dir = joinpath(apps_dir, package.id)
  365.   local install_pkg = install_dir .. '.json'
  366.  
  367.   -- Checks if there's a version of the package installed
  368.   if fs.exists(install_pkg) then
  369.     print('')
  370.     print('Found previous version of package installed, performing uninstall...')
  371.     uninstall_package(install_dir)
  372.   end
  373.  
  374.   -- Puts down new metadata file
  375.   print('')
  376.   print('Installing package metadata...')
  377.   file_write(install_pkg, textutils.serializeJSON(package))
  378.  
  379.   -- Installs
  380.   print('')
  381.   print('Installing new files...')
  382.   fs.makeDir(install_dir)
  383.   installer(package, install_dir)
  384.  
  385.   -- Creates binary shortcuts
  386.   print('')
  387.   print('Linking binaries...')
  388.   for bin, bin_data in pairs(package.bin or {}) do
  389.     -- Parses binary
  390.     bin = shortcut(bin, bin_data, install_dir)
  391.  
  392.     -- Writes the shortcut file
  393.     printf('%s -> %s', bin.name, bin.target)
  394.     file_write(bin.bin, string.format('_=shell.dir();shell.setDir("%s");shell.run("%s",...);shell.setDir(_)', bin.target_dir, bin.target))
  395.   end
  396.  
  397.   -- Shows installed files
  398.   print('')
  399.   print('Install complete! New commands:')
  400.   for bin, bin_data in pairs(package.bin or {}) do
  401.     bin = shortcut(bin, bin_data, install_dir)
  402.     if (bin.text) then
  403.       printf('- %s : %s', bin.name, bin.text)
  404.     else
  405.       printf('- %s', bin.name)
  406.     end
  407.   end
  408. end
  409.  
  410. ----------------------------------------------
  411. -- Main part of the installer
  412. ----------------------------------------------
  413.  
  414. -- Makes sure HTTP is enabled
  415. if not http then
  416.   error('You must have http enabled in your game/server to use this script.')
  417. end
  418.  
  419. -- Parses args
  420. local args = {...}
  421. local command = args[1]
  422. local source = args[2]
  423. local source_pkg = args[3]
  424.  
  425. -- Extra options
  426. local extra_options = {}
  427. for i = 4, #args do
  428.   -- Matches --arg=value
  429.   local raw = args[i]:match('%-%-(.+)')
  430.   if raw then
  431.     -- Parses arg and value
  432.     local opt, val = raw:match('(.+)=(.+)')
  433.     if opt and val then
  434.       -- Handles true/false, numerics, etc
  435.       if val == 'true' then val = true
  436.       elseif val == 'false' then val = false
  437.       elseif tonumber(val) then val = tonumber(val)
  438.       end
  439.     else
  440.       -- Short assignments to true
  441.       opt = raw
  442.       val = true
  443.     end
  444.    
  445.     -- Sets option
  446.     extra_options[opt] = val
  447.   end
  448. end
  449.  
  450. -- Sanity check
  451. if (not command) or (not source) or (not source_pkg) then
  452.   print('Usage: lunar install/uninstall [source] [package]')
  453.   print('Options:')
  454.   print('--source.branch=some/git-branch')
  455.   return 1
  456. end
  457.  
  458. -- Gets our source loader
  459. local get_package = pkg_sources[source]
  460.  
  461. -- Checks if source loader is valid
  462. if not get_package then
  463.   printf('Invalid package source: %s', source)
  464.   printf('Valid options:')
  465.   for source, fn in pairs(pkg_sources) do
  466.     printf('- %s', source)
  467.   end
  468.   return 1
  469. end
  470.  
  471. -- Loads package
  472. local package = get_package(source_pkg)
  473.  
  474. -- installs package
  475. if 'install' == command then
  476.   install_package(package, extra_options)
  477. elseif 'uninstall' == command then
  478.   uninstall_package(package, extra_options)
  479. end
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement