Add jinja templating, payload as tar, tests#2
Conversation
constructor/briefcase.py
Outdated
|
|
||
| archive_path = dst / self.archive_name | ||
|
|
||
| with tarfile.open(archive_path, mode="w:gz", compresslevel=1) as tar: |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Could you add some benchmarks to this?
constructor/template_file.py
Outdated
| from .jinja import render_template | ||
|
|
||
|
|
||
| @dataclass(frozen=True) |
There was a problem hiding this comment.
We can utilize this later also for the other installers.
There was a problem hiding this comment.
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).
marcoesters
left a comment
There was a problem hiding this comment.
My initial thoughts on this. Overall, I think we could do with a little less abstraction here.
|
|
||
| rem Truncates the payload to 0 bytes | ||
| type nul > "%PAYLOAD_TAR%" |
There was a problem hiding this comment.
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.
constructor/briefcase.py
Outdated
|
|
||
| archive_path = dst / self.archive_name | ||
|
|
||
| with tarfile.open(archive_path, mode="w:gz", compresslevel=1) as tar: |
There was a problem hiding this comment.
Could you add some benchmarks to this?
tests/test_briefcase.py
Outdated
| def test_payload_templates_are_rendered(): | ||
| """Test that templates are rendered when the payload is prepared.""" | ||
|
|
||
| def assert_no_jinja_markers(path: Path) -> None: |
There was a problem hiding this comment.
That function looks simple enough to be inlined.
constructor/briefcase.py
Outdated
| root: Path | None = None | ||
| archive_name: str = "payload.tar.gz" | ||
| conda_exe_name: str = "_conda.exe" | ||
| rendered_templates: list[TemplateFile] | None = None |
There was a problem hiding this comment.
I don't know if we want to make that distinction. Rendered templates are like any other file we add to the installer payload.
constructor/briefcase.py
Outdated
|
|
||
| return archive_path | ||
|
|
||
| def _convert_into_archive(self, src: Path, dst: Path) -> Path: |
There was a problem hiding this comment.
That function could be folded into make_tar_gz or vice versa
constructor/briefcase.py
Outdated
| """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() |
There was a problem hiding this comment.
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.
constructor/briefcase.py
Outdated
| }, | ||
| } | ||
| # Render the template files and add them to the necessary config field | ||
| rendered_templates = self.render_templates() |
There was a problem hiding this comment.
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.
constructor/template_file.py
Outdated
| @@ -0,0 +1,26 @@ | |||
| from collections.abc import Mapping | |||
There was a problem hiding this comment.
Let's merge the two template functions into one. There doesn't seem to be enough here to justify a separate module.
constructor/template_file.py
Outdated
| from .jinja import render_template | ||
|
|
||
|
|
||
| @dataclass(frozen=True) |
There was a problem hiding this comment.
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).
|
@marcoesters To answer above comments. I just pushed a bunch of commits that did change a lot based on the review, I hope to summarize it:
|
marcoesters
left a comment
There was a problem hiding this comment.
Thank you! I think it's much closer to being ready now.
constructor/briefcase.py
Outdated
| "post_install_script": str(self.rendered_templates["post_install_script"].dst), | ||
| "pre_uninstall_script": str(self.rendered_templates["pre_uninstall_script"].dst), |
There was a problem hiding this comment.
| "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).
constructor/briefcase.py
Outdated
| def make_tar_gz(self, src: Path, dst: Path) -> Path: | ||
| """Create a .tar.gz of the directory 'src'. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
| 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. |
|
@marcoesters Applied the fixes here 3327439 |
|
@marcoesters I think I've addressed all of your comments. Note that this doesn't go into main but into the next (final) branch. |
marcoesters
left a comment
There was a problem hiding this comment.
Once the canary issue is solved, we should take a closer look at logging. Here are some other comments.
constructor/briefcase.py
Outdated
| # There might be other ways we want to enable `add_debug_logging`, but it has proven | ||
| # very useful at least for the CI environment. | ||
| add_debug_logging: bool = bool(os.environ.get("CI")) and os.environ.get("CI") != "0" |
There was a problem hiding this comment.
constructor has a --debug flag. We shouldn't enable debugging on default in the CI because many installers are built and executed in CI environments.
There was a problem hiding this comment.
Wouldn't it be a tedious design if you have to run the tests once to see if there is an error, and then enable this flag and rerun the tests again to see what the underlying issue is? Especially considering that testing times are already very long. Note also that the "additional logging" is only visible to us upon a test failure.
There was a problem hiding this comment.
We should then improve our error reporting to make sure that in general, the underlying issue is clear from the error message. If the information is important, it shouldn't just be part of the debugging output.
There was a problem hiding this comment.
That I fully agree with but this comes back to if we want to log to file by default or not. Perhaps the variable name is not serving its purpose here but this flag enables all the file logging during pre/post uninstall/install.
This is what we discussed earlier about the issue of not having access to anything similar to tee, which is why I at least enforced file logging during the CI environment otherwise there is no way to tell what goes wrong from an error.
There was a problem hiding this comment.
I'd propose we keep the implementation but rename the variable to log_to_file
There was a problem hiding this comment.
- Always log to file 3c91055 (note that there is a small section remaining that relates now to
add_debugbut now the variable name is more in line with what it does: it enables "echo on" and prints some variables to the log. - Generalize log files and move them to
INSTDIR, remove withinpre_uninstall.batif necessary 556705a
constructor/briefcase.py
Outdated
| shutil.rmtree(self.root) | ||
| def make_archive(self, src: Path, dst: Path) -> Path: | ||
| """Create an archive of the directory 'src'. | ||
| The inputs 'src' and 'dst' must both be existing directories. |
There was a problem hiding this comment.
Why do they need to exist already?
There was a problem hiding this comment.
The directory src must exist since that is what we are trying to archive, but I guess dst does not explicitly need to exist, but since it's a public method I requested that as a defensive measure to avoid confusing tarfile or shutil errors, since I thought current implementation will at least raise a clear error message:
if not dst.is_dir():
raise NotADirectoryError(dst)
There was a problem hiding this comment.
I would generally not require that a destination directory exists. It seems like a fragile design.
There was a problem hiding this comment.
26ebcb3 (changed inputs to input in docstring in later commit)
constructor/briefcase.py
Outdated
| templates = [ | ||
| TemplateFile( | ||
| src=BRIEFCASE_DIR / "run_installation.bat", | ||
| dst=self.root / "run_installation.bat", | ||
| ), | ||
| TemplateFile( | ||
| src=BRIEFCASE_DIR / "pre_uninstall.bat", | ||
| dst=self.root / "pre_uninstall.bat", | ||
| ), | ||
| ] |
There was a problem hiding this comment.
I'm not sure we need that information since the file names are all hard-coded. I don't know that we need a separate dataclass for it either since it just contains source and destination.
There was a problem hiding this comment.
"I'm not sure we need that information" - are you referring to the return format of the function or something else?
And I agree that it is optional, the purpose of the current implementation is to be able to verify easily during testing that the templates exist and to avoid hardcoding any file-names more than in this function. I like to think of it as having an ability to check from the payload object what templates we have rendered.
Ideally I'm not 100% with the current design, there are many ways to do this. I'd either actually use the return value somewhere meaningful perhaps in prepare or design this slightly differently. If you have any other suggestions please let me know.
Note also that write_pyproject_toml indirectly depends on this function since it is referencing the files created by render_templates()
There was a problem hiding this comment.
write_pyproject_toml does indirectly depend on the function, but it doesn't use it. I think creating a separate data class just to store source and destination is a little heavy-handed. After all, you only need the locations of the rendered template files.
If you want to return something to facilitate testing, I suggest returning a dictionary with the key being pre-/post-install and the value being the file location.
There was a problem hiding this comment.
8c8cc7b. I returned simply a list, I thought it might be the simplest because we just want to confirm that they have been rendered as expected.
|
|
||
| {%- if add_debug %} | ||
| rem Get the name of the install directory | ||
| for %%I in ("%INSTDIR%") do set "APPNAME=%%~nxI" |
There was a problem hiding this comment.
Has this been tested with %INSTDIR% that ends in a path with spaces?
There was a problem hiding this comment.
Yes, all of the tests actually contain spaces because the install directory is set as "name versionnumber-buildnumber"
Description
This PR is a continuation of conda#1164, but split into a separate PR to hopefully simplify review, summarizing the changes:
post_install.batandpre_uninstall.bat(although this file is currently empty).write_pyproject_tomlinto classPayloadTemplateFileto better organize files that are templated using jinja.Checklist - did you ...
newsdirectory (using the template) for the next release's release notes?