diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index dceb2cf..356a50f 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -23,7 +23,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install mypy ruff tomlkit + pip install mypy ruff tomlkit keyring - name: Lint with Ruff run: ruff check flatpaker - name: Lint with mypy diff --git a/README.md b/README.md index 2522dc5..e06e45c 100644 --- a/README.md +++ b/README.md @@ -46,8 +46,11 @@ to provide even better security by using Wayland instead of X11 (or XWayland). 2. Download any mods or addons (optional) 3. Generate a toml description `flatpaker generate com.developer.game "Game Name" engine archive.zip` 4. Edit the generated description to fill in missing information -5. run `flatpaker build-runtimes --install` (which adds the runtimes and sdks) -6. run `flatpaker build --install *.toml` or `flatpaker build --export --gpg-sign *.toml` (for local install or for export to a shared repo) +5. run `flatpaker build-runtimes --export=install` (which adds the runtimes and sdks) +6. Run one of: + - `flatpaker build --export=install *.toml` to install for your user + - `flatpaker build --export=repo --gpg-sign *.toml` to export to a local ostree repo + - `flatpaker build --export=flat-manager *.toml` to export to a flat-manager instance ### Toml Format @@ -181,15 +184,44 @@ That file must be written to `$XDG_CONFIG_HOME/flatpaker/config.toml` (if unset # The absolute path to a repo to write to. overwritten by the --repo option repo = "/path/to/a/repo/to/export" + + # The default export mode + # May be one of: "none", "install", "repo", "flat-manager" + export = "none" + +[flat-manager] + # The address that the flat-manager instance listens on + remote = "https://flat-manager.example.com:8080" + + # The default repo on that flat-manager instance to push to + repo = "stable" + + # Only one of the following may be set. This is the repo key that + # will be passed to the flat-manager client. + + # A pair of strings to pass to `keyring.get_password()` + # The token can be written into the keyring with the command line tool + # `keyring set `. + token-keyring = ["service name", "keyname"] + + # A file containing the key + # Both environment variables and `~` can be used here + # This is the only form that can be set on the command line + token-file = "/secret.d/flat-manager/1" + + # The keyfile written straight into the config file + token-str = "ABC123" ``` ## What is required? - python >= 3.10 +- python-keyring (if using flat-manager export with a secret stored in a keyring) - python-tomlkit - flatpak-builder - flatpak +- flat-manager-client (if using flat-manager export) ### Schema diff --git a/flatpaker/actions/build_flatpak.py b/flatpaker/actions/build_flatpak.py index 8f99e3c..ab353db 100644 --- a/flatpaker/actions/build_flatpak.py +++ b/flatpaker/actions/build_flatpak.py @@ -14,7 +14,7 @@ if typing.TYPE_CHECKING: from flatpaker.description import Description, EngineName - from flatpaker.entry import BaseBuildArguments, BuildArguments + from flatpaker.entry import BuildFlatpakConfig JsonWriterImpl = typing.Callable[[Description, pathlib.Path, str, pathlib.Path, pathlib.Path], None] @@ -30,7 +30,8 @@ def select_impl(name: EngineName) -> JsonWriterImpl: return mod.write_rules -def _build(args: BaseBuildArguments, description: Description) -> None: +def _build(args: BuildFlatpakConfig, path: pathlib.Path) -> None: + description = load_description(path) # TODO: This could be common appid = f"{description.common.reverse_url}.{util.sanitize_name(description.common.name)}" @@ -43,29 +44,44 @@ def _build(args: BaseBuildArguments, description: Description) -> None: write_build_rules(description, workdir, appid, desktop_file, appdata_file) build_command: list[str] = [ - 'flatpak-builder', '--force-clean', '--user', 'build', + 'flatpak-builder', '--install-deps-from=flathub', + '--force-clean', '--user', 'build', (workdir / f'{appid}.json').absolute().as_posix(), ] - if args.export: - build_command.extend(['--repo', args.repo]) - if args.gpg: - build_command.extend(['--gpg-sign', args.gpg]) - if args.install: - build_command.extend(['--install']) + repo = args.repo + + match args.export: + case 'repo': + build_command.extend(['--repo', repo]) + if args.gpg: + build_command.extend(['--gpg-sign', args.gpg]) + case 'install': + build_command.extend(['--install']) + case 'flat-manager': + # Use a temporary repo for each runtime and app + # This simplifies uploading with flat-manager-client + repos = pathlib.Path.cwd() / '.flat-manager-repos' + repos.mkdir(exist_ok=True) + repo = repos.joinpath(appid).as_posix() + build_command.extend(['--repo', repo]) subprocess.run(build_command, check=True) + + if args.export == 'flat-manager': + assert args.flat_manager is not None + util.export_to_flat_manager(repo, args.flat_manager) + if args.cleanup: shutil.rmtree('build', ignore_errors=True) -def build_flatpak(args: BuildArguments) -> bool: +def build_flatpak(args: BuildFlatpakConfig) -> bool: success = True for d in args.descriptions: try: - description = load_description(d) - _build(args, description) + _build(args, d) except Exception: if not args.keep_going: raise diff --git a/flatpaker/actions/build_runtime.py b/flatpaker/actions/build_runtime.py index 33acc58..8c48958 100644 --- a/flatpaker/actions/build_runtime.py +++ b/flatpaker/actions/build_runtime.py @@ -11,41 +11,60 @@ from flatpaker import util if typing.TYPE_CHECKING: - from ..entry import BaseBuildArguments, BuildRuntimeArguments + from ..entry import BuildRuntimeConfig -def _build_runtime(args: BaseBuildArguments, sdk: pathlib.Path, +_RUNTIME_ID_BASE = 'com.github.dcbaker.flatpaker' + + +def _get_renpy_branch(sdk: str) -> str: + if '8' in sdk: + return '8' + elif '7.py2' in sdk: + return '7' + elif '7.py3' in sdk: + return '7-PY3' + raise RuntimeError("Unknown Ren'Py branch") + + +def _build_runtime(args: BuildRuntimeConfig, sdk: pathlib.Path, need_platform_workaround: bool) -> None: build_command: list[str] = [ 'flatpak-builder', '--force-clean', '--user', '--install-deps-from=flathub', 'build', sdk.as_posix()] - if args.export: - build_command.extend(['--repo', args.repo]) - if args.gpg: - build_command.extend(['--gpg-sign', args.gpg]) - if args.install: - build_command.extend(['--install']) + repo = args.repo + + match args.export: + case 'repo': + build_command.extend(['--repo', repo]) + if args.gpg: + build_command.extend(['--gpg-sign', args.gpg]) + case 'install': + build_command.extend(['--install']) + case 'flat-manager': + # Use a temporary repo for each runtime and app + # This simplifies uploading with flat-manager-client + repos = pathlib.Path.cwd() / '.flat-manager-repos' + repos.mkdir(exist_ok=True) + repo = repos.joinpath(sdk.name).as_posix() + build_command.extend(['--repo', repo]) subprocess.run(build_command, check=True) + if args.export == 'flat-manager': + assert args.flat_manager is not None + util.export_to_flat_manager(repo, args.flat_manager) + + platform_id = sdk.name.removeprefix(_RUNTIME_ID_BASE).removeprefix('.').split('.', maxsplit=1)[0] # Work around https://github.com/flatpak/flatpak-builder/issues/630 - if need_platform_workaround and args.install and 'Sdk' in sdk.name: - if '8' in sdk.name: - branch = '8' - elif '7.py2' in sdk.name: - branch = '7' - elif '7.py3' in sdk.name: - branch = '7-PY3' - else: - raise RuntimeError('Unexpected Sdk') - - repo = args.repo if args.export else pathlib.Path('.flatpak-builder/cache').absolute().as_posix() - platform_id = '.'.join(sdk.name.split('.', maxsplit=5)[:-1]) + if need_platform_workaround and args.export == 'install' and platform_id == 'RenPy': + repo = pathlib.Path('.flatpak-builder/cache').absolute().as_posix() + branch = _get_renpy_branch(sdk.name) install_command = [ 'flatpak', 'install', '--user', '-y', '--noninteractive', - '--reinstall', repo, f'{platform_id}.Platform//{branch}', + '--reinstall', repo, f'{_RUNTIME_ID_BASE}.{platform_id}.Platform//{branch}', ] subprocess.run(install_command, check=True) @@ -68,24 +87,16 @@ def _need_platform_workaround() -> bool: return tuple(int(v) for v in raw_ver.split('.')) < (1, 4, 5) -def build_runtimes(args: BuildRuntimeArguments) -> bool: - command = [ - 'flatpak', 'install', '--no-auto-pin', '--user', - f'org.freedesktop.Platform//{util.RUNTIME_VERSION}', - f'org.freedesktop.Sdk//{util.RUNTIME_VERSION}', - ] - subprocess.run(command, check=True) - - basename = 'com.github.dcbaker.flatpaker' +def build_runtimes(args: BuildRuntimeConfig) -> bool: runtimes: list[str] = [] if 'rpgmaker' in args.runtimes: - runtimes.append(f'{basename}.RPGM.Platform.yml') + runtimes.append(f'{_RUNTIME_ID_BASE}.RPGM.Platform.yml') if 'renpy8' in args.runtimes: - runtimes.append(f'{basename}.RenPy.8.Sdk.yml') + runtimes.append(f'{_RUNTIME_ID_BASE}.RenPy.8.Sdk.yml') if 'renpy7' in args.runtimes: - runtimes.append(f'{basename}.RenPy.7.py2.Sdk.yml') + runtimes.append(f'{_RUNTIME_ID_BASE}.RenPy.7.py2.Sdk.yml') if 'renpy7-py3' in args.runtimes: - runtimes.append(f'{basename}.RenPy.7.py3.Sdk.yml') + runtimes.append(f'{_RUNTIME_ID_BASE}.RenPy.7.py3.Sdk.yml') success = True diff --git a/flatpaker/actions/generate.py b/flatpaker/actions/generate.py index 1aab883..d0384a1 100644 --- a/flatpaker/actions/generate.py +++ b/flatpaker/actions/generate.py @@ -15,10 +15,10 @@ if typing.TYPE_CHECKING: import tomlkit.items # noqa: TC004 - from flatpaker.entry import GenerateArguments + from flatpaker.entry import GenerateConfig -def generate(args: GenerateArguments) -> bool: +def generate(args: GenerateConfig) -> bool: name = f'{args.url}.{util.sanitize_name(args.appname)}' projectdir = pathlib.Path(name) sourcedir = projectdir / 'sources' @@ -49,7 +49,7 @@ def add(table: tomlkit.items.Table, key: str, entry: object, doc.add('appdata', appdata) archives: list[tomlkit.items.Table] = [] - for src in [args.archive] + args.archives: + for src in args.archives: archive = tomlkit.table() add(archive, 'path', os.path.join(sourcedir.name, os.path.basename(src))) add(archive, 'sha256', util.sha256(pathlib.Path(src))) @@ -93,7 +93,7 @@ def add(table: tomlkit.items.Table, key: str, entry: object, tomlkit.dump(doc, f) # move files after writing the toml, so we don't move things then fail - for srcs, subdir in [([args.archive] + args.archives + args.files, sourcedir), + for srcs, subdir in [(args.archives + args.files, sourcedir), (args.patches, patchdir)]: for src in srcs: srcp = pathlib.Path(src) diff --git a/flatpaker/config.py b/flatpaker/config.py index e12e5b5..7614f94 100644 --- a/flatpaker/config.py +++ b/flatpaker/config.py @@ -9,18 +9,64 @@ import tomlkit if typing.TYPE_CHECKING: + ExportMode = typing.Literal['none', 'repo', 'install', 'flat-manager'] + + FlatManager = typing.TypedDict( + 'FlatManager', + { + 'remote': str, + 'repo': str, + 'token-file': str, + 'token-str': str, + 'token-keyring': tuple[str, str] + }, + total=False, + ) Common = typing.TypedDict( 'Common', { 'gpg-key': str, 'repo': str, + 'export': ExportMode, }, total=False, ) - class Config(typing.TypedDict): - common: Common + Config = typing.TypedDict( + 'Config', + { + 'common': Common, + 'flat-manager': FlatManager, + }, + ) + + +def _load_flat_manager(raw: dict[str, object]) -> FlatManager: + token_file = raw.get('token-file', None) + token_str = raw.get('token-str', None) + token_key = raw.get('token-keyring', None) + + if len([k for k in [token_file, token_str, token_key] if k is not None]) > 1: + raise TypeError('Configuration file may only contain one of: ' + '"flat-manager.token-file", "flat-manager.token-str", or ' + '"flat-manager.token-key"') + + if token_file is not None and not isinstance(token_file, str): + raise TypeError('Configuration key "flat-manager.token-file" must be a string') + if token_str is not None and not isinstance(token_str, str): + raise TypeError('Configuration key "flat-manager.token-str" must be a string') + if token_key is not None: + if not isinstance(token_key, list): + raise TypeError('Configuration key "flat-manager.token-key" must be a a list') + if any(not isinstance(k, str) for k in token_key): + raise TypeError('Configuration key "flat-manager.token-key" elements must be strings') + if len(token_key) != 2: + raise TypeError('Configuration key "flat-manager.token-key" must be an array ' + 'with exactly two elements') + raw['token-keyring'] = tuple(token_key) + + return typing.cast('FlatManager', raw) def load_config() -> Config: @@ -36,4 +82,10 @@ def load_config() -> Config: if 'common' not in raw: raw['common'] = {} + + if fm := raw.get('flat-manager'): + raw['flat-manager'] = _load_flat_manager(fm) + else: + raw['flat-manager'] = {} + return typing.cast('Config', raw) diff --git a/flatpaker/description.py b/flatpaker/description.py index cf77b65..4b76cf5 100644 --- a/flatpaker/description.py +++ b/flatpaker/description.py @@ -88,13 +88,13 @@ class Description: sources: Sources -def load_description(name: str) -> Description: - relpath = pathlib.Path(name).parent.absolute() +def load_description(path: pathlib.Path) -> Description: + relpath = path.parent.absolute() # TODO: the cast to Any leaves us with the same # validation problem with had previous, but without the hints. # I wish python had something like serde - with open(name, 'rb') as f: + with path.open('rb') as f: d = typing.cast('typing.Any', tomlkit.load(f)) quirks = Quirks(**d.get('quirks', {})) diff --git a/flatpaker/entry.py b/flatpaker/entry.py index 99630af..043d898 100644 --- a/flatpaker/entry.py +++ b/flatpaker/entry.py @@ -4,6 +4,9 @@ from __future__ import annotations import argparse +import dataclasses +import os +import pathlib import subprocess import sys import typing @@ -14,6 +17,7 @@ from flatpaker.actions.generate import generate if typing.TYPE_CHECKING: + from flatpaker.config import ExportMode from flatpaker.description import EngineName class BaseArguments(typing.Protocol): @@ -22,14 +26,19 @@ class BaseArguments(typing.Protocol): class BaseBuildArguments(BaseArguments, typing.Protocol): repo: str gpg: str | None - install: bool - export: bool + export: ExportMode cleanup: bool deltas: bool keep_going: bool + flat_manager_remote: str | None + flat_manager_repo: str | None + flat_manager_token: str | None + flat_manager_token_file: str | None + flat_manager_token_keyring_service: str | None + flat_manager_token_keyring_keyid: str | None class BuildArguments(BaseBuildArguments, typing.Protocol): - descriptions: list[str] + descriptions: list[pathlib.Path] class BuildRuntimeArguments(BaseBuildArguments, typing.Protocol): runtimes: list[EngineName] @@ -44,8 +53,56 @@ class GenerateArguments(BaseArguments, typing.Protocol): files: list[str] -def static_deltas(args: BaseBuildArguments) -> None: - if not (args.deltas or args.export): +@dataclasses.dataclass(slots=False, eq=False) +class FlatManagerConfig: + + remote: str + repo: str + token: str + + +@dataclasses.dataclass(slots=False, eq=False) +class _BuildCommonConfig: + """Common configuration for "build" and "build-runtimes".""" + + repo: str + gpg: str | None + export: ExportMode + cleanup: bool + deltas: bool + keep_going: bool + flat_manager: FlatManagerConfig | None + + +@dataclasses.dataclass(slots=False, eq=False) +class BuildFlatpakConfig(_BuildCommonConfig): + """Configuration for "build".""" + + descriptions: list[pathlib.Path] + + +@dataclasses.dataclass(slots=False, eq=False) +class BuildRuntimeConfig(_BuildCommonConfig): + """Configuration for "build-runtimes".""" + + runtimes: list[EngineName] + + +@dataclasses.dataclass(slots=False, eq=False) +class GenerateConfig: + """Configuration for "generate".""" + + url: str + appname: str + engine: EngineName + archives: list[str] + patches: list[str] + files: list[str] + + + +def static_deltas(args: BuildRuntimeConfig | BuildFlatpakConfig) -> None: + if not (args.deltas or args.export != 'repo'): return command = ['flatpak', 'build-update-repo', args.repo, '--generate-static-deltas'] if args.gpg: @@ -54,7 +111,7 @@ def static_deltas(args: BaseBuildArguments) -> None: subprocess.run(command, check=True) -def main() -> None: +def _parse_args() -> BaseArguments: config = flatpaker.config.load_config() # An inheritable parser instance used to add arguments to both build and build-runtimes @@ -69,10 +126,57 @@ def main() -> None: default=config['common'].get('gpg-key'), action='store', help='A GPG key to sign the output to when writing to a repo') - pp.add_argument('--export', action='store_true', help='Export to the provided repo') - pp.add_argument('--install', action='store_true', help="Install for the user (useful for testing)") + pp.add_argument( + '--export', + action='store', + choices=['none', 'install', 'repo', 'flat-manager'], + default=config['common'].get('export', 'none'), + help='Export the repo using one of the following methods. ' + '"none": Do not export, only build; ' + '"export": write to an ostree repo; ' + '"install": install for the user(useful for testing)') + pp.add_argument( + '--flat-manager-remote', + action='store', + default=config['flat-manager'].get('remote'), + help='The flat-manager url', + ) + pp.add_argument( + '--flat-manager-repo', + action='store', + default=config['flat-manager'].get('repo'), + help='The repo of the flat-manager instance to manage', + ) + pp.add_argument( + '--flat-manager-token', + action='store', + default=config['flat-manager'].get('token-str'), + help='A path to a file containing a flat-manager repo token', + ) + pp.add_argument( + '--flat-manager-token-file', + action='store', + default=config['flat-manager'].get('token-file'), + help='Path to a file containing flat-manager repo token', + ) + pp.add_argument( + '--flat-manager-token-keyring-service', + action='store', + default=config['flat-manager'].get('token-keyring', (None, None))[0], + help='A service to pass to `keyring.get_password(service, keyid)', + ) + pp.add_argument( + '--flat-manager-token-keyring-keyid', + action='store', + default=config['flat-manager'].get('token-keyring', (None, None))[1], + help='A keyid to pass to `keyring.get_password(service, keyid)', + ) pp.add_argument('--no-cleanup', action='store_false', dest='cleanup', help="don't delete the temporary directory") - pp.add_argument('--static-deltas', action='store_true', dest='deltas', help="generate static deltas when exporting") + pp.add_argument( + '--static-deltas', + action='store_true', + dest='deltas', + help="generate static deltas when exporting to a repo. Has not effect if `--export-mode` is not `repo`") pp.add_argument('--keep-going', action='store_true', help="Don't stop if building a runtime or app fails.") from . import __version__ @@ -82,7 +186,7 @@ def main() -> None: subparsers = parser.add_subparsers(required=True) build_parser = subparsers.add_parser( 'build', help='Build flatpaks from descriptions', parents=[pp]) - build_parser.add_argument('descriptions', nargs='+', help="A Toml description file") + build_parser.add_argument('descriptions', nargs='+', type=pathlib.Path, help="A Toml description file") build_parser.set_defaults(action='build') _all_runtimes = ['renpy8', 'renpy7', 'renpy7-py3', 'rpgmaker'] @@ -130,20 +234,116 @@ def main() -> None: ) generate_parser.set_defaults(action='generate') - args = typing.cast('BaseArguments', parser.parse_args()) + base = typing.cast('BaseArguments', parser.parse_args()) + + if base.action in {'build', 'build-runtimes'}: + runargs = typing.cast('BaseBuildArguments', base) + if runargs.export == 'repo' and not runargs.repo: + parser.error('export is set to "repo", but no "repo" is defined') + if runargs.export == 'flat-manager': + if not runargs.flat_manager_remote: + parser.error('export is set to "flat-manager", but "flat-manager-remote" is not defined') + if not runargs.flat_manager_repo: + parser.error('export is set to "flat-manager", but "flat-manager-repo" is not defined') + if type(runargs.flat_manager_token_keyring_keyid) != type(runargs.flat_manager_token_keyring_service): + parser.error('only one of: "flat-manager-token-keyring-service" and ' + '"flat-manager-token-keyring-keyid" is set. ' + 'Both must be set to use the keyring.') + # We can check either service or keyid here, since we know they're both None or they're both str + if not any([runargs.flat_manager_token, runargs.flat_manager_token_file, + runargs.flat_manager_token_keyring_service]): + parser.error('export is set to "flat-manager", but no flat-manager token is defined') + + return base + + +def _flat_manager_config(args: BaseBuildArguments) -> FlatManagerConfig | None: + if args.export != 'flat-manager': + return None + + repo = args.flat_manager_repo + assert repo is not None + remote = args.flat_manager_remote + assert remote is not None + + if args.flat_manager_token: + token = args.flat_manager_token + elif p := args.flat_manager_token_file: + with open(os.path.expanduser(os.path.expandvars(p)), 'r', encoding='utf-8') as f: + token = f.read().strip() + else: + # This is imported here becaue it's optional. + # Someday this can be `lazy import`ed + try: + import keyring + except ImportError as e: + raise RuntimeError('Requested the use of `keyring` for flat-manager runtime secret, ' + 'but the keyring module cannot be imported') from e + + service = args.flat_manager_token_keyring_service + assert service is not None + keyid = args.flat_manager_token_keyring_keyid + assert keyid is not None + if t := keyring.get_password(service, keyid): + token = t + else: + raise RuntimeError(f'There is not keyring secret available for: "{service}":"{keyid}"') + + return FlatManagerConfig(remote, repo, token) + + +def _args_to_config() -> BuildFlatpakConfig | BuildRuntimeConfig | GenerateConfig: + args = _parse_args() + match args.action: + case 'build': + bargs = typing.cast('BuildArguments', args) + return BuildFlatpakConfig( + repo=bargs.repo, + cleanup=bargs.cleanup, + deltas=bargs.deltas, + export=bargs.export, + gpg=bargs.gpg, + keep_going=bargs.keep_going, + descriptions=bargs.descriptions, + flat_manager=_flat_manager_config(bargs) + ) + case 'build-runtimes': + rargs = typing.cast('BuildRuntimeArguments', args) + return BuildRuntimeConfig( + repo=rargs.repo, + cleanup=rargs.cleanup, + deltas=rargs.deltas, + export=rargs.export, + gpg=rargs.gpg, + keep_going=rargs.keep_going, + runtimes=rargs.runtimes, + flat_manager=_flat_manager_config(rargs) + ) + case 'generate': + gargs = typing.cast('GenerateArguments', args) + return GenerateConfig( + appname=gargs.appname, + archives=[gargs.archive] + gargs.archives, + engine=gargs.engine, + files=gargs.files, + patches=gargs.patches, + url=gargs.url, + ) + +def main() -> None: + config = _args_to_config() success = True - if args.action == 'build': - bargs = typing.cast('BuildArguments', args) - success = build_flatpak(bargs) - if bargs.deltas: - static_deltas(bargs) - if args.action == 'build-runtimes': - brargs = typing.cast('BuildRuntimeArguments', args) - success = build_runtimes(brargs) - if brargs.deltas: - static_deltas(brargs) - if args.action == 'generate': - success = generate(typing.cast('GenerateArguments', args)) + match config: + case BuildFlatpakConfig(): + success = build_flatpak(config) + if config.deltas: + static_deltas(config) + case BuildRuntimeConfig(): + success = build_runtimes(config) + if config.deltas: + static_deltas(config) + case GenerateConfig(): + success = generate(config) sys.exit(0 if success else 1) diff --git a/flatpaker/impl/rpgmaker.py b/flatpaker/impl/rpgmaker.py index a09ddc7..0a1fbd5 100644 --- a/flatpaker/impl/rpgmaker.py +++ b/flatpaker/impl/rpgmaker.py @@ -65,7 +65,7 @@ def write_rules(description: Description, workdir: pathlib.Path, appid: str, des ] struct = { - 'sdk': f'org.freedesktop.Sdk//{util.RUNTIME_VERSION}', + 'sdk': 'org.freedesktop.Sdk//24.08', 'runtime': 'com.github.dcbaker.flatpaker.RPGM.Platform', 'runtime-version': 'master', 'id': appid, diff --git a/flatpaker/util.py b/flatpaker/util.py index 0f4be22..692e33e 100644 --- a/flatpaker/util.py +++ b/flatpaker/util.py @@ -7,6 +7,7 @@ import hashlib import pathlib import shutil +import subprocess import tempfile import textwrap import typing @@ -14,8 +15,7 @@ if typing.TYPE_CHECKING: from .description import Description - -RUNTIME_VERSION = "24.08" + from .entry import FlatManagerConfig def _subelem(elem: ET.Element, tag: str, text: str | None = None, **extra: str) -> ET.Element: @@ -147,7 +147,7 @@ def sanitize_name(name: str) -> str: @contextlib.contextmanager -def tmpdir(name: str, cleanup: bool = True) -> typing.Iterator[pathlib.Path]: +def tmpdir(name: str, cleanup: bool = True) -> typing.Generator[pathlib.Path]: tdir = pathlib.Path(tempfile.gettempdir()) / 'flatpaker' / name tdir.mkdir(parents=True, exist_ok=True) yield tdir @@ -182,3 +182,29 @@ def bd_metadata(desktop: pathlib.Path, appdata: pathlib.Path, game: list[str]) - 'install -Dm755 game.sh -t /app/bin', ], } + + +def export_to_flat_manager(repodir: str, config: FlatManagerConfig) -> None: + env = {'REPO_TOKEN': config.token} + exe = shutil.which('flat-manager-client') + if exe is None: + raise RuntimeError('Could not find flat-manager-client!') + + out = subprocess.run( + [exe, 'create', config.remote, config.repo], + check=True, + env=env, + stdout=subprocess.PIPE, + text=True, + timeout=5, + ) + build = out.stdout.strip() + + # Now that we have a build repo, we want to ensure it is purged even if we + # somewhere along the line + try: + cmd = [exe, 'push', '--commit', '--publish', build, repodir] + subprocess.run(cmd, check=True, env=env) + finally: + subprocess.run([exe, 'purge', build], env=env, check=True) + diff --git a/pyproject.toml b/pyproject.toml index 891ce7c..ed6f4df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,9 @@ ] dependencies = ['tomlkit'] + [project.optional-dependencies] + flat-manager = ["keyring"] + [project.scripts] flatpaker = "flatpaker.entry:main"