Skip to content

Commit ae5dd2b

Browse files
committed
feat(analyzer): Add support for Conan lockfiles
Filter output of `conan info` to only include packages listed in lockfile. For now only lockfiles located in the same directory as the definition file are supported. Signed-off-by: Oleksandr Yarosevych <[email protected]>
1 parent ecb3c1d commit ae5dd2b

File tree

3 files changed

+96
-12
lines changed

3 files changed

+96
-12
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"graph_lock": {
3+
"nodes": {
4+
"0": {
5+
"ref": "Poco/1.9.3",
6+
"options": "cxx_14=False\nenable_apacheconnector=False\nenable_cppparser=False\nenable_crypto=True\nenable_data=True\nenable_data_mysql=False\nenable_data_odbc=False\nenable_data_sqlite=True\nenable_json=True\nenable_mongodb=True\nenable_net=True\nenable_netssl=True\nenable_netssl_win=True\nenable_pagecompiler=False\nenable_pagecompiler_file2page=False\nenable_pdf=False\nenable_pocodoc=False\nenable_redis=True\nenable_sevenzip=False\nenable_tests=False\nenable_util=True\nenable_xml=True\nenable_zip=True\nforce_openssl=True\npoco_unbundled=False\nshared=False\nopenssl:386=False\nopenssl:capieng_dialog=False\nopenssl:enable_capieng=False\nopenssl:enable_weak_ssl_ciphers=False\nopenssl:no_aria=False\nopenssl:no_asm=False\nopenssl:no_async=False\nopenssl:no_bf=False\nopenssl:no_blake2=False\nopenssl:no_camellia=False\nopenssl:no_cast=False\nopenssl:no_chacha=False\nopenssl:no_cms=False\nopenssl:no_comp=False\nopenssl:no_ct=False\nopenssl:no_deprecated=False\nopenssl:no_des=False\nopenssl:no_dgram=False\nopenssl:no_dh=False\nopenssl:no_dsa=False\nopenssl:no_dso=False\nopenssl:no_ec=False\nopenssl:no_ecdh=False\nopenssl:no_ecdsa=False\nopenssl:no_engine=False\nopenssl:no_filenames=False\nopenssl:no_fips=False\nopenssl:no_gost=False\nopenssl:no_idea=False\nopenssl:no_legacy=False\nopenssl:no_md2=True\nopenssl:no_md4=False\nopenssl:no_mdc2=False\nopenssl:no_ocsp=False\nopenssl:no_pinshared=False\nopenssl:no_rc2=False\nopenssl:no_rc4=False\nopenssl:no_rc5=False\nopenssl:no_rfc3779=False\nopenssl:no_rmd160=False\nopenssl:no_seed=False\nopenssl:no_sm2=False\nopenssl:no_sm3=False\nopenssl:no_sm4=False\nopenssl:no_sock=False\nopenssl:no_srp=False\nopenssl:no_srtp=False\nopenssl:no_sse2=False\nopenssl:no_ssl=False\nopenssl:no_ssl3=False\nopenssl:no_stdio=False\nopenssl:no_threads=False\nopenssl:no_tls1=False\nopenssl:no_ts=False\nopenssl:no_whirlpool=False\nopenssl:no_zlib=False\nopenssl:openssldir=None\nopenssl:shared=False\nzlib:shared=False",
7+
"requires": [
8+
"1"
9+
],
10+
"path": "conanfile.py",
11+
"context": "host"
12+
},
13+
"1": {
14+
"ref": "openssl/3.0.0",
15+
"options": "386=False\ncapieng_dialog=False\nenable_capieng=False\nenable_weak_ssl_ciphers=False\nno_aria=False\nno_asm=False\nno_async=False\nno_bf=False\nno_blake2=False\nno_camellia=False\nno_cast=False\nno_chacha=False\nno_cms=False\nno_comp=False\nno_ct=False\nno_deprecated=False\nno_des=False\nno_dgram=False\nno_dh=False\nno_dsa=False\nno_dso=False\nno_ec=False\nno_ecdh=False\nno_ecdsa=False\nno_engine=False\nno_filenames=False\nno_fips=False\nno_gost=False\nno_idea=False\nno_legacy=False\nno_md2=True\nno_md4=False\nno_mdc2=False\nno_ocsp=False\nno_pinshared=False\nno_rc2=False\nno_rc4=False\nno_rc5=False\nno_rfc3779=False\nno_rmd160=False\nno_seed=False\nno_sm2=False\nno_sm3=False\nno_sm4=False\nno_sock=False\nno_srp=False\nno_srtp=False\nno_sse2=False\nno_ssl=False\nno_ssl3=False\nno_stdio=False\nno_threads=False\nno_tls1=False\nno_ts=False\nno_whirlpool=False\nno_zlib=False\nopenssldir=None\nshared=False\nzlib:shared=False",
16+
"package_id": "03853c3f3f9c6c4fb48aba875d65902d33efec66",
17+
"requires": [
18+
"2"
19+
],
20+
"context": "host"
21+
},
22+
"2": {
23+
"ref": "zlib/1.2.12",
24+
"options": "shared=False",
25+
"package_id": "7bc8c2c85db7a618e5320dc997f27fc33e1df074",
26+
"context": "host"
27+
}
28+
},
29+
"revisions_enabled": false
30+
},
31+
"version": "0.4",
32+
"profile_host": "[settings]\narch=x86_64\narch_build=x86_64\nbuild_type=Release\ncompiler=gcc\ncompiler.libcxx=libstdc++\ncompiler.version=6.3\nos=Windows\nos_build=Windows\n[options]\n[build_requires]\n[env]\n"
33+
}

analyzer/src/funTest/kotlin/managers/ConanFunTest.kt

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,12 @@ import java.io.File
2626

2727
import org.ossreviewtoolkit.downloader.VersionControlSystem
2828
import org.ossreviewtoolkit.model.config.AnalyzerConfiguration
29+
import org.ossreviewtoolkit.model.config.PackageManagerConfiguration
2930
import org.ossreviewtoolkit.model.config.RepositoryConfiguration
3031
import org.ossreviewtoolkit.utils.ort.normalizeVcsUrl
3132
import org.ossreviewtoolkit.utils.test.USER_DIR
3233
import org.ossreviewtoolkit.utils.test.patchActualResult
3334
import org.ossreviewtoolkit.utils.test.patchExpectedResult
34-
3535
class ConanFunTest : StringSpec() {
3636
private val projectsDirTxt = File("src/funTest/assets/projects/synthetic/conan-txt").absoluteFile
3737
private val vcsDirTxt = VersionControlSystem.forDirectory(projectsDirTxt)!!
@@ -55,7 +55,7 @@ class ConanFunTest : StringSpec() {
5555
url = normalizeVcsUrl(vcsUrlTxt)
5656
)
5757

58-
val result = createConan().resolveSingleProject(definitionFile)
58+
val result = createConanDynamicVersions().resolveSingleProject(definitionFile)
5959

6060
patchActualResult(result.toYaml()) shouldBe expectedResult
6161
}
@@ -71,12 +71,43 @@ class ConanFunTest : StringSpec() {
7171
url = normalizeVcsUrl(vcsUrlPy)
7272
)
7373

74-
val result = createConan().resolveSingleProject(definitionFile)
74+
val result = createConanDynamicVersions().resolveSingleProject(definitionFile)
75+
76+
patchActualResult(result.toYaml()) shouldBe expectedResult
77+
}
78+
79+
"Project dependencies are detected correctly with the lockfile" {
80+
val packageFile = projectsDirPy.resolve("conanfile.py")
81+
val vcsPath = vcsDirPy.getPathToRoot(projectsDirPy)
82+
val expectedResult = patchExpectedResult(
83+
projectsDirPy.parentFile.resolve("conan-expected-output-py.yml"),
84+
definitionFilePath = "$vcsPath/conanfile.py",
85+
path = vcsPath,
86+
revision = vcsRevisionPy,
87+
url = normalizeVcsUrl(vcsUrlPy)
88+
)
89+
90+
val result = createConanWithLockFile().resolveSingleProject(packageFile)
7591

7692
patchActualResult(result.toYaml()) shouldBe expectedResult
7793
}
7894
}
7995

80-
private fun createConan() =
81-
Conan("Conan", USER_DIR, AnalyzerConfiguration(), RepositoryConfiguration())
96+
private fun createConanDynamicVersions() =
97+
Conan("Conan", USER_DIR, AnalyzerConfiguration(true), RepositoryConfiguration())
98+
99+
private fun createConanWithLockFile() =
100+
Conan(
101+
"Conan",
102+
USER_DIR,
103+
AnalyzerConfiguration().copy(
104+
packageManagers = mapOf(
105+
"Conan" to PackageManagerConfiguration(
106+
options = mapOf(
107+
"lockfileName" to "lockfile.lock"
108+
)
109+
)
110+
)
111+
),
112+
RepositoryConfiguration())
82113
}

analyzer/src/main/kotlin/managers/Conan.kt

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,7 @@ class Conan(
106106

107107
override fun command(workingDir: File?) = "conan"
108108

109-
// TODO: Add support for Conan lock files.
110-
111-
// protected open fun hasLockFile(projectDir: File) = null
109+
private fun hasLockFile(directory: String) = File(directory).isFile
112110

113111
override fun transformVersion(output: String) =
114112
// Conan could report version strings like:
@@ -144,25 +142,47 @@ class Conan(
144142
stashDirectories(directoryToStash).use {
145143
configureRemoteAuthentication(conanConfig)
146144

145+
// TODO: Support lockfiles which are located in a different directory than the definition file.
146+
val lockfileName = analyzerConfig.packageManagers?.get(managerName)?.options?.get("lockfileName")
147+
requireLockfile(workingDir) { lockfileName?.let { hasLockFile(workingDir.resolve(it).path) } ?: false }
148+
147149
val jsonFile = createOrtTempDir().resolve("info.json")
148150
run(workingDir, "info", definitionFile.name, "--json", jsonFile.absolutePath, *DUMMY_COMPILER_SETTINGS)
149151

150152
val pkgInfos = jsonMapper.readTree(jsonFile)
151153
jsonFile.parentFile.safeDeleteRecursively(force = true)
152154

153-
val packageList = removeProjectPackage(pkgInfos, definitionFile.name)
155+
val filteredPkgInfos = if (!analyzerConfig.allowDynamicVersions && lockfileName != null) {
156+
val lockfileNodes = jsonMapper.readTree(definitionFile.resolveSibling(lockfileName))
157+
val lockfilePackages = lockfileNodes["graph_lock"]["nodes"].mapNotNull {
158+
it["ref"]?.textValue()
159+
}
160+
161+
jsonMapper.createArrayNode().addAll(
162+
pkgInfos.filter { jsonNode ->
163+
lockfilePackages.contains(jsonNode["reference"].textValueOrEmpty())
164+
|| jsonNode["reference"].textValueOrEmpty().contains(definitionFile.name)
165+
}
166+
)
167+
} else {
168+
pkgInfos
169+
}
170+
171+
val packageList = removeProjectPackage(filteredPkgInfos, definitionFile.name)
154172
val packages = parsePackages(packageList, workingDir)
155173

156174
val dependenciesScope = Scope(
157175
name = SCOPE_NAME_DEPENDENCIES,
158-
dependencies = parseDependencies(pkgInfos, definitionFile.name, SCOPE_NAME_DEPENDENCIES, workingDir)
176+
dependencies =
177+
parseDependencies(filteredPkgInfos, definitionFile.name, SCOPE_NAME_DEPENDENCIES, workingDir)
159178
)
160179
val devDependenciesScope = Scope(
161180
name = SCOPE_NAME_DEV_DEPENDENCIES,
162-
dependencies = parseDependencies(pkgInfos, definitionFile.name, SCOPE_NAME_DEV_DEPENDENCIES, workingDir)
181+
dependencies =
182+
parseDependencies(filteredPkgInfos, definitionFile.name, SCOPE_NAME_DEV_DEPENDENCIES, workingDir)
163183
)
164184

165-
val projectPackage = parseProjectPackage(pkgInfos, definitionFile, workingDir)
185+
val projectPackage = parseProjectPackage(filteredPkgInfos, definitionFile, workingDir)
166186

167187
return listOf(
168188
ProjectAnalyzerResult(

0 commit comments

Comments
 (0)