Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions __tests__/find-graalpy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,22 +137,28 @@ describe('findGraalPyToolCache', () => {
jest.restoreAllMocks();
});

it('GraalPy exists on the path and versions are satisfied', () => {
expect(finder.findGraalPyToolCache('23.0.0', architecture)).toEqual({
it('GraalPy exists on the path and versions are satisfied', async () => {
await expect(
finder.findGraalPyToolCache('23.0.0', architecture)
).resolves.toEqual({
installDir: graalpyPath,
resolvedGraalPyVersion: actualGraalPyVersion
});
});

it('GraalPy exists on the path and versions are satisfied with semver', () => {
expect(finder.findGraalPyToolCache('23.0', architecture)).toEqual({
it('GraalPy exists on the path and versions are satisfied with semver', async () => {
await expect(
finder.findGraalPyToolCache('23.0', architecture)
).resolves.toEqual({
installDir: graalpyPath,
resolvedGraalPyVersion: actualGraalPyVersion
});
});

it("GraalPy exists on the path, but version doesn't match", () => {
expect(finder.findGraalPyToolCache('22.3', architecture)).toEqual({
it("GraalPy exists on the path, but version doesn't match", async () => {
await expect(
finder.findGraalPyToolCache('22.3', architecture)
).resolves.toEqual({
installDir: '',
resolvedGraalPyVersion: ''
});
Expand Down
24 changes: 16 additions & 8 deletions __tests__/find-pypy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,32 +187,40 @@ describe('findPyPyToolCache', () => {
jest.restoreAllMocks();
});

it('PyPy exists on the path and versions are satisfied', () => {
expect(finder.findPyPyToolCache('3.6.17', 'v7.5.4', architecture)).toEqual({
it('PyPy exists on the path and versions are satisfied', async () => {
await expect(
finder.findPyPyToolCache('3.6.17', 'v7.5.4', architecture)
).resolves.toEqual({
installDir: pypyPath,
resolvedPythonVersion: actualPythonVersion,
resolvedPyPyVersion: actualPyPyVersion
});
});

it('PyPy exists on the path and versions are satisfied with semver', () => {
expect(finder.findPyPyToolCache('3.6', 'v7.5.x', architecture)).toEqual({
it('PyPy exists on the path and versions are satisfied with semver', async () => {
await expect(
finder.findPyPyToolCache('3.6', 'v7.5.x', architecture)
).resolves.toEqual({
installDir: pypyPath,
resolvedPythonVersion: actualPythonVersion,
resolvedPyPyVersion: actualPyPyVersion
});
});

it("PyPy exists on the path, but Python version doesn't match", () => {
expect(finder.findPyPyToolCache('3.7', 'v7.5.4', architecture)).toEqual({
it("PyPy exists on the path, but Python version doesn't match", async () => {
await expect(
finder.findPyPyToolCache('3.7', 'v7.5.4', architecture)
).resolves.toEqual({
installDir: '',
resolvedPythonVersion: '',
resolvedPyPyVersion: ''
});
});

it("PyPy exists on the path, but PyPy version doesn't match", () => {
expect(finder.findPyPyToolCache('3.6', 'v7.5.1', architecture)).toEqual({
it("PyPy exists on the path, but PyPy version doesn't match", async () => {
await expect(
finder.findPyPyToolCache('3.6', 'v7.5.1', architecture)
).resolves.toEqual({
installDir: null,
resolvedPythonVersion: '',
resolvedPyPyVersion: ''
Expand Down
43 changes: 41 additions & 2 deletions __tests__/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from '@jest/globals';
import {fileURLToPath} from 'url';
import path from 'path';
import fsSync from 'fs';

const __dirname = path.dirname(fileURLToPath(import.meta.url));

Expand Down Expand Up @@ -45,9 +46,19 @@ jest.unstable_mockModule('@actions/core', () => ({
toPosixPath: jest.fn((p: string) => p)
}));

// Mock @actions/io so we can spy on rmRF (issue #1087 purge helper).
// mkdirP is used by other tests to create real temp dirs, so delegate to fs.
// mkdirP is NOT a jest.fn to survive jest.resetAllMocks() calls elsewhere.
jest.unstable_mockModule('@actions/io', () => ({
rmRF: jest.fn(async () => {}),
mkdirP: async (p: string) => {
fsSync.mkdirSync(p, {recursive: true});
}
}));

const cache = await import('@actions/cache');
const core = await import('@actions/core');
import * as io from '@actions/io';
const io = await import('@actions/io');

import fs from 'fs';

Expand All @@ -63,7 +74,9 @@ const {
isGhes,
IS_WINDOWS,
getDownloadFileName,
getVersionInputFromToolVersions
getVersionInputFromToolVersions,
isCachedPythonUsable,
purgeCachedTool
} = await import('../src/utils.js');

describe('validatePythonVersionFormatForPyPy', () => {
Expand Down Expand Up @@ -425,3 +438,29 @@ describe('isGhes', () => {
expect(isGhes()).toBeTruthy();
});
});

describe('OS-compatibility guard (issue #1087)', () => {
afterEach(() => {
jest.restoreAllMocks();
});

it('isCachedPythonUsable returns true immediately on non-Linux', async () => {
if (process.platform === 'linux') return;
// No fs / exec calls should be needed off Linux.
await expect(isCachedPythonUsable('/anything')).resolves.toBe(true);
});

it('isCachedPythonUsable returns false when the binary is missing on Linux', async () => {
if (process.platform !== 'linux') return;
jest.spyOn(fs, 'existsSync').mockReturnValueOnce(false);
await expect(isCachedPythonUsable('/nonexistent')).resolves.toBe(false);
});

it('purgeCachedTool removes the install dir and its .complete marker', async () => {
(io.rmRF as jest.Mock).mockReset();
(io.rmRF as jest.Mock).mockImplementation(async () => undefined);
await purgeCachedTool('/tmp/tool/Python/3.10/x64');
expect(io.rmRF).toHaveBeenCalledWith('/tmp/tool/Python/3.10/x64');
expect(io.rmRF).toHaveBeenCalledWith('/tmp/tool/Python/3.10/x64.complete');
});
});
26 changes: 26 additions & 0 deletions dist/cache-save/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -97046,6 +97046,7 @@ var toml_toml = __nccwpck_require__(4572);




const utils_IS_WINDOWS = process.platform === 'win32';
const utils_IS_LINUX = process.platform === 'linux';
const IS_MAC = process.platform === 'darwin';
Expand Down Expand Up @@ -97391,6 +97392,31 @@ function getDownloadFileName(downloadUrl) {
? path.join(tempDir, path.basename(downloadUrl))
: undefined;
}
async function isCachedPythonUsable(installDir) {
if (!utils_IS_LINUX)
return true;
const py = path.join(installDir, 'bin', 'python');
if (!fs.existsSync(py))
return false;
try {
const rc = await exec.exec(py, ['-c', 'import ssl, sys; sys.stdout.write(sys.version)'], {
silent: true,
ignoreReturnCode: true,
env: {
...process.env,
LD_LIBRARY_PATH: path.join(installDir, 'lib')
}
});
return rc === 0;
}
catch {
return false;
}
}
async function purgeCachedTool(installDir) {
await io.rmRF(installDir);
await io.rmRF(`${installDir}.complete`);
}

;// CONCATENATED MODULE: ./src/cache-distributions/cache-distributor.ts

Expand Down
62 changes: 58 additions & 4 deletions dist/setup/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -97038,6 +97038,7 @@ var toml = __nccwpck_require__(4572);




const utils_IS_WINDOWS = process.platform === 'win32';
const IS_LINUX = process.platform === 'linux';
const IS_MAC = process.platform === 'darwin';
Expand Down Expand Up @@ -97383,6 +97384,31 @@ function getDownloadFileName(downloadUrl) {
? external_path_.join(tempDir, external_path_.basename(downloadUrl))
: undefined;
}
async function isCachedPythonUsable(installDir) {
if (!IS_LINUX)
return true;
const py = external_path_.join(installDir, 'bin', 'python');
if (!external_fs_default().existsSync(py))
return false;
try {
const rc = await exec_exec(py, ['-c', 'import ssl, sys; sys.stdout.write(sys.version)'], {
silent: true,
ignoreReturnCode: true,
env: {
...process.env,
LD_LIBRARY_PATH: external_path_.join(installDir, 'lib')
}
});
return rc === 0;
}
catch {
return false;
}
}
async function purgeCachedTool(installDir) {
await rmRF(installDir);
await rmRF(`${installDir}.complete`);
}

;// CONCATENATED MODULE: ./node_modules/@actions/tool-cache/lib/manifest.js
var manifest_awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
Expand Down Expand Up @@ -98490,6 +98516,18 @@ async function useCpythonVersion(version, architecture, updateEnvironment, check
}
}
let installDir = find('Python', semanticVersionSpec, architecture);
// Issue #1087: on self-hosted runners that reuse the tool cache across
// different Linux OS versions, a cached Python built for one OS can be
// handed to a job running on another and crash with GLIBC / OpenSSL
// errors. Verify the cached interpreter can actually run here; if not,
// purge it and fall through to the normal install path.
if (installDir && !(await isCachedPythonUsable(installDir))) {
warning(`Cached Python at ${installDir} is not runnable on this OS ` +
`(see https://github.com/actions/setup-python/issues/1087). ` +
`Removing and reinstalling.`);
await purgeCachedTool(installDir);
installDir = null;
}
if (!installDir) {
info(`Version ${semanticVersionSpec} was not found in the local cache`);
const foundRelease = await findReleaseFromManifest(semanticVersionSpec, architecture, manifest);
Expand Down Expand Up @@ -98818,7 +98856,8 @@ async function findPyPyVersion(versionSpec, architecture, updateEnvironment, che
}
}
}
({ installDir, resolvedPythonVersion, resolvedPyPyVersion } = findPyPyToolCache(pypyVersionSpec.pythonVersion, pypyVersionSpec.pypyVersion, architecture));
({ installDir, resolvedPythonVersion, resolvedPyPyVersion } =
await findPyPyToolCache(pypyVersionSpec.pythonVersion, pypyVersionSpec.pypyVersion, architecture));
if (!installDir) {
({ installDir, resolvedPythonVersion, resolvedPyPyVersion } =
await installPyPy(pypyVersionSpec.pypyVersion, pypyVersionSpec.pythonVersion, architecture, allowPreReleases, releases));
Expand All @@ -98844,12 +98883,19 @@ async function findPyPyVersion(versionSpec, architecture, updateEnvironment, che
setOutput('python-path', pythonPath);
return { resolvedPyPyVersion, resolvedPythonVersion };
}
function findPyPyToolCache(pythonVersion, pypyVersion, architecture) {
async function findPyPyToolCache(pythonVersion, pypyVersion, architecture) {
let resolvedPyPyVersion = '';
let resolvedPythonVersion = '';
let installDir = utils_IS_WINDOWS
? findPyPyInstallDirForWindows(pythonVersion)
: find('PyPy', pythonVersion, architecture);
if (installDir && !(await isCachedPythonUsable(installDir))) {
warning(`Cached PyPy at ${installDir} is not runnable on this OS ` +
`(see https://github.com/actions/setup-python/issues/1087). ` +
`Removing and reinstalling.`);
await purgeCachedTool(installDir);
installDir = null;
}
if (installDir) {
// 'tc.find' finds tool based on Python version but we also need to check
// whether PyPy version satisfies requested version.
Expand Down Expand Up @@ -99109,7 +99155,7 @@ async function findGraalPyVersion(versionSpec, architecture, updateEnvironment,
}
}
}
({ installDir, resolvedGraalPyVersion } = findGraalPyToolCache(graalpyVersionSpec, architecture));
({ installDir, resolvedGraalPyVersion } = await findGraalPyToolCache(graalpyVersionSpec, architecture));
if (!installDir) {
({ installDir, resolvedGraalPyVersion } = await installGraalPy(graalpyVersionSpec, architecture, allowPreReleases, releases));
}
Expand All @@ -99134,9 +99180,17 @@ async function findGraalPyVersion(versionSpec, architecture, updateEnvironment,
setOutput('python-path', pythonPath);
return resolvedGraalPyVersion;
}
function findGraalPyToolCache(graalpyVersion, architecture) {
async function findGraalPyToolCache(graalpyVersion, architecture) {
let resolvedGraalPyVersion = '';
let installDir = find('GraalPy', graalpyVersion, architecture);
// Issue #1087: purge cache entries whose interpreter can't run on this OS.
if (installDir && !(await isCachedPythonUsable(installDir))) {
warning(`Cached GraalPy at ${installDir} is not runnable on this OS ` +
`(see https://github.com/actions/setup-python/issues/1087). ` +
`Removing and reinstalling.`);
await purgeCachedTool(installDir);
installDir = null;
}
if (installDir) {
// 'tc.find' finds tool based on Python version but we also need to check
// whether GraalPy version satisfies requested version.
Expand Down
23 changes: 20 additions & 3 deletions src/find-graalpy.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import * as path from 'path';
import * as graalpyInstall from './install-graalpy.js';
import {IS_WINDOWS, validateVersion, IGraalPyManifestRelease} from './utils.js';
import {
IS_WINDOWS,
validateVersion,
IGraalPyManifestRelease,
isCachedPythonUsable,
purgeCachedTool
} from './utils.js';

import * as semver from 'semver';
import * as core from '@actions/core';
Expand Down Expand Up @@ -40,7 +46,7 @@ export async function findGraalPyVersion(
}
}

({installDir, resolvedGraalPyVersion} = findGraalPyToolCache(
({installDir, resolvedGraalPyVersion} = await findGraalPyToolCache(
graalpyVersionSpec,
architecture
));
Expand Down Expand Up @@ -77,7 +83,7 @@ export async function findGraalPyVersion(
return resolvedGraalPyVersion;
}

export function findGraalPyToolCache(
export async function findGraalPyToolCache(
graalpyVersion: string,
architecture: string
) {
Expand All @@ -88,6 +94,17 @@ export function findGraalPyToolCache(
architecture
);

// Issue #1087: purge cache entries whose interpreter can't run on this OS.
if (installDir && !(await isCachedPythonUsable(installDir))) {
core.warning(
`Cached GraalPy at ${installDir} is not runnable on this OS ` +
`(see https://github.com/actions/setup-python/issues/1087). ` +
`Removing and reinstalling.`
);
await purgeCachedTool(installDir);
installDir = null;
}

if (installDir) {
// 'tc.find' finds tool based on Python version but we also need to check
// whether GraalPy version satisfies requested version.
Expand Down
Loading
Loading