#!/usr/bin/python3 import argparse import contextlib import hashlib import json import logging import os import subprocess import sys import urllib.request from pathlib import Path from typing import Any, List, Optional, Sequence logger = logging.getLogger(__name__) FLATPAK_ID = 'org.cockpit_project.CockpitClient' # Local filenames MANIFEST_JSON = f'{FLATPAK_ID}.json' PACKAGES_JSON = f'{FLATPAK_ID}.packages.json' RELEASES_XML = f'{FLATPAK_ID}.releases.xml' # Constants related to extra packages UPSTREAM_REPOS = [ 'cockpit-project/cockpit-files', 'cockpit-project/cockpit-machines', 'cockpit-project/cockpit-ostree', 'cockpit-project/cockpit-podman', ] DOWNSTREAM_PACKAGES_URL = f'https://raw.githubusercontent.com/flathub/{FLATPAK_ID}/master/{PACKAGES_JSON}' FLATPAK_DIR = os.path.dirname(__file__) TOP_DIR = os.path.dirname(os.path.dirname(FLATPAK_DIR)) def write_json(filename: str, content: Any) -> None: """Something like g_file_set_contents() for JSON""" tmpfile = f'{filename}.tmp' try: with open(tmpfile, 'w') as file: json.dump(content, file, indent=2) file.write('\n') except BaseException: with contextlib.suppress(OSError): os.unlink(tmpfile) raise else: os.rename(tmpfile, filename) def fetch_json(url: str) -> Any: with urllib.request.urlopen(url) as file: return json.load(file) def module_for_upstream(repo: str) -> Any: logger.info('Fetching release info for %s', repo) release = fetch_json(f'https://api.github.com/repos/{repo}/releases/latest') url = release['assets'][0]['browser_download_url'] logger.info('%s', url) with urllib.request.urlopen(url) as file: sha256 = hashlib.sha256(file.read()).hexdigest() logger.info(' %s', sha256) return { 'name': os.path.basename(repo), 'buildsystem': 'simple', 'build-commands': ['make install PREFIX=/app'], 'sources': [ { 'type': 'archive', 'url': url, 'sha256': sha256 } ] } def create_manifest( source_info: Any, *, branch: str = 'stable', extra_modules: Sequence[Any] = () ) -> Any: return { 'app-id': FLATPAK_ID, 'runtime': 'org.gnome.Platform', 'runtime-version': '51', 'sdk': 'org.gnome.Sdk', 'command': 'cockpit-client', 'rename-icon': 'cockpit-client', 'finish-args': [ '--talk-name=org.freedesktop.Flatpak', '--socket=wayland', '--socket=fallback-x11', '--device=dri', '--share=ipc' ], 'modules': [ { 'name': 'cockpit-client', 'buildsystem': 'autotools', 'config-opts': [ '--enable-cockpit-client', '--with-systemdunitdir=/invalid', 'CPPFLAGS=-Itools/mock-build-env', '--with-admin-group=root', '--disable-doc' ], 'make-args': [ 'build-for-flatpak' ], 'make-install-args': [ f'DOWNSTREAM_RELEASES_XML={RELEASES_XML}' ], 'install-rule': 'install-for-flatpak', 'sources': [ source_info, { 'type': 'file', 'path': RELEASES_XML, } ] }, *extra_modules ] } def get_packages(origin: Optional[str]) -> List[Any]: # 1. If --packages is explicitly given, always use it # 2. Otherwise, try to use the already-existing file # 3. ... but if it doesn't exist, act like 'downstream' (effectively the default) # 4. In any case, write the local file if it changed (or is new) try: with open(PACKAGES_JSON, 'r') as file: local_packages = json.load(file) except FileNotFoundError: local_packages = None if origin == 'none': packages = [] elif origin == 'upstream': packages = [module_for_upstream(repo) for repo in UPSTREAM_REPOS] elif origin == 'downstream' or local_packages is None: packages = fetch_json(DOWNSTREAM_PACKAGES_URL) else: packages = local_packages # Update local file (only if it) changed if packages != local_packages: write_json(PACKAGES_JSON, packages) return packages def main() -> None: parser = argparse.ArgumentParser() parser.add_argument('--packages', choices=('none', 'upstream', 'downstream'), help="Get extra packages list from upstream/downstream") parser.add_argument('--sha256', help="sha256sum of the source release") parser.add_argument('location', help="Location of upstream source release (default: build a new one)", nargs='?') args = parser.parse_args() packages = get_packages(args.packages) branch = 'devel' if args.location is None: try: output = subprocess.check_output([f'{TOP_DIR}/tools/make-dist'], universal_newlines=True) except subprocess.CalledProcessError as exc: sys.exit(exc.returncode) # not using the node tarball here, just the main one location = {'path': output.splitlines()[0]} elif args.location.startswith('https://'): branch = 'stable' location = {'url': args.location} elif args.location.startswith('/') and os.path.exists(args.location): location = {'path': args.location} else: parser.error('If the location is given it must be an absolute path or https URL.') if args.sha256: location['sha256'] = args.sha256 elif 'path' in location: with open(location['path'], 'rb') as file: location['sha256'] = hashlib.sha256(file.read()).hexdigest() else: parser.error('--sha256 must be provided for https URLs') manifest = create_manifest({'type': 'archive', **location}, branch=branch, extra_modules=packages) write_json(MANIFEST_JSON, manifest) Path(RELEASES_XML).touch() print(os.path.abspath(MANIFEST_JSON)) if __name__ == '__main__': main()