feat: more robust pip installation and upgrade

This commit is contained in:
Amin Yahyaabadi 2023-06-28 14:45:15 -07:00
parent 7039a1a602
commit 0f6b349a1e
6 changed files with 116 additions and 70 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,3 +1,4 @@
/* eslint-disable require-atomic-updates */
import { addPath } from "../utils/env/addEnv" import { addPath } from "../utils/env/addEnv"
import { setupAptPack } from "../utils/setup/setupAptPack" import { setupAptPack } from "../utils/setup/setupAptPack"
import { setupPacmanPack } from "../utils/setup/setupPacmanPack" import { setupPacmanPack } from "../utils/setup/setupPacmanPack"
@ -41,6 +42,7 @@ export async function setupPythonViaSystem(
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
_arch: string _arch: string
): Promise<InstallationInfo> { ): Promise<InstallationInfo> {
let installInfo: InstallationInfo
switch (process.platform) { switch (process.platform) {
case "win32": { case "win32": {
if (setupDir) { if (setupDir) {
@ -56,81 +58,123 @@ export async function setupPythonViaSystem(
const pythonSetupDir = dirname(pythonBinPath) const pythonSetupDir = dirname(pythonBinPath)
/** The directory which the tool is installed to */ /** The directory which the tool is installed to */
await addPath(pythonSetupDir) await addPath(pythonSetupDir)
return { installDir: pythonSetupDir, binDir: pythonSetupDir } installInfo = { installDir: pythonSetupDir, binDir: pythonSetupDir }
break
} }
case "darwin": { case "darwin": {
return setupBrewPack("python3", version) installInfo = await setupBrewPack("python3", version)
break
} }
case "linux": { case "linux": {
let installInfo: InstallationInfo
if (isArch()) { if (isArch()) {
installInfo = await setupPacmanPack("python", version) installInfo = await setupPacmanPack("python", version)
await setupPacmanPack("python-pip")
} else if (hasDnf()) { } else if (hasDnf()) {
installInfo = setupDnfPack("python3", version) installInfo = setupDnfPack("python3", version)
setupDnfPack("python3-pip")
} else if (isUbuntu()) { } else if (isUbuntu()) {
installInfo = await setupAptPack([{ name: "python3", version }, { name: "python3-pip" }]) installInfo = await setupAptPack([{ name: "python3", version }])
} else { } else {
throw new Error("Unsupported linux distributions") throw new Error("Unsupported linux distributions")
} }
return installInfo break
} }
default: { default: {
throw new Error("Unsupported platform") throw new Error("Unsupported platform")
} }
} }
await findOrSetupPip((await findPython())!)
return installInfo
} }
let setupPythonAndPipTried = false
/// setup python and pip if needed /// setup python and pip if needed
export async function setupPythonAndPip(): Promise<string> { export async function findOrSetupPythonAndPip(): Promise<string> {
let foundPython: string const foundPython = await findOrSetupPython()
const foundPip = await findOrSetupPip(foundPython)
// install python if (foundPip === undefined) {
if (which.sync("python3", { nothrow: true }) !== null) { throw new Error("pip was not installed correctly")
foundPython = "python3"
} else if (which.sync("python", { nothrow: true }) !== null && (await isBinUptoDate("python", "3.0.0"))) {
foundPython = "python"
} else {
info("python3 was not found. Installing python")
await setupPython(getVersion("python", undefined), "", process.arch)
// try again
if (setupPythonAndPipTried) {
throw new Error("Failed to install python")
}
setupPythonAndPipTried = true
return setupPythonAndPip() // recurse
} }
setupWheel(foundPython)
assert(typeof foundPython === "string")
await setupPip(foundPython)
// install wheel (required for Conan, Meson, etc.)
execaSync(foundPython, ["-m", "pip", "install", "-U", "wheel"], { stdio: "inherit" })
return foundPython return foundPython
} }
async function setupPip(foundPython: string) { let setupPythonTried = false
const mayBePip = unique(["pip3", "pip"])
for (const pip of mayBePip) { async function findPython() {
if (which.sync(pip, { nothrow: true }) !== null) { if (which.sync("python3", { nothrow: true }) !== null) {
// eslint-disable-next-line no-await-in-loop return "python3"
if (await isBinUptoDate(pip, DefaultVersions.pip!)) { } else if (which.sync("python", { nothrow: true }) !== null && (await isBinUptoDate("python", "3.0.0"))) {
return pip return "python"
} else { }
// upgrade pip return undefined
execaSync(foundPython, ["-m", "pip", "install", "-U", "--upgrade", "pip"], { stdio: "inherit" }) }
return setupPip(foundPython) // recurse to check if pip is on PATH and up-to-date
} async function findOrSetupPython() {
} const maybeFoundPython = await findPython()
if (maybeFoundPython !== undefined) {
return maybeFoundPython
} }
// install pip if not installed if (setupPythonTried) {
throw new Error("Failed to install python")
}
setupPythonTried = true
// install python
info("python3 was not found. Installing python")
await setupPython(getVersion("python", undefined), "", process.arch)
return findOrSetupPython() // recurse
}
async function findOrSetupPip(foundPython: string) {
const maybePip = await findPip()
if (maybePip === undefined) {
// install pip if not installed
info("pip was not found. Installing pip")
await setupPip(foundPython)
return findPip() // recurse to check if pip is on PATH and up-to-date
}
return maybePip
}
async function findPip() {
for (const pip of ["pip3", "pip"]) {
if (
which.sync(pip, { nothrow: true }) !== null &&
// eslint-disable-next-line no-await-in-loop
(await isBinUptoDate(pip, DefaultVersions.pip!))
) {
return pip
}
}
return undefined
}
async function setupPip(foundPython: string) {
const upgraded = ensurePipUpgrade(foundPython)
if (!upgraded) {
await setupPipSystem()
}
}
function ensurePipUpgrade(foundPython: string) {
try {
execaSync(foundPython, ["-m", "ensurepip", "-U", "--upgrade"], { stdio: "inherit" })
return true
} catch {
try {
// ensure pip is disabled on Ubuntu
execaSync(foundPython, ["-m", "pip", "install", "--upgrade", "pip"], { stdio: "inherit" })
return true
} catch {
// pip module not found
}
}
// all methods failed
return false
}
async function setupPipSystem() {
if (process.platform === "linux") { if (process.platform === "linux") {
// ensure that pip is installed on Linux (happens when python is found but pip not installed) // ensure that pip is installed on Linux (happens when python is found but pip not installed)
if (isArch()) { if (isArch()) {
@ -140,11 +184,13 @@ async function setupPip(foundPython: string) {
} else if (isUbuntu()) { } else if (isUbuntu()) {
await setupAptPack([{ name: "python3-pip" }]) await setupAptPack([{ name: "python3-pip" }])
} }
} else {
throw new Error(`Could not find pip on ${process.platform}`)
} }
throw new Error(`Could not install pip on ${process.platform}`)
}
return setupPip(foundPython) // recurse to check if pip is on PATH and up-to-date /** Install wheel (required for Conan, Meson, etc.) */
function setupWheel(foundPython: string) {
execaSync(foundPython, ["-m", "pip", "install", "-U", "wheel"], { stdio: "inherit" })
} }
export async function addPythonBaseExecPrefix(python: string) { export async function addPythonBaseExecPrefix(python: string) {

View File

@ -3,7 +3,7 @@ import { execaSync } from "execa"
import { pathExists } from "path-exists" import { pathExists } from "path-exists"
import { addExeExt, dirname, join } from "patha" import { addExeExt, dirname, join } from "patha"
import which from "which" import which from "which"
import { addPythonBaseExecPrefix, setupPythonAndPip } from "../../python/python" import { addPythonBaseExecPrefix, findOrSetupPythonAndPip } from "../../python/python"
import { addPath } from "../env/addEnv" import { addPath } from "../env/addEnv"
import { InstallationInfo } from "./setupBin" import { InstallationInfo } from "./setupBin"
@ -16,7 +16,7 @@ export async function setupPipPack(name: string, version?: string): Promise<Inst
info(`Installing ${name} ${version ?? ""} via pip`) info(`Installing ${name} ${version ?? ""} via pip`)
if (python === undefined) { if (python === undefined) {
python = await setupPythonAndPip() python = await findOrSetupPythonAndPip()
} }
execaSync(python, ["-m", "pip", "install", version !== undefined && version !== "" ? `${name}==${version}` : name], { execaSync(python, ["-m", "pip", "install", version !== undefined && version !== "" ? `${name}==${version}` : name], {