Skip to content

Commit 87605f5

Browse files
author
Yalin Li
authored
Update setup to support multiple isolated virtual environments (#264)
1 parent 9b59e4e commit 87605f5

16 files changed

Lines changed: 454 additions & 105 deletions

File tree

azdev.pyproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
<Compile Include="azdev\operations\__init__.py" />
5050
<Compile Include="azdev\params.py" />
5151
<Compile Include="azdev\utilities\config.py" />
52+
<Compile Include="azdev\utilities\venv.py" />
5253
<Compile Include="azdev\utilities\const.py">
5354
<SubType>Code</SubType>
5455
</Compile>

azdev/help.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,31 @@
1515
helps['setup'] = """
1616
short-summary: Set up your environment for development of Azure CLI command modules and/or extensions.
1717
examples:
18-
- name: Fully interactive setup.
18+
- name: Fully interactive setup (Must be run in an existing virtual environment).
1919
text: azdev setup
2020
21-
- name: Install only the CLI in dev mode and search for the existing repo.
22-
text: azdev setup -c
21+
- name: Install CLI and setup an extensions repo in an existing virtual environment. Will create a azure directory and config in the current virtual environment.
22+
Note the existing virtual environment could created by VENV or PYENV.
23+
text: azdev setup -c azure-cli -r azure-cli-extensions
2324
24-
- name: Install public CLI and setup an extensions repo. Do not install any extensions.
25+
- name: Same as above, but install the `alias` extension in the existing virtual environment too.
26+
text: azdev setup -c azure-cli -r azure-cli-extensions -e alias
27+
28+
- name: Same as above, but will use the CLI repo path in local .azdev config, or the one in global .azdev config if not found the local one.
2529
text: azdev setup -r azure-cli-extensions
2630
27-
- name: Install CLI in dev mode, along with the extensions repo. Auto-find the CLI repo and install the `alias` extension in dev mode.
28-
text: azdev setup -c -r azure-cli-extensions -e alias
31+
- name: Same as above, but only install CLI without setup an extensions repo.
32+
text: azdev setup -c azure-cli
33+
34+
- name: Install CLI and setup an extensions repo in a new virtual environment. Will create a azure directory and config in the current virtual environment.
35+
Note -s is using VENV to create a new virtual environment, should un-install PYENV if you have.
36+
text: azdev setup -c azure-cli -r azure-cli-extensions -s env1
37+
38+
- name: Same as above, but do not setup new azure directory and config in this virtual environment
39+
text: azdev setup -c azure-cli -r azure-cli-extensions -s env1 -g
2940
30-
- name: Install only the CLI in dev mode and resolve dependencies from setup.py.
31-
text: azdev setup -c -d setup.py
41+
- name: Same as above, but copy over system level azure settings into new virtual environment azure settings
42+
text: azdev setup -c azure-cli -r azure-cli-extensions -s env1 --copy
3243
"""
3344

3445

azdev/operations/help/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
from azure.cli.core.extension.operations import list_available_extensions, list_extensions as list_cli_extensions # pylint: disable=import-error
2121
from azdev.utilities import (
2222
display, heading, subheading,
23-
get_cli_repo_path, get_path_table
23+
get_cli_repo_path, get_path_table,
24+
require_virtual_env
2425
)
2526

2627
from azdev.utilities.tools import require_azure_cli

azdev/operations/pypi.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616

1717
from azdev.utilities import (
1818
display, heading, subheading, cmd, py_cmd, get_path_table,
19-
pip_cmd, COMMAND_MODULE_PREFIX, require_azure_cli, find_files)
19+
pip_cmd, COMMAND_MODULE_PREFIX, require_azure_cli, require_virtual_env,
20+
find_files)
2021

2122
logger = get_logger(__name__)
2223

@@ -131,6 +132,7 @@ def verify_versions():
131132
import tempfile
132133
import shutil
133134

135+
require_virtual_env()
134136
require_azure_cli()
135137

136138
heading('Verify CLI Versions')

azdev/operations/setup.py

Lines changed: 169 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,21 @@
66

77
import os
88
from shutil import copytree, rmtree
9+
import shutil
910
import time
11+
import sys
1012

1113
from knack.log import get_logger
1214
from knack.util import CLIError
1315

1416
from azdev.operations.extensions import (
1517
list_extensions, add_extension_repo, remove_extension)
1618
from azdev.params import Flag
19+
import azdev.utilities.const as const
20+
import azdev.utilities.venv as venv
1721
from azdev.utilities import (
18-
display, heading, subheading, pip_cmd, find_file,
19-
get_azdev_config_dir, get_azdev_config, require_virtual_env, get_azure_config)
22+
display, heading, subheading, pip_cmd, find_file, get_env_path,
23+
get_azdev_config_dir, get_azdev_config, get_azure_config, shell_cmd)
2024

2125
logger = get_logger(__name__)
2226

@@ -196,8 +200,8 @@ def add_ext_repo(path):
196200
# repo directory. To use multiple extension repos or identify a repo outside the cwd, they must specify
197201
# the path.
198202
if prompt_y_n('\nDo you plan to develop CLI extensions?'):
199-
display('\nGreat! Input the paths for the extension repos you wish to develop for, one per '
200-
'line. You can add as many repos as you like. (TIP: to quickly get started, press RETURN to '
203+
display('\nGreat! Input the path for the extension repos you wish to develop for. '
204+
'(TIP: to quickly get started, press RETURN to '
201205
'use your current working directory).')
202206
first_repo = True
203207
while True:
@@ -245,14 +249,170 @@ def add_ext_repo(path):
245249
raise CLIError('Installation aborted.')
246250

247251

248-
def setup(cli_path=None, ext_repo_path=None, ext=None, deps=None):
252+
def _validate_input(cli_path, ext_repo_path, set_env, copy, use_global, ext):
253+
if copy and use_global:
254+
raise CLIError("Copy and use global are mutally exlcusive.")
255+
if cli_path == "pypi" and any([use_global, copy, set_env]):
256+
raise CLIError("pypi for cli path is mutally exlcusive with global copy and set env")
257+
if not cli_path and any([use_global, copy, set_env]):
258+
raise CLIError("if global, copy, or set env are set then both an extensions repo "
259+
" and a cli repo must be specified")
260+
if not ext_repo_path and ext:
261+
raise CLIError("Extesions provided to be installed but no extensions path was given")
249262

250-
require_virtual_env()
251263

252-
start = time.time()
264+
def _check_paths(cli_path, ext_repo_path):
265+
if not os.path.isdir(cli_path):
266+
raise CLIError("The cli path is not a valid directory, please check the path")
267+
if ext_repo_path and not os.path.isdir(ext_repo_path):
268+
raise CLIError("The cli extensions path is not a valid directory, please check the path")
269+
270+
271+
def _check_shell():
272+
if 'SHELL' in os.environ and const.IS_WINDOWS and 'bash.exe' in os.environ['SHELL']:
273+
heading("WARNING: You are running bash in Windows, the setup may not work correctly and "
274+
"command may have unexpected behavior")
275+
from knack.prompting import prompt_y_n
276+
if not prompt_y_n('Would you like to continue with the install?'):
277+
sys.exit(0)
278+
279+
280+
def _check_env(set_env):
281+
if not set_env:
282+
if not get_env_path():
283+
raise CLIError('You are not running in a virtual enviroment and have not chosen to set one up.')
284+
_check_pyenv()
285+
elif 'VIRTUAL_ENV' in os.environ:
286+
raise CLIError("You are already running in a virtual enviroment, yet you want to set a new one up")
287+
288+
289+
def _check_pyenv():
290+
if 'PYENV_VIRTUAL_ENV' in os.environ:
291+
if const.IS_WINDOWS:
292+
raise CLIError('AZDEV does not support setup in a pyenv-win virtual environment.')
293+
activate_path = os.path.join(
294+
os.environ['PYENV_ROOT'], 'plugins', 'pyenv-virtualenv', 'bin', 'pyenv-sh-activate')
295+
venv.edit_pyenv_activate(activate_path)
296+
297+
298+
def setup(cli_path=None, ext_repo_path=None, ext=None, deps=None, set_env=None, copy=None, use_global=None):
299+
_check_env(set_env)
300+
301+
_check_shell()
253302

254303
heading('Azure CLI Dev Setup')
255304

305+
# cases for handling legacy install
306+
if not any([cli_path, ext_repo_path]) or cli_path == "pypi":
307+
display("WARNING: Installing azdev in legacy mode. Run with atleast -c "
308+
"to install the latest azdev wihout \"pypi\"\n")
309+
return _handle_legacy(cli_path, ext_repo_path, ext, deps, time.time())
310+
if 'CONDA_PREFIX' in os.environ:
311+
raise CLIError('CONDA virutal enviroments are not supported outside'
312+
' of interactive mode or when -c and -r are provided')
313+
314+
if not cli_path:
315+
cli_path = _handle_no_cli_path()
316+
317+
_validate_input(cli_path, ext_repo_path, set_env, copy, use_global, ext)
318+
_check_paths(cli_path, ext_repo_path)
319+
320+
if set_env:
321+
shell_cmd((const.VENV_CMD if const.IS_WINDOWS else const.VENV_CMD3) + set_env, raise_ex=False)
322+
azure_path = os.path.join(os.path.abspath(os.getcwd()), set_env)
323+
else:
324+
azure_path = os.environ.get('VIRTUAL_ENV')
325+
326+
dot_azure_config = os.path.join(azure_path, '.azure')
327+
dot_azdev_config = os.path.join(azure_path, '.azdev')
328+
329+
# clean up venv dirs if they already existed
330+
# and this is a reinstall/new setup
331+
if os.path.isdir(dot_azure_config):
332+
shutil.rmtree(dot_azure_config)
333+
if os.path.isdir(dot_azdev_config):
334+
shutil.rmtree(dot_azdev_config)
335+
336+
global_az_config = os.path.expanduser(os.path.join('~', '.azure'))
337+
global_azdev_config = os.path.expanduser(os.path.join('~', '.azdev'))
338+
azure_config_path = os.path.join(dot_azure_config, const.CONFIG_NAME)
339+
azdev_config_path = os.path.join(dot_azdev_config, const.CONFIG_NAME)
340+
341+
if os.path.isdir(global_az_config) and copy:
342+
shutil.copytree(global_az_config, dot_azure_config)
343+
if os.path.isdir(global_azdev_config):
344+
shutil.copytree(global_azdev_config, dot_azdev_config)
345+
else:
346+
os.mkdir(dot_azdev_config)
347+
file = open(azdev_config_path, "w")
348+
file.close()
349+
elif not use_global and not copy:
350+
os.mkdir(dot_azure_config)
351+
os.mkdir(dot_azdev_config)
352+
file_az, file_dev = open(azure_config_path, "w"), open(azdev_config_path, "w")
353+
file_az.close()
354+
file_dev.close()
355+
elif os.path.isdir(global_az_config):
356+
dot_azure_config, dot_azdev_config = global_az_config, global_azdev_config
357+
azure_config_path = os.path.join(dot_azure_config, const.CONFIG_NAME)
358+
else:
359+
raise CLIError(
360+
"Global AZ config is not set up, yet it was specified to be used.")
361+
362+
# set env vars for get azure config and get azdev config
363+
os.environ['AZURE_CONFIG_DIR'], os.environ['AZDEV_CONFIG_DIR'] = dot_azure_config, dot_azdev_config
364+
config = get_azure_config()
365+
if not config.get('cloud', 'name', None):
366+
config.set_value('cloud', 'name', 'AzureCloud')
367+
if ext_repo_path:
368+
config.set_value(const.EXT_SECTION, const.AZ_DEV_SRC, os.path.abspath(ext_repo_path))
369+
venv.edit_activate(azure_path, dot_azure_config, dot_azdev_config)
370+
if cli_path:
371+
config.set_value('clipath', const.AZ_DEV_SRC, os.path.abspath(cli_path))
372+
venv.install_cli(os.path.abspath(cli_path), azure_path)
373+
config = get_azdev_config()
374+
config.set_value('ext', 'repo_paths', os.path.abspath(ext_repo_path) if ext_repo_path else '_NONE_')
375+
config.set_value('cli', 'repo_path', os.path.abspath(cli_path))
376+
_copy_config_files()
377+
if ext and ext_repo_path:
378+
venv.install_extensions(azure_path, ext)
379+
380+
if not set_env:
381+
heading("The setup was successful! Please run or re-run the virtual environment activation script.")
382+
else:
383+
heading("The setup was successful!")
384+
return None
385+
386+
387+
def _get_azdev_cli_path(config_file_path):
388+
if not os.path.exists(config_file_path):
389+
return None
390+
391+
import configparser
392+
with open(config_file_path, "r") as file:
393+
config_parser = configparser.RawConfigParser()
394+
config_parser.read_string(file.read())
395+
if config_parser.has_section('cli') and config_parser.has_option('cli', 'repo_path'):
396+
return config_parser.get('cli', 'repo_path')
397+
return None
398+
399+
400+
def _handle_no_cli_path():
401+
local_azdev_config = os.path.join(os.environ.get('VIRTUAL_ENV'), '.azdev', const.CONFIG_NAME)
402+
cli_path = _get_azdev_cli_path(local_azdev_config)
403+
if cli_path is None:
404+
display('Not found cli path in local azdev config file: ' + local_azdev_config)
405+
display('Will use the one in global azdev config.')
406+
global_azdev_config = os.path.expanduser(os.path.join('~', '.azdev', const.CONFIG_NAME))
407+
cli_path = _get_azdev_cli_path(global_azdev_config)
408+
if cli_path is None:
409+
raise CLIError('Not found cli path in global azdev config file: ' + global_azdev_config)
410+
display('cli_path: ' + cli_path)
411+
return cli_path
412+
413+
414+
def _handle_legacy(cli_path, ext_repo_path, ext, deps, start):
415+
ext_repo_path = [ext_repo_path] if ext_repo_path else None
256416
ext_to_install = []
257417
if not any([cli_path, ext_repo_path, ext]):
258418
cli_path, ext_repo_path, ext_to_install = _interactive_setup()
@@ -279,7 +439,6 @@ def setup(cli_path=None, ext_repo_path=None, ext=None, deps=None):
279439
# must add the necessary repo to add an extension
280440
if ext and not ext_repo_path:
281441
raise CLIError('usage error: --repo EXT_REPO [EXT_REPO ...] [--ext EXT_NAME ...]')
282-
283442
get_azure_config().set_value('extension', 'dev_sources', '')
284443
if ext_repo_path:
285444
# add extension repo(s)
@@ -313,11 +472,10 @@ def setup(cli_path=None, ext_repo_path=None, ext=None, deps=None):
313472

314473
# upgrade to latest pip
315474
pip_cmd('install --upgrade pip -q', 'Upgrading pip...')
316-
317475
_install_cli(cli_path, deps=deps)
318-
_install_extensions(ext_to_install)
476+
if ext_repo_path:
477+
_install_extensions(ext_to_install)
319478
_copy_config_files()
320-
321479
end = time.time()
322480
elapsed_min = int((end - start) / 60)
323481
elapsed_sec = int(end - start) % 60

azdev/params.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,13 @@ def load_arguments(self, _):
3434
c.argument('git_repo', options_list='--repo', arg_group='Git', help='Path to the Git repo to check.')
3535

3636
with ArgumentsContext(self, 'setup') as c:
37-
c.argument('cli_path', options_list=['--cli', '-c'], nargs='?', const=Flag, help="Path to an existing Azure CLI repo. Omit value to search for the repo or use special value 'EDGE' to install the latest developer edge build.")
38-
c.argument('ext_repo_path', options_list=['--repo', '-r'], nargs='+', help='Space-separated list of paths to existing Azure CLI extensions repos.')
37+
c.argument('cli_path', options_list=['--cli', '-c'], type=str, help="Path to an existing Azure CLI repo. Use special value 'EDGE' to install the latest developer edge build. Note: if not provide, will use the one in local .azdev config, if not exist will use the one in global .azdev config.")
38+
c.argument('ext_repo_path', options_list=['--repo', '-r'], type=str, help='Path to existing Azure CLI extensions repos.')
3939
c.argument('ext', options_list=['--ext', '-e'], nargs='+', help="Space-separated list of extensions to install initially. Use '*' to install all extensions.")
4040
c.argument('deps', options_list=['--deps-from', '-d'], choices=['requirements.txt', 'setup.py'], default='requirements.txt', help="Choose the file to resolve dependencies.")
41+
c.argument('set_env', options_list=['--set-env', '-s'], type=str, help="Will create a virtual enviroment with the given env name")
42+
c.argument('copy', options_list='--copy', action='store_true', help="Will copy entire global .azure diretory to the newly created virtual enviroment .azure direcotry if it exist")
43+
c.argument('use_global', options_list=['--use-global', '-g'], action='store_true', help="Will use the default global system .azure config")
4144

4245
with ArgumentsContext(self, 'test') as c:
4346
c.argument('discover', options_list='--discover', action='store_true', help='Build an index of test names so that you don\'t need to specify fully qualified test paths.')

azdev/utilities/__init__.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,16 @@
1414
call,
1515
cmd,
1616
py_cmd,
17-
pip_cmd
17+
pip_cmd,
18+
shell_cmd
1819
)
1920
from .const import (
2021
COMMAND_MODULE_PREFIX,
2122
EXTENSION_PREFIX,
2223
IS_WINDOWS,
2324
ENV_VAR_TEST_MODULES,
2425
ENV_VAR_TEST_LIVE,
25-
ENV_VAR_VIRTUAL_ENV,
26-
EXT_REPO_NAME
26+
ENV_VAR_VIRTUAL_ENV
2727
)
2828
from .display import (
2929
display,
@@ -76,7 +76,6 @@
7676
'ENV_VAR_TEST_MODULES',
7777
'ENV_VAR_TEST_LIVE',
7878
'ENV_VAR_VIRTUAL_ENV',
79-
'EXT_REPO_NAME',
8079
'IS_WINDOWS',
8180
'extract_module_name',
8281
'find_file',

0 commit comments

Comments
 (0)