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)
+
+[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\tUTExportedTypeDeclarations.*",
+ "")
+ :gsub("(CFBundleIdentifier.-)(.-)()",
+ "%1"..project.identifier.."%3")
+ :gsub("(CFBundleName.-)(.-)()",
+ "%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