OpenMiInk/third_party/kde-ci-utilities/components/EnvironmentHandler.py
Hellings b8c8ff98e2 Hi
2026-10-16 09:05:56 -04:00

238 lines
12 KiB
Python

import os
import sys
import sysconfig
import copy
import collections
from typing import Any, Dict, List, MutableMapping, Optional
from components import CommonUtils
# Module which handles everything to do with environment variables in relation to builds
# Method which determines the appropriate environment variables to set for builds
# This ensures that build systems and running tests can find the dependencies located within it
def generateFor( installPrefix: str, configuration : Optional[Dict[str, Any]] = None) -> MutableMapping[str, str]:
# Create our initial store for environment changes
envChanges: Dict[str, List[str]] = collections.defaultdict(list)
# We always check the install prefix we've been given - so let's look there first
envChanges = changesForPrefix( os.path.realpath( installPrefix ), envChanges )
# Are we on Windows?
if sys.platform == 'win32' and 'CRAFT_ROOT' in os.environ:
# Then we need to consider Craft...
# Grab our Craft Root directory
craftRoot = os.path.realpath(os.environ['CRAFT_ROOT'])
# Process the base directory first
envChanges = changesForPrefix( craftRoot, envChanges )
# As well as it's dev-utils directory (why it's split up like this - no clue...)
utilsPrefix = os.path.join( craftRoot, 'dev-utils' )
envChanges = changesForPrefix( utilsPrefix, envChanges )
# Are we performing an Android build?
if 'ANDROID_HOME' in os.environ and sys.platform == 'linux':
# Then we need to include the separate Qt SDK for Android
# Grab it's directory
envChanges = changesForPrefix( '/opt/Qt', envChanges )
# We also need to include other dependencies which are shipped in the Docker SDK image
envChanges = changesForPrefix( '/opt/kdeandroid-arm', envChanges )
envChanges = changesForPrefix( '/opt/kdeandroid-arm64', envChanges )
envChanges = changesForPrefix( '/home/user/android-arm-clang', envChanges )
envChanges = changesForPrefix( '/home/user/android-arm64-clang', envChanges )
# Otherwise are we on a Non-Windows / Mac platform?
elif sys.platform != 'win32' and sys.platform != 'darwin':
# Use wayland-protocols that we built from source code
envChanges = changesForPrefix( '/opt/wayland-protocols', envChanges )
# The we need to consider the system install prefix
# Normally these would be setup for us, but this is just to be absolutely sure everything is right
envChanges = changesForPrefix( "/usr/", envChanges, systemPrefix=True )
# We should also consider /usr/local/ as FreeBSD uses this as a system prefix quite extensively
envChanges = changesForPrefix( "/usr/local/", envChanges, systemPrefix=True )
# On FreeBSD LLVM gets installed to it's own separate prefix
# Make sure that is included too
if sys.platform == 'freebsd12' or sys.platform == 'freebsd13' or sys.platform == 'freebsd14' or sys.platform == 'freebsd15':
# Include the necessary LLVM path then
envChanges = changesForPrefix( "/usr/local/llvm21", envChanges )
# Now we can merge this into the real environment
splitChar = separatorCharacter()
# NOTE: convert `os.environ` to a dict before deep-copying, otherwise changes to the environment will leak to the host process
clonedEnv = copy.deepcopy( dict(os.environ) )
for variableName, variableEntries in envChanges.items():
# Join them
newEntry = splitChar.join( variableEntries )
# If the variable already exists in the system environment, we prefix ourselves on
if variableName in clonedEnv:
newEntry = '%s%s%s' % (newEntry, splitChar, clonedEnv[variableName])
# Set the variable into our cloned environment
clonedEnv[variableName] = newEntry
# If we're on Windows or MacOS, then setting XDG_DATA_DIRS will have no effect whatsoever
# Fortunately, even though Qt Upstream doesn't support this, the Craft patched version of Qt does support the QT_DATA_DIRS variable which will be followed
# To ensure all our resources can be found, we make sure QT_DATA_DIRS is set
# We leave XDG_DATA_DIRS set to ensure any tools which haven't been adapted to the Windows/Mac environment continue to work
if sys.platform == 'win32' or sys.platform == 'darwin':
clonedEnv['QT_DATA_DIRS'] = clonedEnv['XDG_DATA_DIRS']
# Qt needs hand holding in order to work properly
# We only do this if we are in a Qt 5 environment which we determine by the absence of a Qt 4 environment
if sys.platform != 'win32' and not os.path.exists('/usr/bin/qmake-qt4'):
clonedEnv['QT_SELECT'] = 'qt5'
# QtWebEngine on Windows needs additional handholding on top of the above
if sys.platform == 'win32' and 'CRAFT_ROOT' in os.environ:
# Determine where the resources folder should be found...
resourceFileLocation = os.path.join( os.environ['CRAFT_ROOT'], 'bin', 'qtwebengine_resources.pak' )
# Then make sure it exists
if os.path.exists( resourceFileLocation ):
clonedEnv['QTWEBENGINE_RESOURCES_PATH'] = os.path.join( os.environ['CRAFT_ROOT'], 'bin' )
# If this is a Linux/FreeBSD system, making sure we're going to have a UTF-8 environment
if sys.platform == 'linux' or sys.platform == 'freebsd12' or sys.platform == 'freebsd13' or sys.platform == 'freebsd14' or sys.platform == 'freebsd15':
clonedEnv['LANG'] = 'en_US.UTF-8'
# Mark ourselves as a KDE session, for things like the platform plugin, etc.
clonedEnv['XDG_CURRENT_DESKTOP'] = 'KDE'
# Ensure Git security measures don't break the likes of Git LFS / Git Annex
clonedEnv['GIT_CLONE_PROTECTION_ACTIVE'] = 'false'
# Ensure that ASAN is not disruptive
asan_options = ["new_delete_type_mismatch=0", "detect_odr_violation=0", "stack-use-after-scope=0", "alloc_dealloc_mismatch=0", "verify_asan_link_order=0"]
lsan_options = []
# Ensure LSAN is disabled if we do not have it enabled
if configuration and not configuration['Options']['enable-lsan']:
asan_options.append("detect_leaks=0")
# If LSAN is enabled then ensure we get the best results from it
if configuration and configuration['Options']['enable-lsan']:
asan_options.append("fast_unwind_on_malloc=0")
suppressionDefinitions = os.path.join(CommonUtils.scriptsBaseDirectory(), 'resources', 'lsan.supp')
lsan_options.append("suppressions=" + suppressionDefinitions)
# On FreeBSD we also want ASAN to be symbolised to aid in debugging
# Additionally, we disable container overflow detection, as this seems to have generated false positives on FreeBSD
if sys.platform == 'freebsd12' or sys.platform == 'freebsd13' or sys.platform == 'freebsd14' or sys.platform == 'freebsd15':
asan_options.append("detect_container_overflow=0")
asan_options.append("symbolize=1")
# Apply the ASAN configuration
clonedEnv['ASAN_OPTIONS'] = ":".join(asan_options)
# Apply the LSAN configuration
clonedEnv['LSAN_OPTIONS'] = ":".join(lsan_options)
# All done
return clonedEnv
def changesForPrefix( installPrefix: str, envChanges: Dict[str, List[str]], systemPrefix: bool=False ) -> Dict[str, List[str]]:
# Setup CMAKE_PREFIX_PATH
extraLocation = os.path.join( installPrefix )
if os.path.exists( extraLocation ):
envChanges['CMAKE_PREFIX_PATH'].append( extraLocation )
# Setup PATH
extraLocation = os.path.join( installPrefix, 'bin' )
if os.path.exists( extraLocation ) and not systemPrefix:
envChanges['PATH'].append(extraLocation)
# Handle those paths which involve $prefix/lib*
libDirSuffixes = ['lib', 'lib32', 'lib64', 'lib/x86_64-linux-gnu', 'libdata']
if sys.platform == 'win32':
libDirSuffixes.append('lib/qt6')
for libraryDirName in libDirSuffixes:
# Do LD_LIBRARY_PATH
extraLocation = os.path.join( installPrefix, libraryDirName )
if os.path.exists( extraLocation ) and not systemPrefix:
envChanges[ libraryPathVariableName() ].append(extraLocation)
# Now do PKG_CONFIG_PATH
extraLocation = os.path.join( installPrefix, libraryDirName, 'pkgconfig' )
if os.path.exists( extraLocation ):
envChanges['PKG_CONFIG_PATH'].append(extraLocation)
# Next up is QT_PLUGIN_PATH
for pluginDirName in ['plugins', 'qca-qt5']:
extraLocation = os.path.join( installPrefix, libraryDirName, pluginDirName )
if os.path.exists( extraLocation ):
envChanges['QT_PLUGIN_PATH'].append(extraLocation)
# Now we do QML2_IMPORT_PATH
extraLocation = os.path.join( installPrefix, libraryDirName, 'qml' )
if os.path.exists( extraLocation ):
envChanges['QML2_IMPORT_PATH'].append(extraLocation)
# For Python bindings
extraLocation = sysconfig.get_path('platlib', vars={'platbase': installPrefix, 'base': installPrefix, 'platlibdir': libraryDirName})
if os.path.exists( extraLocation ):
envChanges['PYTHONPATH'].append(extraLocation)
# .pc files are installed in $prefix/share/pkgconfig as well
extraLocation = os.path.join( installPrefix, 'share', 'pkgconfig' )
if os.path.exists( extraLocation ):
envChanges['PKG_CONFIG_PATH'].append(extraLocation)
# Special Case for QT_PLUGIN_PATH and QML2_IMPORT_PATH
# On Windows these are installed to $prefix/bin/$pluginDirName rather than under $prefix/lib/$pluginDirName
if sys.platform == 'win32':
# Check QT_PLUGIN_PATH first
for pluginDirName in ['plugins', 'qca-qt5']:
extraLocation = os.path.join( installPrefix, 'bin', pluginDirName )
if os.path.exists( extraLocation ):
envChanges['QT_PLUGIN_PATH'].append(extraLocation)
# Now we do QML2_IMPORT_PATH
extraLocation = os.path.join( installPrefix, 'bin', 'qml' )
if os.path.exists( extraLocation ):
envChanges['QML2_IMPORT_PATH'].append(extraLocation)
# Setup XDG_DATA_DIRS - used to ensure applications on XDG platforms find their resources
if sys.platform == 'win32':
# On Windows KDE_INSTALL_DATADIR and KDE_INSTALL_DATAROOTDIR will install to "bin/data" instead of "share"
# See https://api.kde.org/ecm/kde-module/KDEInstallDirs6.html
extraLocation = os.path.join( installPrefix, 'bin', 'data' )
else:
extraLocation = os.path.join( installPrefix, 'share' )
if os.path.exists( extraLocation ):
envChanges['XDG_DATA_DIRS'].append(extraLocation)
# Setup XDG_CONFIG_DIRS - needed to ensure XDG based applications find their configuration
extraLocation = os.path.join( installPrefix, 'etc/xdg' )
if os.path.exists( extraLocation ):
envChanges['XDG_CONFIG_DIRS'].append(extraLocation)
# Setup QMAKEFEATURES
# This mostly undocumented environment variable lets QMake find *.pri files which are outside Qt's install prefix
extraLocation = os.path.join( installPrefix, 'mkspecs/features' )
if os.path.exists( extraLocation ):
envChanges['QMAKEFEATURES'].append( extraLocation )
return envChanges
# Which character should be used as a separator for paths?
def separatorCharacter() -> str:
# Are we on Windows?
if sys.platform == 'win32':
# Then semi-colon it is
return ';'
# Everything else uses a full colon
return ':'
# Which environment variable is used to influence the loading of libraries?
def libraryPathVariableName() -> str:
# Are we on OS X?
if sys.platform == 'darwin':
# OS X with System Integrity Protection does not support this at all - it will be unset when calling any system binary (incl. shell interpreters)
# The variable is DYLD_LIBRARY_PATH, but considering it will only be passed through sometimes, we may as well not bother with setting it
return 'IGNORE_THIS_VARIABLE'
# Are we on Windows?
if sys.platform == 'win32':
# Windows uses PATH for this - interesting choice Microsoft...
return 'PATH'
# Everything else (Linux, FreeBSD, etc) uses LD_LIBRARY_PATH for this
return 'LD_LIBRARY_PATH'