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'