commit 38f6f3feecb1ac76ea8ae8a9749be8e9795668ad Author: Jelmer Vernooij Date: Sat Oct 10 16:10:53 2020 +0000 Add ognibuild script. diff --git a/ognibuild.py b/ognibuild.py new file mode 100644 index 0000000..c5f05f1 --- /dev/null +++ b/ognibuild.py @@ -0,0 +1,264 @@ +#!/usr/bin/python +# Copyright (C) 2019-2020 Jelmer Vernooij +# 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))