common-python-tasks 0.2.0__py3-none-any.whl → 0.3.0__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.
- common_python_tasks/__init__.py +2 -8
- common_python_tasks/__main__.py +23 -19
- common_python_tasks/docker.py +296 -233
- common_python_tasks/{compose.py → docker_compose.py} +15 -9
- common_python_tasks/env.py +41 -16
- common_python_tasks/git.py +106 -49
- common_python_tasks/github.py +59 -41
- common_python_tasks/project.py +67 -27
- common_python_tasks/tasks.py +144 -91
- common_python_tasks/utils.py +44 -6
- {common_python_tasks-0.2.0.dist-info → common_python_tasks-0.3.0.dist-info}/METADATA +10 -10
- {common_python_tasks-0.2.0.dist-info → common_python_tasks-0.3.0.dist-info}/RECORD +14 -14
- {common_python_tasks-0.2.0.dist-info → common_python_tasks-0.3.0.dist-info}/WHEEL +0 -0
- {common_python_tasks-0.2.0.dist-info → common_python_tasks-0.3.0.dist-info}/licenses/LICENSE +0 -0
common_python_tasks/docker.py
CHANGED
|
@@ -1,54 +1,201 @@
|
|
|
1
1
|
import hashlib
|
|
2
|
+
import logging
|
|
2
3
|
import platform
|
|
3
4
|
import re
|
|
5
|
+
import tempfile
|
|
6
|
+
from enum import IntEnum, StrEnum
|
|
4
7
|
from pathlib import Path
|
|
5
8
|
from typing import Sequence
|
|
6
9
|
|
|
7
|
-
from . import git
|
|
8
|
-
from . import project as _project
|
|
9
|
-
from . import utils
|
|
10
|
+
from . import git, project, utils
|
|
10
11
|
from .env import get_cache_id_suffix, get_workdir_path
|
|
11
12
|
|
|
13
|
+
LOGGER = logging.getLogger(__name__)
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
|
|
16
|
+
class PruneKeep(IntEnum):
|
|
17
|
+
"""Special keep values for image pruning behavior."""
|
|
18
|
+
|
|
19
|
+
DISABLED = -1
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DockerPlatform(StrEnum):
|
|
23
|
+
"""Supported Docker target platforms for multi-arch builds."""
|
|
24
|
+
|
|
25
|
+
AMD64 = "linux/amd64"
|
|
26
|
+
ARM64 = "linux/arm64"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class BuildStage(StrEnum):
|
|
30
|
+
"""Named Docker build stages used by image builders."""
|
|
31
|
+
|
|
32
|
+
RUNTIME = "runtime"
|
|
33
|
+
DEBUG = "debug"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class BuildTag(StrEnum):
|
|
37
|
+
"""Reserved tags managed by image build helpers."""
|
|
38
|
+
|
|
39
|
+
LATEST = "latest"
|
|
40
|
+
DEBUG = "debug"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class BuildSuffix(StrEnum):
|
|
44
|
+
"""Suffixes appended to version and commit image tags."""
|
|
45
|
+
|
|
46
|
+
NONE = ""
|
|
47
|
+
DEBUG = "-debug"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _resolve_context_path(context_path: Path | None) -> Path:
|
|
51
|
+
return context_path if context_path is not None else Path(".")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _resolve_archs(single_arch: bool) -> list[DockerPlatform] | None:
|
|
55
|
+
return None if single_arch else [DockerPlatform.AMD64, DockerPlatform.ARM64]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _merge_build_args(
|
|
59
|
+
base_build_args: dict[str, str | None],
|
|
60
|
+
extra_build_args: dict[str, str] | None = None,
|
|
61
|
+
) -> dict[str, str]:
|
|
62
|
+
build_args = {k: v for k, v in base_build_args.items() if v is not None}
|
|
63
|
+
if extra_build_args is not None:
|
|
64
|
+
build_args.update({k: v for k, v in extra_build_args.items() if v is not None})
|
|
65
|
+
return build_args
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _create_temp_dockerfile(
|
|
69
|
+
dockerfile_text: str,
|
|
70
|
+
prefix: str,
|
|
71
|
+
) -> tuple[Path, str]:
|
|
72
|
+
with tempfile.NamedTemporaryFile(
|
|
73
|
+
mode="w",
|
|
74
|
+
encoding="utf-8",
|
|
75
|
+
delete=False,
|
|
76
|
+
prefix=prefix,
|
|
77
|
+
suffix=".generated",
|
|
78
|
+
) as temp_file:
|
|
79
|
+
temp_file.write(dockerfile_text)
|
|
80
|
+
temp_file_path = temp_file.name
|
|
81
|
+
|
|
82
|
+
return Path(temp_file_path), temp_file_path
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _safe_unlink(path: Path | str | None) -> None:
|
|
86
|
+
if path is None:
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
Path(path).unlink()
|
|
91
|
+
except FileNotFoundError:
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _build_docker_build_command(
|
|
96
|
+
*,
|
|
97
|
+
context_path: Path,
|
|
98
|
+
dockerfile_path: Path,
|
|
99
|
+
build_args: dict[str, str],
|
|
100
|
+
archs: list[DockerPlatform] | None,
|
|
101
|
+
no_cache: bool,
|
|
102
|
+
plain: bool,
|
|
103
|
+
target: BuildStage | None = None,
|
|
104
|
+
tags: Sequence[str] | None = None,
|
|
105
|
+
) -> list[str]:
|
|
106
|
+
build_cmd = [
|
|
107
|
+
"docker",
|
|
108
|
+
"build",
|
|
109
|
+
str(context_path),
|
|
110
|
+
"-f",
|
|
111
|
+
str(dockerfile_path),
|
|
112
|
+
]
|
|
113
|
+
if target is not None:
|
|
114
|
+
build_cmd += ["--target", target]
|
|
115
|
+
|
|
116
|
+
build_cmd += [
|
|
117
|
+
item for k, v in build_args.items() for item in ("--build-arg", f"{k}={v}")
|
|
118
|
+
]
|
|
119
|
+
if archs:
|
|
120
|
+
build_cmd += ["--platform", ",".join(archs)]
|
|
121
|
+
if no_cache:
|
|
122
|
+
build_cmd += ["--no-cache", "--pull"]
|
|
123
|
+
if tags is not None:
|
|
124
|
+
for tag in tags:
|
|
125
|
+
build_cmd += ["-t", tag]
|
|
126
|
+
if plain:
|
|
127
|
+
build_cmd += ["--progress", "plain"]
|
|
128
|
+
|
|
129
|
+
return build_cmd
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def prune_images_keep(
|
|
133
|
+
full_name: str,
|
|
134
|
+
package_name: str,
|
|
135
|
+
keep: PruneKeep,
|
|
136
|
+
protect_tags: Sequence[str] | None = None,
|
|
22
137
|
) -> None:
|
|
23
|
-
"""
|
|
138
|
+
"""Prune images for `full_name` keeping the most-recent `keep + 1` images.
|
|
24
139
|
|
|
25
140
|
Args:
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
single_arch: Build the container for the current host architecture only.
|
|
32
|
-
build_args: Additional build arguments for the Docker build as repeated
|
|
33
|
-
`KEY=VALUE` values.
|
|
34
|
-
container_env: Inline container environment variables as repeated
|
|
35
|
-
`KEY=VALUE` values.
|
|
36
|
-
container_envfile: Repeated list of container environment files.
|
|
141
|
+
full_name: The full image name (including registry and repository) to prune, e.g. `ghcr.io/myorg/myimage`.
|
|
142
|
+
package_name: The name of the package (repository) to prune, e.g. `myimage`.
|
|
143
|
+
keep: The number of most-recent images to keep. Use
|
|
144
|
+
`PruneKeep.DISABLED` to skip pruning.
|
|
145
|
+
protect_tags: A list of tags to protect from pruning.
|
|
37
146
|
"""
|
|
38
|
-
from common_python_tasks.tasks import build_image as build_container_image
|
|
39
|
-
from common_python_tasks.tasks import build_package
|
|
40
147
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
148
|
+
if keep <= PruneKeep.DISABLED:
|
|
149
|
+
return
|
|
150
|
+
if protect_tags is None:
|
|
151
|
+
protect_tags = []
|
|
152
|
+
|
|
153
|
+
retain_count = int(keep) + 1
|
|
154
|
+
|
|
155
|
+
res = utils.run_command(
|
|
156
|
+
[
|
|
157
|
+
"docker",
|
|
158
|
+
"image",
|
|
159
|
+
"ls",
|
|
160
|
+
"--format",
|
|
161
|
+
"{{.Repository}}:{{.Tag}}",
|
|
162
|
+
"--filter",
|
|
163
|
+
f"reference={full_name}:*",
|
|
164
|
+
],
|
|
165
|
+
capture_output=True,
|
|
166
|
+
acceptable_returncodes={0, 1},
|
|
167
|
+
)
|
|
168
|
+
if res.returncode != 0:
|
|
169
|
+
LOGGER.exception("Failed to list images for pruning: %s", full_name)
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
lines = [
|
|
173
|
+
line.strip()
|
|
174
|
+
for line in res.stdout.splitlines()
|
|
175
|
+
if line.strip() and not line.strip().startswith("<none>")
|
|
176
|
+
]
|
|
177
|
+
tags_in_order = []
|
|
178
|
+
for entry in lines:
|
|
179
|
+
if ":" not in entry:
|
|
180
|
+
continue
|
|
181
|
+
repo, tag = entry.rsplit(":", 1)
|
|
182
|
+
if entry.startswith(full_name) or repo.endswith(package_name):
|
|
183
|
+
tags_in_order.append(tag)
|
|
184
|
+
|
|
185
|
+
candidates = [t for t in tags_in_order if t not in protect_tags]
|
|
186
|
+
if len(candidates) <= retain_count:
|
|
187
|
+
return
|
|
188
|
+
|
|
189
|
+
to_delete = candidates[retain_count:]
|
|
190
|
+
for tag in to_delete:
|
|
191
|
+
for img in (f"{package_name}:{tag}", f"{full_name}:{tag}"):
|
|
192
|
+
try:
|
|
193
|
+
LOGGER.info("Pruning image %s", img)
|
|
194
|
+
utils.run_command(["docker", "rmi", img], acceptable_returncodes={0, 1})
|
|
195
|
+
except SystemExit:
|
|
196
|
+
LOGGER.warning(
|
|
197
|
+
"Failed to remove image %s during pruning; continuing", img
|
|
198
|
+
)
|
|
52
199
|
|
|
53
200
|
|
|
54
201
|
def build_deps_image(
|
|
@@ -61,10 +208,23 @@ def build_deps_image(
|
|
|
61
208
|
extra_build_args: dict[str, str] | None = None,
|
|
62
209
|
cache_id_suffix: str = "",
|
|
63
210
|
) -> str:
|
|
64
|
-
"""Build the dependency collector image and return its full tag.
|
|
211
|
+
"""Build the dependency collector image and return its full tag.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
deps_content: The content to use for the dependencies Dockerfile. If provided, this will be rendered into a template Dockerfile.
|
|
215
|
+
deps_dockerfile_path: A path or list of paths to Dockerfile(s) to use for the dependencies image. If multiple paths are provided, their contents will be concatenated. If provided, this takes precedence over `deps_content`.
|
|
216
|
+
context_path: The build context path for the Docker build. Defaults to the current directory.
|
|
217
|
+
no_cache: If `True`, build the image without using cache.
|
|
218
|
+
plain: If `True`, use plain output mode for the Docker build.
|
|
219
|
+
single_arch: If `True`, build the image for the current host architecture only.
|
|
220
|
+
extra_build_args: Additional build arguments for the Docker build as a dictionary of `KEY: VALUE` pairs.
|
|
221
|
+
cache_id_suffix: A suffix to append to the cache ID.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
The full tag of the built dependency image.
|
|
225
|
+
"""
|
|
65
226
|
|
|
66
|
-
|
|
67
|
-
context_path = Path(".")
|
|
227
|
+
context_path = _resolve_context_path(context_path)
|
|
68
228
|
|
|
69
229
|
deps_image_name = f"{utils.get_package_name()}-deps"
|
|
70
230
|
if deps_content is not None and deps_dockerfile_path is not None:
|
|
@@ -103,21 +263,10 @@ def build_deps_image(
|
|
|
103
263
|
utils.load_data_file("Dockerfile.deps.j2")[1],
|
|
104
264
|
{"DEPS_CONTENT": deps_content},
|
|
105
265
|
)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
tf = tempfile.NamedTemporaryFile(
|
|
110
|
-
mode="w",
|
|
111
|
-
encoding="utf-8",
|
|
112
|
-
delete=False,
|
|
266
|
+
dockerfile_path, temp_file_path = _create_temp_dockerfile(
|
|
267
|
+
dockerfile_text,
|
|
113
268
|
prefix="Dockerfile.deps.",
|
|
114
|
-
suffix=".generated",
|
|
115
269
|
)
|
|
116
|
-
temp_file_path = tf.name
|
|
117
|
-
with open(temp_file_path, "w", encoding="utf-8") as f:
|
|
118
|
-
f.write(dockerfile_text)
|
|
119
|
-
|
|
120
|
-
dockerfile_path = Path(temp_file_path)
|
|
121
270
|
else:
|
|
122
271
|
if isinstance(deps_dockerfile_path, list):
|
|
123
272
|
if len(deps_dockerfile_path) == 1:
|
|
@@ -127,124 +276,43 @@ def build_deps_image(
|
|
|
127
276
|
path.read_text(encoding="utf-8").strip()
|
|
128
277
|
for path in deps_dockerfile_path
|
|
129
278
|
)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
tf = tempfile.NamedTemporaryFile(
|
|
133
|
-
mode="w",
|
|
134
|
-
encoding="utf-8",
|
|
135
|
-
delete=False,
|
|
279
|
+
dockerfile_path, temp_file_path = _create_temp_dockerfile(
|
|
280
|
+
dockerfile_text,
|
|
136
281
|
prefix="Dockerfile.deps.",
|
|
137
|
-
suffix=".generated",
|
|
138
282
|
)
|
|
139
|
-
temp_file_path = tf.name
|
|
140
|
-
with open(temp_file_path, "w", encoding="utf-8") as f:
|
|
141
|
-
f.write(dockerfile_text)
|
|
142
|
-
|
|
143
|
-
dockerfile_path = Path(temp_file_path)
|
|
144
283
|
else:
|
|
145
284
|
dockerfile_path = deps_dockerfile_path
|
|
146
285
|
|
|
147
286
|
python_version = platform.python_version()
|
|
148
|
-
archs =
|
|
287
|
+
archs = _resolve_archs(single_arch)
|
|
149
288
|
|
|
150
|
-
build_args
|
|
151
|
-
|
|
152
|
-
|
|
289
|
+
build_args = _merge_build_args(
|
|
290
|
+
{"PYTHON_VERSION": python_version},
|
|
291
|
+
extra_build_args,
|
|
292
|
+
)
|
|
153
293
|
if cache_id_suffix:
|
|
154
294
|
build_args["CACHE_ID_SUFFIX"] = cache_id_suffix
|
|
155
295
|
|
|
156
296
|
full_tag = f"{deps_image_name}:{deps_tag}"
|
|
157
|
-
build_cmd =
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
"--no-cache" if no_cache else None,
|
|
167
|
-
"--pull" if no_cache else None,
|
|
168
|
-
"-t",
|
|
169
|
-
full_tag,
|
|
170
|
-
]
|
|
171
|
-
if plain:
|
|
172
|
-
build_cmd += ["--progress", "plain"]
|
|
297
|
+
build_cmd = _build_docker_build_command(
|
|
298
|
+
context_path=context_path,
|
|
299
|
+
dockerfile_path=dockerfile_path,
|
|
300
|
+
build_args=build_args,
|
|
301
|
+
archs=archs,
|
|
302
|
+
no_cache=no_cache,
|
|
303
|
+
plain=plain,
|
|
304
|
+
tags=[full_tag],
|
|
305
|
+
)
|
|
173
306
|
|
|
174
|
-
|
|
307
|
+
LOGGER.info("Building deps image: %s", full_tag)
|
|
175
308
|
try:
|
|
176
309
|
utils.run_command(build_cmd)
|
|
177
310
|
finally:
|
|
178
|
-
|
|
179
|
-
try:
|
|
180
|
-
Path(temp_file_path).unlink()
|
|
181
|
-
except FileNotFoundError:
|
|
182
|
-
pass
|
|
311
|
+
_safe_unlink(temp_file_path)
|
|
183
312
|
|
|
184
313
|
return full_tag
|
|
185
314
|
|
|
186
315
|
|
|
187
|
-
def prune_images_keep(
|
|
188
|
-
full_name: str,
|
|
189
|
-
package_name: str,
|
|
190
|
-
keep: int,
|
|
191
|
-
protect_tags: Sequence[str] | None = None,
|
|
192
|
-
) -> None:
|
|
193
|
-
"""Prune images for `full_name` keeping the most-recent `keep + 1` images."""
|
|
194
|
-
|
|
195
|
-
if keep < 0:
|
|
196
|
-
return
|
|
197
|
-
if protect_tags is None:
|
|
198
|
-
protect_tags = []
|
|
199
|
-
|
|
200
|
-
retain_count = keep + 1
|
|
201
|
-
|
|
202
|
-
res = utils.run_command(
|
|
203
|
-
[
|
|
204
|
-
"docker",
|
|
205
|
-
"image",
|
|
206
|
-
"ls",
|
|
207
|
-
"--format",
|
|
208
|
-
"{{.Repository}}:{{.Tag}}",
|
|
209
|
-
"--filter",
|
|
210
|
-
f"reference={full_name}:*",
|
|
211
|
-
],
|
|
212
|
-
capture_output=True,
|
|
213
|
-
acceptable_returncodes={0, 1},
|
|
214
|
-
)
|
|
215
|
-
if res.returncode != 0:
|
|
216
|
-
utils.LOGGER.exception("Failed to list images for pruning: %s", full_name)
|
|
217
|
-
return
|
|
218
|
-
|
|
219
|
-
lines = [
|
|
220
|
-
line.strip()
|
|
221
|
-
for line in res.stdout.splitlines()
|
|
222
|
-
if line.strip() and not line.strip().startswith("<none>")
|
|
223
|
-
]
|
|
224
|
-
tags_in_order = []
|
|
225
|
-
for entry in lines:
|
|
226
|
-
if ":" not in entry:
|
|
227
|
-
continue
|
|
228
|
-
repo, tag = entry.rsplit(":", 1)
|
|
229
|
-
if entry.startswith(full_name) or repo.endswith(package_name):
|
|
230
|
-
tags_in_order.append(tag)
|
|
231
|
-
|
|
232
|
-
candidates = [t for t in tags_in_order if t not in protect_tags]
|
|
233
|
-
if len(candidates) <= retain_count:
|
|
234
|
-
return
|
|
235
|
-
|
|
236
|
-
to_delete = candidates[retain_count:]
|
|
237
|
-
for tag in to_delete:
|
|
238
|
-
for img in (f"{package_name}:{tag}", f"{full_name}:{tag}"):
|
|
239
|
-
try:
|
|
240
|
-
utils.LOGGER.info("Pruning image %s", img)
|
|
241
|
-
utils.run_command(["docker", "rmi", img], acceptable_returncodes={0, 1})
|
|
242
|
-
except SystemExit:
|
|
243
|
-
utils.LOGGER.warning(
|
|
244
|
-
"Failed to remove image %s during pruning; continuing", img
|
|
245
|
-
)
|
|
246
|
-
|
|
247
|
-
|
|
248
316
|
def build_image(
|
|
249
317
|
dockerfile_path: Path | None = None,
|
|
250
318
|
dockerfile_text: str | None = None,
|
|
@@ -257,47 +325,51 @@ def build_image(
|
|
|
257
325
|
image_name: str | None = None,
|
|
258
326
|
extra_build_args: dict[str, str] | None = None,
|
|
259
327
|
) -> tuple[str, str]:
|
|
260
|
-
"""Build
|
|
261
|
-
from common_python_tasks.tasks import clean
|
|
328
|
+
"""Build the primary image and return its version and commit tags.
|
|
262
329
|
|
|
330
|
+
Args:
|
|
331
|
+
dockerfile_path: Path to the Dockerfile.
|
|
332
|
+
dockerfile_text: Dockerfile content as a string.
|
|
333
|
+
context_path: Path to the build context.
|
|
334
|
+
debug: If `True`, build the debug image.
|
|
335
|
+
no_cache: If `True`, do not use cache when building the image.
|
|
336
|
+
plain: If `True`, use plain output mode for the Docker build.
|
|
337
|
+
single_arch: If `True`, build the image for the current host architecture only.
|
|
338
|
+
omit_target: If `True`, omit the target stage.
|
|
339
|
+
image_name: Name of the image to build.
|
|
340
|
+
extra_build_args: Additional build arguments for the Docker build as a dictionary of `KEY: VALUE` pairs.
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
A tuple containing the version tag and commit tag of the built image.
|
|
344
|
+
"""
|
|
263
345
|
dist_path = Path("dist")
|
|
264
|
-
if
|
|
265
|
-
|
|
346
|
+
if utils.directory_has_contents(dist_path):
|
|
347
|
+
utils.remove_path(dist_path)
|
|
266
348
|
|
|
267
|
-
|
|
268
|
-
context_path = Path(".")
|
|
349
|
+
context_path = _resolve_context_path(context_path)
|
|
269
350
|
|
|
270
351
|
temp_file_path = None
|
|
271
352
|
if dockerfile_path is None:
|
|
272
353
|
if dockerfile_text is None:
|
|
273
354
|
utils.fatal("Either dockerfile_path or dockerfile_text must be provided.")
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
tf = tempfile.NamedTemporaryFile(
|
|
277
|
-
mode="w",
|
|
278
|
-
encoding="utf-8",
|
|
279
|
-
delete=False,
|
|
355
|
+
dockerfile_path, temp_file_path = _create_temp_dockerfile(
|
|
356
|
+
dockerfile_text,
|
|
280
357
|
prefix="Dockerfile.",
|
|
281
|
-
suffix=".generated",
|
|
282
358
|
)
|
|
283
|
-
temp_file_path = tf.name
|
|
284
|
-
with open(temp_file_path, "w", encoding="utf-8") as f:
|
|
285
|
-
f.write(dockerfile_text)
|
|
286
|
-
dockerfile_path = Path(temp_file_path)
|
|
287
359
|
|
|
288
360
|
dockerignore_path = context_path / ".dockerignore"
|
|
289
361
|
temp_dockerignore_created = False
|
|
290
362
|
if not dockerignore_path.exists():
|
|
291
|
-
|
|
363
|
+
LOGGER.debug("No .dockerignore found; using built-in .dockerignore")
|
|
292
364
|
builtin_dockerignore_content = utils.load_data_file(".dockerignore")[1]
|
|
293
365
|
dockerignore_path.write_text(builtin_dockerignore_content, encoding="utf-8")
|
|
294
366
|
temp_dockerignore_created = True
|
|
295
367
|
|
|
296
368
|
delete_temp_file = False
|
|
297
369
|
try:
|
|
298
|
-
archs =
|
|
370
|
+
archs = _resolve_archs(single_arch)
|
|
299
371
|
files_to_ignore = [Path(".dockerignore")] if temp_dockerignore_created else []
|
|
300
|
-
version_string =
|
|
372
|
+
version_string = git.get_image_tag(ignore=files_to_ignore)
|
|
301
373
|
|
|
302
374
|
dockerfile_source = (
|
|
303
375
|
dockerfile_text
|
|
@@ -308,7 +380,7 @@ def build_image(
|
|
|
308
380
|
debug
|
|
309
381
|
and not omit_target
|
|
310
382
|
and not re.search(
|
|
311
|
-
|
|
383
|
+
rf"^\s*FROM\s+\S+\s+AS\s+{BuildStage.DEBUG}\s*$",
|
|
312
384
|
dockerfile_source,
|
|
313
385
|
re.IGNORECASE | re.MULTILINE,
|
|
314
386
|
)
|
|
@@ -319,50 +391,43 @@ def build_image(
|
|
|
319
391
|
)
|
|
320
392
|
|
|
321
393
|
if debug:
|
|
322
|
-
suffix =
|
|
323
|
-
target =
|
|
324
|
-
tag =
|
|
394
|
+
suffix = BuildSuffix.DEBUG
|
|
395
|
+
target = BuildStage.DEBUG
|
|
396
|
+
tag = BuildTag.DEBUG
|
|
325
397
|
else:
|
|
326
|
-
suffix =
|
|
327
|
-
target =
|
|
328
|
-
tag =
|
|
398
|
+
suffix = BuildSuffix.NONE
|
|
399
|
+
target = BuildStage.RUNTIME
|
|
400
|
+
tag = BuildTag.LATEST if not git.has_tags_later_in_history() else None
|
|
329
401
|
|
|
330
402
|
version_tag = f"{version_string}{suffix}"
|
|
331
|
-
commit_tag = f"{utils.run_command(['git', 'rev-parse', '--short', 'HEAD'], capture_output=True).stdout.strip()}{'-dirty' if
|
|
403
|
+
commit_tag = f"{utils.run_command(['git', 'rev-parse', '--short', 'HEAD'], capture_output=True).stdout.strip()}{'-dirty' if git.get_dirty_files(ignore=files_to_ignore) else ''}{suffix}"
|
|
332
404
|
python_version = platform.python_version()
|
|
333
|
-
poetry_version =
|
|
405
|
+
poetry_version = project.get_poetry_version()
|
|
334
406
|
|
|
335
|
-
build_args =
|
|
336
|
-
|
|
337
|
-
for k, v in {
|
|
407
|
+
build_args = _merge_build_args(
|
|
408
|
+
{
|
|
338
409
|
"PYTHON_VERSION": python_version,
|
|
339
410
|
"POETRY_VERSION": poetry_version,
|
|
340
|
-
"POETRY_DYNAMIC_VERSIONING_PLUGIN_VERSION":
|
|
411
|
+
"POETRY_DYNAMIC_VERSIONING_PLUGIN_VERSION": project.get_installed_requirement_version(
|
|
341
412
|
"poetry-dynamic-versioning[plugin]"
|
|
342
413
|
),
|
|
343
|
-
"POETRY_PLUGIN_EXPORT_VERSION":
|
|
414
|
+
"POETRY_PLUGIN_EXPORT_VERSION": project.get_installed_requirement_version(
|
|
344
415
|
"poetry-plugin-export"
|
|
345
416
|
),
|
|
346
|
-
"TOMLKIT_VERSION":
|
|
347
|
-
"tomlkit"
|
|
348
|
-
),
|
|
417
|
+
"TOMLKIT_VERSION": project.get_installed_requirement_version("tomlkit"),
|
|
349
418
|
"PACKAGE_NAME": utils.get_package_name(use_underscores=True),
|
|
350
419
|
"AUTHORS": ",".join(
|
|
351
|
-
[f"{name} <{email}>" for name, email in
|
|
420
|
+
[f"{name} <{email}>" for name, email in project.get_authors()]
|
|
352
421
|
),
|
|
353
422
|
"GIT_COMMIT": commit_tag,
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
if extra_build_args:
|
|
358
|
-
for k, v in extra_build_args.items():
|
|
359
|
-
if v is not None:
|
|
360
|
-
build_args[k] = v
|
|
423
|
+
},
|
|
424
|
+
extra_build_args,
|
|
425
|
+
)
|
|
361
426
|
|
|
362
427
|
build_args.setdefault("WORKDIR_PATH", get_workdir_path())
|
|
363
428
|
|
|
364
429
|
tags_to_use = [t for t in (tag, version_tag, commit_tag) if t is not None]
|
|
365
|
-
|
|
430
|
+
LOGGER.debug("Building image with tags: %s", ", ".join(tags_to_use))
|
|
366
431
|
image_short_name = (
|
|
367
432
|
image_name if image_name is not None else utils.get_package_name()
|
|
368
433
|
)
|
|
@@ -375,43 +440,26 @@ def build_image(
|
|
|
375
440
|
image_full_name = f"{prefix}/{image_name}"
|
|
376
441
|
else:
|
|
377
442
|
image_full_name = image_name
|
|
378
|
-
build_cmd =
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
target if
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
for
|
|
389
|
-
for item in ("--build-arg", f"{k}={v if v is not None else ''}")
|
|
443
|
+
build_cmd = _build_docker_build_command(
|
|
444
|
+
context_path=context_path,
|
|
445
|
+
dockerfile_path=dockerfile_path,
|
|
446
|
+
build_args=build_args,
|
|
447
|
+
archs=archs,
|
|
448
|
+
no_cache=no_cache,
|
|
449
|
+
plain=plain,
|
|
450
|
+
target=None if omit_target else target,
|
|
451
|
+
tags=[
|
|
452
|
+
*[f"{image_short_name}:{t}" for t in tags_to_use],
|
|
453
|
+
*[f"{image_full_name}:{t}" for t in tags_to_use],
|
|
390
454
|
],
|
|
391
|
-
|
|
392
|
-
",".join(archs) if archs else None,
|
|
393
|
-
"--no-cache" if no_cache else None,
|
|
394
|
-
"--pull" if no_cache else None,
|
|
395
|
-
*[item for t in tags_to_use for item in ("-t", f"{image_short_name}:{t}")],
|
|
396
|
-
]
|
|
397
|
-
for t in tags_to_use:
|
|
398
|
-
build_cmd += ["-t", f"{image_full_name}:{t}"]
|
|
399
|
-
|
|
400
|
-
if plain:
|
|
401
|
-
build_cmd += ["--progress", "plain"]
|
|
455
|
+
)
|
|
402
456
|
utils.run_command(build_cmd)
|
|
403
457
|
delete_temp_file = True
|
|
404
458
|
finally:
|
|
405
459
|
if temp_file_path is not None and delete_temp_file:
|
|
406
|
-
|
|
407
|
-
dockerfile_path.unlink()
|
|
408
|
-
except FileNotFoundError:
|
|
409
|
-
pass
|
|
460
|
+
_safe_unlink(dockerfile_path)
|
|
410
461
|
if temp_dockerignore_created:
|
|
411
|
-
|
|
412
|
-
dockerignore_path.unlink()
|
|
413
|
-
except FileNotFoundError:
|
|
414
|
-
pass
|
|
462
|
+
_safe_unlink(dockerignore_path)
|
|
415
463
|
return version_tag, commit_tag
|
|
416
464
|
|
|
417
465
|
|
|
@@ -427,12 +475,27 @@ def build_extension_image(
|
|
|
427
475
|
single_arch: bool = False,
|
|
428
476
|
extra_build_args: dict[str, str] | None = None,
|
|
429
477
|
) -> tuple[str, str]:
|
|
430
|
-
"""Build an image that starts `FROM` the primary image and returns its tags.
|
|
478
|
+
"""Build an image that starts `FROM` the primary image and returns its version and commit tags.
|
|
479
|
+
|
|
480
|
+
Args:
|
|
481
|
+
base_full_name: The full name of the base image.
|
|
482
|
+
base_version_tag: The version tag of the base image.
|
|
483
|
+
extension_content: The content to append to the Dockerfile.
|
|
484
|
+
context_path: Path to the build context.
|
|
485
|
+
image_name_override: Override for the image name.
|
|
486
|
+
debug: If `True`, build the debug image.
|
|
487
|
+
no_cache: If `True`, do not use cache when building the image.
|
|
488
|
+
plain: If `True`, use plain output mode for the Docker build.
|
|
489
|
+
single_arch: If `True`, build the image for the current host architecture only.
|
|
490
|
+
extra_build_args: Additional build arguments for the Docker build as a dictionary of `KEY: VALUE` pairs.
|
|
491
|
+
|
|
492
|
+
Returns:
|
|
493
|
+
A tuple containing the version tag and commit tag of the built image.
|
|
494
|
+
"""
|
|
431
495
|
|
|
432
|
-
|
|
433
|
-
context_path = Path(".")
|
|
496
|
+
context_path = _resolve_context_path(context_path)
|
|
434
497
|
|
|
435
|
-
|
|
498
|
+
LOGGER.debug(
|
|
436
499
|
"Building extension image based on %s:%s with override name '%s'",
|
|
437
500
|
base_full_name,
|
|
438
501
|
base_version_tag,
|