├── Makefile ├── README ├── README.md ├── game-to-flatpak ├── lib ├── flatpak-game.lua ├── libraries-cleanup.lua └── utils.lua ├── run-tests.lua └── tests ├── BaseballStars2.sh-config.lua ├── deb-download.html ├── gog_another_world_20th_anniversary_edition_2.0.0.2.sh-header ├── hello_2.9-2+deb8u1_amd64 ├── hello_2.9-2+deb8u1_arm64 ├── hello_2.9-2+deb8u1_i386 ├── i386 └── hello_2.9-2+deb8u1_i386 ├── libcaca.so.0 ├── libcaca0_0.99.beta17-2.1ubuntu2_amd64.deb ├── libcaca0_0.99.beta18-1ubuntu5_i386.deb ├── lugaru-full-linux-x86-1.0c.bin-config.luac ├── lugaru-full-linux-x86-1.0c.bin-header ├── mixed └── withparen(foo) │ ├── hello_2.9-2+deb8u1_amd64 │ └── hello_2.9-2+deb8u1_i386 └── x86_64 └── hello_2.9-2+deb8u1_amd64 /Makefile: -------------------------------------------------------------------------------- 1 | check: Makefile run-tests.lua 2 | lua run-tests.lua 3 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | README.md -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # game-to-flatpak 2 | 3 | A script to automatically convert Linux game installers in various 4 | formats to flatpak bundles. 5 | 6 | ## Requirements 7 | 8 | - lua 9 | - lua-posix 10 | - lua-socket 11 | - lua-archive ([from this location](https://github.com/hadess/lua-archive), [PR](https://github.com/brimworks/lua-archive/pull/2)) 12 | - luasec 13 | - unzip 14 | - flatpak-builder 15 | 16 | ## Features 17 | 18 | - Supports the MojoSetup installer 19 | - Supports the MojoSetup + makeself installer (as used by GOG.com) 20 | 21 | ## Usage 22 | 23 | This will add the game to the `repo` directory: 24 | ``` 25 | ./game-to-flatpak [installer file] 26 | ``` 27 | 28 | You will only need to do this once: 29 | ``` 30 | flatpak --user remote-add --no-gpg-verify --if-not-exists game-repo repo 31 | ``` 32 | 33 | Check which games are available in the repo: 34 | ``` 35 | flatpak --user remote-ls game-repo 36 | ``` 37 | 38 | Install the game for that user: 39 | ``` 40 | flatpak --user install game-repo com.gog.Call_of_Cthulhu__Shadow_of_the_Comet 41 | ``` 42 | 43 | ## Similar projects 44 | 45 | - [Unpacker classes](https://cgit.gentoo.org/proj/gamerlay.git/tree/eclass) from [Gentoo's gamerlay](https://cgit.gentoo.org/proj/gamerlay.git/) 46 | - [./play.it](http://wiki.dotslashplay.it/en/start)'s [Debianification scripts](http://www.dotslashplay.it/scripts/) 47 | - [flatpak-gog](https://github.com/kujeger/flatpak-gog/), a project with similar goals, written in Python 48 | 49 | ## Out of scope 50 | 51 | - WINE, DOSBox, etc. automagic wrappers are not planned, don't ask for them. 52 | -------------------------------------------------------------------------------- /game-to-flatpak: -------------------------------------------------------------------------------- 1 | #!/bin/lua 2 | 3 | --[[ 4 | * Copyright (C) 2016 Red Hat, Inc. 5 | * Author: Bastien Nocera 6 | * 7 | * This program is free software; you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation; either version 2 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with this program; if not, write to the Free Software 19 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 20 | * 21 | --]] 22 | 23 | local fg = require "lib.flatpak-game" 24 | 25 | function usage() 26 | print("game-to-flatpak [--network] [--keep-files] [--bundle] [FILE...]") 27 | end 28 | 29 | local file = nil 30 | local options = {} 31 | for k, v in ipairs(arg) do 32 | if string.sub (v, 1, 2) == "--" then 33 | if v == '--network' then 34 | options.network = true 35 | elseif v == '--keep-files' then 36 | options.keep_files = true 37 | elseif v == '--bundle' then 38 | options.bundle = true 39 | else 40 | print ("Unknown option '" .. v .. "'") 41 | usage() 42 | return 1 43 | end 44 | else 45 | file = v 46 | break 47 | end 48 | end 49 | 50 | if not file then 51 | usage() 52 | return 1 53 | end 54 | 55 | local ret = handle(file, options) 56 | return ret 57 | -------------------------------------------------------------------------------- /lib/flatpak-game.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | * Copyright (C) 2016 Red Hat, Inc. 3 | * Author: Bastien Nocera 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation; either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 18 | * 19 | --]] 20 | 21 | local io = require "io" 22 | require "lib.utils" 23 | require "lib.libraries-cleanup" 24 | 25 | TYPE_SNIFFING_BUFFER_SIZE = 256 * 1024 26 | ROOT_DIR = 'exploded-flatpak-game/' 27 | 28 | -- FIXME should be in separate modules 29 | function identify_gog(buffer) 30 | -- the GOG.com installers use MojoSetup inside a self-extracting 31 | -- to provide archive integrity checks 32 | if buffer:match('with modifications for mojosetup and GOG%.com installer%.') then 33 | return true 34 | end 35 | return false 36 | end 37 | 38 | function identify_mojo(buffer) 39 | if buffer:match('mojosetup,') and 40 | buffer:match('MOJOSETUP_LOGLEVEL') then 41 | return true 42 | end 43 | if buffer:match('using Makeself') and 44 | buffer:match('modifications for mojosetup') then 45 | return true 46 | end 47 | return false 48 | end 49 | 50 | function identify(file) 51 | local f = io.open(file, "rb") 52 | if not f then 53 | return nil 54 | end 55 | 56 | local buffer = f:read(TYPE_SNIFFING_BUFFER_SIZE) 57 | f:close() 58 | if identify_gog(buffer) then 59 | return 'gog' 60 | elseif identify_mojo(buffer) then 61 | return 'mojo' 62 | else 63 | return nil 64 | end 65 | end 66 | 67 | function verify_gog(file) 68 | local command = { 'sh', file, '--check' } 69 | local f = io.popen(shell_quote(command), 'r') 70 | if not f then 71 | return false 72 | end 73 | local s = f:read('*a') 74 | local exit_code = f:close() 75 | if exit_code or 76 | exit_code == 0 then 77 | return true 78 | end 79 | return false 80 | end 81 | 82 | function verify_mojo(file) 83 | local command = { 'unzip', '-t', file } 84 | local f = io.popen(shell_quote(command), 'r') 85 | if not f then 86 | return false 87 | end 88 | local s = f:read('*a') 89 | local exit_code = f:close() 90 | if exit_code or 91 | exit_code == 0 then 92 | return true 93 | end 94 | 95 | -- FIXME return true here until we can skip forward 96 | -- in the file to avoid errors 97 | return true 98 | end 99 | 100 | function verify(archive_type, file) 101 | if archive_type == 'gog' then 102 | return verify_gog(file) 103 | elseif archive_type == 'mojo' then 104 | return verify_mojo(file) 105 | else 106 | error('Can not verify unhandled archive_type ' .. (archive_type or '')) 107 | end 108 | end 109 | 110 | function unpack_mojo(file) 111 | -- FIXME replace with lua-archive: 112 | -- https://github.com/brimworks/lua-archive 113 | local command = { 'unzip', '-d', ROOT_DIR .. '/files/lib/game/', file } 114 | local f = io.popen(shell_quote(command), 'r') 115 | if not f then return false end 116 | f:read('*a') 117 | f:close() 118 | 119 | return true 120 | end 121 | 122 | function unpack(archive_type, file) 123 | if archive_type == 'gog' or 124 | archive_type == 'mojo' then 125 | return unpack_mojo(file) 126 | else 127 | error('Can not unpack unhandled archive_type ' .. (archive_type or '')) 128 | end 129 | end 130 | 131 | function get_id(metadata) 132 | local name = metadata.name:gsub('%W','_') 133 | return metadata.id_prefix .. '.' .. name 134 | end 135 | 136 | -- From https://rosettacode.org/wiki/Reverse_words_in_a_string#Lua 137 | function table.reverse(a) 138 | local res = {} 139 | for i = #a, 1, -1 do 140 | res[#res+1] = a[i] 141 | end 142 | return res 143 | end 144 | 145 | function splittokens(s) 146 | local res = {} 147 | for w in s:gmatch("%w+") do 148 | res[#res+1] = w 149 | end 150 | return res 151 | end 152 | 153 | function reverse_dns(vendor) 154 | local tokens = splittokens(vendor) 155 | return table.concat(table.reverse(tokens), '.') 156 | end 157 | 158 | function splitfields(s) 159 | local res = {} 160 | for w in s:gmatch("[%g ]+") do 161 | res[#res+1] = w 162 | end 163 | return res 164 | end 165 | 166 | function is_keyword(str) 167 | if str == 'vendor' or 168 | str == 'id' or 169 | str == 'description' or 170 | str == 'version' or 171 | str == 'name' or 172 | str == 'icon' or 173 | str == 'commandline' or 174 | str == 'category' then 175 | return true 176 | end 177 | return false 178 | end 179 | 180 | function get_mojo_executable(metadata) 181 | if not metadata or not metadata.orig_executable then return metadata end 182 | local no_prefix_executable = string.gsub(metadata.orig_executable, '%%0', '/lib/game/data/noarch/') 183 | -- FIXME we might have better luck finding the filename instead 184 | metadata.executable = string.gsub(metadata.orig_executable, '%%0', '/app/lib/game/data/noarch/') 185 | if not file_exists(ROOT_DIR .. '/files/' .. no_prefix_executable) then 186 | no_prefix_executable = string.gsub(metadata.orig_executable, '%%0', '/lib/game/data/') 187 | end 188 | 189 | metadata.executable = '/app' .. no_prefix_executable 190 | 191 | return metadata 192 | end 193 | 194 | function get_metadata_mojo_compiled_parse(data) 195 | local metadata = {} 196 | local tokens = splitfields(data) 197 | local vendor 198 | local skip_next = false 199 | for index,value in ipairs(tokens) do 200 | if skip_next then 201 | skip_next = false 202 | elseif value == 'vendor' and 203 | not is_keyword(tokens[index + 1]) then 204 | vendor = tokens[index + 1] 205 | elseif value == 'description' and 206 | not is_keyword(tokens[index + 1]) then 207 | metadata.name = tokens[index + 1] 208 | elseif value == 'version' and 209 | not is_keyword(tokens[index + 1]) then 210 | metadata.version = tokens[index + 1] 211 | elseif value == 'icon' and 212 | not is_keyword(tokens[index + 1]) then 213 | metadata.icon = tokens[index + 1] 214 | elseif value == 'commandline' and 215 | not is_keyword(tokens[index + 1]) then 216 | metadata.orig_executable = tokens[index + 1] 217 | end 218 | end 219 | 220 | metadata.id_prefix = reverse_dns(vendor) 221 | metadata.id = get_id(metadata) 222 | 223 | return metadata 224 | end 225 | 226 | function get_metadata_mojo_compiled(file) 227 | data = read_all(ROOT_DIR .. '/files/lib/game/scripts/config.luac') 228 | if not data then 229 | return nil 230 | end 231 | 232 | local metadata = get_metadata_mojo_compiled_parse(data) 233 | metadata.arch = get_arch_for_dir(ROOT_DIR) 234 | return metadata 235 | end 236 | 237 | function get_metadata_mojo_parse(data) 238 | local metadata = {} 239 | 240 | local vendor = data:match('vendor = "(.-)"') 241 | metadata.id_prefix = reverse_dns(vendor) 242 | metadata.name = data:match('game_title = "(.-)"') 243 | if not metadata.name then 244 | metadata.name = data:match("game_title = '(.-)'") 245 | end 246 | metadata.version = data:match('version = "(.-)"') 247 | metadata.orig_executable = data:match('commandline = "(.-)"') 248 | metadata.icon = data:match('icon = "(.-)"') 249 | metadata.id = get_id(metadata) 250 | 251 | return metadata 252 | end 253 | 254 | function get_metadata_mojo(file) 255 | data = read_all(ROOT_DIR .. '/files/lib/game/scripts/config.lua') 256 | if not data then 257 | return nil 258 | end 259 | 260 | metadata = get_metadata_mojo_parse(data) 261 | metadata.arch = get_arch_for_dir(ROOT_DIR) 262 | return metadata 263 | end 264 | 265 | function get_metadata(archive_type, file) 266 | local metadata 267 | 268 | if archive_type == 'gog' or 269 | archive_type == 'mojo' then 270 | metadata = get_metadata_mojo(file) 271 | if not metadata then 272 | metadata = get_metadata_mojo_compiled(file) 273 | end 274 | 275 | metadata = get_mojo_executable(metadata) 276 | else 277 | error('Can not get metadata for unhandled archive_type ' .. (archive_type or '')) 278 | end 279 | 280 | -- Verify that the metadata is complete 281 | if not metadata or 282 | not metadata.id_prefix or 283 | not metadata.name or 284 | not metadata.version or 285 | not metadata.executable or 286 | not metadata.icon or 287 | not metadata.arch then 288 | return nil 289 | end 290 | 291 | return metadata 292 | end 293 | 294 | 295 | function create_hier() 296 | return mkdir_with_parents (ROOT_DIR .. '/files/bin') and 297 | mkdir_with_parents (ROOT_DIR .. '/files/lib/game') and 298 | mkdir_with_parents (ROOT_DIR .. '/export/share/applications') and 299 | mkdir_with_parents (ROOT_DIR .. '/export/share/icons/') 300 | end 301 | 302 | function read_all(file) 303 | local f = io.open(file, "r") 304 | if not f then return nil end 305 | local t = f:read("*all") 306 | f:close() 307 | return t 308 | end 309 | 310 | function save_desktop(metadata) 311 | local f = io.open(ROOT_DIR .. '/export/share/applications/' .. metadata.id .. '.desktop', "w") 312 | if not f then return false end 313 | 314 | write_line(f, '[Desktop Entry]') 315 | write_line(f, 'Type=Application') 316 | write_line(f, 'Name=' .. metadata.name) 317 | write_line(f, 'Icon=' .. metadata.id) 318 | write_line(f, 'Exec=' .. metadata.executable) 319 | write_line(f, 'Categories=Game;') 320 | f:close() 321 | return true 322 | end 323 | 324 | function save_icon_mojo(metadata) 325 | path = ROOT_DIR .. 'files/lib/game/data/noarch/' .. metadata.icon 326 | if not file_exists(path) then 327 | path = ROOT_DIR .. 'files/lib/game/data/' .. metadata.icon 328 | end 329 | local ret = os.rename(path, ROOT_DIR .. 'export/share/icons/' .. metadata.id .. '.png') 330 | if not ret then return false end 331 | return true 332 | end 333 | 334 | function save_icon(archive_type, metadata) 335 | if archive_type == 'gog' or 336 | archive_type == 'mojo' then 337 | return save_icon_mojo(metadata) 338 | else 339 | error('Can not save icon for unhandled archive_type ' .. (archive_type or '')) 340 | end 341 | end 342 | 343 | function write_line(f, line) 344 | f:write(line .. '\n') 345 | end 346 | 347 | function save_manifest(metadata, enable_network) 348 | local f = io.open(ROOT_DIR .. '/metadata', "w") 349 | if not f then return false end 350 | 351 | -- http://flatpak.org/developer.html#Anatomy_of_a_Flatpak_App 352 | write_line(f, '[Application]') 353 | write_line(f, 'name=' .. metadata.id) 354 | write_line(f, 'runtime=org.freedesktop.Platform/' .. metadata.arch .. '/1.4') 355 | -- FIXME is that going to work? 356 | write_line(f, 'command=' .. metadata.executable) 357 | f:write('\n') 358 | 359 | write_line(f, '[Context]') 360 | if enable_network then 361 | write_line(f, 'shared=network;ipc;') 362 | else 363 | write_line(f, 'shared=ipc;') 364 | end 365 | write_line(f, 'sockets=x11;wayland;pulseaudio;') 366 | write_line(f, 'devices=dri;') 367 | f:write('\n'); 368 | 369 | write_line(f, '[Environment]') 370 | write_line(f, 'SDL_VIDEO_X11_WMCLASS=' .. metadata.id) 371 | write_line(f, 'SDL_VIDEO_WAYLAND_WMCLASS=' .. metadata.id) 372 | 373 | f:close() 374 | return true 375 | end 376 | 377 | function build_export() 378 | local command = { 'flatpak', 'build-export', 'repo', ROOT_DIR } 379 | local f = io.popen(shell_quote(command), 'r') 380 | if not f then 381 | return false 382 | end 383 | f:close() 384 | 385 | return true 386 | end 387 | 388 | function build_bundle(id, arch) 389 | local command = { 'flatpak', 'build-bundle', '--arch', arch, 'repo', id .. '.flatpak', id } 390 | local f = io.popen(shell_quote(command), 'r') 391 | if not f then 392 | return false 393 | end 394 | f:close() 395 | 396 | return true 397 | end 398 | 399 | function handle(file, options) 400 | local archive_type = identify(file) 401 | 402 | if archive_type == nil then 403 | print("Could not find archive type for file " .. file) 404 | return 1 405 | end 406 | 407 | if not create_hier() then 408 | print ("Could not create directory hierarchy") 409 | return 1 410 | end 411 | 412 | if not verify(archive_type, file) then 413 | print ("Failed to verify file " .. file) 414 | return 1 415 | end 416 | 417 | if not unpack(archive_type, file) then 418 | print ("Failed to unpack file " .. file) 419 | return 1 420 | end 421 | 422 | local metadata = get_metadata(archive_type, file) 423 | if not metadata then 424 | print ("Failed to gather metadata for file " .. file) 425 | return 1 426 | end 427 | 428 | -- FIXME make this optional 429 | local ret, err = add_missing_libcaca(ROOT_DIR, '1.4', metadata.arch) 430 | if not ret then 431 | print(err) 432 | return 1 433 | end 434 | local ret, err = add_missing_libslang(ROOT_DIR, '1.4', metadata.arch) 435 | if not ret then 436 | print(err) 437 | return 1 438 | end 439 | local ret, err = add_missing_libjack(ROOT_DIR, '1.4', metadata.arch) 440 | if not ret then 441 | print(err) 442 | return 1 443 | end 444 | 445 | if not save_manifest(metadata, options.network) then 446 | print ("Failed to create metadata file for " .. file) 447 | return 1 448 | end 449 | 450 | if not save_desktop(metadata) then 451 | print ("Failed to save desktop file for " .. file) 452 | return 1 453 | end 454 | 455 | if not save_icon(archive_type, metadata) then 456 | print ("Failed to save icon file for " .. file) 457 | return 1 458 | end 459 | 460 | -- FIXME cleanup 461 | 462 | if not build_export() then 463 | print ("Could not export build for " .. file) 464 | return 1 465 | end 466 | 467 | if options.bundle and not build_bundle(metadata.id, metadata.arch) then 468 | print ("Could not build bundle for " .. file) 469 | return 1 470 | end 471 | 472 | if not options.keep_files then 473 | remove_dir(ROOT_DIR) 474 | end 475 | end 476 | 477 | 478 | -------------------------------------------------------------------------------- /lib/libraries-cleanup.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | * Copyright (C) 2016 Red Hat, Inc. 3 | * Author: Bastien Nocera 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation; either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 18 | * 19 | --]] 20 | 21 | require "lib.utils" 22 | local archive = require "archive" 23 | 24 | -- From: https://packages.ubuntu.com/precise/libcaca0 25 | LIBCACA_URLS_PRECISE = { 26 | ["i386"] = 'https://packages.ubuntu.com/precise/i386/libcaca0/download', 27 | ['x86_64'] = 'https://packages.ubuntu.com/precise/amd64/libcaca0/download' 28 | } 29 | 30 | -- From: https://packages.ubuntu.com/trusty/libcaca0 31 | LIBCACA_URLS_TRUSTY = { 32 | ["i386"] = 'https://packages.ubuntu.com/trusty/i386/libcaca0/download', 33 | ['x86_64'] = 'https://packages.ubuntu.com/trusty/amd64/libcaca0/download' 34 | } 35 | 36 | LIBCACA_URLS = { 37 | ["1.2"] = LIBCACA_URLS_PRECISE, 38 | ["1.4"] = LIBCACA_URLS_TRUSTY 39 | } 40 | 41 | -- From: https://packages.ubuntu.com/precise/libslang2 42 | LIBSLANG_URLS_PRECISE = { 43 | ["i386"] = 'https://packages.ubuntu.com/precise/i386/libslang2/download', 44 | ['x86_64'] = 'https://packages.ubuntu.com/precise/amd64/libslang2/download' 45 | } 46 | 47 | -- From: https://packages.ubuntu.com/trusty/libslang2 48 | LIBSLANG_URLS_TRUSTY = { 49 | ["i386"] = 'https://packages.ubuntu.com/trusty/i386/libslang2/download', 50 | ['x86_64'] = 'https://packages.ubuntu.com/trusty/amd64/libslang2/download' 51 | } 52 | 53 | LIBSLANG_URLS = { 54 | ["1.2"] = LIBSLANG_URLS_PRECISE, 55 | ["1.4"] = LIBSLANG_URLS_TRUSTY 56 | } 57 | 58 | -- From: https://packages.ubuntu.com/precise/libjack0 59 | LIBJACK_URLS_PRECISE = { 60 | ["i386"] = 'https://packages.ubuntu.com/precise/i386/libjack0/download', 61 | ['x86_64'] = 'https://packages.ubuntu.com/precise/amd64/libjack0/download' 62 | } 63 | 64 | -- From: https://packages.ubuntu.com/trusty/libjack0 65 | LIBJACK_URLS_TRUSTY = { 66 | ["i386"] = 'https://packages.ubuntu.com/trusty/i386/libjack0/download', 67 | ['x86_64'] = 'https://packages.ubuntu.com/trusty/amd64/libjack0/download' 68 | } 69 | 70 | LIBJACK_URLS = { 71 | ["1.2"] = LIBJACK_URLS_PRECISE, 72 | ["1.4"] = LIBJACK_URLS_TRUSTY 73 | } 74 | 75 | function verify_missing_lib_args(root_dir, framework_version, arch) 76 | if not root_dir then 77 | return false, ('Missing root directory') 78 | end 79 | if framework_version ~= '1.4' and 80 | framework_version ~= '1.2' then 81 | return false, ('Unsupported framework version ' .. (framework_version or '')) 82 | end 83 | if arch ~= 'i386' and 84 | arch ~= 'x86_64' then 85 | return false, ('Unsupported architecture ' .. (arch or '')) 86 | end 87 | 88 | return true 89 | end 90 | 91 | function get_libcaca_dl_page_url(framework_version, arch) 92 | return LIBCACA_URLS[framework_version][arch] 93 | end 94 | 95 | function get_libslang_dl_page_url(framework_version, arch) 96 | return LIBSLANG_URLS[framework_version][arch] 97 | end 98 | 99 | function get_libjack_dl_page_url(framework_version, arch) 100 | return LIBJACK_URLS[framework_version][arch] 101 | end 102 | 103 | function parse_deb_download_page(body) 104 | return body:match('href="(http://mirrors%.kernel%.org.-)">') 105 | end 106 | 107 | function read_entry_data(ar) 108 | local ar_file_content = {} 109 | while true do 110 | local data = ar:data() 111 | if nil == data then break end 112 | ar_file_content[#ar_file_content + 1] = data 113 | end 114 | ar_file_content = table.concat(ar_file_content) 115 | 116 | return ar_file_content 117 | end 118 | 119 | function unpack_deb(data, fname_match) 120 | local read = false 121 | local function reader(ar) 122 | if read then 123 | return nil 124 | end 125 | return data 126 | end 127 | 128 | ar = archive.read { reader = reader } 129 | local header = ar:next_header() 130 | while header do 131 | if header:pathname() == 'data.tar.xz' or 132 | header:pathname() == 'data.tar.gz' then 133 | break 134 | end 135 | -- FIXME this is a bit fragile as next_header() 136 | -- returns an error for "ar" archives when reaching the 137 | -- end instead of an EOF 138 | header = ar:next_header() 139 | end 140 | if not header then return nil end 141 | 142 | data = read_entry_data(ar) 143 | -- New data gathered! 144 | ar:close() 145 | if not data then return nil end 146 | 147 | ar = archive.read { reader = reader } 148 | local header = ar:next_header() 149 | while header do 150 | path = header:pathname() 151 | if path:match(fname_match) then 152 | break 153 | end 154 | header = ar:next_header() 155 | end 156 | 157 | data = read_entry_data(ar) 158 | ar:close() 159 | 160 | return data 161 | end 162 | 163 | function unpack_libcaca_deb(data) 164 | return unpack_deb(data, '.-(libcaca%.so%.0%..-)$') 165 | end 166 | 167 | function unpack_libslang_deb(data) 168 | return unpack_deb(data, '.-(libslang%.so%.2%..-)$') 169 | end 170 | 171 | function unpack_libjack_deb(data) 172 | return unpack_deb(data, '.-(libjack%.so%.0%..-)$') 173 | end 174 | 175 | function add_missing_libcaca(root_dir, framework_version, arch) 176 | local ret, error = verify_missing_lib_args(root_dir, framework_version, arch) 177 | if not ret then 178 | return false, error 179 | end 180 | 181 | local url = get_libcaca_dl_page_url(framework_version, arch) 182 | local body = get_url(url) 183 | if not body then 184 | return false, 'Failed to get download page at ' .. url 185 | end 186 | local pkg_url = parse_deb_download_page(body) 187 | local pkg = get_url(pkg_url) 188 | local lib_data = unpack_libcaca_deb(pkg) 189 | 190 | local target_lib_dir = find_lib_dir(root_dir, arch) 191 | if not target_lib_dir then 192 | target_lib_dir = root_dir .. '/files/lib/' 193 | end 194 | local target_lib_path = target_lib_dir .. '/libcaca.so.0' 195 | local fd = io.output(target_lib_path) 196 | fd:write(lib_data) 197 | fd:close() 198 | 199 | return true 200 | end 201 | 202 | function add_missing_libslang(root_dir, framework_version, arch) 203 | local ret, error = verify_missing_lib_args(root_dir, framework_version, arch) 204 | if not ret then 205 | return false, error 206 | end 207 | 208 | local url = get_libslang_dl_page_url(framework_version, arch) 209 | local body = get_url(url) 210 | if not body then 211 | return false, 'Failed to get download page at ' .. url 212 | end 213 | local pkg_url = parse_deb_download_page(body) 214 | local pkg = get_url(pkg_url) 215 | local lib_data = unpack_libslang_deb(pkg) 216 | 217 | local target_lib_dir = find_lib_dir(root_dir, arch) 218 | if not target_lib_dir then 219 | target_lib_dir = root_dir .. '/files/lib/' 220 | end 221 | local target_lib_path = target_lib_dir .. '/libslang.so.2' 222 | local fd = io.output(target_lib_path) 223 | fd:write(lib_data) 224 | fd:close() 225 | 226 | return true 227 | end 228 | 229 | function add_missing_libjack(root_dir, framework_version, arch) 230 | local ret, error = verify_missing_lib_args(root_dir, framework_version, arch) 231 | if not ret then 232 | return false, error 233 | end 234 | 235 | local url = get_libjack_dl_page_url(framework_version, arch) 236 | local body = get_url(url) 237 | if not body then 238 | return false, 'Failed to get download page at ' .. url 239 | end 240 | local pkg_url = parse_deb_download_page(body) 241 | local pkg = get_url(pkg_url) 242 | local lib_data = unpack_libjack_deb(pkg) 243 | 244 | local target_lib_dir = find_lib_dir(root_dir, arch) 245 | if not target_lib_dir then 246 | target_lib_dir = root_dir .. '/files/lib/' 247 | end 248 | local target_lib_path = target_lib_dir .. '/libjack.so.0' 249 | local fd = io.output(target_lib_path) 250 | fd:write(lib_data) 251 | fd:close() 252 | 253 | return true 254 | end 255 | -------------------------------------------------------------------------------- /lib/utils.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | * Copyright (C) 2016 Red Hat, Inc. 3 | * Author: Bastien Nocera 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation; either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 18 | * 19 | --]] 20 | 21 | local posix = require "posix" 22 | local socket = require "socket" 23 | local http = require "socket.http" 24 | local https = require "ssl.https" 25 | 26 | -- From http://luci.subsignal.org/trac/browser/luci/trunk/libs/core/luasrc/fs.lua?rev=4103 27 | --- Create a new directory, recursively on demand. 28 | -- @param path String with the name or path of the directory to create 29 | -- @param recursive Create multiple directory levels (optional, default is true) 30 | -- @return Number with the return code, 0 on sucess or nil on error 31 | -- @return String containing the error description on error 32 | -- @return Number containing the os specific errno on error 33 | function mkdir(path, recursive) 34 | if recursive then 35 | local base = "." 36 | 37 | if path:sub(1,1) == "/" then 38 | base = "" 39 | path = path:gsub("^/+","") 40 | end 41 | 42 | for elem in path:gmatch("([^/]+)/*") do 43 | base = base .. "/" .. elem 44 | 45 | local stat = posix.stat( base ) 46 | 47 | if not stat then 48 | local stat, errmsg, errno = posix.mkdir( base ) 49 | 50 | if type(stat) ~= "number" or stat ~= 0 then 51 | return stat, errmsg, errno 52 | end 53 | else 54 | if stat.type ~= "directory" then 55 | return nil, base .. ": File exists", 17 56 | end 57 | end 58 | end 59 | 60 | return 0 61 | else 62 | return posix.mkdir( path ) 63 | end 64 | end 65 | 66 | function mkdir_with_parents(path) 67 | return mkdir (path, true) 68 | end 69 | 70 | function file_exists(path) 71 | local stat = posix.stat(path) 72 | if not stat then 73 | return false 74 | end 75 | return stat.type == 'regular' 76 | end 77 | 78 | function get_url(url) 79 | local body, code, headers 80 | if url:match('https%:.-') then 81 | body, code, headers = https.request(url) 82 | elseif url:match('http%:.-') then 83 | body, code, headers = http.request(url) 84 | else 85 | return nil 86 | end 87 | 88 | if code == 200 then 89 | return body 90 | else 91 | return nil 92 | end 93 | end 94 | 95 | function get_stdout(command) 96 | local f = io.popen(command, 'r') 97 | if not f then return nil end 98 | local s = f:read('*a') 99 | f:close() 100 | return s 101 | end 102 | 103 | function get_arch_for_path(path) 104 | local command = { "file", path } 105 | out = get_stdout(shell_quote(command)) 106 | if not out then return nil end 107 | if out:match('ELF 32%-bit.-Intel 80386') then 108 | return 'i386' 109 | elseif out:match('ELF 64%-bit.-x86%-64') then 110 | return 'x86_64' 111 | end 112 | return nil 113 | end 114 | 115 | function get_arch_for_dir(dir) 116 | local files = posix.dir(dir) 117 | local ret = nil 118 | for i, name in ipairs(files) do 119 | if name ~= '.' and name ~= '..' then 120 | local full_name = string.format('%s/%s', dir, name) 121 | local info = posix.stat(full_name) 122 | local new_ret = nil 123 | if info and info.type == 'directory' then 124 | new_ret = get_arch_for_dir(full_name) 125 | elseif info and info.type == 'regular' then 126 | new_ret = get_arch_for_path(full_name) 127 | end 128 | 129 | if new_ret ~= nil then 130 | if ret == 'i386' or 131 | ret == nil then 132 | ret = new_ret 133 | elseif new_ret == 'x86_64' then 134 | ret = new_ret 135 | break 136 | end 137 | end 138 | end 139 | end 140 | 141 | return ret 142 | end 143 | 144 | -- Similar to get_arch_for_dir() 145 | function find_lib_dir(dir, arch) 146 | if not arch then 147 | error('arch is empty') 148 | end 149 | 150 | local ret = nil 151 | local files = posix.dir(dir) 152 | for i, name in ipairs(files) do 153 | if name ~= '.' and name ~= '..' then 154 | local full_name = string.format('%s/%s', dir, name) 155 | local info = posix.stat(full_name) 156 | if info and info.type == 'directory' then 157 | local libdir = find_lib_dir(full_name, arch) 158 | if libdir ~= nil then 159 | ret = libdir 160 | break 161 | end 162 | elseif info and info.type == 'regular' and name:match('lib.-%.so.-') then 163 | found_arch = get_arch_for_path(full_name) 164 | if found_arch == arch then 165 | ret = dir 166 | end 167 | end 168 | end 169 | end 170 | 171 | return ret 172 | end 173 | 174 | function remove_dir(dir) 175 | local files = posix.dir(dir) 176 | for i, name in ipairs(files) do 177 | if name ~= '.' and name ~= '..' then 178 | local full_name = string.format('%s/%s', dir, name) 179 | local info = posix.stat(full_name) 180 | if info and info.type == 'directory' then 181 | remove_dir(full_name) 182 | elseif info then 183 | posix.unlink(full_name) 184 | end 185 | end 186 | end 187 | posix.rmdir(dir) 188 | end 189 | 190 | function shell_quote(...) 191 | local command = type(...) == 'table' and ... or { ... } 192 | for i, s in ipairs(command) do 193 | s = (tostring(s) or ''):gsub("'", "'\\''") 194 | s = "'" .. s .. "'" 195 | command[i] = s 196 | end 197 | return table.concat(command, ' ') 198 | end 199 | -------------------------------------------------------------------------------- /run-tests.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | * Copyright (C) 2016 Red Hat, Inc. 3 | * Author: Bastien Nocera 4 | * 5 | * This program is free software; you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation; either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program; if not, write to the Free Software 17 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 18 | * 19 | --]] 20 | 21 | local fg = require "lib.flatpak-game" 22 | local posix = require "posix" 23 | 24 | -- Test the luac metadata parser 25 | data = read_all('tests/lugaru-full-linux-x86-1.0c.bin-config.luac') 26 | local metadata = get_metadata_mojo_compiled_parse(data) 27 | assert(metadata) 28 | assert(metadata.id_prefix == 'com.wolfire') 29 | assert(metadata.version == '1.0c') 30 | 31 | -- Test the lua metadata parser 32 | data = read_all('tests/BaseballStars2.sh-config.lua') 33 | local metadata = get_metadata_mojo_parse(data) 34 | assert(metadata) 35 | assert(metadata.id_prefix == 'com.dotemu') 36 | assert(metadata.version == '1.00') 37 | assert(metadata.name == 'Baseball Stars 2') 38 | 39 | -- Test the identification 40 | assert(identify('tests/gog_another_world_20th_anniversary_edition_2.0.0.2.sh-header') == 'gog') 41 | assert(identify('tests/lugaru-full-linux-x86-1.0c.bin-header') == 'mojo') 42 | 43 | -- Test the reverse DNS 44 | assert(reverse_dns('gog.com') == 'com.gog') 45 | assert(reverse_dns('wolfire.com') == 'com.wolfire') 46 | 47 | -- Test file exists 48 | assert(file_exists('run-tests.lua')) 49 | assert(not file_exists('DOES NOT EXIST.lua')) 50 | 51 | -- Add and remove a directory 52 | assert(mkdir_with_parents('tests/foo/bar/baz/foobazbar') == 0) 53 | local fd = io.output('tests/foo/bar/baz/foobazbar/contents') 54 | fd:write('full of contents') 55 | fd:close() 56 | remove_dir('tests/foo') 57 | assert(not posix.stat('tests/foo')) 58 | 59 | -- Architecture detection 60 | assert(get_arch_for_path('tests/hello_2.9-2+deb8u1_amd64') == 'x86_64') 61 | assert(get_arch_for_path('tests/hello_2.9-2+deb8u1_arm64') == nil) 62 | assert(get_arch_for_path('tests/hello_2.9-2+deb8u1_i386') == 'i386') 63 | 64 | assert(get_arch_for_dir('tests/x86_64') == 'x86_64') 65 | assert(get_arch_for_dir('tests/mixed') == 'x86_64') 66 | assert(get_arch_for_dir('tests/i386') == 'i386') 67 | 68 | -- Verify deb patch URLs 69 | assert(verify_missing_lib_args('bleh', '1.4', 'x86_64')) 70 | local ret, error = verify_missing_lib_args('bleh', '1.1', 'x86_64') 71 | assert(not ret) 72 | assert(error == 'Unsupported framework version 1.1') 73 | ret, error = verify_missing_lib_args('bleh', '1.2', 'super8') 74 | assert(not ret) 75 | assert(error == 'Unsupported architecture super8') 76 | assert(get_libcaca_dl_page_url('1.2', 'x86_64')) 77 | 78 | local body = read_all('tests/deb-download.html') 79 | assert(parse_deb_download_page(body) == 'http://mirrors.kernel.org/ubuntu/pool/main/libc/libcaca/libcaca0_0.99.beta18-1ubuntu5_i386.deb') 80 | 81 | local deb = read_all('tests/libcaca0_0.99.beta18-1ubuntu5_i386.deb') 82 | local lib = unpack_libcaca_deb(deb) 83 | local lib_ref = read_all('tests/libcaca.so.0') 84 | assert (#lib == 813364) 85 | assert (#lib == #lib_ref) 86 | assert(lib_ref == lib) 87 | 88 | local deb = read_all('tests/libcaca0_0.99.beta17-2.1ubuntu2_amd64.deb') 89 | local lib = unpack_libcaca_deb(deb) 90 | assert(lib) 91 | 92 | assert(find_lib_dir('tests', 'i386') == 'tests') 93 | 94 | -- Shell quoting 95 | -- Tests ported from GLib 96 | assert(shell_quote("") == "''") 97 | assert(shell_quote("a") == "'a'") 98 | assert(shell_quote("(") == "'('") 99 | assert(shell_quote("'a") == "''\\''a'") 100 | assert(shell_quote("'") == "''\\'''") 101 | assert(shell_quote("a'") == "'a'\\'''") 102 | assert(shell_quote("a'a") == "'a'\\''a'") 103 | 104 | -- Won't work offline 105 | -- assert(get_url('http://packages.ubuntu.com/precise/amd64/libcaca0/download')) 106 | -------------------------------------------------------------------------------- /tests/BaseballStars2.sh-config.lua: -------------------------------------------------------------------------------- 1 | local NOARCH_INSTALL_SIZE = 23980270 -- Change to the size of the noarch folder in bytes 2 | local X86_INSTALL_SIZE = 7297307 -- Change to the size of the x86 folder in bytes 3 | local X86_64_INSTALL_SIZE = 5417530 -- Change to the size of the x86_64 folder in bytes (if you have it.. otherwise 0) 4 | local _ = MojoSetup.translate 5 | 6 | -- force 32bit only 7 | local force32bit = X86_64_INSTALL_SIZE == 0 -- this forces 32bit if we have no x86_64 dir 8 | local is32bit = force32bit or 9 | MojoSetup.cmdline("32bit") or 10 | MojoSetup.info.machine == "x86" or 11 | MojoSetup.info.machine == "i386" or 12 | MojoSetup.info.machine == "i586" or 13 | MojoSetup.info.machine == "i686" 14 | 15 | local game_title = 'Baseball Stars 2' -- Change this to the full game name 16 | 17 | Setup.Package 18 | { 19 | vendor = "dotemu.com", -- DNS domain of the game 20 | id = "BaseballStars2", -- short ID of the game name (the install folder) 21 | description = game_title, 22 | version = "1.00", 23 | -- splash = "splash.png", -- an image in the meta folder to display. 24 | splashpos = "top", 25 | superuser = false, 26 | write_manifest = true, 27 | support_uninstall = true, 28 | recommended_destinations = 29 | { 30 | MojoSetup.info.homedir, 31 | "/opt/games", 32 | "/usr/local/games" 33 | }, 34 | 35 | -- Setup.Eula -- add a EULA by uncommenting this section source is relative to data/ 36 | -- { 37 | -- description = _("EULA"), 38 | -- source = _("EULA/en.txt"), 39 | -- }, 40 | 41 | -- Setup.Readme -- a readme file (can occur multiple times) 42 | -- { 43 | -- description = _("Readme"), 44 | -- source = _("noarch/README.linux") 45 | -- }, 46 | 47 | Setup.Option 48 | { 49 | value = true, 50 | required = true, 51 | disabled = false, 52 | bytes = NOARCH_INSTALL_SIZE, 53 | description = game_title, 54 | 55 | Setup.OptionGroup 56 | { 57 | description = _("CPU Architecture"), 58 | Setup.Option 59 | { 60 | value = is32bit, 61 | required = is32bit, 62 | disabled = false, 63 | bytes = X86_INSTALL_SIZE, 64 | description = "x86", 65 | Setup.File 66 | { 67 | wildcards = "x86/*"; 68 | filter = function(fn) 69 | return string.gsub(fn, "^x86/", "", 1), nil 70 | end 71 | }, 72 | Setup.DesktopMenuItem 73 | { 74 | disabled = false, 75 | name = game_title, 76 | genericname = game_title, 77 | tooltip = _(game_title), 78 | builtin_icon = false, 79 | icon = "BaseballStars2.png", -- Change the PNG Icon (relative to install dir) 80 | commandline = "%0/NeogeoEmu.bin.x86", -- Executable name (%0 is install dir) 81 | workingdir = "%0", 82 | category = "Game;" 83 | }, 84 | }, 85 | Setup.Option 86 | { 87 | value = not is32bit, 88 | required = false, 89 | disabled = is32bit, 90 | bytes = X86_64_INSTALL_SIZE, 91 | description = "x86_64", 92 | Setup.File 93 | { 94 | wildcards = "x86_64/*"; 95 | filter = function(fn) 96 | return string.gsub(fn, "^x86_64/", "", 1), nil 97 | end 98 | }, 99 | Setup.DesktopMenuItem 100 | { 101 | disabled = false, 102 | name = game_title, 103 | genericname = game_title, 104 | tooltip = _(game_title), 105 | builtin_icon = false, 106 | icon = "BaseballStars2.png", -- Change the PNG Icon (relative to install dir) 107 | commandline = "%0/NeogeoEmu.bin.x86_64", -- Executable name (%0 is install dir) 108 | workingdir = "%0", 109 | category = "Game;" 110 | }, 111 | }, 112 | }, 113 | 114 | Setup.File 115 | { 116 | wildcards = "noarch/*"; 117 | filter = function(fn) 118 | return string.gsub(fn, "^noarch/", "", 1), nil 119 | end 120 | }, 121 | } 122 | } 123 | 124 | -- end of config.lua ... 125 | -------------------------------------------------------------------------------- /tests/deb-download.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Ubuntu – Package Download Selection -- libcaca0_0.99.beta18-1ubuntu5_i386.deb 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 27 | 28 |
29 | » Ubuntu 30 | » Packages 31 | 32 | » trusty 33 | 34 | » libcaca0 35 | 36 | » i386 37 | 38 | » Download 39 | 40 |
41 | 42 |
43 | 44 | 45 | 46 | 47 | 48 |

Download Page for libcaca0_0.99.beta18-1ubuntu5_i386.deb on Intel x86 machines

49 | 50 | 51 |
52 |

If you are running Ubuntu, it is strongly suggested to use a package manager like aptitude or synaptic to download and install packages, instead of doing so manually via this website.

53 |

You should be able to use any of the listed mirrors by adding a line to your /etc/apt/sources.list like this:

54 | 55 |
 56 | deb http://cz.archive.ubuntu.com/ubuntu trusty main 
 57 | 
58 |

Replacing cz.archive.ubuntu.com/ubuntu with the mirror in question. 59 | 60 | 61 |

62 | 63 | 64 |

You can download the requested file from the pool/main/libc/libcaca/ subdirectory at any of these sites:

65 |
66 | 67 | 68 |

North America

69 | 96 | 97 | 98 | 99 | 100 | 101 |

Asia

102 | 119 | 120 | 121 | 122 |

Africa

123 | 128 | 129 | 130 |
185 | 186 | 187 |
188 | 189 | 190 |

If none of the above sites are fast enough for you, please see our complete mirror list.

191 | 192 | 193 | 194 | 195 |

Note that in some browsers you will need to tell your browser you want the file saved to a file. For example, in Firefox or Mozilla, you should hold the Shift key when you click on the URL.

196 | 197 |
198 | 199 |

More information on libcaca0_0.99.beta18-1ubuntu5_i386.deb:

200 | 201 | 202 | 203 | 204 | 205 |
Exact Size 201664 Byte (196.9 kByte)
MD5 checksum a13c2e6d67550d0f5e094f63aba2c4cb
SHA1 checksum 2afb6ea53016d7665f3f2520cac556b2eb6a4e51
SHA256 checksum ce65423f431da890b0dec2a5a67b2ec8fbe67d98b6768161b7e7398274dd3f24
206 |
207 |
208 | 209 |