Skip to content

Commit 1e9b539

Browse files
committed
refactor: use Jinja2 template for Containerfile generation
- Replace list concatenation with Jinja2 template in builder.py - Export shared jinja_env from templates.py for reuse - Simplify generate_containerfile() from 60 to 31 lines
1 parent 718302c commit 1e9b539

2 files changed

Lines changed: 63 additions & 96 deletions

File tree

src/distrobox_plus/utils/builder.py

Lines changed: 57 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,37 @@
1515
generate_hooks_cmd,
1616
generate_install_cmd,
1717
generate_upgrade_cmd,
18+
jinja_env,
1819
)
1920

2021
if TYPE_CHECKING:
2122
from ..container import ContainerManager
2223

24+
# Containerfile template - stages are conditionally included
25+
_CONTAINERFILE_TEMPLATE = jinja_env.from_string("""\
26+
FROM {{ base_image }} AS init
2327
24-
def get_boost_image_name(name: str) -> str:
25-
"""Get the name for a boosted image.
28+
# Marker for boosted image - distrobox-init will skip package setup
29+
RUN touch /.distrobox-boost
2630
27-
Args:
28-
name: Base container name
31+
# Upgrade existing packages
32+
RUN {{ upgrade_cmd }}
2933
30-
Returns:
31-
Image tag with distrobox-plus suffix
32-
"""
33-
# Sanitize name to be a valid image tag
34+
# Install distrobox dependencies (conditional per-package check)
35+
RUN {{ install_cmd }}
36+
37+
{% for stage in stages %}
38+
FROM {{ stage.from }} AS {{ stage.name }}
39+
40+
# {{ stage.comment }}
41+
RUN {{ stage.run }}
42+
43+
{% endfor %}
44+
""")
45+
46+
47+
def get_boost_image_name(name: str) -> str:
48+
"""Get the name for a boosted image."""
3449
safe_name = name.lower().replace("/", "-").replace(":", "-")
3550
return f"{safe_name}:distrobox-plus"
3651

@@ -41,24 +56,9 @@ def get_boost_image_tag(
4156
init_hooks: str = "",
4257
pre_init_hooks: str = "",
4358
) -> str:
44-
"""Generate a unique image tag based on inputs.
45-
46-
Creates a hash-based tag to enable caching of different configurations.
47-
48-
Args:
49-
base_image: Base image name
50-
additional_packages: Space-separated additional packages
51-
init_hooks: Init hooks command
52-
pre_init_hooks: Pre-init hooks command
53-
54-
Returns:
55-
Unique image tag
56-
"""
57-
# Create a hash of the configuration
59+
"""Generate a unique image tag based on inputs."""
5860
content = f"{base_image}|{additional_packages}|{init_hooks}|{pre_init_hooks}"
5961
config_hash = hashlib.sha256(content.encode()).hexdigest()[:12]
60-
61-
# Sanitize base image name
6262
safe_name = base_image.lower().replace("/", "-").replace(":", "-")
6363
return f"{safe_name}-boost:{config_hash}"
6464

@@ -71,83 +71,50 @@ def generate_containerfile(
7171
) -> str:
7272
"""Generate multi-stage Containerfile content for a boosted image.
7373
74-
Creates a multi-stage build with conditional stages:
75-
- Stage 1: init - Always present, installs base dependencies
76-
- Stage 2: pre-hooks - Only if pre_init_hooks provided
77-
- Stage 3: packages - Only if additional_packages provided
78-
- Stage 4: runner - Only if init_hooks provided
79-
80-
Each stage inherits from the previous existing stage, enabling
81-
efficient layer caching by Docker/Podman.
82-
83-
Args:
84-
base_image: Base image to build from
85-
additional_packages: Space-separated additional packages to install
86-
init_hooks: Commands to run at end of init
87-
pre_init_hooks: Commands to run at start of init
88-
89-
Returns:
90-
Containerfile content as string
74+
Creates a multi-stage build with conditional stages that chain together.
9175
"""
92-
lines: list[str] = []
93-
current_stage = "init"
94-
95-
# Stage 1: init (always present)
96-
lines.extend(
97-
[
98-
f"FROM {base_image} AS init",
99-
"",
100-
"# Marker for boosted image - distrobox-init will skip package setup",
101-
"RUN touch /.distrobox-boost",
102-
"",
103-
"# Upgrade existing packages",
104-
f"RUN {generate_upgrade_cmd()}",
105-
"",
106-
"# Install distrobox dependencies (conditional per-package check)",
107-
f"RUN {generate_install_cmd()}",
108-
"",
109-
]
110-
)
76+
# Build stages list dynamically - each stage depends on the previous
77+
stages: list[dict[str, str]] = []
78+
prev_stage = "init"
11179

112-
# Stage 2: pre-hooks (conditional)
11380
if pre_init_hooks:
114-
lines.extend(
115-
[
116-
f"FROM {current_stage} AS pre-hooks",
117-
"",
118-
"# Pre-init hooks",
119-
f"RUN {generate_hooks_cmd(pre_init_hooks)}",
120-
"",
121-
]
81+
stages.append(
82+
{
83+
"from": prev_stage,
84+
"name": "pre-hooks",
85+
"comment": "Pre-init hooks",
86+
"run": generate_hooks_cmd(pre_init_hooks),
87+
}
12288
)
123-
current_stage = "pre-hooks"
89+
prev_stage = "pre-hooks"
12490

125-
# Stage 3: packages (conditional)
12691
if additional_packages:
127-
lines.extend(
128-
[
129-
f"FROM {current_stage} AS packages",
130-
"",
131-
"# Install additional packages",
132-
f"RUN {generate_additional_packages_cmd(additional_packages)}",
133-
"",
134-
]
92+
stages.append(
93+
{
94+
"from": prev_stage,
95+
"name": "packages",
96+
"comment": "Install additional packages",
97+
"run": generate_additional_packages_cmd(additional_packages),
98+
}
13599
)
136-
current_stage = "packages"
100+
prev_stage = "packages"
137101

138-
# Stage 4: runner (conditional)
139102
if init_hooks:
140-
lines.extend(
141-
[
142-
f"FROM {current_stage} AS runner",
143-
"",
144-
"# Init hooks",
145-
f"RUN {generate_hooks_cmd(init_hooks)}",
146-
"",
147-
]
103+
stages.append(
104+
{
105+
"from": prev_stage,
106+
"name": "runner",
107+
"comment": "Init hooks",
108+
"run": generate_hooks_cmd(init_hooks),
109+
}
148110
)
149111

150-
return "\n".join(lines)
112+
return _CONTAINERFILE_TEMPLATE.render(
113+
base_image=base_image,
114+
upgrade_cmd=generate_upgrade_cmd(),
115+
install_cmd=generate_install_cmd(),
116+
stages=stages,
117+
)
151118

152119

153120
def build_image(

src/distrobox_plus/utils/templates.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77

88
from jinja2 import BaseLoader, Environment
99

10-
# Jinja2 environment (singleton)
11-
_env = Environment(loader=BaseLoader(), trim_blocks=True, lstrip_blocks=True)
10+
# Shared Jinja2 environment (singleton) - can be imported by other modules
11+
jinja_env = Environment(loader=BaseLoader(), trim_blocks=True, lstrip_blocks=True)
1212

1313
# Package manager configuration
1414
_PM_CONFIG: dict[str, dict[str, str]] = {
@@ -355,7 +355,7 @@
355355
}
356356

357357
# Pre-compiled Jinja2 templates
358-
_INSTALL_TEMPLATE = _env.from_string("""\
358+
_INSTALL_TEMPLATE = jinja_env.from_string("""\
359359
set -e
360360
{%- for pm, cfg in configs.items() %}
361361
&& if command -v {{ cfg.detect }} > /dev/null 2>&1{% if cfg.exclude %} && ! command -v {{ cfg.exclude }} > /dev/null 2>&1{% endif %}{% if cfg.fallback %} || command -v {{ cfg.fallback }} > /dev/null 2>&1{% endif %}; then \
@@ -365,7 +365,7 @@
365365
{%- endfor %}; fi\
366366
{%- endfor %}""")
367367

368-
_INSTALL_FULL_TEMPLATE = _env.from_string("""\
368+
_INSTALL_FULL_TEMPLATE = jinja_env.from_string("""\
369369
set -e\
370370
{%- for pm, cfg in configs.items() %} && \
371371
if command -v {{ cfg.detect }} > /dev/null 2>&1{% if cfg.exclude %} && ! command -v {{ cfg.exclude }} > /dev/null 2>&1{% endif %}; then \
@@ -374,14 +374,14 @@
374374
fi\
375375
{%- endfor %}""")
376376

377-
_UPGRADE_TEMPLATE = _env.from_string("""\
377+
_UPGRADE_TEMPLATE = jinja_env.from_string("""\
378378
set -e && \
379379
{%- for pm, cfg in configs.items() %}\
380380
{% if not loop.first %}elif {% else %}if {% endif %}\
381381
command -v {{ cfg.detect }} > /dev/null 2>&1; then {{ cfg.upgrade }} || true\
382382
{%- endfor %}; fi""")
383383

384-
_ADDITIONAL_PKG_TEMPLATE = _env.from_string("""\
384+
_ADDITIONAL_PKG_TEMPLATE = jinja_env.from_string("""\
385385
set -e && \
386386
{%- for pm, cfg in configs.items() %}\
387387
{% if not loop.first %}elif {% else %}if {% endif %}\

0 commit comments

Comments
 (0)