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

Add support for installing portables from a zip #2500

Merged
merged 23 commits into from
Sep 27, 2022

Conversation

ryfu-msft
Copy link
Contributor

@ryfu-msft ryfu-msft commented Sep 6, 2022

Resolves #140
Resolves #2523

This PR adds functionality for supporting the installation of portable(s) from a zip archive.

Changes:

  • PortableEntry has been removed and replaced with PortableInstaller, which encapsulates the state and logic flow for handling a portable install/uninstall.
  • Portables from a zip are extracted to a temporary folder. If the NestedInstallerType is a portable, those extracted files are moved to the appropriate install location during the install flow.
  • The implementation for installing and uninstall portables is now based on the expected and desired state, which are determined prior to the installation phase. The expected state will first be examined to determine which files need to be cleaned up. The desired state will then be examined to place down the correct files. This implementation will be optimized and improved on in a separate PR by determining and applying the differences in the expected and desired state.
  • Uninstall will now check that all exe and symlinks that are created have not been modified and can be safely removed prior to starting the uninstall flow. This is to ensure that we do not partially uninstall the package if a file does not match what is recorded in the index. This check can be overridden with the --force argument.

Tests:

  • E2E tests to verify that install, update, and uninstall work as expected for portable in zip.
  • Unit tests to ensure that all files recorded in the index are correctly verified prior to uninstall.
  • Added more granular tests for verifying portable installation (moving exe, creating symlink, and adding to PATH).
Microsoft Reviewers: Open in CodeFlow

@ryfu-msft ryfu-msft requested a review from a team as a code owner September 6, 2022 23:47
@ghost ghost added the Issue-Feature This is a feature request for the Windows Package Manager client. label Sep 6, 2022
src/AppInstallerCLICore/PortableInstaller.cpp Outdated Show resolved Hide resolved
src/AppInstallerCLICore/PortableInstaller.cpp Show resolved Hide resolved
src/AppInstallerCLICore/PortableInstaller.cpp Outdated Show resolved Hide resolved
src/AppInstallerCLICore/PortableInstaller.cpp Outdated Show resolved Hide resolved
src/AppInstallerCLICore/PortableInstaller.cpp Outdated Show resolved Hide resolved
src/AppInstallerCLICore/PortableInstaller.h Show resolved Hide resolved
src/AppInstallerCLICore/PortableInstaller.h Outdated Show resolved Hide resolved
src/AppInstallerRepositoryCore/Microsoft/PortableIndex.cpp Outdated Show resolved Hide resolved
src/AppInstallerCLICore/PortableInstaller.cpp Outdated Show resolved Hide resolved
src/AppInstallerCLICore/PortableInstaller.cpp Outdated Show resolved Hide resolved
src/AppInstallerCLICore/PortableInstaller.cpp Outdated Show resolved Hide resolved
src/AppInstallerCLICore/PortableInstaller.cpp Outdated Show resolved Hide resolved
src/AppInstallerCLICore/PortableInstaller.cpp Outdated Show resolved Hide resolved
src/AppInstallerRepositoryCore/Microsoft/PortableIndex.h Outdated Show resolved Hide resolved
src/AppInstallerCLICore/PortableInstaller.cpp Show resolved Hide resolved
}
}

HRESULT PortableInstaller::InstallSingle(const std::filesystem::path& installerPath)
Copy link
Member

Choose a reason for hiding this comment

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

I don't think splitting single/multiple is necessarily a good idea from a maintainability standpoint. It isn't really necessary either given what I'm going to suggest in this rather long comment.

Any action (install/upgrade/uninstall) taken on the portable file(s) can be described by 3 states: { Desired, Expected, Actual }

  • Desired contains the incoming file(s)
  • Expected contains the description of the file(s) that we maintain (registry or index)
  • Actual is the state of the filesystem

Every filesystem entry that we want can be described in the desired state, and each one has a related state in the expected and actual sets (even if it isn't in the set in memory). This includes the root install directory for the package, which should not be treated as a special case. Each filesystem entry can also be acted on individually, so long as they are properly ordered.

If you use this model, there is no install/upgrade/uninstall distinction in the core code. Those APIs can still be presented, but they are used to create the desired state, which is then passed to the resolution engine. That code can be clear and consistent because it needs only handle one filesystem entry at a time. Each entry can be properly recorded before it committed to disk, or properly removed before removing the recorded entry.

Additionally, creating an abstraction over the single/multiple file state storage seems like a better way to keep the core code clean and in one place. The desired state can easily be inspected to determine if it meets the criteria for storing in the registry (contains exactly one directory, file, and link) or not. The correct interface implementation can be created and not need to have multiple code paths.

InstallerSha256: <ZIPHASH>
NestedInstallerType: portable
NestedInstallerFiles:
- RelativeFilePath: AppInstallerTestExeInstaller.exe
Copy link
Contributor

Choose a reason for hiding this comment

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

There should be some tests which consider two entries with the same RelativeFilePath, Two entries with the same PortableCommandAlias, and possibly Exact duplicate entries?

I was doing some testing and I don't think that these cases are being validated against at all. (#2523)
Perhaps @denelon can provide his thoughts on if these validations would be required

@Trenly
Copy link
Contributor

Trenly commented Sep 17, 2022

Is the output of the files removed intentional?

PS D:\Git\winget-pkgs> wingetdev install -m D:\Git\winget-pkgs\manifests\c\Codex\ffmpeg\essentials\5.1.1
This application is licensed to you by its owner.
Microsoft is not responsible for, nor does it grant any licenses to, third-party packages.
Downloading https://www.gyan.dev/ffmpeg/builds/packages/ffmpeg-5.1.1-essentials_build.zip
  ██████████████████████████████  78.9 MB / 78.9 MB
Successfully verified installer hash
Starting package install...
Successfully installed
PS D:\Git\winget-pkgs> winget list ffmpeg
Name              Id                                     Version
----------------------------------------------------------------
ffmpeg essentials Codex.ffmpeg.essentials__DefaultSource 5.1.1
PS D:\Git\winget-pkgs> wingetdev uninstall ffmpeg  
No installed package found matching input criteria.
PS D:\Git\winget-pkgs> wingetdev uninstall 'ffmpeg essentials'
Found ffmpeg essentials [Codex.ffmpeg.essentials__DefaultSource]
Starting package uninstall...
"C:\\Users\\Trenly\\AppData\\Local\\Microsoft\\WinGet\\Packages\\Codex.ffmpeg.essentials__DefaultSource\\ffmpeg-5.1.1-essentials_build"
"C:\\Users\\Trenly\\AppData\\Local\\Microsoft\\WinGet\\Links\\ffmpeg.exe"
"C:\\Users\\Trenly\\AppData\\Local\\Microsoft\\WinGet\\Links\\ffplay.exe"
"C:\\Users\\Trenly\\AppData\\Local\\Microsoft\\WinGet\\Links\\ffprobe.exe"
Successfully uninstalled

@vedantmgoyal9
Copy link
Contributor

vedantmgoyal9 commented Sep 17, 2022

Is the output of the files removed intentional?

I don't know if it is there for debugging purposes, but it should be written to log instead of stdout.

WinGet-2022-09-17-21-50-32.732.log

// TODO: For portables, extract portables to final install location and log to local database.
HRESULT hr = AppInstaller::Archive::TryExtractArchive(installerPath, installerParentPath);
AICLI_LOG(CLI, Info, << "Extracting archive to: " << installerParentPath);
AICLI_LOG(CLI, Info, << "Extracting archive to: " << destinationFolder);
Copy link
Contributor

Choose a reason for hiding this comment

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

In testing this, it feels like perhaps there should be output written to inform the user the archive is being extracted. I noticed with particularly large archives there appeared to be significant delay between Successfully verified installer hash and Starting package install. . .

Copy link
Member

Choose a reason for hiding this comment

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

Agreed; the extraction should be part of the install phase, not the download.

Copy link
Member

Choose a reason for hiding this comment

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

We may also be able to have progress callbacks for the extraction, and certainly can for the progress of moving files to their target location.

src/AppInstallerCLICore/PortableInstaller.h Outdated Show resolved Hide resolved

RemoveInstallDirectory();

RemoveFromPathVariable();
Copy link
Member

Choose a reason for hiding this comment

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

Why not remove it from path first, to keep the flow fully in the opposite order?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The path should only be removed if the directory added to path is empty. That can change depending on whether the user wants to purge the install directory as checked in RemoveInstallDirectory or if there are leftover files after applying the desired state. Even though the flow is not fully in the opposite order, I think this flow ensures that we don't accidentally remove from path if other portables depend on it.


void PortableInstaller::SetExpectedState()
{
const auto& indexPath = InstallLocation / GetPortableIndexFileName();
Copy link
Member

Choose a reason for hiding this comment

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

Is the file name now package specific to enable multiple portables to target the same 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.

Yes, GetPortableIndexFileName returns the package specific filename which is now the productCode + ".db" extension

}
}

void PortableInstaller::ApplyDesiredState()
Copy link
Member

Choose a reason for hiding this comment

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

So while this has the structure of what I was pushing for (a single function that applies the state), this is a very inefficient implementation. I was hoping that we would be able to apply the differences rather than removing everything and putting all the files back. That isn't strictly necessary; we could have a follow-up PR at some point to handle that. It is possible that we will find it necessary to do before shipping for real depending on the performance/size of the archives that people are using.

Additionally, while it also isn't necessary to do it this way, it would be cleaner if the index/no index was decided once by creating an object that implemented the necessary interface. That way you just throw values at it and it decides where to put them, rather than having if blocks sprinkled throughout.

@ghost ghost added the Issue-Bug It either shouldn't be doing this or needs an investigation. label Sep 27, 2022
@ryfu-msft ryfu-msft merged commit e0ac965 into microsoft:master Sep 27, 2022
@ryfu-msft ryfu-msft linked an issue Sep 28, 2022 that may be closed by this pull request
@SeanFeldman
Copy link

@ryfu-msft, is there documentation on how to prepare WinGet zip-based manifest? Thank you.

@Trenly
Copy link
Contributor

Trenly commented Oct 11, 2022

@ryfu-msft, is there documentation on how to prepare WinGet zip-based manifest? Thank you.

I have a few examples on my fork - https://github.com/Trenly/winget-pkgs/branches/all?query=zips

YamlCreate.ps1 also supports Zip, but you have to enable developer options and override the manifest version to 1.4.0 using the YamlCreate settings. Just be aware that the community repository won't start accepting zip based manifests until the 1.4 client is in a stable release and has a high percentage of adoption

@denelon
Copy link
Contributor

denelon commented Oct 11, 2022

We're going to update wingetcreate as well so it can also create manifests for .zip based packages.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Issue-Bug It either shouldn't be doing this or needs an investigation. Issue-Feature This is a feature request for the Windows Package Manager client.
Projects
None yet
6 participants