Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support app bundles nested in directories #73

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

jonathanj
Copy link

What does this change accomplish?

Allows zip artifacts to contain app bundles that are zipped with a parent directory.

Resolves #32

How have you achieved it?

I decided to modify ArtifactUnpacker, since it seemed like the least intrusive change. If it could get straightened out at this point, then the remainder of Tophat would continue working as expected.

The change implements a heuristic to detect nested app bundles:

  • Search for the shallowest "Info.plist" in the archive, by counting the number of / in the path of each zip entry
  • If one doesn't exist, or the "Info.plist" is already at the root, there is nothing more to do
  • If one does exist, then assume its parent directory is the app bundle root

If a nested app bundle is detected, then the unpacked bundle is moved out of the way (to _tmp, which I'll admit is not the best), and the nested app bundle directory is moved into the original destination.

There are multiple Info.plist files inside an app bundle, and the one that matters for this purpose is the one nearest the root, since it will either be at the root (in which case there is nothing to do), or it'll be at the root of the app bundle itself.

It's possible that I've overlooked something, and improvements would be greatly appreciated.

How can the change be tested?

  1. Produce an app bundle from a build (MyApp.app)
  2. Zip it so that the bundle directory (not the contents) are at the root of the zipfile
    $ zip -r MyApp.zip ./MyApp.app
    # Verify that the zip contains the bundle directory, not just the contents
    $ unzip -t MyApp.zip
    Archive:  MyApp.zip
     testing: MyApp.app/         OK
     …
  3. Install the zip with Tophat, previously it would fail with "This application canʼt be installed"

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
It is possible, using a heuristic, to move nested app bundles to the
root of the working directory when installing an artifact, and prevent
an installation error.

The heuristic can be outlined as follows:

- Search for the shallowest "Info.plist" in the archive
- If one doesn't exist, or the "Info.plist" is already at the root,
  there is nothing more to do
- If one does exist, then assume its parent directory is the app bundle root
@jonathanj
Copy link
Author

I have signed the CLA!

@lfroms lfroms self-requested a review March 11, 2025 17:23
Copy link
Member

@lfroms lfroms left a comment

Choose a reason for hiding this comment

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

Thanks @jonathanj, just one question.

Comment on lines +86 to +93
if let appBundleRoot = try? zipArtifactApplicationBundleRoot(at: url) {
log.info("Nested application bundle detected")
// Move the original destination to "_tmp", then move the nested app bundle to the destination.
let workingDestinationURL = url.deletingLastPathComponent().appending(path: "_tmp")
try FileManager.default.moveItem(at: destinationURL, to: workingDestinationURL)
let nestedAppBundleRootURL = workingDestinationURL.appending(path: appBundleRoot)
try FileManager.default.moveItem(at: nestedAppBundleRootURL, to: destinationURL)
}
Copy link
Member

@lfroms lfroms Mar 11, 2025

Choose a reason for hiding this comment

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

I wonder if this logic might be a bit too complex for what we need—is there any reason why we can't simply check for Info.plist at the root of the zip, then check for Info.plist inside the first nested folder? We could probably just throw an error if it doesn't exist in either of those locations.

I'm wondering how often the structure of the zip would actually be more complicated than that. For applications built with standard tooling you would probably only ever find the .app at 0 or 1 levels deep.

Copy link
Author

@jonathanj jonathanj Mar 12, 2025

Choose a reason for hiding this comment

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

Hi @lfroms, it's a good question, let me try answer it:

"First" (quoted because zip archives have no well-defined ordering) may not necessarily be the correct app folder. For example, if the archive contains a metadata folder (e.g. screenshots, or a dot-folder) then the first folder may unluckily be the wrong folder. Regardless, finding the first folder involves scanning the contents of the zip archive, at this stage the decision is whether you blindly accept the first folder or try to find the right folder (I decided that was one containing an Info.plist).

Once you're already iterating the zip archive looking for the right first folder, the logic isn't much less complicated than doing it for nested folders, and I thought it might be prudent to avoid the reoccurence of the "Tophat doesn't work with app bundles nested N+1 levels deep" issues.

Hopefully this gives you some insight into my journey to get to this point.

Copy link
Member

Choose a reason for hiding this comment

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

I'm not super convinced that we would often encounter a structure other than at the 0th or 1st level. Right-clicking and compressing in Finder or Xcode's default output yields one level of nesting, or zipping the contents yields 0 levels of nesting. I think that should be enough to cover 99% of use cases.

Could we update the logic to match that?

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.

Unable to handle application bundles zipped with a parent directory
2 participants