azdev/operations/setup.py (321 lines of code) (raw):
# -----------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for
# license information.
# -----------------------------------------------------------------------------
import os
import subprocess
from shutil import copytree, rmtree
import time
from knack.log import get_logger
from knack.util import CLIError
from azdev.operations.extensions import (
list_extensions, add_extension_repo, remove_extension)
from azdev.params import Flag
from azdev.utilities import (
display, heading, subheading, pip_cmd, CommandError, find_file,
get_azdev_config_dir, get_azdev_config, require_virtual_env, get_azure_config)
logger = get_logger(__name__)
def _check_path(path, file_name):
""" Ensures the file_name is provided in the supplied path. """
path = os.path.abspath(path)
if not os.path.exists(path):
raise CLIError('{} is not a valid path.'.format(path))
_check_repo(path)
if file_name not in os.listdir(path):
raise CLIError("'{}' does not contain the expected file '{}'".format(path, file_name))
return path
def _check_repo(path):
if not os.path.isdir(os.path.join(path, '.git')):
raise CLIError("'{}' is not a valid git repository.".format(path))
def _install_extensions(ext_paths):
# clear pre-existing dev extensions
try:
installed_extensions = [x['name'] for x in list_extensions() if x['install'] == 'Y']
remove_extension(installed_extensions)
except KeyError as ex:
logger.warning('Error occurred determining installed extensions. Run with --debug for more info.')
logger.debug(ex)
# install specified extensions
for path in ext_paths or []:
result = pip_cmd('install -e {}'.format(path), "Adding extension '{}'...".format(path))
if result.error:
raise result.error # pylint: disable=raising-bad-type
def _install_cli(cli_path, deps=None):
if not cli_path:
# install public CLI off PyPI if no repo found
pip_cmd('install --upgrade azure-cli', "Installing `azure-cli`...")
pip_cmd('install git+https://github.com/Azure/azure-cli@main#subdirectory=src/azure-cli-testsdk',
"Installing `azure-cli-testsdk`...")
return
if cli_path == 'EDGE':
# install the public edge build
pip_cmd('install --pre azure-cli --extra-index-url https://azurecliprod.blob.core.windows.net/edge',
"Installing `azure-cli` edge build...")
pip_cmd('install git+https://github.com/Azure/azure-cli@main#subdirectory=src/azure-cli-testsdk',
"Installing `azure-cli-testsdk`...")
return
# otherwise editable install from source
# install private whls if there are any
privates_dir = os.path.join(cli_path, "privates")
if os.path.isdir(privates_dir) and os.listdir(privates_dir):
whl_list = " ".join(
[os.path.join(privates_dir, f) for f in os.listdir(privates_dir)]
)
pip_cmd("install {}".format(whl_list), "Installing private whl files...")
# install general requirements
pip_cmd(
"install -r {}".format(os.path.join(cli_path, "requirements.txt")),
"Installing `requirements.txt`..."
)
cli_src = os.path.join(cli_path, 'src')
if deps == 'setup.py':
# Resolve dependencies from setup.py files.
# command modules have dependency on azure-cli-core so install this first
pip_cmd(
"install -e {}".format(os.path.join(cli_src, 'azure-cli-telemetry')),
"Installing `azure-cli-telemetry`..."
)
pip_cmd(
"install -e {}".format(os.path.join(cli_src, 'azure-cli-core')),
"Installing `azure-cli-core`..."
)
# azure cli has dependencies on the above packages so install this one last
pip_cmd(
"install -e {}".format(os.path.join(cli_src, 'azure-cli')),
"Installing `azure-cli`..."
)
pip_cmd(
"install -e {}".format(os.path.join(cli_src, 'azure-cli-testsdk')),
"Installing `azure-cli-testsdk`..."
)
else:
# First install packages without dependencies,
# then resolve dependencies from requirements.*.txt file.
pip_cmd(
"install -e {} --no-deps".format(os.path.join(cli_src, 'azure-cli-telemetry')),
"Installing `azure-cli-telemetry`..."
)
pip_cmd(
"install -e {} --no-deps".format(os.path.join(cli_src, 'azure-cli-core')),
"Installing `azure-cli-core`..."
)
pip_cmd(
"install -e {} --no-deps".format(os.path.join(cli_src, 'azure-cli')),
"Installing `azure-cli`..."
)
# The dependencies of testsdk are not in requirements.txt as this package is not needed by the
# azure-cli package for running commands.
# Here we need to install with dependencies for azdev test.
pip_cmd(
"install -e {}".format(os.path.join(cli_src, 'azure-cli-testsdk')),
"Installing `azure-cli-testsdk`..."
)
import platform
system = platform.system()
req_file = 'requirements.py3.{}.txt'.format(system)
pip_cmd(
"install -r {}".format(os.path.join(cli_src, 'azure-cli', req_file)),
"Installing `{}`...".format(req_file)
)
def _copy_config_files():
from glob import glob
from importlib import import_module
config_mod = import_module('azdev.config')
config_dir_path = config_mod.__dict__['__path__'][0]
dest_path = os.path.join(get_azdev_config_dir(), 'config_files')
if os.path.exists(dest_path):
rmtree(dest_path)
copytree(config_dir_path, dest_path)
# remove the python __init__ files
pattern = os.path.join(dest_path, '*.py*')
for path in glob(pattern):
os.remove(path)
# pylint: disable=too-many-statements
def _interactive_setup():
from knack.prompting import prompt_y_n, prompt
while True:
cli_path = None
ext_repos = []
exts = []
# CLI Installation
if prompt_y_n('Do you plan to develop CLI modules?'):
display("\nGreat! Please enter the path to your azure-cli repo, 'EDGE' to install "
"the latest developer edge build or simply press "
"RETURN and we will attempt to find your repo for you.")
while True:
cli_path = prompt('\nPath (RETURN to auto-find): ', None)
cli_path = os.path.abspath(os.path.expanduser(cli_path)) if cli_path else None
CLI_SENTINEL = 'azure-cli.pyproj'
if not cli_path:
cli_path = find_file(CLI_SENTINEL)
if not cli_path:
raise CLIError('Unable to locate your CLI repo. Things to check:'
'\n Ensure you have cloned the repo. '
'\n Specify the path explicitly with `-c PATH`. '
'\n If you run with `-c` to autodetect, ensure you are running '
'this command from a folder upstream of the repo.')
try:
if cli_path != 'EDGE':
cli_path = _check_path(cli_path, CLI_SENTINEL)
display('Found: {}'.format(cli_path))
break
except CLIError as ex:
logger.error(ex)
continue
else:
display('\nOK. We will install the latest `azure-cli` from PyPI then.')
def add_ext_repo(path):
try:
_check_repo(path)
except CLIError as ex:
logger.error(ex)
return False
ext_repos.append(path)
display('Repo {} OK.'.format(path))
return True
# Determine extension repos
# Allows the user to simply press RETURN to use their cwd, assuming they are in their desired extension
# repo directory. To use multiple extension repos or identify a repo outside the cwd, they must specify
# the path.
if prompt_y_n('\nDo you plan to develop CLI extensions?'):
display('\nGreat! Input the paths for the extension repos you wish to develop for, one per '
'line. You can add as many repos as you like. (TIP: to quickly get started, press RETURN to '
'use your current working directory).')
first_repo = True
while True:
msg = '\nPath ({}): '.format('RETURN to use current directory' if first_repo else 'RETURN to continue')
ext_repo_path = prompt(msg, None)
if not ext_repo_path:
if first_repo and not add_ext_repo(os.getcwd()):
first_repo = False
continue
break
add_ext_repo(os.path.abspath(os.path.expanduser(ext_repo_path)))
first_repo = False
display('\nTIP: you can manage extension repos later with the `azdev extension repo` commands.')
# Determine extensions
if ext_repos:
if prompt_y_n('\nWould you like to install certain extensions by default? '):
display('\nGreat! Input the names of the extensions you wish to install, one per '
'line. You can add as many repos as you like. Use * to install all extensions. '
'Press RETURN to continue to the next step.')
available_extensions = [x['name'] for x in list_extensions()]
while True:
ext_name = prompt('\nName (RETURN to continue): ', None)
if not ext_name:
break
if ext_name == '*':
exts = [x['path'] for x in list_extensions()]
break
if ext_name not in available_extensions:
logger.error("Extension '%s' not found. Check the spelling, and make "
"sure you added the repo first!", ext_name)
continue
display('Extension {} OK.'.format(ext_name))
exts.append(next(x['path'] for x in list_extensions() if x['name'] == ext_name))
display('\nTIP: you can manage extensions later with the `azdev extension` commands.')
subheading('Summary')
display('CLI: {}'.format(cli_path if cli_path else 'PyPI'))
display('Extension repos: {}'.format(' '.join(ext_repos)))
display('Extensions: \n {}'.format('\n '.join(exts)))
if prompt_y_n('\nProceed with installation? '):
return cli_path, ext_repos, exts
raise CLIError('Installation aborted.')
def _setup_azure_cli_repo(cli_path):
if cli_path and cli_path != 'EDGE':
# Store original directory
original_dir = os.getcwd()
try:
# Change to CLI repo root directory
os.chdir(cli_path)
display(f"\nSetting up Azure CLI repo: {cli_path}\n")
# Change git hooks path
_change_git_hooks_path(cli_path)
# Check existing remotes
remotes = subprocess.check_output(['git', 'remote', '-v'], text=True)
# If upstream already exists, nothing to do
if 'upstream' in remotes:
return
# Check origin remote URL
origin_url = None
for line in remotes.splitlines():
if line.startswith('origin') and '(fetch)' in line:
origin_url = line.split()[1]
break
# Only add upstream if origin is an azure-cli fork
if origin_url and origin_url.endswith('/azure-cli.git'):
upstream_url = 'https://github.com/Azure/azure-cli.git'
subprocess.check_call(['git', 'remote', 'add', 'upstream', upstream_url])
display(f"Added upstream remote: {upstream_url}")
# fetch the upstream/dev branch
subprocess.check_call(['git', 'fetch', 'upstream', 'dev'])
display(f"Fetched upstream/dev branch for CLI in {cli_path}")
except subprocess.CalledProcessError as e:
logger.warning("Failed to add upstream remote: %s", str(e))
finally:
# Always return to original directory
os.chdir(original_dir)
def _setup_azure_cli_extension_repo(ext_repo_path):
if not ext_repo_path:
return
try:
# Handle both single path and list of paths
repo_paths = ext_repo_path if isinstance(ext_repo_path, list) else [ext_repo_path]
# Iterate over all repository paths
for repo_path in repo_paths:
_setup_single_extension_repo(repo_path)
except subprocess.CalledProcessError as e:
logger.warning("Failed to add upstream remote for extensions: %s", str(e))
def _setup_single_extension_repo(repo_path):
# Store original directory
original_dir = os.getcwd()
try:
# Change to extension repo root directory
os.chdir(repo_path)
display(f"\nSetting up Azure CLI extension repo: {repo_path}\n")
# Change git hooks path
_change_git_hooks_path(repo_path)
# Check existing remotes
remotes = subprocess.check_output(['git', 'remote', '-v'], text=True)
# If upstream already exists, return
if 'upstream' in remotes:
return
# Check origin remote URL
origin_url = None
for line in remotes.splitlines():
if line.startswith('origin') and '(fetch)' in line:
origin_url = line.split()[1]
break
# Only add upstream if origin is an azure-cli-extensions fork
if origin_url and origin_url.endswith('/azure-cli-extensions.git'):
upstream_url = 'https://github.com/Azure/azure-cli-extensions.git'
subprocess.check_call(['git', 'remote', 'add', 'upstream', upstream_url])
display(f"Added upstream remote for extensions in {repo_path}: {upstream_url}")
# fetch the upstream/main branch
subprocess.check_call(['git', 'fetch', 'upstream', 'main'])
display(f"Fetched upstream/main branch for extensions in {repo_path}")
finally:
# Always return to original directory
os.chdir(original_dir)
def _change_git_hooks_path(repo_path):
# if .githooks folder exists in the repo folder, change the git config to use the .githooks folder in the repo
githooks_path = os.path.join(repo_path, '.githooks')
if os.path.exists(githooks_path):
subprocess.check_call(['git', 'config', 'core.hooksPath', githooks_path])
display(f"Changed git hooks path to {githooks_path}")
def setup(cli_path=None, ext_repo_path=None, ext=None, deps=None):
require_virtual_env()
start = time.time()
heading('Azure CLI Dev Setup')
ext_to_install = []
if not any([cli_path, ext_repo_path, ext]):
cli_path, ext_repo_path, ext_to_install = _interactive_setup()
else:
if cli_path == "pypi":
cli_path = None
# otherwise assume programmatic setup
if cli_path:
CLI_SENTINEL = 'azure-cli.pyproj'
if cli_path == Flag:
cli_path = find_file(CLI_SENTINEL)
if not cli_path:
raise CLIError('Unable to locate your CLI repo. Things to check:'
'\n Ensure you have cloned the repo. '
'\n Specify the path explicitly with `-c PATH`. '
'\n If you run with `-c` to autodetect, ensure you are running '
'this command from a folder upstream of the repo.')
if cli_path != 'EDGE':
cli_path = _check_path(cli_path, CLI_SENTINEL)
display('Azure CLI:\n {}\n'.format(cli_path))
else:
display('Azure CLI:\n PyPI\n')
# must add the necessary repo to add an extension
if ext and not ext_repo_path:
raise CLIError('usage error: --repo EXT_REPO [EXT_REPO ...] [--ext EXT_NAME ...]')
get_azure_config().set_value('extension', 'dev_sources', '')
if ext_repo_path:
# add extension repo(s)
add_extension_repo(ext_repo_path)
display('Azure CLI extension repos:\n {}'.format(
'\n '.join([os.path.abspath(x) for x in ext_repo_path])))
if ext == ['*']:
ext_to_install = [x['path'] for x in list_extensions()]
elif ext:
# add extension(s)
available_extensions = [x['name'] for x in list_extensions()]
not_found = [x for x in ext if x not in available_extensions]
if not_found:
raise CLIError("The following extensions were not found. Ensure you have added "
"the repo using `--repo/-r PATH`.\n {}".format('\n '.join(not_found)))
ext_to_install = [x['path'] for x in list_extensions() if x['name'] in ext]
if ext_to_install:
display('\nAzure CLI extensions:\n {}'.format('\n '.join(ext_to_install)))
dev_sources = get_azure_config().get('extension', 'dev_sources', None)
# save data to config files
config = get_azdev_config()
config.set_value('ext', 'repo_paths', dev_sources if dev_sources else '_NONE_')
config.set_value('cli', 'repo_path', cli_path if cli_path else '_NONE_')
# Add upstreams for CLI and extensions repos if they are forks
if cli_path:
_setup_azure_cli_repo(cli_path)
if ext_repo_path:
_setup_azure_cli_extension_repo(ext_repo_path)
# install packages
subheading('Installing packages')
try:
# upgrade to latest pip
pip_cmd('install --upgrade pip', 'Upgrading pip...')
_install_cli(cli_path, deps=deps)
_install_extensions(ext_to_install)
except CommandError as err:
logger.error(err)
return
_copy_config_files()
end = time.time()
elapsed_min = int((end - start) / 60)
elapsed_sec = int(end - start) % 60
display('\nElapsed time: {} min {} sec'.format(elapsed_min, elapsed_sec))
subheading('Finished dev setup!')