diff --git a/Tasks/UsePythonVersionV0/archutil.ts b/Tasks/UsePythonVersionV0/archutil.ts new file mode 100644 index 000000000000..c7a0144eb321 --- /dev/null +++ b/Tasks/UsePythonVersionV0/archutil.ts @@ -0,0 +1,18 @@ +export type Arch = 'x86' | 'x64' | 'arm' | 'arm64'; + +export function resolveArchitecture(input?: string): Arch { + if (input && ['x86', 'x64', 'arm', 'arm64'].includes(input)) { + return input as Arch; + } + + const envArch = (process.env['AGENT_OSARCH'] || '').toLowerCase(); + if (envArch.includes('arm64') || envArch.includes('aarch64')) return 'arm64'; + if (envArch.startsWith('arm')) return 'arm'; + if (envArch.includes('x86')) return 'x86'; + + const nodeArch = (process.arch || '').toLowerCase(); + if (nodeArch === 'arm64' || nodeArch === 'aarch64') return 'arm64'; + if (nodeArch.startsWith('arm')) return 'arm'; + if (nodeArch === 'ia32' || nodeArch === 'x86') return 'x86'; + return 'x64'; +} \ No newline at end of file diff --git a/Tasks/UsePythonVersionV0/installpythonversion.ts b/Tasks/UsePythonVersionV0/installpythonversion.ts index 1f758e67b8e5..481c9c6e67f5 100644 --- a/Tasks/UsePythonVersionV0/installpythonversion.ts +++ b/Tasks/UsePythonVersionV0/installpythonversion.ts @@ -5,7 +5,7 @@ import * as rest from 'typed-rest-client'; import * as task from 'azure-pipelines-task-lib/task'; import * as tool from 'azure-pipelines-tool-lib/tool'; import * as osutil from './osutil'; - +import * as fs from 'fs'; import { TaskParameters, PythonRelease, PythonFileInfo } from './interfaces'; const MANIFEST_URL = 'https://raw.githubusercontent.com/actions/python-versions/main/versions-manifest.json'; @@ -22,6 +22,12 @@ export async function installPythonVersion(versionSpec: string, parameters: Task task.debug(`Extracted python archive to ${pythonInstallerDir}; running installation script`); + const pythonLibDir = + [path.join(pythonInstallerDir, 'python', 'lib'), + path.join(pythonInstallerDir, 'lib'), + path.join(pythonInstallerDir, 'python', 'lib64')] + .find(p => fs.existsSync(p)) || path.join(pythonInstallerDir, 'python', 'lib'); + const installerScriptOptions = { cwd: pythonInstallerDir, windowsHide: true @@ -30,7 +36,17 @@ export async function installPythonVersion(versionSpec: string, parameters: Task if (os.platform() === 'win32') { return task.exec('powershell', './setup.ps1', installerScriptOptions); } else { - return task.exec('bash', './setup.sh', installerScriptOptions); + //return task.exec('bash', './setup.sh', installerScriptOptions); + const linuxOpts = { + ...installerScriptOptions, + env: { + ...process.env, + LD_LIBRARY_PATH: `${pythonLibDir}:${process.env.LD_LIBRARY_PATH || ''}` + } + }; + task.debug(`Using LD_LIBRARY_PATH=${linuxOpts.env!.LD_LIBRARY_PATH}`); + return task.exec('bash', './setup.sh', linuxOpts); + } } @@ -44,7 +60,7 @@ export async function installPythonVersion(versionSpec: string, parameters: Task */ async function downloadPythonVersion(versionSpec: string, parameters: TaskParameters): Promise { const auth = `token ${parameters.githubToken}`; - const additionalHeaders = {}; + const additionalHeaders: Record = {}; if (parameters.githubToken) { additionalHeaders['Authorization'] = auth; } else { @@ -71,7 +87,14 @@ async function downloadPythonVersion(versionSpec: string, parameters: TaskParame task.debug(`Found matching file for system: ${matchingPythonFile.filename}`); - const pythonArchivePath: string = await tool.downloadTool(matchingPythonFile.download_url, matchingPythonFile.filename, null, additionalHeaders); + // const pythonArchivePath: string = await tool.downloadTool(matchingPythonFile.download_url, matchingPythonFile.filename, null, additionalHeaders); + + const pythonArchivePath: string = await tool.downloadTool( + matchingPythonFile.download_url, + matchingPythonFile.filename, + undefined, + additionalHeaders + ); task.debug(`Downloaded python archive to ${pythonArchivePath}`); diff --git a/Tasks/UsePythonVersionV0/task.json b/Tasks/UsePythonVersionV0/task.json index 9fb06adc7abf..a4ff76bfa2bc 100644 --- a/Tasks/UsePythonVersionV0/task.json +++ b/Tasks/UsePythonVersionV0/task.json @@ -14,7 +14,7 @@ "minimumAgentVersion": "2.182.1", "version": { "Major": 0, - "Minor": 263, + "Minor": 265, "Patch": 0 }, "demands": [], @@ -78,7 +78,8 @@ "groupName": "advanced", "options": { "x86": "x86", - "x64": "x64" + "x64": "x64", + "arm64": "arm64" } } ], diff --git a/Tasks/UsePythonVersionV0/task.loc.json b/Tasks/UsePythonVersionV0/task.loc.json index 2794758f61f8..711087bc3f0f 100644 --- a/Tasks/UsePythonVersionV0/task.loc.json +++ b/Tasks/UsePythonVersionV0/task.loc.json @@ -14,7 +14,7 @@ "minimumAgentVersion": "2.182.1", "version": { "Major": 0, - "Minor": 263, + "Minor": 265, "Patch": 0 }, "demands": [], @@ -78,7 +78,8 @@ "groupName": "advanced", "options": { "x86": "x86", - "x64": "x64" + "x64": "x64", + "arm64": "arm64" } } ], diff --git a/Tasks/UsePythonVersionV0/usepythonversion.ts b/Tasks/UsePythonVersionV0/usepythonversion.ts index 4a4a62d9d294..308b43968ca5 100644 --- a/Tasks/UsePythonVersionV0/usepythonversion.ts +++ b/Tasks/UsePythonVersionV0/usepythonversion.ts @@ -1,6 +1,6 @@ import * as os from 'os'; import * as path from 'path'; - +import * as fs from 'fs'; import * as semver from 'semver'; import * as task from 'azure-pipelines-task-lib/task'; @@ -12,6 +12,7 @@ import * as toolUtil from './toolutil'; import { desugarDevVersion, pythonVersionToSemantic, isExactVersion } from './versionspec'; import { TaskParameters } from './interfaces'; +import { resolveArchitecture } from './archutil'; // Python has "scripts" or "bin" directories where command-line tools that come with packages are installed. // This is where pip is, along with anything that pip installs. @@ -94,16 +95,29 @@ async function useCpythonVersion(parameters: Readonly, platform: const semanticVersionSpec = pythonVersionToSemantic(desugaredVersionSpec); task.debug(`Semantic version spec of ${parameters.versionSpec} is ${semanticVersionSpec}`); + const effectiveArch = resolveArchitecture(parameters.architecture); + task.debug(`Effective architecture resolved to: ${effectiveArch}`); + // Throw warning if Python version is 3.5 - if (semver.satisfies(semver.coerce(parameters.versionSpec), "3.5.*")) { - task.warning(task.loc('PythonVersionRetirement')); + + // if (semver.satisfies(semver.coerce(parameters.versionSpec), "3.5.*")) { + // task.warning(task.loc('PythonVersionRetirement')); + // } + + { + const coerced = semver.coerce(parameters.versionSpec); + if (coerced && semver.satisfies(coerced, "3.5.*")) { + task.warning(task.loc('PythonVersionRetirement')); + } } + if (isExactVersion(semanticVersionSpec)) { task.warning(task.loc('ExactVersionNotRecommended')); } - let installDir: string | null = tool.findLocalTool('Python', semanticVersionSpec, parameters.architecture); + //let installDir: string | null = tool.findLocalTool('Python', semanticVersionSpec, parameters.architecture); + let installDir: string | null = tool.findLocalTool('Python', semanticVersionSpec, effectiveArch); // Python version not found in local cache, try to download and install if (!installDir) { @@ -111,14 +125,20 @@ async function useCpythonVersion(parameters: Readonly, platform: if (!parameters.disableDownloadFromRegistry) { try { task.debug('Trying to download python from registry.'); - await installPythonVersion(semanticVersionSpec, parameters); - installDir = tool.findLocalTool('Python', semanticVersionSpec, parameters.architecture); + // await installPythonVersion(semanticVersionSpec, parameters); + // installDir = tool.findLocalTool('Python', semanticVersionSpec, parameters.architecture); + await installPythonVersion(semanticVersionSpec, { ...parameters, architecture: effectiveArch }); + installDir = tool.findLocalTool('Python', semanticVersionSpec, effectiveArch); + if (installDir) { task.debug(`Successfully installed python from registry to ${installDir}.`); } - } catch (err) { + + } catch (err: unknown) { task.error(task.loc('DownloadFailed', err.toString())); + throw err; } + } } @@ -133,17 +153,33 @@ async function useCpythonVersion(parameters: Readonly, platform: .map(s => `${s} (x64)`) .join(os.EOL); + + const arm64Versions = tool.findLocalToolVersions('Python', 'arm64') + .map(s => `${s} (arm64)`) + .join(os.EOL); throw new Error([ - task.loc('VersionNotFound', parameters.versionSpec, parameters.architecture), + task.loc('VersionNotFound', parameters.versionSpec, effectiveArch), task.loc('ListAvailableVersions', task.getVariable('Agent.ToolsDirectory')), x86Versions, x64Versions, + arm64Versions, task.loc('ToolNotFoundMicrosoftHosted', 'Python', 'https://aka.ms/hosted-agent-software'), task.loc('ToolNotFoundSelfHosted', 'Python', 'https://go.microsoft.com/fwlink/?linkid=871498') ].join(os.EOL)); } task.setVariable('pythonLocation', installDir); + //task.setVariable('LD_LIBRARY_PATH', `${path.join(installDir, 'lib')}:${process.env.LD_LIBRARY_PATH || ''}`, false, true); + + const libCandidates = [ + path.join(installDir, 'lib'), + path.join(installDir, 'python', 'lib'), + path.join(installDir, 'lib64') + ]; + const libDir = libCandidates.find(p => fs.existsSync(p)) || libCandidates[0]; + + task.setVariable('LD_LIBRARY_PATH', `${libDir}:${process.env.LD_LIBRARY_PATH || ''}`, false, true); + if (parameters.addToPath) { toolUtil.prependPathSafe(installDir); toolUtil.prependPathSafe(binDir(installDir, platform))