Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions posit-bakery/posit_bakery/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from posit_bakery.config.repository import Repository
from posit_bakery.config.shared import BakeryPathMixin, BakeryYAMLModel
from posit_bakery.config.templating import TPL_CONTAINERFILE, TPL_BAKERY_CONFIG_YAML
from posit_bakery.config.templating.render import jinja2_env
from posit_bakery.config.templating.render import jinja2_env, normalize_rendered_output
from posit_bakery.config.image.parsed_version import ParsedVersion
from posit_bakery.config.image.posit_product.const import ReleaseStreamEnum
from posit_bakery.const import DEFAULT_BASE_IMAGE, DevVersionInclusionEnum, MatrixVersionInclusionEnum
Expand Down Expand Up @@ -197,7 +197,7 @@ def create_image_files_template(image_path: Path, image_name: str, base_tag: str
tpl = jinja2_env().from_string(TPL_CONTAINERFILE)
rendered = tpl.render(image_name=image_name, base_tag=base_tag)
with open(containerfile_path, "w") as f:
f.write(rendered)
f.write(normalize_rendered_output(rendered))

image_test_path = image_template_path / "test"
if not image_test_path.is_dir():
Expand Down Expand Up @@ -417,14 +417,13 @@ def new(base_path: str | Path | os.PathLike) -> None:
tpl = jinja2_env(loader=jinja2.FileSystemLoader(config_file.parent)).from_string(TPL_BAKERY_CONFIG_YAML)
rendered = tpl.render(repo_url=util.try_get_repo_url(base_path))
with open(config_file, "w") as f:
f.write(rendered)
f.write(normalize_rendered_output(rendered))

def write(self) -> None:
"""Write the bakery config to the config file."""
stream = io.StringIO()
self.yaml.dump(self._config_yaml, stream)
text = re.sub(r"[ \t]+$", "", stream.getvalue(), flags=re.MULTILINE)
self.config_file.write_text(text)
self.config_file.write_text(normalize_rendered_output(stream.getvalue()))

def _get_image_index(self, image_name: str) -> int:
"""Returns the index of the image with the given name in the config.
Expand Down
14 changes: 5 additions & 9 deletions posit-bakery/posit_bakery/config/image/matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from posit_bakery.config.registry import BaseRegistry, Registry
from posit_bakery.config.shared import BakeryPathMixin, BakeryYAMLModel
from posit_bakery.config.templating import jinja2_env
from posit_bakery.config.templating.render import normalize_rendered_output
from posit_bakery.const import JINJA2_TEMPLATE_EXTENSIONS
from posit_bakery.error import BakeryFileError, BakeryRenderError, BakeryTemplateError, BakeryRenderErrorGroup
from .variant import ImageVariant
Expand Down Expand Up @@ -571,11 +572,6 @@ def render_files(self, variants: list[ImageVariant] | None = None, regex_filters
)
continue

# Enable trim_blocks for Containerfile templates
render_kwargs = {}
if tpl_rel_path.startswith("Containerfile"):
render_kwargs["trim_blocks"] = True

# If variants are specified, render Containerfile for each variant
if tpl_rel_path.startswith("Containerfile") and variants:
containerfile_base_name = tpl_rel_path.removesuffix(".jinja2")
Expand All @@ -592,7 +588,7 @@ def render_files(self, variants: list[ImageVariant] | None = None, regex_filters
template_values = self.generate_template_values(variant, containerfile_os)
containerfile: Path = self.path / f"{containerfile_base_name}.{variant.extension}"
try:
rendered = tpl.render(**template_values, **render_kwargs)
rendered = tpl.render(**template_values)
except (jinja2.TemplateError, BakeryTemplateError) as e:
log.error(
f"Failed to render template [bold]{tpl_rel_path}[/] for image '{self.parent.name}' "
Expand All @@ -612,7 +608,7 @@ def render_files(self, variants: list[ImageVariant] | None = None, regex_filters
continue
log.debug(f"[bright_black]Rendering [bold]{containerfile}")
copy2(tpl_full_path, containerfile)
containerfile.write_text(rendered)
containerfile.write_text(normalize_rendered_output(rendered))

# Render other templates once
else:
Expand All @@ -621,7 +617,7 @@ def render_files(self, variants: list[ImageVariant] | None = None, regex_filters
output_file.parent.mkdir(parents=True, exist_ok=True)
template_values = self.generate_template_values()
try:
rendered = tpl.render(**template_values, **render_kwargs)
rendered = tpl.render(**template_values)
except (jinja2.TemplateError, BakeryTemplateError) as e:
log.error(
f"Failed to render template [bold]{tpl_rel_path}[/] for image '{self.parent.name}' matrix"
Expand All @@ -639,7 +635,7 @@ def render_files(self, variants: list[ImageVariant] | None = None, regex_filters
continue
log.debug(f"[bright_black]Rendering [bold]{output_file}")
copy2(tpl_full_path, output_file)
output_file.write_text(rendered)
output_file.write_text(normalize_rendered_output(rendered))

if exceptions:
if len(exceptions) == 1:
Expand Down
14 changes: 5 additions & 9 deletions posit-bakery/posit_bakery/config/image/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from .variant import ImageVariant
from .version_os import ImageVersionOS
from ..templating import jinja2_env
from ..templating.render import normalize_rendered_output
from ...error import BakeryFileError, BakeryRenderError, BakeryTemplateError, BakeryRenderErrorGroup

log = logging.getLogger(__name__)
Expand Down Expand Up @@ -468,11 +469,6 @@ def render_files(
)
continue

# Enable trim_blocks for Containerfile templates
render_kwargs = {}
if tpl_rel_path.startswith("Containerfile"):
render_kwargs["trim_blocks"] = True

# If variants are specified, render Containerfile for each variant
if tpl_rel_path.startswith("Containerfile") and variants:
containerfile_base_name = tpl_rel_path.removesuffix(".jinja2")
Expand All @@ -497,7 +493,7 @@ def render_files(
template_values = self.generate_template_values(variant, containerfile_os)
containerfile: Path = self.path / f"{containerfile_base_name}.{variant.extension}"
try:
rendered = tpl.render(**template_values, **render_kwargs)
rendered = tpl.render(**template_values)
except (jinja2.TemplateError, BakeryTemplateError) as e:
log.error(
f"Failed to render template [bold]{tpl_rel_path}[/] for image '{self.parent.name}' "
Expand All @@ -517,7 +513,7 @@ def render_files(
continue
log.debug(f"[bright_black]Rendering [bold]{containerfile}")
copy2(tpl_full_path, containerfile)
containerfile.write_text(rendered)
containerfile.write_text(normalize_rendered_output(rendered))

# Render other templates once
else:
Expand All @@ -526,7 +522,7 @@ def render_files(
output_file.parent.mkdir(parents=True, exist_ok=True)
template_values = self.generate_template_values()
try:
rendered = tpl.render(**template_values, **render_kwargs)
rendered = tpl.render(**template_values)
except (jinja2.TemplateError, BakeryTemplateError) as e:
log.error(
f"Failed to render template [bold]{tpl_rel_path}[/] for image '{self.parent.name}' "
Expand All @@ -545,7 +541,7 @@ def render_files(
continue
log.debug(f"[bright_black]Rendering [bold]{output_file}")
copy2(tpl_full_path, output_file)
output_file.write_text(rendered)
output_file.write_text(normalize_rendered_output(rendered))

if exceptions:
if len(exceptions) == 1:
Expand Down
18 changes: 18 additions & 0 deletions posit-bakery/posit_bakery/config/templating/render.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,21 @@ def render_template(template: str, **kwargs) -> str:
"""
template = jinja2_env().from_string(template)
return template.render(**kwargs).strip()


def normalize_rendered_output(text: str) -> str:
"""Normalize rendered template output to match common pre-commit hook fixes.

Strips trailing spaces and tabs from each line and ensures the result
ends with exactly one newline (or stays empty if it was empty). This
matches the output of the `trailing-whitespace` and `end-of-file-fixer`
pre-commit hooks, so files written by bakery's renderer don't fail
those hooks in consuming repositories.

:param text: The rendered template text to normalize.
:return: The normalized text.
"""
text = re.sub(r"[ \t]+$", "", text, flags=re.MULTILINE)
if text:
text = text.rstrip("\n") + "\n"
return text
34 changes: 33 additions & 1 deletion posit-bakery/test/config/templating/test_render.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import jinja2
import pytest

from posit_bakery.config.templating.render import jinja2_env
from posit_bakery.config.templating.render import jinja2_env, normalize_rendered_output


pytestmark = [
Expand Down Expand Up @@ -61,3 +61,35 @@ def test_render_template():
template = "{{ 'Test String' | condense }} {{ 'foo-bar-baz' | regexReplace('-', '_') }}"
rendered = jinja2_env().from_string(template).render()
assert rendered == "TestString foo_bar_baz"


class TestNormalizeRenderedOutput:
def test_strips_trailing_spaces(self):
assert normalize_rendered_output("foo \nbar\n") == "foo\nbar\n"

def test_strips_trailing_tabs(self):
assert normalize_rendered_output("foo\t\nbar\n") == "foo\nbar\n"

def test_strips_mixed_trailing_whitespace(self):
assert normalize_rendered_output("foo \t \nbar\n") == "foo\nbar\n"

def test_strips_whitespace_only_lines(self):
assert normalize_rendered_output("a\n \nb\n") == "a\n\nb\n"

def test_collapses_multiple_trailing_newlines(self):
assert normalize_rendered_output("foo\n\n\n") == "foo\n"

def test_adds_missing_trailing_newline(self):
assert normalize_rendered_output("foo\nbar") == "foo\nbar\n"

def test_preserves_single_trailing_newline(self):
assert normalize_rendered_output("foo\nbar\n") == "foo\nbar\n"

def test_empty_input_stays_empty(self):
assert normalize_rendered_output("") == ""

def test_preserves_interior_blank_lines(self):
assert normalize_rendered_output("a\n\nb\n") == "a\n\nb\n"

def test_does_not_touch_leading_whitespace(self):
assert normalize_rendered_output(" indented\n") == " indented\n"
3 changes: 2 additions & 1 deletion posit-bakery/test/config/test_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from posit_bakery.config import BakeryConfig
from posit_bakery.config.templating import TPL_BAKERY_CONFIG_YAML, render_template
from posit_bakery.config.templating.render import normalize_rendered_output
from posit_bakery.const import DEFAULT_BASE_IMAGE
from test.helpers import IMAGE_INDENT, VERSION_INDENT

Expand All @@ -30,7 +31,7 @@ def test_create_from_scratch(tmpdir, common_image_variants):

# Check that the bakery.yaml file was created as expected from its template.
assert config_file.is_file()
expected_config = render_template(TPL_BAKERY_CONFIG_YAML, repo_url=repo_url)
expected_config = normalize_rendered_output(render_template(TPL_BAKERY_CONFIG_YAML, repo_url=repo_url))
assert config_file.read_text() == expected_config

# Load the new project
Expand Down
Loading