Skip to content

Commit 7241760

Browse files
authored
Add microvenv support for non-windows platforms (#20985)
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
1 parent 3c84470 commit 7241760

File tree

11 files changed

+570
-154
lines changed

11 files changed

+570
-154
lines changed

pythonFiles/create_conda.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from typing import Optional, Sequence, Union
1010

1111
CONDA_ENV_NAME = ".conda"
12-
CWD = pathlib.PurePath(os.getcwd())
12+
CWD = pathlib.Path.cwd()
1313

1414

1515
class VenvError(Exception):

pythonFiles/create_microvenv.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
import argparse
5+
import os
6+
import pathlib
7+
import subprocess
8+
import sys
9+
import urllib.request as url_lib
10+
from typing import Optional, Sequence
11+
12+
VENV_NAME = ".venv"
13+
LIB_ROOT = pathlib.Path(__file__).parent / "lib" / "python"
14+
CWD = pathlib.Path.cwd()
15+
16+
17+
class MicroVenvError(Exception):
18+
pass
19+
20+
21+
def run_process(args: Sequence[str], error_message: str) -> None:
22+
try:
23+
print("Running: " + " ".join(args))
24+
subprocess.run(args, cwd=os.getcwd(), check=True)
25+
except subprocess.CalledProcessError:
26+
raise MicroVenvError(error_message)
27+
28+
29+
def parse_args(argv: Sequence[str]) -> argparse.Namespace:
30+
parser = argparse.ArgumentParser()
31+
32+
parser.add_argument(
33+
"--install-pip",
34+
action="store_true",
35+
default=False,
36+
help="Install pip into the virtual environment.",
37+
)
38+
39+
parser.add_argument(
40+
"--name",
41+
default=VENV_NAME,
42+
type=str,
43+
help="Name of the virtual environment.",
44+
metavar="NAME",
45+
action="store",
46+
)
47+
return parser.parse_args(argv)
48+
49+
50+
def create_microvenv(name: str):
51+
run_process(
52+
[sys.executable, os.fspath(LIB_ROOT / "microvenv.py"), name],
53+
"CREATE_MICROVENV.MICROVENV_FAILED_CREATION",
54+
)
55+
56+
57+
def download_pip_pyz(name: str):
58+
url = "https://bootstrap.pypa.io/pip/pip.pyz"
59+
print("CREATE_MICROVENV.DOWNLOADING_PIP")
60+
61+
try:
62+
with url_lib.urlopen(url) as response:
63+
pip_pyz_path = os.fspath(CWD / name / "pip.pyz")
64+
with open(pip_pyz_path, "wb") as out_file:
65+
data = response.read()
66+
out_file.write(data)
67+
out_file.flush()
68+
except Exception:
69+
raise MicroVenvError("CREATE_MICROVENV.DOWNLOAD_PIP_FAILED")
70+
71+
72+
def install_pip(name: str):
73+
pip_pyz_path = os.fspath(CWD / name / "pip.pyz")
74+
executable = os.fspath(CWD / name / "bin" / "python")
75+
print("CREATE_MICROVENV.INSTALLING_PIP")
76+
run_process(
77+
[executable, pip_pyz_path, "install", "pip"],
78+
"CREATE_MICROVENV.INSTALL_PIP_FAILED",
79+
)
80+
81+
82+
def main(argv: Optional[Sequence[str]] = None) -> None:
83+
if argv is None:
84+
argv = []
85+
args = parse_args(argv)
86+
87+
print("CREATE_MICROVENV.CREATING_MICROVENV")
88+
create_microvenv(args.name)
89+
print("CREATE_MICROVENV.CREATED_MICROVENV")
90+
91+
if args.install_pip:
92+
download_pip_pyz(args.name)
93+
install_pip(args.name)
94+
95+
96+
if __name__ == "__main__":
97+
main(sys.argv[1:])

pythonFiles/create_venv.py

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
from typing import List, Optional, Sequence, Union
1111

1212
VENV_NAME = ".venv"
13-
CWD = pathlib.PurePath(os.getcwd())
13+
CWD = pathlib.Path.cwd()
14+
MICROVENV_SCRIPT_PATH = pathlib.Path(__file__).parent / "create_microvenv.py"
1415

1516

1617
class VenvError(Exception):
@@ -130,22 +131,39 @@ def main(argv: Optional[Sequence[str]] = None) -> None:
130131
argv = []
131132
args = parse_args(argv)
132133

134+
use_micro_venv = False
133135
if not is_installed("venv"):
134-
raise VenvError("CREATE_VENV.VENV_NOT_FOUND")
136+
if sys.platform == "win32":
137+
raise VenvError("CREATE_VENV.VENV_NOT_FOUND")
138+
else:
139+
use_micro_venv = True
135140

136141
pip_installed = is_installed("pip")
137142
deps_needed = args.requirements or args.extras or args.toml
138-
if deps_needed and not pip_installed:
143+
if deps_needed and not pip_installed and not use_micro_venv:
139144
raise VenvError("CREATE_VENV.PIP_NOT_FOUND")
140145

141146
if venv_exists(args.name):
142147
venv_path = get_venv_path(args.name)
143148
print(f"EXISTING_VENV:{venv_path}")
144149
else:
145-
run_process(
146-
[sys.executable, "-m", "venv", args.name],
147-
"CREATE_VENV.VENV_FAILED_CREATION",
148-
)
150+
if use_micro_venv:
151+
run_process(
152+
[
153+
sys.executable,
154+
os.fspath(MICROVENV_SCRIPT_PATH),
155+
"--install-pip",
156+
"--name",
157+
args.name,
158+
],
159+
"CREATE_VENV.MICROVENV_FAILED_CREATION",
160+
)
161+
pip_installed = True
162+
else:
163+
run_process(
164+
[sys.executable, "-m", "venv", args.name],
165+
"CREATE_VENV.VENV_FAILED_CREATION",
166+
)
149167
venv_path = get_venv_path(args.name)
150168
print(f"CREATED_VENV:{venv_path}")
151169
if args.git_ignore:
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
import importlib
5+
import os
6+
import sys
7+
8+
import create_microvenv
9+
import pytest
10+
11+
12+
def test_create_microvenv():
13+
importlib.reload(create_microvenv)
14+
run_process_called = False
15+
16+
def run_process(args, error_message):
17+
nonlocal run_process_called
18+
run_process_called = True
19+
assert args == [
20+
sys.executable,
21+
os.fspath(create_microvenv.LIB_ROOT / "microvenv.py"),
22+
create_microvenv.VENV_NAME,
23+
]
24+
assert error_message == "CREATE_MICROVENV.MICROVENV_FAILED_CREATION"
25+
26+
create_microvenv.run_process = run_process
27+
28+
create_microvenv.main()
29+
assert run_process_called == True
30+
31+
32+
def test_create_microvenv_with_pip():
33+
importlib.reload(create_microvenv)
34+
35+
download_pip_pyz_called = False
36+
37+
def download_pip_pyz(name):
38+
nonlocal download_pip_pyz_called
39+
download_pip_pyz_called = True
40+
assert name == create_microvenv.VENV_NAME
41+
42+
create_microvenv.download_pip_pyz = download_pip_pyz
43+
44+
run_process_called = False
45+
46+
def run_process(args, error_message):
47+
if "install" in args and "pip" in args:
48+
nonlocal run_process_called
49+
run_process_called = True
50+
pip_pyz_path = os.fspath(
51+
create_microvenv.CWD / create_microvenv.VENV_NAME / "pip.pyz"
52+
)
53+
executable = os.fspath(
54+
create_microvenv.CWD / create_microvenv.VENV_NAME / "bin" / "python"
55+
)
56+
assert args == [executable, pip_pyz_path, "install", "pip"]
57+
assert error_message == "CREATE_MICROVENV.INSTALL_PIP_FAILED"
58+
59+
create_microvenv.run_process = run_process
60+
create_microvenv.main(["--install-pip"])

pythonFiles/tests/test_create_venv.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,46 @@
22
# Licensed under the MIT License.
33

44
import importlib
5+
import os
56
import sys
67

78
import create_venv
89
import pytest
910

1011

11-
def test_venv_not_installed():
12+
@pytest.mark.skipif(
13+
sys.platform == "win32", reason="Windows does not have micro venv fallback."
14+
)
15+
def test_venv_not_installed_unix():
16+
importlib.reload(create_venv)
17+
create_venv.is_installed = lambda module: module != "venv"
18+
run_process_called = False
19+
20+
def run_process(args, error_message):
21+
nonlocal run_process_called
22+
if "--install-pip" in args:
23+
run_process_called = True
24+
assert args == [
25+
sys.executable,
26+
os.fspath(create_venv.MICROVENV_SCRIPT_PATH),
27+
"--install-pip",
28+
"--name",
29+
".test_venv",
30+
]
31+
assert error_message == "CREATE_VENV.MICROVENV_FAILED_CREATION"
32+
33+
create_venv.run_process = run_process
34+
35+
create_venv.main(["--name", ".test_venv"])
36+
37+
# run_process is called when the venv does not exist
38+
assert run_process_called == True
39+
40+
41+
@pytest.mark.skipif(
42+
sys.platform != "win32", reason="Windows does not have microvenv fallback."
43+
)
44+
def test_venv_not_installed_windows():
1245
importlib.reload(create_venv)
1346
create_venv.is_installed = lambda module: module != "venv"
1447
with pytest.raises(create_venv.VenvError) as e:

requirements.in

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,6 @@
55

66
# Unittest test adapter
77
typing-extensions==4.5.0
8+
9+
# Fallback env creator for debian
10+
microvenv

requirements.txt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
#
2-
# This file is autogenerated by pip-compile with python 3.7
3-
# To update, run:
2+
# This file is autogenerated by pip-compile with Python 3.7
3+
# by the following command:
44
#
55
# pip-compile --generate-hashes requirements.in
66
#
7+
microvenv==2023.2.0 \
8+
--hash=sha256:5b46296d6a65992946da504bd9e724a5becf5c256091f2f9383e5b4e9f567f23 \
9+
--hash=sha256:a07e88a8fb5ee90219b86dd90095cb5646462d45d30285ea3b1a3c7cf33616d3
10+
# via -r requirements.in
711
typing-extensions==4.5.0 \
812
--hash=sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb \
913
--hash=sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4

src/client/common/utils/localize.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,7 +438,11 @@ export namespace CreateEnv {
438438

439439
export namespace Venv {
440440
export const creating = l10n.t('Creating venv...');
441+
export const creatingMicrovenv = l10n.t('Creating microvenv...');
441442
export const created = l10n.t('Environment created...');
443+
export const existing = l10n.t('Using existing environment...');
444+
export const downloadingPip = l10n.t('Downloading pip...');
445+
export const installingPip = l10n.t('Installing pip...');
442446
export const upgradingPip = l10n.t('Upgrading pip...');
443447
export const installingPackages = l10n.t('Installing packages...');
444448
export const errorCreatingEnvironment = l10n.t('Error while creating virtual environment.');

0 commit comments

Comments
 (0)