blob: c809ac40318d184c58e01b9771ba0d509e5fb654 [file] [log] [blame]
# Copyright (c) 2020-2021 The Linux Foundation
#
# SPDX-License-Identifier: Apache-2.0
import os
from west import log
from west.util import west_topdir, WestNotFound
from zspdx.cmakecache import parseCMakeCacheFile
from zspdx.cmakefileapijson import parseReply
from zspdx.datatypes import DocumentConfig, Document, File, PackageConfig, Package, RelationshipDataElementType, RelationshipData, Relationship
from zspdx.getincludes import getCIncludes
import zspdx.spdxids
# WalkerConfig contains configuration data for the Walker.
class WalkerConfig:
def __init__(self):
super(WalkerConfig, self).__init__()
# prefix for Document namespaces; should not end with "/"
self.namespacePrefix = ""
# location of build directory
self.buildDir = ""
# should also analyze for included header files?
self.analyzeIncludes = False
# should also add an SPDX document for the SDK?
self.includeSDK = False
# Walker is the main analysis class: it walks through the CMake codemodel,
# build files, and corresponding source and SDK files, and gathers the
# information needed to build the SPDX data classes.
class Walker:
# initialize with WalkerConfig
def __init__(self, cfg):
super(Walker, self).__init__()
# configuration - WalkerConfig
self.cfg = cfg
# the various Documents that we will be building
self.docBuild = None
self.docZephyr = None
self.docApp = None
self.docSDK = None
# dict of absolute file path => the Document that owns that file
self.allFileLinks = {}
# queue of pending source Files to create, process and assign
self.pendingSources = []
# queue of pending relationships to create, process and assign
self.pendingRelationships = []
# parsed CMake codemodel
self.cm = None
# parsed CMake cache dict, once we have the build path
self.cmakeCache = {}
# C compiler path from parsed CMake cache
self.compilerPath = ""
# SDK install path from parsed CMake cache
self.sdkPath = ""
# primary entry point
def makeDocuments(self):
# parse CMake cache file and get compiler path
log.inf("parsing CMake Cache file")
self.getCacheFile()
# parse codemodel from Walker cfg's build dir
log.inf("parsing CMake Codemodel files")
self.cm = self.getCodemodel()
if not self.cm:
log.err("could not parse codemodel from CMake API reply; bailing")
return False
# set up Documents
log.inf("setting up SPDX documents")
retval = self.setupDocuments()
if not retval:
return False
# walk through targets in codemodel to gather information
log.inf("walking through targets")
self.walkTargets()
# walk through pending sources and create corresponding files
log.inf("walking through pending sources files")
self.walkPendingSources()
# walk through pending relationship data and create relationships
log.inf("walking through pending relationships")
self.walkRelationships()
return True
# parse cache file and pull out relevant data
def getCacheFile(self):
cacheFilePath = os.path.join(self.cfg.buildDir, "CMakeCache.txt")
self.cmakeCache = parseCMakeCacheFile(cacheFilePath)
if self.cmakeCache:
self.compilerPath = self.cmakeCache.get("CMAKE_C_COMPILER", "")
self.sdkPath = self.cmakeCache.get("ZEPHYR_SDK_INSTALL_DIR", "")
# determine path from build dir to CMake file-based API index file, then
# parse it and return the Codemodel
def getCodemodel(self):
log.dbg("getting codemodel from CMake API reply files")
# make sure the reply directory exists
cmakeReplyDirPath = os.path.join(self.cfg.buildDir, ".cmake", "api", "v1", "reply")
if not os.path.exists(cmakeReplyDirPath):
log.err(f'cmake api reply directory {cmakeReplyDirPath} does not exist')
log.err('was query directory created before cmake build ran?')
return None
if not os.path.isdir(cmakeReplyDirPath):
log.err(f'cmake api reply directory {cmakeReplyDirPath} exists but is not a directory')
return None
# find file with "index" prefix; there should only be one
indexFilePath = ""
for f in os.listdir(cmakeReplyDirPath):
if f.startswith("index"):
indexFilePath = os.path.join(cmakeReplyDirPath, f)
break
if indexFilePath == "":
# didn't find it
log.err(f'cmake api reply index file not found in {cmakeReplyDirPath}')
return None
# parse it
return parseReply(indexFilePath)
# set up Documents before beginning
def setupDocuments(self):
log.dbg("setting up placeholder documents")
# set up build document
cfgBuild = DocumentConfig()
cfgBuild.name = "build"
cfgBuild.namespace = self.cfg.namespacePrefix + "/build"
cfgBuild.docRefID = "DocumentRef-build"
self.docBuild = Document(cfgBuild)
# we'll create the build packages in walkTargets()
# the DESCRIBES relationship for the build document will be
# with the zephyr_final package
rd = RelationshipData()
rd.ownerType = RelationshipDataElementType.DOCUMENT
rd.ownerDocument = self.docBuild
rd.otherType = RelationshipDataElementType.TARGETNAME
rd.otherTargetName = "zephyr_final"
rd.rlnType = "DESCRIBES"
# add it to pending relationships queue
self.pendingRelationships.append(rd)
# set up zephyr document
cfgZephyr = DocumentConfig()
cfgZephyr.name = "zephyr-sources"
cfgZephyr.namespace = self.cfg.namespacePrefix + "/zephyr"
cfgZephyr.docRefID = "DocumentRef-zephyr"
self.docZephyr = Document(cfgZephyr)
# also set up zephyr sources package
cfgPackageZephyr = PackageConfig()
cfgPackageZephyr.name = "zephyr-sources"
cfgPackageZephyr.spdxID = "SPDXRef-zephyr-sources"
# relativeBaseDir is Zephyr sources topdir
try:
cfgPackageZephyr.relativeBaseDir = west_topdir(self.cm.paths_source)
except WestNotFound:
log.err(f"cannot find west_topdir for CMake Codemodel sources path {self.cm.paths_source}; bailing")
return False
pkgZephyr = Package(cfgPackageZephyr, self.docZephyr)
self.docZephyr.pkgs[pkgZephyr.cfg.spdxID] = pkgZephyr
# create DESCRIBES relationship data
rd = RelationshipData()
rd.ownerType = RelationshipDataElementType.DOCUMENT
rd.ownerDocument = self.docZephyr
rd.otherType = RelationshipDataElementType.PACKAGEID
rd.otherPackageID = cfgPackageZephyr.spdxID
rd.rlnType = "DESCRIBES"
# add it to pending relationships queue
self.pendingRelationships.append(rd)
# set up app document
cfgApp = DocumentConfig()
cfgApp.name = "app-sources"
cfgApp.namespace = self.cfg.namespacePrefix + "/app"
cfgApp.docRefID = "DocumentRef-app"
self.docApp = Document(cfgApp)
# also set up app sources package
cfgPackageApp = PackageConfig()
cfgPackageApp.name = "app-sources"
cfgPackageApp.spdxID = "SPDXRef-app-sources"
# relativeBaseDir is app sources dir
cfgPackageApp.relativeBaseDir = self.cm.paths_source
pkgApp = Package(cfgPackageApp, self.docApp)
self.docApp.pkgs[pkgApp.cfg.spdxID] = pkgApp
# create DESCRIBES relationship data
rd = RelationshipData()
rd.ownerType = RelationshipDataElementType.DOCUMENT
rd.ownerDocument = self.docApp
rd.otherType = RelationshipDataElementType.PACKAGEID
rd.otherPackageID = cfgPackageApp.spdxID
rd.rlnType = "DESCRIBES"
# add it to pending relationships queue
self.pendingRelationships.append(rd)
if self.cfg.includeSDK:
# set up SDK document
cfgSDK = DocumentConfig()
cfgSDK.name = "sdk"
cfgSDK.namespace = self.cfg.namespacePrefix + "/sdk"
cfgSDK.docRefID = "DocumentRef-sdk"
self.docSDK = Document(cfgSDK)
# also set up zephyr sdk package
cfgPackageSDK = PackageConfig()
cfgPackageSDK.name = "sdk"
cfgPackageSDK.spdxID = "SPDXRef-sdk"
# relativeBaseDir is SDK dir
cfgPackageSDK.relativeBaseDir = self.sdkPath
pkgSDK = Package(cfgPackageSDK, self.docSDK)
self.docSDK.pkgs[pkgSDK.cfg.spdxID] = pkgSDK
# create DESCRIBES relationship data
rd = RelationshipData()
rd.ownerType = RelationshipDataElementType.DOCUMENT
rd.ownerDocument = self.docSDK
rd.otherType = RelationshipDataElementType.PACKAGEID
rd.otherPackageID = cfgPackageSDK.spdxID
rd.rlnType = "DESCRIBES"
# add it to pending relationships queue
self.pendingRelationships.append(rd)
return True
# walk through targets and gather information
def walkTargets(self):
log.dbg("walking targets from codemodel")
# assuming just one configuration; consider whether this is incorrect
cfgTargets = self.cm.configurations[0].configTargets
for cfgTarget in cfgTargets:
# build the Package for this target
pkg = self.initConfigTargetPackage(cfgTarget)
# see whether this target has any build artifacts at all
if len(cfgTarget.target.artifacts) > 0:
# add its build file
bf = self.addBuildFile(cfgTarget, pkg)
# get its source files if build file is found
if bf:
self.collectPendingSourceFiles(cfgTarget, pkg, bf)
else:
log.dbg(f" - target {cfgTarget.name} has no build artifacts")
# get its target dependencies
self.collectTargetDependencies(cfgTargets, cfgTarget, pkg)
# build a Package in the Build doc for the given ConfigTarget
def initConfigTargetPackage(self, cfgTarget):
log.dbg(f" - initializing Package for target: {cfgTarget.name}")
# create target Package's config
cfg = PackageConfig()
cfg.name = cfgTarget.name
cfg.spdxID = "SPDXRef-" + zspdx.spdxids.convertToSPDXIDSafe(cfgTarget.name)
cfg.relativeBaseDir = self.cm.paths_build
# build Package
pkg = Package(cfg, self.docBuild)
# add Package to build Document
self.docBuild.pkgs[cfg.spdxID] = pkg
return pkg
# create a target's build product File and add it to its Package
# call with:
# 1) ConfigTarget
# 2) Package for that target
# returns: File
def addBuildFile(self, cfgTarget, pkg):
# assumes only one artifact in each target
artifactPath = os.path.join(pkg.cfg.relativeBaseDir, cfgTarget.target.artifacts[0])
log.dbg(f" - adding File {artifactPath}")
log.dbg(f" - relativeBaseDir: {pkg.cfg.relativeBaseDir}")
log.dbg(f" - artifacts[0]: {cfgTarget.target.artifacts[0]}")
# don't create build File if artifact path points to nonexistent file
if not os.path.exists(artifactPath):
log.dbg(f" - target {cfgTarget.name} lists build artifact {artifactPath} but file not found after build; skipping")
return None
# create build File
bf = File(self.docBuild, pkg)
bf.abspath = artifactPath
bf.relpath = cfgTarget.target.artifacts[0]
# can use nameOnDisk b/c it is just the filename w/out directory paths
bf.spdxID = zspdx.spdxids.getUniqueFileID(cfgTarget.target.nameOnDisk, self.docBuild.timesSeen)
# don't fill hashes / licenses / rlns now, we'll do that after walking
# add File to Package
pkg.files[bf.spdxID] = bf
# add file path link to Document and global links
self.docBuild.fileLinks[bf.abspath] = bf
self.allFileLinks[bf.abspath] = self.docBuild
# also set this file as the target package's build product file
pkg.targetBuildFile = bf
return bf
# collect a target's source files, add to pending sources queue, and
# create pending relationship data entry
# call with:
# 1) ConfigTarget
# 2) Package for that target
# 3) build File for that target
def collectPendingSourceFiles(self, cfgTarget, pkg, bf):
log.dbg(f" - collecting source files and adding to pending queue")
targetIncludesSet = set()
# walk through target's sources
for src in cfgTarget.target.sources:
log.dbg(f" - add pending source file and relationship for {src.path}")
# get absolute path if we don't have it
srcAbspath = src.path
if not os.path.isabs(src.path):
srcAbspath = os.path.join(self.cm.paths_source, src.path)
# check whether it even exists
if not (os.path.exists(srcAbspath) and os.path.isfile(srcAbspath)):
log.dbg(f" - {srcAbspath} does not exist but is referenced in sources for target {pkg.cfg.name}; skipping")
continue
# add it to pending source files queue
self.pendingSources.append(srcAbspath)
# create relationship data
rd = RelationshipData()
rd.ownerType = RelationshipDataElementType.FILENAME
rd.ownerFileAbspath = bf.abspath
rd.otherType = RelationshipDataElementType.FILENAME
rd.otherFileAbspath = srcAbspath
rd.rlnType = "GENERATED_FROM"
# add it to pending relationships queue
self.pendingRelationships.append(rd)
# collect this source file's includes
if self.cfg.analyzeIncludes and self.compilerPath:
includes = self.collectIncludes(cfgTarget, pkg, bf, src)
for inc in includes:
targetIncludesSet.add(inc)
# make relationships for the overall included files,
# avoiding duplicates for multiple source files including
# the same headers
targetIncludesList = list(targetIncludesSet)
targetIncludesList.sort()
for inc in targetIncludesList:
# add it to pending source files queue
self.pendingSources.append(inc)
# create relationship data
rd = RelationshipData()
rd.ownerType = RelationshipDataElementType.FILENAME
rd.ownerFileAbspath = bf.abspath
rd.otherType = RelationshipDataElementType.FILENAME
rd.otherFileAbspath = inc
rd.rlnType = "GENERATED_FROM"
# add it to pending relationships queue
self.pendingRelationships.append(rd)
# collect the include files corresponding to this source file
# call with:
# 1) ConfigTarget
# 2) Package for this target
# 3) build File for this target
# 4) TargetSource entry for this source file
# returns: sorted list of include files for this source file
def collectIncludes(self, cfgTarget, pkg, bf, src):
# get the right compile group for this source file
if len(cfgTarget.target.compileGroups) < (src.compileGroupIndex + 1):
log.dbg(f" - {cfgTarget.target.name} has compileGroupIndex {src.compileGroupIndex} but only {len(cfgTarget.target.compileGroups)} found; skipping included files search")
return []
cg = cfgTarget.target.compileGroups[src.compileGroupIndex]
# currently only doing C includes
if cg.language != "C":
log.dbg(f" - {cfgTarget.target.name} has compile group language {cg.language} but currently only searching includes for C files; skipping included files search")
return []
srcAbspath = src.path
if src.path[0] != "/":
srcAbspath = os.path.join(self.cm.paths_source, src.path)
return getCIncludes(self.compilerPath, srcAbspath, cg)
# collect relationships for dependencies of this target Package
# call with:
# 1) all ConfigTargets from CodeModel
# 2) this particular ConfigTarget
# 3) Package for this Target
def collectTargetDependencies(self, cfgTargets, cfgTarget, pkg):
log.dbg(f" - collecting target dependencies for {pkg.cfg.name}")
# walk through target's dependencies
for dep in cfgTarget.target.dependencies:
# extract dep name from its id
depFragments = dep.id.split(":")
depName = depFragments[0]
log.dbg(f" - adding pending relationship for {depName}")
# create relationship data between dependency packages
rd = RelationshipData()
rd.ownerType = RelationshipDataElementType.TARGETNAME
rd.ownerTargetName = pkg.cfg.name
rd.otherType = RelationshipDataElementType.TARGETNAME
rd.otherTargetName = depName
rd.rlnType = "HAS_PREREQUISITE"
# add it to pending relationships queue
self.pendingRelationships.append(rd)
# if this is a target with any build artifacts (e.g. non-UTILITY),
# also create STATIC_LINK relationship for dependency build files,
# together with this Package's own target build file
if len(cfgTarget.target.artifacts) == 0:
continue
# find the filename for the dependency's build product, using the
# codemodel (since we might not have created this dependency's
# Package or File yet)
depAbspath = ""
for ct in cfgTargets:
if ct.name == depName:
# skip utility targets
if len(ct.target.artifacts) == 0:
continue
# all targets use the same relativeBaseDir, so this works
# even though pkg is the owner package
depAbspath = os.path.join(pkg.cfg.relativeBaseDir, ct.target.artifacts[0])
break
if depAbspath == "":
continue
# create relationship data between build files
rd = RelationshipData()
rd.ownerType = RelationshipDataElementType.FILENAME
rd.ownerFileAbspath = pkg.targetBuildFile.abspath
rd.otherType = RelationshipDataElementType.FILENAME
rd.otherFileAbspath = depAbspath
rd.rlnType = "STATIC_LINK"
# add it to pending relationships queue
self.pendingRelationships.append(rd)
# walk through pending sources and create corresponding files,
# assigning them to the appropriate Document and Package
def walkPendingSources(self):
log.dbg(f"walking pending sources")
# only one package in each doc; get it
pkgZephyr = list(self.docZephyr.pkgs.values())[0]
pkgApp = list(self.docApp.pkgs.values())[0]
if self.cfg.includeSDK:
pkgSDK = list(self.docSDK.pkgs.values())[0]
for srcAbspath in self.pendingSources:
# check whether we've already seen it
srcDoc = self.allFileLinks.get(srcAbspath, None)
srcPkg = None
if srcDoc:
log.dbg(f" - {srcAbspath}: already seen, assigned to {srcDoc.cfg.name}")
continue
# not yet assigned; figure out where it goes
pkgBuild = self.findBuildPackage(srcAbspath)
if pkgBuild:
log.dbg(f" - {srcAbspath}: assigning to build document, package {pkgBuild.cfg.name}")
srcDoc = self.docBuild
srcPkg = pkgBuild
elif self.cfg.includeSDK and os.path.commonpath([srcAbspath, pkgSDK.cfg.relativeBaseDir]) == pkgSDK.cfg.relativeBaseDir:
log.dbg(f" - {srcAbspath}: assigning to sdk document")
srcDoc = self.docSDK
srcPkg = pkgSDK
elif os.path.commonpath([srcAbspath, pkgApp.cfg.relativeBaseDir]) == pkgApp.cfg.relativeBaseDir:
log.dbg(f" - {srcAbspath}: assigning to app document")
srcDoc = self.docApp
srcPkg = pkgApp
elif os.path.commonpath([srcAbspath, pkgZephyr.cfg.relativeBaseDir]) == pkgZephyr.cfg.relativeBaseDir:
log.dbg(f" - {srcAbspath}: assigning to zephyr document")
srcDoc = self.docZephyr
srcPkg = pkgZephyr
else:
log.dbg(f" - {srcAbspath}: can't determine which document should own; skipping")
continue
# create File and assign it to the Package and Document
sf = File(srcDoc, srcPkg)
sf.abspath = srcAbspath
sf.relpath = os.path.relpath(srcAbspath, srcPkg.cfg.relativeBaseDir)
filenameOnly = os.path.split(srcAbspath)[1]
sf.spdxID = zspdx.spdxids.getUniqueFileID(filenameOnly, srcDoc.timesSeen)
# don't fill hashes / licenses / rlns now, we'll do that after walking
# add File to Package
srcPkg.files[sf.spdxID] = sf
# add file path link to Document and global links
srcDoc.fileLinks[sf.abspath] = sf
self.allFileLinks[sf.abspath] = srcDoc
# figure out which build Package contains the given file, if any
# call with:
# 1) absolute path for source filename being searched
def findBuildPackage(self, srcAbspath):
# Multiple target Packages might "contain" the file path, if they
# are nested. If so, the one with the longest path would be the
# most deeply-nested target directory, so that's the one which
# should get the file path.
pkgLongestMatch = None
for pkg in self.docBuild.pkgs.values():
if os.path.commonpath([srcAbspath, pkg.cfg.relativeBaseDir]) == pkg.cfg.relativeBaseDir:
# the package does contain this file; is it the deepest?
if pkgLongestMatch:
if len(pkg.cfg.relativeBaseDir) > len(pkgLongestMatch.cfg.relativeBaseDir):
pkgLongestMatch = pkg
else:
# first package containing it, so assign it
pkgLongestMatch = pkg
return pkgLongestMatch
# walk through pending RelationshipData entries, create corresponding
# Relationships, and assign them to the applicable Files / Packages
def walkRelationships(self):
for rlnData in self.pendingRelationships:
rln = Relationship()
# get left side of relationship data
docA, spdxIDA, rlnsA = self.getRelationshipLeft(rlnData)
if not docA or not spdxIDA:
continue
rln.refA = spdxIDA
# get right side of relationship data
spdxIDB = self.getRelationshipRight(rlnData, docA)
if not spdxIDB:
continue
rln.refB = spdxIDB
rln.rlnType = rlnData.rlnType
rlnsA.append(rln)
log.dbg(f" - adding relationship to {docA.cfg.name}: {rln.refA} {rln.rlnType} {rln.refB}")
# get owner (left side) document and SPDX ID of Relationship for given RelationshipData
# returns: doc, spdxID, rlnsArray (for either Document, Package, or File, as applicable)
def getRelationshipLeft(self, rlnData):
if rlnData.ownerType == RelationshipDataElementType.FILENAME:
# find the document for this file abspath, and then the specific file's ID
ownerDoc = self.allFileLinks.get(rlnData.ownerFileAbspath, None)
if not ownerDoc:
log.dbg(f" - searching for relationship, can't find document with file {rlnData.ownerFileAbspath}; skipping")
return None, None, None
sf = ownerDoc.fileLinks.get(rlnData.ownerFileAbspath, None)
if not sf:
log.dbg(f" - searching for relationship for file {rlnData.ownerFileAbspath} points to document {ownerDoc.cfg.name} but file not found; skipping")
return None, None, None
# found it
if not sf.spdxID:
log.dbg(f" - searching for relationship for file {rlnData.ownerFileAbspath} found file, but empty ID; skipping")
return None, None, None
return ownerDoc, sf.spdxID, sf.rlns
elif rlnData.ownerType == RelationshipDataElementType.TARGETNAME:
# find the document for this target name, and then the specific package's ID
# for target names, must be docBuild
ownerDoc = self.docBuild
# walk through target Packages and check names
for pkg in ownerDoc.pkgs.values():
if pkg.cfg.name == rlnData.ownerTargetName:
if not pkg.cfg.spdxID:
log.dbg(f" - searching for relationship for target {rlnData.ownerTargetName} found package, but empty ID; skipping")
return None, None, None
return ownerDoc, pkg.cfg.spdxID, pkg.rlns
log.dbg(f" - searching for relationship for target {rlnData.ownerTargetName}, target not found in build document; skipping")
return None, None, None
elif rlnData.ownerType == RelationshipDataElementType.DOCUMENT:
# will always be SPDXRef-DOCUMENT
return rlnData.ownerDocument, "SPDXRef-DOCUMENT", rlnData.ownerDocument.relationships
else:
log.dbg(f" - unknown relationship type {rlnData.ownerType}; skipping")
return None, None, None
# get other (right side) SPDX ID of Relationship for given RelationshipData
def getRelationshipRight(self, rlnData, docA):
if rlnData.otherType == RelationshipDataElementType.FILENAME:
# find the document for this file abspath, and then the specific file's ID
otherDoc = self.allFileLinks.get(rlnData.otherFileAbspath, None)
if not otherDoc:
log.dbg(f" - searching for relationship, can't find document with file {rlnData.otherFileAbspath}; skipping")
return None
bf = otherDoc.fileLinks.get(rlnData.otherFileAbspath, None)
if not bf:
log.dbg(f" - searching for relationship for file {rlnData.otherFileAbspath} points to document {otherDoc.cfg.name} but file not found; skipping")
return None
# found it
if not bf.spdxID:
log.dbg(f" - searching for relationship for file {rlnData.otherFileAbspath} found file, but empty ID; skipping")
return None
# figure out whether to append DocumentRef
spdxIDB = bf.spdxID
if otherDoc != docA:
spdxIDB = otherDoc.cfg.docRefID + ":" + spdxIDB
docA.externalDocuments.add(otherDoc)
return spdxIDB
elif rlnData.otherType == RelationshipDataElementType.TARGETNAME:
# find the document for this target name, and then the specific package's ID
# for target names, must be docBuild
otherDoc = self.docBuild
# walk through target Packages and check names
for pkg in otherDoc.pkgs.values():
if pkg.cfg.name == rlnData.otherTargetName:
if not pkg.cfg.spdxID:
log.dbg(f" - searching for relationship for target {rlnData.otherTargetName} found package, but empty ID; skipping")
return None
spdxIDB = pkg.cfg.spdxID
if otherDoc != docA:
spdxIDB = otherDoc.cfg.docRefID + ":" + spdxIDB
docA.externalDocuments.add(otherDoc)
return spdxIDB
log.dbg(f" - searching for relationship for target {rlnData.otherTargetName}, target not found in build document; skipping")
return None
elif rlnData.otherType == RelationshipDataElementType.PACKAGEID:
# will just be the package ID that was passed in
return rlnData.otherPackageID
else:
log.dbg(f" - unknown relationship type {rlnData.otherType}; skipping")
return None