diff --git a/.github/workflows/job-compile-and-test.yml b/.github/workflows/job-compile-and-test.yml index 8369c1a68..f3c6803b2 100644 --- a/.github/workflows/job-compile-and-test.yml +++ b/.github/workflows/job-compile-and-test.yml @@ -38,6 +38,10 @@ jobs: run: yarn test-yarn-lock && yarn verify-yarn-lock working-directory: Extension + - name: Validate LLDB-MI component manifest + run: yarn test-lldb-mi-component-manifest && yarn verify-lldb-mi-component-manifest + working-directory: Extension + - name: Install Dependencies run: yarn install ${{ inputs.yarn-args }} working-directory: Extension diff --git a/Extension/.scripts/verifyLldbMiComponentManifest.mjs b/Extension/.scripts/verifyLldbMiComponentManifest.mjs new file mode 100644 index 000000000..c7179d7d2 --- /dev/null +++ b/Extension/.scripts/verifyLldbMiComponentManifest.mjs @@ -0,0 +1,113 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import process from 'node:process'; +import { fileURLToPath, pathToFileURL, URL } from 'node:url'; + +const componentParameters = [ + { repository: 'llvm_repo', commit: 'llvm_commit' }, + { repository: 'lldb_mi_repo', commit: 'lldb_mi_commit' } +]; + +function parseScalarParameter(template, parameterName) { + const matches = [...template.matchAll(new RegExp(`^ ${parameterName}:\\s*(.*?)\\s*$`, 'gm'))]; + if (matches.length !== 1) { + throw new Error(`Expected ${parameterName} to appear exactly once in the LLDB-MI template.`); + } + + const serializedValue = matches[0][1]; + if (serializedValue.startsWith('"')) { + const value = JSON.parse(serializedValue); + if (typeof value !== 'string') { + throw new Error(`Expected ${parameterName} to be a string.`); + } + return value; + } + if (serializedValue.startsWith("'")) { + if (!serializedValue.endsWith("'")) { + throw new Error(`Expected ${parameterName} to be a valid scalar value.`); + } + return serializedValue.slice(1, -1).replaceAll("''", "'"); + } + if (!serializedValue || /\s/.test(serializedValue)) { + throw new Error(`Expected ${parameterName} to be a non-empty scalar value.`); + } + return serializedValue; +} + +function normalizeRepositoryUrl(repositoryUrl) { + const normalizedUrl = new URL(repositoryUrl); + normalizedUrl.hash = ''; + normalizedUrl.search = ''; + normalizedUrl.pathname = normalizedUrl.pathname.replace(/\/+$/, '').replace(/\.git$/i, ''); + return normalizedUrl.href.replace(/\/$/, ''); +} + +function getGitRegistrations(manifest) { + if (!Array.isArray(manifest.registrations)) { + throw new Error('Component manifest does not contain a registrations array.'); + } + + const registrations = new Map(); + for (const registration of manifest.registrations) { + const component = registration?.component; + if (component?.type !== 'git') { + continue; + } + + const repositoryUrl = component.git?.repositoryUrl; + const commitHash = component.git?.commitHash; + if (typeof repositoryUrl !== 'string' || typeof commitHash !== 'string') { + throw new Error('Git component registrations require repositoryUrl and commitHash strings.'); + } + + const normalizedRepositoryUrl = normalizeRepositoryUrl(repositoryUrl); + if (registrations.has(normalizedRepositoryUrl)) { + throw new Error(`Component manifest contains duplicate registrations for ${repositoryUrl}.`); + } + registrations.set(normalizedRepositoryUrl, { repositoryUrl, commitHash }); + } + return registrations; +} + +function validateLldbMiComponentManifest(template, manifest) { + const registrations = getGitRegistrations(manifest); + const errors = []; + + for (const parameters of componentParameters) { + const repositoryUrl = parseScalarParameter(template, parameters.repository); + const commitHash = parseScalarParameter(template, parameters.commit); + if (!/^[0-9a-f]{40}$/.test(commitHash)) { + errors.push(`${parameters.commit} must be a 40-character lowercase Git commit hash.`); + continue; + } + + const registration = registrations.get(normalizeRepositoryUrl(repositoryUrl)); + if (!registration) { + errors.push(`${parameters.repository} references ${repositoryUrl}, which is missing from the component manifest.`); + } else if (registration.commitHash !== commitHash) { + errors.push(`${parameters.commit} is ${commitHash}, but the component manifest registers ${registration.commitHash} for ${registration.repositoryUrl}.`); + } + } + + if (errors.length > 0) { + throw new Error(`LLDB-MI component manifest validation failed:\n${errors.map(error => ` ${error}`).join('\n')}`); + } +} + +const invokedUrl = process.argv[1] ? pathToFileURL(path.resolve(process.argv[1])).href : undefined; +if (invokedUrl === import.meta.url) { + const extensionRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); + const manifestPath = process.argv[2] ?? path.join(extensionRoot, 'cgmanifest.json'); + const templatePath = process.argv[3] ?? path.join(extensionRoot, '..', 'Build', 'lldb-mi', 'lldb-mi.template.yml'); + + try { + const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); + const template = fs.readFileSync(templatePath, 'utf8'); + validateLldbMiComponentManifest(template, manifest); + } catch (error) { + process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); + process.exitCode = 1; + } +} + +export { normalizeRepositoryUrl, parseScalarParameter, validateLldbMiComponentManifest }; \ No newline at end of file diff --git a/Extension/.scripts/verifyLldbMiComponentManifest.test.mjs b/Extension/.scripts/verifyLldbMiComponentManifest.test.mjs new file mode 100644 index 000000000..f9719a571 --- /dev/null +++ b/Extension/.scripts/verifyLldbMiComponentManifest.test.mjs @@ -0,0 +1,65 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { parseScalarParameter, validateLldbMiComponentManifest } from './verifyLldbMiComponentManifest.mjs'; + +const template = `parameters: + llvm_repo: https://github.com/llvm/llvm-project.git + llvm_commit: 0d44201451f03ba907cdb268ddddfc3fa38a0ebd + lldb_mi_repo: https://github.com/lldb-tools/lldb-mi.git + lldb_mi_commit: 2388bd74133bc21eac59b2e2bf97f2a30770a315 + +jobs: +`; + +function createManifest(llvmCommit = '0d44201451f03ba907cdb268ddddfc3fa38a0ebd') { + return { + registrations: [ + { + component: { + type: 'git', + git: { + repositoryUrl: 'https://github.com/lldb-tools/lldb-mi', + commitHash: '2388bd74133bc21eac59b2e2bf97f2a30770a315' + } + } + }, + { + component: { + type: 'git', + git: { + repositoryUrl: 'https://github.com/llvm/llvm-project', + commitHash: llvmCommit + } + } + } + ] + }; +} + +test('accepts matching build and component manifest pins', () => { + assert.doesNotThrow(() => validateLldbMiComponentManifest(template, createManifest())); +}); + +test('reports a missing repository registration', () => { + const manifest = createManifest(); + manifest.registrations.pop(); + + assert.throws( + () => validateLldbMiComponentManifest(template, manifest), + /llvm_repo references https:\/\/github\.com\/llvm\/llvm-project\.git, which is missing/ + ); +}); + +test('reports a stale registered commit', () => { + assert.throws( + () => validateLldbMiComponentManifest(template, createManifest('1111111111111111111111111111111111111111')), + /llvm_commit is 0d44201451f03ba907cdb268ddddfc3fa38a0ebd, but the component manifest registers 1111111111111111111111111111111111111111/ + ); +}); + +test('requires each build parameter exactly once', () => { + assert.throws( + () => parseScalarParameter(`${template} llvm_repo: https://example.com/duplicate.git\n`, 'llvm_repo'), + /Expected llvm_repo to appear exactly once/ + ); +}); \ No newline at end of file diff --git a/Extension/cgmanifest.json b/Extension/cgmanifest.json index 92adbdc44..add25de0b 100644 --- a/Extension/cgmanifest.json +++ b/Extension/cgmanifest.json @@ -1,15 +1,25 @@ { "$schema": "https://json.schemastore.org/component-detection-manifest.json", - "Registrations": [ + "registrations": [ { - "Component": { - "Type": "git", - "Git": { - "RepositoryUrl": "https://github.com/lldb-tools/lldb-mi", - "CommitHash": "2388bd74133bc21eac59b2e2bf97f2a30770a315" + "component": { + "type": "git", + "git": { + "repositoryUrl": "https://github.com/lldb-tools/lldb-mi", + "commitHash": "2388bd74133bc21eac59b2e2bf97f2a30770a315" } } + }, + { + "component": { + "type": "git", + "git": { + "repositoryUrl": "https://github.com/llvm/llvm-project", + "commitHash": "0d44201451f03ba907cdb268ddddfc3fa38a0ebd" + } + }, + "developmentDependency": true } ], - "Version": 1 + "version": 1 } diff --git a/Extension/package.json b/Extension/package.json index a9e22974c..805ab83ff 100644 --- a/Extension/package.json +++ b/Extension/package.json @@ -7163,7 +7163,9 @@ "scripts": "ts-node -T .scripts/scripts.ts", "show": "ts-node -T .scripts/clean.ts show", "clean": "ts-node -T .scripts/clean.ts", + "test-lldb-mi-component-manifest": "node --test .scripts/verifyLldbMiComponentManifest.test.mjs", "test-yarn-lock": "node --test .scripts/verifyYarnLock.test.mjs", + "verify-lldb-mi-component-manifest": "node .scripts/verifyLldbMiComponentManifest.mjs", "verify-yarn-lock": "node .scripts/verifyYarnLock.mjs", "test": "yarn install && (yarn verify prep --quiet || yarn prep) && (yarn verify compiled --quiet || yarn build) && ts-node -T .scripts/test.ts", "code": "yarn install && (yarn verify compiled --quiet || yarn build) && yarn verify binaries && ts-node -T .scripts/code.ts",