Im Build-Prozess eines C++-Projekts kann es sinnvoll sein, Python-Skripte einzusetzen, beispielsweise für Codegeneratoren. Um sicherzustellen, dass diese Skripte stets mit denselben Abhängigkeiten arbeiten, unabhängig vom verwendeten Rechner, bietet sich die Nutzung einer virtuellen Python-Umgebung als gute Lösung an.
Kurze Einführung in venv#
venv
ist ein Python-Paket, das die Erstellung von virtuellen Umgebungen ermöglicht. Es hilft, Projektabhängigkeiten von globalen Python-Installationen zu trennen. Da venv
die Isolation und Reproduzierbarkeit von Projekt Abhängigkeiten gewährleistet, wird empfohlen, es in den meisten Fällen - auch außerhalb von Build-Umgebungen - zu verwenden.
Hinweis Ein wesentlicher Aspekt von venv ist die Tatsache, dass es seit Python 3.3 ein Bestandteil der Python-Standardbibliothek ist. Das bedeutet, dass man keine zusätzlichen Pakete oder Tools installieren muss, um venv zu verwenden. Sollte das für Dich kein Hindernis sein, existieren Python-Bibliotheken auf PyPi, die die Erstellung von virtuellen Umgebungen noch benutzerfreundlicher und einfacher machen. Insbesondere ist hier virtualenv zu nennen, aus dem venv ursprünglich hervorgegangen ist.
Um eine virtuelle Umgebung mit venv
zu erstellen, führe den folgenden Befehl aus:
python -m venv mein_venv
Der angegebene Befehl erstellt im aktuellen Arbeitsverzeichnis ein neues Verzeichnis mit dem Namen mein_venv
. In diesem Verzeichnis wird die virtuelle Umgebung eingerichtet. Das bedeutet unter anderem, dass der Python-Interpreter und die Standardbibliothek dorthin kopiert werden.
Anschließend kann sie durch folgenden Befehl aktiviert werden:
Windows: mein_venv\Scripts\activate
Linux/macOS: source mein_venv/bin/activate
Von diesem Zeitpunkt an ist die virtuelle Umgebung aktiv. Dies lässt sich am einfachsten daran erkennen, dass der Kommandozeile mein_venv
vorangestellt wird. Alle Aufrufe des Python-Interpreters beziehen sich nun auf die virtuelle Umgebung.
Achtung: Der Befehl ändert den Pfad. Verwendet ihr absolute
Pfade zum installierten Python Interpreter so wird nicht die
virtuelle Umgebung verwendet. Das kann insbesondere in Makefiles
leicht passiern.
Um die Umgebung zu deaktivieren, gib einfach deactivate
ein.
Pakete in einer virtuellen Umgebung installieren#
Nachdem du deine virtuelle Umgebung erstellt und aktiviert hast,
kannst du pip
verwenden, um benötigte Pakete zu installieren.
Am besten tust du dies, indem du eine requirements.txt
erstellst und sie wie folgt mit pip
verwendest.
pip install -r requirements.txt
Dadurch werden alle in der requirements.txt
Datei
angegebenen Pakete und ihre angegebenen Versionen in
die aktive virtuelle Umgebung installiert.
Das Format dieser Datei ist hier beschrieben.
Nach Installation der benötigen Pakete kann die virtuelle Umgebung, so wie eine globale Python Installation verwendet werden.
Tipp: Wird in der requirements.txt
lokalen Paketen die -e
Option vorangstellt (steht für “editable”), erkennt das
System Änderungen direkt am ursprünglichen Speicherort des Pakets. Diese Option ist besonders nützlich, wenn
lokale Pakete installiert werden sollen, die sich innerhalb der Build-Umgebung befinden. Dadurch können
Anpassungen und Updates im Paket leichter verfolgt und übernommen werden, ohne das Paket jedes Mal
erneut installieren zu müssen.
Verwendung von venv in einer Build Umgebung#
Alles zuvor Beschriebene zusammengenommen ermöglicht es, venv
in einer Build-Umgebung zu verwenden. Wenn etwa Historie ein klassisches Makefile zum Einsatz kommt, kann man, bevor der Python-Interpreter aufgerufen wird, erst einmal eine virtuelle Umgebung aufsetzen.
Da dies jedoch einige Sekunden benötigt, die bei inkrementellen Builds durchaus störend sein können (vor allem für mich 😃), möchte ich hier noch ein Skript anbieten. Es führt die zuvor genannten Schritte aus und bietet darüber hinaus noch folgende Funktionen:
- Es erstellt die virtuelle Umgebung nur, wenn sie noch nicht existiert.
- Es installiert Pakete nur, wenn sich die
requirements.txt
nicht geändert hat. - Es baut die virtuelle Umgebung neu auf, wenn sich das Skript selbst geändert hat.
- Schließlich aktiviert es die virtuelle Umgebung direkt.
Die angegebenen Funktionen werden durch das Erstellen von Zeitstempel-Dateien im Verzeichnis der virtuellen Umgebung erreicht.
Man kann das, was das Skript erreicht, auch durch Makerules erzielen. Allerdings finde ich den Ansatz über ein Skript angenehmer, da er von den meisten Entwicklern intuitiv besser verstanden wird. Entscheidet selbst.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
| """
This script creates a virtual environment and updates its dependencies.
The following features are supported.
- The script checks if the venv exists. If it does it will not recreate it.
- The script checks if the requirements.txt has changed. If it has, packages of the venv will be updated.
- The script checks if itself has changed. If it has, the venv will be recreated.
"""
import os
import subprocess
import sys
import argparse
import logging
from pathlib import Path
logger = logging.getLogger(__name__)
REQUIREMENTS_TIMESTAMP_FILENAME = ".requirements_timestamp"
SCRIPT_TIMESTAMP_FILENAME = ".setup_venv_timestamp"
PYTHON_CMD = "python"
SCRIPT_PATH = os.path.realpath(__file__)
SCRIPT_DIR = os.path.dirname(SCRIPT_PATH)
REQUIREMENTS_FILE = os.path.join(SCRIPT_DIR, "requirements.txt")
def get_file_timestamp(filepath):
return os.path.getmtime(filepath)
def create_virtualenv(venv_dir, clear):
"""Creates a virtual environment with venv
Parameters
----------
venv_dir : path to the directory where the venv will be created
clear : if true the former venv will be deleted before creation of a new one.
"""
sargs = [PYTHON_CMD]
sargs += ["-m", "venv"]
if clear:
sargs += ["--clear"]
sargs += [venv_dir]
subprocess.run(sargs)
python_executable = os.path.join(venv_dir, "Scripts", "python")
# Upgrade pip to its newest version - does not work in some build environments
subprocess.run([python_executable, "-m", "pip", "install", "--upgrade", "pip"])
def install_requirements(venv_dir, requirements_file):
# Get the current working directory
cwd = os.getcwd()
logger.debug(f'install_requirements: {{old_cwd: "{cwd}"}}')
python_executable = os.path.join(venv_dir, "Scripts", "python")
# !!This is a hack!! The working directory is changed to the directory where this
# script is contained to support relative imports in the requirements.txt.
# The script and requiremnts.txt must therefore be always in the same directory.
os.chdir(SCRIPT_DIR)
cwd = os.getcwd()
logger.debug(f'changed working directory to: {{cwd: "{cwd}"}}')
logger.debug(f'install_requirements: {{python_loc: "{python_executable}"}}')
subprocess.run([python_executable, "-m", "pip", "install", "-r", requirements_file])
os.chdir(cwd)
cwd = os.getcwd()
logger.debug(f'changed working directory to: {{cwd: "{cwd}"}}')
def update_virtual_env(
venv_dir,
requirements_timestamp_file,
current_requirements_timestamp,
script_timestamp_file,
current_script_timestamp,
):
print("Installing/Updating requirements...")
install_requirements(venv_dir, REQUIREMENTS_FILE)
Path(requirements_timestamp_file).write_text(str(current_requirements_timestamp))
Path(script_timestamp_file).write_text(str(current_script_timestamp))
def is_requirement_txt_outdated(
current_requirements_timestamp, stored_requirements_timestamp
):
"""Returns true when the requirements.txt file was changed"""
return (stored_requirements_timestamp is None) or (
current_requirements_timestamp > stored_requirements_timestamp
)
def is_script_outdated(current_script_timestamp, stored_script_timestamp):
"""Returns true when this script was changed"""
return (stored_script_timestamp is None) or (
current_script_timestamp > stored_script_timestamp
)
def main(arguments):
parser = argparse.ArgumentParser(
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
)
parser.add_argument(
"-d",
"--venv_root",
help="Root dir where the environment is generated",
default=".",
type=str,
)
parser.add_argument(
"-n",
"--venv_name",
help="Name of the environment to be generated",
default="venv",
type=str,
)
parser.add_argument(
"-v",
"--verbose",
action="store_true",
help="Activates verbose output",
default=False,
)
parser.add_argument(
"-c",
"--clear",
action="store_true",
help="Delete the contents of the environment directory before environment creation.",
default=False,
)
args = parser.parse_args(arguments)
if args.verbose:
logging.basicConfig(level=logging.DEBUG)
venv_dir = os.path.join(args.venv_root, args.venv_name)
logger.info(f"Directory of venv is {venv_dir}")
venv_exists = os.path.isdir(venv_dir)
# Get timestamp of this script
script_timestamp_file = os.path.join(venv_dir, SCRIPT_TIMESTAMP_FILENAME)
logger.info(f"Path of script timestamp file is {script_timestamp_file}")
current_script_timestamp = get_file_timestamp(SCRIPT_PATH)
stored_script_timestamp = (
float(Path(script_timestamp_file).read_text().strip())
if os.path.isfile(script_timestamp_file)
else None
)
script_outdated = is_script_outdated(
current_script_timestamp, stored_script_timestamp
)
clear_venv = args.clear or script_outdated
if (not venv_exists) or clear_venv:
print("Creating virtual environment...")
create_virtualenv(venv_dir, clear_venv)
#----
# Get timestamp of requirements.txt
requirements_timestamp_file = os.path.join(
venv_dir, REQUIREMENTS_TIMESTAMP_FILENAME
)
logger.info(f"Path of requirements timestamp file is {requirements_timestamp_file}")
current_requirements_timestamp = get_file_timestamp(REQUIREMENTS_FILE)
stored_requirements_timestamp = (
float(Path(requirements_timestamp_file).read_text().strip())
if os.path.isfile(requirements_timestamp_file)
else None
)
# Check requirements.txt
req_txt_outdated = is_requirement_txt_outdated(
current_requirements_timestamp, stored_requirements_timestamp
)
if req_txt_outdated or script_outdated:
update_virtual_env(
venv_dir,
requirements_timestamp_file,
current_requirements_timestamp,
script_timestamp_file,
current_script_timestamp,
)
else:
print("Virtual environment is up-to-date.")
return 0 # successfull termination
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
|
Zusammenfassung#
In diesem Artikel habe ich gezeigt, warum die Verwendung von venv
im Build-Prozess eines
C++ Projekts sinnvoll ist. Wir haben gesehen, wie man eine virtuelle Umgebung erstellt, aktiviert und deaktiviert.
Zudem haben wir besprochen, wie man eine requirements.txt
Datei verwendet um komfortabel Abhängigkeiten zu
installieren. Letztlich wird dem Leser ein Skript an die Hand gegeben um die Schritte
zu automatisieren.