flatpaker 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flatpaker/__init__.py +15 -0
- flatpaker/config.py +38 -0
- flatpaker/entry.py +58 -0
- flatpaker/impl/__init__.py +0 -0
- flatpaker/impl/renpy.py +191 -0
- flatpaker/impl/rpgmaker.py +75 -0
- flatpaker/util.py +263 -0
- flatpaker-0.0.1.dist-info/LICENSE +21 -0
- flatpaker-0.0.1.dist-info/METADATA +150 -0
- flatpaker-0.0.1.dist-info/RECORD +12 -0
- flatpaker-0.0.1.dist-info/WHEEL +4 -0
- flatpaker-0.0.1.dist-info/entry_points.txt +3 -0
flatpaker/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright © 2024 Dylan Baker
|
|
3
|
+
|
|
4
|
+
"""Utilities to convert various kinds of native binaries into flatpaks.
|
|
5
|
+
|
|
6
|
+
Current support includes Ren'Py and some versions of RPGMaker (MV and MZ, when
|
|
7
|
+
they have Linux packages).
|
|
8
|
+
|
|
9
|
+
Attempts to make some optimizations of the packages, such as recompiling
|
|
10
|
+
bytecode, and patches various games to honor XDG variables, so that flatpaks
|
|
11
|
+
don't need access to the user home directory. This increases security and is
|
|
12
|
+
generally beneficial for end users.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
__version__ = "0.0.1"
|
flatpaker/config.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright © 2024 Dylan Baker
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
import os
|
|
6
|
+
import typing
|
|
7
|
+
|
|
8
|
+
from flatpaker.util import tomllib
|
|
9
|
+
|
|
10
|
+
if typing.TYPE_CHECKING:
|
|
11
|
+
|
|
12
|
+
Common = typing.TypedDict(
|
|
13
|
+
'Common',
|
|
14
|
+
{
|
|
15
|
+
'gpg-key': str,
|
|
16
|
+
'repo': str,
|
|
17
|
+
},
|
|
18
|
+
total=False,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
class Config(typing.TypedDict):
|
|
22
|
+
common: Common
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def load_config() -> Config:
|
|
26
|
+
root = os.environ.get('XDG_CONFIG_HOME', os.path.expanduser('~/.config'))
|
|
27
|
+
conf = os.path.join(root, 'flatpaker', 'config.toml')
|
|
28
|
+
raw: typing.Dict[str, typing.Any]
|
|
29
|
+
if os.path.exists(conf):
|
|
30
|
+
with open(conf, 'rb') as f:
|
|
31
|
+
raw = tomllib.load(f)
|
|
32
|
+
assert isinstance(raw, dict), 'invalid config file?'
|
|
33
|
+
else:
|
|
34
|
+
raw = {}
|
|
35
|
+
|
|
36
|
+
if 'common' not in raw:
|
|
37
|
+
raw['common'] = {}
|
|
38
|
+
return typing.cast('Config', raw)
|
flatpaker/entry.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright © 2022-2024 Dylan Baker
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
import argparse
|
|
6
|
+
import importlib
|
|
7
|
+
import pathlib
|
|
8
|
+
import typing
|
|
9
|
+
|
|
10
|
+
import flatpaker.config
|
|
11
|
+
import flatpaker.util
|
|
12
|
+
|
|
13
|
+
if typing.TYPE_CHECKING:
|
|
14
|
+
JsonWriterImpl = typing.Callable[[flatpaker.util.Description, pathlib.Path, str, pathlib.Path, pathlib.Path], None]
|
|
15
|
+
|
|
16
|
+
class ImplMod(typing.Protocol):
|
|
17
|
+
|
|
18
|
+
write_rules: JsonWriterImpl
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def select_impl(name: typing.Literal['renpy', 'rpgmaker']) -> JsonWriterImpl:
|
|
22
|
+
mod = typing.cast('ImplMod', importlib.import_module(name, 'flatpaker.impl'))
|
|
23
|
+
assert hasattr(mod, 'write_rules'), 'should be good enough'
|
|
24
|
+
return mod.write_rules
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def main() -> None:
|
|
28
|
+
config = flatpaker.config.load_config()
|
|
29
|
+
parser = argparse.ArgumentParser()
|
|
30
|
+
parser.add_argument('description', help="A Toml description file")
|
|
31
|
+
parser.add_argument(
|
|
32
|
+
'--repo',
|
|
33
|
+
default=config['common'].get('repo', 'repo'),
|
|
34
|
+
action='store',
|
|
35
|
+
help='a flatpak repo to put the result in')
|
|
36
|
+
parser.add_argument(
|
|
37
|
+
'--gpg',
|
|
38
|
+
default=config['common'].get('gpg-key'),
|
|
39
|
+
action='store',
|
|
40
|
+
help='A GPG key to sign the output to when writing to a repo')
|
|
41
|
+
parser.add_argument('--export', action='store_true', help='Export to the provided repo')
|
|
42
|
+
parser.add_argument('--install', action='store_true', help="Install for the user (useful for testing)")
|
|
43
|
+
parser.add_argument('--no-cleanup', action='store_false', dest='cleanup', help="don't delete the temporary directory")
|
|
44
|
+
args = typing.cast('flatpaker.util.Arguments', parser.parse_args())
|
|
45
|
+
# Don't use type for this because it swallows up the exception
|
|
46
|
+
description = flatpaker.util.load_description(args.description)
|
|
47
|
+
|
|
48
|
+
# TODO: This could be common
|
|
49
|
+
appid = f"{description['common']['reverse_url']}.{flatpaker.util.sanitize_name(description['common']['name'])}"
|
|
50
|
+
|
|
51
|
+
write_build_rules = select_impl(description['common']['engine'])
|
|
52
|
+
|
|
53
|
+
with flatpaker.util.tmpdir(description['common']['name'], args.cleanup) as d:
|
|
54
|
+
wd = pathlib.Path(d)
|
|
55
|
+
desktop_file = flatpaker.util.create_desktop(description, wd, appid)
|
|
56
|
+
appdata_file = flatpaker.util.create_appdata(description, wd, appid)
|
|
57
|
+
write_build_rules(description, wd, appid, desktop_file, appdata_file)
|
|
58
|
+
flatpaker.util.build_flatpak(args, wd, appid)
|
|
File without changes
|
flatpaker/impl/renpy.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright © 2022-2024 Dylan Baker
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import pathlib
|
|
8
|
+
import textwrap
|
|
9
|
+
import typing
|
|
10
|
+
|
|
11
|
+
from flatpaker import util
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _create_game_sh(use_x11: bool) -> str:
|
|
15
|
+
lines: typing.List[str] = [
|
|
16
|
+
'#!/usr/bin/env sh',
|
|
17
|
+
'',
|
|
18
|
+
'export RENPY_PERFORMANCE_TEST=0',
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
if not use_x11:
|
|
22
|
+
lines.append('export SDL_VIDEODRIVER=wayland')
|
|
23
|
+
|
|
24
|
+
lines.extend([
|
|
25
|
+
'cd /app/lib/game',
|
|
26
|
+
'exec sh *.sh',
|
|
27
|
+
])
|
|
28
|
+
|
|
29
|
+
return '\n'.join(lines)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def quote(s: str) -> str:
|
|
33
|
+
return f'"{s}"'
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def bd_game(description: util.Description) -> typing.Dict[str, typing.Any]:
|
|
37
|
+
sh = _create_game_sh(description.get('workarounds', {}).get('use_x11', True))
|
|
38
|
+
return {
|
|
39
|
+
'buildsystem': 'simple',
|
|
40
|
+
'name': 'game_sh',
|
|
41
|
+
'sources': [],
|
|
42
|
+
'build-commands': [
|
|
43
|
+
'mkdir -p /app/bin',
|
|
44
|
+
f"echo '{sh}' > /app/bin/game.sh",
|
|
45
|
+
'chmod +x /app/bin/game.sh'
|
|
46
|
+
],
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def bd_build_commands(description: util.Description) -> typing.List[str]:
|
|
51
|
+
commands: typing.List[str] = [
|
|
52
|
+
'mkdir -p /app/lib/game',
|
|
53
|
+
|
|
54
|
+
# install the main game files
|
|
55
|
+
'mv *.sh *.py renpy game lib /app/lib/game/',
|
|
56
|
+
|
|
57
|
+
# Move archives that have not been strippped as they would conflict
|
|
58
|
+
# with the main source archive
|
|
59
|
+
'cp -r */game/* /app/lib/game/game/ || true',
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
# Insert these commands before any rpy and py files are compiled
|
|
63
|
+
for p in description.get('sources', {}).get('files', []):
|
|
64
|
+
dest = os.path.join('/app/lib/game', p.get('dest', 'game'))
|
|
65
|
+
commands.extend([
|
|
66
|
+
f'mkdir -p {os.path.dirname(dest)}',
|
|
67
|
+
f'mv {p["path"].name} {dest}',
|
|
68
|
+
])
|
|
69
|
+
|
|
70
|
+
commands.extend([
|
|
71
|
+
# Patch the game to not require sandbox access
|
|
72
|
+
'''sed -i 's@"~/.renpy/"@os.environ.get("XDG_DATA_HOME", "~/.local/share") + "/"@g' /app/lib/game/*.py''',
|
|
73
|
+
|
|
74
|
+
# Recompile all of the rpy files
|
|
75
|
+
textwrap.dedent('''
|
|
76
|
+
pushd /app/lib/game;
|
|
77
|
+
script="$PWD/$(ls *.sh)";
|
|
78
|
+
dirs="$(find . -type f -name '*.rpy' -printf '%h\\0' | sort -zu | sed -z 's@$@ @')";
|
|
79
|
+
for d in $dirs; do
|
|
80
|
+
bash $script $d compile --keep-orphan-rpyc;
|
|
81
|
+
done;
|
|
82
|
+
popd;
|
|
83
|
+
'''),
|
|
84
|
+
|
|
85
|
+
# Recompile all python py files, so we can remove the py files
|
|
86
|
+
# form the final distribution
|
|
87
|
+
#
|
|
88
|
+
# Use -f to force the files mtimes to be updated, otherwise
|
|
89
|
+
# flatpak-builder will delete them as "stale"
|
|
90
|
+
#
|
|
91
|
+
# Use -b for python3 to allow us to delete the .py files
|
|
92
|
+
# I have run into a couple of python2 based ren'py programs that lack
|
|
93
|
+
# the python infrastructure to run with -m, so we'll just open code it to
|
|
94
|
+
# make it more portable
|
|
95
|
+
textwrap.dedent('''
|
|
96
|
+
pushd /app/lib/game;
|
|
97
|
+
if [ -d "lib/py3-linux-x86_64" ]; then
|
|
98
|
+
lib/py3-linux-x86_64/python -m compileall -b -f . || exit 1;
|
|
99
|
+
else
|
|
100
|
+
lib/linux-x86_64/python -c 'import compileall; compileall.main()' -f . || exit 1;
|
|
101
|
+
fi;
|
|
102
|
+
popd;
|
|
103
|
+
''')
|
|
104
|
+
])
|
|
105
|
+
|
|
106
|
+
return commands
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def bd_icon(description: util.Description, appid: str) -> typing.Dict[str, typing.Any]:
|
|
110
|
+
icon_src = '/app/lib/game/game/gui/window_icon.png'
|
|
111
|
+
icon_dst = f'/app/share/icons/hicolor/256x256/apps/{appid}.png'
|
|
112
|
+
# Must at least be before the appdata is generated
|
|
113
|
+
|
|
114
|
+
_icon_install_cmd: str
|
|
115
|
+
if description.get('workarounds', {}).get('icon_is_webp'):
|
|
116
|
+
_icon_install_cmd = f'dwebp {icon_src} -o {icon_dst}'
|
|
117
|
+
else:
|
|
118
|
+
_icon_install_cmd = f'cp {icon_src} {icon_dst}'
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
'buildsystem': 'simple',
|
|
122
|
+
'name': 'icon',
|
|
123
|
+
'sources': [],
|
|
124
|
+
'build-commands': [
|
|
125
|
+
'mkdir -p /app/share/icons/hicolor/256x256/apps/',
|
|
126
|
+
_icon_install_cmd,
|
|
127
|
+
],
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def write_rules(description: util.Description, workdir: pathlib.Path, appid: str, desktop_file: pathlib.Path, appdata_file: pathlib.Path) -> None:
|
|
132
|
+
sources = util.extract_sources(description)
|
|
133
|
+
|
|
134
|
+
# TODO: typing requires more thought
|
|
135
|
+
modules: typing.List[typing.Dict[str, typing.Any]] = [
|
|
136
|
+
{
|
|
137
|
+
'buildsystem': 'simple',
|
|
138
|
+
'name': util.sanitize_name(description['common']['name']),
|
|
139
|
+
'sources': sources,
|
|
140
|
+
'build-commands': bd_build_commands(description),
|
|
141
|
+
'cleanup': [
|
|
142
|
+
'*.exe',
|
|
143
|
+
'*.app',
|
|
144
|
+
'*.rpyc.bak',
|
|
145
|
+
'*.txt',
|
|
146
|
+
'*.rpy',
|
|
147
|
+
'/lib/game/lib/*darwin-*',
|
|
148
|
+
'/lib/game/lib/*windows-*',
|
|
149
|
+
'/lib/game/lib/*-i686',
|
|
150
|
+
],
|
|
151
|
+
},
|
|
152
|
+
]
|
|
153
|
+
if not description.get('workarounds', {}).get('icon', False):
|
|
154
|
+
modules.append(bd_icon(description, appid))
|
|
155
|
+
modules.extend([
|
|
156
|
+
bd_game(description),
|
|
157
|
+
util.bd_desktop(desktop_file),
|
|
158
|
+
util.bd_appdata(appdata_file),
|
|
159
|
+
])
|
|
160
|
+
|
|
161
|
+
if description.get('workarounds', {}).get('use_x11', True):
|
|
162
|
+
finish_args = ['--socket=x11']
|
|
163
|
+
else:
|
|
164
|
+
finish_args = ['--socket=wayland', '--socket=fallback-x11']
|
|
165
|
+
|
|
166
|
+
struct = {
|
|
167
|
+
'sdk': 'org.freedesktop.Sdk',
|
|
168
|
+
'runtime': 'org.freedesktop.Platform',
|
|
169
|
+
'runtime-version': util.RUNTIME_VERSION,
|
|
170
|
+
'id': appid,
|
|
171
|
+
'build-options': {
|
|
172
|
+
'no-debuginfo': True,
|
|
173
|
+
'strip': False
|
|
174
|
+
},
|
|
175
|
+
'command': 'game.sh',
|
|
176
|
+
'finish-args': [
|
|
177
|
+
*finish_args,
|
|
178
|
+
'--socket=pulseaudio',
|
|
179
|
+
'--device=dri',
|
|
180
|
+
],
|
|
181
|
+
'modules': modules,
|
|
182
|
+
'cleanup-commands': [
|
|
183
|
+
"find /app/lib/game/game -name '*.py' -delete",
|
|
184
|
+
"find /app/lib/game/lib -name '*.py' -delete",
|
|
185
|
+
"find /app/lib/game/renpy -name '*.py' -delete",
|
|
186
|
+
'find /app/lib/game -name __pycache__ -print | xargs -n1 rm -vrf',
|
|
187
|
+
]
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
with (pathlib.Path(workdir) / f'{appid}.json').open('w') as f:
|
|
191
|
+
json.dump(struct, f, indent=4)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright © 2022-2024 Dylan Baker
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
import json
|
|
6
|
+
import pathlib
|
|
7
|
+
import typing
|
|
8
|
+
|
|
9
|
+
import flatpaker.util as util
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def write_rules(description: util.Description, workdir: pathlib.Path, appid: str, desktop_file: pathlib.Path, appdata_file: pathlib.Path) -> None:
|
|
13
|
+
sources = util.extract_sources(description)
|
|
14
|
+
|
|
15
|
+
# TODO: typing requires more thought
|
|
16
|
+
modules: typing.List[typing.Dict[str, typing.Any]] = [
|
|
17
|
+
{
|
|
18
|
+
'buildsystem': 'simple',
|
|
19
|
+
'name': util.sanitize_name(description['common']['name']),
|
|
20
|
+
'sources': sources,
|
|
21
|
+
'build-commands': [
|
|
22
|
+
'mkdir -p /app/share/icons/hicolor/256x256/apps/',
|
|
23
|
+
f'mv icon/*.png /app/share/icons/hicolor/256x256/apps/{appid}.png',
|
|
24
|
+
'rm -r icon',
|
|
25
|
+
|
|
26
|
+
# the main executable usually isn't executable
|
|
27
|
+
'chmod +x nw',
|
|
28
|
+
|
|
29
|
+
# Likewise, but seem to only exist for RPGMaker MZ, not MV
|
|
30
|
+
'[[ -f "chrome_crashpad_handler" ]] && chmod +x chrome_crashpad_handler',
|
|
31
|
+
'[[ -f "nacl_helper" ]] && chmod +x nacl_helper',
|
|
32
|
+
|
|
33
|
+
# install the main game files
|
|
34
|
+
'mkdir -p /app/lib/game',
|
|
35
|
+
'mv * /app/lib/game/',
|
|
36
|
+
],
|
|
37
|
+
'cleanup': [
|
|
38
|
+
'*.desktop', # is incorrect
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
'buildsystem': 'simple',
|
|
43
|
+
'name': 'game_sh',
|
|
44
|
+
'sources': [],
|
|
45
|
+
'build-commands': [
|
|
46
|
+
'mkdir -p /app/bin',
|
|
47
|
+
'echo \'exec /app/lib/game/nw\' > /app/bin/game.sh',
|
|
48
|
+
'chmod +x /app/bin/game.sh',
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
util.bd_desktop(desktop_file),
|
|
52
|
+
util.bd_appdata(appdata_file),
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
# TODO: share this somehow?
|
|
56
|
+
struct = {
|
|
57
|
+
'sdk': 'org.freedesktop.Sdk',
|
|
58
|
+
'runtime': 'org.freedesktop.Platform',
|
|
59
|
+
'runtime-version': util.RUNTIME_VERSION,
|
|
60
|
+
'id': appid,
|
|
61
|
+
'build-options': {
|
|
62
|
+
'no-debuginfo': True,
|
|
63
|
+
'strip': False
|
|
64
|
+
},
|
|
65
|
+
'command': 'game.sh',
|
|
66
|
+
'finish-args': [
|
|
67
|
+
'--socket=pulseaudio',
|
|
68
|
+
'--socket=x11',
|
|
69
|
+
'--device=dri',
|
|
70
|
+
],
|
|
71
|
+
'modules': modules,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
with (pathlib.Path(workdir) / f'{appid}.json').open('w') as f:
|
|
75
|
+
json.dump(struct, f)
|
flatpaker/util.py
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright © 2022-2024 Dylan Baker
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
from xml.etree import ElementTree as ET
|
|
6
|
+
import contextlib
|
|
7
|
+
import hashlib
|
|
8
|
+
import pathlib
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess
|
|
11
|
+
import tempfile
|
|
12
|
+
import textwrap
|
|
13
|
+
import typing
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
import tomllib as tomllib
|
|
17
|
+
except ImportError:
|
|
18
|
+
import tomli as tomllib # type: ignore[import-not-found,no-redef]
|
|
19
|
+
|
|
20
|
+
if typing.TYPE_CHECKING:
|
|
21
|
+
from typing_extensions import NotRequired
|
|
22
|
+
|
|
23
|
+
class Arguments(typing.Protocol):
|
|
24
|
+
description: str
|
|
25
|
+
repo: str
|
|
26
|
+
gpg: typing.Optional[str]
|
|
27
|
+
install: bool
|
|
28
|
+
export: bool
|
|
29
|
+
cleanup: bool
|
|
30
|
+
|
|
31
|
+
class _Common(typing.TypedDict):
|
|
32
|
+
|
|
33
|
+
reverse_url: str
|
|
34
|
+
name: str
|
|
35
|
+
engine: typing.Literal['renpy', 'rpgmaker']
|
|
36
|
+
categories: NotRequired[typing.List[str]]
|
|
37
|
+
|
|
38
|
+
class _AppData(typing.TypedDict):
|
|
39
|
+
|
|
40
|
+
summary: str
|
|
41
|
+
description: str
|
|
42
|
+
content_rating: NotRequired[typing.Dict[str, typing.Literal['none', 'mild', 'moderate', 'intense']]]
|
|
43
|
+
releases: NotRequired[typing.Dict[str, str]]
|
|
44
|
+
license: NotRequired[str]
|
|
45
|
+
|
|
46
|
+
class _Workarounds(typing.TypedDict, total=False):
|
|
47
|
+
icon: bool
|
|
48
|
+
icon_is_webp: bool
|
|
49
|
+
use_x11: bool
|
|
50
|
+
|
|
51
|
+
class Archive(typing.TypedDict):
|
|
52
|
+
|
|
53
|
+
path: pathlib.Path
|
|
54
|
+
strip_components: NotRequired[int]
|
|
55
|
+
|
|
56
|
+
class File(typing.TypedDict):
|
|
57
|
+
|
|
58
|
+
path: pathlib.Path
|
|
59
|
+
dest: NotRequired[str]
|
|
60
|
+
|
|
61
|
+
class Sources(typing.TypedDict):
|
|
62
|
+
|
|
63
|
+
archives: typing.List[Archive]
|
|
64
|
+
files: NotRequired[typing.List[File]]
|
|
65
|
+
patches: NotRequired[typing.List[Archive]]
|
|
66
|
+
|
|
67
|
+
class Description(typing.TypedDict):
|
|
68
|
+
|
|
69
|
+
common: _Common
|
|
70
|
+
appdata: _AppData
|
|
71
|
+
workarounds: NotRequired[_Workarounds]
|
|
72
|
+
sources: NotRequired[Sources]
|
|
73
|
+
|
|
74
|
+
RUNTIME_VERSION = "24.08"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _subelem(elem: ET.Element, tag: str, text: typing.Optional[str] = None, **extra: str) -> ET.Element:
|
|
78
|
+
new = ET.SubElement(elem, tag, extra)
|
|
79
|
+
new.text = text
|
|
80
|
+
return new
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def extract_sources(description: Description) -> typing.List[typing.Dict[str, object]]:
|
|
84
|
+
sources: typing.List[typing.Dict[str, object]] = []
|
|
85
|
+
|
|
86
|
+
if 'sources' in description:
|
|
87
|
+
for a in description['sources']['archives']:
|
|
88
|
+
sources.append({
|
|
89
|
+
'path': a['path'].as_posix(),
|
|
90
|
+
'sha256': sha256(a['path']),
|
|
91
|
+
'type': 'archive',
|
|
92
|
+
'strip-components': a.get('strip_components', 1),
|
|
93
|
+
})
|
|
94
|
+
for source in description['sources'].get('files', []):
|
|
95
|
+
p = source['path']
|
|
96
|
+
sources.append({
|
|
97
|
+
'path': p.as_posix(),
|
|
98
|
+
'sha256': sha256(p),
|
|
99
|
+
'type': 'file',
|
|
100
|
+
})
|
|
101
|
+
for a in description['sources'].get('patches', []):
|
|
102
|
+
sources.append({
|
|
103
|
+
'type': 'patch',
|
|
104
|
+
'path': a['path'].as_posix(),
|
|
105
|
+
'strip-components': a.get('strip_components', 1),
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
return sources
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def create_appdata(description: Description, workdir: pathlib.Path, appid: str) -> pathlib.Path:
|
|
112
|
+
p = workdir / f'{appid}.metainfo.xml'
|
|
113
|
+
|
|
114
|
+
root = ET.Element('component', type="desktop-application")
|
|
115
|
+
_subelem(root, 'id', appid)
|
|
116
|
+
_subelem(root, 'name', description['common']['name'])
|
|
117
|
+
_subelem(root, 'summary', description['appdata']['summary'])
|
|
118
|
+
_subelem(root, 'metadata_license', 'CC0-1.0')
|
|
119
|
+
_subelem(root, 'project_license', description['appdata'].get('license', 'LicenseRef-Proprietary'))
|
|
120
|
+
|
|
121
|
+
recommends = ET.SubElement(root, 'recommends')
|
|
122
|
+
for c in ['pointing', 'keyboard', 'touch', 'gamepad']:
|
|
123
|
+
_subelem(recommends, 'control', c)
|
|
124
|
+
|
|
125
|
+
requires = ET.SubElement(root, 'requires')
|
|
126
|
+
_subelem(requires, 'display_length', '360', compare="ge")
|
|
127
|
+
_subelem(requires, 'internet', 'offline-only')
|
|
128
|
+
|
|
129
|
+
categories = ET.SubElement(root, 'categories')
|
|
130
|
+
for c in ['Game'] + description['common'].get('categories', []):
|
|
131
|
+
_subelem(categories, 'category', c)
|
|
132
|
+
|
|
133
|
+
desc = ET.SubElement(root, 'description')
|
|
134
|
+
_subelem(desc, 'p', description['appdata']['description'])
|
|
135
|
+
_subelem(root, 'launchable', f'{appid}.desktop', type="desktop-id")
|
|
136
|
+
|
|
137
|
+
# There is an oars-1.1, but it doesn't appear to be supported by KDE
|
|
138
|
+
# discover yet
|
|
139
|
+
if 'content_rating' in description['appdata']:
|
|
140
|
+
cr = ET.SubElement(root, 'content_rating', type="oars-1.0")
|
|
141
|
+
for k, r in description['appdata']['content_rating'].items():
|
|
142
|
+
_subelem(cr, 'content_attribute', r, id=k)
|
|
143
|
+
|
|
144
|
+
if 'releases' in description['appdata']:
|
|
145
|
+
cr = ET.SubElement(root, 'releases')
|
|
146
|
+
for date, version in description['appdata']['releases'].items():
|
|
147
|
+
_subelem(cr, 'release', version=version, date=date)
|
|
148
|
+
|
|
149
|
+
tree = ET.ElementTree(root)
|
|
150
|
+
ET.indent(tree)
|
|
151
|
+
tree.write(p, encoding='utf-8', xml_declaration=True)
|
|
152
|
+
|
|
153
|
+
return p
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def create_desktop(description: Description, workdir: pathlib.Path, appid: str) -> pathlib.Path:
|
|
157
|
+
p = workdir / f'{appid}.desktop'
|
|
158
|
+
with p.open('w') as f:
|
|
159
|
+
f.write(textwrap.dedent(f'''\
|
|
160
|
+
[Desktop Entry]
|
|
161
|
+
Name={description['common']['name']}
|
|
162
|
+
Exec=game.sh
|
|
163
|
+
Type=Application
|
|
164
|
+
Categories={';'.join(['Game'] + description['common'].get('categories', []))};
|
|
165
|
+
'''))
|
|
166
|
+
if description.get('workarounds', {}).get('icon', True):
|
|
167
|
+
f.write(f'Icon={appid}')
|
|
168
|
+
|
|
169
|
+
return p
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def sha256(path: pathlib.Path) -> str:
|
|
173
|
+
with path.open('rb') as f:
|
|
174
|
+
return hashlib.sha256(f.read()).hexdigest()
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def sanitize_name(name: str) -> str:
|
|
178
|
+
"""Replace invalid characters in a name with valid ones."""
|
|
179
|
+
return name \
|
|
180
|
+
.replace(' ', '_') \
|
|
181
|
+
.replace("&", '_') \
|
|
182
|
+
.replace(':', '') \
|
|
183
|
+
.replace("'", '')
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def build_flatpak(args: Arguments, workdir: pathlib.Path, appid: str) -> None:
|
|
187
|
+
build_command: typing.List[str] = [
|
|
188
|
+
'flatpak-builder', '--force-clean', '--install-deps-from=flathub', '--user', 'build',
|
|
189
|
+
(workdir / f'{appid}.json').absolute().as_posix(),
|
|
190
|
+
]
|
|
191
|
+
|
|
192
|
+
if args.export:
|
|
193
|
+
build_command.extend(['--repo', args.repo])
|
|
194
|
+
if args.gpg:
|
|
195
|
+
build_command.extend(['--gpg-sign', args.gpg])
|
|
196
|
+
if args.install:
|
|
197
|
+
build_command.extend(['--install'])
|
|
198
|
+
|
|
199
|
+
subprocess.run(build_command)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def load_description(name: str) -> Description:
|
|
203
|
+
relpath = pathlib.Path(name).parent.absolute()
|
|
204
|
+
with open(name, 'rb') as f:
|
|
205
|
+
d = typing.cast('Description', tomllib.load(f))
|
|
206
|
+
|
|
207
|
+
# Fixup relative paths
|
|
208
|
+
if 'sources' in d:
|
|
209
|
+
for a in d['sources']['archives']:
|
|
210
|
+
a['path'] = relpath / a['path']
|
|
211
|
+
if 'files' in d['sources']:
|
|
212
|
+
for s in d['sources']['files']:
|
|
213
|
+
s['path'] = relpath / s['path']
|
|
214
|
+
if 'patches' in d['sources']:
|
|
215
|
+
for a in d['sources']['patches']:
|
|
216
|
+
a['path'] = relpath / a['path']
|
|
217
|
+
|
|
218
|
+
return d
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@contextlib.contextmanager
|
|
222
|
+
def tmpdir(name: str, cleanup: bool = True) -> typing.Iterator[pathlib.Path]:
|
|
223
|
+
tdir = pathlib.Path(tempfile.gettempdir()) / name
|
|
224
|
+
tdir.mkdir(parents=True, exist_ok=True)
|
|
225
|
+
yield tdir
|
|
226
|
+
if cleanup:
|
|
227
|
+
shutil.rmtree(tdir)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def bd_desktop(file_: pathlib.Path) -> typing.Dict[str, typing.Any]:
|
|
231
|
+
return {
|
|
232
|
+
'buildsystem': 'simple',
|
|
233
|
+
'name': 'desktop_file',
|
|
234
|
+
'sources': [
|
|
235
|
+
{
|
|
236
|
+
'path': file_.as_posix(),
|
|
237
|
+
'sha256': sha256(file_),
|
|
238
|
+
'type': 'file',
|
|
239
|
+
}
|
|
240
|
+
],
|
|
241
|
+
'build-commands': [
|
|
242
|
+
'mkdir -p /app/share/applications',
|
|
243
|
+
f'cp {file_.name} /app/share/applications',
|
|
244
|
+
],
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def bd_appdata(file_: pathlib.Path) -> typing.Dict[str, typing.Any]:
|
|
249
|
+
return {
|
|
250
|
+
'buildsystem': 'simple',
|
|
251
|
+
'name': 'appdata_file',
|
|
252
|
+
'sources': [
|
|
253
|
+
{
|
|
254
|
+
'path': file_.as_posix(),
|
|
255
|
+
'sha256': sha256(file_),
|
|
256
|
+
'type': 'file',
|
|
257
|
+
}
|
|
258
|
+
],
|
|
259
|
+
'build-commands': [
|
|
260
|
+
'mkdir -p /app/share/metainfo',
|
|
261
|
+
f'cp {file_.name} /app/share/metainfo',
|
|
262
|
+
],
|
|
263
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Dylan Baker
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: flatpaker
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Utilities to convert various kinds of native binaries into flatpaks.
|
|
5
|
+
Keywords: flatpak,renpy,rpgmaker
|
|
6
|
+
Author-email: Dylan Baker <dylan@pnwbakers.com>
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: System :: Archiving :: Packaging
|
|
19
|
+
Requires-Dist: tomli; python_version<"3.11"
|
|
20
|
+
|
|
21
|
+
# flatpaker
|
|
22
|
+
|
|
23
|
+
Script to mostly automate creating flatpaks from published Ren'Py and Linux
|
|
24
|
+
builds of RPGMaker MV and MZ. open to additional support
|
|
25
|
+
|
|
26
|
+
## What is it?
|
|
27
|
+
|
|
28
|
+
It's a script that automatically handles much of the task of generating a
|
|
29
|
+
flatpak for pre-built projects, including adding patches or mods. You
|
|
30
|
+
write a small, simple toml file, fetch the sources, and get a ready to publish
|
|
31
|
+
flatpak.
|
|
32
|
+
|
|
33
|
+
It currently automatically does the following automatically:
|
|
34
|
+
|
|
35
|
+
- Generates an appstream xml file
|
|
36
|
+
- Generates a .desktop file
|
|
37
|
+
- Extracts an icon from the game source, and installs it
|
|
38
|
+
- patches the game to honor $XDG_DATA_HOME for storing game data inside the sandbox (instead of needing $HOME access)
|
|
39
|
+
- sets up the sandbox to allow audio and display, but nothing else
|
|
40
|
+
- recompiles the program when mods are applied
|
|
41
|
+
- strips .rpy files to save space (keeping the rpyc files)
|
|
42
|
+
- strips windows and macos specific files
|
|
43
|
+
- allows local install or publishing to a repo
|
|
44
|
+
|
|
45
|
+
## Why?
|
|
46
|
+
|
|
47
|
+
I like playing Ren'Py games sometimes. I also don't always trust random
|
|
48
|
+
pre-compiled binaries from the internet. Flatpak provides a nice, convenient
|
|
49
|
+
way to sandbox applications. It also makes supporting Steam Deck and Fedora
|
|
50
|
+
immutable a breeze. But generating flatpaks by hand is a lot of work, especially
|
|
51
|
+
when most of the process will be exactly the same for every renpy project.
|
|
52
|
+
|
|
53
|
+
## How do I use it?
|
|
54
|
+
|
|
55
|
+
1. Download the compressed project
|
|
56
|
+
2. Download any mods or addons (optional)
|
|
57
|
+
3. Write a toml description
|
|
58
|
+
4. run the program
|
|
59
|
+
|
|
60
|
+
### Toml Format
|
|
61
|
+
|
|
62
|
+
```toml
|
|
63
|
+
[common]
|
|
64
|
+
name = 'Game or VN' # use properly formatted name like "The Cool Adventures of Bob", or "Bob's Quest 7: Lawnmower Confusion"
|
|
65
|
+
reverse_url = 'com.example.JDoe' # name will be appended
|
|
66
|
+
# "Game" is added automatically
|
|
67
|
+
# used freedesktop menu categories. see: https://specifications.freedesktop.org/menu-spec/latest/apas02.html
|
|
68
|
+
categories = ['Simulation']
|
|
69
|
+
engine = ['renpy'] # Or 'rpgmaker'
|
|
70
|
+
|
|
71
|
+
[appdata]
|
|
72
|
+
summary = "A short summary, one sentence or so."
|
|
73
|
+
description = """
|
|
74
|
+
A longer description.
|
|
75
|
+
|
|
76
|
+
probably on multiple \
|
|
77
|
+
lines
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
# This is an optional value for the license of the renpy project itself.
|
|
81
|
+
# If unset it defaults to LicenseRef-Proprietary.
|
|
82
|
+
# if you have specific terms which are not an Open Source license, you can use the form:
|
|
83
|
+
# LicenseRef-Proprietary=https://www.example.com/my-license
|
|
84
|
+
# See: https://spdx.org/specifications for more information
|
|
85
|
+
license = "SPDX identifier"
|
|
86
|
+
|
|
87
|
+
[appdata.content_rating]
|
|
88
|
+
# optional
|
|
89
|
+
# Uses OARS specifications. See: https://hughsie.github.io/oars/
|
|
90
|
+
# keys should be ids, and the values are must be a rating (as a string):
|
|
91
|
+
# none, mild, moderate, or intense
|
|
92
|
+
language-profanity = "mild"
|
|
93
|
+
|
|
94
|
+
[appdata.releases]
|
|
95
|
+
# optional
|
|
96
|
+
# in the form "date = version"
|
|
97
|
+
"2023-01-01" = "1.0.0"
|
|
98
|
+
|
|
99
|
+
# Optional, alternatively may be passed on teh command line
|
|
100
|
+
[[sources.archives]]
|
|
101
|
+
# path must be set if this is provided
|
|
102
|
+
path = "relative to toml or absolute path"
|
|
103
|
+
|
|
104
|
+
# Optional, defaults to 1. How many directory levels to remove from this component
|
|
105
|
+
strip_comonents = 2
|
|
106
|
+
|
|
107
|
+
# Optional, cannot be set from command line
|
|
108
|
+
[[sources.patches]]
|
|
109
|
+
# path must be set if this is provided
|
|
110
|
+
path = "relative to toml or absolute path"
|
|
111
|
+
|
|
112
|
+
# Optional, defaults to 1. How many directory levels to remove from this component
|
|
113
|
+
strip_comonents = 2
|
|
114
|
+
|
|
115
|
+
# Optional, cannot be set from command line
|
|
116
|
+
[[sources.files]]
|
|
117
|
+
# path must be set if this is provided
|
|
118
|
+
path = "relative to toml or absolute path"
|
|
119
|
+
|
|
120
|
+
# Optional, if set the file will be installed to this name
|
|
121
|
+
# Does not have to be set for .rpy files that go in the game root directory
|
|
122
|
+
dest = "where to install"
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Configuration
|
|
126
|
+
|
|
127
|
+
Some options can be given on the command line or via a configuration file.
|
|
128
|
+
That file must be written to `$XDG_CONFIG_HOME/flatpaker/config.toml` (if unset
|
|
129
|
+
`$XDG_CONFIG_HOME` defaults to `~/.config`).
|
|
130
|
+
|
|
131
|
+
```toml
|
|
132
|
+
[common]
|
|
133
|
+
# A gpg private key to sign with, overwritten by the --gpg option
|
|
134
|
+
gpg-key = "0x123456789"
|
|
135
|
+
|
|
136
|
+
# The absolute path to a repo to write to. overwritten by the --repo option
|
|
137
|
+
repo = "/path/to/a/repo/to/export"
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
## What is required?
|
|
142
|
+
|
|
143
|
+
- python 3.11 or a modern version of python3 with tomli
|
|
144
|
+
- flatpak-builder
|
|
145
|
+
|
|
146
|
+
### Schema
|
|
147
|
+
|
|
148
|
+
A Json based schema is provided, which can be used with VSCode's EvenBetterToml
|
|
149
|
+
extension. It may be useful elsewhere.
|
|
150
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
flatpaker/__init__.py,sha256=PsGR5gs45aoJvPD1NHYiyPzrqGJjlynQgcPzUULvCZs,534
|
|
2
|
+
flatpaker/config.py,sha256=z3GFLh3It1qM1W_Brds1AjFojPDjqswPrxdGxkp7T94,882
|
|
3
|
+
flatpaker/entry.py,sha256=u4o6U3rz75b-5M8pRJ0SeW5V3zXSYoGvcyeY8Jvq18c,2370
|
|
4
|
+
flatpaker/util.py,sha256=Ulx6VKzVNEz_v2U5x5ORZnX6gaN0190H_WYSTk6JV8I,8105
|
|
5
|
+
flatpaker/impl/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
flatpaker/impl/renpy.py,sha256=LooyGP4qM8-uwEtkRSpqAyxgEa805_wabc6D1SnfsAU,6076
|
|
7
|
+
flatpaker/impl/rpgmaker.py,sha256=Kvy6Mb_iOjD-AmUfJphyvFUnJzIBxWYoGoEIAsrx7kk,2392
|
|
8
|
+
flatpaker-0.0.1.dist-info/entry_points.txt,sha256=_6R4HPhjK68ou2LtVSgm26ltyMaQF4tCQT-L9ymdOgQ,50
|
|
9
|
+
flatpaker-0.0.1.dist-info/LICENSE,sha256=QqvRDZzvUrvgv6CnNh5LdWCnoF19JuPUYzE-Qs8pNFY,1068
|
|
10
|
+
flatpaker-0.0.1.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81
|
|
11
|
+
flatpaker-0.0.1.dist-info/METADATA,sha256=awJgom54TVC5npmCQob03mQNnOLw0YsV1w0HJHmXAe0,5149
|
|
12
|
+
flatpaker-0.0.1.dist-info/RECORD,,
|