diff --git a/lib/modules/datasource/conan/__fixtures__/poco_revisions.json b/lib/modules/datasource/conan/__fixtures__/poco_revisions.json new file mode 100644 index 0000000000000000000000000000000000000000..290a11b6e2d55815bd29ef8671174ebe824a8401 --- /dev/null +++ b/lib/modules/datasource/conan/__fixtures__/poco_revisions.json @@ -0,0 +1,51 @@ +{ + "1.10.1": { + "reference": "poco/1.10.1@_/_", + "revisions": [ + { + "revision": "32815db5b18046e4f8879af7bb743422", + "time": "2022-07-18T09:38:37.372+0000" + } + ] + }, + "1.10.0": { + "reference": "poco/1.10.0@_/_", + "revisions": [ + { + "revision": "7c3fbb12b69d7e7fd7134ba1efb04d5e", + "time": "2022-07-18T09:38:37.768+0000" + } + ] + }, + "1.9.4": { + "reference": "poco/1.9.4@_/_", + "revisions": [ + { + "revision": "736fdb96cd0add0cf85ca420e0bac453", + "time": "2022-07-18T09:38:38.396+0000" + } + ] + }, + "1.9.3": { + "reference": "poco/1.9.3@_/_", + "revisions": [ + { + "revision": "dc80dcf02da270cc5f629c00df151a56", + "time": "2022-07-18T09:38:38.975+0000" + }, + { + "revision": "92b4e6cf83f6be0b9589ba61cc4a5b4e", + "time": "2022-07-11T16:37:30.926+0000" + } + ] + }, + "1.8.1": { + "reference": "poco/1.8.1@_/_", + "revisions": [ + { + "revision": "3a9b47caee2e2c1d3fb7d97788339aa8", + "time": "2022-07-18T09:38:38.815+0000" + } + ] + } +} diff --git a/lib/modules/datasource/conan/common.ts b/lib/modules/datasource/conan/common.ts index 08ca266d05e9278a989f21aae2c37e295dadf988..505aadb2c9669e282b1b77d30fba9e6a225cc805 100644 --- a/lib/modules/datasource/conan/common.ts +++ b/lib/modules/datasource/conan/common.ts @@ -1,4 +1,5 @@ import { regEx } from '../../../util/regex'; +import type { ConanPackage } from './types'; export const defaultRegistryUrl = 'https://center.conan.io/'; @@ -8,3 +9,9 @@ export const conanDatasourceRegex = regEx( /(?<name>[a-z\-_0-9]+)\/(?<version>[^@/\n]+)(?<userChannel>@\S+\/\S+)/, 'gim' ); + +export function getConanPackage(packageName: string): ConanPackage { + const depName = packageName.split('/')[0]; + const userAndChannel = packageName.split('@')[1]; + return { depName, userAndChannel }; +} diff --git a/lib/modules/datasource/conan/index.spec.ts b/lib/modules/datasource/conan/index.spec.ts index 999cf62c99dd216e342b32ab596d247ef16fa621..68fc393a514315f813b5ac33e08728b1d99cb204 100644 --- a/lib/modules/datasource/conan/index.spec.ts +++ b/lib/modules/datasource/conan/index.spec.ts @@ -1,12 +1,13 @@ -import { getPkgReleases } from '..'; +import { getDigest, getPkgReleases } from '..'; import { Fixtures } from '../../../../test/fixtures'; import * as httpMock from '../../../../test/http-mock'; import * as conan from '../../versioning/conan'; -import type { GetPkgReleasesConfig } from '../types'; +import type { GetDigestInputConfig, GetPkgReleasesConfig } from '../types'; import { defaultRegistryUrl } from './common'; import { ConanDatasource } from '.'; const pocoJson = Fixtures.get('poco.json'); +const pocoRevisions = Fixtures.getJson('poco_revisions.json'); const pocoYamlGitHubContent = Fixtures.get('poco.yaml'); const malformedJson = Fixtures.get('malformed.json'); const fakeJson = Fixtures.get('fake.json'); @@ -21,11 +22,37 @@ const config: GetPkgReleasesConfig = { registryUrls: [nonDefaultRegistryUrl], }; +const digestConfig: GetDigestInputConfig = { + depName: 'fake', + datasource, + registryUrls: [nonDefaultRegistryUrl], +}; + describe('modules/datasource/conan/index', () => { beforeEach(() => { config.registryUrls = [nonDefaultRegistryUrl]; }); + describe('getDigest', () => { + it('handles package without digest', async () => { + digestConfig.packageName = 'fakepackage/1.2@_/_'; + expect(await getDigest(digestConfig)).toBeNull(); + }); + + it('handles digest', async () => { + const version = '1.8.1'; + httpMock + .scope(nonDefaultRegistryUrl) + .get(`/v2/conans/poco/${version}/_/_/revisions`) + .reply(200, pocoRevisions[version]); + digestConfig.packageName = `poco/${version}@_/_`; + digestConfig.currentDigest = '4fc13d60fd91ba44fefe808ad719a5af'; + expect(await getDigest(digestConfig, version)).toBe( + '3a9b47caee2e2c1d3fb7d97788339aa8' + ); + }); + }); + describe('getReleases', () => { it('handles bad return', async () => { httpMock @@ -117,7 +144,7 @@ describe('modules/datasource/conan/index', () => { }); }); - it('uses github isntead of conan center', async () => { + it('uses github instead of conan center', async () => { httpMock .scope('https://api.github.com') .get( diff --git a/lib/modules/datasource/conan/index.ts b/lib/modules/datasource/conan/index.ts index f85269a59013768bd046600d8d7f30eeb5d42a8f..037062c12761400e310fab45c7e571ace4ff5377 100644 --- a/lib/modules/datasource/conan/index.ts +++ b/lib/modules/datasource/conan/index.ts @@ -5,9 +5,19 @@ import { cache } from '../../../util/cache/package/decorator'; import { GithubHttp } from '../../../util/http/github'; import { ensureTrailingSlash, joinUrlParts } from '../../../util/url'; import { Datasource } from '../datasource'; -import type { GetReleasesConfig, Release, ReleaseResult } from '../types'; -import { conanDatasourceRegex, datasource, defaultRegistryUrl } from './common'; -import type { ConanJSON, ConanYAML } from './types'; +import type { + DigestConfig, + GetReleasesConfig, + Release, + ReleaseResult, +} from '../types'; +import { + conanDatasourceRegex, + datasource, + defaultRegistryUrl, + getConanPackage, +} from './common'; +import type { ConanJSON, ConanRevisionsJSON, ConanYAML } from './types'; export class ConanDatasource extends Datasource { static readonly id = datasource; @@ -50,6 +60,36 @@ export class ConanDatasource extends Datasource { }; } + @cache({ + namespace: `datasource-${datasource}-revisions`, + key: ({ registryUrl, packageName }: DigestConfig, newValue?: string) => + // TODO: types (#7154) + `${registryUrl!}:${packageName}:${newValue!}`, + }) + override async getDigest( + { registryUrl, packageName }: DigestConfig, + newValue?: string + ): Promise<string | null> { + if (is.undefined(newValue) || is.undefined(registryUrl)) { + return null; + } + const url = ensureTrailingSlash(registryUrl); + const conanPackage = getConanPackage(packageName); + const revisionLookUp = joinUrlParts( + url, + 'v2/conans/', + conanPackage.depName, + newValue, + conanPackage.userAndChannel, + '/revisions' + ); + const revisionRep = await this.http.getJson<ConanRevisionsJSON>( + revisionLookUp + ); + const revisions = revisionRep?.body.revisions; + return revisions?.[0].revision ?? null; + } + @cache({ namespace: `datasource-${datasource}`, key: ({ registryUrl, packageName }: GetReleasesConfig) => @@ -61,8 +101,9 @@ export class ConanDatasource extends Datasource { registryUrl, packageName, }: GetReleasesConfig): Promise<ReleaseResult | null> { - const depName = packageName.split('/')[0]; - const userAndChannel = '@' + packageName.split('@')[1]; + const conanPackage = getConanPackage(packageName); + const depName = conanPackage.depName; + const userAndChannel = '@' + conanPackage.userAndChannel; if ( is.string(registryUrl) && ensureTrailingSlash(registryUrl) === defaultRegistryUrl diff --git a/lib/modules/datasource/conan/types.ts b/lib/modules/datasource/conan/types.ts index 4a72a5557bb1375e58caac5b8218befdc9731632..543ff3d6876386a548e82b2741f47336915c9026 100644 --- a/lib/modules/datasource/conan/types.ts +++ b/lib/modules/datasource/conan/types.ts @@ -2,6 +2,20 @@ export interface ConanJSON { results?: Record<string, string>; } +export interface ConanRevisionJSON { + revision: string; + time: string; +} + +export interface ConanRevisionsJSON { + revisions?: Record<string, ConanRevisionJSON>; +} + export interface ConanYAML { versions?: Record<string, unknown>; } + +export interface ConanPackage { + depName: string; + userAndChannel: string; +} diff --git a/lib/modules/manager/conan/__fixtures__/conanfile.py b/lib/modules/manager/conan/__fixtures__/conanfile.py index b1cfdb5f902910f257f54e5c0aa4ea99a2857c00..7c2db16df9374c8166373f436b4d38d999e541e9 100644 --- a/lib/modules/manager/conan/__fixtures__/conanfile.py +++ b/lib/modules/manager/conan/__fixtures__/conanfile.py @@ -12,6 +12,10 @@ class Pkg(ConanFile): requires = (("req_c/1.0@user/stable", "private"), ) requires = ("req_f/1.0@user/stable", ("req_h/3.0@other/beta", "override")) requires = "req_g/[>1.0 <1.8]@user/stable" +# requires = "commentedout/[>1.0 <1.8]@user/stable" + # requires = "commentedout2/[>1.0 <1.8]@user/stable" + requires = (("req_l/1.0@user/stable#bc592346b33fd19c1fbffce25d1e4236", "private"), ) + def requirements(self): diff --git a/lib/modules/manager/conan/__fixtures__/conanfile.txt b/lib/modules/manager/conan/__fixtures__/conanfile.txt index e06417f57dd93bdc8a9cf5c8a9bfff220d7f85d0..d9f3d1d993ed89eee42122b9f7d5120d241b8fd3 100644 --- a/lib/modules/manager/conan/__fixtures__/conanfile.txt +++ b/lib/modules/manager/conan/__fixtures__/conanfile.txt @@ -2,6 +2,7 @@ poco/1.9.4 zlib/[~1.2.3, loose=False] fake/8.62.134@test/dev +cairo/1.17.2@_/_#aff2d03608351db075ec1348a3afc9ff [build_requires] 7zip/[>1.1 <2.1, include_prerelease=True] @@ -13,6 +14,7 @@ cmake/[>1.1 || 0.8] cryptopp/[1.2.7 || >=1.2.9 <2.0.0]@test/local #commentedout/1.2 # commentedout/3.4 +meson/0.63.0@_/_#bc592346b33fd19c1fbffce25d1e4236 [generators] xcode diff --git a/lib/modules/manager/conan/common.ts b/lib/modules/manager/conan/common.ts new file mode 100644 index 0000000000000000000000000000000000000000..4b3e6a9d14f8595c3eddabcfed1e1a1f3a378e18 --- /dev/null +++ b/lib/modules/manager/conan/common.ts @@ -0,0 +1,3 @@ +export function isComment(line: string): boolean { + return line.trim().startsWith('#'); +} diff --git a/lib/modules/manager/conan/extract.spec.ts b/lib/modules/manager/conan/extract.spec.ts index c988c8ef6ac8101033e1b8b2bb7c6e8ce2369f20..ec20d05de28f9019421c7f34b2aa37c85e676a25 100644 --- a/lib/modules/manager/conan/extract.spec.ts +++ b/lib/modules/manager/conan/extract.spec.ts @@ -35,6 +35,16 @@ describe('modules/manager/conan/extract', () => { packageName: 'fake/8.62.134@test/dev', replaceString: 'fake/8.62.134@test/dev', }, + { + autoReplaceStringTemplate: + '{{depName}}/{{newValue}}@_/_{{#if newDigest}}#{{newDigest}}{{/if}}', + currentDigest: 'aff2d03608351db075ec1348a3afc9ff', + currentValue: '1.17.2', + depName: 'cairo', + depType: 'requires', + packageName: 'cairo/1.17.2@_/_', + replaceString: 'cairo/1.17.2@_/_#aff2d03608351db075ec1348a3afc9ff', + }, { currentValue: '[>1.1 <2.1, include_prerelease=True]', depName: '7zip', @@ -86,6 +96,16 @@ describe('modules/manager/conan/extract', () => { packageName: 'cryptopp/[1.2.7 || >=1.2.9 <2.0.0]@test/local', replaceString: 'cryptopp/[1.2.7 || >=1.2.9 <2.0.0]@test/local', }, + { + autoReplaceStringTemplate: + '{{depName}}/{{newValue}}@_/_{{#if newDigest}}#{{newDigest}}{{/if}}', + currentDigest: 'bc592346b33fd19c1fbffce25d1e4236', + currentValue: '0.63.0', + depName: 'meson', + depType: 'build_requires', + packageName: 'meson/0.63.0@_/_', + replaceString: 'meson/0.63.0@_/_#bc592346b33fd19c1fbffce25d1e4236', + }, ]); }); @@ -181,6 +201,17 @@ describe('modules/manager/conan/extract', () => { packageName: 'req_g/[>1.0 <1.8]@user/stable', replaceString: 'req_g/[>1.0 <1.8]@user/stable', }, + { + autoReplaceStringTemplate: + '{{depName}}/{{newValue}}@user/stable{{#if newDigest}}#{{newDigest}}{{/if}}', + currentDigest: 'bc592346b33fd19c1fbffce25d1e4236', + currentValue: '1.0', + depName: 'req_l', + depType: 'requires', + packageName: 'req_l/1.0@user/stable', + replaceString: + 'req_l/1.0@user/stable#bc592346b33fd19c1fbffce25d1e4236', + }, { currentValue: '1.2', depName: 'req_i', diff --git a/lib/modules/manager/conan/extract.ts b/lib/modules/manager/conan/extract.ts index 40c178dec007e462da6540eb4afa831cfaf9d3e5..9d545cca300ccb8b2faa6e7c9471a9ac3be640f9 100644 --- a/lib/modules/manager/conan/extract.ts +++ b/lib/modules/manager/conan/extract.ts @@ -1,9 +1,10 @@ import is from '@sindresorhus/is'; import { regEx } from '../../../util/regex'; import type { PackageDependency, PackageFile } from '../types'; +import { isComment } from './common'; const regex = regEx( - `(?<name>[-_a-z0-9]+)/(?<version>[^@\n{*"']+)(?<userChannel>@[-_a-zA-Z0-9]+/[^\n.{*"' ]+)?` + `(?<name>[-_a-z0-9]+)/(?<version>[^@\n{*"']+)(?<userChannel>@[-_a-zA-Z0-9]+/[^#\n.{*"' ]+)?#?(?<revision>[-_a-f0-9]+[^\n{*"'])?` ); function setDepType(content: string, originalType: string): string { @@ -32,13 +33,11 @@ export function extractPackageFile(content: string): PackageFile | null { let depType = setDepType(section, 'requires'); const rawLines = section.split('\n').filter(is.nonEmptyString); - for (const rawline of rawLines) { - // don't process after a comment - const sanitizedLine = rawline.split('#')[0].split('//')[0]; - if (sanitizedLine) { - depType = setDepType(sanitizedLine, depType); + for (const rawLine of rawLines) { + if (!isComment(rawLine)) { + depType = setDepType(rawLine, depType); // extract all dependencies from each line - const lines = sanitizedLine.split(/["'],/); + const lines = rawLine.split(/["'],/); for (const line of lines) { const matches = regex.exec(line.trim()); if (matches?.groups) { @@ -64,6 +63,12 @@ export function extractPackageFile(content: string): PackageFile | null { replaceString, depType, }; + if (matches.groups.revision) { + dep.currentDigest = matches.groups.revision; + dep.autoReplaceStringTemplate = `{{depName}}/{{newValue}}${userAndChannel}{{#if newDigest}}#{{newDigest}}{{/if}}`; + dep.replaceString = `${replaceString}#${dep.currentDigest}`; + } + deps.push(dep); } }