337 lines
12 KiB
Python
337 lines
12 KiB
Python
# =================
|
|
# Attempt to fix all files recursively matching */pkgconfig/*.pc in $DESTDIR
|
|
# in a way that makes them portable/relocatable and thus suitable for packaging.
|
|
# By no means handles all cases, just common ones.
|
|
#
|
|
# This is largely achieved by utilizing ${pcfiledir} to ensure paths are relative
|
|
# to the .pc file itself, and attempt to verify that there are no other absoulte,
|
|
# system-dependent paths present in the files.
|
|
#
|
|
# We also substitute a couple of special cases of absolute paths that are deemed
|
|
# reasonable to expect gcc to find on its own, such as libatomic and libz,
|
|
# and attempt to fix paths that look like they are in the build directory rather
|
|
# than the install directory.
|
|
# =================
|
|
|
|
import os
|
|
import re
|
|
import sys
|
|
import argparse
|
|
from collections import defaultdict
|
|
import glob
|
|
|
|
# unquoted absolute unix paths are hard to distinguish, especially if they are in a linker statement such as -lm/bad/times (if we at the same time dont want to confuse them with relative paths -lm../this/is/not/an/abspath)
|
|
# some things may actually also like an abspath, and even be an existing path on a given system, but be used as an infix (eg /usr). there are times where we are forced to simply make an assumption
|
|
# we are not going to handle devious cases like " or ' in file names
|
|
# we are also ignoring the possibility of escaped whitespace in unquoted filenames ("\ ")
|
|
|
|
# so.. the idea for this mask is to use it after all other masks, and then check if the characters immediately preceeding it are ".." or a ${var}. if neither, check if its a path inside the install prefix and if so, we actually have path we need to handle. if it's not within the install prefix and not a currently existing file, we have to ignore it, as it might not be a path at all
|
|
unquotedAbsPathCandidate = re.compile("(/[^\\s]+)")
|
|
|
|
quotedAbsPath = re.compile("([\"'])(/[^\"]*)\\1")
|
|
# we are also not handling a windows path that is just a drive name without a path serperator (plain "C:")
|
|
winUnquotedAbsPath = re.compile("([a-zA-Z]:[\\\\/][^<>:\\?\\|\\*\"\\s]*)")
|
|
winQuotedAbsPath = re.compile("\"([a-zA-Z]:[\\\\/][^<>:\\?\\|\\*\"]*)\"")
|
|
|
|
|
|
pcVarFormat = "\\$\\{([^\\{\\}]+)\\}"
|
|
pcVarRegex = re.compile(pcVarFormat)
|
|
pcVarPrefixRegex = re.compile(pcVarFormat + "$")
|
|
|
|
# globals
|
|
line = ""
|
|
lineNr = -1
|
|
currentFileName = ""
|
|
myvars = defaultdict(lambda: "")
|
|
DESTDIR = ""
|
|
PATH_TO_ARCHIVE = ""
|
|
installPath = ""
|
|
|
|
def fix_abs_path(destdir, path):
|
|
#print("fix_abs_path: path={}, destdir={}".format(path, destdir))
|
|
outpath = path
|
|
if os.path.isabs(path):
|
|
if destdir:
|
|
normedPath = os.path.normpath(path)
|
|
normedPath = os.path.splitdrive(normedPath)[1]
|
|
if normedPath.startswith(os.sep):
|
|
normedPath = normedPath[1:]
|
|
outpath = os.path.join(destdir, normedPath)
|
|
else:
|
|
outpath = path
|
|
return outpath
|
|
|
|
# copied from https://invent.kde.org/sysadmin/ci-utilities/-/blob/master/components/CommonUtils.py
|
|
# Converts a path to a relative one, to allow for it to be passed to os.path.join
|
|
# This is primarily relevant on Windows, where full paths have the drive letter, and thus can be simply joined together as you can on Unix systems
|
|
def stripDriveOrRoot(path):
|
|
# If we're on Windows, chop the drive letter off...
|
|
if sys.platform == "win32":
|
|
return path[3:]
|
|
|
|
# Otherwise we just drop the starting slash off
|
|
return path[1:]
|
|
|
|
def substitute(s):
|
|
limit = 99
|
|
while True:
|
|
repeat = False
|
|
|
|
m = pcVarRegex.search(s)
|
|
|
|
if m and m[1]:
|
|
if m[1] == 'pc_sysrootdir':
|
|
error("${pc_sysrootdir} is not implemented")
|
|
|
|
# we need to not accidentally modify myvars here
|
|
value = m[1] in myvars and myvars[m[1]] or ""
|
|
s = s[:m.start(0)] + value + s[m.end(0):]
|
|
repeat = True
|
|
|
|
limit = limit - 1
|
|
if limit < 0:
|
|
error("probably ran into recursion")
|
|
|
|
if not repeat:
|
|
break
|
|
|
|
return s
|
|
|
|
# normalization for windows to avoid accidents around case and seperator character
|
|
def norm(path):
|
|
if not sys.platform == "win32":
|
|
return path
|
|
|
|
return os.path.normcase(path).replace('\\', '/')
|
|
|
|
def norm_abs(path):
|
|
return norm(os.path.abspath(os.path.normpath(path)))
|
|
|
|
def error(msg):
|
|
print("ERROR:", msg)
|
|
print(" " + currentFileName + ":" + str(lineNr) + ":")
|
|
print(" " + line)
|
|
sys.exit(-1)
|
|
|
|
def best_prefix(path):
|
|
bestVar = ""
|
|
bestRelPath = ""
|
|
bestDotDot = 9999
|
|
|
|
for k,v in myvars.items():
|
|
candidate = norm(os.path.normpath(substitute(v)))
|
|
normpath = norm(os.path.normpath(path))
|
|
l = len(os.path.commonprefix([normpath, candidate]))
|
|
relpath = os.path.relpath(normpath, candidate)
|
|
dd = relpath.count("..")
|
|
if l > 0 and dd <= bestDotDot:
|
|
# bias towards prefix
|
|
if dd == bestDotDot and bestVar == "prefix":
|
|
continue
|
|
|
|
bestDotDot = dd
|
|
bestRelPath = norm(relpath)
|
|
bestVar = k
|
|
|
|
if bestVar == "":
|
|
error("failed to create relative prefix for path: " + path)
|
|
|
|
separator = "/"
|
|
if myvars[bestVar].endswith('/') or myvars[bestVar].endswith('\\') or bestRelPath == ".":
|
|
separator = ""
|
|
if bestRelPath == ".":
|
|
bestRelPath = ""
|
|
|
|
return "${" + bestVar + "}" + separator + bestRelPath
|
|
|
|
def make_path_relocatable(path):
|
|
testpath = norm_abs(path)
|
|
|
|
if len(os.path.normpath(os.path.commonprefix([testpath, installPath]))) != len(installPath):
|
|
# on windows, QT references the _build directory in some places for unknown reasons, we just strip that down to the raw library file name and hope for the best
|
|
if sys.platform == 'win32':
|
|
# TODO: guard this better against a random _build directory higher up the chain if possible (this is a little convoluted due to shared/not shared install/build dirs)
|
|
# also maybe test if the file exists in archivedir/lib
|
|
# also actually test this
|
|
m = re.match(".*[/\\\\]_build[/\\\\].*[/\\\\]([\\w.]+\\.[\\w]+)$", testpath)
|
|
if m and m[1]:
|
|
return m[1]
|
|
|
|
# TODO: fold this into a list of lists
|
|
# handle some specific system deps that creep in from ffmpeg
|
|
m = re.search("/libatomic\\.(a|so)$", testpath)
|
|
if m:
|
|
return "-latomic"
|
|
m = re.search("/libz\\.(a|so)$", testpath)
|
|
if m:
|
|
return "-lz"
|
|
m = re.search("/libbz2\\.(a|so)$", testpath)
|
|
if m:
|
|
return "-lbz2"
|
|
m = re.search("/libbcrypt\\.(a|so)$", testpath)
|
|
if m:
|
|
return "-lbcrypt"
|
|
m = re.search("/libgdi32\\.a$", testpath)
|
|
if m:
|
|
return "-lgdi32"
|
|
m = re.search("/libandroid\\.(a|so)$", testpath)
|
|
if m:
|
|
return "-landroid"
|
|
|
|
error("path not within install prefix[" + installPath + "]:\n" + path)
|
|
|
|
return best_prefix(path)
|
|
|
|
def make_relocatable(s):
|
|
limit = 99
|
|
pos = 0 # we only advance pos if we found something that looks like an unqoted unix path but didn't handle
|
|
while True:
|
|
wrap = ""
|
|
match = None
|
|
matchIndex = 1
|
|
remaining = s[pos:]
|
|
|
|
limit = limit - 1
|
|
if limit < 0:
|
|
error("probably ran into recursion")
|
|
|
|
if sys.platform == 'win32':
|
|
m = winQuotedAbsPath.search(remaining)
|
|
if m and m[1]:
|
|
match = m
|
|
wrap = '"'
|
|
else:
|
|
m = winUnquotedAbsPath.search(remaining)
|
|
if m and m[1]:
|
|
match = m
|
|
else:
|
|
m = quotedAbsPath.search(remaining)
|
|
if m and m[1] and m[2]:
|
|
match = m
|
|
matchIndex = 2
|
|
wrap = m[1]
|
|
else:
|
|
m = unquotedAbsPathCandidate.search(remaining)
|
|
if m and m[1]:
|
|
prefix = remaining[:m.start(1)]
|
|
m2 = pcVarPrefixRegex.search(prefix)
|
|
if not m2 and not prefix.endswith("..") and not prefix.endswith("'") and not prefix.endswith('"') and (m[1].startswith(installPath) or os.path.exists(m[1])):
|
|
match = m
|
|
else:
|
|
pos = pos + m.end(0)
|
|
continue
|
|
|
|
if match:
|
|
s = s[:pos + match.start(0)] + wrap + make_path_relocatable(match[matchIndex]) + wrap + s[pos + match.end(0):]
|
|
else:
|
|
break
|
|
|
|
return s
|
|
|
|
def assign(lhs, rhs):
|
|
global myvars
|
|
myvars[lhs] = rhs.rstrip("\r\n")
|
|
|
|
ignoredDefs = ["conflicts", "name", "description", "version", "requires", "url"]
|
|
handledDefs = ["libs", "cflags"]
|
|
|
|
parser = argparse.ArgumentParser(description='Utility to perform a CI run for a KDE project.')
|
|
parser.add_argument('--destdir', type=str, required=False)
|
|
parser.add_argument('--prefix', type=str, required=False)
|
|
arguments = parser.parse_args()
|
|
|
|
if not arguments.destdir is None:
|
|
DESTDIR = norm_abs(arguments.destdir)
|
|
elif 'DESTDIR' in os.environ:
|
|
DESTDIR = norm_abs(os.environ['DESTDIR'])
|
|
else:
|
|
print("ERROR: DESTDIR not set")
|
|
sys.exit(-1)
|
|
|
|
if not arguments.prefix is None:
|
|
print("normabs prefix: {}".format(norm_abs(arguments.prefix)))
|
|
PATH_TO_ARCHIVE = fix_abs_path(DESTDIR, norm_abs(arguments.prefix))
|
|
elif 'KDECI_REAL_PREFIX_PATH' in os.environ:
|
|
PATH_TO_ARCHIVE = fix_abs_path(DESTDIR, norm_abs(os.environ['KDECI_REAL_PREFIX_PATH']))
|
|
else:
|
|
print("ERROR: KDECI_PATH_TO_ARCHIVE not set")
|
|
sys.exit(-1)
|
|
|
|
installPath = (sys.platform == 'win32' and PATH_TO_ARCHIVE[:2] or "") + PATH_TO_ARCHIVE[len(DESTDIR):]
|
|
installPath = norm_abs(installPath)
|
|
|
|
for subDir in ["lib/pkgconfig", "share/pkgconfig", "lib/Python.framework/Versions/Current/lib/pkgconfig"]:
|
|
stagingPcFileDir = norm(os.path.join(PATH_TO_ARCHIVE, subDir))
|
|
if not os.path.exists(stagingPcFileDir):
|
|
continue
|
|
|
|
os.chdir(stagingPcFileDir)
|
|
for currentFileName in glob.glob("*.pc"):
|
|
myvars = defaultdict(lambda: "")
|
|
assign("pcfiledir", norm(os.path.join(installPath, subDir)))
|
|
|
|
print("processing " + currentFileName + "...")
|
|
|
|
outPc = ""
|
|
with open(currentFileName, 'r') as file:
|
|
input = file.read()
|
|
|
|
lineNr = 0
|
|
for line in input.splitlines(keepends=True):
|
|
lineNr += 1
|
|
trimedLine = line.strip()
|
|
|
|
if len(trimedLine) <= 0 or trimedLine[0] == '#':
|
|
outPc += line
|
|
continue
|
|
|
|
firstColon = line.find(':')
|
|
firstEquals = line.find('=')
|
|
|
|
isDefinition = False
|
|
isAssignment = False
|
|
if firstColon < 0 and firstEquals < 0:
|
|
outPc += line
|
|
continue
|
|
elif firstEquals < 0 or (firstColon > 0 and firstColon < firstEquals):
|
|
isDefinition = True
|
|
splitPos = firstColon
|
|
else:
|
|
isAssignment = True
|
|
splitPos = firstEquals
|
|
|
|
|
|
if splitPos <= 0:
|
|
outPc += line
|
|
continue
|
|
|
|
lhsOrig = line[:splitPos]
|
|
lhs = lhsOrig.strip()
|
|
rhs = line[splitPos+1:]
|
|
|
|
if len(lhs) <= 0 or len(rhs.strip()) <= 0:
|
|
outPc += line
|
|
continue
|
|
|
|
if isDefinition:
|
|
lhs = lhs.lower()
|
|
if lhs.startswith(tuple(ignoredDefs)):
|
|
outPc += line
|
|
continue
|
|
|
|
if lhs.startswith(tuple(handledDefs)):
|
|
reloc = make_relocatable(rhs)
|
|
outPc += lhsOrig + ':' + reloc
|
|
else:
|
|
print("WARNING: ignoring unknown definition:", line)
|
|
outPc += line
|
|
else:
|
|
reloc = make_relocatable(rhs)
|
|
outLine = lhsOrig + "=" + reloc
|
|
outPc += outLine
|
|
assign(lhs, reloc)
|
|
|
|
if (input != outPc):
|
|
print("writing modified pc file: " + currentFileName)
|
|
with open(currentFileName, 'w') as file:
|
|
file.write(outPc)
|