--- crush - The uncomplicated dependency system for LÖVE. -- -- Author: Lorenzo Cogotti -- Copyright: 2022 The DoubleFourteen Code Forge -- Home: https://codeberg.org/1414codeforge/crush -- License: MIT local io = require 'io' local os = require 'os' -- Lua version check local function check_version() -- Generic Lua version check - 5.2 required if _VERSION then local maj, min = _VERSION:match("Lua (%d+)%.(%d+)") if maj and min then maj, min = tonumber(maj), tonumber(min) if maj > 5 or (maj == 5 and min >= 2) then return true end end end -- LuaJIT check - 2.0.0 required (technically 2.0.0_beta11) if jit and jit.version_num and jit.version_num >= 20000 then return true end return false end if not check_version() then error("Unsupported Lua version!\nSorry, crush requires Lua 5.2 or LuaJIT 2.0.0.") end -- System specific functions -- -- 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 -- 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 -- On success, os.execute() returns: -- true on regular Lua -- 0 on LuaJIT (actual OS error code) local code = os.execute(fullcmd) if code ~= true and code ~= 0 then error(dep.name..": Dependency fetch failed ("..cmd..").") end end -- .lovedeps file scan local function map_file(name) local fh = io.open(name, 'r') if fh == nil then error(name..": can't read file.") end local contents = fh:read('*all') fh:close() return contents end local function scandeps(manifest, mode, deps) mode = mode or 'nodups' deps = deps or {} local contents = map_file(manifest) contents = "return "..contents local fun, res = load(contents, manifest, 't', {}) if not fun then error(res) end local ok, def = pcall(fun) if not ok then error(def) -- def is now pcall()'s error message end if type(def) ~= 'table' then error("[string \""..manifest.."\"]: Loading resulted in a '"..type(def).."', while 'table' was expected.") end for name,url in pairs(def) do if type(url) == 'function' then goto skip -- ignore functions end if type(url) ~= 'string' then error("[string \""..manifest.."\"]: "..name..": git repository URL must be a 'string'.") end for i in ipairs(deps) do if name == deps[i].name then if mode == 'skipdups' then goto skip end error("[string \""..manifest.."\"]: "..name..": Duplicate dependency.") 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 local i = 1 while i <= #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 i = i + 1 end end run()