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

Fix PATH issue for archive containing nested portables with binary dependencies #4816

Merged
merged 5 commits into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions schemas/JSON/manifests/v1.9.0/manifest.installer.1.9.0.json
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@
"maxLength": 2048,
"description": "Custom switches will be passed directly to the installer by winget"
},
"Repair" : {
"Repair": {
"type": [ "string", "null" ],
"minLength": 1,
"maxLength": 512,
Expand Down Expand Up @@ -587,7 +587,7 @@
"type": [ "boolean", "null" ],
"description": "Indicates whether the installer is prohibited from being downloaded for offline installation."
},
"RepairBehavior": {
"RepairBehavior": {
"type": [ "string", "null" ],
"enum": [
"modify",
Expand All @@ -596,6 +596,10 @@
],
"description": "The repair method"
},
"ArchiveBinariesDependOnPath": {
"type": [ "boolean", "null" ],
"description": "Indicates whether the install location should be added directly to the PATH environment variable. Only applies to an archive containing portable packages."
},
"Installer": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -716,6 +720,9 @@
},
"RepairBehavior": {
"$ref": "#/definitions/RepairBehavior"
},
"ArchiveBinariesDependOnPath": {
"$ref": "#/definitions/ArchiveBinariesDependOnPath"
}
},
"required": [
Expand Down Expand Up @@ -835,6 +842,9 @@
"RepairBehavior": {
"$ref": "#/definitions/RepairBehavior"
},
"ArchiveBinariesDependOnPath": {
"$ref": "#/definitions/ArchiveBinariesDependOnPath"
},
"Installers": {
"type": "array",
"items": {
Expand Down Expand Up @@ -863,4 +873,4 @@
"ManifestType",
"ManifestVersion"
]
}
}
12 changes: 11 additions & 1 deletion schemas/JSON/manifests/v1.9.0/manifest.singleton.1.9.0.json
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,10 @@
],
"description": "The repair method"
},
"ArchiveBinariesDependOnPath": {
"type": [ "boolean", "null" ],
"description": "Indicates whether the install location should be added directly to the PATH environment variable. Only applies to an archive containing portable packages."
},
"Installer": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -815,6 +819,9 @@
},
"RepairBehavior": {
"$ref": "#/definitions/RepairBehavior"
},
"ArchiveBinariesDependOnPath": {
"$ref": "#/definitions/ArchiveBinariesDependOnPath"
}
},
"required": [
Expand Down Expand Up @@ -1057,6 +1064,9 @@
"RepairBehavior": {
"$ref": "#/definitions/RepairBehavior"
},
"ArchiveBinariesDependOnPath": {
"$ref": "#/definitions/ArchiveBinariesDependOnPath"
},
"Installers": {
"type": "array",
"items": {
Expand Down Expand Up @@ -1090,4 +1100,4 @@
"ManifestType",
"ManifestVersion"
]
}
}
11 changes: 10 additions & 1 deletion src/AppInstallerCLICore/PortableInstaller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,16 @@ namespace AppInstaller::CLI::Portable
}
else if (entry.FileType == PortableFileType::Symlink)
{
if (!InstallDirectoryAddedToPath)
if (BinariesDependOnPath && !InstallDirectoryAddedToPath)
{
// Scenario indicated by 'ArchiveBinariesDependOnPath' manifest entry.
// Skip symlink creation for portables dependent on binaries that require the install directory to be added to PATH.
std::filesystem::path installDirectory = std::filesystem::path(entry.SymlinkTarget).parent_path();
AddToPathVariable(installDirectory);
AICLI_LOG(Core, Info, << "Install directory added to PATH: " << installDirectory);
CommitToARPEntry(PortableValueName::InstallDirectoryAddedToPath, InstallDirectoryAddedToPath = true);
}
else if (!InstallDirectoryAddedToPath)
{
std::filesystem::file_status status = std::filesystem::status(filePath);
if (std::filesystem::is_directory(status))
Expand Down
3 changes: 2 additions & 1 deletion src/AppInstallerCLICore/PortableInstaller.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ namespace AppInstaller::CLI::Portable
std::string WinGetPackageIdentifier;
std::string WinGetSourceIdentifier;
bool InstallDirectoryCreated = false;
bool BinariesDependOnPath = false;
// If we fail to create a symlink, add install directory to PATH variable
bool InstallDirectoryAddedToPath = false;

Expand Down Expand Up @@ -112,4 +113,4 @@ namespace AppInstaller::CLI::Portable
void AddToPathVariable(const std::filesystem::path& value);
void RemoveFromPathVariable(const std::filesystem::path& value);
};
}
}
11 changes: 8 additions & 3 deletions src/AppInstallerCLICore/Workflows/PortableFlow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -135,12 +135,18 @@ namespace AppInstaller::CLI::Workflow
}
}

Utility::Architecture arch = context.Get<Execution::Data::Installer>()->Arch;
const auto& installer = context.Get<Execution::Data::Installer>().value();
Utility::Architecture arch = installer.Arch;
const std::string& productCode = GetPortableProductCode(context);

PortableInstaller portableInstaller = PortableInstaller(scope, arch, productCode);
portableInstaller.IsUpdate = isUpdate;

if (IsArchiveType(installer.BaseInstallerType) && installer.ArchiveBinariesDependOnPath)
{
portableInstaller.BinariesDependOnPath = true;
}

// Set target install directory
std::string_view locationArg = context.Args.GetArg(Execution::Args::Type::InstallLocation);
std::filesystem::path targetInstallDirectory;
Expand Down Expand Up @@ -239,7 +245,6 @@ namespace AppInstaller::CLI::Workflow
void PortableInstallImpl(Execution::Context& context)
{
PortableInstaller& portableInstaller = context.Get<Execution::Data::PortableInstaller>();

try
{
context.Reporter.Info() << Resource::String::InstallFlowStartingPackageInstall << std::endl;
Expand Down Expand Up @@ -324,4 +329,4 @@ namespace AppInstaller::CLI::Workflow
EnsureVolumeSupportsReparsePoints;
}
}
}
}
10 changes: 6 additions & 4 deletions src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs
Original file line number Diff line number Diff line change
Expand Up @@ -354,13 +354,15 @@ public static string GetCheckpointsDirectory()
/// <param name="productCode">Product code.</param>
/// <param name="shouldExist">Should exists.</param>
/// <param name="scope">Scope.</param>
/// <param name="installDirectoryAddedToPath">Install directory added to path instead of the symlink directory.</param>
public static void VerifyPortablePackage(
string installDir,
string commandAlias,
string filename,
string productCode,
bool shouldExist,
Scope scope = Scope.User)
Scope scope = Scope.User,
bool installDirectoryAddedToPath = false)
{
// When portables are installed, if the exe path is inside a directory it will not be aliased
// if the exe path is at the root level, it will be aliased. Therefore, if either exist, the exe exists
Expand All @@ -387,7 +389,7 @@ public static void VerifyPortablePackage(
{
string pathName = "Path";
var currentPathValue = (string)environmentRegistryKey.GetValue(pathName);
var portablePathValue = symlinkDirectory + ';';
var portablePathValue = (installDirectoryAddedToPath ? installDir : symlinkDirectory) + ';';
isAddedToPath = currentPathValue.Contains(portablePathValue);
}

Expand All @@ -397,9 +399,9 @@ public static void VerifyPortablePackage(
}

Assert.AreEqual(shouldExist, exeExists, $"Expected portable exe path: {exePath}");
Assert.AreEqual(shouldExist, symlinkExists, $"Expected portable symlink path: {symlinkPath}");
Assert.AreEqual(shouldExist && !installDirectoryAddedToPath, symlinkExists, $"Expected portable symlink path: {symlinkPath}");
Assert.AreEqual(shouldExist, portableEntryExists, $"Expected {productCode} subkey in path: {uninstallSubKey}");
Assert.AreEqual(shouldExist, isAddedToPath, $"Expected path variable: {symlinkDirectory}");
Assert.AreEqual(shouldExist, isAddedToPath, $"Expected path variable: {(installDirectoryAddedToPath ? installDir : symlinkDirectory)}");
}

/// <summary>
Expand Down
19 changes: 19 additions & 0 deletions src/AppInstallerCLIE2ETests/InstallCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,25 @@ public void InstallZip_Portable()
TestCommon.VerifyPortablePackage(Path.Combine(installDir, packageDirName), commandAlias, fileName, productCode, true, TestCommon.Scope.User);
}

/// <summary>
/// Test install zip portable with binaries that depend on PATH variable.
/// </summary>
[Test]
public void InstallZip_ArchivePortableWithBinariesDependentOnPath()
{
string installDir = TestCommon.GetPortablePackagesDirectory();
string packageId, commandAlias, fileName, packageDirName, productCode;
packageId = "AppInstallerTest.ArchivePortableWithBinariesDependentOnPath";
packageDirName = productCode = packageId + "_" + Constants.TestSourceIdentifier;
commandAlias = "TestPortable.exe";
fileName = "AppInstallerTestExeInstaller.exe";

var result = TestCommon.RunAICLICommand("install", $"{packageId}");
Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode);
Assert.True(result.StdOut.Contains("Successfully installed"));
TestCommon.VerifyPortablePackage(Path.Combine(installDir, packageDirName), commandAlias, fileName, productCode, true, TestCommon.Scope.User, true);
}

/// <summary>
/// Test install zip with invalid relative file path.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
PackageIdentifier: AppInstallerTest.ArchivePortableWithBinariesDependentOnPath
PackageVersion: 1.0.0.0
PackageName: TestArchivePortableWithBinariesDependentOnPath
PackageLocale: en-US
Publisher: AppInstallerTest
License: Test
ShortDescription: E2E test for installing a zip containing a portable with binaries that depend on the PATH variable.
Installers:
- Architecture: x64
InstallerUrl: https://localhost:5001/TestKit/AppInstallerTestZipInstaller/AppInstallerTestZipInstaller.zip
InstallerType: zip
InstallerSha256: <ZIPHASH>
NestedInstallerType: portable
NestedInstallerFiles:
- RelativeFilePath: AppInstallerTestExeInstaller.exe
PortableCommandAlias: TestPortable
ArchiveBinariesDependOnPath: true
ManifestType: singleton
ManifestVersion: 1.9.0
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ InstallationMetadata:
InvocationParameter: "/arg"
DisplayName: "DisplayName"
DownloadCommandProhibited: true
ArchiveBinariesDependOnPath: true

Installers:
- Architecture: x86
Expand Down Expand Up @@ -194,5 +195,6 @@ Installers:
ReturnResponse: custom
ReturnResponseUrl: https://defaultReturnResponseUrl.com
DownloadCommandProhibited: false
ArchiveBinariesDependOnPath: false
ManifestType: singleton
ManifestVersion: 1.9.0
ManifestVersion: 1.9.0
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ InstallationMetadata:
InvocationParameter: "/arg"
DisplayName: "DisplayName"
DownloadCommandProhibited: true
ArchiveBinariesDependOnPath: true

Installers:
- Architecture: x86
Expand Down Expand Up @@ -159,6 +160,7 @@ Installers:
UnsupportedArguments:
- location
DownloadCommandProhibited: false
ArchiveBinariesDependOnPath: false
- Architecture: x64
InstallerType: exe
InstallerUrl: https://www.microsoft.com/msixsdk/msixsdkx64.exe
Expand Down Expand Up @@ -197,6 +199,7 @@ Installers:
FileType: other
InvocationParameter: "/arg2"
DisplayName: "DisplayName2"
ArchiveBinariesDependOnPath: true
- Architecture: x64
InstallerType: burn
InstallerUrl: https://www.microsoft.com/msixsdk/msixsdkx64.exe
Expand All @@ -205,4 +208,4 @@ Installers:
UpgradeBehavior: deny
RepairBehavior: modify
ManifestType: installer
ManifestVersion: 1.9.0
ManifestVersion: 1.9.0
16 changes: 16 additions & 0 deletions src/AppInstallerCLITests/YamlManifest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,11 @@ namespace
REQUIRE(defaultSwitches.at(InstallerSwitchType::Repair) == "/repair");
REQUIRE(manifest.DefaultInstallerInfo.RepairBehavior == RepairBehaviorEnum::Modify);
}

if (manifestVer >= ManifestVer{ s_ManifestVersionV1_9 })
{
REQUIRE(manifest.DefaultInstallerInfo.ArchiveBinariesDependOnPath);
}
}

if (isSingleton || isExported)
Expand Down Expand Up @@ -375,6 +380,11 @@ namespace
REQUIRE(installer1.RepairBehavior == RepairBehaviorEnum::Modify);
}

if (manifestVer >= ManifestVer{ s_ManifestVersionV1_9 })
{
REQUIRE_FALSE(installer1.ArchiveBinariesDependOnPath);
}

if (!isSingleton)
{
if (!isExported)
Expand Down Expand Up @@ -467,6 +477,12 @@ namespace
REQUIRE(installer5.Switches.at(InstallerSwitchType::Repair) == "/repair");
REQUIRE(installer5.RepairBehavior == RepairBehaviorEnum::Modify);
}

if (manifestVer >= ManifestVer{ s_ManifestVersionV1_9 })
{
ManifestInstaller installer4 = manifest.Installers.at(3);
REQUIRE(installer4.ArchiveBinariesDependOnPath);
}
}

// Localization
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ namespace AppInstaller::Manifest::YamlParser
{ "RequireExplicitUpgrade"sv, YamlScalarType::Bool },
{ "DisplayInstallWarnings"sv, YamlScalarType::Bool },
{ "InstallerReturnCode"sv, YamlScalarType::Int },
{ "DownloadCommandProhibited", YamlScalarType::Bool }
{ "DownloadCommandProhibited", YamlScalarType::Bool },
{ "ArchiveBinariesDependOnPath", YamlScalarType::Bool }
};

YamlScalarType GetManifestScalarValueType(const std::string& key)
Expand Down
4 changes: 2 additions & 2 deletions src/AppInstallerCommonCore/Manifest/ManifestValidation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ namespace AppInstaller::Manifest
{ AppInstaller::Manifest::ManifestError::ArpValidationError, "Arp Validation Error."sv },
{ AppInstaller::Manifest::ManifestError::SchemaError, "Schema Error."sv },
{ AppInstaller::Manifest::ManifestError::MsixSignatureHashFailed, "Failed to calculate MSIX signature hash.Please verify that the input file is a valid, signed MSIX."sv },
{ AppInstaller::Manifest::ManifestError::ShadowManifestNotAllowed, "Shadow manifest is not allowed."}
{ AppInstaller::Manifest::ManifestError::ShadowManifestNotAllowed, "Shadow manifest is not allowed." }
};

return ErrorIdToMessageMap;
Expand Down Expand Up @@ -414,4 +414,4 @@ namespace AppInstaller::Manifest

return Utility::ConvertToUTF8(Message);
}
}
}
12 changes: 11 additions & 1 deletion src/AppInstallerCommonCore/Manifest/ManifestYamlPopulator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,16 @@ namespace AppInstaller::Manifest

std::move(fields_v1_7.begin(), fields_v1_7.end(), std::inserter(result, result.end()));
}

if (m_manifestVersion.get() >= ManifestVer{ s_ManifestVersionV1_9 })
{
std::vector<FieldProcessInfo> fields_v1_9 =
{
{ "ArchiveBinariesDependOnPath", [](const YAML::Node& value, const VariantManifestPtr& v)->ValidationErrors { GetManifestInstallerPtr(v)->ArchiveBinariesDependOnPath = value.as<bool>(); return {}; } },
};

std::move(fields_v1_9.begin(), fields_v1_9.end(), std::inserter(result, result.end()));
}
}

return result;
Expand Down Expand Up @@ -1243,4 +1253,4 @@ namespace AppInstaller::Manifest

return errors;
}
}
}
Loading
Loading