ognibuild/ognibuild.py
2020-10-22 19:14:50 +01:00

264 lines
8.9 KiB
Python

#!/usr/bin/python
# Copyright (C) 2019-2020 Jelmer Vernooij <jelmer@jelmer.uk>
# encoding: utf-8
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
import os
import stat
import subprocess
import sys
from typing import List
DEFAULT_PYTHON = 'python3'
class UnidentifiedError(Exception):
def __init__(self, retcode, argv, lines):
self.retcode = retcode
self.argv = argv
self.lines = lines
class NoBuildToolsFound(Exception):
"""No supported build tools were found."""
def shebang_binary(p):
if not (os.stat(p).st_mode & stat.S_IEXEC):
return None
with open(p, 'rb') as f:
firstline = f.readline()
if not firstline.startswith(b'#!'):
return None
args = firstline[2:].split(b' ')
if args[0] in (b'/usr/bin/env', b'env'):
return os.path.basename(args[1].decode())
return os.path.basename(args[0].decode())
def note(m):
sys.stdout.write('%s\n' % m)
def warning(m):
sys.stderr.write('WARNING: %s\n' % m)
def run_with_tee(session, args: List[str], **kwargs):
p = session.Popen(
args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, **kwargs)
contents = []
while p.poll() is None:
line = p.stdout.readline()
sys.stdout.buffer.write(line)
sys.stdout.buffer.flush()
contents.append(line.decode('utf-8', 'surrogateescape'))
return p.returncode, contents
def run_apt(session, args: List[str]) -> None:
args = ['apt', '-y'] + args
retcode, lines = run_with_tee(session, args, cwd='/', user='root')
if retcode == 0:
return
raise UnidentifiedError(retcode, args, lines)
def apt_install(session, packages: List[str]) -> None:
run_apt(session, ['install'] + packages)
def run_with_build_fixer(session, args):
session.check_call(args)
def run_dist(session):
# TODO(jelmer): Check $PATH rather than hardcoding?
if not os.path.exists('/usr/bin/git'):
apt_install(session, ['git'])
# Some things want to write to the user's home directory,
# e.g. pip caches in ~/.cache
session.create_home()
if os.path.exists('package.xml'):
apt_install(session, ['php-pear', 'php-horde-core'])
note('Found package.xml, assuming pear package.')
session.check_call(['pear', 'package'])
return
if os.path.exists('pyproject.toml'):
import toml
with open('pyproject.toml', 'r') as pf:
pyproject = toml.load(pf)
if 'poetry' in pyproject.get('tool', []):
note('Found pyproject.toml with poetry section, '
'assuming poetry project.')
apt_install(session, ['python3-venv', 'python3-pip'])
session.check_call(['pip3', 'install', 'poetry'], user='root')
session.check_call(['poetry', 'build', '-f', 'sdist'])
return
if os.path.exists('setup.py'):
note('Found setup.py, assuming python project.')
apt_install(session, ['python3', 'python3-pip'])
with open('setup.py', 'r') as f:
setup_py_contents = f.read()
try:
with open('setup.cfg', 'r') as f:
setup_cfg_contents = f.read()
except FileNotFoundError:
setup_cfg_contents = ''
if 'setuptools' in setup_py_contents:
note('Reference to setuptools found, installing.')
apt_install(session, ['python3-setuptools'])
if ('setuptools_scm' in setup_py_contents or
'setuptools_scm' in setup_cfg_contents):
note('Reference to setuptools-scm found, installing.')
apt_install(
session, ['python3-setuptools-scm', 'git', 'mercurial'])
# TODO(jelmer): Install setup_requires
interpreter = shebang_binary('setup.py')
if interpreter is not None:
if interpreter == 'python2' or interpreter.startswith('python2.'):
apt_install(session, [interpreter])
elif (interpreter == 'python3' or
interpreter.startswith('python3.')):
apt_install(session, [interpreter])
else:
apt_install(session, [DEFAULT_PYTHON])
run_with_build_fixer(session, ['./setup.py', 'sdist'])
else:
# Just assume it's Python 3
apt_install(session, ['python3'])
run_with_build_fixer(session, ['python3', './setup.py', 'sdist'])
return
if os.path.exists('setup.cfg'):
note('Found setup.cfg, assuming python project.')
apt_install(session, ['python3-pep517', 'python3-pip'])
session.check_call(['python3', '-m', 'pep517.build', '-s', '.'])
return
if os.path.exists('dist.ini') and not os.path.exists('Makefile.PL'):
apt_install(session, ['libdist-inkt-perl'])
with open('dist.ini', 'rb') as f:
for line in f:
if not line.startswith(b';;'):
continue
try:
(key, value) = line[2:].split(b'=', 1)
except ValueError:
continue
if (key.strip() == b'class' and
value.strip().startswith(b"'Dist::Inkt")):
note('Found Dist::Inkt section in dist.ini, '
'assuming distinkt.')
# TODO(jelmer): install via apt if possible
session.check_call(
['cpan', 'install', value.decode().strip("'")],
user='root')
run_with_build_fixer(session, ['distinkt-dist'])
return
# Default to invoking Dist::Zilla
note('Found dist.ini, assuming dist-zilla.')
apt_install(session, ['libdist-zilla-perl'])
run_with_build_fixer(session, ['dzil', 'build', '--in', '..'])
return
if os.path.exists('package.json'):
apt_install(session, ['npm'])
run_with_build_fixer(session, ['npm', 'pack'])
return
gemfiles = [name for name in os.listdir('.') if name.endswith('.gem')]
if gemfiles:
apt_install(session, ['gem2deb'])
if len(gemfiles) > 1:
warning('More than one gemfile. Trying the first?')
run_with_build_fixer(session, ['gem2tgz', gemfiles[0]])
return
if os.path.exists('waf'):
apt_install(session, ['python3'])
run_with_build_fixer(session, ['./waf', 'dist'])
return
if os.path.exists('Makefile.PL') and not os.path.exists('Makefile'):
apt_install(session, ['perl'])
run_with_build_fixer(session, ['perl', 'Makefile.PL'])
if not os.path.exists('Makefile') and not os.path.exists('configure'):
if os.path.exists('autogen.sh'):
if shebang_binary('autogen.sh') is None:
run_with_build_fixer(session, ['/bin/sh', './autogen.sh'])
else:
run_with_build_fixer(session, ['./autogen.sh'])
elif os.path.exists('configure.ac') or os.path.exists('configure.in'):
apt_install(session, [
'autoconf', 'automake', 'gettext', 'libtool', 'gnu-standards'])
run_with_build_fixer(session, ['autoreconf', '-i'])
if not os.path.exists('Makefile') and os.path.exists('configure'):
session.check_call(['./configure'])
if os.path.exists('Makefile'):
apt_install(session, ['make'])
run_with_build_fixer(session, ['make', 'dist'])
raise NoBuildToolsFound()
class PlainSession(object):
"""Session ignoring user."""
def create_home(self):
pass
def check_call(self, args):
return subprocess.check_call(args)
def Popen(self, args, stdout=None, stderr=None, user=None, cwd=None):
return subprocess.Popen(
args, stdout=stdout, stderr=stderr, cwd=cwd)
def main(argv):
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('subcommand', type=str, choices=['dist'])
parser.add_argument(
'--directory', '-d', type=str, help='Directory for project.',
default='.')
args = parser.parse_args()
session = PlainSession()
os.chdir(args.directory)
try:
if args.subcommand == 'dist':
run_dist(session)
except NoBuildToolsFound:
note('No build tools found.')
return 1
return 0
if __name__ == '__main__':
sys.exit(main(sys.argv))