diff --git a/README.md b/README.md index 4a1bad28..ef055b58 100644 --- a/README.md +++ b/README.md @@ -1164,6 +1164,7 @@ targets: kmp: rootDistDirRegex: /sentry-kotlin-multiplatform-[0-9]+.*$/ appleDistDirRegex: /sentry-kotlin-multiplatform-(macos|ios|tvos|watchos).*/ + klibDistDirRegex: /sentry-kotlin-multiplatform-(js|wasm-js).*/ ``` ### Symbol Collector (`symbol-collector`) diff --git a/src/targets/__tests__/maven.test.ts b/src/targets/__tests__/maven.test.ts index 9b051e66..9a63071e 100644 --- a/src/targets/__tests__/maven.test.ts +++ b/src/targets/__tests__/maven.test.ts @@ -13,6 +13,7 @@ import { import { retrySpawnProcess, sleep } from '../../utils/async'; import { withTempDir } from '../../utils/files'; import { importGPGKey } from '../../utils/gpg'; +import * as fs from 'fs'; jest.mock('../../utils/files'); jest.mock('../../utils/gpg'); @@ -73,9 +74,10 @@ function getFullTargetConfig(): any { fileReplacerStr: 'replacer', }, kmp: { - rootDistDirRegex: '/distDir/', + rootDistDirRegex: '/root-distDir/', appleDistDirRegex: '/apple-distDir/', - }, + klibDistDirRegex: '/klib-distDir/', + }, }; } @@ -258,6 +260,7 @@ describe('Maven target configuration', () => { expect(typeof mvnTarget.config.android.fileReplacerStr).toBe('string'); expect(typeof mvnTarget.config.kmp.rootDistDirRegex).toBe('string'); expect(typeof mvnTarget.config.kmp.appleDistDirRegex).toBe('string'); + expect(typeof mvnTarget.config.kmp.klibDistDirRegex).toBe('string'); }); test('import GPG private key if one is present in the environment', async () => { @@ -294,6 +297,38 @@ describe('publish', () => { describe('transform KMP artifacts', () => { const tmpDirName = 'tmpDir'; + test('transform klib distDir target side artifacts', async () => { + (withTempDir as jest.MockedFunction).mockImplementation( + async cb => { + return await cb(tmpDirName); + } + ); + + const mvnTarget = createMavenTarget(getFullTargetConfig()); + const files: Record = { + javadocFile: `${tmpDirName}-javadoc.jar`, + sourcesFile: `${tmpDirName}-sources.jar`, + klibFiles: [ + `${tmpDirName}.klib`, + ], + allFile: '', + metadataFile: ``, + moduleFile: `${tmpDirName}.module`, + }; + const { + sideArtifacts, + classifiers, + types, + } = mvnTarget.transformKmpSideArtifacts(false, false, true, files); + expect(sideArtifacts).toEqual( + `${files.javadocFile},${files.sourcesFile},${files.klibFiles},${files.moduleFile}` + ); + expect(classifiers).toEqual( + 'javadoc,sources,,' + ); + expect(types).toEqual('jar,jar,klib,module'); + }); + test('transform apple target side artifacts', async () => { (withTempDir as jest.MockedFunction).mockImplementation( async cb => { @@ -317,7 +352,7 @@ describe('transform KMP artifacts', () => { sideArtifacts, classifiers, types, - } = mvnTarget.transformKmpSideArtifacts(false, true, files); + } = mvnTarget.transformKmpSideArtifacts(false, true, false, files); expect(sideArtifacts).toEqual( `${files.javadocFile},${files.sourcesFile},${files.klibFiles},${files.metadataFile},${files.moduleFile}` ); @@ -348,7 +383,7 @@ describe('transform KMP artifacts', () => { sideArtifacts, classifiers, types, - } = mvnTarget.transformKmpSideArtifacts(true, false, files); + } = mvnTarget.transformKmpSideArtifacts(true, false, false, files); expect(sideArtifacts).toEqual( `${files.javadocFile},${files.sourcesFile},${files.allFile},${files.kotlinToolingMetadataFile},${files.moduleFile}` ); @@ -507,8 +542,6 @@ describe('upload', () => { }); test('should skip upload for artifacts without any POM/BOM', async () => { - // simple mock to always use the same temporary directory, - // instead of creating a new one (withTempDir as jest.MockedFunction).mockImplementation( async cb => { return await cb(tmpDirName); @@ -531,6 +564,73 @@ describe('upload', () => { expect(retrySpawnProcess).toHaveBeenCalledTimes(0); }); + + test('upload KMP klib-only distribution', async () => { + const klibDistDirName = 'sentry-klib-distDir-linuxx64-1.0.0'; // matches klib regex + const klibDistDir = `${tmpDirName}/${klibDistDirName}`; + + (withTempDir as jest.MockedFunction).mockImplementation( + async cb => { + return await cb(tmpDirName); + } + ); + + // Override fs.promises.readdir for this test to return klib files + const readdirSpy = jest.spyOn(fs.promises, 'readdir').mockImplementation((dirPath: any) => { + if (dirPath.toString().includes(klibDistDirName)) { + return Promise.resolve([ + `${klibDistDirName}-javadoc.jar`, + `${klibDistDirName}.klib`, + `${klibDistDirName}-sources.jar`, + `${klibDistDirName}.module`, + POM_DEFAULT_FILENAME, + ] as any); + } + return Promise.resolve([] as any); + }); + + const mvnTarget = createMavenTarget(getFullTargetConfig()); + mvnTarget.getArtifactsForRevision = jest + .fn() + .mockResolvedValueOnce([{ filename: `${klibDistDirName}.zip` }]); + mvnTarget.artifactProvider.downloadArtifact = jest + .fn() + .mockResolvedValueOnce('artifact/download/path'); + mvnTarget.isBomFile = jest.fn().mockResolvedValueOnce(false); + mvnTarget.getPomFileInDist = jest.fn().mockResolvedValueOnce('pom-default.xml'); + mvnTarget.fileExists = jest.fn().mockResolvedValue(true); + + await mvnTarget.upload('r3v1s10n'); + + expect(retrySpawnProcess).toHaveBeenCalledTimes(1); + const callArgs = (retrySpawnProcess as jest.MockedFunction< + typeof retrySpawnProcess + >).mock.calls[0]; + + expect(callArgs).toHaveLength(2); + expect(callArgs[0]).toEqual(DEFAULT_OPTION_VALUE); + + const cmdArgs = callArgs[1] as string[]; + expect(cmdArgs).toHaveLength(11); + expect(cmdArgs[0]).toBe('gpg:sign-and-deploy-file'); + expect(cmdArgs[1]).toMatch(new RegExp(`-Dfile=${klibDistDir}/${klibDistDirName}`)); + expect(cmdArgs[2]).toBe( + `-Dfiles=${klibDistDir}/${klibDistDirName}-javadoc.jar,${klibDistDir}/${klibDistDirName}-sources.jar,${klibDistDir}/${klibDistDirName}.klib,${klibDistDir}/${klibDistDirName}.module` + ); + expect(cmdArgs[3]).toBe(`-Dclassifiers=javadoc,sources,,`); + expect(cmdArgs[4]).toBe(`-Dtypes=jar,jar,klib,module`); + expect(cmdArgs[5]).toMatch( + new RegExp(`-DpomFile=${klibDistDir}/pom-default\\.xml`) + ); + expect(cmdArgs[6]).toBe(`-DrepositoryId=${DEFAULT_OPTION_VALUE}`); + expect(cmdArgs[7]).toBe(`-Durl=${DEFAULT_OPTION_VALUE}`); + expect(cmdArgs[8]).toBe(`-Dgpg.passphrase=${DEFAULT_OPTION_VALUE}`); + expect(cmdArgs[9]).toBe('--settings'); + expect(cmdArgs[10]).toBe(DEFAULT_OPTION_VALUE); + + // Restore original mock + readdirSpy.mockRestore(); + }); }); describe('closeAndReleaseRepository', () => { diff --git a/src/targets/maven.ts b/src/targets/maven.ts index 1edb502e..fcc2355d 100644 --- a/src/targets/maven.ts +++ b/src/targets/maven.ts @@ -64,6 +64,7 @@ type KotlinMultiplatformFields = { | { appleDistDirRegex: RegExp; rootDistDirRegex: RegExp; + klibDistDirRegex: RegExp; }; }; @@ -170,10 +171,17 @@ export class MavenTarget extends BaseTarget { ); } + if (!this.config.kmp.klibDistDirRegex) { + throw new ConfigurationError( + 'Required klib configuration for Kotlin Multiplatform is incorrect. See the documentation for more details.' + ); + } + return { kmp: { appleDistDirRegex: stringToRegexp(this.config.kmp.appleDistDirRegex), rootDistDirRegex: stringToRegexp(this.config.kmp.rootDistDirRegex), + klibDistDirRegex: stringToRegexp(this.config.kmp.klibDistDirRegex), }, }; } @@ -376,13 +384,16 @@ export class MavenTarget extends BaseTarget { const isAppleDistDir = this.mavenConfig.kmp.appleDistDirRegex.test( moduleName ); + const isKlibDistDir = this.mavenConfig.kmp.klibDistDirRegex.test( + moduleName + ); const files = await this.getFilesForKmpMavenPomDist(distDir); const { targetFile, pomFile } = files; const { sideArtifacts, classifiers, types, - } = this.transformKmpSideArtifacts(isRootDistDir, isAppleDistDir, files); + } = this.transformKmpSideArtifacts(isRootDistDir, isAppleDistDir, isKlibDistDir, files); await retrySpawnProcess(this.mavenConfig.mavenCliPath, [ 'gpg:sign-and-deploy-file', @@ -418,6 +429,7 @@ export class MavenTarget extends BaseTarget { * * @param isRootDistDir boolean indicating whether the distDir is the root distDir * @param isAppleDistDir boolean indicating whether the distDir is the Apple distDir + * @param isKlibDistDir boolean indicating whether the distDir is the klib-only distDir * @param files an object containing the input files, as described above * @returns a Record with three fields: * - sideArtifacts: a comma-separated string listing the paths to all generated "side artifacts" @@ -427,6 +439,7 @@ export class MavenTarget extends BaseTarget { public transformKmpSideArtifacts( isRootDistDir: boolean, isAppleDistDir: boolean, + isKlibDistDir: boolean, files: Record ): Record { const { @@ -447,25 +460,38 @@ export class MavenTarget extends BaseTarget { types += ',jar,json'; classifiers += ',all,kotlin-tooling-metadata'; } else if (isAppleDistDir) { - if (klibFiles) { - sideArtifacts += `,${klibFiles}`; - - // In order to upload cinterop klib files we need to extract the classifier from the file name. - // e.g: "sentry-kotlin-multiplatform-iosarm64-0.0.1-cinterop-Sentry.klib", - // the classifier is "cinterop-Sentry". - for (let i = 0; i < klibFiles.length; i++) { - const input = klibFiles[i]; - const start = input.indexOf('cinterop'); - const end = input.indexOf('.klib', start); - const classifier = input.substring(start, end); - - types += ',klib'; - classifiers += `,${classifier}`; - } + if (!Array.isArray(klibFiles)) { + throw new ConfigurationError( + 'klib files in apple distributions must be an array' + ); + } + sideArtifacts += `,${klibFiles}`; + + // In order to upload cinterop klib files we need to extract the classifier from the file name. + // e.g: "sentry-kotlin-multiplatform-iosarm64-0.0.1-cinterop-Sentry.klib", + // the classifier is "cinterop-Sentry". + for (let i = 0; i < klibFiles.length; i++) { + const input = klibFiles[i]; + const start = input.indexOf('cinterop'); + const end = input.indexOf('.klib', start); + const classifier = input.substring(start, end); + + types += ',klib'; + classifiers += `,${classifier}`; } + sideArtifacts += `,${metadataFile}`; types += ',jar'; classifiers += ',metadata'; + } else if (isKlibDistDir) { + if (!Array.isArray(klibFiles) || klibFiles.length !== 1) { + throw new ConfigurationError( + 'klib files in klib-only distributions must be an array with exactly one element' + ); + } + sideArtifacts += `,${klibFiles}`; + types += ',klib'; + classifiers += ','; } // .module files should be available in every KMP artifact @@ -596,12 +622,12 @@ export class MavenTarget extends BaseTarget { const moduleName = parse(distDir).base; if (this.mavenConfig.kmp !== false) { - const isRootDistDir = this.mavenConfig.kmp.rootDistDirRegex.test( - moduleName - ); - const isAppleDistDir = this.mavenConfig.kmp.appleDistDirRegex.test( - moduleName - ); + const { klibDistDirRegex, appleDistDirRegex, rootDistDirRegex } = this.mavenConfig.kmp; + + const isRootDistDir = rootDistDirRegex.test(moduleName); + const isAppleDistDir = appleDistDirRegex.test(moduleName); + const isKlibDistDir = klibDistDirRegex.test(moduleName); + if (isRootDistDir) { files['allFile'] = join(distDir, `${moduleName}-all.jar`); files['kotlinToolingMetadataFile'] = join( @@ -615,6 +641,8 @@ export class MavenTarget extends BaseTarget { .map(file => join(distDir, file)); files['klibFiles'] = cinteropFiles; + } else if (isKlibDistDir) { + files['klibFiles'] = [join(distDir, `${moduleName}.klib`)]; } } return files; @@ -649,13 +677,13 @@ export class MavenTarget extends BaseTarget { } } if (this.mavenConfig.kmp !== false) { - const isAppleDistDir = this.mavenConfig.kmp.appleDistDirRegex.test( - moduleName - ); - if (isAppleDistDir) { + const { klibDistDirRegex, appleDistDirRegex } = this.mavenConfig.kmp; + + if (klibDistDirRegex.test(moduleName) || appleDistDirRegex.test(moduleName)) { return `${moduleName}.klib`; } } + return `${moduleName}.jar`; }