opam2rpm/opam2rpm.py

202 lines
7.3 KiB
Python
Raw Normal View History

#!/usr/bin/python3.11
"""
Copyright 2023, Georg Pfuetzenreuter
Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the European Commission - subsequent versions of the EUPL (the "Licence").
You may not use this work except in compliance with the Licence.
An English copy of the Licence is shipped in a file called LICENSE along with this applications source code.
You may obtain copies of the Licence in any of the official languages at https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12.
---
opam2rpm - a tool to generate openSUSE RPM packages from OCAML packages on opam.ocaml.org.
"""
from argparse import ArgumentParser
import logging
import requests
from bs4 import BeautifulSoup
import re
import jinja2
import os
import sys
from contextlib import contextmanager
import subprocess
from time import sleep
argparser = ArgumentParser()
argparser.add_argument('--debug', help='Print verbose output', action='store_const', dest='loglevel', const=logging.DEBUG, default=logging.INFO)
argparser.add_argument('--package', help='Name of the OPAM package', required=True)
argparser.add_argument('--prefixed', help='Create RPM package with "ocaml-" prefix', action='store_true', default=True)
argparser.add_argument('--opam-web', help='URL pointing to the OPAM web interface', default='https://opam.ocaml.org/')
argparser.add_argument('--obs-project-dir', help='Path to a local checkout of the desired target project')
args = argparser.parse_args()
downstream_prefixed = args.prefixed
mydir = os.path.dirname(os.path.realpath(sys.argv[0]))
def _fail(msg):
log.error(msg)
sys.exit(1)
# https://gist.github.com/howardhamilton/537e13179489d6896dd3
@contextmanager
def pushd(new_dir):
old_dir = os.getcwd()
os.chdir(new_dir)
try:
yield
finally:
os.chdir(old_dir)
def query_opam(package):
url = f'{args.opam_web}/packages/{package}'
result = requests.get(url)
status = result.status_code
log.debug(f'{url} returned {status}')
if status != 200:
_fail(f'{url} returned {status}')
return result.content
class parse_opam():
def __init__(self, content):
log.debug(f'Parsing {content}')
self.soup = BeautifulSoup(content, 'html.parser')
self.table = self.soup.find('table', attrs={'class': 'package-info'})
self.info = { element.find('th').text: [entry.find('span', {'class': 'formula-package'}).text for entry in element.findAll('li')] or element.find('td').text for element in self.table.findAll('tr') }
def get_title(self):
log.debug(f'Returning title')
return self.soup.title
def get_info(self):
log.debug(f'Returning info dict: {self.info}')
return self.info
def parse_homepage(homepage):
if 'github.com' in homepage:
repository_url = f'{homepage}.git'
log.debug(f'Set repository URL to {repository_url}')
repository_name = homepage.rsplit('/')[-1]
log.debug(f'Set repository name to {repository_name}')
# add parsing for other common homepages here
else:
_fail(f'Unsupported homepage: {homepage}')
if repository_name.startswith('ocaml-'):
is_prefixed = True
else:
is_prefixed = False
return repository_url, repository_name, is_prefixed
def evaluate_prefixing(upstream_prefixed):
if upstream_prefixed and downstream_prefixed:
# RPM package name == upstream repository name
return False
if upstream_prefixed and not downstream_prefixed:
# RPM package name = upstream repository name w/ stripped "ocaml-" prefix
_fail('Unsupported casting from upstream "ocaml-" prefixed package')
if not upstream_prefixed and downstream_prefixed:
# RPM package name = "ocaml-" + upstream repository name
return True
def parse_dependencies(dependencies):
buildrequires = []
for dependency in dependencies:
log.debug(f'Parsing dependency: {dependency}')
ocamldep = re.search('^\w+', dependency).group(0)
log.debug(f'Captured dependency: {ocamldep}')
if not ocamldep:
log.warning(f'Unable to parse upstream dependency {dependency}')
elif not ocamldep in ['ocaml', 'dune']:
buildrequires.append(f'ocamlfind({ocamldep})')
log.debug(f'Constructed OCAML build dependencies: {buildrequires}')
return buildrequires
def manglesrcname(name):
if name.startswith('ocaml-') and prefixed: # where is "prefixed" defined ?
return name.lstrip('ocaml-')
else:
return name
def manglepkgname(srcname, add_prefix):
if add_prefix:
pkgname = f'ocaml-{srcname}'
else:
pkgname = srcname
return pkgname
def render(name, srcname, url, license, buildrequires, prefixed, vcs_url=None):
if not vcs_url:
vcs_url = url
templenv = jinja2.Environment(loader=jinja2.FileSystemLoader(searchpath=f'{mydir}/templates'))
template_service = templenv.get_template('service')
template_spec = templenv.get_template('ultimate.spec')
log.debug(f'Rendering {template_spec}')
rendered_service = template_service.render(pkgname=name, url=vcs_url)
rendered_spec = template_spec.render(pkgname=srcname, url=url, buildrequires=buildrequires, license=license, mode='dune', do_prefix=prefixed)
log.debug(f'Service: {rendered_service}')
log.debug(f'Spec: {rendered_spec}')
return rendered_service, rendered_spec
def mkpac(pkgdir):
if os.path.isdir(pkgdir):
log.warning(f'Package directory {pkgdir} already exists')
else:
subprocess.run(['osc', 'mkpac', pkgdir], check=True)
def mkfile(file, data):
if os.path.isfile(file):
log.warning(f'File {file} already exists, overwriting in 10 seconds ...')
sleep(10)
with open(file, 'w') as fh:
fh.write(data)
def spec_cleaner(file):
subprocess.run(['spec-cleaner', '-i', file], check=True)
# spec-cleaner thinks "Development/Languages/OCaml" is an invalid group
with open(file, 'r') as fh:
lines = fh.readlines()
with open(file, 'w') as fh:
for line in lines:
if not 'FIXME' in line:
fh.write(line)
def main():
opam_package = parse_opam(query_opam(args.package))
info = opam_package.get_info()
repository_url, name, is_prefixed = parse_homepage(info['Homepage'])
add_prefix = evaluate_prefixing(is_prefixed)
dependencies = parse_dependencies(info['Dependencies'])
srcname = manglesrcname(name)
pkgname = manglepkgname(name, add_prefix)
data_service, data_spec = render(name, srcname, info['Homepage'], info['License'], dependencies, add_prefix, repository_url)
prjdir = args.obs_project_dir
if prjdir:
if not os.path.isfile(f'{prjdir}/.osc/_project'):
_fail(f'Invalid project directory: {prjdir}')
specfile = f'{pkgname}.spec'
with pushd(prjdir):
mkpac(pkgname)
with pushd(pkgname):
mkfile('_service', data_service)
mkfile(specfile, data_spec)
spec_cleaner(specfile)
else:
log.warning('Will not create package, --obs-project-dir was not declared')
logging.basicConfig(format='%(asctime)s %(levelname)s - %(funcName)s: %(message)s', datefmt='%H:%M:%S')
log = logging.getLogger(__name__)
if __name__ == '__main__':
log.setLevel(args.loglevel)
log.debug(args)
main()