ansys-saf-cli 4.1.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ansys/saf/cli/__init__.py +15 -0
- ansys/saf/cli/_cli/__init__.py +15 -0
- ansys/saf/cli/_cli/main.py +816 -0
- ansys/saf/cli/_cli/py.typed +0 -0
- ansys/saf/cli/_config/__init__.py +15 -0
- ansys/saf/cli/_config/const.py +45 -0
- ansys/saf/cli/_config/py.typed +0 -0
- ansys/saf/cli/_database/__init__.py +15 -0
- ansys/saf/cli/_database/manager.py +94 -0
- ansys/saf/cli/_database/models.py +53 -0
- ansys/saf/cli/_database/py.typed +0 -0
- ansys/saf/cli/_solutions/__init__.py +15 -0
- ansys/saf/cli/_solutions/add_step.py +256 -0
- ansys/saf/cli/_solutions/dependencies.py +162 -0
- ansys/saf/cli/_solutions/environment.py +1145 -0
- ansys/saf/cli/_solutions/plugins.py +254 -0
- ansys/saf/cli/_solutions/py.typed +0 -0
- ansys/saf/cli/_solutions/scaffolding.py +73 -0
- ansys/saf/cli/_solutions/templates/__init__.py +15 -0
- ansys/saf/cli/_solutions/templates/py.typed +0 -0
- ansys/saf/cli/_solutions/templates/solution/cookiecutter.json +18 -0
- ansys/saf/cli/_solutions/templates/solution/hooks/post_gen_project.py +54 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.codespell.ignore +2 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.devcontainer/devcontainer.json +26 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.env +5 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.flake8 +8 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.github/labeler.yml +9 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.github/labels.yml +27 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.github/workflows/build-release.yml +175 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.github/workflows/label.yml +89 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.gitignore +137 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.pre-commit-config.yaml +48 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.release-please-manifest.json +3 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.reuse/templates/copyright.jinja2 +1 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.vscode/extensions.json +8 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/.vscode/launch.json +28 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/AUTHORS +12 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/CHANGELOG.md +1 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/CODEOWNERS +3 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/CODE_OF_CONDUCT.md +65 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/CONTRIBUTING.md +2 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/CONTRIBUTORS.md +9 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/LICENSE.rst +41 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/README.md +69 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/deployments/Dockerfile +43 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/deployments/README.md +13 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/deployments/distributed-deployment-template/.env +27 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/deployments/distributed-deployment-template/compose.yaml +132 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/deployments/standalone/.env +20 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/deployments/standalone/compose.yaml +118 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/deployments/standalone-with-hps/.env +28 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/deployments/standalone-with-hps/compose.yaml +146 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/deployments/standalone-with-minerva/.env +25 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/deployments/standalone-with-minerva/compose.yaml +122 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/.vale.ini +28 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/Makefile +30 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/changelog.d/changelog_template.jinja +17 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/make.bat +46 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/source/_static/css/custom.css +8 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/source/_static/images/repository-banner.png +0 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/source/_templates/README.md +1 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/source/changelog.rst +12 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/source/conf.py +253 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/source/examples.rst +8 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/source/getting_started/desktop_installation.rst +165 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/source/getting_started/docker_installation.rst +68 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/source/index.rst +51 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/source/user_guide/index.rst +6 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/styles/config/vocabularies/ANSYS/accept.txt +5 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/doc/styles/config/vocabularies/ANSYS/reject.txt +0 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/examples/README.md +1 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/lock_files/dash/poetry.lock +7621 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/lock_files/no_ui/poetry.lock +7173 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/minerva/cli/README.md +3 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/minerva/oauth/App_Data/Certificates/README.md +3 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/minerva/oauth/oauth.config +9 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/pyproject.toml +192 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/release-please-config.json +9 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/sonar-project.properties +12 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/__init__.py +3 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/main.py +19 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/portal_assets/application.svg +36 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/portal_assets/description.json +3 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/portal_assets/project.svg +1 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/solution/definition.py +24 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/solution/first_step.py +34 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/solution/method_assets/README.md +1 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/solution/scripts/README.md +1 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/solution/scripts/assets/README.md +1 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/solution/second_step.py +12 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/app.py +30 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/css/all.css +9 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/css/bootstrap.min.css +6 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/css/style.css +47 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/icons/dark/carbon--ibm-engineering-workflow-mgmt.svg +1 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/icons/dark/carbon--return.svg +1 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/icons/dark/game-icons--crossed-air-flows.svg +1 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/icons/dark/material-symbols--home.svg +1 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/icons/dark/radix-icons--moon.svg +1 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/icons/dark/radix-icons--sun.svg +1 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/icons/dark/streamline--startup-solid.svg +1 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/icons/dark/teenyicons--doc-solid.svg +1 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/icons/light/carbon--ibm-engineering-workflow-mgmt.svg +1 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/icons/light/carbon--return.svg +1 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/icons/light/game-icons--crossed-air-flows.svg +1 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/icons/light/material-symbols--home.svg +1 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/icons/light/radix-icons--moon.svg +1 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/icons/light/radix-icons--sun.svg +1 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/icons/light/streamline--startup-solid.svg +1 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/icons/light/teenyicons--doc-solid.svg +1 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/images/README.md +1 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/images/workflow-placeholder.png +0 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/installer/README.md +5 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/logos/dark/placeholder_logo.png +0 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/logos/light/placeholder_logo.png +0 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/orchestrator/README.md +3 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/pywebview/README.md +3 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/assets/scripts/README.md +1 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/components/README.md +1 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/pages/about_page.py +71 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/pages/first_page.py +147 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/pages/page.py +367 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/src/{{ cookiecutter.__solution_namespace_path }}/{{cookiecutter.__solution_module_name}}/ui/pages/second_page.py +32 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/tests/common_test_files/README.md +1 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/tests/conftest.py +37 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/tests/unit/test_solution_api.py +42 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/tests/unit/test_solution_ui.py +30 -0
- ansys/saf/cli/_solutions/templates/solution/{{cookiecutter.__solution_name}}/tox.ini +56 -0
- ansys/saf/cli/_utilities/__init__.py +15 -0
- ansys/saf/cli/_utilities/archiver.py +67 -0
- ansys/saf/cli/_utilities/backup.py +181 -0
- ansys/saf/cli/_utilities/conversion.py +64 -0
- ansys/saf/cli/_utilities/platform.py +26 -0
- ansys/saf/cli/_utilities/py.typed +0 -0
- ansys/saf/cli/_utilities/solution_modules.py +173 -0
- ansys/saf/cli/integrations/solutions_manager.py +64 -0
- ansys/saf/cli/py.typed +0 -0
- ansys_saf_cli-4.1.dev0.dist-info/METADATA +201 -0
- ansys_saf_cli-4.1.dev0.dist-info/RECORD +143 -0
- ansys_saf_cli-4.1.dev0.dist-info/WHEEL +4 -0
- ansys_saf_cli-4.1.dev0.dist-info/entry_points.txt +3 -0
- ansys_saf_cli-4.1.dev0.dist-info/licenses/AUTHORS +12 -0
- ansys_saf_cli-4.1.dev0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,1145 @@
|
|
|
1
|
+
# Copyright (C) 2026 ANSYS, Inc. and/or its affiliates.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
#
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
from contextlib import contextmanager
|
|
18
|
+
import os
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
import platform
|
|
21
|
+
import re
|
|
22
|
+
import shutil
|
|
23
|
+
import subprocess
|
|
24
|
+
import sys
|
|
25
|
+
import time
|
|
26
|
+
from typing import Any
|
|
27
|
+
from urllib.parse import urlparse
|
|
28
|
+
|
|
29
|
+
import click
|
|
30
|
+
from dotenv import load_dotenv
|
|
31
|
+
from packaging.markers import Marker
|
|
32
|
+
import requests
|
|
33
|
+
from requests.auth import HTTPBasicAuth
|
|
34
|
+
import tomlkit
|
|
35
|
+
|
|
36
|
+
from ansys.saf.cli._config.const import (
|
|
37
|
+
WORKSPACE_CLEAR_OPTIONS,
|
|
38
|
+
)
|
|
39
|
+
from ansys.saf.cli._utilities.solution_modules import get_exec_from_solution_venv
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class InstallError(Exception):
|
|
43
|
+
"""Custom exception for install errors.
|
|
44
|
+
Errors of this type are not internal issues with
|
|
45
|
+
the saf install command but are instead issues
|
|
46
|
+
relating to the user's environment or input.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# Path definition ---------------------------------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# leave as relative paths, they are resolved after setup_environment changes the working dir to the solution_root_dir
|
|
54
|
+
PYPROJECT_FILE = Path("pyproject.toml")
|
|
55
|
+
VENV_DIR = Path(".venv")
|
|
56
|
+
POETRY_LOCK_FILE = Path("poetry.lock")
|
|
57
|
+
POETRY_VENV_DIR = Path(".poetry") / ".venv"
|
|
58
|
+
POETRY_CACHE_DIR = Path(".poetry") / ".cache"
|
|
59
|
+
POETRY_PYTHON_EXEC = (
|
|
60
|
+
POETRY_VENV_DIR / "Scripts" / "python.exe" if platform.system() == "Windows" else POETRY_VENV_DIR / "bin" / "python"
|
|
61
|
+
)
|
|
62
|
+
POETRY_POETRY_EXEC = (
|
|
63
|
+
POETRY_VENV_DIR / "Scripts" / "poetry.exe" if platform.system() == "Windows" else POETRY_VENV_DIR / "bin" / "poetry"
|
|
64
|
+
)
|
|
65
|
+
PYTHON_EXEC = VENV_DIR / "Scripts" / "python.exe" if platform.system() == "Windows" else VENV_DIR / "bin" / "python"
|
|
66
|
+
POETRY_EXEC = VENV_DIR / "Scripts" / "poetry.exe" if platform.system() == "Windows" else VENV_DIR / "bin" / "poetry"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# Console prints ----------------------------------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _print_main_header(text: str, max_length: int = 100) -> None:
|
|
73
|
+
"""Display main header."""
|
|
74
|
+
for _ in range(max_length):
|
|
75
|
+
print("=", end="")
|
|
76
|
+
print()
|
|
77
|
+
print(text)
|
|
78
|
+
for _ in range(max_length):
|
|
79
|
+
print("=", end="")
|
|
80
|
+
print()
|
|
81
|
+
print()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _print_section_header(text: str, max_length: int = 100) -> None:
|
|
85
|
+
"""Display a section header in the console."""
|
|
86
|
+
section_header = ""
|
|
87
|
+
if len(text) < max_length:
|
|
88
|
+
section_header = text + " "
|
|
89
|
+
for _ in range(len(text), max_length):
|
|
90
|
+
section_header += "-"
|
|
91
|
+
else:
|
|
92
|
+
section_header = text
|
|
93
|
+
print(section_header)
|
|
94
|
+
print()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _print_input_value(input_str: str, value: str, separator: str = ":", separator_position: int = 60) -> None:
|
|
98
|
+
"""Print input value in console."""
|
|
99
|
+
if len(input_str) < separator_position:
|
|
100
|
+
text = input_str
|
|
101
|
+
for _ in range(len(text), separator_position):
|
|
102
|
+
text += " "
|
|
103
|
+
text += separator + " " + value
|
|
104
|
+
else:
|
|
105
|
+
text = input_str + " " + separator + " " + value
|
|
106
|
+
print(text)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _print_inputs_summary(
|
|
110
|
+
dependency_groups: list[str],
|
|
111
|
+
build_system_version: str,
|
|
112
|
+
optional_dependency_groups: list[str],
|
|
113
|
+
) -> None:
|
|
114
|
+
"""Display a summary of the inputs."""
|
|
115
|
+
print(f"OS : {platform.system()}")
|
|
116
|
+
print(f"Python version : {_get_python_version()}")
|
|
117
|
+
print("Virtual environment name : .venv")
|
|
118
|
+
for dependency_group in optional_dependency_groups:
|
|
119
|
+
_print_input_value(
|
|
120
|
+
f"{dependency_group} dependencies",
|
|
121
|
+
"yes" if dependency_group in dependency_groups else "no",
|
|
122
|
+
separator=":",
|
|
123
|
+
separator_position=37,
|
|
124
|
+
)
|
|
125
|
+
print("Dependency management system : poetry")
|
|
126
|
+
print(f"Dependency management system version : {build_system_version}")
|
|
127
|
+
print()
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# Checks ------------------------------------------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _check_python_version(configuration: dict[str, Any]) -> None: # noqa: C901
|
|
134
|
+
"""Check if current Python is consistent with the python specifications from the build system."""
|
|
135
|
+
# Initialize lower/upper versions
|
|
136
|
+
lower_version, upper_version, lower_bound_symbol, upper_bound_symbol = None, None, None, None
|
|
137
|
+
lower_specification, upper_specification, single_specification = None, None, None
|
|
138
|
+
# Read Python version constraint
|
|
139
|
+
if (
|
|
140
|
+
"tool" not in configuration
|
|
141
|
+
or "poetry" not in configuration["tool"]
|
|
142
|
+
or "dependencies" not in configuration["tool"]["poetry"]
|
|
143
|
+
or "python" not in configuration["tool"]["poetry"]["dependencies"]
|
|
144
|
+
):
|
|
145
|
+
raise RuntimeError("Python dependency not specified in pyproject.")
|
|
146
|
+
python_compatibility = configuration["tool"]["poetry"]["dependencies"]["python"].replace(" ", "")
|
|
147
|
+
# Split lower/upper version constraint
|
|
148
|
+
if "," in python_compatibility:
|
|
149
|
+
lower_specification, upper_specification = (
|
|
150
|
+
python_compatibility.split(",")[0],
|
|
151
|
+
python_compatibility.split(",")[1],
|
|
152
|
+
)
|
|
153
|
+
elif python_compatibility.startswith(">"):
|
|
154
|
+
lower_specification, upper_specification = python_compatibility, None
|
|
155
|
+
elif python_compatibility.startswith("<"):
|
|
156
|
+
lower_specification, upper_specification = None, python_compatibility
|
|
157
|
+
else:
|
|
158
|
+
single_specification = python_compatibility
|
|
159
|
+
|
|
160
|
+
if single_specification:
|
|
161
|
+
version = _get_version_from_python_specification(single_specification)
|
|
162
|
+
sign = _get_sign_from_python_specification(single_specification)
|
|
163
|
+
if sign != "==":
|
|
164
|
+
raise Exception("Unable to interpret python version specification.")
|
|
165
|
+
marker = Marker(f"python_full_version {sign} '{version}'")
|
|
166
|
+
if not marker.evaluate():
|
|
167
|
+
raise Exception(f"Python version must be equal to {version}.")
|
|
168
|
+
return
|
|
169
|
+
error_message = ""
|
|
170
|
+
lower_marker: Marker | None = None
|
|
171
|
+
upper_marker: Marker | None = None
|
|
172
|
+
if lower_specification:
|
|
173
|
+
lower_version = _get_version_from_python_specification(lower_specification)
|
|
174
|
+
lower_bound_symbol = _get_sign_from_python_specification(lower_specification)
|
|
175
|
+
lower_marker = Marker(f"python_full_version {lower_bound_symbol} '{lower_version}'")
|
|
176
|
+
error_message += (
|
|
177
|
+
f"Python version must be {lower_bound_symbol} {lower_version}. " if not lower_marker.evaluate() else ""
|
|
178
|
+
)
|
|
179
|
+
if upper_specification:
|
|
180
|
+
upper_version = _get_version_from_python_specification(upper_specification)
|
|
181
|
+
upper_bound_symbol = _get_sign_from_python_specification(upper_specification)
|
|
182
|
+
upper_marker = Marker(f"python_full_version {upper_bound_symbol} '{upper_version}'")
|
|
183
|
+
error_message += (
|
|
184
|
+
f"Python version must be {upper_bound_symbol} {upper_version}." if not upper_marker.evaluate() else ""
|
|
185
|
+
)
|
|
186
|
+
if error_message:
|
|
187
|
+
raise Exception(error_message)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _check_virtual_environment_exists() -> bool:
|
|
191
|
+
"""Check if an install exists already."""
|
|
192
|
+
return VENV_DIR.is_dir()
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def set_user_level_environment_variable(variable_name: str, value: str) -> None:
|
|
196
|
+
"""Set environment variable at the user level for persistence."""
|
|
197
|
+
os.environ[variable_name] = value
|
|
198
|
+
|
|
199
|
+
variable_name_formatted = click.style(variable_name, fg="cyan", bold=True)
|
|
200
|
+
|
|
201
|
+
if platform.system() == "Windows":
|
|
202
|
+
try:
|
|
203
|
+
subprocess.run(
|
|
204
|
+
["setx", variable_name, value],
|
|
205
|
+
check=True,
|
|
206
|
+
capture_output=True,
|
|
207
|
+
text=True,
|
|
208
|
+
)
|
|
209
|
+
click.echo(
|
|
210
|
+
f"Environment variable {variable_name_formatted} set permanently for the current Windows user",
|
|
211
|
+
)
|
|
212
|
+
except subprocess.CalledProcessError:
|
|
213
|
+
click.echo(f"Warning: Could not set permanent environment variable {variable_name_formatted}")
|
|
214
|
+
else:
|
|
215
|
+
home_dir = Path.home()
|
|
216
|
+
profile_files = [
|
|
217
|
+
home_dir / ".bash_profile",
|
|
218
|
+
home_dir / ".bashrc",
|
|
219
|
+
home_dir / ".profile",
|
|
220
|
+
]
|
|
221
|
+
|
|
222
|
+
export_line = f'export {variable_name}="{value}"\n'
|
|
223
|
+
|
|
224
|
+
for profile_file in profile_files:
|
|
225
|
+
if profile_file.exists():
|
|
226
|
+
try:
|
|
227
|
+
content = profile_file.read_text()
|
|
228
|
+
if f"export {variable_name}=" not in content:
|
|
229
|
+
with profile_file.open("a") as f:
|
|
230
|
+
f.write(export_line)
|
|
231
|
+
click.echo(f"Environment variable {variable_name_formatted} added to {profile_file}")
|
|
232
|
+
break
|
|
233
|
+
except OSError:
|
|
234
|
+
continue
|
|
235
|
+
else:
|
|
236
|
+
try:
|
|
237
|
+
bashrc = home_dir / ".bashrc"
|
|
238
|
+
with bashrc.open("a") as f:
|
|
239
|
+
f.write(export_line)
|
|
240
|
+
click.echo(f"Created {bashrc} and added {variable_name_formatted}")
|
|
241
|
+
except OSError:
|
|
242
|
+
click.echo(f"Warning: Could not set permanent environment variable {variable_name_formatted}")
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _prompt_for_credentials(source: dict[str, Any], is_first_attempt: bool) -> tuple[str, str]:
|
|
246
|
+
"""Interactively prompt user for PyPI credentials."""
|
|
247
|
+
source_name = source["name"]
|
|
248
|
+
source_url = source.get("url", "")
|
|
249
|
+
|
|
250
|
+
if is_first_attempt:
|
|
251
|
+
click.echo(f"\nMissing credentials for private PyPI source: {click.style(source_name, fg='cyan', bold=True)}")
|
|
252
|
+
click.echo(f"URL: {source_url}")
|
|
253
|
+
|
|
254
|
+
click.echo()
|
|
255
|
+
|
|
256
|
+
username = click.prompt(
|
|
257
|
+
f"Enter username for {click.style(source_name, fg='cyan', bold=True)}",
|
|
258
|
+
type=str,
|
|
259
|
+
show_default=False,
|
|
260
|
+
).strip()
|
|
261
|
+
|
|
262
|
+
password = click.prompt(
|
|
263
|
+
f"Enter password/token for {click.style(source_name, fg='cyan', bold=True)}",
|
|
264
|
+
type=str,
|
|
265
|
+
hide_input=True,
|
|
266
|
+
show_default=False,
|
|
267
|
+
).strip()
|
|
268
|
+
|
|
269
|
+
return username, password
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _get_netrc_path():
|
|
273
|
+
netrc_raw_path = os.getenv("NETRC")
|
|
274
|
+
if netrc_raw_path is None:
|
|
275
|
+
netrc_file_name = "_netrc" if platform.system() == "Windows" else ".netrc"
|
|
276
|
+
netrc_path = Path.home() / netrc_file_name
|
|
277
|
+
else:
|
|
278
|
+
netrc_path = Path(netrc_raw_path)
|
|
279
|
+
return netrc_path
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _get_credentials_from_netrc(source: dict[str, Any]) -> tuple[Path, str, str, str] | None:
|
|
283
|
+
hostname = _get_parsed_url_from_source(source).hostname
|
|
284
|
+
|
|
285
|
+
if hostname is None:
|
|
286
|
+
return None
|
|
287
|
+
|
|
288
|
+
netrc_path = _get_netrc_path()
|
|
289
|
+
|
|
290
|
+
if not netrc_path.is_file():
|
|
291
|
+
return None
|
|
292
|
+
|
|
293
|
+
try:
|
|
294
|
+
with netrc_path.open("r") as netrc_file:
|
|
295
|
+
netrc_content = netrc_file.read()
|
|
296
|
+
|
|
297
|
+
machine_pattern = re.compile(
|
|
298
|
+
r"default\s+login\s+(\S+)\s+password\s+(\S+)",
|
|
299
|
+
re.IGNORECASE,
|
|
300
|
+
)
|
|
301
|
+
match = machine_pattern.search(netrc_content)
|
|
302
|
+
|
|
303
|
+
if match:
|
|
304
|
+
raise InstallError(
|
|
305
|
+
"The netrc file contains the 'default' keyword which is not supported by pypi. "
|
|
306
|
+
f"The netrc file is located at: {netrc_path}",
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
machine_pattern = re.compile(
|
|
310
|
+
rf"machine\s+{re.escape(hostname)}\s+login\s+(\S+)\s+password\s+(\S+)",
|
|
311
|
+
re.IGNORECASE,
|
|
312
|
+
)
|
|
313
|
+
match = machine_pattern.search(netrc_content)
|
|
314
|
+
|
|
315
|
+
if match:
|
|
316
|
+
netrc_username, netrc_password = match.groups()
|
|
317
|
+
return netrc_path, hostname, netrc_username, netrc_password
|
|
318
|
+
|
|
319
|
+
except InstallError:
|
|
320
|
+
raise
|
|
321
|
+
except Exception as e:
|
|
322
|
+
click.secho(f"Warning: Could not read .netrc file at {netrc_path}: {e}", err=True, fg="yellow")
|
|
323
|
+
|
|
324
|
+
return None
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _check_credentials_consistent_with_netrc(source: dict[str, Any], username: str, password: str) -> bool:
|
|
328
|
+
"""Check for consistency between provided credentials and .netrc file."""
|
|
329
|
+
|
|
330
|
+
credentials = _get_credentials_from_netrc(source)
|
|
331
|
+
|
|
332
|
+
if credentials is None:
|
|
333
|
+
return True # No .netrc entry to compare against
|
|
334
|
+
|
|
335
|
+
netrc_path, hostname, netrc_username, netrc_password = credentials
|
|
336
|
+
if netrc_username != username or netrc_password != password:
|
|
337
|
+
raise InstallError(
|
|
338
|
+
f"Error: Credentials in poetry environment variables for {hostname} do not match those in {netrc_path}.",
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
return True
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def validate_credentials(
|
|
345
|
+
source: dict[str, Any],
|
|
346
|
+
username: str,
|
|
347
|
+
password: str,
|
|
348
|
+
certificate: str | None = None,
|
|
349
|
+
verify: bool = True,
|
|
350
|
+
) -> bool:
|
|
351
|
+
"""
|
|
352
|
+
Validate the provided username and password against the remote PyPI server specified in the source.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
source (dict[str, Any]): The source dictionary containing the PyPI server URL.
|
|
356
|
+
username (str): The username to authenticate with.
|
|
357
|
+
password (str): The password to authenticate with.
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
bool: True if credentials are valid, False if unauthorized. Exits on other errors.
|
|
361
|
+
"""
|
|
362
|
+
url = str(source["url"])
|
|
363
|
+
|
|
364
|
+
try:
|
|
365
|
+
response = requests.get(
|
|
366
|
+
url + "/JUNK",
|
|
367
|
+
auth=HTTPBasicAuth(username, password),
|
|
368
|
+
timeout=(10, 10),
|
|
369
|
+
verify=certificate if verify else False,
|
|
370
|
+
)
|
|
371
|
+
except requests.exceptions.SSLError as e:
|
|
372
|
+
raise InstallError(
|
|
373
|
+
f"SSL certificate validation failed for {_get_source_name_formatted(source)}. "
|
|
374
|
+
"Please check the provided certificate.",
|
|
375
|
+
) from e
|
|
376
|
+
except requests.RequestException as e:
|
|
377
|
+
raise InstallError(f"Unable to reach {_get_source_name_formatted(source)} on {url}") from e
|
|
378
|
+
|
|
379
|
+
if response.status_code == 401:
|
|
380
|
+
return False
|
|
381
|
+
elif response.status_code in (200, 404):
|
|
382
|
+
return True
|
|
383
|
+
|
|
384
|
+
raise InstallError(
|
|
385
|
+
f"Unexpected response from {_get_source_name_formatted(source)}, Code is {response.status_code}",
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _get_parsed_url_from_source(source: dict[str, Any]):
|
|
390
|
+
parsed_url = urlparse(str(source["url"]))
|
|
391
|
+
return parsed_url
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _can_use_credentials_from_environment_variables_or_netrc(
|
|
395
|
+
source: dict[str, Any],
|
|
396
|
+
username_var: str,
|
|
397
|
+
password_var: str,
|
|
398
|
+
certificate_var: str,
|
|
399
|
+
) -> bool:
|
|
400
|
+
username = os.environ.get(username_var)
|
|
401
|
+
password = os.environ.get(password_var)
|
|
402
|
+
certificate, certificate_verification_disabled = _read_certificate_from_environment_variable(certificate_var)
|
|
403
|
+
|
|
404
|
+
if username and password:
|
|
405
|
+
_check_credentials_consistent_with_netrc(source, username, password)
|
|
406
|
+
|
|
407
|
+
click.echo(f"Validating credentials for {_get_source_name_formatted(source)}...")
|
|
408
|
+
if validate_credentials(
|
|
409
|
+
source,
|
|
410
|
+
username,
|
|
411
|
+
password,
|
|
412
|
+
certificate=certificate,
|
|
413
|
+
verify=not certificate_verification_disabled,
|
|
414
|
+
):
|
|
415
|
+
click.secho(f"Credentials for {source['name']} are valid.", fg="green")
|
|
416
|
+
return True
|
|
417
|
+
else:
|
|
418
|
+
click.secho(f"Invalid credentials for {source['name']}", err=True, fg="red")
|
|
419
|
+
else:
|
|
420
|
+
credentials = _get_credentials_from_netrc(source)
|
|
421
|
+
if credentials is not None:
|
|
422
|
+
netrc_path, _, username, password = credentials
|
|
423
|
+
source_name_formatted = _get_source_name_formatted(source)
|
|
424
|
+
if validate_credentials(
|
|
425
|
+
source,
|
|
426
|
+
username,
|
|
427
|
+
password,
|
|
428
|
+
certificate=certificate,
|
|
429
|
+
verify=not certificate_verification_disabled,
|
|
430
|
+
):
|
|
431
|
+
click.secho(f"Using credentials from {netrc_path} for {source_name_formatted}.", fg="green")
|
|
432
|
+
return True
|
|
433
|
+
else:
|
|
434
|
+
raise InstallError(
|
|
435
|
+
f"Unable to access private PyPI source {source_name_formatted}. "
|
|
436
|
+
"Poetry credential environment variables are not set "
|
|
437
|
+
f"and invalid credentials appear in {netrc_path}",
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
return False
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def _check_private_sources(configuration: dict[str, Any]) -> None:
|
|
444
|
+
"""Check and interactively manage private PyPI credentials."""
|
|
445
|
+
private_sources = configuration["tool"]["poetry"].get("source", [])
|
|
446
|
+
credentials_updated, certificate_updated = False, False
|
|
447
|
+
|
|
448
|
+
for source in private_sources:
|
|
449
|
+
if source["name"].lower() == "pypi":
|
|
450
|
+
source["url"] = "https://pypi.org/simple"
|
|
451
|
+
|
|
452
|
+
click.secho(
|
|
453
|
+
f"Checking access to {_get_source_name_formatted(source)}...",
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
source_name_slug = source["name"].upper().replace("-", "_")
|
|
457
|
+
certificate_var = f"POETRY_CERTIFICATES_{source_name_slug}_CERT"
|
|
458
|
+
|
|
459
|
+
certificate_updated = _prompt_validate_and_set_certificates(source, certificate_var) or certificate_updated
|
|
460
|
+
|
|
461
|
+
if source["name"].lower() == "pypi":
|
|
462
|
+
click.echo()
|
|
463
|
+
continue
|
|
464
|
+
|
|
465
|
+
username_var = f"POETRY_HTTP_BASIC_{source_name_slug}_USERNAME"
|
|
466
|
+
password_var = f"POETRY_HTTP_BASIC_{source_name_slug}_PASSWORD"
|
|
467
|
+
|
|
468
|
+
if not _can_use_credentials_from_environment_variables_or_netrc(
|
|
469
|
+
source,
|
|
470
|
+
username_var,
|
|
471
|
+
password_var,
|
|
472
|
+
certificate_var,
|
|
473
|
+
):
|
|
474
|
+
credentials_updated = (
|
|
475
|
+
_prompt_validate_and_set_credentials(source, username_var, password_var, certificate_var)
|
|
476
|
+
or credentials_updated
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
click.echo()
|
|
480
|
+
|
|
481
|
+
if credentials_updated or certificate_updated:
|
|
482
|
+
click.echo()
|
|
483
|
+
click.secho(
|
|
484
|
+
"IMPORTANT: Environment variables have been updated.\n"
|
|
485
|
+
"Please restart your terminal/IDE for the changes to take effect:\n"
|
|
486
|
+
" - If using VS Code: Close ALL instances and restart VS Code\n"
|
|
487
|
+
" - If using Command Prompt/PowerShell: Close and reopen your terminal\n"
|
|
488
|
+
" - If using Linux/macOS terminal: Start a new terminal session or run 'source ~/.bashrc'\n"
|
|
489
|
+
"Then run the install command again to continue with the setup.",
|
|
490
|
+
fg="yellow",
|
|
491
|
+
bold=True,
|
|
492
|
+
)
|
|
493
|
+
sys.exit(0)
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def _prompt_validate_and_set_credentials(
|
|
497
|
+
source: dict[str, Any],
|
|
498
|
+
username_var: str,
|
|
499
|
+
password_var: str,
|
|
500
|
+
certificate_var: str,
|
|
501
|
+
):
|
|
502
|
+
attempt = 0
|
|
503
|
+
credentials_updated = False
|
|
504
|
+
|
|
505
|
+
certificate, certificate_verification_disabled = _read_certificate_from_environment_variable(certificate_var)
|
|
506
|
+
|
|
507
|
+
while True:
|
|
508
|
+
try:
|
|
509
|
+
attempt += 1
|
|
510
|
+
new_username, new_password = _prompt_for_credentials(source, bool(attempt))
|
|
511
|
+
click.echo("Validating credentials...")
|
|
512
|
+
if validate_credentials(
|
|
513
|
+
source,
|
|
514
|
+
new_username,
|
|
515
|
+
new_password,
|
|
516
|
+
certificate=certificate,
|
|
517
|
+
verify=not certificate_verification_disabled,
|
|
518
|
+
):
|
|
519
|
+
set_user_level_environment_variable(username_var, new_username)
|
|
520
|
+
set_user_level_environment_variable(password_var, new_password)
|
|
521
|
+
credentials_updated = True
|
|
522
|
+
|
|
523
|
+
click.secho(f"Successfully configured credentials for {source['name']}", fg="green")
|
|
524
|
+
break
|
|
525
|
+
else:
|
|
526
|
+
click.secho(
|
|
527
|
+
"Invalid credentials. Please check your username and password/token and try again.",
|
|
528
|
+
err=True,
|
|
529
|
+
fg="red",
|
|
530
|
+
)
|
|
531
|
+
except click.Abort as e:
|
|
532
|
+
raise InstallError(
|
|
533
|
+
f"\nSetup cancelled. Install will fail without valid credentials for {source['name']}",
|
|
534
|
+
) from e
|
|
535
|
+
except KeyboardInterrupt as e:
|
|
536
|
+
raise InstallError(
|
|
537
|
+
f"\nSetup interrupted. Install will fail without valid credentials for {source['name']}",
|
|
538
|
+
) from e
|
|
539
|
+
|
|
540
|
+
return credentials_updated
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def _prompt_validate_and_set_certificates(source: dict[str, Any], certificate_var: str):
|
|
544
|
+
"""Prompt, validate and set SSL certificate for a given source."""
|
|
545
|
+
certificate, certificate_verification_disabled = _read_certificate_from_environment_variable(certificate_var)
|
|
546
|
+
|
|
547
|
+
if certificate_verification_disabled:
|
|
548
|
+
click.secho(
|
|
549
|
+
f"SSL certificate verification is currently disabled for {_get_source_name_formatted(source)}. "
|
|
550
|
+
"Note that this is not recommended as it can pose security risks.",
|
|
551
|
+
fg="yellow",
|
|
552
|
+
)
|
|
553
|
+
return False
|
|
554
|
+
elif not is_valid_certificate(source, certificate):
|
|
555
|
+
click.secho(
|
|
556
|
+
f"An SSL certificate is required to access the {_get_source_name_formatted(source)} source. "
|
|
557
|
+
"This is likely because you are in a corporate network with SSL interception "
|
|
558
|
+
"or because the source uses a self-signed certificate.",
|
|
559
|
+
)
|
|
560
|
+
else:
|
|
561
|
+
return False
|
|
562
|
+
|
|
563
|
+
while True:
|
|
564
|
+
try:
|
|
565
|
+
new_certificate = _prompt_for_certificate(source)
|
|
566
|
+
set_user_level_environment_variable(certificate_var, new_certificate)
|
|
567
|
+
certificate = new_certificate
|
|
568
|
+
except click.Abort as e:
|
|
569
|
+
raise InstallError(
|
|
570
|
+
f"\nSetup cancelled. Install will fail without a valid SSL certificate for {source['name']}",
|
|
571
|
+
) from e
|
|
572
|
+
except KeyboardInterrupt as e:
|
|
573
|
+
raise InstallError(
|
|
574
|
+
f"\nSetup interrupted. Install will fail without a valid SSL certificate for {source['name']}",
|
|
575
|
+
) from e
|
|
576
|
+
|
|
577
|
+
if not is_valid_certificate(source, certificate):
|
|
578
|
+
click.secho(
|
|
579
|
+
"Invalid certificate. Please check the provided certificate and try again.",
|
|
580
|
+
err=True,
|
|
581
|
+
fg="red",
|
|
582
|
+
)
|
|
583
|
+
else:
|
|
584
|
+
break
|
|
585
|
+
|
|
586
|
+
click.secho(f"Successfully configured certificate for {source['name']}", fg="green")
|
|
587
|
+
return True
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
def _prompt_for_certificate(source: dict[str, Any]):
|
|
591
|
+
"""Interactively prompt user for PyPI certificate."""
|
|
592
|
+
source_name = source["name"]
|
|
593
|
+
|
|
594
|
+
while True:
|
|
595
|
+
certificate = click.prompt(
|
|
596
|
+
f"Enter the path to a valid SSL certificate for {click.style(source_name, fg='cyan', bold=True)}",
|
|
597
|
+
type=str,
|
|
598
|
+
show_default=False,
|
|
599
|
+
).strip()
|
|
600
|
+
if Path(certificate).is_file():
|
|
601
|
+
break
|
|
602
|
+
else:
|
|
603
|
+
click.secho("Invalid file path. Please enter a valid path to the SSL certificate.", err=True, fg="red")
|
|
604
|
+
|
|
605
|
+
return certificate
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def is_valid_certificate(source: dict[str, Any], certificate: str | None) -> bool:
|
|
609
|
+
"""
|
|
610
|
+
Validate the provided SSL certificate against the remote PyPI server specified in the source.
|
|
611
|
+
|
|
612
|
+
Args:
|
|
613
|
+
source (dict[str, Any]): The source dictionary containing the PyPI server URL.
|
|
614
|
+
certificate_path (str | None): The path to the SSL certificate to validate.
|
|
615
|
+
|
|
616
|
+
Returns:
|
|
617
|
+
bool: True if certificate is valid, False otherwise.
|
|
618
|
+
"""
|
|
619
|
+
url = str(source["url"])
|
|
620
|
+
|
|
621
|
+
try:
|
|
622
|
+
requests.get(
|
|
623
|
+
url + "/JUNK",
|
|
624
|
+
verify=certificate or True,
|
|
625
|
+
timeout=(10, 10),
|
|
626
|
+
)
|
|
627
|
+
return True
|
|
628
|
+
except (requests.exceptions.SSLError, requests.RequestException):
|
|
629
|
+
return False
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def _get_source_name_formatted(source: dict[str, Any]):
|
|
633
|
+
return click.style(source["name"], fg="cyan", bold=True)
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def _read_certificate_from_environment_variable(certificate_var: str) -> tuple[str | None, bool]:
|
|
637
|
+
"""Read certificate path from environment variable and check if certificate verification is disabled."""
|
|
638
|
+
certificate = os.environ.get(certificate_var)
|
|
639
|
+
|
|
640
|
+
if certificate: # noqa: SIM102
|
|
641
|
+
if certificate.lower() == "false":
|
|
642
|
+
return None, True
|
|
643
|
+
|
|
644
|
+
return certificate, False
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
# General-purpose ---------------------------------------------------------------------------------------------------
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def _read_integers_from_string(string: str) -> list[str]:
|
|
651
|
+
"""Scan a string and extract integers. The regex matches any digit character (0-9)."""
|
|
652
|
+
return re.findall(r"\d+", string)
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def _get_python_version() -> str:
|
|
656
|
+
"""Get Python version."""
|
|
657
|
+
return f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
def _set_pip_command(
|
|
661
|
+
package_name: str,
|
|
662
|
+
package_version: str = "*",
|
|
663
|
+
method: str = "install",
|
|
664
|
+
pypi_url: str = "https://pypi.org/simple",
|
|
665
|
+
private_pypi_token_name: str = "",
|
|
666
|
+
no_deps: bool = False,
|
|
667
|
+
python_executable: str = sys.executable,
|
|
668
|
+
) -> list[str]:
|
|
669
|
+
"""Make pip command."""
|
|
670
|
+
package_name += f"=={package_version}" if package_version != "*" else ""
|
|
671
|
+
command = [python_executable, "-m", "pip", method, package_name]
|
|
672
|
+
# Add part related to private PyPI source
|
|
673
|
+
if pypi_url != "https://pypi.org/simple":
|
|
674
|
+
# Check if token is available
|
|
675
|
+
if not private_pypi_token_name:
|
|
676
|
+
raise Exception(f"No token specified for private PyPI server {pypi_url}.")
|
|
677
|
+
# Check if the environment variable associated to the private PyPI token exists
|
|
678
|
+
if (private_pypi_token := os.getenv(private_pypi_token_name)) is None:
|
|
679
|
+
raise Exception(f"Environment variable {private_pypi_token_name} does not exist.")
|
|
680
|
+
# Split absolute URL
|
|
681
|
+
parsed_url = urlparse(pypi_url)
|
|
682
|
+
# Update command line
|
|
683
|
+
command += ["-i", f"{parsed_url.scheme}://PAT:{private_pypi_token}@{parsed_url.netloc + parsed_url.path}"]
|
|
684
|
+
# Add no-deps option
|
|
685
|
+
if no_deps:
|
|
686
|
+
command += ["--no-deps"]
|
|
687
|
+
|
|
688
|
+
return command
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def _get_python_package(
|
|
692
|
+
package_name: str,
|
|
693
|
+
package_version: str = "*",
|
|
694
|
+
method: str = "install",
|
|
695
|
+
pypi_url: str = "https://pypi.org/simple",
|
|
696
|
+
private_pypi_token_name: str = "",
|
|
697
|
+
no_deps: bool = False,
|
|
698
|
+
python_executable: str = sys.executable,
|
|
699
|
+
) -> None:
|
|
700
|
+
"""
|
|
701
|
+
Install or download a python package using PIP.
|
|
702
|
+
|
|
703
|
+
Parameters
|
|
704
|
+
----------
|
|
705
|
+
package_name: str
|
|
706
|
+
Name of the package
|
|
707
|
+
package_version : str
|
|
708
|
+
Version of the package.
|
|
709
|
+
method : str
|
|
710
|
+
Get method: ``download`` or ``install``
|
|
711
|
+
pypi_url : str
|
|
712
|
+
URL of the PyPI server.
|
|
713
|
+
private_pypi_token_name : str
|
|
714
|
+
Name of the environment variable holding the token to access to the PyPI.
|
|
715
|
+
python_executable : str
|
|
716
|
+
Path to the Python executable.
|
|
717
|
+
|
|
718
|
+
Returns
|
|
719
|
+
-------
|
|
720
|
+
None
|
|
721
|
+
"""
|
|
722
|
+
# Set pip command
|
|
723
|
+
command = _set_pip_command(
|
|
724
|
+
package_name=package_name,
|
|
725
|
+
package_version=package_version,
|
|
726
|
+
method=method,
|
|
727
|
+
pypi_url=pypi_url,
|
|
728
|
+
private_pypi_token_name=private_pypi_token_name,
|
|
729
|
+
no_deps=no_deps,
|
|
730
|
+
python_executable=python_executable,
|
|
731
|
+
)
|
|
732
|
+
# Run pip command
|
|
733
|
+
process = subprocess.run(
|
|
734
|
+
command,
|
|
735
|
+
check=False,
|
|
736
|
+
capture_output=True,
|
|
737
|
+
text=True,
|
|
738
|
+
)
|
|
739
|
+
if process.returncode != 0:
|
|
740
|
+
print(process.stdout)
|
|
741
|
+
print(process.stderr)
|
|
742
|
+
raise Exception("Command failed with error.")
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
def _create_virtual_environment(
|
|
746
|
+
virtual_environment_exists: bool,
|
|
747
|
+
workspace_clear: str | None,
|
|
748
|
+
venv: str = VENV_DIR.name,
|
|
749
|
+
) -> None:
|
|
750
|
+
"""Create a virtual environment."""
|
|
751
|
+
print("Create virtual environment")
|
|
752
|
+
if virtual_environment_exists and not workspace_clear:
|
|
753
|
+
print("Skipped\n")
|
|
754
|
+
return
|
|
755
|
+
|
|
756
|
+
if platform.system() == "Linux":
|
|
757
|
+
subprocess.run([sys.executable, "-m", "pip", "install", "virtualenv"], check=True)
|
|
758
|
+
subprocess.run([sys.executable, "-m", "venv", venv], check=True)
|
|
759
|
+
print()
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
@contextmanager
|
|
763
|
+
def _exit_virtual_environment():
|
|
764
|
+
# this is necessary to trick poetry into thinking it is not in a virtual environment. Otherwise, even if we execute
|
|
765
|
+
# the poetry in the solution's venv with the CWD where the solution's pyproject.toml is located,
|
|
766
|
+
# it will install the packages in the caller's venv (saf-cli)
|
|
767
|
+
venv_path = os.environ.pop("VIRTUAL_ENV", None)
|
|
768
|
+
yield
|
|
769
|
+
if venv_path is not None:
|
|
770
|
+
os.environ["VIRTUAL_ENV"] = venv_path
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
@contextmanager
|
|
774
|
+
def _switch_cwd(solution_root_dir: Path):
|
|
775
|
+
working_directory = Path.cwd()
|
|
776
|
+
try:
|
|
777
|
+
os.chdir(solution_root_dir)
|
|
778
|
+
yield
|
|
779
|
+
finally:
|
|
780
|
+
os.chdir(working_directory)
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
# Dependency Management System (Build System) -----------------------------------------------------------------------
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
def _get_version_from_python_specification(specification: str) -> str:
|
|
787
|
+
"""Read the version."""
|
|
788
|
+
return ".".join(_read_integers_from_string(specification))
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
def _get_sign_from_python_specification(specification: str) -> str:
|
|
792
|
+
"""Read the mathematical symbol expressing a constraint on the version."""
|
|
793
|
+
first_part = specification.split(".")[0]
|
|
794
|
+
sign = "".join([i for i in first_part if not i.isdigit()])
|
|
795
|
+
if sign == "":
|
|
796
|
+
sign = "=="
|
|
797
|
+
return sign
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
def _check_poetry_plugin_export_is_required(build_system_version: str, python_interpreter: str) -> bool:
|
|
801
|
+
if int(build_system_version[0]) < 2:
|
|
802
|
+
return False
|
|
803
|
+
result = subprocess.run(
|
|
804
|
+
[python_interpreter, "-m", "pip", "show", "poetry-plugin-export"],
|
|
805
|
+
capture_output=True,
|
|
806
|
+
text=True,
|
|
807
|
+
)
|
|
808
|
+
return result.returncode != 0
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
def _install_build_system(
|
|
812
|
+
virtual_environment_exists: bool,
|
|
813
|
+
workspace_clear: str | None,
|
|
814
|
+
build_system_version: str,
|
|
815
|
+
) -> None:
|
|
816
|
+
"""Install build system."""
|
|
817
|
+
print("Install dependency management system.")
|
|
818
|
+
if virtual_environment_exists and not workspace_clear:
|
|
819
|
+
print("Skipped\n")
|
|
820
|
+
return
|
|
821
|
+
|
|
822
|
+
# We create a separate virtual environment for poetry, as it's recommended in its documentation:
|
|
823
|
+
# `Poetry should always be installed in a dedicated virtual environment to isolate it from the rest of your system.`
|
|
824
|
+
# See: https://python-poetry.org/docs/#installation
|
|
825
|
+
print()
|
|
826
|
+
_create_virtual_environment(
|
|
827
|
+
virtual_environment_exists,
|
|
828
|
+
workspace_clear,
|
|
829
|
+
venv=POETRY_VENV_DIR.as_posix(),
|
|
830
|
+
)
|
|
831
|
+
_get_python_package(
|
|
832
|
+
"poetry",
|
|
833
|
+
build_system_version,
|
|
834
|
+
method="install",
|
|
835
|
+
python_executable=POETRY_PYTHON_EXEC.absolute().as_posix(),
|
|
836
|
+
)
|
|
837
|
+
# poetry-plugin-export is currently included as a required plugin in the pyproject.toml of the solution.
|
|
838
|
+
# However, if saf install is run in an old solution, we need to make sure the plugin is installed when
|
|
839
|
+
# using poetry >=2.
|
|
840
|
+
if _check_poetry_plugin_export_is_required(build_system_version, POETRY_PYTHON_EXEC.absolute().as_posix()):
|
|
841
|
+
_get_python_package(
|
|
842
|
+
"poetry-plugin-export>=1.8",
|
|
843
|
+
method="install",
|
|
844
|
+
python_executable=POETRY_PYTHON_EXEC.absolute().as_posix(),
|
|
845
|
+
)
|
|
846
|
+
if not POETRY_POETRY_EXEC.is_file():
|
|
847
|
+
raise RuntimeError(f"Poetry executable not found at {POETRY_POETRY_EXEC}.")
|
|
848
|
+
if int(build_system_version[0]) < 2:
|
|
849
|
+
print("Downgrade virtualenv in the poetry virtual environment.")
|
|
850
|
+
_get_python_package(
|
|
851
|
+
"virtualenv",
|
|
852
|
+
"20.30.0", # virtualenv >= 20.31.0 is incompatible with poetry < 2
|
|
853
|
+
method="install",
|
|
854
|
+
python_executable=POETRY_PYTHON_EXEC.absolute().as_posix(),
|
|
855
|
+
)
|
|
856
|
+
# Create a file symbolic link from the virtual environment (.venv) that the build system (poetry) manages
|
|
857
|
+
# to the build system executable (poetry.exe) in the poetry virtual environment (.poetry/.venv). This
|
|
858
|
+
# ensures that the correct poetry is accessible when the managed virtual environment is activated
|
|
859
|
+
|
|
860
|
+
if platform.system() == "Windows":
|
|
861
|
+
subprocess.run(
|
|
862
|
+
[
|
|
863
|
+
"powershell",
|
|
864
|
+
"-Command",
|
|
865
|
+
"New-Item",
|
|
866
|
+
"-ItemType",
|
|
867
|
+
"HardLink",
|
|
868
|
+
"-Path",
|
|
869
|
+
f"'{POETRY_EXEC.absolute().as_posix()}'",
|
|
870
|
+
"-Target",
|
|
871
|
+
f"'{POETRY_POETRY_EXEC.absolute().as_posix()}'",
|
|
872
|
+
],
|
|
873
|
+
check=True,
|
|
874
|
+
)
|
|
875
|
+
elif platform.system() == "Linux":
|
|
876
|
+
subprocess.run(
|
|
877
|
+
[
|
|
878
|
+
"ln",
|
|
879
|
+
"-sf",
|
|
880
|
+
POETRY_POETRY_EXEC.absolute().as_posix(),
|
|
881
|
+
POETRY_EXEC.absolute().as_posix(),
|
|
882
|
+
],
|
|
883
|
+
check=True,
|
|
884
|
+
)
|
|
885
|
+
print()
|
|
886
|
+
|
|
887
|
+
|
|
888
|
+
def _configure_build_system(
|
|
889
|
+
virtual_environment_exists: bool,
|
|
890
|
+
workspace_clear: str | None,
|
|
891
|
+
) -> None:
|
|
892
|
+
"""Configure the build system to enable connection to private sources."""
|
|
893
|
+
print("Configure dependency management system.")
|
|
894
|
+
if virtual_environment_exists and not workspace_clear:
|
|
895
|
+
print("Skipped\n")
|
|
896
|
+
return
|
|
897
|
+
|
|
898
|
+
_configure_poetry()
|
|
899
|
+
print()
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
def _configure_poetry() -> None:
|
|
903
|
+
"""Configure Poetry."""
|
|
904
|
+
subprocess.run(
|
|
905
|
+
[
|
|
906
|
+
POETRY_EXEC.absolute().as_posix(),
|
|
907
|
+
"config",
|
|
908
|
+
"cache-dir",
|
|
909
|
+
POETRY_CACHE_DIR.absolute().as_posix(),
|
|
910
|
+
"--local",
|
|
911
|
+
],
|
|
912
|
+
check=True,
|
|
913
|
+
)
|
|
914
|
+
|
|
915
|
+
|
|
916
|
+
# Specifics ---------------------------------------------------------------------------------------------------------
|
|
917
|
+
|
|
918
|
+
|
|
919
|
+
def _clear_workspace(workspace_clear: str | None) -> None:
|
|
920
|
+
"""Remove residual items form previous installation (like venv directory, lock file ...)."""
|
|
921
|
+
if not workspace_clear:
|
|
922
|
+
print("Skip workspace clear")
|
|
923
|
+
print()
|
|
924
|
+
return
|
|
925
|
+
|
|
926
|
+
if VENV_DIR.is_dir():
|
|
927
|
+
print("Delete existing virtual environment '.venv'")
|
|
928
|
+
shutil.rmtree(VENV_DIR)
|
|
929
|
+
if POETRY_VENV_DIR.is_dir():
|
|
930
|
+
print("Delete existing poetry virtual environment")
|
|
931
|
+
shutil.rmtree(POETRY_VENV_DIR)
|
|
932
|
+
if POETRY_CACHE_DIR.is_dir():
|
|
933
|
+
print("Delete existing poetry cache")
|
|
934
|
+
shutil.rmtree(POETRY_CACHE_DIR)
|
|
935
|
+
if workspace_clear == "hard":
|
|
936
|
+
if POETRY_LOCK_FILE.is_file():
|
|
937
|
+
print("Delete existing poetry lock file")
|
|
938
|
+
POETRY_LOCK_FILE.unlink()
|
|
939
|
+
else:
|
|
940
|
+
print("Skip deleting poetry lock file")
|
|
941
|
+
print()
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
def _resolve_dependency_groups(
|
|
945
|
+
dependency_groups: list[str] | None,
|
|
946
|
+
optional_dependency_groups: list[str],
|
|
947
|
+
) -> list[str]:
|
|
948
|
+
if not dependency_groups:
|
|
949
|
+
default_dependency_groups = [
|
|
950
|
+
dependency_group
|
|
951
|
+
for dependency_group in ["desktop", "ui", "doc", "build"]
|
|
952
|
+
if dependency_group in optional_dependency_groups
|
|
953
|
+
]
|
|
954
|
+
return default_dependency_groups
|
|
955
|
+
if "all" in dependency_groups:
|
|
956
|
+
return optional_dependency_groups
|
|
957
|
+
for dependency_group in dependency_groups:
|
|
958
|
+
if dependency_group not in optional_dependency_groups:
|
|
959
|
+
raise ValueError(f"Invalid dependency group: {dependency_group}")
|
|
960
|
+
return dependency_groups
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
def _run_poetry_install(args: list[str]) -> None:
|
|
964
|
+
with _exit_virtual_environment():
|
|
965
|
+
try:
|
|
966
|
+
subprocess.run(
|
|
967
|
+
[POETRY_POETRY_EXEC.absolute().as_posix()] + args,
|
|
968
|
+
check=True,
|
|
969
|
+
text=True,
|
|
970
|
+
stderr=subprocess.PIPE,
|
|
971
|
+
)
|
|
972
|
+
|
|
973
|
+
except subprocess.CalledProcessError as e:
|
|
974
|
+
if "Authorization error" in e.stderr or "401 Client Error:" in e.stderr:
|
|
975
|
+
raise Exception(
|
|
976
|
+
"Poetry install failed due to authorization issues with a private PyPI source. "
|
|
977
|
+
"Please verify each username and password/token for private PyPI sources. ",
|
|
978
|
+
) from e
|
|
979
|
+
else:
|
|
980
|
+
raise Exception(
|
|
981
|
+
f"Poetry install failed with an unexpected error. {e.stderr}",
|
|
982
|
+
) from e
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
def _get_optional_dependency_groups(configuration: dict[str, Any]) -> list[str]:
|
|
986
|
+
optional_dependency_groups = []
|
|
987
|
+
if "group" in configuration["tool"]["poetry"]:
|
|
988
|
+
optional_dependency_groups = list(configuration["tool"]["poetry"]["group"])
|
|
989
|
+
return optional_dependency_groups
|
|
990
|
+
|
|
991
|
+
|
|
992
|
+
def _install_dependencies(dependency_groups: list[str]) -> None:
|
|
993
|
+
"""Install optional requirements (doc, tests, build or style)."""
|
|
994
|
+
# Install standard optional dependency groups
|
|
995
|
+
print(f"Install the following dependency groups: {', '.join(dependency_groups)}")
|
|
996
|
+
dependency_groups_str = ",".join(dependency_groups)
|
|
997
|
+
poetry_cmd = ["install"] + (["--with", dependency_groups_str] if dependency_groups_str else [])
|
|
998
|
+
_run_poetry_install(poetry_cmd)
|
|
999
|
+
print()
|
|
1000
|
+
|
|
1001
|
+
|
|
1002
|
+
def _install_dotnet_linux_dependencies():
|
|
1003
|
+
"""Install .NET for Linux."""
|
|
1004
|
+
print("Install dotnet dependencies")
|
|
1005
|
+
if platform.system() != "Linux":
|
|
1006
|
+
print("Skipped\n")
|
|
1007
|
+
return
|
|
1008
|
+
|
|
1009
|
+
subprocess.run(
|
|
1010
|
+
"""
|
|
1011
|
+
set -xe \
|
|
1012
|
+
&& wget https://dot.net/v1/dotnet-install.sh \
|
|
1013
|
+
&& chmod +x dotnet-install.sh \
|
|
1014
|
+
&& ./dotnet-install.sh --install-dir /home/$USER/.dotnet --version 8.0.8 --runtime aspnetcore \
|
|
1015
|
+
&& grep -qxF "DOTNET_ROOT=/home/$USER/.dotnet" /home/$USER/.bash_profile \
|
|
1016
|
+
|| echo DOTNET_ROOT=/home/$USER/.dotnet >> /home/$USER/.bash_profile \
|
|
1017
|
+
&& grep -qxF "PATH=\\$PATH:/home/$USER/.dotnet" /home/$USER/.bash_profile \
|
|
1018
|
+
|| echo "PATH=\\$PATH:/home/$USER/.dotnet" >> /home/$USER/.bash_profile \
|
|
1019
|
+
&& echo "\nsource /home/$USER/.bash_profile" >> ./.venv/bin/activate \
|
|
1020
|
+
&& rm dotnet-install.sh
|
|
1021
|
+
""",
|
|
1022
|
+
check=True,
|
|
1023
|
+
shell=True,
|
|
1024
|
+
)
|
|
1025
|
+
print()
|
|
1026
|
+
|
|
1027
|
+
|
|
1028
|
+
def _preload_solution_main_module(
|
|
1029
|
+
solution_python_exec: Path,
|
|
1030
|
+
solution_main_module_name: str,
|
|
1031
|
+
selected_dependency_groups: list[str],
|
|
1032
|
+
optional_dependency_groups: list[str],
|
|
1033
|
+
) -> None:
|
|
1034
|
+
"""Import the main module of the solution to speed up first saf run call."""
|
|
1035
|
+
print("Preload solution package")
|
|
1036
|
+
if "ui" in optional_dependency_groups and "ui" not in selected_dependency_groups:
|
|
1037
|
+
print("Skipped: UI is not in the selected dependency groups.\n")
|
|
1038
|
+
return
|
|
1039
|
+
try:
|
|
1040
|
+
subprocess.run(
|
|
1041
|
+
[solution_python_exec.absolute().as_posix(), "-c", f"import {solution_main_module_name}"],
|
|
1042
|
+
stderr=subprocess.PIPE,
|
|
1043
|
+
text=True,
|
|
1044
|
+
check=True,
|
|
1045
|
+
)
|
|
1046
|
+
except subprocess.CalledProcessError as e:
|
|
1047
|
+
raise RuntimeError(f"Importing the solution's main module failed: {e.stderr}") from e
|
|
1048
|
+
print()
|
|
1049
|
+
|
|
1050
|
+
|
|
1051
|
+
def setup_environment(
|
|
1052
|
+
solution_root_dir: Path,
|
|
1053
|
+
solution_main_module_name: str,
|
|
1054
|
+
dependency_groups: list[str] | None = None,
|
|
1055
|
+
workspace_clear: str | None = None,
|
|
1056
|
+
env_file: Path | None = None,
|
|
1057
|
+
) -> None:
|
|
1058
|
+
"""Sequence of operations leading to the complete Python ecosystem."""
|
|
1059
|
+
if workspace_clear is not None and workspace_clear not in WORKSPACE_CLEAR_OPTIONS:
|
|
1060
|
+
raise ValueError(f"Invalid workspace clear option: {workspace_clear}")
|
|
1061
|
+
|
|
1062
|
+
start_time = time.time()
|
|
1063
|
+
|
|
1064
|
+
# Initialize setup ------------------------------------------------------------------------------------------------
|
|
1065
|
+
|
|
1066
|
+
with _switch_cwd(solution_root_dir):
|
|
1067
|
+
configuration = tomlkit.loads(PYPROJECT_FILE.read_bytes()).unwrap()
|
|
1068
|
+
|
|
1069
|
+
if env_file is not None:
|
|
1070
|
+
print(f"Environment variables loaded from {env_file.resolve()}")
|
|
1071
|
+
load_dotenv(env_file)
|
|
1072
|
+
|
|
1073
|
+
_check_private_sources(configuration)
|
|
1074
|
+
|
|
1075
|
+
_check_python_version(configuration)
|
|
1076
|
+
|
|
1077
|
+
virtual_environment_exists = _check_virtual_environment_exists()
|
|
1078
|
+
|
|
1079
|
+
optional_dependency_groups = _get_optional_dependency_groups(configuration)
|
|
1080
|
+
selected_dependency_groups = _resolve_dependency_groups(dependency_groups, optional_dependency_groups)
|
|
1081
|
+
|
|
1082
|
+
build_system_version = configuration.get("build-system-requirements", {}).get("build-system-version", None)
|
|
1083
|
+
if not build_system_version:
|
|
1084
|
+
raise RuntimeError("No build system version found in the configuration file.")
|
|
1085
|
+
|
|
1086
|
+
_print_main_header("Setup Environment")
|
|
1087
|
+
|
|
1088
|
+
_print_inputs_summary(
|
|
1089
|
+
selected_dependency_groups,
|
|
1090
|
+
build_system_version,
|
|
1091
|
+
optional_dependency_groups,
|
|
1092
|
+
)
|
|
1093
|
+
|
|
1094
|
+
# Clear workspace ----------------------------------------------------------------------------------------------
|
|
1095
|
+
|
|
1096
|
+
_print_section_header("Clear workspace", max_length=100)
|
|
1097
|
+
|
|
1098
|
+
_clear_workspace(workspace_clear)
|
|
1099
|
+
|
|
1100
|
+
# Setup virtual environment ------------------------------------------------------------------------------------
|
|
1101
|
+
|
|
1102
|
+
_print_section_header("Setup virtual environment", max_length=100)
|
|
1103
|
+
|
|
1104
|
+
_create_virtual_environment(virtual_environment_exists, workspace_clear)
|
|
1105
|
+
|
|
1106
|
+
# Setup dependency management system ---------------------------------------------------------------------------
|
|
1107
|
+
|
|
1108
|
+
_print_section_header("Setup dependency management system", max_length=100)
|
|
1109
|
+
|
|
1110
|
+
_install_build_system(
|
|
1111
|
+
virtual_environment_exists,
|
|
1112
|
+
workspace_clear,
|
|
1113
|
+
build_system_version,
|
|
1114
|
+
)
|
|
1115
|
+
|
|
1116
|
+
_configure_build_system(
|
|
1117
|
+
virtual_environment_exists,
|
|
1118
|
+
workspace_clear,
|
|
1119
|
+
)
|
|
1120
|
+
|
|
1121
|
+
# Install dependencies -----------------------------------------------------------------------------------------
|
|
1122
|
+
|
|
1123
|
+
_print_section_header("Install dependencies", max_length=100)
|
|
1124
|
+
|
|
1125
|
+
_install_dependencies(selected_dependency_groups)
|
|
1126
|
+
|
|
1127
|
+
_install_dotnet_linux_dependencies()
|
|
1128
|
+
|
|
1129
|
+
# Preload installed solution package so that the first execution with saf run is faster.
|
|
1130
|
+
solution_python_exec = get_exec_from_solution_venv(solution_root_dir, "python")
|
|
1131
|
+
_preload_solution_main_module(
|
|
1132
|
+
solution_python_exec,
|
|
1133
|
+
solution_main_module_name,
|
|
1134
|
+
selected_dependency_groups,
|
|
1135
|
+
optional_dependency_groups,
|
|
1136
|
+
)
|
|
1137
|
+
|
|
1138
|
+
# Finish -------------------------------------------------------------------------------------------------------
|
|
1139
|
+
|
|
1140
|
+
_print_section_header("Summary", max_length=100)
|
|
1141
|
+
|
|
1142
|
+
elapsed_time = (time.time() - start_time) / 60
|
|
1143
|
+
|
|
1144
|
+
print("You are all set!")
|
|
1145
|
+
print(f"Execution time: {elapsed_time:.1f} minutes.")
|