Files
autospec/autospec/buildreq.py
T
William Douglas fa1e7951fe Allow manual override of requires
Allow developers to specify Requres that will not be dropped during
existence detection normally done for auto-detecting runtime
requirements.
2017-10-10 14:40:50 -07:00

563 lines
17 KiB
Python

#!/bin/true
#
# buildreq.py - part of autospec
# Copyright (C) 2015 Intel Corporation
#
# 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 3 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, see <http://www.gnu.org/licenses/>.
#
# Deduce and manage build requirements
#
import os
import re
import ast
import toml
import buildpattern
import util
import config
import subprocess
banned_requires = set()
buildreqs = set()
requires = set()
verbose = False
cargo_bin = False
banned_buildreqs = set(["llvm-devel",
"gcj",
"pkgconfig(dnl)",
"pkgconfig(hal)",
"tslib-0.0",
"pkgconfig(parallels-sdk)",
"oslo-python",
"libxml2No-python"])
autoreconf_reqs = ["gettext-bin",
"automake-dev",
"automake",
"m4",
"libtool",
"libtool-dev",
"pkg-config-dev"]
def add_buildreq(req):
"""
Add req to the global buildreqs set if req is not banned
"""
global buildreqs
new = True
req.strip()
if req in banned_buildreqs:
return False
if req in buildreqs:
new = False
if verbose and new:
print(" Adding buildreq:", req)
buildreqs.add(req)
return new
def add_requires(req, override=False):
"""
Add req to the global requires set if it is present in buildreqs and
os_packages and is not banned.
"""
global requires
new = True
req = req.strip()
if req in requires:
new = False
if req in banned_requires:
return False
if req not in buildreqs and req not in config.os_packages and not override:
if req:
print("requirement '{}' not found is buildreqs or os_packages, skipping".format(req))
return False
if new:
# print("Adding requirement:", req)
requires.add(req)
return new
def add_pkgconfig_buildreq(preq):
"""
Format preq as pkgconfig req and add to buildreqs
"""
if config.config_opts['32bit']:
req = "pkgconfig(32" + preq + ")"
add_buildreq(req)
req = "pkgconfig(" + preq + ")"
return add_buildreq(req)
def configure_ac_line(line):
"""
Parse configure_ac line and add appropriate buildreqs
"""
# print("----\n", line, "\n----")
# ignore comments
if line.startswith('#'):
return
pat_reqs = [(r"AC_CHECK_FUNC\([tgetent]", ["ncurses-devel"]),
("PROG_INTLTOOL", ["intltool"]),
("GETTEXT_PACKAGE", ["gettext", "perl(XML::Parser)"]),
("AM_GLIB_GNU_GETTEXT", ["gettext", "perl(XML::Parser)"]),
("GTK_DOC_CHECK", ["gtk-doc", "gtk-doc-dev", "libxslt-bin", "docbook-xml"]),
("AC_PROG_SED", ["sed"]),
("AC_PROG_GREP", ["grep"])]
for pat, reqs in pat_reqs:
if pat in line:
for req in reqs:
add_buildreq(req)
line = line.strip()
# XFCE uses an equivalent to PKG_CHECK_MODULES, handle them both the same
for style in [r"PKG_CHECK_MODULES\((.*?)\)", r"XDT_CHECK_PACKAGE\((.*?)\)"]:
match = re.search(style, line)
L = []
if match:
L = match.group(1).split(",")
if len(L) > 1:
rqlist = L[1].strip()
for req in parse_modules_list(rqlist):
add_pkgconfig_buildreq(req)
# PKG_CHECK_EXISTS(MODULES, action-if-found, action-if-not-found)
match = re.search(r"PKG_CHECK_EXISTS\((.*?)\)", line)
if match:
L = match.group(1).split(",")
rqlist = L[0].strip()
for req in parse_modules_list(rqlist):
add_pkgconfig_buildreq(req)
def is_number(num_str):
"""
Return True if num_str can be represented as a number
"""
try:
float(num_str)
return True
except ValueError:
return False
def parse_modules_list(modules_string):
"""
parse the modules_string for the list of modules, stripping out the version
requirements
"""
modules = [m.strip('[]') for m in modules_string.split()]
res = []
next_is_ver = False
for mod in modules:
if next_is_ver:
next_is_ver = False
continue
if any(s in mod for s in ['<', '>', '=']):
next_is_ver = True
continue
if is_number(mod):
continue
if mod.startswith('$'):
continue
if len(mod) >= 2:
res.append(mod)
return res
def parse_configure_ac(filename):
"""
Parse the configure.ac file for build requirements
"""
buf = ""
depth = 0
# print("Configure parse: ", filename)
buildpattern.set_build_pattern("configure_ac", 1)
f = open(filename, "r", encoding="latin-1")
while 1:
c = f.read(1)
if not c:
break
if c == "(":
depth += 1
if c == ")" and depth > 0:
depth -= 1
if c != "\n":
buf += c
if c == "\n" and depth == 0:
configure_ac_line(buf)
buf = ""
configure_ac_line(buf)
f.close()
def parse_cargo_toml(filename):
"""Update build requirements using Cargo.toml
Set the build requirements for building rust programs using cargo.
"""
global cargo_bin
buildpattern.set_build_pattern("cargo", 1)
add_buildreq("rustc")
with open(filename, "r", encoding="latin-1") as ctoml:
cargo = toml.loads(ctoml.read())
if cargo.get("bin") or os.path.exists(os.path.join(os.path.dirname(filename), "src/main.rs")):
cargo_bin = True
if not cargo.get("dependencies"):
return
for bdep in cargo["dependencies"]:
add_buildreq(bdep)
def set_build_req():
"""
Add build requirements based on the buildpattern pattern
"""
if buildpattern.default_pattern == "maven":
maven_reqs = ["apache-maven",
"xmvn",
"openjdk-dev",
"javapackages-tools",
"python3",
"six",
"lxml",
"jdk-plexus-classworlds",
"jdk-aether",
"jdk-aopalliance",
"jdk-atinject",
"jdk-cdi-api",
"jdk-commons-cli",
"jdk-commons-codec",
"jdk-commons-io",
"jdk-commons-lang",
"jdk-commons-lang3",
"jdk-commons-logging",
"jdk-guice",
"jdk-guava",
"jdk-httpcomponents-client",
"jdk-httpcomponents-core",
"jdk-jsoup",
"jdk-jsr-305",
"jdk-wagon",
"jdk-objectweb-asm",
"jdk-sisu",
"jdk-plexus-containers",
"jdk-plexus-interpolation",
"jdk-plexus-cipher",
"jdk-plexus-sec-dispatcher",
"jdk-plexus-utils",
"jdk-slf4j"]
for req in maven_reqs:
add_buildreq(req)
if buildpattern.default_pattern == "ruby":
add_buildreq("ruby")
add_buildreq("rubygem-rdoc")
if buildpattern.default_pattern == "cargo":
add_buildreq("rustc")
def rakefile(filename):
"""
Scan Rakefile for build requirements
"""
with open(filename, "r", encoding="latin-1") as f:
lines = f.readlines()
pat = re.compile(r"^require '(.*)'$")
for line in lines:
match = pat.search(line)
if match:
s = match.group(1)
if s != "rubygems" and s in config.gems:
print("Rakefile-dep: " + config.gems[s])
add_buildreq(config.gems[s])
else:
print("Rakefile-new: rubygem-" + s)
def clean_python_req(req, add_python=True):
"""
Strip version information from req
"""
if req.find("#") == 0:
return ""
ret = req.rstrip("\n\r").strip()
i = ret.find("<")
if i > 0:
ret = ret[:i]
i = ret.find("\n")
if i > 0:
ret = ret[:i]
i = ret.find(">")
if i > 0:
ret = ret[:i]
i = ret.find("=")
if i > 0:
ret = ret[:i]
i = ret.find("#")
if i > 0:
ret = ret[:i]
i = ret.find("!")
if i > 0:
ret = ret[:i]
ret = ret.strip()
# is ret actually a valid (non-empty) string?
if ret and add_python:
ret = ret.strip()
# use the dictionary to translate funky names to our current pgk names
ret = util.translate(ret)
return ret
def grab_python_requirements(descfile):
"""
Add python requirements from requirements.txt file
"""
with open(descfile, "r", encoding="latin-1") as f:
lines = f.readlines()
for line in lines:
add_requires(clean_python_req(line))
def grab_pip_requirements(pkgname):
"""
Determine python requirements for pkgname using pip show
"""
try:
pipeout = subprocess.check_output(['/usr/bin/pip3', 'show', pkgname])
except:
return
lines = pipeout.decode("utf-8").split('\n')
for line in lines:
words = line.split(" ")
if words[0] == "Requires:":
for w in words[1:]:
w2 = w.replace(",", "")
if len(w2) > 2:
print("Suggesting python requirement ", w2)
add_requires(w2)
def add_setup_py_requires(filename):
"""
Detect build requirements listed in setup.py in the install_requires and
setup_requires lists.
Handles the following patterns:
install_requires='one'
install_requires=['one', 'two', 'three']
install_requires=['one',
'two',
'three']
setup_requires=[
'one>=2.1', # >=2.1 is removed
'two',
'three'
]
setuptools.setup(
setup_requires=['one', 'two'],
...)
setuptools.setup(setup_requires=['one', 'two'], ...)
Does not evaluate variables for security purposes
"""
multiline = False
with open(filename) as f:
lines = f.readlines()
for line in lines:
if "install_requires" in line or "setup_requires" in line:
req = "install_requires" in line
# find the value for *_requires
line = line.split("=", 1)
if len(line) == 2:
line = line[1].strip()
else:
# skip because this could be a conditionally extended list
# we only want to automatically detect the core packages
continue
# easy, one-line case
if line.startswith("[") and "]" in line:
# remove the leading [ and split off everthing after the ]
line = line[1:].split("]")[0]
for item in line.split(','):
item = item.strip()
try:
# eval the string and add requirements
dep = clean_python_req(ast.literal_eval(item), False)
add_buildreq(dep)
if req:
add_requires(dep)
except:
# do not fail, the line contained a variable and
# had to be skipped
pass
continue
# more complicated, multi-line list.
# this sets the py_dep_string with the current line, which
# is the beginning of a multi-line list.
elif line.startswith("["):
multiline = True
line = line.lstrip("[")
# if the line doesn't start with '[' it is the case where
# there is (should be) a single dependency as a string
else:
line = line.strip()
try:
dep = clean_python_req(ast.literal_eval(line), False)
add_buildreq(dep)
if req:
add_requires(dep)
except:
# Do not fail, just keep looking
pass
continue
# if multiline was set above when a multi-line list was
# detected, for each line until the end bracket is found attempt to
# add the line as a buildreq
if multiline:
# if end bracket found, reset the flag
if "]" in line:
multiline = False
line = line.split("]")[0]
try:
dep = ast.literal_eval(line.split('#')[0].strip(' ,\n'))
dep = clean_python_req(dep)
add_buildreq(dep)
if req:
add_requires(dep)
except:
# do not fail, the line contained a variable and had to
# be skipped
pass
def scan_for_configure(dirn):
"""
Scan the package directory for build files to determine build pattern
"""
count = 0
for dirpath, _, files in os.walk(dirn):
default_score = 2 if dirpath == dirn else 1
if any(f.endswith(".go") for f in files):
add_buildreq("go")
buildpattern.set_build_pattern("golang", default_score)
if "CMakeLists.txt" in files and "configure.ac" not in files:
add_buildreq("cmake")
buildpattern.set_build_pattern("cmake", default_score)
if "configure" in files and os.access(dirpath + '/configure', os.X_OK):
buildpattern.set_build_pattern("configure", default_score)
if "requires.txt" in files:
grab_python_requirements(dirpath + '/requires.txt')
if "setup.py" in files:
add_buildreq("python-dev")
add_buildreq("python3-dev")
add_buildreq("setuptools")
add_buildreq("pbr")
add_buildreq("pip")
add_setup_py_requires(dirpath + '/setup.py')
buildpattern.set_build_pattern("distutils23", default_score)
if "Makefile.PL" in files or "Build.PL" in files:
buildpattern.set_build_pattern("cpan", default_score)
if "SConstruct" in files:
add_buildreq("scons")
add_buildreq("python-dev")
buildpattern.set_build_pattern("scons", default_score)
if "requirements.txt" in files:
grab_python_requirements(dirpath + '/requirements.txt')
if "meson.build" in files:
add_buildreq("meson")
add_buildreq("ninja")
add_buildreq("python3")
buildpattern.set_build_pattern("meson", default_score)
for name in files:
if name.lower() == "cargo.toml":
parse_cargo_toml(os.path.join(dirpath, name))
if name.lower().startswith("configure."):
parse_configure_ac(os.path.join(dirpath, name))
if name.lower().startswith("rakefile") and buildpattern.default_pattern == "ruby":
rakefile(os.path.join(dirpath, name))
if name.lower() == "makefile":
buildpattern.set_build_pattern("make", default_score)
if name.lower() == "autogen.sh":
buildpattern.set_build_pattern("autogen", default_score)
if name.lower() == "cmakelists.txt":
buildpattern.set_build_pattern("cmake", default_score)
can_reconf = os.path.exists(os.path.join(dirn, "configure.ac"))
if not can_reconf:
can_reconf = os.path.exists(os.path.join(dirn, "configure.in"))
if can_reconf and config.autoreconf:
print("Patches touch configure.*, adding autoreconf stage")
for breq in autoreconf_reqs:
add_buildreq(breq)
else:
config.autoreconf = False
print("Buildreqs : ", end="")
for lic in sorted(buildreqs):
if count > 4:
count = 0
print("\nBuildreqs : ", end="")
count = count + 1
print(lic + " ", end="")
print("")
def load_specfile(specfile):
"""
Load specfile object with necessary buildreq data
"""
specfile.buildreqs = buildreqs
specfile.requires = requires
specfile.cargo_bin = cargo_bin