MSI: Reorganize payload preparation#1164
MSI: Reorganize payload preparation#1164lrandersson merged 46 commits intoconda:briefcase-integrationfrom
Conversation
|
I'm tagging @marcoesters here because I assume you will review this out of the possible reviewers. I just wanted to inform you that there is an additional PR that depends on this one, that also is in need of review. lrandersson#2, this PR is on my own fork because I need to merge that PR into this one (PR 2 -> PR 1164). |
constructor/briefcase.py
Outdated
| def create(info, verbose=False): | ||
| if not IS_WINDOWS: | ||
| raise Exception(f"Invalid platform '{sys.platform}'. MSI installers require Windows.") | ||
| @dataclass(frozen=True) |
There was a problem hiding this comment.
Why use a dataclass over NamedTuple?
There was a problem hiding this comment.
I'd say that they are very similar but in my opinion this more comes down to how we use this. The class PayloadLayout represents one thing, and we can add methods to it as well. I tried to keep it minimal for presentation. For a tuple also positional meaning is primary, but for the frozen data class the fields are primary.
There was a problem hiding this comment.
I'm suggesting a NamedTuple, not a tuple. A NamedTuple also goes by fields and is immutable. I don't see that we will need methods for a structure that just describes a layout.
There was a problem hiding this comment.
I see, what would the gain be to use a NamedTuple over a dataclass in this situation? I agree that we still get the data organized in a similar fashion but they signal different intent, and my thought process is:
- I'd use a
NamedTuplewhen the thing is a tuple. - I'd use frozen dataclass when the thing represents a thing.
There was a problem hiding this comment.
Overall, a NamedTuple is a smaller object. I use a NamedTuple when I just need a simple immutable data container. However, I'm still not fully convinced we need this (yet). See my comment on the perpare function below.
constructor/briefcase.py
Outdated
| def remove(self) -> None: | ||
| shutil.rmtree(self.root) |
There was a problem hiding this comment.
Should there be a guard in case the directory doesn't exist (or an ignore_errors=True)?
There was a problem hiding this comment.
I thought about it, but, I think it would make the most sense that the one who implements the interface is the one who should care about potential errors. Silently catching/ignoring errors here could lead to unexpected behavior.
There was a problem hiding this comment.
By the way I have now guards for this since Payload.root is now a cached property in lrandersson#2
constructor/briefcase.py
Outdated
| def _ensure_root(self) -> Path: | ||
| if self.root is None: | ||
| self.root = Path(tempfile.mkdtemp()) | ||
| return self.root |
There was a problem hiding this comment.
This should be part of __init__ or we shouldn't allow None root. I don't see an advantage having a potentially "unbound" root directory.
There was a problem hiding this comment.
That is an option, the reason why I have done as above is to avoid filesystem side effects upon class instantiation. The caller ideally only need to use this as:
payload = Payload(info)
payload.prepare()
However, if we are fine with above the side effects, let me know and I will update it. It is a minor thing to change in terms of code.
constructor/briefcase.py
Outdated
| self._stage_dists(layout) | ||
| self._stage_conda(layout) | ||
| return layout |
There was a problem hiding this comment.
This part makes me think that PayloadLayout may be overkill. _stage_dists and _stage_conda only use one element of PayloadLayout and only root is used outside of that function, so the entire object doesn't need to be returned.
Why not combine _create_layout and prepare into one function and remove the PayloadLayout data class? This seems simpler for now.
If we find that we want to use this class later for other installers, we can create these interfaces later.
There was a problem hiding this comment.
I see your point, but I think it'll be a bit more "clear" after reviewing the follow-up PR https://github.com/lrandersson/constructor/pull/2/changes where more code has been added.
I like to think about it this way, alternative 1 (which we are very close to in the PR I linked):
self.render_templates(layout)
self.write_pyproject_toml(layout)
self.archive(layout)
self._stage_dists(layout)
self._stage_conda(layout)
Compared to alternative 2:
external = root / "external"
base = external / "base"
pkgs = base / "pkgs"
self.render_templates(root, external)
self.write_pyproject_toml(root, external)
self.archive(base, external)
self._stage_dists(pkgs)
self._stage_conda(external)
And if for some reason we need to add more directories as arguments to any of the functions, one does not need to change the function definitions, only the implementation of each function.
To me it doesn't seem overkill but for maintenance and ease of implementation, alternative 1 is much simpler. Please let me know your thoughts.
There was a problem hiding this comment.
Alternative 1 already lists a few MSI-specific function calls, like preparing the pyproject.toml file (which is technically not part of the installer payload anyway). The SH format structures its payload differently (preconda, postconda), and EXE installers have temp_extra_files.
I am overall in favor of a more object-oriented way rather than the current scripting appraoch constructor have, and alternative 1 looks reasonable at first glance.
However, I am concerned about introducing premature abstraction now that we may have to undo later. I think for now, alternative 2 is good enough for MSI. When there is earnest effort in making constructor more OOP-based, we can take a holistic look at all payloads and find what an appropriate level of abstraction is.
There was a problem hiding this comment.
I see your point. Do you mind if we take this action as the last thing after merging lrandersson#2 into this PR? That way we can also see a simple diff between the two layouts given the current implementation.
(It will also be easier for me to merge the second PR that way).
There was a problem hiding this comment.
@marcoesters I've now added a commit that basically is the same functionality but without the class PayloadLayout for comparison: 6af0910, let me know what you think.
There was a problem hiding this comment.
Ping @marcoesters this should be the last open thread of this PR
There was a problem hiding this comment.
I can't say that I miss the PayloadLayout class, honestly.
constructor/briefcase.py
Outdated
| """The layout is created as: | ||
| root/ | ||
| └── external/ | ||
| └── base/ | ||
| └── pkgs/ | ||
| """ |
There was a problem hiding this comment.
I think this is a good opportunity to elaborate a little more on why this directory structure, specifically the intermediary external directory is needed.
There was a problem hiding this comment.
Added here c4b11e8, note that there is also a path to external in the pyproject.toml that is generated, but I didn't explicitly mention that in the docstring.
MSI: Update uninstall scripts
Add jinja templating, payload as tar, tests
| "%CONDA_EXE%" --log-file "%LOG%" constructor uninstall --prefix "%BASE_PATH%" | ||
| if errorlevel 1 ( exit /b %errorlevel% ) | ||
|
|
||
| rem If we reached this far without any errors, remove any log-files. |
There was a problem hiding this comment.
| rem If we reached this far without any errors, remove any log-files. | |
| rem If we reached this far without any errors, remove any log files. |
constructor/briefcase.py
Outdated
| def remove(self, *, ignore_errors: bool = True) -> None: | ||
| """Remove the root of the payload. | ||
|
|
||
| This function requires some extra care due to the root being a cached property. |
There was a problem hiding this comment.
| This function requires some extra care due to the root being a cached property. | |
| This function requires some extra care due to the root directory being a cached property. |
| try: | ||
| delattr(self, "root") | ||
| except Exception: | ||
| # delattr on a cached_property may raise on some versions / edge cases |
There was a problem hiding this comment.
For example? And why is passing the appropriate course of action?
There was a problem hiding this comment.
There are a few ways to look at it:
- If this fails, the temporary directory is already removed which is the primary goal.
- The
payload.remove()function here is intended to be called as the final step (currently insidecreate(...))
It is likely that we could skip the entiredelattrshenanigan, but to me it looks more complete to avoid a reference to a directory that might not exist.
I was also thinking of writingPayloadto be used as a context-manager to achieve automatic clean-up but I thought it might be a bit overkill right now.
| {{ error_block('Failed to create "%PAYLOAD_TAR%"', '%errorlevel%') }} | ||
| ) | ||
|
|
||
| "%CONDA_EXE%" --log-file "%LOG%" constructor uninstall --prefix "%BASE_PATH%" |
There was a problem hiding this comment.
Since this assumes the --log-file feature, I think we need to add a check to ensure that it exists. That information exists in info, we just need to raise when it's False
9f0b620
into
conda:briefcase-integration
* Reorganize payload into a class and add tests * Ensure new tests are Windows only * Add jinja templating, payload as tar, tests * Update docstring * Update briefcase.py * Inline test utility function * Remove payload tar, update pre_uninstall.bat * Use conda-standalone for extracting tar * Merge archive functions, update root as cached property * Remove template_file.py improve handling of templates * Add missing .dst * Remove compresslevel arg * Review fixes * Fix typo in file name causing build errors * Dynamically set archive type from file name * Rename class function and update docstring * Update uninstallation scripts * Update pre_uninstall.bat * Add logging * Improve log handling for msi tests * Add register_envs * Fix syntax error with remove * Ensure .exe test not running for MSI * Properly disable test for MSI * Add more tests and another check errorlevel * Make logging more neat * Removed all use of 'sanity' * Updated test for MSI (remove pytest.skip) * Update to use CLI for newer conda-standalone * Fix missing quote and properly use --log-file * Docstring formatting * pre-commit fix * Hopefully fix issue with conda-standalone canary * Fix typo in workflow * Automatically create 'dst' * Remove TemplateFile * Fix docstring * FIx pre-commit * Always log to file * Generalize install/uninstall logs and move into INSTDIR * pre-commit fix * Remove PayloadLayout * Fix remaining syntax error * Review fixes
Description
This PR proposes a class
Payloadto organize the payload preparation.I still think more work can be done on this but I tried to keep the changes minimal to simplify the review.
I have also added a couple of unit tests to demonstrate what I think is the biggest gain of using this structure over the old one.
I think this class can be extracted later and also be used for other installers, since we can make it possible (if needed) to override the function that creates the payload layout.
For the review, it might be easier to also open the whole file via "View file" and not only browsing from the "Files changed" tab.
Checklist - did you ...
newsdirectory (using the template) for the next release's release notes?