Skip to content

Commit

Permalink
Add microvenv support for non-windows platforms (#20985)
Browse files Browse the repository at this point in the history
This PR contains:
1. `microvenv` fallback if `venv` is not available (implemented in
python with tests)
2. Updates to telemetry to include microvenv.

Closes #20905
  • Loading branch information
karthiknadig authored and eleanorjboyd committed Apr 6, 2023
1 parent f59b23e commit e36b27c
Show file tree
Hide file tree
Showing 11 changed files with 570 additions and 154 deletions.
2 changes: 1 addition & 1 deletion pythonFiles/create_conda.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from typing import Optional, Sequence, Union

CONDA_ENV_NAME = ".conda"
CWD = pathlib.PurePath(os.getcwd())
CWD = pathlib.Path.cwd()


class VenvError(Exception):
Expand Down
97 changes: 97 additions & 0 deletions pythonFiles/create_microvenv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

import argparse
import os
import pathlib
import subprocess
import sys
import urllib.request as url_lib
from typing import Optional, Sequence

VENV_NAME = ".venv"
LIB_ROOT = pathlib.Path(__file__).parent / "lib" / "python"
CWD = pathlib.Path.cwd()


class MicroVenvError(Exception):
pass


def run_process(args: Sequence[str], error_message: str) -> None:
try:
print("Running: " + " ".join(args))
subprocess.run(args, cwd=os.getcwd(), check=True)
except subprocess.CalledProcessError:
raise MicroVenvError(error_message)


def parse_args(argv: Sequence[str]) -> argparse.Namespace:
parser = argparse.ArgumentParser()

parser.add_argument(
"--install-pip",
action="store_true",
default=False,
help="Install pip into the virtual environment.",
)

parser.add_argument(
"--name",
default=VENV_NAME,
type=str,
help="Name of the virtual environment.",
metavar="NAME",
action="store",
)
return parser.parse_args(argv)


def create_microvenv(name: str):
run_process(
[sys.executable, os.fspath(LIB_ROOT / "microvenv.py"), name],
"CREATE_MICROVENV.MICROVENV_FAILED_CREATION",
)


def download_pip_pyz(name: str):
url = "https://bootstrap.pypa.io/pip/pip.pyz"
print("CREATE_MICROVENV.DOWNLOADING_PIP")

try:
with url_lib.urlopen(url) as response:
pip_pyz_path = os.fspath(CWD / name / "pip.pyz")
with open(pip_pyz_path, "wb") as out_file:
data = response.read()
out_file.write(data)
out_file.flush()
except Exception:
raise MicroVenvError("CREATE_MICROVENV.DOWNLOAD_PIP_FAILED")


def install_pip(name: str):
pip_pyz_path = os.fspath(CWD / name / "pip.pyz")
executable = os.fspath(CWD / name / "bin" / "python")
print("CREATE_MICROVENV.INSTALLING_PIP")
run_process(
[executable, pip_pyz_path, "install", "pip"],
"CREATE_MICROVENV.INSTALL_PIP_FAILED",
)


def main(argv: Optional[Sequence[str]] = None) -> None:
if argv is None:
argv = []
args = parse_args(argv)

print("CREATE_MICROVENV.CREATING_MICROVENV")
create_microvenv(args.name)
print("CREATE_MICROVENV.CREATED_MICROVENV")

if args.install_pip:
download_pip_pyz(args.name)
install_pip(args.name)


if __name__ == "__main__":
main(sys.argv[1:])
32 changes: 25 additions & 7 deletions pythonFiles/create_venv.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
from typing import List, Optional, Sequence, Union

VENV_NAME = ".venv"
CWD = pathlib.PurePath(os.getcwd())
CWD = pathlib.Path.cwd()
MICROVENV_SCRIPT_PATH = pathlib.Path(__file__).parent / "create_microvenv.py"


class VenvError(Exception):
Expand Down Expand Up @@ -130,22 +131,39 @@ def main(argv: Optional[Sequence[str]] = None) -> None:
argv = []
args = parse_args(argv)

use_micro_venv = False
if not is_installed("venv"):
raise VenvError("CREATE_VENV.VENV_NOT_FOUND")
if sys.platform == "win32":
raise VenvError("CREATE_VENV.VENV_NOT_FOUND")
else:
use_micro_venv = True

pip_installed = is_installed("pip")
deps_needed = args.requirements or args.extras or args.toml
if deps_needed and not pip_installed:
if deps_needed and not pip_installed and not use_micro_venv:
raise VenvError("CREATE_VENV.PIP_NOT_FOUND")

if venv_exists(args.name):
venv_path = get_venv_path(args.name)
print(f"EXISTING_VENV:{venv_path}")
else:
run_process(
[sys.executable, "-m", "venv", args.name],
"CREATE_VENV.VENV_FAILED_CREATION",
)
if use_micro_venv:
run_process(
[
sys.executable,
os.fspath(MICROVENV_SCRIPT_PATH),
"--install-pip",
"--name",
args.name,
],
"CREATE_VENV.MICROVENV_FAILED_CREATION",
)
pip_installed = True
else:
run_process(
[sys.executable, "-m", "venv", args.name],
"CREATE_VENV.VENV_FAILED_CREATION",
)
venv_path = get_venv_path(args.name)
print(f"CREATED_VENV:{venv_path}")
if args.git_ignore:
Expand Down
60 changes: 60 additions & 0 deletions pythonFiles/tests/test_create_microvenv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

import importlib
import os
import sys

import create_microvenv
import pytest


def test_create_microvenv():
importlib.reload(create_microvenv)
run_process_called = False

def run_process(args, error_message):
nonlocal run_process_called
run_process_called = True
assert args == [
sys.executable,
os.fspath(create_microvenv.LIB_ROOT / "microvenv.py"),
create_microvenv.VENV_NAME,
]
assert error_message == "CREATE_MICROVENV.MICROVENV_FAILED_CREATION"

create_microvenv.run_process = run_process

create_microvenv.main()
assert run_process_called == True


def test_create_microvenv_with_pip():
importlib.reload(create_microvenv)

download_pip_pyz_called = False

def download_pip_pyz(name):
nonlocal download_pip_pyz_called
download_pip_pyz_called = True
assert name == create_microvenv.VENV_NAME

create_microvenv.download_pip_pyz = download_pip_pyz

run_process_called = False

def run_process(args, error_message):
if "install" in args and "pip" in args:
nonlocal run_process_called
run_process_called = True
pip_pyz_path = os.fspath(
create_microvenv.CWD / create_microvenv.VENV_NAME / "pip.pyz"
)
executable = os.fspath(
create_microvenv.CWD / create_microvenv.VENV_NAME / "bin" / "python"
)
assert args == [executable, pip_pyz_path, "install", "pip"]
assert error_message == "CREATE_MICROVENV.INSTALL_PIP_FAILED"

create_microvenv.run_process = run_process
create_microvenv.main(["--install-pip"])
35 changes: 34 additions & 1 deletion pythonFiles/tests/test_create_venv.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,46 @@
# Licensed under the MIT License.

import importlib
import os
import sys

import create_venv
import pytest


def test_venv_not_installed():
@pytest.mark.skipif(
sys.platform == "win32", reason="Windows does not have micro venv fallback."
)
def test_venv_not_installed_unix():
importlib.reload(create_venv)
create_venv.is_installed = lambda module: module != "venv"
run_process_called = False

def run_process(args, error_message):
nonlocal run_process_called
if "--install-pip" in args:
run_process_called = True
assert args == [
sys.executable,
os.fspath(create_venv.MICROVENV_SCRIPT_PATH),
"--install-pip",
"--name",
".test_venv",
]
assert error_message == "CREATE_VENV.MICROVENV_FAILED_CREATION"

create_venv.run_process = run_process

create_venv.main(["--name", ".test_venv"])

# run_process is called when the venv does not exist
assert run_process_called == True


@pytest.mark.skipif(
sys.platform != "win32", reason="Windows does not have microvenv fallback."
)
def test_venv_not_installed_windows():
importlib.reload(create_venv)
create_venv.is_installed = lambda module: module != "venv"
with pytest.raises(create_venv.VenvError) as e:
Expand Down
3 changes: 3 additions & 0 deletions requirements.in
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@

# Unittest test adapter
typing-extensions==4.5.0

# Fallback env creator for debian
microvenv
8 changes: 6 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
#
# This file is autogenerated by pip-compile with python 3.7
# To update, run:
# This file is autogenerated by pip-compile with Python 3.7
# by the following command:
#
# pip-compile --generate-hashes requirements.in
#
microvenv==2023.2.0 \
--hash=sha256:5b46296d6a65992946da504bd9e724a5becf5c256091f2f9383e5b4e9f567f23 \
--hash=sha256:a07e88a8fb5ee90219b86dd90095cb5646462d45d30285ea3b1a3c7cf33616d3
# via -r requirements.in
typing-extensions==4.5.0 \
--hash=sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb \
--hash=sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4
Expand Down
4 changes: 4 additions & 0 deletions src/client/common/utils/localize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,11 @@ export namespace CreateEnv {

export namespace Venv {
export const creating = l10n.t('Creating venv...');
export const creatingMicrovenv = l10n.t('Creating microvenv...');
export const created = l10n.t('Environment created...');
export const existing = l10n.t('Using existing environment...');
export const downloadingPip = l10n.t('Downloading pip...');
export const installingPip = l10n.t('Installing pip...');
export const upgradingPip = l10n.t('Upgrading pip...');
export const installingPackages = l10n.t('Installing packages...');
export const errorCreatingEnvironment = l10n.t('Error while creating virtual environment.');
Expand Down
Loading

0 comments on commit e36b27c

Please sign in to comment.