--- crush - The uncomplicated dependency system for LÖVE. -- -- Author: Lorenzo Cogotti -- Copyright: 2022 The DoubleFourteen Code Forge -- License: MIT (see LICENSE file for details) local io = require 'io' local os = require 'os' -- Utility functions local function split(sep, s) local t = {} for i in s:gmatch("([^"..sep.."]+)") do t[#t+1] = i end return t end local function startswith(s, prefix) return s:sub(1, #prefix) == prefix end local function trim(s) local from = s:match("^%s*()") return from > #s and "" or s:match(".*%S", from) end -- System specific -- -- Portions of this code are based on work from the LuaRocks project. -- LuaRocks is free software and uses the MIT license. -- -- LuaRocks website: https://luarocks.org -- LuaRocks sources: https://github.com/luarocks/luarocks local is_windows = package.config:sub(1,1) == "\\" local is_directory local Q local quiet local chdir local mkdir if is_windows then -- --------------- -- NOTE: untested! -- --------------- -- local function is_directory(path) local fh, _, code = io.open(path, 'r') if code == 13 then -- directories return "Permission denied" fh, _, code = io.open(path.."\\", 'r') if code == 2 then -- directories return 2, files return 22 return true end end if fh then fh:close() end return false end local function split_path(s) local drive = "" local root = "" local rest local unquoted = s:match("^['\"](.*)['\"]$") if unquoted then s = unquoted end if s:match("^.:") then drive = s:sub(1, 2) s = s:sub(3) end if s:match("^[\\/]") then root = s:sub(1, 1) rest = s:sub(2) else rest = s end return drive, root, rest end -- local function Q(s) local drive, root, rest = split_path(s) if root ~= "" then s = s:gsub("/", "\\") end if s == "\\" then return '\\' -- CHDIR needs special handling for root dir end -- URLs and anything else s = s:gsub('\\(\\*)"', '\\%1%1"') s = s:gsub('\\+$', '%0%0') s = s:gsub('"', '\\"') s = s:gsub('(\\*)%%', '%1%1"%%"') return '"'..s..'"' end -- local function quiet(cmd) return cmd.." 2> NUL 1> NUL" end -- local function chdir(newdir, cmd) local drive = newdir:match("^([A-Za-z]:)") cmd = "cd "..Q(newdir).." & "..cmd if drive then cmd = drive.." & "..cmd end return cmd end -- local function mkdir(path) local cmd = "mkdir "..Q(path).." 2> NUL 1> NUL" os.execute(cmd) if not is_directory(path) then error("Couldn't create directory '"..path.."'.") end end else -- local function is_directory(path) local fh, _, code = io.open(path.."/.", 'r') if code == 2 then -- "No such file or directory" return false end if code == 20 then -- "Not a directory", regardless of permissions return false end if code == 13 then -- "Permission denied", but is a directory return true end if fh then _, _, code = fh:read(1) fh:close() if code == 21 then -- "Is a directory" return true end end return false end -- local function Q(s) return "'"..s:gsub("'", "'\\''").."'" end -- local function quiet(cmd) return cmd.." >/dev/null 2>&1" end -- local function chdir(newdir, cmd) return "cd "..Q(newdir).." && "..cmd end -- local function mkdir(path) local cmd = "mkdir "..Q(path).." >/dev/null 2>&1" os.execute(cmd) if not is_directory(path) then error("Couldn't create directory '"..path.."'.") end end end -- Dependency fetch local function fetch(dep) local dest = 'lib/'..dep.name print(("Dependency %s -> %s (%s)"):format(dep.name, dest, dep.url)) local cmd, fullcmd if is_directory(dest) then -- Directory exists, pull operation cmd = "git pull" fullcmd = chdir(dest, quiet("git pull")) else -- Directory doesn't exist, clone operation cmd = "git clone "..Q(dep.url).." "..Q(dep.name) fullcmd = chdir("lib", quiet(cmd)) end if not os.execute(fullcmd) then error(name..": Dependency fetch failed ("..cmd..").") end end -- .lovedeps file scan local function scandeps(manifest, mode, deps) mode = mode or 'nodups' deps = deps or {} local i = 0 for line in io.lines(manifest) do line = trim(line) i = i + 1 if line == "" or startswith(line, "#") then goto skip end local fields = split(' ', line) if #fields ~= 2 then error(manifest..":"..i..": Syntax error, line must be in 'name url' form.") end local name, url = fields[1], fields[2] for i in ipairs(deps) do if name == deps[i].name then if mode == 'skipdups' then goto skip end error(manifest..":"..i..": Duplicate dependency '"..name.."'.") end end deps[#deps+1] = { name = name, url = url } ::skip:: end return deps end -- Entry point local function file_exists(name) local fh = io.open(name, 'r') if fh ~= nil then fh:close() return true end return false end local function run() local deps = scandeps(".lovedeps") mkdir("lib") -- NOTE: deps array may grow while scanning for i = 1,#deps do local dep = deps[i] -- Fetch dependency fetch(dep) -- Resolve dependency's dependencies local depmanifest = "lib/"..dep.name.."/.lovedeps" if file_exists(depmanifest) then scandeps(depmanifest, 'skipdups', deps) end end end run()