This commit is contained in:
Bruno Verachten 2026-03-19 13:43:23 +00:00 committed by GitHub
commit 875291d648
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 223 additions and 0 deletions

View file

@ -21,6 +21,7 @@ process.env['RUNNER_TEMP'] = tempDir;
import * as tc from '@actions/tool-cache';
import * as core from '@actions/core';
import * as exec from '@actions/exec';
import * as finder from '../src/find-python';
import * as installer from '../src/install-python';
@ -298,4 +299,133 @@ describe('Finder tests', () => {
expect(spyCoreAddPath).not.toHaveBeenCalled();
expect(spyCoreExportVariable).not.toHaveBeenCalled();
});
describe('System Python fallback', () => {
let execSpy: jest.SpyInstance;
let manifestSpy: jest.SpyInstance;
beforeEach(() => {
// Mock the manifest to return entries only for x64, not riscv64
manifestSpy = jest.spyOn(installer, 'getManifest');
manifestSpy.mockImplementation(
async () => <tc.IToolRelease[]>manifestData
);
});
it('Falls back to system Python on unsupported architecture', async () => {
execSpy = jest.spyOn(exec, 'getExecOutput');
execSpy.mockImplementation(async () => ({
exitCode: 0,
stdout:
'/usr/bin/python3\n3.12.0\n/usr\n/usr/bin\n',
stderr: ''
}));
const result = await finder.useCpythonVersion(
'3.12',
'riscv64',
true,
false,
false,
false
);
expect(result).toEqual({impl: 'CPython', version: '3.12.0'});
expect(spyCoreAddPath).toHaveBeenCalledWith('/usr/bin');
expect(spyCoreExportVariable).toHaveBeenCalledWith(
'pythonLocation',
'/usr'
);
});
it('Does not fall back on supported architecture with missing version', async () => {
// x64 has manifest entries, so fallback should NOT trigger
let thrown = false;
try {
await finder.useCpythonVersion(
'3.300000',
'x64',
true,
false,
false,
false
);
} catch {
thrown = true;
}
expect(thrown).toBeTruthy();
});
it('Does not fall back when system Python version does not match', async () => {
execSpy = jest.spyOn(exec, 'getExecOutput');
execSpy.mockImplementation(async () => ({
exitCode: 0,
stdout:
'/usr/bin/python3\n3.11.5\n/usr\n/usr/bin\n',
stderr: ''
}));
let thrown = false;
try {
await finder.useCpythonVersion(
'3.12',
'riscv64',
true,
false,
false,
false
);
} catch {
thrown = true;
}
expect(thrown).toBeTruthy();
});
it('Does not fall back for freethreaded builds', async () => {
execSpy = jest.spyOn(exec, 'getExecOutput');
execSpy.mockImplementation(async () => ({
exitCode: 0,
stdout:
'/usr/bin/python3\n3.13.0\n/usr\n/usr/bin\n',
stderr: ''
}));
let thrown = false;
try {
await finder.useCpythonVersion(
'3.13t',
'riscv64',
true,
false,
false,
false
);
} catch {
thrown = true;
}
expect(thrown).toBeTruthy();
});
it('Handles missing system Python gracefully', async () => {
execSpy = jest.spyOn(exec, 'getExecOutput');
execSpy.mockImplementation(async () => {
throw new Error('python3 not found');
});
let thrown = false;
try {
await finder.useCpythonVersion(
'3.12',
'riscv64',
true,
false,
false,
false
);
} catch {
thrown = true;
}
expect(thrown).toBeTruthy();
});
});
});

39
dist/setup/index.js vendored
View file

@ -82997,6 +82997,45 @@ async function useCpythonVersion(version, architecture, updateEnvironment, check
installDir = tc.find('Python', semanticVersionSpec, architecture);
}
}
if (!installDir && !freethreaded) {
// Try system Python as fallback, but only for architectures that have
// no pre-built binaries in the manifest at all. This prevents the
// fallback from firing when a specific version is missing on a
// supported architecture (e.g., requesting Python 3.99 on x86_64).
const baseArchitecture = architecture.replace('-freethreaded', '');
if (!manifest) {
manifest = await installer.getManifest();
}
const archHasManifestEntries = manifest?.some(release => release.files?.some((file) => file.arch === baseArchitecture));
if (!archHasManifestEntries) {
try {
const sysInfo = await exec.getExecOutput('python3', [
'-c',
'import sys, os; print(sys.executable + "\\n" + f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" + "\\n" + sys.prefix + "\\n" + os.path.dirname(sys.executable))'
]);
if (sysInfo.exitCode === 0) {
const [sysExecutable, sysVersion, sysPrefix, sysBinDir] = sysInfo.stdout.trim().split('\n');
if (semver.satisfies(sysVersion, semanticVersionSpec)) {
core.warning(`Pre-built Python not available for architecture '${baseArchitecture}'. Using system Python ${sysVersion} at ${sysExecutable}.`);
if (updateEnvironment) {
core.exportVariable('pythonLocation', sysPrefix);
core.exportVariable('PKG_CONFIG_PATH', sysPrefix + '/lib/pkgconfig');
core.exportVariable('Python_ROOT_DIR', sysPrefix);
core.exportVariable('Python2_ROOT_DIR', sysPrefix);
core.exportVariable('Python3_ROOT_DIR', sysPrefix);
core.addPath(sysBinDir);
}
core.setOutput('python-version', sysVersion);
core.setOutput('python-path', sysExecutable);
return { impl: 'CPython', version: sysVersion };
}
}
}
catch {
// System Python not available, fall through to error
}
}
}
if (!installDir) {
const osInfo = await (0, utils_1.getOSInfo)();
const msg = [

View file

@ -122,6 +122,60 @@ export async function useCpythonVersion(
}
}
if (!installDir && !freethreaded) {
// Try system Python as fallback, but only for architectures that have
// no pre-built binaries in the manifest at all. This prevents the
// fallback from firing when a specific version is missing on a
// supported architecture (e.g., requesting Python 3.99 on x86_64).
const baseArchitecture = architecture.replace('-freethreaded', '');
if (!manifest) {
manifest = await installer.getManifest();
}
const archHasManifestEntries = manifest?.some(
release =>
release.files?.some(
(file: {arch: string}) => file.arch === baseArchitecture
)
);
if (!archHasManifestEntries) {
try {
const sysInfo = await exec.getExecOutput('python3', [
'-c',
'import sys, os; print(sys.executable + "\\n" + f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" + "\\n" + sys.prefix + "\\n" + os.path.dirname(sys.executable))'
]);
if (sysInfo.exitCode === 0) {
const [sysExecutable, sysVersion, sysPrefix, sysBinDir] =
sysInfo.stdout.trim().split('\n');
if (semver.satisfies(sysVersion, semanticVersionSpec)) {
core.warning(
`Pre-built Python not available for architecture '${baseArchitecture}'. Using system Python ${sysVersion} at ${sysExecutable}.`
);
if (updateEnvironment) {
core.exportVariable('pythonLocation', sysPrefix);
core.exportVariable(
'PKG_CONFIG_PATH',
sysPrefix + '/lib/pkgconfig'
);
core.exportVariable('Python_ROOT_DIR', sysPrefix);
core.exportVariable('Python2_ROOT_DIR', sysPrefix);
core.exportVariable('Python3_ROOT_DIR', sysPrefix);
core.addPath(sysBinDir);
}
core.setOutput('python-version', sysVersion);
core.setOutput('python-path', sysExecutable);
return {impl: 'CPython', version: sysVersion};
}
}
} catch {
// System Python not available, fall through to error
}
}
}
if (!installDir) {
const osInfo = await getOSInfo();
const msg = [