#!/usr/bin/python3import argparseimport contextlibimport hashlibimport jsonimport loggingimport osimport subprocessimport sysimport urllib.requestfrom pathlib import Pathfrom typing import Any, List, Optional, Sequencelogger = logging.getLogger(__name__)FLATPAK_ID = 'org.cockpit_project.CockpitClient'# Local filenamesMANIFEST_JSON = f'{FLATPAK_ID}.json'PACKAGES_JSON = f'{FLATPAK_ID}.packages.json'RELEASES_XML = f'{FLATPAK_ID}.releases.xml'# Constants related to extra packagesUPSTREAM_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)raiseelse: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 = Noneif 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) changedif packages != local_packages:write_json(PACKAGES_JSON, packages)return packagesdef 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 onelocation = {'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.sha256elif '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()