From 873934e526c6b782bfc1a2a7d40b8763fd870425 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonin=20D=C3=A9cimo?= Date: Thu, 21 Jan 2016 00:48:04 +0100 Subject: [PATCH] Rewrite in Lua with LuaRocks --- .gitignore | 44 +++++ README.md | 85 ++++++++ config.ld | 8 + rockspecs/love-release-scm-1.rockspec | 42 ++++ src/main.lua | 20 ++ src/pipes/conf.lua | 78 ++++++++ src/pipes/env.lua | 115 +++++++++++ src/project.lua | 272 ++++++++++++++++++++++++++ src/script.lua | 87 ++++++++ src/scripts/debian.lua | 138 +++++++++++++ src/scripts/love.lua | 20 ++ src/scripts/macosx.lua | 91 +++++++++ src/scripts/windows.lua | 87 ++++++++ src/utils.lua | 123 ++++++++++++ 14 files changed, 1210 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 config.ld create mode 100644 rockspecs/love-release-scm-1.rockspec create mode 100644 src/main.lua create mode 100644 src/pipes/conf.lua create mode 100644 src/pipes/env.lua create mode 100644 src/project.lua create mode 100644 src/script.lua create mode 100644 src/scripts/debian.lua create mode 100644 src/scripts/love.lua create mode 100644 src/scripts/macosx.lua create mode 100644 src/scripts/windows.lua create mode 100644 src/utils.lua diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..46c2489 --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +### Lua ### +# LDoc +/doc + +# Compiled Lua sources +luac.out + +# luarocks build files +*.src.rock +*.zip +*.tar.gz + +# Object files +*.o +*.os +*.ko +*.obj +*.elf + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo +*.def +*.exp + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex diff --git a/README.md b/README.md new file mode 100644 index 0000000..57e4e4b --- /dev/null +++ b/README.md @@ -0,0 +1,85 @@ +# love-release +[![License](http://img.shields.io/badge/License-MIT-brightgreen.svg)](LICENSE) + +[Lua][lua] 5.1 script that makes [LÖVE][love] game release easier (previously Bash script). +Automates LÖVE [Game Distribution][game_dist]. +LÖVE [forum topic][forum_topic]. +Available as a [LuaRocks][luarocks] [package][package]. + +## Features +love-release makes your LÖVE game release easier. It can create from your sources Windows executables, MacOS X applications, Debian packages and simple LÖVE files. + +love-release creates only one LÖVE file in a release directory and keeps it synced with your sources. + +love-release can extract its informations from the environment: it guesses your game's title from the directory where it's stored, selects by default the latest LÖVE version from the web or uses its latest bundled LÖVE version, then parses the `conf.lua` file to extract even more informations such as the real LÖVE version your project uses. + +### Configuration +love-release prints to the command-line a Lua table containing the informations it uses to generate your project. These informations can be stored in your `conf.lua` file to be used later. + +```lua +function love.conf(t) + t.releases = { + title = nil, -- The project title (string) + package = nil, -- The project command and package name (string) + loveVersion = nil, -- The project LÖVE version + version = nil, -- The project version + author = nil, -- Your name (string) + email = nil, -- Your email (string) + description = nil, -- The project description (string) + homepage = nil, -- The project homepage (string) + identifier = nil, -- The project Uniform Type Identifier (string) + releaseDirectory = nil, -- Where to store the project releases (string) + } +end +``` + +## Installation + +### Dependencies +love-release is only installable through LuaRocks and highly depends on LuaRocks internal API. love-release is currently build on LuaRocks 2.3.0. LuaRocks API is not meant to be stable, and a future update could break love-release. As love-release is made for LÖVE, it is written for Lua 5.1. + +#### Required +- [libzip][libzip] headers for lua-zip. +- [lua-zip][lua-zip] has no official stable version, thus while available on LuaRocks it must be installed manually. +- Other libraries are automatically installed, but let's give them some credit: [luafilesystem][lfs], [loadconf][loadconf], [middleclass][middleclass], [semver][semver]. + +#### Optional +- `love` can be used to determine your system LÖVE version. +- `fakeroot` and `dpkg-deb` are required to create Debian packages. + +### Install + +```sh +# sudo +luarocks install --server=http://luarocks.org/dev lua-zip +luarocks install --server=http://luarocks.org/dev love-release +``` + +### Remove Bash version +You may have previously installed the Bash version of love-release. You can remove it with the following piece of code. Take the time to assure yourself that the paths are correct and match your installation of love-release. + +```sh +rm -rf '/usr/bin/love-release' +rm -rf '/usr/share/love-release' +rm -rf '/usr/share/man/man1/love-release.1.gz' +rm -rf '/usr/share/bash-completion/completions/love-release' '/etc/bash_completion.d/love-release' +``` + +## Contribute +The documentation of love-release internals is written with [LDoc][ldoc]. Generate it by running `ldoc .`. +I do not plan to keep developing the Bash script, not even fixing it. If there appears to be any need for it, let me know and I might consider doing so. +Every bug report or feature request is gladly welcome ! + +[forum_topic]: https://love2d.org/forums/viewtopic.php?t=75387 +[game_dist]: https://www.love2d.org/wiki/Game_Distribution +[ldoc]: https://github.com/stevedonovan/LDoc +[lfs]: https://github.com/keplerproject/luafilesystem +[libzip]: http://www.nih.at/libzip/ +[love]: https://www.love2d.org/ +[lua]: http://www.lua.org/ +[luarocks]: https://luarocks.org/ +[lua-zip]: https://github.com/brimworks/lua-zip +[loadconf]: https://github.com/Alloyed/loadconf +[middleclass]: https://github.com/kikito/middleclass +[package]: https://luarocks.org/modules/rucikir/love-release +[semver]: https://github.com/kikito/semver.lua diff --git a/config.ld b/config.ld new file mode 100644 index 0000000..40e7a59 --- /dev/null +++ b/config.ld @@ -0,0 +1,8 @@ +file = "src" +project = "love-release" +description = [[ +love-release - a Lua script to make LÖVE games releases easier +]] +readme = "README.md" +format = "discount" +title = "love-release documentation" diff --git a/rockspecs/love-release-scm-1.rockspec b/rockspecs/love-release-scm-1.rockspec new file mode 100644 index 0000000..8d6987d --- /dev/null +++ b/rockspecs/love-release-scm-1.rockspec @@ -0,0 +1,42 @@ +package = "love-release" +version = "scm-1" +source = { + url = "git://github.com/MisterDA/love-release.git", + branch = "lua", +} +description = { + summary = "Make LÖVE games releases easier", + detailed = [[ +love-release make LÖVE games releases easier. +It automates LÖVE Game Distribution. +]], + license = "MIT", + homepage = "https://github.com/MisterDA/love-release", +} +dependencies = { + "loadconf", + "lua ~> 5.1", + "luafilesystem", + "lua-zip", + "middleclass", + "semver", +} +build = { + type = "builtin", + modules = { + ["love-release.scripts.debian"] = "src/scripts/debian.lua", + ["love-release.scripts.love"] = "src/scripts/love.lua", + ["love-release.scripts.macosx"] = "src/scripts/macosx.lua", + ["love-release.scripts.windows"] = "src/scripts/windows.lua", + ["love-release.pipes.conf"] = "src/pipes/conf.lua", + ["love-release.pipes.env"] = "src/pipes/env.lua", + ["love-release.project"] = "src/project.lua", + ["love-release.script"] = "src/script.lua", + ["love-release.utils"] = "src/utils.lua", + }, + install = { + bin = { + ["love-release"] = "src/main.lua" + }, + }, +} diff --git a/src/main.lua b/src/main.lua new file mode 100644 index 0000000..55b1b3e --- /dev/null +++ b/src/main.lua @@ -0,0 +1,20 @@ +--- love-release main. +-- @script love-release + +local conf = require 'love-release.pipes.conf' +local env = require 'love-release.pipes.env' +local Project = require 'love-release.project' +local p = Project:new() +conf(env(p)) + +print(p) + +local script +script = require 'love-release.scripts.love' +script(p) +script = require 'love-release.scripts.macosx' +script(p) +script = require 'love-release.scripts.windows' +script(p) +script = require 'love-release.scripts.debian' +script(p) diff --git a/src/pipes/conf.lua b/src/pipes/conf.lua new file mode 100644 index 0000000..083bfee --- /dev/null +++ b/src/pipes/conf.lua @@ -0,0 +1,78 @@ +--- Gather informations from the LÖVE conf.lua file. +-- @module conf +-- @usage conf(project) + +local fs = require 'luarocks.fs' +local loadconf = require 'loadconf' +local semver = require 'semver' + +local utils = require 'love-release.utils' + + +local pipe = {} + +function pipe.pipe(project) + local err = utils.io.err + + -- checks for a conf.lua file + fs.change_dir(project.projectDirectory) + if not fs.exists("conf.lua") then + err("CONF: No conf.lua provided.\n") + return project + end + local conf = assert(loadconf.parse_file("conf.lua")) + fs.pop_dir() + + local function setString(key, value) + if type(value) == "string" then + project["set"..key](project, value) + end + end + + local function setLoveVersion(v) + if type(v) == "string" and v ~= "" then + local version = semver(v) + if not utils.love.isSupported(version) then + local scriptLoveVersion = project.loveVersion + err("CONF: Your LÖVE conf version ("..v + .. ") is not supported by love-release ("..tostring(scriptLoveVersion) + .. ").\n") + if version > scriptLoveVersion then + err(" You should update love-release.\n") + elseif version < scriptLoveVersion then + err(" You should update your project.\n") + end + end + project:setLoveVersion(version) + end + end + + -- extract LÖVE standard fields + setString("Title", conf.title) + setString("Package", conf.package) + setLoveVersion(conf.version) + + -- extract love-release fields + local releases = conf.releases + if type(releases) == "table" then + setString("Title", releases.title) + setString("Package", releases.package) + setLoveVersion(releases.loveVersion) + setString("Version", releases.version) + setString("Author", releases.author) + setString("Email", releases.email) + setString("Description", releases.description) + setString("Homepage", releases.homepage) + setString("Identifier", releases.identifier) + setString("ReleaseDirectory", releases.releaseDirectory) + end + + return project +end + + +setmetatable(pipe, { + __call = function(_, project) return pipe.pipe(project) end, +}) + +return pipe diff --git a/src/pipes/env.lua b/src/pipes/env.lua new file mode 100644 index 0000000..8ee4a63 --- /dev/null +++ b/src/pipes/env.lua @@ -0,0 +1,115 @@ +--- Gather informations from the environment. +-- @module env +-- @usage env(project) + +local fs = require 'luarocks.fs' +local semver = require 'semver' +local utils = require 'love-release.utils' + +local pipe = {} + + +--- Gets the version of the installed LÖVE. +-- @treturn semver LÖVE version. +-- @local +local function getSystemLoveVersion() + local handle = io.popen('love --version') + local result = handle:read("*a") + handle:close() + local version = result:match('%d+%.%d+%.%d+') + if version then + return semver(version) + end +end + +--- Gets the latest LÖVE version from the web. +-- @treturn semver LÖVE version. +-- @local +local function getWebLoveVersion() + local releasesPath = utils.cache.."/releases.xml" + + local ok, err = fs.download("https://love2d.org/releases.xml", + releasesPath, + true) + if ok then + local releasesXml = io.open(releasesPath, "rb") + local version = releasesXml:read("*a"):match("(%d+%.%d+%.%d+)") + releasesXml:close() + return semver(version) + else + return nil, err + end +end + +--- Gets the latest LÖVE version from the script, the system and the web. +-- @tparam semver script script version. +-- @tparam semver system system version. +-- @tparam semver web web version. +-- @treturn semver the latest version. +-- @local +local function getLatestLoveVersion(script, system, web) + local version = script + if system and system >= script then + version = system + end + if web and web > version then + version = web + end + return version +end + +function pipe.pipe(project) + local err = utils.io.err + + -- checks for a main.lua file + fs.change_dir(project.projectDirectory) + if not fs.exists("main.lua") then + err("ENV: No main.lua provided.\n") + os.exit(1) + end + fs.pop_dir() + + -- title + project:setTitle(project.projectDirectory:match("[^/]+$")) + + -- package + project:setPackage(project.title:gsub("%W", "-"):lower()) + + -- LÖVE version + + local systemLoveVersion = getSystemLoveVersion() + local webLoveVersion = getWebLoveVersion() + local scriptLoveVersion = utils.love.lastVersion() + local isSupported = utils.love.isSupported + + if systemLoveVersion and not isSupported(systemLoveVersion) then + err("ENV: Your LÖVE installed version (" .. tostring(systemLoveVersion) .. + ") is not supported by love-release (" .. tostring(scriptLoveVersion) .. + ").\n") + if systemLoveVersion > scriptLoveVersion then + err(" You should update love-release.\n") + elseif systemLoveVersion < scriptLoveVersion then + err(" You should update LÖVE.\n") + end + end + + if webLoveVersion and not isSupported(webLoveVersion) then + err("ENV: The upstream LÖVE version (" .. tostring(webLoveVersion) .. + ") is not supported by love-release (" .. tostring(scriptLoveVersion) .. + ").\n") + err(" You should update love-release.\n") + end + + project:setLoveVersion(getLatestLoveVersion(scriptLoveVersion, + systemLoveVersion, + webLoveVersion)) + + return project +end + + +setmetatable(pipe, { + __call = function(_, project) return pipe.pipe(project) end, +}) + +return pipe diff --git a/src/project.lua b/src/project.lua new file mode 100644 index 0000000..db2a447 --- /dev/null +++ b/src/project.lua @@ -0,0 +1,272 @@ +--- Provides tools to manipulate a LÖVE project. +-- @classmod project + +local fs = require 'luarocks.fs' +local lr_dir = require 'luarocks.dir' +local class = require 'middleclass' +local utils = require 'love-release.utils' + +local Project = class('Project') + + +--- Title of this project. +Project.title = nil + +--- Package name. It's the title converted to lowercase, with alpha-numerical +-- characters and hyphens only. +Project.package = nil + +--- LÖVE version the project uses. +Project.loveVersion = nil + +--- Version. +Project.version = nil + +--- Author full name. +Project.author = nil + +--- Email. +Project.email = nil + +--- Description. +Project.description = nil + +--- Homepage URL. +Project.homepage = nil + +--- Uniform Type Identifier in reverse-DNS format. +Project.identifier = nil + +--- Project directory, where to find the game sources. +Project.projectDirectory = nil + +--- Project release directory, where to store the releases. +Project.releaseDirectory = nil + +Project._fileTree = nil +Project._fileList = nil + +function Project:initialize() + local defaultDirectory = fs.current_dir() + self:setProjectDirectory(defaultDirectory) + self:setReleaseDirectory(defaultDirectory.."/releases") +end + +--- Recursive function used to build the tree. +-- @local +local _buildFileTree +_buildFileTree = function(dir) + local subDir + for file in assert(fs.dir()) do + if not file:find("^%.git") then + if fs.is_dir(file) then + subDir = {} + dir[file] = subDir + assert(fs.change_dir(file)) + _buildFileTree(subDir, file) + assert(fs.pop_dir()) + elseif fs.is_file(file) then + dir[#dir+1] = file + end + end + end +end + +--- Constructs the file tree. +-- @return File tree. The table represents the root directory. +-- Sub-directories are represented as sub-tables, indexed by the directory name. +-- Files are strings stored in each sub-tables. +function Project:fileTree() + if not self._fileTree then + assert(fs.change_dir(self.projectDirectory)) + self._fileTree = {} + _buildFileTree(self._fileTree) + assert(fs.pop_dir()) + end + return self._fileTree +end + +--- Recursive function used to build the file list. +-- @local +local _buildFileList +_buildFileList = function(list, tree, dir) + for k, v in pairs(tree) do + if type(v) == "table" then + list[#list+1] = dir..k.."/" + _buildFileList(list, tree[k], dir..k.."/") + elseif type(v) == "string" then + list[#list+1] = dir..v + end + end +end + +--- Constructs the file list. +-- @bool build Rebuild the file tree. +-- @treturn table List of this project's files. +function Project:fileList(build) + if not self._fileList or build then + self._fileList = {} + _buildFileList(self._fileList, self:fileTree(), "") + self:excludeFiles() + end + return self._fileList +end + +--- Excludes files from the LÖVE file. +-- @todo This function should be able to parse and use CVS files such as +-- gitignore. It should also work on the file tree rather than on the file list. +-- For now it only exludes the release directory if it is within the project +-- directory and works on the file list. +function Project:excludeFiles() + local dir = self.releaseDirectory:gsub( + "^"..utils.lua.escape_string_regex(self.projectDirectory).."/", + "") + if dir then + local n = #self._fileList + for i = 1, n do + if self._fileList[i]:find("^"..dir) then + self._fileList[i] = nil + end + end + end +end + +--[[ +-- File tree traversal +local function deep(tree) + for k, v in pairs(tree) do + if type(v) == "string" then + print(v) + elseif type(v) == "table" then + print(k) + deep(v) + end + end +end +deep(t) +--]] + +local function escape(var) + if type(var) == "string" then + return "'"..var:gsub("'", "\'").."'" + else + return tostring(var) + end +end + +--- Prints debug informations. +-- @local +function Project:__tostring() + return + '{\n'.. + ' title = '..escape(self.title)..',\n'.. + ' package = '..escape(self.package)..',\n'.. + ' loveVersion = \''..escape(self.loveVersion)..'\',\n'.. + ' version = '..escape(self.version)..',\n'.. + ' author = '..escape(self.author)..',\n'.. + ' email = '..escape(self.email)..',\n'.. + ' description = '..escape(self.description)..',\n'.. + ' homepage = '..escape(self.homepage)..',\n'.. + ' identifier = '..escape(self.identifier)..',\n'.. + ' projectDirectory = '..escape(self.projectDirectory)..',\n'.. + ' releaseDirectory = '..escape(self.releaseDirectory)..',\n'.. + '}' +end + +--- Sets the title. +-- @string title the title. +-- @treturn project self. +function Project:setTitle(title) + self.title = title + return self +end + +--- Sets the package name. +-- @string package the package name. +-- @treturn project self. +function Project:setPackage(package) + self.package = package + return self +end + +--- Sets the LÖVE version used. +-- @tparam semver version the LÖVE version. +-- @treturn project self. +function Project:setLoveVersion(version) + self.loveVersion = version + return self +end + +--- Sets the project's version. +-- @string version the version. +-- @treturn project self. +function Project:setVersion(version) + self.version = version + return self +end + +--- Sets the author. +-- @string author the author. +-- @treturn project self. +function Project:setAuthor(author) + self.author = author + return self +end + +--- Sets the author's email. +-- @string email the email. +-- @treturn project self. +function Project:setEmail(email) + self.email = email + return self +end + +--- Sets the description. +-- @string description the description. +-- @treturn project self. +function Project:setDescription(description) + self.description = description + return self +end + +--- Sets the homepage. +-- @string homepage the homepage. +-- @treturn project self. +function Project:setHomepage(homepage) + self.homepage = homepage + return self +end + +--- Sets the identifier. +-- @string identifier the identifier. +-- @treturn project self. +function Project:setIdentifier(identifier) + self.identifier = identifier + return self +end + +--- Sets the source directory. The path is normalized and absoluted. +-- @string directory the directory. +-- @treturn project self. +function Project:setProjectDirectory(directory) + directory = fs.absolute_name(lr_dir.normalize(directory)) + assert(fs.change_dir(directory)) + fs.pop_dir() + self.projectDirectory = directory + return self +end + +--- Sets the release directory. The path is normalized and absoluted. +-- @string directory the directory. +-- @treturn project self. +function Project:setReleaseDirectory(directory) + directory = fs.absolute_name(lr_dir.normalize(directory)) + assert(fs.make_dir(directory)) + assert(fs.change_dir(directory)) + fs.pop_dir() + self.releaseDirectory = directory + return self +end + + +return Project diff --git a/src/script.lua b/src/script.lua new file mode 100644 index 0000000..dfeafc5 --- /dev/null +++ b/src/script.lua @@ -0,0 +1,87 @@ +--- A love-release script. +-- @classmod script + +local fs = require 'luarocks.fs' +local class = require 'middleclass' +local lfs = require "lfs" +local zip = require 'brimworks.zip' +local utils = require 'love-release.utils' + +local Script = class('Script') + + +--- Current project. +Script.project = nil + +--- Name of the LÖVE file. +Script.loveFile = nil + +local function validate(project) + local valid, err = true, utils.io.err + if type(project.title) ~= "string" or project.title == "" then + err("SCRIPT: No title specified.\n") + valid = false + end + if type(project.package) ~= "string" or project.package == "" then + err("SCRIPT: No package specified.\n") + valid = false + end + if not type(project.loveVersion) then + err("SCRIPT: No LÖVE version specified.\n") + valid = false + end + if not valid then os.exit(1) end + return project +end + +function Script:initialize(project) + self.project = validate(project) + self.loveFile = project.title..'.love' +end + +--- Creates a LÖVE file in the release directory of the current project. +function Script:createLoveFile() + local ar = assert(zip.open(self.project.releaseDirectory.."/"..self.loveFile, + zip.OR(zip.CREATE, zip.CHECKCONS))) + + assert(fs.change_dir(self.project.projectDirectory)) + + local attributes, stat + for _, file in ipairs(self.project:fileList()) do + attributes = assert(lfs.attributes(file)) + stat = ar:stat(file) + + -- file is not present in the filesystem nor the archive + if not attributes and not stat then + utils.io.err("BUILD: "..file.." is not present in the file system.\n") + -- file is not present in the archive + elseif attributes and not stat then + utils.io.out("Add "..file.."\n") + if attributes.mode == "directory" then + ar:add_dir(file) + else + ar:add(file, "file", file) + end + -- file in the filesystem is more recent than in the archive + elseif attributes and stat and attributes.modification > stat.mtime + 5 then + if attributes.mode == "file" then + utils.io.out("Update "..file.."\n") + ar:replace(assert(ar:name_locate(file)), "file", file) + end + end + end + + for i = 1, #ar do + local file = ar:stat(i).name + -- file is present in the archive, but not in the filesystem + if not lfs.attributes(file) then + utils.io.out("Delete "..file.."\n") + ar:delete(i) + end + end + + ar:close() + assert(fs.pop_dir()) +end + +return Script diff --git a/src/scripts/debian.lua b/src/scripts/debian.lua new file mode 100644 index 0000000..46c7f44 --- /dev/null +++ b/src/scripts/debian.lua @@ -0,0 +1,138 @@ +--- Debian package release. +-- @module scripts.debian +-- @usage debian(project) + +local fs = require 'luarocks.fs' +local dir = require 'luarocks.dir' +local lfs = require 'lfs' +local Script = require 'love-release.script' +local utils = require 'love-release.utils' + +local s = {} + + +local function validate(project) + local valid, err = true, utils.io.err + if type(project.author) ~= "string" or project.author == "" then + err("DEBIAN: No author specified.\n") + valid = false + end + if type(project.description) ~= "string" or project.description == "" then + err("DEBIAN: No description specified.\n") + valid = false + end + if type(project.email) ~= "string" or project.email == "" then + err("DEBIAN: No email specified.\n") + valid = false + end + if type(project.homepage) ~= "string" or project.homepage == "" then + err("DEBIAN: No homepage specified.\n") + valid = false + end + if type(project.version) ~= "string" or project.version == "" then + err("DEBIAN: No version specified.\n") + valid = false + end + if not valid then os.exit(1) end + return project +end + +-- Is it such a good design to load the Debian package into memory with +-- temporary files ? +function s.script(project) + local ok1, err1 = fs.is_tool_available("fakeroot", "fakeroot", "-v") + local ok2, err2 = fs.is_tool_available("dpkg-deb", "dpkg-deb") + if not ok1 or not ok2 then + if not ok1 then utils.io.err(err1) end + if not ok2 then utils.io.err(err2) end + os.exit(1) + end + + local script = Script:new(validate(project)) + script:createLoveFile() + + local tempDir = assert(fs.make_temp_dir("debian")) + local loveFileDeb = "/usr/share/games/"..project.package.."/"..script.loveFile + local loveFileRel = project.releaseDirectory.."/"..script.loveFile + local md5sums = {} + + local function writeFile(path, content, md5) + local fullPath = tempDir..path + assert(fs.make_dir(dir.dir_name(fullPath))) + local file = assert(io.open(fullPath, "wb")) + file:write(content) + file:close() + + if md5 then + md5sums[#md5sums+1] = { path = path, md5 = assert(fs.get_md5(fullPath)) } + end + end + + local function copyFile(orig, dest, md5) + local fullPath = tempDir..dest + assert(fs.make_dir(dir.dir_name(fullPath))) + assert(fs.copy(orig, fullPath)) + + if md5 then + md5sums[#md5sums+1] = { path = dest, md5 = assert(fs.get_md5(fullPath)) } + end + end + + -- /DEBIAN/control + writeFile("/DEBIAN/control", + "Package: "..project.package.."\n".. + "Version: "..project.version.."\n".. + "Architecture: all\n".. + "Maintainer: "..project.author.." <"..project.email..">\n".. + "Installed-Size: ".. + math.floor(assert(lfs.attributes(loveFileRel, "size")) / 1024).."\n".. + "Depends: love (>= "..tostring(project.loveVersion)..")\n".. + "Priority: extra\n".. + "Homepage: "..project.homepage.."\n".. + "Description: "..project.description.."\n" + ) + + -- /usr/share/applications/${PACKAGE}.desktop + writeFile("/usr/share/applications/"..project.package..".desktop", + "[Desktop Entry]\n".. + "Name="..project.title.."\n".. + "Comment="..project.description.."\n".. + "Exec="..project.package.."\n".. + "Type=Application\n".. + "Categories=Game;\n", + true + ) + + -- /usr/bin/${PACKAGE} + writeFile("/usr/bin/"..project.package, + "#!/bin/sh\n".. + "love "..loveFileDeb.."\n", + true + ) + + -- /usr/share/games/${PACKAGE}/${LOVE_FILE} + copyFile(project.releaseDirectory.."/"..script.loveFile, loveFileDeb, true) + + -- /DEBIAN/md5sums + local sum = "" + for _, v in ipairs(md5sums) do + sum = sum..v.md5.." "..v.path.."\n" + end + writeFile("/DEBIAN/md5sums", sum) + + -- create the package + local deb = project.releaseDirectory.."/"..project.package.."-".. + project.version.."_all.deb" + fs.delete(deb) + assert(fs.execute("fakeroot dpkg-deb -b ", tempDir, deb), + "DEBIAN: error while building the package.") + + fs.delete(tempDir) +end + + +setmetatable(s, { + __call = function(_, project) return s.script(project) end, +}) + +return s diff --git a/src/scripts/love.lua b/src/scripts/love.lua new file mode 100644 index 0000000..0426aa4 --- /dev/null +++ b/src/scripts/love.lua @@ -0,0 +1,20 @@ +--- LÖVE file release. +-- @module scripts.love +-- @usage love(project) + +local Script = require "love-release.script" + +local s = {} + + +function s.script(project) + local script = Script:new(project) + script:createLoveFile() +end + + +setmetatable(s, { + __call = function(_, project) return s.script(project) end, +}) + +return s diff --git a/src/scripts/macosx.lua b/src/scripts/macosx.lua new file mode 100644 index 0000000..a5f27b1 --- /dev/null +++ b/src/scripts/macosx.lua @@ -0,0 +1,91 @@ +--- MacOS X app release. +-- @module scripts.macosx +-- @usage macosx(project) + +local fs = require "luarocks.fs" +local semver = require "semver" +local zip = require "brimworks.zip" +local Script = require "love-release.script" +local utils = require "love-release.utils" + +local s = {} + + +local function validate(project) + local valid, err = true, utils.io.err + if type(project.identifier) ~= "string" or project.identifier == "" then + err("DEBIAN: No author specified.\n") + valid = false + end + if not valid then os.exit(1) end + return project +end + +function s.script(project) + local script = Script:new(validate(project)) + script:createLoveFile() + fs.change_dir(project.releaseDirectory) + + local prefix = "love-"..tostring(project.loveVersion).."-macosx-" + local bin + if project.loveVersion >= semver'0.9.0' then + bin = prefix.."x64.zip" + else + bin = prefix.."ub.zip" + end + local url = "https://bitbucket.org/rude/love/downloads/"..bin + local cache = utils.cache.."/"..bin + + -- Can't cache the archive because luarocks functions use a HEAD request to + -- Amazon AWS which will answer a 403. + -- assert(fs.download(url, cache, true)) + if not fs.exists(cache) then + assert(fs.download(url, cache)) + end + + fs.delete(bin) + assert(fs.copy(cache, bin)) + + -- local ar = assert(zip.open(bin, zip.OR(zip.CHECKCONS))) + local ar = zip.open(bin) + + local infoPlistIndex = assert(ar:name_locate("love.app/Contents/Info.plist")) + local infoPlistSize = assert(ar:stat(infoPlistIndex).size) + local infoPlistHandle = assert(ar:open(infoPlistIndex)) + local infoPlist = assert(infoPlistHandle:read(infoPlistSize)) + infoPlistHandle:close() + infoPlist = infoPlist + :gsub("\n\t<key>UTExportedTypeDeclarations</key>.*</array>", + "") + :gsub("(CFBundleIdentifier.-<string>)(.-)(</string>)", + "%1"..project.identifier.."%3") + :gsub("(CFBundleName.-<string>)(.-)(</string>)", + "%1"..project.title..".love%3") + + ar:add("love.app/Contents/Resources/"..script.loveFile, + "file", script.loveFile) + + local app = project.title..".app" + for i = 1, #ar do + ar:rename(i, ar:stat(i).name:gsub("^love%.app", app)) + end + + ar:close() + + -- for unknown reason, replacing the Info.plist content earlier would cause + -- random crashes + ar = zip.open(bin) + assert(ar:replace(infoPlistIndex, "string", infoPlist)) + ar:close() + + os.rename(bin, project.title.."-macosx.zip") + + fs.pop_dir() +end + + +setmetatable(s, { + __call = function(_, project) return s.script(project) end, +}) + +return s diff --git a/src/scripts/windows.lua b/src/scripts/windows.lua new file mode 100644 index 0000000..db5ad21 --- /dev/null +++ b/src/scripts/windows.lua @@ -0,0 +1,87 @@ +--- Windows exe release. +-- @module scripts.windows +-- @usage windows(project) + +local fs = require "luarocks.fs" +local semver = require "semver" +local zip = require "brimworks.zip" +local Script = require "love-release.script" +local utils = require "love-release.utils" + +local s = {} + + +local function release(script, project, arch) + local prefix = "love-"..tostring(project.loveVersion).."-win" + local dir, bin + if project.loveVersion >= semver'0.9.0' then + bin = prefix..arch..".zip" + dir = prefix..arch.."/" + else + if arch == 32 then + bin = prefix.."-x86.zip" + dir = prefix.."-x86/" + elseif arch == 64 then + bin = prefix.."-x64.zip" + dir = prefix.."-x64/" + end + end + local url = "https://bitbucket.org/rude/love/downloads/"..bin + local cache = utils.cache.."/"..bin + + -- Can't cache the archive because luarocks functions use a HEAD request to + -- Amazon AWS which will answer a 403. + -- assert(fs.download(url, cache, true)) + if not fs.exists(cache) then + assert(fs.download(url, cache)) + end + + fs.delete(bin) + assert(fs.copy(cache, bin)) + + local gameHandle = assert(io.open(script.loveFile, "rb")) + local game = gameHandle:read("*a") + gameHandle:close() + + -- local ar = assert(zip.open(bin, zip.OR(zip.CHECKCONS))) + local ar = zip.open(bin) + + local exeHandle = assert(ar:open(dir.."love.exe")) + local exe = assert(exeHandle:read(assert(ar:stat(dir.."love.exe")).size)) + exeHandle:close() + + ar:add(dir..project.package..".exe", "string", exe..game) + ar:delete(dir.."love.exe") + + local stat + for i = 1, #ar do + stat = ar:stat(i) + if stat then + ar:rename(i, stat.name:gsub( + "^"..utils.lua.escape_string_regex(dir), + utils.lua.escape_string_regex(project.title).."-win"..arch.."/")) + end + end + + ar:close() + + os.rename(bin, project.title.."-win"..arch..".zip") +end + +function s.script(project) + local script = Script:new(project) + script:createLoveFile() + fs.change_dir(project.releaseDirectory) + release(script, project, 32) + if project.loveVersion >= semver'0.8.0' then + release(script, project, 64) + end + fs.pop_dir() +end + + +setmetatable(s, { + __call = function(_, project) return s.script(project) end, +}) + +return s diff --git a/src/utils.lua b/src/utils.lua new file mode 100644 index 0000000..01ee2d6 --- /dev/null +++ b/src/utils.lua @@ -0,0 +1,123 @@ +--- Provides utility functions and constants. +-- @module utils + +local cfg = require 'luarocks.cfg' +local fs = require 'luarocks.fs' +local semver = require 'semver' + +local utils = {} + + +--[[ CACHE ]]-- + +--- Cache directory. +utils.cache = nil + +do + local cache + if cfg.platforms.windows then + cache = os.getenv("APPDATA") + else + cache = os.getenv("HOME").."/.cache" + end + cache = fs.absolute_name(cache.."/love-release") + assert(fs.make_dir(cache)) + utils.cache = cache +end + + +--[[ LÖVE VERSION ]]-- + +utils.love = {} + +--- All supported LÖVE versions. +-- @local +utils.love.versionTable = { + semver'0.10.0', + semver'0.9.2', semver'0.9.1', semver'0.9.0', + semver'0.8.0', + semver'0.7.2', semver'0.7.1', semver'0.7.0', + semver'0.6.2', semver'0.6.1', semver'0.6.0', +--[[ + semver'0.5.0', + semver'0.4.0', + semver'0.3.2', semver'0.3.1', semver'0.3.0', + semver'0.2.1', semver'0.2.0', + semver'0.1.1', +--]] +} + +--- Last script LÖVE version. +function utils.love.lastVersion() + return utils.love.versionTable[1] +end + +--- First supported LÖVE version. +function utils.love.minVersion() + return utils.love.versionTable[#utils.love.versionTable] +end + +--- Checks if a LÖVE version exists and is supported. +-- @tparam semver version LÖVE version. +-- @treturn bool true is the version is supported. +function utils.love.isSupported(version) + if version >= utils.love.minVersion() + and version <= utils.love.lastVersion() then + for _, v in ipairs(utils.love.versionTable) do + if version == v then + return true + end + end + end + return false +end + +--[[ LUA ]]-- + +utils.lua = {} + +--- Compiles a file to LuaJIT bytecode. +-- @string file file path. +-- @treturn string bytecode. +function utils.lua.bytecode(file) + if package.loaded.jit then + return string.dump(assert(loadfile(file)), true) + else + local handle = io.popen('luajit -b '..file..' -') + local result = handle:read("*a") + handle:close() + return result + end +end + +--- Escapes a string to use as a regex. +-- @string string to escape. +function utils.lua.escape_string_regex(string) + -- ^$()%.[]*+-? + return string:gsub('%%', '%%%%'):gsub('^%^', '%%^'):gsub('%$$', '%%$') + :gsub('%(', '%%('):gsub('%)', '%%)'):gsub('%.', '%%.') + :gsub('%[', '%%['):gsub('%]', '%%]'):gsub('%*', '%%*') + :gsub('%+', '%%+'):gsub('%-', '%%-'):gsub('%?', '%%?') +end + + +--[[ IO ]]-- + +local stdout = io.output(io.stdout) +local stderr = io.output(io.stderr) +utils.io = {} + +--- Prints a message to stdout. +-- @string string the message. +function utils.io.out(string) + stdout:write(string) +end + +--- Prints a message to stderr. +-- @string string the message. +function utils.io.err(string) + stderr:write(string) +end + + +return utils