diff --git a/.lovedeps b/.lovedeps new file mode 100644 index 0000000..9004671 --- /dev/null +++ b/.lovedeps @@ -0,0 +1,3 @@ +{ + serialize = 'https://git.doublefourteen.io/lua/df-serialize' +} diff --git a/crush.lua b/crush.lua new file mode 100644 index 0000000..78d4131 --- /dev/null +++ b/crush.lua @@ -0,0 +1,284 @@ +--- 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' + +-- 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 + -- --------------- + -- 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 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()