Skip to content

Add jinja templating, payload as tar, tests#2

Open
lrandersson wants to merge 10 commits intodev-ra-798from
dev-ra-798-2
Open

Add jinja templating, payload as tar, tests#2
lrandersson wants to merge 10 commits intodev-ra-798from
dev-ra-798-2

Conversation

@lrandersson
Copy link
Owner

@lrandersson lrandersson commented Feb 5, 2026

Description

This PR is a continuation of conda#1164, but split into a separate PR to hopefully simplify review, summarizing the changes:

  1. Add jinja templating for post_install.bat and pre_uninstall.bat (although this file is currently empty).
  2. Move write_pyproject_toml into class Payload
  3. Add new class TemplateFile to better organize files that are templated using jinja.
  4. Add unit tests

Checklist - did you ...

  • Add a file to the news directory (using the template) for the next release's release notes?
  • Add / update necessary tests?
  • Add / update outdated documentation?


archive_path = dst / self.archive_name

with tarfile.open(archive_path, mode="w:gz", compresslevel=1) as tar:
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I chose tar.gz here over tar.bz2, the reason is that MSI already compresses the contents, therefor the format of the compression does not do much of a difference in terms of disk space. However, .tar.gz is in general a little bit faster over .tar.bz2, so I have intentionally chose speed here over disk space.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add some benchmarks to this?

set "PAYLOAD_TAR=%INSTDIR%\{{ archive_name }}"

"%INSTDIR%\_conda.exe" constructor --prefix "%BASE_PATH%" --extract-conda-pkgs
tar -xzf "%PAYLOAD_TAR%" -C "%INSTDIR%"
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be updated in the future to instead use conda-standalone instead of built-in tar ...

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no time like the present!


# Create a Briefcase configuration file. Using a full TOML writer rather than a Jinja
# template allows us to avoid escaping strings everywhere.
def write_pyproject_toml(tmp_dir, info):
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was not deleted, but moved into class Payload

from .jinja import render_template


@dataclass(frozen=True)
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can utilize this later also for the other installers.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to overly abstract. The name property appears to be too tailor-made (which other format needs it besides MSI) and then we just have a source and destination path. With all my other comments, I'm not sure we need this class.

The render_template_files function could then just handle one file and deal with all the file handling operations (which I think is nice).

Copy link
Collaborator

@marcoesters marcoesters left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My initial thoughts on this. Overall, I think we could do with a little less abstraction here.

set "PAYLOAD_TAR=%INSTDIR%\{{ archive_name }}"

"%INSTDIR%\_conda.exe" constructor --prefix "%BASE_PATH%" --extract-conda-pkgs
tar -xzf "%PAYLOAD_TAR%" -C "%INSTDIR%"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no time like the present!

Comment on lines 18 to 20

rem Truncates the payload to 0 bytes
type nul > "%PAYLOAD_TAR%"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not delete it? That zero-byte file doesn't need to be there until the uninstallation.

There should probably also be a comment about why this is needed.


archive_path = dst / self.archive_name

with tarfile.open(archive_path, mode="w:gz", compresslevel=1) as tar:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add some benchmarks to this?

def test_payload_templates_are_rendered():
"""Test that templates are rendered when the payload is prepared."""

def assert_no_jinja_markers(path: Path) -> None:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That function looks simple enough to be inlined.

root: Path | None = None
archive_name: str = "payload.tar.gz"
conda_exe_name: str = "_conda.exe"
rendered_templates: list[TemplateFile] | None = None
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if we want to make that distinction. Rendered templates are like any other file we add to the installer payload.


return archive_path

def _convert_into_archive(self, src: Path, dst: Path) -> Path:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That function could be folded into make_tar_gz or vice versa

"""Render all configured Jinja templates into the payload root directory.
The set of successfully rendered templates is recorded on the instance and returned to the caller.
"""
root = self._ensure_root()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what I meant about either using __init__ to create the directory or not allowing a None value/non-existing directory. We have to manually add self._ensure_root() in all appropriate places.

},
}
# Render the template files and add them to the necessary config field
rendered_templates = self.render_templates()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should go into prepare like with any other payload preparation. Since we will always have pre_uninstall and pre_install scripts for our installers, we can add those hard-coded into the config object above.

@@ -0,0 +1,26 @@
from collections.abc import Mapping
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's merge the two template functions into one. There doesn't seem to be enough here to justify a separate module.

from .jinja import render_template


@dataclass(frozen=True)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to overly abstract. The name property appears to be too tailor-made (which other format needs it besides MSI) and then we just have a source and destination path. With all my other comments, I'm not sure we need this class.

The render_template_files function could then just handle one file and deal with all the file handling operations (which I think is nice).

@lrandersson
Copy link
Owner Author

@marcoesters To answer above comments.
The requested benchmarking, using a dummy tar with 800 files of 32kB each, below are the results for different compression levels:

cl=1: 0.225s, size=209.1 KiB
cl=3: 0.215s, size=210.1 KiB
cl=6: 0.264s, size=116.9 KiB
cl=9: 0.285s, size=108.3 KiB

I just pushed a bunch of commits that did change a lot based on the review, I hope to summarize it:

  1. Removed template_file.py, merged the rendering functions into one
  2. Using cached properties now for both Payload.root and Payload.rendered_templates. Let me know what you think, this also removes the call to _ensure_root(). I think this looks pretty good and we don't get side-effects from class instantiation (as mentioned in the other PR).
  3. Updated the scripts run_installation.bat and pre_uninstall.bat, we now call conda-standalone instead of `tar.
  4. Folded/merged the make_tar_gz functionality.
  5. Inlined the test utility function to verify we are free from unrendered jinja tokens.

Copy link
Collaborator

@marcoesters marcoesters left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! I think it's much closer to being ready now.

Comment on lines 368 to 369
"post_install_script": str(self.rendered_templates["post_install_script"].dst),
"pre_uninstall_script": str(self.rendered_templates["pre_uninstall_script"].dst),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"post_install_script": str(self.rendered_templates["post_install_script"].dst),
"pre_uninstall_script": str(self.rendered_templates["pre_uninstall_script"].dst),
"post_install_script": layout.root / "post_install.bat",
"pre_uninstall_script": layout.root / "pre_uninstall.bat",

We know where these files go and that location is not configurable, so we might as well hard-code them here. That also gives layout even more of a purpose and makes the TemplateParameter dataclass obsolete and you don't need to store the template files in the class (the fact that they are templates isn't that important, I think).

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied here 3327439

Comment on lines +291 to +292
def make_tar_gz(self, src: Path, dst: Path) -> Path:
"""Create a .tar.gz of the directory 'src'.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would overall be good see a benchmark of tar.gz vs. tar.bz2 in terms of space and extraction speed, especially compared to the native MSI compression. While registry handling becomes much easier with one file, it would be good to say what the cost is.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, will do that separately.

"%INSTDIR%\_conda.exe" install --offline --file "%BASE_PATH%\conda-meta\initial-state.explicit.txt" -yp "%BASE_PATH%"
"%CONDA_EXE%" install --offline --file "%BASE_PATH%\conda-meta\initial-state.explicit.txt" -yp "%BASE_PATH%"

rem Delete the payload to save disk space, a truncated placeholder of 0 bytes is recreated during uninstall
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
rem Delete the payload to save disk space, a truncated placeholder of 0 bytes is recreated during uninstall
rem Delete the payload to save disk space.
rem A truncated placeholder of 0 bytes is recreated during uninstall
rem because MSI expects the file to be there to clean the registry.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied here 3327439

@lrandersson
Copy link
Owner Author

@marcoesters Applied the fixes here 3327439

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants