2023-08-27 21:39:42 +02:00
|
|
|
#!/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
|
2023-08-27 22:28:27 +02:00
|
|
|
import os
|
2023-08-27 21:39:42 +02:00
|
|
|
import sys
|
2023-08-27 22:28:27 +02:00
|
|
|
from contextlib import contextmanager
|
|
|
|
import subprocess
|
|
|
|
from time import sleep
|
2023-08-27 21:39:42 +02:00
|
|
|
|
|
|
|
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)
|
2023-08-27 22:28:27 +02:00
|
|
|
argparser.add_argument('--prefixed', help='Create RPM package with "ocaml-" prefix', action='store_true', default=True)
|
2023-08-27 21:39:42 +02:00
|
|
|
argparser.add_argument('--opam-web', help='URL pointing to the OPAM web interface', default='https://opam.ocaml.org/')
|
2023-08-27 22:28:27 +02:00
|
|
|
argparser.add_argument('--obs-project-dir', help='Path to a local checkout of the desired target project')
|
2023-08-27 21:39:42 +02:00
|
|
|
|
|
|
|
args = argparser.parse_args()
|
|
|
|
downstream_prefixed = args.prefixed
|
2023-08-27 22:28:27 +02:00
|
|
|
mydir = os.path.dirname(os.path.realpath(sys.argv[0]))
|
2023-08-27 21:39:42 +02:00
|
|
|
|
|
|
|
def _fail(msg):
|
|
|
|
log.error(msg)
|
|
|
|
sys.exit(1)
|
|
|
|
|
2023-08-27 22:28:27 +02:00
|
|
|
# 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)
|
|
|
|
|
2023-08-27 21:39:42 +02:00
|
|
|
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
|
|
|
|
|
2023-08-27 22:28:27 +02:00
|
|
|
def manglesrcname(name):
|
2023-08-28 00:58:11 +02:00
|
|
|
log.debug(f'Mangling srcname {name}')
|
|
|
|
if name.startswith('ocaml-') and not downstream_prefixed:
|
|
|
|
name = name.removeprefix('ocaml-')
|
|
|
|
log.debug(f'Rewriting srcname to {name}')
|
|
|
|
return name
|
2023-08-27 22:28:27 +02:00
|
|
|
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):
|
2023-08-27 21:39:42 +02:00
|
|
|
if not vcs_url:
|
|
|
|
vcs_url = url
|
2023-08-27 22:28:27 +02:00
|
|
|
templenv = jinja2.Environment(loader=jinja2.FileSystemLoader(searchpath=f'{mydir}/templates'))
|
2023-08-27 21:39:42 +02:00
|
|
|
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)
|
2023-08-27 22:28:27 +02:00
|
|
|
rendered_spec = template_spec.render(pkgname=srcname, url=url, buildrequires=buildrequires, license=license, mode='dune', do_prefix=prefixed)
|
2023-08-27 21:39:42 +02:00
|
|
|
|
|
|
|
log.debug(f'Service: {rendered_service}')
|
|
|
|
log.debug(f'Spec: {rendered_spec}')
|
|
|
|
return rendered_service, rendered_spec
|
|
|
|
|
2023-08-27 22:28:27 +02:00
|
|
|
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)
|
|
|
|
|
2023-08-27 21:39:42 +02:00
|
|
|
def main():
|
|
|
|
opam_package = parse_opam(query_opam(args.package))
|
|
|
|
info = opam_package.get_info()
|
2023-08-27 22:28:27 +02:00
|
|
|
repository_url, name, is_prefixed = parse_homepage(info['Homepage'])
|
2023-08-27 21:39:42 +02:00
|
|
|
add_prefix = evaluate_prefixing(is_prefixed)
|
|
|
|
dependencies = parse_dependencies(info['Dependencies'])
|
2023-08-27 22:28:27 +02:00
|
|
|
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')
|
|
|
|
|
2023-08-27 21:39:42 +02:00
|
|
|
|
|
|
|
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()
|
|
|
|
|
|
|
|
|