diff --git a/bin/Package/PackageBase.py b/bin/Package/PackageBase.py index 123ff339f..dce2d759f 100644 --- a/bin/Package/PackageBase.py +++ b/bin/Package/PackageBase.py @@ -1,268 +1,270 @@ # # copyright (c) 2009 Ralf Habacker # from CraftBase import * from CraftCompiler import * from InstallDB import * from Blueprints.CraftPackageObject import * from Utils import CraftHash, GetFiles, CraftChoicePrompt from Utils.CraftManifest import CraftManifest import json class PackageBase(CraftBase): """ provides a generic interface for packages and implements the basic stuff for all packages """ # uses the following instance variables # todo: place in related ...Base # rootdir -> CraftBase # package -> PackageBase # force -> PackageBase # category -> PackageBase # version -> PackageBase # packagedir -> PackageBase # imagedir -> PackageBase def __init__(self): CraftCore.log.debug("PackageBase.__init__ called") CraftBase.__init__(self) def qmerge(self): """mergeing the imagedirectory into the filesystem""" ## \todo is this the optimal place for creating the post install scripts ? if self.package.isInstalled: self.unmerge() copiedFiles = [] # will be populated by the next call if not utils.copyDir(self.imageDir(), CraftCore.standardDirs.craftRoot(), copiedFiles=copiedFiles): return False # add package to installed database -> is this not the task of the manifest files ? revision = self.sourceRevision() package = CraftCore.installdb.addInstalled(self.package, self.version, revision=revision) fileList = self.getFileListFromDirectory(CraftCore.standardDirs.craftRoot(), copiedFiles) package.addFiles(fileList) package.install() if (CraftCore.settings.getboolean("Packager", "CreateCache") or CraftCore.settings.getboolean("Packager", "UseCache")): package.setCacheVersion(self.cacheVersion()) return True def unmerge(self): """unmergeing the files from the filesystem""" CraftCore.log.debug("Packagebase unmerge called") packageList = CraftCore.installdb.getInstalledPackages(self.package) for package in packageList: fileList = package.getFilesWithHashes() self.unmergeFileList(CraftCore.standardDirs.craftRoot(), fileList) package.uninstall() return True def strip(self, fileName, symbolDest=None): """strip debugging informations from shared libraries and executables - mingw only!!! """ if self.subinfo.options.package.disableStriping or CraftCore.compiler.isMSVC() or not CraftCore.compiler.isGCCLike(): CraftCore.log.warning(f"Skipping stripping of {fileName} -- either disabled or unsupported with this compiler") return True if OsUtils.isMac(): CraftCore.log.debug(f"Skipping stripping of files on macOS -- not implemented") return True if os.path.isabs(fileName): filepath = fileName else: CraftCore.log.warning("Please pass an absolute file path to strip") basepath = os.path.join(self.installDir()) filepath = os.path.join(basepath, "bin", fileName) if not symbolDest: return utils.system(["strip", "-s", filepath]) else: symFile = os.path.join(symbolDest, f"{os.path.basename(filepath)}.sym") return (utils.system(["objcopy", "--only-keep-debug", filepath, symFile]) and utils.system(["strip", "--strip-debug", "--strip-unneeded", filepath]) and utils.system(["objcopy", "--add-gnu-debuglink", symFile, filepath])) def createImportLibs(self, pkgName): """create the import libraries for the other compiler(if ANSI-C libs)""" basepath = os.path.join(self.installDir()) utils.createImportLibs(pkgName, basepath) def printFiles(self): packageList = CraftCore.installdb.getInstalledPackages(self.package) for package in packageList: fileList = package.getFiles() fileList.sort() for file in fileList: print(file[0]) return True def getAction(self, cmd=None): if not cmd: command = sys.argv[1] options = None # print sys.argv if (len(sys.argv) > 2): options = sys.argv[2:] else: command = cmd options = None # \todo options are not passed through by craft.py fix it return [command, options] def execute(self, cmd=None): """called to run the derived class this will be executed from the package if the package is started on its own it shouldn't be called if the package is imported as a python module""" CraftCore.log.debug("PackageBase.execute called. args: %s" % sys.argv) command, _ = self.getAction(cmd) return self.runAction(command) def fetchBinary(self, downloadRetriesLeft=3) -> bool: if self.subinfo.options.package.disableBinaryCache: return False - for url in [self.cacheLocation()] + self.cacheRepositoryUrls(): CraftCore.log.debug(f"Trying to restore {self} from cache: {url}.") if url == self.cacheLocation(): fileUrl = f"{url}/manifest.json" if os.path.exists(fileUrl): with open(fileUrl, "rt", encoding="UTF-8") as f: manifest = CraftManifest.fromJson(json.load(f)) else: continue else: manifest = CraftManifest.fromJson(CraftCore.cache.cacheJsonFromUrl(f"{url}/manifest.json")) fileEntry = manifest.get(str(self)).files files = [] for f in fileEntry: if f.version == self.version: files.append(f) - latest = None if not files: CraftCore.log.debug(f"Could not find {self}={self.version} in {url}") continue latest = files[0] + if latest.configHash and latest.configHash != self.subinfo.options.dynamic.configHash(): + CraftCore.log.warning("Failed to restore package, configuration missmatch") + return False + if url != self.cacheLocation(): downloadFolder = self.cacheLocation(os.path.join(CraftCore.standardDirs.downloadDir(), "cache")) else: downloadFolder = self.cacheLocation() localArchiveAbsPath = OsUtils.toNativePath(os.path.join(downloadFolder, latest.fileName)) localArchivePath, localArchiveName = os.path.split(localArchiveAbsPath) if url != self.cacheLocation(): if not os.path.exists(localArchiveAbsPath): os.makedirs(localArchivePath, exist_ok=True) fUrl = f"{url}/{latest.fileName}" if not GetFiles.getFile(fUrl, localArchivePath, localArchiveName): CraftCore.log.warning(f"Failed to fetch {fUrl}") return False elif not os.path.isfile(localArchiveAbsPath): continue if not CraftHash.checkFilesDigests(localArchivePath, [localArchiveName], digests=latest.checksum, digestAlgorithm=CraftHash.HashAlgorithm.SHA256): CraftCore.log.warning(f"Hash did not match, {localArchiveName} might be corrupted") if downloadRetriesLeft and CraftChoicePrompt.promptForChoice("Do you want to delete the files and redownload them?", [("Yes", True), ("No", False)], default="Yes"): return utils.deleteFile(localArchiveAbsPath) and self.fetchBinary(downloadRetriesLeft=downloadRetriesLeft-1) return False self.subinfo.buildPrefix = latest.buildPrefix self.subinfo.isCachedBuild = True if not (self.cleanImage() and utils.unpackFile(localArchivePath, localArchiveName, self.imageDir()) and self.internalPostInstall() and self.postInstall() and self.qmerge() and self.internalPostQmerge() and self.postQmerge()): return False return True return False @staticmethod def getFileListFromDirectory(imagedir, filePaths): """ create a file list containing hashes """ ret = [] algorithm = CraftHash.HashAlgorithm.SHA256 for filePath in filePaths: relativeFilePath = os.path.relpath(filePath, imagedir) digest = algorithm.stringPrefix() + CraftHash.digestFile(filePath, algorithm) ret.append((relativeFilePath, digest)) return ret @staticmethod def unmergeFileList(rootdir, fileList): """ delete files in the fileList if has matches """ for filename, filehash in fileList: fullPath = os.path.join(rootdir, os.path.normcase(filename)) if os.path.isfile(fullPath) or os.path.islink(fullPath): if filehash: algorithm = CraftHash.HashAlgorithm.getAlgorithmFromPrefix(filehash) currentHash = algorithm.stringPrefix() + CraftHash.digestFile(fullPath, algorithm) if not filehash or currentHash == filehash: OsUtils.rm(fullPath, True) else: CraftCore.log.warning( f"We can't remove {fullPath} as its hash has changed," f" that usually implies that the file was modified or replaced") elif not os.path.isdir(fullPath) and os.path.lexists(fullPath): CraftCore.log.debug(f"Remove a dead symlink {fullPath}") OsUtils.rm(fullPath, True) elif not os.path.isdir(fullPath): CraftCore.log.warning("file %s does not exist" % fullPath) containingDir = os.path.dirname(fullPath) if os.path.exists(containingDir) and not os.listdir(containingDir): CraftCore.log.debug(f"Delete empty dir {containingDir}") utils.rmtree(containingDir) def runAction(self, command): functions = {"fetch": "fetch", "cleanimage": "cleanImage", "cleanbuild": "cleanBuild", "unpack": "unpack", "compile": "compile", "configure": "configure", "make": "make", "install": ["install", "internalPostInstall"], "post-install": "postInstall", "test": "unittest", "qmerge": ["qmerge", "internalPostQmerge"], "post-qmerge": "postQmerge", "unmerge": "unmerge", "package": "createPackage", "createpatch": "createPatch", "print-files": "printFiles", "checkdigest": "checkDigest", "fetch-binary": "fetchBinary"} if command in functions: try: steps = functions[command] if not isinstance(steps, list): steps = [steps] for step in steps: if not getattr(self, step)(): return False except AttributeError as e: raise BlueprintException(str(e), self.package, e) else: CraftCore.log.error("command %s not understood" % command) return False return True diff --git a/bin/Packager/PackagerBase.py b/bin/Packager/PackagerBase.py index 6d0819727..ec0e53e77 100644 --- a/bin/Packager/PackagerBase.py +++ b/bin/Packager/PackagerBase.py @@ -1,85 +1,85 @@ # # copyright (c) 2009 Ralf Habacker # # Packager base import datetime import json import glob from CraftBase import * from Utils import CraftHash from Utils.CraftManifest import * from CraftDebug import deprecated class PackagerBase(CraftBase): """ provides a generic interface for packagers and implements basic package creating stuff """ @InitGuard.init_once def __init__(self): CraftBase.__init__(self) self.whitelist_file = [] self.blacklist_file = [] self.defines = {} self.ignoredPackages = [] def setDefaults(self, defines: {str:str}) -> {str:str}: defines = dict(defines) defines.setdefault("setupname", os.path.join(self.packageDestinationDir(), self.binaryArchiveName(includeRevision=True, fileType=""))) defines.setdefault("shortcuts", "") defines.setdefault("architecture", CraftCore.compiler.architecture) defines.setdefault("company", "KDE e.V.") defines.setdefault("productname", self.subinfo.displayName) defines.setdefault("display_name", self.subinfo.displayName) defines.setdefault("description", self.subinfo.description) defines.setdefault("icon", os.path.join(CraftCore.standardDirs.craftBin(), "data", "icons", "craft.ico")) defines.setdefault("icon_png", os.path.join(CraftCore.standardDirs.craftBin(), "data", "icons", "craftyBENDER.png")) defines.setdefault("icon_png_44", defines["icon_png"]) defines.setdefault("license", "") defines.setdefault("version", self.sourceRevision() if self.subinfo.hasSvnTarget() else self.version) defines.setdefault("website", self.subinfo.webpage if self.subinfo.webpage else "https://community.kde.org/Craft") # mac defines.setdefault("apppath", "") defines.setdefault("appname", self.package.name.lower()) return defines def getMacAppPath(self, defines, lookupPath = None): lookPath = os.path.normpath(lookupPath if lookupPath else self.archiveDir()) appPath = defines['apppath'] if not appPath: apps = glob.glob(os.path.join(lookPath, f"**/{defines['appname']}.app"), recursive=True) if len(apps) != 1: CraftCore.log.error(f"Failed to detect {defines['appname']}.app for {self}, please provide a correct self.defines['apppath'] or a relative path to the app as self.defines['apppath']") return False appPath = apps[0] appPath = os.path.join(lookPath, appPath) return os.path.normpath(appPath) def preArchive(self): utils.abstract() def archiveDir(self): return os.path.join(self.buildRoot(), "archive") # """ create a package """ def createPackage(self): utils.abstract() def _generateManifest(self, destDir, archiveName, manifestLocation=None, manifestUrls=None): if not manifestLocation: manifestLocation = destDir manifestLocation = os.path.join(manifestLocation, "manifest.json") archiveFile = os.path.join(destDir, archiveName) name = archiveName if not os.path.isabs(archiveName) else os.path.relpath(archiveName, destDir) manifest = CraftManifest.load(manifestLocation, urls=manifestUrls) entry = manifest.get(str(self)) - entry.addFile(name, CraftHash.digestFile(archiveFile, CraftHash.HashAlgorithm.SHA256), version=self.version) + entry.addFile(name, CraftHash.digestFile(archiveFile, CraftHash.HashAlgorithm.SHA256), version=self.version, config=self.subinfo.options.dynamic) manifest.dump(manifestLocation) \ No newline at end of file diff --git a/bin/Utils/CraftManifest.py b/bin/Utils/CraftManifest.py index f927e7187..c6f177d38 100644 --- a/bin/Utils/CraftManifest.py +++ b/bin/Utils/CraftManifest.py @@ -1,165 +1,169 @@ import collections import datetime import json import os - from CraftCore import CraftCore -import utils class CraftManifestEntryFile(object): def __init__(self, fileName : str, checksum : str, version : str="") -> None: self.fileName = fileName self.checksum = checksum self.date = datetime.datetime.utcnow() self.version = version self.buildPrefix = CraftCore.standardDirs.craftRoot() + self.configHash = None @staticmethod def fromJson(data : dict): out = CraftManifestEntryFile(data["fileName"], data["checksum"]) out.date = CraftManifest._parseTimeStamp(data["date"]) out.version = data.get("version", "") out.buildPrefix = data.get("buildPrefix", None) + out.configHash = data.get("configHash", None) return out def toJson(self) -> dict: - return {"fileName" : self.fileName, - "checksum" : self.checksum, - "date" : self.date.strftime(CraftManifest._TIME_FORMAT), - "version" : self.version, - "buildPrefix" : self.buildPrefix} + return { + "fileName" : self.fileName, + "checksum" : self.checksum, + "date" : self.date.strftime(CraftManifest._TIME_FORMAT), + "version" : self.version, + "buildPrefix" : self.buildPrefix, + "configHash" : self.configHash + } class CraftManifestEntry(object): def __init__(self, name : str) -> None: self.name = name self.files = [] @staticmethod def fromJson(data : dict): entry = CraftManifestEntry(data["name"]) entry.files = sorted([CraftManifestEntryFile.fromJson(fileData) for fileData in data["files"]], key=lambda x:x.date, reverse=True) return entry def toJson(self) -> dict: return {"name":self.name, "files":[x.toJson() for x in self.files]} - def addFile(self, fileName : str, checksum : str, version : str="") -> CraftManifestEntryFile: + def addFile(self, fileName : str, checksum : str, version : str="", config=None) -> CraftManifestEntryFile: f = CraftManifestEntryFile(fileName, checksum, version) + f.configHash = config.configHash() self.files.insert(0, f) return f @property def latest(self) -> CraftManifestEntryFile: return self.files[0] if self.files else None class CraftManifest(object): _TIME_FORMAT = "%Y-%m-%d %H:%M:%S.%f" def __init__(self): self.date = datetime.datetime.utcnow() self.packages = {str(CraftCore.compiler) : {}} self.origin = None @staticmethod def version() -> int: return 1 @staticmethod def _migrate0(data : dict): manifest = CraftManifest() packages = manifest.packages[str(CraftCore.compiler)] for name, package in data.items(): if not name in packages: packages[name] = CraftManifestEntry(name) p = packages[name] for fileName, pData in data[name].items(): f = p.addFile(fileName, pData["checksum"]) f.date = datetime.datetime(1, 1, 1) return manifest @staticmethod def fromJson(data : dict): version = data.get("version", 0) if version == 0: return CraftManifest._migrate0(data) elif version != CraftManifest.version(): raise Exception("Invalid manifest version detected") manifest = CraftManifest() manifest.date = CraftManifest._parseTimeStamp(data["date"]) manifest.origin = data.get("origin", None) for compiler in data["packages"]: manifest.packages[compiler] = {} for package in data["packages"][compiler]: p = CraftManifestEntry.fromJson(package) manifest.packages[compiler][p.name] = p return manifest def update(self, other): for compiler in other.packages.keys(): if not compiler in self.packages: self.packages[compiler] = {} self.packages[compiler].update(other.packages[compiler]) def toJson(self) -> dict: out = {"date": str(self.date), "origin": self.origin, "packages":{}, "version": CraftManifest.version()} for compiler, packages in self.packages.items(): out["packages"][compiler] = [x.toJson() for x in self.packages[compiler].values()] return out def get(self, package : str) -> CraftManifestEntry: compiler = str(CraftCore.compiler) if not compiler in self.packages: self.packages[compiler] = {} if not package in self.packages[compiler]: self.packages[compiler][package] = CraftManifestEntry(package) return self.packages[compiler][package] def dump(self, cacheFilePath, includeTime=False): if includeTime: name, ext = os.path.splitext(cacheFilePath) cacheFilePath = f"{name}-{self.date.strftime('%Y%m%dT%H%M%S')}{ext}" self.date = datetime.datetime.utcnow() with open(cacheFilePath, "wt+") as cacheFile: json.dump(self, cacheFile, sort_keys=True, indent=2, default=lambda x:x.toJson()) @staticmethod def load(manifestFileName : str, urls : [str]=None): """ Load a manifest. If a url is provided a manifest is fetch from that the url and merged with a local manifest. TODO: in that case we are merging all repositories so we should also merge the cache files """ old = None if not urls and ("ContinuousIntegration", "RepositoryUrl") in CraftCore.settings: urls = [CraftCore.settings.get("ContinuousIntegration", "RepositoryUrl").rstrip("/")] if urls: old = CraftManifest() for url in urls: new = CraftManifest.fromJson(CraftCore.cache.cacheJsonFromUrl(f"{url}/manifest.json")) if new: new.origin = url new.dump(manifestFileName, includeTime=True) old.update(new) cache = None if os.path.isfile(manifestFileName): try: with open(manifestFileName, "rt+") as cacheFile: cache = CraftManifest.fromJson(json.load(cacheFile)) cache.dump(manifestFileName, includeTime=True) except Exception as e: CraftCore.log.warning(f"Failed to load {cacheFile}, {e}") pass if old: if cache: old.update(cache) return old if not cache: return CraftManifest() return cache @staticmethod def _parseTimeStamp(time : str) -> datetime.datetime: return datetime.datetime.strptime(time, CraftManifest._TIME_FORMAT) diff --git a/bin/options.py b/bin/options.py index da3e5baf7..04e203fa5 100644 --- a/bin/options.py +++ b/bin/options.py @@ -1,467 +1,493 @@ ## @package property handling # # (c) copyright 2009-2011 Ralf Habacker # # # + import utils from CraftConfig import * from CraftCore import CraftCore from Blueprints.CraftPackageObject import * from CraftDebug import deprecated import configparser import atexit -import copy +import zlib +from typing import Dict + +class RegisteredOption(object): + def __init__(self, value, compatible): + self.value = value + # whether or not this change breaks binary cache compatibility + self.compatible = compatible class UserOptions(object): class UserOptionsSingleton(object): _instance = None @property def __header(self): return """\ # The content of this file is partly autogenerated # You can modify values and add settings for your blueprints # Common settings available for all blueprints are: # ignored: [True|False] # version: some version # # use the same url as defined for the target but checks out a different branch # branch: str A branch # revision: str A revision or tag, overrides branch # patchLevel: int # buildType: [Release|RelWithDebInfo|Debug] The default is defined by CraftSettings.ini [Compile]BuildType # buildTests: [True|False] # buildStatic: [True|False] # # arguments passed to the configure step # args: str # # Example: ## [libs] ## ignored = True ## ## [lib/qt5] ## version = 5.9.3 ## ignored = False ## withMySQL = True ## ## [kde/pim/akonadi] ## args = -DAKONADI_BUILD_QSQLITE=On ## # # Settings are inherited, so you can set them for a whole sub branch or a single blueprint. # While blueprint from [libs] are all ignored blueprint from [libs/qt5] are not. # """ def __init__(self): self.cachedOptions = {} self.packageOptions = {} - self.registeredOptions = {} + self.registeredOptions = {} # type: Dict[str : RegisteredOption] self.path = CraftCore.settings.get("Blueprints", "Settings", os.path.join(CraftCore.standardDirs.etcDir(), "BlueprintSettings.ini")) self.settings = configparser.ConfigParser(allow_no_value=True) self.settings.optionxform = str if os.path.isfile(self.path): self.settings.read(self.path, encoding="utf-8") def initPackage(self, option): path = option._package.path if not self.settings.has_section(path): self.settings.add_section(path) settings = self.settings[path] return settings def toBool(self, x : str) -> bool: if not x: return False return self.settings._convert_to_boolean(x) @staticmethod @atexit.register def __dump(): instance = UserOptions.UserOptionsSingleton._instance if instance: try: with open(instance.path, "wt", encoding="utf-8") as configfile: print(instance.__header, file=configfile) instance.settings.write(configfile) except Exception as e: CraftCore.log.warning(f"Failed so save {instance.path}: {e}") @staticmethod def instance(): if not UserOptions.UserOptionsSingleton._instance: UserOptions.UserOptionsSingleton._instance = UserOptions.UserOptionsSingleton() return UserOptions.UserOptionsSingleton._instance def __init__(self, package): self._cachedFromParent = {} self._package = package _register = self.registerOption _convert = self._convert _register("version", str, permanent=False) _register("branch", str, permanent=False) _register("revision", str, permanent=False) _register("patchLevel", int, permanent=False) _register("ignored", bool, permanent=False) - _register("buildTests", bool, permanent=False) + _register("buildTests", bool, permanent=False, compatible=True) _register("buildStatic",bool, permanent=False) _register("buildType", CraftCore.settings.get("Compile", "BuildType"), permanent=False) _register("args", "", permanent=False) settings = UserOptions.instance().settings if settings.has_section(package.path): _registered = UserOptions.instance().registeredOptions[package.path] - for k, v in settings[package.path].items(): - if k in _registered: - v = _convert(_registered[k], v) - setattr(self, k, v) + for key, value in settings[package.path].items(): + if key in _registered: + value = _convert(_registered[key].value, value) + setattr(self, key, value) def __str__(self): out = [] - for k, v in UserOptions.instance().registeredOptions[self._package.path].items(): - atr = getattr(self, k) + for key, option in UserOptions.instance().registeredOptions[self._package.path].items(): + value = option.value + atr = getattr(self, key) if atr is None: - if callable(v): - atr = f"({v.__name__})" + if callable(value): + atr = f"({value.__name__})" else: - atr = v - out.append((k, atr)) + atr = value + out.append((key, atr)) return ", ".join([f"{x}={y}" for x, y in sorted(out)]) + def configHash(self): + tmp = [] + for key, option in sorted(UserOptions.instance().registeredOptions[self._package.path].items()): + # ignore flags that have no influence on the archive + if not option.compatible: + value = option.value + atr = getattr(self, key) + if atr is not None: + if key == "buildType": + # Releaseand and RelWithDebInfo are compatible + atr = 1 if atr in {"Release", "RelWithDebInfo"} else 0 + tmp.append(key.encode()) + tmp.append(bytes(atr, "UTF-8") if isinstance(atr, str) else bytes([atr])) + return zlib.adler32(b"".join(tmp)) + + @staticmethod def get(package): _instance = UserOptions.instance() packagePath = package.path if packagePath in _instance.cachedOptions: option = _instance.cachedOptions[packagePath] else: option = UserOptions(package) _instance.cachedOptions[packagePath] = option return option def _convert(self, valA, valB): """ Converts valB to type(valA) """ try: if valA is None: return valB _type = valA if callable(valA) else type(valA) if _type == type(valB): return valB if _type is bool: return UserOptions.instance().toBool(valB) return _type(valB) except Exception as e: CraftCore.log.error(f"Can't convert {valB} to {_type.__name__}") raise e @staticmethod def setOptions(optionsIn): packageOptions = UserOptions.instance().packageOptions sectionRe = re.compile(r"\[([^\[\]]+)\](.*)") for o in optionsIn: key, value = o.split("=", 1) key, value = key.strip(), value.strip() match = sectionRe.findall(key) if match: # TODO: move out of options.py section, key = match[0] CraftCore.log.info(f"setOptions: [{section}]{key} = {value}") CraftCore.settings.set(section, key, value) else: package, key = key.split(".", 1) if CraftPackageObject.get(package): if package not in packageOptions: packageOptions[package] = {} CraftCore.log.info(f"setOptions: BlueprintSettings.ini [{package}]{key} = {value}") packageOptions[package][key] = value else: raise BlueprintNotFoundException(package, f"Package {package} not found, failed to set option {key} = {value}") @staticmethod def addPackageOption(package : CraftPackageObject, key : str, value : str) -> None: if package.path not in UserOptions.instance().packageOptions: UserOptions.instance().packageOptions[package.path] = {} UserOptions.instance().packageOptions[package.path][key] = value def setOption(self, key, value) -> bool: - _instance = UserOptions.instance() + _instance = UserOptions.instance() # type: UserOptions.UserOptionsSingleton package = self._package if package.path not in _instance.registeredOptions:# actually that can only happen if package is invalid CraftCore.log.error(f"{package} has no options") return False if key not in _instance.registeredOptions[package.path]: CraftCore.log.error(f"{package} unknown option {key}") CraftCore.log.error(f"Valid options are") - for opt, default in _instance.registeredOptions[package.path].items(): + for optionKey, defaultOption in _instance.registeredOptions[package.path].items(): + default = defaultOption.value default = default if callable(default) else type(default) - CraftCore.log.error(f"\t{default.__name__} : {opt}") + CraftCore.log.error(f"\t{default.__name__} : {optionKey}") return False settings = _instance.initPackage(self) if value == "" and key in settings: del settings[key] delattr(self, key) else: - value = self._convert(_instance.registeredOptions[package.path][key], value) + value = self._convert(_instance.registeredOptions[package.path][key].value, value) settings[key] = str(value) setattr(self, key, value) return True - def registerOption(self, key : str, default, permanent=True) -> bool: + def registerOption(self, key : str, default, permanent=True, compatible=False) -> bool: _instance = UserOptions.instance() package = self._package if package.path not in _instance.registeredOptions: _instance.registeredOptions[package.path] = {} if key in _instance.registeredOptions[package.path]: raise BlueprintException(f"Failed to register option:\n[{package}]\n{key}={default}\nThe setting {key} is already registered.", package) return False - _instance.registeredOptions[package.path][key] = default + _instance.registeredOptions[package.path][key] = RegisteredOption(default, compatible) if permanent: settings = _instance.initPackage(self) if key and key not in settings: settings[key] = str(default) - # don't try to save types - if not callable(default): - if not hasattr(self, key): - setattr(self, key, default) - else: - # convert type - old = getattr(self, key) - try: - new = self._convert(default, old) - except: - raise BlueprintException(f"Found an invalid option in BlueprintSettings.ini,\n[{self._package}]\n{key}={old}", self._package) - #print(key, type(old), old, type(new), new) - setattr(self, key, new) + # don't try to save types + if not callable(default): + if not hasattr(self, key): + setattr(self, key, default) + else: + # convert type + old = getattr(self, key) + try: + new = self._convert(default, old) + except: + raise BlueprintException(f"Found an invalid option in BlueprintSettings.ini,\n[{self._package}]\n{key}={old}", self._package) + #print(key, type(old), old, type(new), new) + setattr(self, key, new) return True def setDefault(self, key : str, default) -> bool: _instance = UserOptions.instance() package = self._package if key not in _instance.registeredOptions[package.path]: raise BlueprintException(f"Failed to set default for unregistered option: [{package}]{key}.", package) return False settings = _instance.initPackage(self) - _instance.registeredOptions[package.path][key] = default + _instance.registeredOptions[package.path][key].value = default if key not in settings: settings[key] = str(default) setattr(self, key, default) return True def __getattribute__(self, name): if name.startswith("_"): return super().__getattribute__(name) try: member = super().__getattribute__(name) except AttributeError: member = None if member and callable(member): return member #check cache _cache = self._cachedFromParent if not member and name in _cache: return _cache[name] out = None _instance = UserOptions.instance() _package = self._package _packagePath = _package.path if _packagePath in _instance.packageOptions and name in _instance.packageOptions[_packagePath]: if _packagePath not in _instance.registeredOptions or name not in _instance.registeredOptions[_packagePath]: raise BlueprintException(f"Package {_package} has no registered option {name}", _package) - out = self._convert(_instance.registeredOptions[_packagePath][name], _instance.packageOptions[_packagePath][name]) + out = self._convert(_instance.registeredOptions[_packagePath][name].value, _instance.packageOptions[_packagePath][name]) elif member is not None: # value is not overwritten by comand line options return member else: parent = _package.parent if parent: out = getattr(UserOptions.get(parent), name) if not out: # name is a registered option and not a type but a default value if _packagePath in _instance.registeredOptions and name in _instance.registeredOptions[_packagePath]: - default = _instance.registeredOptions[_packagePath][name] + default = _instance.registeredOptions[_packagePath][name].value if not callable(default): out = default # skip lookup in command line options and parent objects the enxt time _cache[name] = out #print(_packagePath, name, type(out), out) return out class OptionsBase(object): def __init__(self): pass ## options for the fetch action class OptionsFetch(OptionsBase): def __init__(self): ## option comment self.option = None self.ignoreExternals = False ## enable submodule support in git single branch mode self.checkoutSubmodules = False ## options for the unpack action class OptionsUnpack(OptionsBase): def __init__(self): # Use this option to run 3rd party installers self.runInstaller = False ## options for the configure action class OptionsConfigure(OptionsBase): def __init__(self, dynamic): ## with this option additional arguments could be added to the configure commmand line self.args = dynamic.args ## with this option additional arguments could be added to the configure commmand line (for static builds) self.staticArgs = "" ## set source subdirectory as source root for the configuration tool. # Sometimes it is required to take a subdirectory from the source tree as source root # directory for the configure tool, which could be enabled by this option. The value of # this option is added to sourceDir() and the result is used as source root directory. self.configurePath = None # add the cmake defines that are needed to build tests here self.testDefine = None ## run autogen in autotools self.bootstrap = False ## run "autoreconf -vfi" in autotools self.autoreconf = True ## optional arguments for autoreconf self.autoreconfArgs = "-vfi" ## Whether to add the default -I flags when running autoreconf ## This is needed since some packages fail if we pass -I to autoreconf self.useDefaultAutoreconfIncludes = True # do not use default include path self.noDefaultInclude = False ## do not use default lib path self.noDefaultLib = False ## set this attribute in case a non standard configuration # tool is required (supported currently by QMakeBuildSystem only) self.tool = False # cflags currently only used for autotools self.cflags = "" # cxxflags currently only used for autotools self.cxxflags = "" # ldflags currently only used for autotools self.ldflags = "" # the project file, this is either a .pro for qmake or a sln for msbuild self.projectFile = None # whether to not pass --datarootdir configure self.noDataRootDir = False ## options for the make action class OptionsMake(OptionsBase): def __init__(self): ## ignore make error self.ignoreErrors = None ## options for the make tool self.args = "" self.supportsMultijob = True @property @deprecated("options.make.args") def makeOptions(self): return self.args @makeOptions.setter @deprecated("options.make.args") def makeOptions(self, x): self.args = x class OptionsInstall(OptionsBase): def __init__(self): ## options passed to make on install self.args = "install" ## options for the package action class OptionsPackage(OptionsBase): def __init__(self): ## defines the package name self.packageName = None ## defines the package version self.version = None ## use compiler in package name self.withCompiler = True ## use special packaging mode (only for qt) self.specialMode = False ## pack also sources self.packSources = True ## pack from subdir of imageDir() # currently supported by SevenZipPackager self.packageFromSubDir = None ## use architecture in package name # currently supported by SevenZipPackager self.withArchitecture = False ## add file digests to the package located in the manifest sub dir ##disable stripping of binary files # needed for mysql, striping make the library unusable self.disableStriping = False ##disable the binary cache for this package self.disableBinaryCache = False ## whether to move the plugins to bin self.movePluginsToBin = utils.OsUtils.isWin() ## main option class class Options(object): def __init__(self, package=None): self.dynamic = UserOptions.get(package) ## options of the fetch action self.fetch = OptionsFetch() ## options of the unpack action self.unpack = OptionsUnpack() ## options of the configure action self.configure = OptionsConfigure(self.dynamic) self.make = OptionsMake() self.install = OptionsInstall() ## options of the package action self.package = OptionsPackage() ## add the date to the target self.dailyUpdate = False ## there is a special option available already self.buildTools = False self.useShadowBuild = True @property def buildStatic(self): return self.dynamic.buildStatic def isActive(self, package): if isinstance(package, str): package = CraftPackageObject.get(package) return not package.isIgnored()