Skip to content

MSI: Reorganize payload preparation#1164

Open
lrandersson wants to merge 3 commits intoconda:briefcase-integrationfrom
lrandersson:dev-ra-798
Open

MSI: Reorganize payload preparation#1164
lrandersson wants to merge 3 commits intoconda:briefcase-integrationfrom
lrandersson:dev-ra-798

Conversation

@lrandersson
Copy link
Contributor

@lrandersson lrandersson commented Feb 4, 2026

Description

This PR proposes a class Payload to 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 ...

  • 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?

@lrandersson lrandersson requested a review from a team as a code owner February 4, 2026 15:55
@lrandersson lrandersson changed the base branch from main to briefcase-integration February 4, 2026 15:59
@github-project-automation github-project-automation bot moved this to 🆕 New in 🔎 Review Feb 4, 2026
@conda-bot conda-bot added the cla-signed [bot] added once the contributor has signed the CLA label Feb 4, 2026
@lrandersson
Copy link
Contributor Author

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).

def create(info, verbose=False):
if not IS_WINDOWS:
raise Exception(f"Invalid platform '{sys.platform}'. MSI installers require Windows.")
@dataclass(frozen=True)
Copy link
Contributor

Choose a reason for hiding this comment

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

Why use a dataclass over NamedTuple?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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:

  1. I'd use a NamedTuple when the thing is a tuple.
  2. I'd use frozen dataclass when the thing represents a thing.

Copy link
Contributor

Choose a reason for hiding this comment

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

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.

Comment on lines +283 to +284
def remove(self) -> None:
shutil.rmtree(self.root)
Copy link
Contributor

Choose a reason for hiding this comment

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

Should there be a guard in case the directory doesn't exist (or an ignore_errors=True)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

By the way I have now guards for this since Payload.root is now a cached property in lrandersson#2

Comment on lines +289 to +292
def _ensure_root(self) -> Path:
if self.root is None:
self.root = Path(tempfile.mkdtemp())
return self.root
Copy link
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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.

Comment on lines +279 to +281
self._stage_dists(layout)
self._stage_conda(layout)
return layout
Copy link
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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).

Comment on lines 295 to 300
"""The layout is created as:
root/
└── external/
└── base/
└── pkgs/
"""
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this is a good opportunity to elaborate a little more on why this directory structure, specifically the intermediary external directory is needed.

Copy link
Contributor Author

@lrandersson lrandersson Feb 6, 2026

Choose a reason for hiding this comment

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

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.

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

Labels

cla-signed [bot] added once the contributor has signed the CLA

Projects

Status: 🆕 New

Development

Successfully merging this pull request may close these issues.

3 participants