Skip to content

Commit d03a985

Browse files
authored
fix(cloudformation): Use GitHub manifest to resolve cfn lsp first, target old linux builds (#8383)
## Problem * Fix potential GitHub API rate limiting issue * Use older build of linux for older machines ## Solution * Attempt using manifest json to resolve cfn lsp versions, fallback to original GitHub releases approach * When in a sandbox environment, or required version of GLIC is not available, use older version of linux build --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). - License: I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent 0605975 commit d03a985

File tree

8 files changed

+504
-73
lines changed

8 files changed

+504
-73
lines changed

packages/core/src/awsService/cloudformation/extension.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ import { CfnInitUiInterface } from './cfn-init/cfnInitUiInterface'
8383
import { CfnInitCliCaller } from './cfn-init/cfnInitCliCaller'
8484
import { CfnEnvironmentFileSelector } from './ui/cfnEnvironmentFileSelector'
8585
import { fs } from '../../shared/fs/fs'
86+
import { ToolkitError } from '../../shared/errors'
8687

8788
let client: LanguageClient
8889
let clientDisposables: Disposable[] = []
@@ -167,11 +168,10 @@ async function startClient(context: ExtensionContext) {
167168
},
168169
errorHandler: {
169170
error: (error: Error, message: Message | undefined, count: number | undefined): ErrorHandlerResult => {
170-
void window.showErrorMessage(formatMessage(`Error count = ${count}): ${toString(message)}`))
171+
void window.showErrorMessage(formatMessage(`${toString(message)} - ${toString(error)}`))
171172
return { action: ErrorAction.Continue }
172173
},
173174
closed: (): CloseHandlerResult => {
174-
void window.showWarningMessage(formatMessage(`Server connection closed`))
175175
return { action: CloseAction.DoNotRestart }
176176
},
177177
},
@@ -331,8 +331,8 @@ export async function activate(context: ExtensionContext) {
331331

332332
try {
333333
await startClient(context)
334-
} catch (err: any) {
335-
getLogger().error(`CloudFormation language server failed to start: ${toString(err)}`)
334+
} catch (err) {
335+
getLogger('awsCfnLsp').error(ToolkitError.chain(err, 'CloudFormation language server failed to start'))
336336
}
337337
}
338338

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { execSync } from 'child_process' // eslint-disable-line no-restricted-imports, aws-toolkits/no-string-exec-for-child-process
7+
import * as fs from 'fs' // eslint-disable-line no-restricted-imports
8+
import * as semver from 'semver'
9+
import { getLogger } from '../../../shared/logger/logger'
10+
11+
interface VersionResult {
12+
maxFound: string | undefined
13+
allAvailable: string[]
14+
}
15+
16+
export class CLibCheck {
17+
/**
18+
* Checks the GNU C Library (glibc) version.
19+
* Uses `ldd --version` to parse the version number.
20+
*/
21+
public static getGLibCVersion(): string | undefined {
22+
try {
23+
const output = execSync('ldd --version', {
24+
encoding: 'utf8',
25+
stdio: ['ignore', 'pipe', 'ignore'],
26+
timeout: 5000,
27+
})
28+
// Output usually looks like: "ldd (Ubuntu GLIBC 2.35-0ubuntu3.1) 2.35"
29+
// We look for the first version number pattern on the first line.
30+
const firstLine = output.split('\n')[0]
31+
const match = firstLine.match(/(\d+\.\d+)/)
32+
return match ? semver.coerce(match[0])?.version || match[0] : undefined
33+
} catch (error) {
34+
getLogger('awsCfnLsp').warn('Could not run ldd. Is this a glibc-based distro?')
35+
return undefined
36+
}
37+
}
38+
39+
/**
40+
* Checks available GLIBCXX versions in libstdc++.
41+
* 1. Finds libstdc++.so.6 location.
42+
* 2. Scans the binary for "GLIBCXX_*" strings.
43+
* 3. Sorts them to find the maximum version supported.
44+
*/
45+
public static getGLibCXXVersions(): VersionResult {
46+
const libPath = this.findLibStdCpp()
47+
48+
if (!libPath) {
49+
return { maxFound: undefined, allAvailable: [] }
50+
}
51+
52+
try {
53+
// Method 1: Try using the `strings` command (fastest, but requires binutils)
54+
const output = execSync(`strings "${libPath}" | grep GLIBCXX`, {
55+
encoding: 'utf8',
56+
stdio: ['ignore', 'pipe', 'ignore'],
57+
timeout: 10000,
58+
})
59+
return this.parseGLibCXXOutput(output)
60+
} catch (e) {
61+
// Method 2: Fallback to Node.js FS reading (works in minimal containers w/o strings)
62+
try {
63+
const content = fs.readFileSync(libPath, 'binary') // Read as binary string
64+
// Regex to find all GLIBCXX_x.x.x occurrences
65+
const matches = content.match(/GLIBCXX_\d+\.\d+(\.\d+)?/g)
66+
if (matches) {
67+
return this.parseGLibCXXOutput(matches.join('\n'))
68+
}
69+
} catch (readError) {
70+
getLogger('awsCfnLsp').error(`Failed to read library at ${libPath}`)
71+
}
72+
}
73+
74+
return { maxFound: undefined, allAvailable: [] }
75+
}
76+
77+
private static parseGLibCXXOutput(rawOutput: string): VersionResult {
78+
const rawVersions = rawOutput
79+
.trim()
80+
.split('\n')
81+
.map((line) => line.trim())
82+
// 1. Strict Filter: Must be GLIBCXX_ followed immediately by a digit
83+
.filter((line) => /^GLIBCXX_\d/.test(line))
84+
// 2. Extraction: Capture strictly the numeric part
85+
.map((line) => {
86+
const match = line.match(/^GLIBCXX_(\d+\.\d+(?:\.\d+)?)/)
87+
return match ? match[1] : undefined
88+
})
89+
.filter((v): v is string => v !== undefined)
90+
91+
// 3. Deduplicate
92+
const uniqueVersions = [...new Set(rawVersions)]
93+
94+
// 4. Sort using Semver
95+
// We use coerce() because "3.4" is not valid strict semver, but "3.4.0" is.
96+
const sorted = uniqueVersions.sort((a, b) => {
97+
const verA = semver.coerce(a)
98+
const verB = semver.coerce(b)
99+
// Handle unlikely case where coerce fails (returns null) by pushing it to the bottom
100+
if (!verA || !verB) {
101+
return 0
102+
}
103+
return semver.compare(verA, verB)
104+
})
105+
106+
return {
107+
maxFound: sorted.length > 0 ? sorted[sorted.length - 1] : undefined,
108+
allAvailable: sorted,
109+
}
110+
}
111+
112+
private static findLibStdCpp(): string | undefined {
113+
// 1. Try ldconfig cache (most reliable on standard linux)
114+
try {
115+
const ldconfig = execSync('/sbin/ldconfig -p | grep libstdc++.so.6', { encoding: 'utf8', timeout: 5000 })
116+
// Output: "libstdc++.so.6 (libc6,x86-64) => /lib/x86_64-linux-gnu/libstdc++.so.6"
117+
const match = ldconfig.match(/=>\s+(.+)$/m)
118+
if (match && match[1]) {
119+
return match[1].trim()
120+
}
121+
} catch (e) {
122+
/* ignore */
123+
}
124+
125+
// 2. Search common paths (fallback for containers/weird setups)
126+
const commonPaths = [
127+
'/usr/lib/x86_64-linux-gnu/libstdc++.so.6',
128+
'/usr/lib64/libstdc++.so.6',
129+
'/usr/lib/libstdc++.so.6',
130+
'/lib/x86_64-linux-gnu/libstdc++.so.6',
131+
]
132+
133+
for (const p of commonPaths) {
134+
if (fs.existsSync(p)) {
135+
return p
136+
}
137+
}
138+
139+
return undefined
140+
}
141+
}

packages/core/src/awsService/cloudformation/lsp-server/githubManifestAdapter.ts

Lines changed: 102 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,18 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import { Manifest, LspVersion, Target } from '../../../shared/lsp/types'
76
import { CfnLspName, CfnLspServerEnvType } from './lspServerConfig'
8-
import { addWindows, dedupeAndGetLatestVersions } from './utils'
7+
import {
8+
addWindows,
9+
CfnManifest,
10+
CfnTarget,
11+
CfnLspVersion,
12+
dedupeAndGetLatestVersions,
13+
extractPlatformAndArch,
14+
useOldLinuxVersion,
15+
} from './utils'
916
import { getLogger } from '../../../shared/logger/logger'
17+
import { ToolkitError } from '../../../shared/errors'
1018

1119
export class GitHubManifestAdapter {
1220
constructor(
@@ -15,7 +23,78 @@ export class GitHubManifestAdapter {
1523
readonly environment: CfnLspServerEnvType
1624
) {}
1725

18-
async getManifest(): Promise<Manifest> {
26+
async getManifest(): Promise<CfnManifest> {
27+
let manifest: CfnManifest
28+
try {
29+
manifest = await this.getManifestJson()
30+
} catch (err) {
31+
getLogger('awsCfnLsp').error(ToolkitError.chain(err, 'Failed to get CloudFormation manifest'))
32+
manifest = await this.getFromReleases()
33+
}
34+
35+
getLogger('awsCfnLsp').info(
36+
'Candidate versions: %s',
37+
manifest.versions
38+
.map(
39+
(v) =>
40+
`${v.serverVersion}[${v.targets
41+
.sort()
42+
.map((t) => `${t.platform}-${t.arch}-${t.nodejs}`)
43+
.join(',')}]`
44+
)
45+
.join(', ')
46+
)
47+
48+
if (process.platform !== 'linux') {
49+
return manifest
50+
}
51+
52+
const useFallbackLinux = useOldLinuxVersion()
53+
if (!useFallbackLinux) {
54+
return manifest
55+
}
56+
57+
getLogger('awsCfnLsp').warn('Using GLIBC compatible version for Linux')
58+
const versions = manifest.versions.map((version) => {
59+
const targets = version.targets
60+
.filter((target) => {
61+
return target.platform !== 'linux'
62+
})
63+
.map((target) => {
64+
if (target.platform !== 'linuxglib2.28') {
65+
return target
66+
}
67+
68+
return {
69+
...target,
70+
platform: 'linux',
71+
}
72+
})
73+
74+
return {
75+
...version,
76+
targets,
77+
}
78+
})
79+
80+
manifest.versions = versions
81+
82+
getLogger('awsCfnLsp').info(
83+
'Remapped candidate versions from platform linuxglib2.28 to linux: %s',
84+
manifest.versions
85+
.map(
86+
(v) =>
87+
`${v.serverVersion}[${v.targets
88+
.sort()
89+
.map((t) => `${t.platform}-${t.arch}-${t.nodejs}`)
90+
.join(',')}]`
91+
)
92+
.join(', ')
93+
)
94+
return manifest
95+
}
96+
97+
private async getFromReleases(): Promise<CfnManifest> {
1998
const releases = await this.fetchGitHubReleases()
2099
const envReleases = this.filterByEnvironment(releases)
21100
const sortedReleases = envReleases.sort((a, b) => {
@@ -58,21 +137,22 @@ export class GitHubManifestAdapter {
58137
return response.json()
59138
}
60139

61-
private convertRelease(release: GitHubRelease): LspVersion {
140+
private convertRelease(release: GitHubRelease): CfnLspVersion {
62141
return {
63142
serverVersion: release.tag_name,
64143
isDelisted: false,
65144
targets: addWindows(this.extractTargets(release.assets)),
66145
}
67146
}
68147

69-
private extractTargets(assets: GitHubAsset[]): Target[] {
70-
return this.filterByNodeVersion(assets).map((asset) => {
71-
const { arch, platform } = this.extractPlatformAndArch(asset.name)
148+
private extractTargets(assets: GitHubAsset[]): CfnTarget[] {
149+
return assets.map((asset) => {
150+
const { arch, platform, nodejs } = extractPlatformAndArch(asset.name)
72151

73152
return {
74153
platform,
75154
arch,
155+
nodejs,
76156
contents: [
77157
{
78158
filename: asset.name,
@@ -85,58 +165,28 @@ export class GitHubManifestAdapter {
85165
})
86166
}
87167

88-
private filterByNodeVersion(assets: GitHubAsset[]): GitHubAsset[] {
89-
const hasNodeVersion = assets.map((asset) => asset.name).some((name) => name.includes('-node'))
90-
const nodeVersion = process.version.replaceAll('v', '').split('.')[0]
91-
92-
if (hasNodeVersion) {
93-
const matchedVersion = assets.filter((asset) => {
94-
return asset.name.includes(`-node${nodeVersion}`)
95-
})
96-
97-
if (matchedVersion.length > 0) {
98-
return matchedVersion
99-
}
100-
101-
const latestVersion = this.getLatestNodeVersion(assets)
102-
getLogger().warn(`Could not find bundle for Node.js version ${nodeVersion}, using latest ${latestVersion}`)
103-
return assets.filter((asset) => asset.name.includes(`-node${latestVersion}`))
168+
private async getManifestJson(): Promise<CfnManifest> {
169+
const response = await fetch(
170+
`https://raw.githubusercontent.com/${this.repoOwner}/${this.repoName}/refs/heads/main/assets/release-manifest.json`
171+
)
172+
if (!response.ok) {
173+
throw new Error(`GitHub API error: ${response.status}`)
104174
}
105175

106-
return assets
107-
}
108-
109-
private extractPlatformAndArch(filename: string): {
110-
arch: string
111-
platform: string
112-
} {
113-
const lower = filename.toLowerCase().replaceAll(/-node.*$/g, '')
114-
const parts = lower.split('-')
176+
const json = (await response.json()) as Record<string, unknown>
115177

116-
const arch = parts.pop()
117-
const platform = parts.pop()
118-
119-
if (!platform || !arch) {
120-
throw new Error(`Unknown arch and platform ${arch} ${platform}`)
178+
return {
179+
manifestSchemaVersion: json.manifestSchemaVersion as string,
180+
artifactId: json.artifactId as string,
181+
artifactDescription: json.artifactDescription as string,
182+
isManifestDeprecated: json.isManifestDeprecated as boolean,
183+
versions: json[this.environment] as CfnLspVersion[],
121184
}
122-
123-
return { arch, platform }
124-
}
125-
126-
private getLatestNodeVersion(assets: GitHubAsset[]): number {
127-
const versions = assets
128-
.map((asset) => {
129-
const match = asset.name.match(/-node(\d+)/)
130-
return match ? parseInt(match[1]) : undefined
131-
})
132-
.filter((v): v is number => v !== undefined)
133-
134-
return Math.max(...versions)
135185
}
136186
}
137187

138188
/* eslint-disable @typescript-eslint/naming-convention */
139-
export interface GitHubAsset {
189+
interface GitHubAsset {
140190
url: string
141191
browser_download_url: string
142192
id: number
@@ -151,7 +201,7 @@ export interface GitHubAsset {
151201
updated_at: string
152202
}
153203

154-
export interface GitHubRelease {
204+
interface GitHubRelease {
155205
url: string
156206
html_url: string
157207
assets_url: string

0 commit comments

Comments
 (0)