Skip to content
Open
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
130 changes: 130 additions & 0 deletions __tests__/finder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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 changes: 39 additions & 0 deletions dist/setup/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
54 changes: 54 additions & 0 deletions src/find-python.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down