6e41b300dc
logilab-astng is packaged incorrectly by default and needs a special install script. Fixes: bug #1191042 Change-Id: I60b7fb299f2822ff69f62501a17a4087bca51bb0
508 lines
15 KiB
Python
Executable File
508 lines
15 KiB
Python
Executable File
#!/usr/bin/python
|
|
|
|
import argparse
|
|
import distutils.spawn
|
|
import logging
|
|
import re
|
|
import os
|
|
import os.path
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
|
|
import pip.util
|
|
import pkg_resources
|
|
|
|
|
|
logger = logging.getLogger()
|
|
|
|
headers = [
|
|
'Name',
|
|
'Version',
|
|
'Release',
|
|
'Copyright',
|
|
'Source',
|
|
'URL',
|
|
'Packager',
|
|
'BuildRoot',
|
|
'License',
|
|
'Prefix',
|
|
'Group',
|
|
]
|
|
|
|
package_map = {}
|
|
arch_dependent = set()
|
|
epoch_map = {}
|
|
|
|
requirements_section_re = re.compile(r'\[(.*?)\]')
|
|
version_re = re.compile(r"^(.*[^.0])(\.0+)*$")
|
|
setup_py = "setup.py"
|
|
|
|
|
|
class InstallationError(Exception):
|
|
pass
|
|
|
|
|
|
def python_name_to_key(name):
|
|
return pkg_resources.Requirement.parse(name).key
|
|
|
|
|
|
def python_key_to_rpm(python_name):
|
|
python_name = python_name.lower()
|
|
try:
|
|
return package_map[python_name]
|
|
except:
|
|
pass
|
|
python_name = python_name.replace("_", "-").replace(".", "-")
|
|
if python_name.startswith("python-"):
|
|
prefixed_name = python_name
|
|
else:
|
|
prefixed_name = "python-%s" % python_name
|
|
return prefixed_name
|
|
|
|
|
|
def egg_info_path(source_dir, filename):
|
|
base = os.path.join(source_dir, "pip-egg-info")
|
|
filenames = os.listdir(base)
|
|
if not filenames:
|
|
raise InstallationError("No files/directories in %s (from %s)"
|
|
% (base, filename))
|
|
|
|
# if we have more than one match, we pick the toplevel one.
|
|
if len(filenames) > 1:
|
|
filenames.sort(key=lambda x: x.count(os.path.sep) +
|
|
(os.path.altsep and
|
|
x.count(os.path.altsep) or 0))
|
|
return os.path.join(base, filenames[0], filename)
|
|
|
|
|
|
def egg_info_lines(source_dir, filename):
|
|
filename = egg_info_path(source_dir, filename)
|
|
if not os.path.exists(filename):
|
|
return []
|
|
with open(filename, "r") as f:
|
|
return f.readlines()
|
|
|
|
|
|
def egg_info_requirements(source_dir, extras=()):
|
|
in_extra = None
|
|
for line in egg_info_lines(source_dir, 'requires.txt'):
|
|
match = requirements_section_re.match(line.lower())
|
|
if match:
|
|
in_extra = match.group(1)
|
|
continue
|
|
if in_extra and in_extra not in extras:
|
|
# Skip requirement for an extra we aren't requiring
|
|
continue
|
|
yield line
|
|
|
|
|
|
def setup_py_one_line(source_dir, command):
|
|
"""Run `python setup.py $command` and return the last line.
|
|
|
|
python ldap is so clever that is prints extra stuff
|
|
before package name or version. Lets return the last line
|
|
"""
|
|
return call_subprocess(
|
|
[sys.executable, setup_py, command],
|
|
cwd=source_dir, show_stdout=False)[0].splitlines()[-1].strip()
|
|
|
|
|
|
def create_parser():
|
|
parser = argparse.ArgumentParser()
|
|
|
|
rpm_base = os.path.expanduser("~/rpmbuild")
|
|
source_dir = os.getcwd()
|
|
|
|
rpmbuild_executable = (distutils.spawn.find_executable("rpmbuild") or
|
|
distutils.spawn.find_executable("rpm"))
|
|
parser.add_argument(
|
|
"--pip-verbose", "-f",
|
|
action="store_true",
|
|
default=False,
|
|
help="Show pip stdout")
|
|
parser.add_argument(
|
|
"--debug", "-d",
|
|
action="store_true",
|
|
default=False,
|
|
help="Print debug information")
|
|
parser.add_argument(
|
|
"--source-only", "-s",
|
|
action="store_true",
|
|
default=False,
|
|
help="Only generate source RPM")
|
|
parser.add_argument(
|
|
"--rpm-base",
|
|
metavar="<dir>",
|
|
default=rpm_base,
|
|
help="rpmbuild directory (default: %s)" % rpm_base)
|
|
parser.add_argument(
|
|
"--rpmbuild",
|
|
metavar="<dir>",
|
|
default=rpmbuild_executable,
|
|
help="rpmbuild executable (default: %s)" % rpmbuild_executable)
|
|
parser.add_argument(
|
|
"--convert", "-c",
|
|
dest="convert",
|
|
metavar="<name>",
|
|
nargs="+",
|
|
default=[],
|
|
help="Python requirement name to be converted to RPM package names")
|
|
parser.add_argument(
|
|
dest="sources",
|
|
metavar="<dir or archive>",
|
|
nargs="*",
|
|
default=[source_dir],
|
|
help="Source directories of packages (default: current directory)")
|
|
parser.add_argument(
|
|
"--install-script",
|
|
metavar="<filename>",
|
|
default=None,
|
|
help="Specify a script for the INSTALL phase of RPM building")
|
|
parser.add_argument(
|
|
"--scripts-dir",
|
|
metavar="<dir>",
|
|
default=None,
|
|
help="Specify a directory with scripts for packages")
|
|
parser.add_argument(
|
|
"--arch-dependent", "-a",
|
|
metavar="<Python package name>",
|
|
nargs="+",
|
|
default=[],
|
|
help="Known architecture dependent packages")
|
|
parser.add_argument(
|
|
"--epoch", "-e",
|
|
metavar="<number>",
|
|
type=int,
|
|
default=None,
|
|
help="RPM epoch for generated packages")
|
|
parser.add_argument(
|
|
"--epoch-map", "-x",
|
|
metavar="<Python package name == epoch number>",
|
|
nargs="+",
|
|
default=[],
|
|
help="Forced RPM epochs for packages")
|
|
parser.add_argument(
|
|
"--package-map", "-p",
|
|
metavar="<Python package name == RPM name>",
|
|
nargs="+",
|
|
default=[],
|
|
help="Correspondence between Python and RPM package names")
|
|
return parser
|
|
|
|
|
|
def call_subprocess(cmd, cwd=None, show_stdout=True, raise_on_returncode=True):
|
|
if show_stdout:
|
|
stdout = None
|
|
else:
|
|
stdout = subprocess.PIPE
|
|
proc = subprocess.Popen(cmd, cwd=cwd, stderr=None, stdin=None, stdout=stdout)
|
|
ret = proc.communicate()
|
|
if proc.returncode:
|
|
cwd = cwd or os.getcwd()
|
|
command_desc = " ".join(cmd)
|
|
if raise_on_returncode:
|
|
raise InstallationError(
|
|
"Command %s failed with error code %s in %s"
|
|
% (command_desc, proc.returncode, cwd))
|
|
else:
|
|
logger.warn(
|
|
"Command %s had error code %s in %s"
|
|
% (command_desc, proc.returncode, cwd))
|
|
return ret
|
|
|
|
|
|
def setup_logging(options):
|
|
level = logging.DEBUG if options.debug else logging.WARNING
|
|
handler = logging.StreamHandler(sys.stderr)
|
|
logger.addHandler(handler)
|
|
logger.setLevel(level)
|
|
|
|
|
|
def truncate(text, max_len=77):
|
|
if max_len <= 0:
|
|
return ''
|
|
if len(text) < max_len:
|
|
return text
|
|
text = text[0:max_len] + "..."
|
|
return text
|
|
|
|
|
|
def build_map(arguments):
|
|
result = {}
|
|
for arg in arguments:
|
|
try:
|
|
(key, value) = arg.split("==")
|
|
key = python_name_to_key(key)
|
|
value = value.strip()
|
|
assert value
|
|
except (IndexError, ValueError, AssertionError):
|
|
raise InstallationError("Bad specifier: `%s'" % arg)
|
|
else:
|
|
result[key] = value
|
|
return result
|
|
|
|
|
|
def run_egg_info(source_dir, options):
|
|
script = """
|
|
__file__ = __SETUP_PY__
|
|
from setuptools.command import egg_info
|
|
import pkg_resources
|
|
import os
|
|
def replacement_run(self):
|
|
self.mkpath(self.egg_info)
|
|
installer = self.distribution.fetch_build_egg
|
|
for ep in pkg_resources.iter_entry_points('egg_info.writers'):
|
|
# require=False is the change we're making:
|
|
writer = ep.load(require=False)
|
|
if writer:
|
|
writer(self, ep.name, os.path.join(self.egg_info,ep.name))
|
|
self.find_sources()
|
|
egg_info.egg_info.run = replacement_run
|
|
exec(compile(open(__file__).read().replace('\\r\\n', '\\n'), __file__, 'exec'))
|
|
"""
|
|
script = script.replace('__SETUP_PY__', "'setup.py'")
|
|
egg_info_dir = os.path.join(source_dir, 'pip-egg-info')
|
|
if not os.path.exists(egg_info_dir):
|
|
os.makedirs(egg_info_dir)
|
|
egg_base_option = ['--egg-base', 'pip-egg-info']
|
|
call_subprocess(
|
|
[sys.executable, '-c', script, 'egg_info'] + egg_base_option,
|
|
cwd=source_dir,
|
|
show_stdout=options.pip_verbose)
|
|
|
|
|
|
def trim_zeroes(version):
|
|
"""RPM mishandles versions like "0.8.0". Make it happy."""
|
|
match = version_re.match(version)
|
|
if match:
|
|
return match.group(1)
|
|
return version
|
|
|
|
|
|
def requires_and_conflicts(req_list, multiline):
|
|
global epoch_map
|
|
|
|
rpm_requires = ""
|
|
rpm_conflicts = ""
|
|
for line in req_list:
|
|
try:
|
|
req = pkg_resources.Requirement.parse(line)
|
|
except:
|
|
continue
|
|
rpm_name = python_key_to_rpm(req.key)
|
|
if not req.specs:
|
|
if multiline:
|
|
rpm_requires += "\nRequires:"
|
|
rpm_requires = "%s %s" % (
|
|
rpm_requires, rpm_name)
|
|
for spec in req.specs:
|
|
# kind in ("==", "<=", ">=", "!=")
|
|
kind = spec[0]
|
|
version = trim_zeroes(spec[1])
|
|
try:
|
|
version = "%s:%s" % (epoch_map[req.key], version)
|
|
except KeyError:
|
|
pass
|
|
if kind == "!=":
|
|
if multiline:
|
|
rpm_conflicts += "\nConflicts:"
|
|
rpm_conflicts = "%s %s = %s" % (
|
|
rpm_conflicts, rpm_name, version)
|
|
continue
|
|
if kind == "==":
|
|
kind = "="
|
|
if multiline:
|
|
rpm_requires += "\nRequires:"
|
|
rpm_requires = "%s %s %s %s" % (
|
|
rpm_requires, rpm_name, kind, version)
|
|
return rpm_requires, rpm_conflicts
|
|
|
|
|
|
def clean_summary(filename):
|
|
with open(filename, "rb") as fh:
|
|
contents = fh.read()
|
|
|
|
# Fix how summary is not supposed to be more than 1 line but sometimes
|
|
# seems to be.
|
|
def summary_cleaner(mtch):
|
|
summary = mtch.group(1)
|
|
summary = summary.strip()
|
|
summary = summary.replace("\n", " ")
|
|
summary = truncate(summary)
|
|
summary = summary.strip()
|
|
return summary + "\n" + mtch.group(2)
|
|
|
|
spec_headers = [h + ":" for h in headers]
|
|
spec_headers = "|".join(spec_headers)
|
|
contents = re.sub(re.compile(r"(^Summary:.*?)(" + spec_headers+ ")", re.M|re.S),
|
|
summary_cleaner, contents)
|
|
|
|
with open(filename, "wb") as fh:
|
|
fh.write(contents)
|
|
|
|
|
|
def build_rpm(options, filename):
|
|
if os.path.isfile(filename):
|
|
temp_dir = tempfile.mkdtemp('-unpack', 'py2rpm-')
|
|
pip.util.unpack_file(filename, temp_dir, None, None)
|
|
source_dir = temp_dir
|
|
archive_name = filename
|
|
elif os.path.isdir(filename):
|
|
temp_dir = None
|
|
archive_name = None
|
|
source_dir = filename
|
|
else:
|
|
raise InstallationError(
|
|
"`%s' is not a regular file nor a directory" % filename)
|
|
|
|
run_egg_info(source_dir, options)
|
|
rpm_requires, rpm_conflicts = requires_and_conflicts(
|
|
egg_info_requirements(source_dir), multiline=False)
|
|
|
|
pkg_name = setup_py_one_line(source_dir, "--name")
|
|
pkg_key = python_name_to_key(pkg_name)
|
|
build_dir = options.rpm_base
|
|
if options.scripts_dir:
|
|
install_script = "%s/%s-install.sh" % (options.scripts_dir, pkg_key)
|
|
if not os.path.isfile(install_script):
|
|
install_script = options.install_script
|
|
else:
|
|
install_script = options.install_script
|
|
cmdline = [
|
|
sys.executable, setup_py, "bdist_rpm",
|
|
"--rpm-base", build_dir,
|
|
"--source-only",
|
|
"--install-script", install_script,
|
|
]
|
|
if rpm_requires:
|
|
cmdline += ["--requires", rpm_requires]
|
|
if rpm_conflicts:
|
|
cmdline += ["--conflicts", rpm_conflicts]
|
|
call_subprocess(cmdline, cwd=source_dir, raise_on_returncode=False)
|
|
|
|
rpm_name = python_key_to_rpm(pkg_key)
|
|
spec_name = os.path.join(build_dir, "SPECS", "%s.spec" % pkg_name)
|
|
if not os.path.exists(spec_name):
|
|
raise InstallationError("`%s' does not exist" % spec_name)
|
|
if rpm_name != pkg_name:
|
|
old_name = spec_name
|
|
spec_name = os.path.join(build_dir, "SPECS", "%s.spec" % rpm_name)
|
|
os.rename(old_name, spec_name)
|
|
cmdline = [
|
|
"sed", "-i",
|
|
"-e", "s/^Name:.*$/Name: %s/" % rpm_name,
|
|
"-e", "s/%{name}/%{pkg_name}/g",
|
|
"-e", "s/^%%define name.*$/%%define pkg_name %s/" % pkg_name,
|
|
]
|
|
epoch = epoch_map.get(pkg_key, options.epoch)
|
|
if epoch is not None:
|
|
cmdline += [
|
|
"-e", "s/^Version:/Epoch: %s\\nVersion:/" % epoch,
|
|
]
|
|
if pkg_key in arch_dependent:
|
|
cmdline += [
|
|
"-e", "/^BuildArch/d",
|
|
]
|
|
if archive_name:
|
|
cmdline += [
|
|
"-e",
|
|
"s/^Source0: .*$/Source0: %s/" % os.path.basename(archive_name)
|
|
]
|
|
shutil.copy(archive_name,
|
|
os.path.join(build_dir, "SOURCES"))
|
|
call_subprocess(cmdline + [spec_name])
|
|
cmdline = [
|
|
"sed", "-i", "-r",
|
|
"-e", "/%doc/s/ man[^ ]+//",
|
|
]
|
|
call_subprocess(cmdline + [spec_name])
|
|
clean_summary(spec_name)
|
|
|
|
if options.source_only:
|
|
rpmbuild_what = "-bs"
|
|
else:
|
|
rpmbuild_what = "-ba"
|
|
if rpmbuild_what:
|
|
call_subprocess(
|
|
[options.rpmbuild, rpmbuild_what,
|
|
"--define", "_topdir %s" % build_dir,
|
|
spec_name])
|
|
if temp_dir:
|
|
shutil.rmtree(temp_dir)
|
|
|
|
|
|
def main():
|
|
parser = create_parser()
|
|
options = parser.parse_args()
|
|
setup_logging(options)
|
|
global arch_dependent
|
|
global package_map
|
|
global epoch_map
|
|
arch_dependent = set(python_name_to_key(pkg)
|
|
for pkg in options.arch_dependent)
|
|
package_map = build_map(options.package_map)
|
|
epoch_map = build_map(options.epoch_map)
|
|
|
|
if options.convert:
|
|
rpm_requires, rpm_conflicts = requires_and_conflicts(
|
|
options.convert, multiline=True)
|
|
if rpm_requires:
|
|
print rpm_requires.strip()
|
|
if rpm_conflicts:
|
|
print rpm_conflicts.strip()
|
|
return
|
|
|
|
install_script_content = """python setup.py install -O1 --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES
|
|
abspath_installed_files=$(readlink -f INSTALLED_FILES)
|
|
(
|
|
cd $RPM_BUILD_ROOT
|
|
for i in usr/*/python*/site-packages/* usr/bin/*; do
|
|
if [ -e "$i" ]; then
|
|
sed -i "s@/$i/@DELETE_ME@" "$abspath_installed_files"
|
|
echo "/$i"
|
|
fi
|
|
done
|
|
if [ -d usr/man ]; then
|
|
rm -rf usr/share/man
|
|
mkdir -p usr/share
|
|
mv usr/man usr/share/
|
|
sed -i "s@/usr/man/@DELETE_ME@" "$abspath_installed_files"
|
|
for i in usr/share/man/*; do
|
|
echo "/$i/*"
|
|
done
|
|
fi
|
|
) >> GATHERED_FILES
|
|
{ sed '/^DELETE_ME/d' INSTALLED_FILES; cat GATHERED_FILES; } | sort -u > INSTALLED_FILES.tmp
|
|
mv -f INSTALLED_FILES{.tmp,}
|
|
"""
|
|
if not options.install_script:
|
|
tmp_install_script = tempfile.mkstemp()
|
|
options.install_script = tmp_install_script[1]
|
|
os.write(tmp_install_script[0], install_script_content)
|
|
os.close(tmp_install_script[0])
|
|
else:
|
|
tmp_install_script = None
|
|
failed_pkgs = []
|
|
for src in (os.path.abspath(sdir) for sdir in options.sources):
|
|
try:
|
|
build_rpm(options, src)
|
|
except Exception as ex:
|
|
failed_pkgs.append((src, ex))
|
|
print >> sys.stderr, ex
|
|
if tmp_install_script:
|
|
os.unlink(tmp_install_script[1])
|
|
if failed_pkgs:
|
|
print >> sys.stderr, "These packages failed to build:"
|
|
for descr in failed_pkgs:
|
|
print >> sys.stderr, "%s:\n\t%s" % descr
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
main()
|
|
except Exception as exp:
|
|
print >> sys.stderr, exp
|