diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a8350464..3839a3ad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,9 +22,28 @@ jobs: with: node-version: '22.x' + - name: Install sysroot + if: runner.os == 'Linux' + run: | + sudo apt-get update -qq + sudo apt-get install -y gcc-10 g++-10 + SYSROOT_PATH=$(node scripts/linux/install-sysroot.js x64 | grep "SYSROOT_PATH=" | cut -d= -f2) + echo "SYSROOT_PATH=$SYSROOT_PATH" >> $GITHUB_ENV + echo "Sysroot path set to: $SYSROOT_PATH" + echo "CC=gcc-10" >> $GITHUB_ENV + echo "CXX=g++-10" >> $GITHUB_ENV + - name: Install dependencies and build run: npm ci + - name: Verify GLIBC requirements + if: runner.os == 'Linux' + run: | + EXPECTED_GLIBC_VERSION="2.28" \ + EXPECTED_GLIBCXX_VERSION="3.4.25" \ + SEARCH_PATH="build" \ + ./scripts/linux/verify-glibc-requirements.sh + - name: Test run: npm test diff --git a/binding.gyp b/binding.gyp index 5f63978b..8c8c535a 100644 --- a/binding.gyp +++ b/binding.gyp @@ -80,6 +80,7 @@ '-lutil' ], 'cflags': ['-Wall', '-O2', '-D_FORTIFY_SOURCE=2'], + 'ldflags': [], 'conditions': [ # http://www.gnu.org/software/gnulib/manual/html_node/forkpty.html # One some systems (at least including Cygwin, Interix, @@ -88,6 +89,61 @@ 'libraries!': [ '-lutil' ] + }], + ['OS=="linux"', { + 'variables': { + 'sysroot%': '=11.0.0" + } + }, "node_modules/functional-red-black-tree": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", @@ -3150,6 +3168,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -3228,6 +3247,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz", "integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/pipelines/build.yml b/pipelines/build.yml index c73dbec7..781a091e 100644 --- a/pipelines/build.yml +++ b/pipelines/build.yml @@ -13,8 +13,23 @@ steps: addToPath: true displayName: 'Use latest Python 3.x' +- bash: | + if [ "$(uname)" = "Linux" ]; then + sudo apt-get update -qq + sudo apt-get install -y gcc-10 g++-10 + SYSROOT_PATH=$(node scripts/linux/install-sysroot.js ${{ parameters.arch }} | grep "SYSROOT_PATH=" | cut -d= -f2) + echo "##vso[task.setvariable variable=SYSROOT_PATH]$SYSROOT_PATH" + echo "##vso[task.setvariable variable=CC]gcc-10" + echo "##vso[task.setvariable variable=CXX]g++-10" + echo "Sysroot path set to: $SYSROOT_PATH" + fi + displayName: 'Install sysroot (Linux only)' + - script: npm ci displayName: 'Install dependencies' env: ARCH: ${{ parameters.arch }} npm_config_arch: ${{ parameters.arch }} + SYSROOT_PATH: $(SYSROOT_PATH) + CC: $(CC) + CXX: $(CXX) diff --git a/pipelines/prebuilds.yml b/pipelines/prebuilds.yml index 360e86ef..b6934a93 100644 --- a/pipelines/prebuilds.yml +++ b/pipelines/prebuilds.yml @@ -138,8 +138,6 @@ extends: cp -r $(Build.ArtifactStagingDirectory)/win32-arm64 $(Build.ArtifactStagingDirectory)/prebuilds/ cp -r $(Build.ArtifactStagingDirectory)/darwin-x64 $(Build.ArtifactStagingDirectory)/prebuilds/ cp -r $(Build.ArtifactStagingDirectory)/darwin-arm64 $(Build.ArtifactStagingDirectory)/prebuilds/ - - # Exclude Linux prebuilds for now - # cp -r $(Build.ArtifactStagingDirectory)/linux-x64 $(Build.ArtifactStagingDirectory)/prebuilds/ - # cp -r $(Build.ArtifactStagingDirectory)/linux-arm64 $(Build.ArtifactStagingDirectory)/prebuilds/ + cp -r $(Build.ArtifactStagingDirectory)/linux-x64 $(Build.ArtifactStagingDirectory)/prebuilds/ + cp -r $(Build.ArtifactStagingDirectory)/linux-arm64 $(Build.ArtifactStagingDirectory)/prebuilds/ displayName: 'Create prebuilds archive' diff --git a/scripts/linux/checksums.txt b/scripts/linux/checksums.txt new file mode 100644 index 00000000..66504f0a --- /dev/null +++ b/scripts/linux/checksums.txt @@ -0,0 +1,2 @@ +3122af49c493c5c767c2b0772a41119cbdc9803125a705683445b4066dc88b82 x86_64-linux-gnu-glibc-2.28-gcc-10.5.0.tar.gz +3baac81a39b69e0929e4700f4f78f022adefc515010054ec393565657c4fff32 aarch64-linux-gnu-glibc-2.28-gcc-10.5.0.tar.gz diff --git a/scripts/linux/install-sysroot.js b/scripts/linux/install-sysroot.js new file mode 100755 index 00000000..4b68f2a5 --- /dev/null +++ b/scripts/linux/install-sysroot.js @@ -0,0 +1,157 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +const { execSync } = require('child_process'); +const { tmpdir } = require('os'); +const fs = require('fs'); +const path = require('path'); +const { createHash } = require('crypto'); + +const REPO_ROOT = path.join(__dirname, '..', '..'); + +const ghApiHeaders = { + Accept: 'application/vnd.github.v3+json', + 'User-Agent': 'node-pty Build', +}; + +if (process.env.GITHUB_TOKEN) { + ghApiHeaders.Authorization = 'Basic ' + Buffer.from(process.env.GITHUB_TOKEN).toString('base64'); +} + +const ghDownloadHeaders = { + ...ghApiHeaders, + Accept: 'application/octet-stream', +}; + +function getSysrootChecksum(expectedName) { + const checksumPath = path.join(REPO_ROOT, 'scripts', 'linux', 'checksums.txt'); + const checksums = fs.readFileSync(checksumPath, 'utf8'); + for (const line of checksums.split('\n')) { + const [checksum, name] = line.split(/\s+/); + if (name === expectedName) { + return checksum; + } + } + return undefined; +} + +async function fetchUrl(options, retries = 10, retryDelay = 1000) { + try { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 30 * 1000); + const version = '20250407-330404'; + try { + const response = await fetch(`https://api.github.com/repos/Microsoft/vscode-linux-build-agent/releases/tags/v${version}`, { + headers: ghApiHeaders, + signal: controller.signal + }); + if (response.ok && (response.status >= 200 && response.status < 300)) { + console.log(`Fetch completed: Status ${response.status}.`); + const contents = Buffer.from(await response.arrayBuffer()); + const asset = JSON.parse(contents.toString()).assets.find((a) => a.name === options.assetName); + if (!asset) { + throw new Error(`Could not find asset in release of Microsoft/vscode-linux-build-agent @ ${version}`); + } + console.log(`Found asset ${options.assetName} @ ${asset.url}.`); + const assetResponse = await fetch(asset.url, { + headers: ghDownloadHeaders + }); + if (assetResponse.ok && (assetResponse.status >= 200 && assetResponse.status < 300)) { + const assetContents = Buffer.from(await assetResponse.arrayBuffer()); + console.log(`Fetched response body buffer: ${assetContents.byteLength} bytes`); + if (options.checksumSha256) { + const actualSHA256Checksum = createHash('sha256').update(assetContents).digest('hex'); + if (actualSHA256Checksum !== options.checksumSha256) { + throw new Error(`Checksum mismatch for ${asset.url} (expected ${options.checksumSha256}, actual ${actualSHA256Checksum})`); + } + } + console.log(`Verified SHA256 checksums match for ${asset.url}`); + const tarCommand = `tar -xz -C ${options.dest}`; + execSync(tarCommand, { input: assetContents }); + console.log(`Fetch complete!`); + return; + } + throw new Error(`Request ${asset.url} failed with status code: ${assetResponse.status}`); + } + throw new Error(`Request https://api.github.com failed with status code: ${response.status}`); + } finally { + clearTimeout(timeout); + } + } catch (e) { + if (retries > 0) { + console.log(`Fetching failed: ${e}`); + await new Promise(resolve => setTimeout(resolve, retryDelay)); + return fetchUrl(options, retries - 1, retryDelay); + } + throw e; + } +} + +async function getSysroot(arch) { + let expectedName; + let triple; + const prefix = '-glibc-2.28-gcc-10.5.0'; + + switch (arch) { + case 'x64': + expectedName = `x86_64-linux-gnu${prefix}.tar.gz`; + triple = 'x86_64-linux-gnu'; + break; + case 'arm64': + expectedName = `aarch64-linux-gnu${prefix}.tar.gz`; + triple = 'aarch64-linux-gnu'; + break; + default: + throw new Error(`Unsupported architecture: ${arch}`); + } + + console.log(`Fetching ${expectedName} for ${triple}`); + const checksumSha256 = getSysrootChecksum(expectedName); + if (!checksumSha256) { + throw new Error(`Could not find checksum for ${expectedName}`); + } + + const sysroot = path.join(tmpdir(), `vscode-${arch}-sysroot`); + const stamp = path.join(sysroot, '.stamp'); + const result = `${sysroot}/${triple}/${triple}/sysroot`; + + if (fs.existsSync(stamp) && fs.readFileSync(stamp).toString() === expectedName) { + console.log(`Sysroot already installed: ${result}`); + return result; + } + + console.log(`Installing ${arch} root image: ${sysroot}`); + fs.rmSync(sysroot, { recursive: true, force: true }); + fs.mkdirSync(sysroot, { recursive: true }); + + await fetchUrl({ + checksumSha256, + assetName: expectedName, + dest: sysroot + }); + + fs.writeFileSync(stamp, expectedName); + console.log(`Sysroot installed: ${result}`); + return result; +} + +async function main() { + const arch = process.argv[2] || process.env.ARCH || 'x64'; + console.log(`Installing sysroot for architecture: ${arch}`); + + try { + const sysrootPath = await getSysroot(arch); + console.log(`SYSROOT_PATH=${sysrootPath}`); + } catch (error) { + console.error('Error installing sysroot:', error); + process.exit(1); + } +} + +if (require.main === module) { + main(); +} + +module.exports = { getSysroot }; diff --git a/scripts/linux/verify-glibc-requirements.sh b/scripts/linux/verify-glibc-requirements.sh new file mode 100755 index 00000000..a911dad1 --- /dev/null +++ b/scripts/linux/verify-glibc-requirements.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash + +set -e + +# Get all files with .node extension from given folder +files=$(find $SEARCH_PATH -name "*.node") + +echo "Verifying requirements for files: $files" + +for file in $files; do + glibc_version="$EXPECTED_GLIBC_VERSION" + glibcxx_version="$EXPECTED_GLIBCXX_VERSION" + while IFS= read -r line; do + if [[ $line == *"GLIBC_"* ]]; then + version=$(echo "$line" | awk '{if ($5 ~ /^[0-9a-fA-F]+$/) print $6; else print $5}' | tr -d '()') + version=${version#*_} + if [[ $(printf "%s\n%s" "$version" "$glibc_version" | sort -V | tail -n1) == "$version" ]]; then + glibc_version=$version + fi + elif [[ $line == *"GLIBCXX_"* ]]; then + version=$(echo "$line" | awk '{if ($5 ~ /^[0-9a-fA-F]+$/) print $6; else print $5}' | tr -d '()') + version=${version#*_} + if [[ $(printf "%s\n%s" "$version" "$glibcxx_version" | sort -V | tail -n1) == "$version" ]]; then + glibcxx_version=$version + fi + fi + done < <("$SYSROOT_PATH/../bin/objdump" -T "$file") + + if [[ "$glibc_version" != "$EXPECTED_GLIBC_VERSION" ]]; then + echo "Error: File $file has dependency on GLIBC > $EXPECTED_GLIBC_VERSION, found $glibc_version" + exit 1 + fi + if [[ "$glibcxx_version" != "$EXPECTED_GLIBCXX_VERSION" ]]; then + echo "Error: File $file has dependency on GLIBCXX > $EXPECTED_GLIBCXX_VERSION, found $glibcxx_version" + fi +done