diff --git a/lib/NuGet-Chocolatey/src/Core/IPackageManager.cs b/lib/NuGet-Chocolatey/src/Core/IPackageManager.cs index 9abb140258..4430b10e68 100644 --- a/lib/NuGet-Chocolatey/src/Core/IPackageManager.cs +++ b/lib/NuGet-Chocolatey/src/Core/IPackageManager.cs @@ -1,7 +1,38 @@ using System; +using System.Runtime.Versioning; namespace NuGet { + public enum WalkerType + { + install, + update, + uninstall + } + + /// + /// Parameters for specific walker initialization. + /// + public class WalkerInfo + { + public WalkerType type; + + // for install + public bool ignoreDependencies; + public bool ignoreWalkInfo = false; + public FrameworkName targetFramework = null; + + // for update + public bool updateDependencies; + + // for install & update + public bool allowPrereleaseVersions; + + // for uninstall + public bool forceRemove; + public bool removeDependencies; + } + public interface IPackageManager { /// @@ -35,6 +66,7 @@ public interface IPackageManager event EventHandler PackageUninstalled; event EventHandler PackageUninstalling; + IPackage FindLocalPackage(string packageId, SemanticVersion version); void InstallPackage(IPackage package, bool ignoreDependencies, bool allowPrereleaseVersions); void InstallPackage(IPackage package, bool ignoreDependencies, bool allowPrereleaseVersions, bool ignoreWalkInfo); void InstallPackage(string packageId, SemanticVersion version, bool ignoreDependencies, bool allowPrereleaseVersions); @@ -43,5 +75,12 @@ public interface IPackageManager void UpdatePackage(string packageId, IVersionSpec versionSpec, bool updateDependencies, bool allowPrereleaseVersions); void UninstallPackage(IPackage package, bool forceRemove, bool removeDependencies); void UninstallPackage(string packageId, SemanticVersion version, bool forceRemove, bool removeDependencies); + + /// + /// Creates package dependency walker + /// + /// walker parameters + /// walker + IPackageOperationResolver GetWalker( WalkerInfo walkerInfo ); } } diff --git a/lib/NuGet-Chocolatey/src/Core/PackageManager.cs b/lib/NuGet-Chocolatey/src/Core/PackageManager.cs index 6cc20f76a6..ac51aaf39e 100644 --- a/lib/NuGet-Chocolatey/src/Core/PackageManager.cs +++ b/lib/NuGet-Chocolatey/src/Core/PackageManager.cs @@ -140,22 +140,14 @@ protected void InstallPackage( bool allowPrereleaseVersions, bool ignoreWalkInfo = false) { - if (WhatIf) - { - // This prevents InstallWalker from downloading the packages - ignoreWalkInfo = true; - } + var installerWalker = GetWalker( new WalkerInfo { + type = WalkerType.install, + ignoreDependencies = ignoreDependencies, + allowPrereleaseVersions = allowPrereleaseVersions, + ignoreWalkInfo = ignoreWalkInfo, + targetFramework = targetFramework + } ); - var installerWalker = new InstallWalker( - LocalRepository, SourceRepository, - targetFramework, Logger, - ignoreDependencies, allowPrereleaseVersions, - DependencyVersion) - { - DisableWalkInfo = ignoreWalkInfo, - CheckDowngrade = CheckDowngrade, - SkipPackageTargetCheck = SkipPackageTargetCheck - }; Execute(package, installerWalker); } @@ -176,7 +168,7 @@ private void Execute(IPackage package, IPackageOperationResolver resolver) } } - protected void Execute(PackageOperation operation) + public void Execute(PackageOperation operation) { bool packageExists = LocalRepository.Exists(operation.Package); @@ -293,7 +285,7 @@ public void UninstallPackage(string packageId, SemanticVersion version, bool for UninstallPackage(packageId, version: version, forceRemove: forceRemove, removeDependencies: false); } - public virtual void UninstallPackage(string packageId, SemanticVersion version, bool forceRemove, bool removeDependencies) + public IPackage FindLocalPackage(string packageId, SemanticVersion version) { if (String.IsNullOrEmpty(packageId)) { @@ -309,7 +301,13 @@ public virtual void UninstallPackage(string packageId, SemanticVersion version, NuGetResources.UnknownPackage, packageId)); } - UninstallPackage(package, forceRemove, removeDependencies); + return package; + } + + public virtual void UninstallPackage(string packageId, SemanticVersion version, bool forceRemove, bool removeDependencies) + { + + UninstallPackage(FindLocalPackage(packageId, version), forceRemove, removeDependencies); } public void UninstallPackage(IPackage package) @@ -324,15 +322,11 @@ public void UninstallPackage(IPackage package, bool forceRemove) public virtual void UninstallPackage(IPackage package, bool forceRemove, bool removeDependencies) { - Execute(package, new UninstallWalker(LocalRepository, - new DependentsWalker(LocalRepository, targetFramework: null), - targetFramework: null, - logger: Logger, - removeDependencies: removeDependencies, - forceRemove: forceRemove) - { - DisableWalkInfo = WhatIf - }); + Execute(package, GetWalker( new WalkerInfo() { + type = WalkerType.uninstall, + forceRemove = forceRemove, + removeDependencies = removeDependencies + }) ); } protected virtual void ExecuteUninstall(IPackage package) @@ -473,16 +467,74 @@ internal void UpdatePackage(string packageId, Func resolvePackage, boo public void UpdatePackage(IPackage newPackage, bool updateDependencies, bool allowPrereleaseVersions) { - Execute(newPackage, new UpdateWalker(LocalRepository, - SourceRepository, - new DependentsWalker(LocalRepository, targetFramework: null), - NullConstraintProvider.Instance, - targetFramework: null, - logger: Logger, - updateDependencies: updateDependencies, - allowPrereleaseVersions: allowPrereleaseVersions)); + var upgradeWalker = GetWalker( new WalkerInfo() { + type = WalkerType.update, + updateDependencies = updateDependencies, + allowPrereleaseVersions = allowPrereleaseVersions + }); + + Execute(newPackage, upgradeWalker); + } + + public IPackageOperationResolver GetWalker( WalkerInfo walkinfo ) + { + switch (walkinfo.type) + { + case WalkerType.install: + { + if (WhatIf) + { + // This prevents InstallWalker from downloading the packages + walkinfo.ignoreWalkInfo = true; + } + + var installerWalker = new InstallWalker( + LocalRepository, SourceRepository, + walkinfo.targetFramework, Logger, + walkinfo.ignoreDependencies, walkinfo.allowPrereleaseVersions, + DependencyVersion) + { + DisableWalkInfo = walkinfo.ignoreWalkInfo, + CheckDowngrade = CheckDowngrade, + SkipPackageTargetCheck = SkipPackageTargetCheck + }; + + return installerWalker; + } + + case WalkerType.update: + { + return new UpdateWalker(LocalRepository, + SourceRepository, + new DependentsWalker(LocalRepository, targetFramework: null), + NullConstraintProvider.Instance, + targetFramework: null, + logger: Logger, + updateDependencies: walkinfo.updateDependencies, + allowPrereleaseVersions: walkinfo.allowPrereleaseVersions); + } + + case WalkerType.uninstall: + { + var uninstallWalker = new UninstallWalker(LocalRepository, + new DependentsWalker(LocalRepository, targetFramework: null), + targetFramework: null, + logger: Logger, + removeDependencies: walkinfo.removeDependencies, + forceRemove: walkinfo.forceRemove) + { + DisableWalkInfo = WhatIf + }; + + return uninstallWalker; + } + } + + return null; } public bool CheckDowngrade { get; set; } } -} \ No newline at end of file + +} + diff --git a/lib/NuGet-Chocolatey/src/Core/Packages/PackageTags.cs b/lib/NuGet-Chocolatey/src/Core/Packages/PackageTags.cs index 4c7f7d25de..0a5c17a0cc 100644 --- a/lib/NuGet-Chocolatey/src/Core/Packages/PackageTags.cs +++ b/lib/NuGet-Chocolatey/src/Core/Packages/PackageTags.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; @@ -40,6 +41,48 @@ public static string GetKey(this IPackage pack, string key) return value; } + /// + /// Gets all key value pairs from package + /// + /// package to query + /// Dictionary of items + public static Dictionary GetKeyValuePairs(this IPackage pack, Func matcher = null) + { + Dictionary d = new Dictionary(); + + if (matcher == null) + { + matcher = (key) => { return true; }; + } + + if (pack.TagsExtra != null) + { + foreach (var tag in pack.TagsExtra) + { + if (matcher(tag.Key)) + { + d[tag.Key] = tag.Value; + } + } + } + else + { + var lines = pack.Tags.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + + var keyValuePairs = lines. + Select(x => reSplitEqual.Match(x)). + Where(m => m.Success && matcher(m.Groups[1].Value)). + Select(m => (m.Groups[1].Value, Uri.UnescapeDataString(m.Groups[2].Value))); + + foreach (var kvPair in keyValuePairs) + { + d[kvPair.Item1] = kvPair.Item2; + } + } + + return d; + } + public static string GetInstallLocation(this IPackage pack) { return GetKey(pack, "InstallLocation"); diff --git a/lib/NuGet-Chocolatey/src/Core/Repositories/LocalPackageRepository.cs b/lib/NuGet-Chocolatey/src/Core/Repositories/LocalPackageRepository.cs index cb8bf2d779..2fa68352b8 100644 --- a/lib/NuGet-Chocolatey/src/Core/Repositories/LocalPackageRepository.cs +++ b/lib/NuGet-Chocolatey/src/Core/Repositories/LocalPackageRepository.cs @@ -74,6 +74,45 @@ protected IFileSystem FileSystem private set; } + /// + /// When packageIdToFilesystem is in use - this must be non-null + /// + public Func GetPackageInstallPath; + + /// + /// Package id to it's file system. + /// + public Dictionary PackageIdToFilesystem = new Dictionary(); + + + /// + /// Gets current file system for specific package id + /// + /// package id + /// file system + public IFileSystem GetFS(string packageId) + { + IFileSystem fs; + + if (GetPackageInstallPath != null && packageId != null) + { + if (!PackageIdToFilesystem.ContainsKey(packageId)) + { + string dir = GetPackageInstallPath(packageId); + PackageIdToFilesystem[packageId] = new PhysicalFileSystem(dir); + } + + fs = PackageIdToFilesystem[packageId]; + } + else + { + fs = FileSystem; + } + + return fs; + } + + public override IQueryable GetPackages() { return GetPackages(OpenPackage).AsQueryable(); @@ -170,8 +209,8 @@ public virtual IEnumerable GetPackageLookupPaths(string packageId, Seman var packageFileName = PathResolver.GetPackageFileName(packageId, version); var manifestFileName = Path.ChangeExtension(packageFileName, Constants.ManifestExtension); var filesMatchingFullName = Enumerable.Concat( - GetPackageFiles(packageFileName), - GetPackageFiles(manifestFileName)); + GetPackageFiles(packageId,packageFileName), + GetPackageFiles(packageId, manifestFileName)); if (version != null && version.Version.Revision < 1) { @@ -190,15 +229,15 @@ public virtual IEnumerable GetPackageLookupPaths(string packageId, Seman // Partial names would result is gathering package with matching major and minor but different build and revision. // Attempt to match the version in the path to the version we're interested in. - var partialNameMatches = GetPackageFiles(partialName).Where(path => FileNameMatchesPattern(packageId, version, path)); - var partialManifestNameMatches = GetPackageFiles(partialManifestName).Where( + var partialNameMatches = GetPackageFiles(packageId, partialName).Where(path => FileNameMatchesPattern(packageId, version, path)); + var partialManifestNameMatches = GetPackageFiles(packageId, partialManifestName).Where( path => FileNameMatchesPattern(packageId, version, path)); return Enumerable.Concat(filesMatchingFullName, partialNameMatches).Concat(partialManifestNameMatches); } return filesMatchingFullName; } - internal IPackage FindPackage(Func openPackage, string packageId, SemanticVersion version) + internal IPackage FindPackage(Func openPackage, string packageId, SemanticVersion version) { var lookupPackageName = new PackageName(packageId, version); string packagePath; @@ -208,19 +247,19 @@ internal IPackage FindPackage(Func openPackage, string package FileSystem.FileExists(packagePath)) { // When depending on the cached path, verify the file exists on disk. - return GetPackage(openPackage, packagePath); + return GetPackage(packageId, openPackage, packagePath); } // Lookup files which start with the name "." and attempt to match it with all possible version string combinations (e.g. 1.2.0, 1.2.0.0) // before opening the package. To avoid creating file name strings, we attempt to specifically match everything after the last path separator // which would be the file name and extension. return (from path in GetPackageLookupPaths(packageId, version) - let package = GetPackage(openPackage, path) + let package = GetPackage(packageId, openPackage, path) where lookupPackageName.Equals(new PackageName(package.Id, package.Version)) select package).FirstOrDefault(); } - internal IEnumerable FindPackagesById(Func openPackage, string packageId) + internal IEnumerable FindPackagesById(Func openPackage, string packageId) { Debug.Assert(!String.IsNullOrEmpty(packageId), "The caller has to ensure packageId is never null."); @@ -231,18 +270,18 @@ internal IEnumerable FindPackagesById(Func openPacka GetPackages( openPackage, packageId, - GetPackageFiles(packageId + "*" + Constants.PackageExtension))); + GetPackageFiles(packageId, packageId + "*" + Constants.PackageExtension))); // then, get packages through nuspec files packages.AddRange( GetPackages( openPackage, packageId, - GetPackageFiles(packageId + "*" + Constants.ManifestExtension))); + GetPackageFiles(packageId, packageId + "*" + Constants.ManifestExtension))); return packages; } - internal IEnumerable GetPackages(Func openPackage, + internal IEnumerable GetPackages(Func openPackage, string packageId, IEnumerable packagePaths) { @@ -251,7 +290,7 @@ internal IEnumerable GetPackages(Func openPackage, IPackage package = null; try { - package = GetPackage(openPackage, path); + package = GetPackage(packageId, openPackage, path); } catch (InvalidOperationException) { @@ -277,14 +316,14 @@ internal IEnumerable GetPackages(Func openPackage, [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We want to suppress all errors opening a package")] - internal IEnumerable GetPackages(Func openPackage) + internal IEnumerable GetPackages(Func openPackage) { - return GetPackageFiles() + return GetPackageFiles(null) .Select(path => { try { - return GetPackage(openPackage, path); + return GetPackage(null, openPackage, path); } catch { @@ -294,10 +333,11 @@ internal IEnumerable GetPackages(Func openPackage) .Where(p => p != null); } - private IPackage GetPackage(Func openPackage, string path) + private IPackage GetPackage(string packageId, Func openPackage, string path) { PackageCacheEntry cacheEntry; - DateTimeOffset lastModified = FileSystem.GetLastModified(path); + var fs = GetFS(packageId); + DateTimeOffset lastModified = fs.GetLastModified(path); // If we never cached this file or we did and it's current last modified time is newer // create a new entry if (!_packageCache.TryGetValue(path, out cacheEntry) || @@ -307,7 +347,7 @@ private IPackage GetPackage(Func openPackage, string path) string packagePath = path; // Create the package - IPackage package = openPackage(packagePath); + IPackage package = openPackage(packageId, packagePath); // create a cache entry with the last modified time cacheEntry = new PackageCacheEntry(package, lastModified); @@ -323,34 +363,38 @@ private IPackage GetPackage(Func openPackage, string path) return cacheEntry.Package; } - internal IEnumerable GetPackageFiles(string filter = null) + internal IEnumerable GetPackageFiles(string packageId, string filter = null) { filter = filter ?? "*" + Constants.PackageExtension; Debug.Assert( filter.EndsWith(Constants.PackageExtension, StringComparison.OrdinalIgnoreCase) || filter.EndsWith(Constants.ManifestExtension, StringComparison.OrdinalIgnoreCase)); + var fs = GetFS(packageId); + // Check for package files one level deep. We use this at package install time // to determine the set of installed packages. Installed packages are copied to // {id}.{version}\{packagefile}.{extension}. - foreach (var dir in FileSystem.GetDirectories(String.Empty)) + foreach (var dir in fs.GetDirectories(String.Empty)) { - foreach (var path in FileSystem.GetFiles(dir, filter)) + foreach (var path in fs.GetFiles(dir, filter)) { yield return path; } } // Check top level directory - foreach (var path in FileSystem.GetFiles(String.Empty, filter)) + foreach (var path in fs.GetFiles(String.Empty, filter)) { yield return path; } } - internal virtual IPackage OpenPackage(string path) + internal virtual IPackage OpenPackage(string packageId, string path) { - if (!FileSystem.FileExists(path)) + var fs = GetFS(packageId); + + if (!fs.FileExists(path)) { return null; } @@ -360,7 +404,7 @@ internal virtual IPackage OpenPackage(string path) OptimizedZipPackage package; try { - package = new OptimizedZipPackage(FileSystem, path); + package = new OptimizedZipPackage(fs, path); } catch (FileFormatException ex) { @@ -368,15 +412,15 @@ internal virtual IPackage OpenPackage(string path) } // Set the last modified date on the package - package.Published = FileSystem.GetLastModified(path); + package.Published = fs.GetLastModified(path); return package; } else if (Path.GetExtension(path) == Constants.ManifestExtension) { - if (FileSystem.FileExists(path)) + if (fs.FileExists(path)) { - return new UnzippedPackage(FileSystem, Path.GetFileNameWithoutExtension(path)); + return new UnzippedPackage(fs, Path.GetFileNameWithoutExtension(path)); } } diff --git a/lib/NuGet-Chocolatey/src/Core/Repositories/MachineCache.cs b/lib/NuGet-Chocolatey/src/Core/Repositories/MachineCache.cs index 0c8132d217..535493c2fa 100644 --- a/lib/NuGet-Chocolatey/src/Core/Repositories/MachineCache.cs +++ b/lib/NuGet-Chocolatey/src/Core/Repositories/MachineCache.cs @@ -65,7 +65,7 @@ internal static MachineCache CreateDefault(Func getCachePath) public override void AddPackage(IPackage package) { // If we exceed the package count then clear the cache. - var files = GetPackageFiles().ToList(); + var files = GetPackageFiles(package.Id).ToList(); if (files.Count >= MaxPackages) { // It's expensive to hit the file system to get the last accessed date for files @@ -144,7 +144,7 @@ public bool InvokeOnPackage(string packageId, SemanticVersion version, Action files) diff --git a/lib/NuGet-Chocolatey/src/Core/Repositories/SharedPackageRepository.cs b/lib/NuGet-Chocolatey/src/Core/Repositories/SharedPackageRepository.cs index 050868c104..0e1675902c 100644 --- a/lib/NuGet-Chocolatey/src/Core/Repositories/SharedPackageRepository.cs +++ b/lib/NuGet-Chocolatey/src/Core/Repositories/SharedPackageRepository.cs @@ -202,9 +202,11 @@ protected virtual IPackageRepository CreateRepository(string path) return new PackageReferenceRepository(absolutePath, sourceRepository: this); } - internal override IPackage OpenPackage(string path) + internal override IPackage OpenPackage(string packageId, string path) { - if (!FileSystem.FileExists(path)) + var fs = GetFS(packageId); + + if (!fs.FileExists(path)) { return null; } @@ -214,7 +216,7 @@ internal override IPackage OpenPackage(string path) { try { - return new SharedOptimizedZipPackage(FileSystem, path); + return new SharedOptimizedZipPackage(fs, path); } catch (FileFormatException ex) { diff --git a/nuspec/chocolatey/choco/choco.nuspec b/nuspec/chocolatey/choco/choco.nuspec index f677f7d87b..2b769728f2 100644 --- a/nuspec/chocolatey/choco/choco.nuspec +++ b/nuspec/chocolatey/choco/choco.nuspec @@ -18,7 +18,9 @@ https://github.com/chocolatey/choco/issues--> nuget apt-get machine repository chocolatey - + + + Chocolatey is the package manager for Windows (like apt-get but for Windows) @@ -67,7 +69,7 @@ In that mess there is a link to the [PowerShell Chocolatey module reference](htt See all - https://docs.chocolatey.org/en-us/choco/release-notes - + diff --git a/src/chocolatey.tests.integration/context/dependencies/reghasdependency/1.0.0/reghasdependency.nuspec b/src/chocolatey.tests.integration/context/dependencies/reghasdependency/1.0.0/reghasdependency.nuspec new file mode 100644 index 0000000000..db6d6a8b90 --- /dev/null +++ b/src/chocolatey.tests.integration/context/dependencies/reghasdependency/1.0.0/reghasdependency.nuspec @@ -0,0 +1,23 @@ + + + + reghasdependency + 1.0.0 + reghasdependency + Publisher + false + test package + + + + + + + + + + + + + + diff --git a/src/chocolatey.tests.integration/context/dependencies/reghasdependency/1.0.0/tools/chocolateyinstall.ps1 b/src/chocolatey.tests.integration/context/dependencies/reghasdependency/1.0.0/tools/chocolateyinstall.ps1 new file mode 100644 index 0000000000..d64eb8f47b --- /dev/null +++ b/src/chocolatey.tests.integration/context/dependencies/reghasdependency/1.0.0/tools/chocolateyinstall.ps1 @@ -0,0 +1 @@ +Write-Output "$env:PackageName $env:PackageVersion Installed" \ No newline at end of file diff --git a/src/chocolatey.tests.integration/context/dependencies/reghasdependency/1.0.0/tools/chocolateyuninstall.ps1 b/src/chocolatey.tests.integration/context/dependencies/reghasdependency/1.0.0/tools/chocolateyuninstall.ps1 new file mode 100644 index 0000000000..9ead91ffa3 --- /dev/null +++ b/src/chocolatey.tests.integration/context/dependencies/reghasdependency/1.0.0/tools/chocolateyuninstall.ps1 @@ -0,0 +1 @@ +Write-Output "$env:PackageName $env:PackageVersion Uninstalled" \ No newline at end of file diff --git a/src/chocolatey.tests2/ChocoTestContext.cs b/src/chocolatey.tests2/ChocoTestContext.cs index 95403c29d6..479cd10d73 100644 --- a/src/chocolatey.tests2/ChocoTestContext.cs +++ b/src/chocolatey.tests2/ChocoTestContext.cs @@ -15,6 +15,7 @@ public enum ChocoTestContext installupdate, installupdate2, installpackage3, + install_regpackage_with_dependencies, isdependency, isdependency_hasdependency, isdependency_hasdependency_sxs, @@ -36,6 +37,8 @@ public enum ChocoTestContext packages_for_dependency_testing9, packages_for_dependency_testing10, packages_for_dependency_testing11, + + packages_for_reg_dependency_testing, packages_for_upgrade_testing, upgrade_testing_context, uninstall_testing_context, @@ -68,5 +71,8 @@ public enum ChocoTestContext pack_upgradepackage_1_1_0, pack_upgradepackage_1_1_1_beta, pack_upgradepackage_1_1_1_beta2, + + pack_reghasdependency_1_0_0 }; } + diff --git a/src/chocolatey.tests2/LogTesting.cs b/src/chocolatey.tests2/LogTesting.cs index a687e33ec4..0b21f8c9d6 100644 --- a/src/chocolatey.tests2/LogTesting.cs +++ b/src/chocolatey.tests2/LogTesting.cs @@ -29,6 +29,7 @@ public class LogTesting protected IChocolateyPackageService Service; public const string installpackage2_id = "installpackage2"; + public const string reghasdependency_id = "reghasdependency"; public LogTesting() { @@ -658,6 +659,14 @@ bool PrepareTestContext(ChocoTestContext testcontext, ChocolateyConfiguration _c ); break; + case ChocoTestContext.packages_for_reg_dependency_testing: + PrepareMultiPackageFolder( + ChocoTestContext.pack_reghasdependency_1_0_0, + ChocoTestContext.pack_isdependency_1_0_0, + ChocoTestContext.pack_isexactversiondependency_1_1_0 + ); + break; + case ChocoTestContext.packages_for_upgrade_testing: PrepareMultiPackageFolder( ChocoTestContext.pack_badpackage_1_0, @@ -714,6 +723,22 @@ bool PrepareTestContext(ChocoTestContext testcontext, ChocolateyConfiguration _c } break; + case ChocoTestContext.install_regpackage_with_dependencies: + { + const string packageId = LogTesting.reghasdependency_id; + + using (var tester = new TestRegistry()) + { + tester.DeleteInstallEntries(packageId); + + Install(packageId, "1.0.0", ChocoTestContext.packages_for_reg_dependency_testing); + + tester.LogInstallEntries(true, packageId); + tester.DeleteInstallEntries(packageId); + } + } + break; + case ChocoTestContext.installpackage3: { Install("installpackage3", "1.0.0", ChocoTestContext.pack_installpackage3_1_0_0, true); diff --git a/src/chocolatey.tests2/LogTesting/PrepareTestFolder_install_regpackage_with_dependencies.txt b/src/chocolatey.tests2/LogTesting/PrepareTestFolder_install_regpackage_with_dependencies.txt new file mode 100644 index 0000000000..23e1d750b4 --- /dev/null +++ b/src/chocolatey.tests2/LogTesting/PrepareTestFolder_install_regpackage_with_dependencies.txt @@ -0,0 +1,79 @@ +Installing the following packages: +reghasdependency +By installing you accept licenses for the packages. +[NuGet] Attempting to resolve dependency 'isdependency (≥ 1.0.0)'. +[NuGet] Attempting to resolve dependency 'isexactversiondependency (= 1.1.0)'. +[NuGet] Installing 'isdependency 1.0.0'. +[NuGet] Successfully installed 'isdependency 1.0.0'. + +isdependency v1.0.0 +isdependency package files install completed. Performing other installation steps. +isdependency 1.0.0 Installed + The install of isdependency was successful. + Software install location not explicitly set, could be in package or + default install location if installer. +[NuGet] Installing 'isexactversiondependency 1.1.0'. +[NuGet] Successfully installed 'isexactversiondependency 1.1.0'. + +isexactversiondependency v1.1.0 +isexactversiondependency package files install completed. Performing other installation steps. +isexactversiondependency 1.1.0 Installed + The install of isexactversiondependency was successful. + Software install location not explicitly set, could be in package or + default install location if installer. +[NuGet] Installing 'reghasdependency 1.0.0'. +[NuGet] Successfully installed 'reghasdependency 1.0.0'. + +reghasdependency v1.0.0 +reghasdependency package files install completed. Performing other installation steps. +reghasdependency 1.0.0 Installed + The install of reghasdependency was successful. + Software install location not explicitly set, could be in package or + default install location if installer. + +Chocolatey installed 3/3 packages. + See the log for details (logs\chocolatey.log). +=> install result for isdependency/1.0.0: succeeded +=> install result for isexactversiondependency/1.1.0: succeeded +=> install result for reghasdependency/1.0.0: succeeded +=> added new files: +custominstalldir\lib\isexactversiondependency\.install_info\.arguments +custominstalldir\lib\isexactversiondependency\.install_info\.files +custominstalldir\lib\isexactversiondependency\isexactversiondependency.nupkg + version: 1.1.0.0 +custominstalldir\lib\isexactversiondependency\isexactversiondependency.nuspec +custominstalldir\lib\isexactversiondependency\tools\chocolateyinstall.ps1 +custominstalldir\lib\isexactversiondependency\tools\chocolateyuninstall.ps1 +custominstalldir\plugins\isdependency\.install_info\.arguments +custominstalldir\plugins\isdependency\.install_info\.files +custominstalldir\plugins\isdependency\isdependency.nupkg + version: 1.0.0.0 +custominstalldir\plugins\isdependency\isdependency.nuspec +custominstalldir\plugins\isdependency\tools\chocolateyinstall.ps1 +custominstalldir\plugins\isdependency\tools\chocolateyuninstall.ps1 +custominstalldir\reghasdependency\.install_info\.arguments +custominstalldir\reghasdependency\.install_info\.files +custominstalldir\reghasdependency\reghasdependency.nupkg + version: 1.0.0.0 +custominstalldir\reghasdependency\reghasdependency.nuspec +custominstalldir\reghasdependency\tools\chocolateyinstall.ps1 +custominstalldir\reghasdependency\tools\chocolateyuninstall.ps1 + +- after operation reghasdependency registry: + Hive: LocalMachine + KeyPath: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\reghasdependency + PackageId: reghasdependency + IsPinned: False + DisplayName: reghasdependency + InstallLocation: custominstalldir\reghasdependency + UninstallString: uninstall string: to do + HasQuietUninstall: False + Publisher: Publisher + DisplayVersion: 1.0.0 + Version: 1.0.0 + NoRemove: False + NoModify: True + NoRepair: True + EstimatedSize: 3 + +shared context ends diff --git a/src/chocolatey.tests2/TestRegistry.cs b/src/chocolatey.tests2/TestRegistry.cs index 22e5d7f715..382fafc0e1 100644 --- a/src/chocolatey.tests2/TestRegistry.cs +++ b/src/chocolatey.tests2/TestRegistry.cs @@ -74,14 +74,17 @@ public void AddInstallEntry(RegistryApplicationKey appKey) registryService.set_key_values(appKey, propNames.ToArray()); } - public void AddInstallPackage2Entry() + public void AddInstallPackage2Entry( + string packageId = "installpackage2", + string installdirectory = "custominstalldir", + string version = "1.0.0" ) { AddInstallEntry( new RegistryApplicationKey() { - PackageId = "installpackage2", - Version = "1.0.0", - InstallLocation = Path.Combine(InstallContext.Instance.RootLocation, "custominstalldir", "installpackage2"), + PackageId = packageId, + Version = version, + InstallLocation = Path.Combine(InstallContext.Instance.RootLocation, installdirectory, packageId), Tags = "test" } ); diff --git a/src/chocolatey.tests2/commands/TestInstallCommand.cs b/src/chocolatey.tests2/commands/TestInstallCommand.cs index 85345a0067..6746e60885 100644 --- a/src/chocolatey.tests2/commands/TestInstallCommand.cs +++ b/src/chocolatey.tests2/commands/TestInstallCommand.cs @@ -478,6 +478,26 @@ public void when_installing_regpackage_on_already_installed() } } + [LogTest] + public void when_installing_regpackage_with_dependencies_on_empty() + { + string packageId = reghasdependency_id; + + using (var tester = new TestRegistry()) + { + tester.DeleteInstallEntries(packageId); + tester.LogInstallEntries(false, packageId); + + InstallOnEmpty((conf) => + { + conf.PackageNames = conf.Input = packageId; + }, ChocoTestContext.packages_for_reg_dependency_testing); + + tester.LogInstallEntries(true, packageId); + tester.DeleteInstallEntries(packageId); + } + } + } } diff --git a/src/chocolatey.tests2/commands/TestInstallCommand/when_installing_regpackage_with_dependencies_on_empty.txt b/src/chocolatey.tests2/commands/TestInstallCommand/when_installing_regpackage_with_dependencies_on_empty.txt new file mode 100644 index 0000000000..4ad95df5f3 --- /dev/null +++ b/src/chocolatey.tests2/commands/TestInstallCommand/when_installing_regpackage_with_dependencies_on_empty.txt @@ -0,0 +1,81 @@ + +- before operation reghasdependency - not installed +Installing the following packages: +reghasdependency +By installing you accept licenses for the packages. +[NuGet] Attempting to resolve dependency 'isdependency (≥ 1.0.0)'. +[NuGet] Attempting to resolve dependency 'isexactversiondependency (= 1.1.0)'. +[NuGet] Installing 'isdependency 1.0.0'. +[NuGet] Successfully installed 'isdependency 1.0.0'. + +isdependency v1.0.0 +isdependency package files install completed. Performing other installation steps. +isdependency 1.0.0 Installed + The install of isdependency was successful. + Software install location not explicitly set, could be in package or + default install location if installer. +[NuGet] Installing 'isexactversiondependency 1.1.0'. +[NuGet] Successfully installed 'isexactversiondependency 1.1.0'. + +isexactversiondependency v1.1.0 +isexactversiondependency package files install completed. Performing other installation steps. +isexactversiondependency 1.1.0 Installed + The install of isexactversiondependency was successful. + Software install location not explicitly set, could be in package or + default install location if installer. +[NuGet] Installing 'reghasdependency 1.0.0'. +[NuGet] Successfully installed 'reghasdependency 1.0.0'. + +reghasdependency v1.0.0 +reghasdependency package files install completed. Performing other installation steps. +reghasdependency 1.0.0 Installed + The install of reghasdependency was successful. + Software install location not explicitly set, could be in package or + default install location if installer. + +Chocolatey installed 3/3 packages. + See the log for details (logs\chocolatey.log). +=> install result for isdependency/1.0.0: succeeded +=> install result for isexactversiondependency/1.1.0: succeeded +=> install result for reghasdependency/1.0.0: succeeded +=> added new files: +custominstalldir\lib\isexactversiondependency\.install_info\.arguments +custominstalldir\lib\isexactversiondependency\.install_info\.files +custominstalldir\lib\isexactversiondependency\isexactversiondependency.nupkg + version: 1.1.0.0 +custominstalldir\lib\isexactversiondependency\isexactversiondependency.nuspec +custominstalldir\lib\isexactversiondependency\tools\chocolateyinstall.ps1 +custominstalldir\lib\isexactversiondependency\tools\chocolateyuninstall.ps1 +custominstalldir\plugins\isdependency\.install_info\.arguments +custominstalldir\plugins\isdependency\.install_info\.files +custominstalldir\plugins\isdependency\isdependency.nupkg + version: 1.0.0.0 +custominstalldir\plugins\isdependency\isdependency.nuspec +custominstalldir\plugins\isdependency\tools\chocolateyinstall.ps1 +custominstalldir\plugins\isdependency\tools\chocolateyuninstall.ps1 +custominstalldir\reghasdependency\.install_info\.arguments +custominstalldir\reghasdependency\.install_info\.files +custominstalldir\reghasdependency\reghasdependency.nupkg + version: 1.0.0.0 +custominstalldir\reghasdependency\reghasdependency.nuspec +custominstalldir\reghasdependency\tools\chocolateyinstall.ps1 +custominstalldir\reghasdependency\tools\chocolateyuninstall.ps1 + +- after operation reghasdependency registry: + Hive: LocalMachine + KeyPath: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\reghasdependency + PackageId: reghasdependency + IsPinned: False + DisplayName: reghasdependency + InstallLocation: custominstalldir\reghasdependency + UninstallString: uninstall string: to do + HasQuietUninstall: False + Publisher: Publisher + DisplayVersion: 1.0.0 + Version: 1.0.0 + NoRemove: False + NoModify: True + NoRepair: True + EstimatedSize: 3 + +end of test diff --git a/src/chocolatey.tests2/commands/TestUninstallCommand.cs b/src/chocolatey.tests2/commands/TestUninstallCommand.cs index f99ae73529..9633d49111 100644 --- a/src/chocolatey.tests2/commands/TestUninstallCommand.cs +++ b/src/chocolatey.tests2/commands/TestUninstallCommand.cs @@ -216,6 +216,29 @@ public void when_uninstalling_registry_package() } + [LogTest] + public void when_uninstalling_regpackage_with_dependencies() + { + string packageId = reghasdependency_id; + + using (var tester = new TestRegistry(false)) + { + TestUninstall((conf) => + { + conf.PackageNames = conf.Input = packageId; + conf.ForceDependencies = true; + + tester.Lock(); + tester.AddInstallPackage2Entry(packageId); + tester.LogInstallEntries(false, packageId); + + }, ChocoTestContext.install_regpackage_with_dependencies); + + tester.LogInstallEntries(true, packageId); + tester.DeleteInstallEntries(packageId); + } + } + } } diff --git a/src/chocolatey.tests2/commands/TestUninstallCommand/when_uninstalling_regpackage_with_dependencies.txt b/src/chocolatey.tests2/commands/TestUninstallCommand/when_uninstalling_regpackage_with_dependencies.txt new file mode 100644 index 0000000000..a225dc6a35 --- /dev/null +++ b/src/chocolatey.tests2/commands/TestUninstallCommand/when_uninstalling_regpackage_with_dependencies.txt @@ -0,0 +1,71 @@ + +- before operation reghasdependency registry: + Hive: LocalMachine + KeyPath: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\reghasdependency + PackageId: reghasdependency + IsPinned: False + DisplayName: reghasdependency + InstallLocation: custominstalldir\reghasdependency + UninstallString: none + HasQuietUninstall: False + Publisher: TestRegistry + DisplayVersion: 1.0.0 + Version: 1.0.0 + NoRemove: False + NoModify: False + NoRepair: False + EstimatedSize: 0 + Tags: test + +Uninstalling the following packages: +reghasdependency +[NuGet] Uninstalling 'reghasdependency 1.0.0'. + +reghasdependency v1.0.0 +reghasdependency 1.0.0 Uninstalled + Skipping auto uninstaller - No registry snapshot. +[NuGet] Successfully uninstalled 'reghasdependency 1.0.0'. + reghasdependency has been successfully uninstalled. +[NuGet] Uninstalling 'isexactversiondependency 1.1.0'. + +isexactversiondependency v1.1.0 +isexactversiondependency 1.1.0 Uninstalled + Skipping auto uninstaller - No registry snapshot. +[NuGet] Successfully uninstalled 'isexactversiondependency 1.1.0'. + isexactversiondependency has been successfully uninstalled. +[NuGet] Uninstalling 'isdependency 1.0.0'. + +isdependency v1.0.0 +isdependency 1.0.0 Uninstalled + Skipping auto uninstaller - No registry snapshot. +[NuGet] Successfully uninstalled 'isdependency 1.0.0'. + isdependency has been successfully uninstalled. + +Chocolatey uninstalled 3/3 packages. + See the log for details (logs\chocolatey.log). +=> uninstall result for isdependency/1.0.0: succeeded +=> uninstall result for isexactversiondependency/1.1.0: succeeded +=> uninstall result for reghasdependency/1.0.0: succeeded +=> folder was not updated +=> removed files: +custominstalldir\lib\isexactversiondependency\.install_info\.arguments +custominstalldir\lib\isexactversiondependency\.install_info\.files +custominstalldir\lib\isexactversiondependency\isexactversiondependency.nupkg +custominstalldir\lib\isexactversiondependency\isexactversiondependency.nuspec +custominstalldir\lib\isexactversiondependency\tools\chocolateyinstall.ps1 +custominstalldir\lib\isexactversiondependency\tools\chocolateyuninstall.ps1 +custominstalldir\plugins\isdependency\.install_info\.arguments +custominstalldir\plugins\isdependency\.install_info\.files +custominstalldir\plugins\isdependency\isdependency.nupkg +custominstalldir\plugins\isdependency\isdependency.nuspec +custominstalldir\plugins\isdependency\tools\chocolateyinstall.ps1 +custominstalldir\plugins\isdependency\tools\chocolateyuninstall.ps1 +custominstalldir\reghasdependency\.install_info\.arguments +custominstalldir\reghasdependency\.install_info\.files +custominstalldir\reghasdependency\reghasdependency.nupkg +custominstalldir\reghasdependency\reghasdependency.nuspec +custominstalldir\reghasdependency\tools\chocolateyinstall.ps1 +custominstalldir\reghasdependency\tools\chocolateyuninstall.ps1 + +- after operation reghasdependency - not installed +end of test diff --git a/src/chocolatey.tests2/commands/TestUpgradeCommand.cs b/src/chocolatey.tests2/commands/TestUpgradeCommand.cs index 39ddb7df4f..915f364325 100644 --- a/src/chocolatey.tests2/commands/TestUpgradeCommand.cs +++ b/src/chocolatey.tests2/commands/TestUpgradeCommand.cs @@ -588,28 +588,28 @@ public void when_upgrading_all_packages_with_except() [LogTest] public void when_upgrading_regpackage() { + string packageId = installpackage2_id; + using (var tester = new TestRegistry(false)) { TestUpgrade( (conf) => { - conf.PackageNames = conf.Input = installpackage2_id; + conf.PackageNames = conf.Input = packageId; tester.Lock(); - tester.AddInstallPackage2Entry(); - tester.LogInstallEntries(false, installpackage2_id); + tester.AddInstallPackage2Entry(packageId); + tester.LogInstallEntries(false, packageId); }, ChocoTestContext.installupdate2, ChocoTestContext.pack_installpackage2_2_3_0 ); - tester.LogInstallEntries(true, installpackage2_id); - tester.DeleteInstallEntries(installpackage2_id); + tester.LogInstallEntries(true, packageId); + tester.DeleteInstallEntries(packageId); } } - - } } diff --git a/src/chocolatey.tests2/infrastructure.app/services/NugetServiceSpecs.cs b/src/chocolatey.tests2/infrastructure.app/services/NugetServiceSpecs.cs index 26c52ec9ee..cbe5c72113 100644 --- a/src/chocolatey.tests2/infrastructure.app/services/NugetServiceSpecs.cs +++ b/src/chocolatey.tests2/infrastructure.app/services/NugetServiceSpecs.cs @@ -3,6 +3,7 @@ using chocolatey.infrastructure.app.domain; using chocolatey.infrastructure.app.services; using chocolatey.infrastructure.filesystem; +using chocolatey.infrastructure.logging; using logtesting; using Moq; using NuGet; @@ -133,5 +134,66 @@ public void TestBackupRemove() service.pack_noop(config); } } + + /// + /// Makes dummy package with specific tags + /// + /// tags to add to package + /// new package + RegistryPackage NewPack(string packageId, params string[] tags) + { + var p = new RegistryPackage() { Id = packageId }; + + p.Tags = ""; + p.TagsExtra = new List(); + for (int i = 0; i < tags.Length; i += 2) + { + p.TagsExtra.Add(new NuGet.Authoring.Tag() { Key = tags[i], Value = tags[i + 1] }); + } + + return p; + } + + [LogTest] + public void InstallFolderSelection() + { + var main = NewPack("mainp", + "InstallLocation", "%RootLocation%\\installdir", + + // Parent package may define general installation directory + "AddonsInstallFolder", "addons", + + // Parent package may define multiple child install locations + "*.plugin_InstallFolder", "plugins", + + // Parent package may define child install location + "forth.plugin_InstallFolder", "forthplugin_special" + ); + + List packages = new List(); + packages.Add(main); + packages.Add(NewPack("childp1")); + packages.Add(NewPack("childp2")); + packages.Add(NewPack("first.plugin")); + packages.Add(NewPack("second.plugin")); + packages.Add( + NewPack("third.plugin", + // plugin can override it's install location + "InstallLocation", "%RootLocation%\\thirdplugin_special" + ) + ); + packages.Add(NewPack("forth.plugin")); + + // does not end with '.plugin' must not match. + packages.Add(NewPack("fifth.plugin2")); + + foreach (var p in packages) + { + var dir = NugetService.GetInstallDirectory(main, p); + string id2 = "'" + p.Id + "'"; + LogService.console.Info(InstallContext.NormalizeMessage($"Package {id2,-15} - install dir: '{dir}'")); + } + } + } } diff --git a/src/chocolatey.tests2/infrastructure.app/services/NugetServiceSpecs/InstallFolderSelection.txt b/src/chocolatey.tests2/infrastructure.app/services/NugetServiceSpecs/InstallFolderSelection.txt new file mode 100644 index 0000000000..71b40e1152 --- /dev/null +++ b/src/chocolatey.tests2/infrastructure.app/services/NugetServiceSpecs/InstallFolderSelection.txt @@ -0,0 +1,9 @@ +Package 'mainp' - install dir: 'installdir' +Package 'childp1' - install dir: 'installdir\addons' +Package 'childp2' - install dir: 'installdir\addons' +Package 'first.plugin' - install dir: 'installdir\plugins' +Package 'second.plugin' - install dir: 'installdir\plugins' +Package 'third.plugin' - install dir: 'thirdplugin_special' +Package 'forth.plugin' - install dir: 'installdir\forthplugin_special' +Package 'fifth.plugin2' - install dir: 'installdir\addons' +end of test diff --git a/src/chocolatey/infrastructure.app/nuget/ChocolateyNugetLogger.cs b/src/chocolatey/infrastructure.app/nuget/ChocolateyNugetLogger.cs index 14fbbaa27c..eeb39fce5f 100644 --- a/src/chocolatey/infrastructure.app/nuget/ChocolateyNugetLogger.cs +++ b/src/chocolatey/infrastructure.app/nuget/ChocolateyNugetLogger.cs @@ -28,27 +28,29 @@ public FileConflictResolution ResolveFileConflict(string message) return FileConflictResolution.OverwriteAll; } + public const string NuGet = "[NuGet] "; + public void Log(MessageLevel level, string message, params object[] args) { switch (level) { case MessageLevel.Debug: - this.Log().Debug("[NuGet] " + message, args); + this.Log().Debug(NuGet + message, args); break; case MessageLevel.Info: - this.Log().Info("[NuGet] " + message, args); + this.Log().Info(NuGet + message, args); break; case MessageLevel.Warning: - this.Log().Warn("[NuGet] " + message, args); + this.Log().Warn(NuGet + message, args); break; case MessageLevel.Error: - this.Log().Error("[NuGet] " + message, args); + this.Log().Error(NuGet + message, args); break; case MessageLevel.Fatal: - this.Log().Fatal("[NuGet] " + message, args); + this.Log().Fatal(NuGet + message, args); break; case MessageLevel.Verbose: - this.Log().Info(ChocolateyLoggers.Verbose, "[NuGet] " + message, args); + this.Log().Info(ChocolateyLoggers.Verbose, NuGet + message, args); break; } } diff --git a/src/chocolatey/infrastructure.app/nuget/PackageManagerEx.cs b/src/chocolatey/infrastructure.app/nuget/PackageManagerEx.cs index f53e70668b..6a7e418469 100644 --- a/src/chocolatey/infrastructure.app/nuget/PackageManagerEx.cs +++ b/src/chocolatey/infrastructure.app/nuget/PackageManagerEx.cs @@ -25,7 +25,7 @@ public PackageManagerEx( /// /// Finds locally installed package - either in local repostory or via registry /// - public IPackage FindLocalPackage(string packageName) + public IPackage FindAnyLocalPackage(string packageName) { IPackage package = LocalRepository.FindPackage(packageName); if (package != null) diff --git a/src/chocolatey/infrastructure.app/services/NugetService.cs b/src/chocolatey/infrastructure.app/services/NugetService.cs index a49c77eec8..1380ea2b1a 100644 --- a/src/chocolatey/infrastructure.app/services/NugetService.cs +++ b/src/chocolatey/infrastructure.app/services/NugetService.cs @@ -493,7 +493,7 @@ public virtual ConcurrentDictionary install_run(Chocolate config = originalConfig.deep_copy(); //todo: get smarter about realizing multiple versions have been installed before and allowing that - IPackage installedPackage = packageManager.FindLocalPackage(packageName); + IPackage installedPackage = packageManager.FindAnyLocalPackage(packageName); if (installedPackage != null && (version == null || version == installedPackage.Version) && !config.Force) { @@ -536,7 +536,7 @@ Version was specified as '{0}'. It is possible that version @" Please see https://chocolatey.org/docs/troubleshooting for more assistance."); - + if (ApplicationParameters.runningUnitTesting) { logMessage = $"{packageName} not installed. The package was not found with the source(s) listed."; @@ -548,36 +548,7 @@ Version was specified as '{0}'. It is possible that version continue; } - // Figure out installation directory. - string targetDir = availablePackage.GetInstallLocation(); - if (string.IsNullOrEmpty(targetDir)) - { - targetDir = InstallContext.Instance.PackagesLocation; - }else - { - // properties use "$propertykey$", use '%' to avoid conflicts - targetDir = Regex.Replace(targetDir, "%(.*?)%", (m) => - { - Environment.SpecialFolder e; - string propKey = m.Groups[1].Value; - - // Using "%ProgramFiles%\yourcompany" can set install directory to program files - if (Enum.TryParse(propKey, out e)) - { - return Environment.GetFolderPath(e); - } - - // Using "%RootLocation%\plugins" can set install directory to plugins folder. - var prop = typeof(InstallContext).GetProperty(propKey, System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public); - if (prop != null && prop.PropertyType == typeof(string)) - { - return (string)prop.GetValue(InstallContext.Instance); - } - - return m.Value; - }); - } - packageManager.FileSystem.Root = targetDir; + packageManager.FileSystem.Root = GetInstallDirectory(availablePackage, availablePackage); if (installedPackage != null && (installedPackage.Version == availablePackage.Version) && config.Force) { @@ -605,39 +576,296 @@ Version was specified as '{0}'. It is possible that version } } - try + var installWalker = new WalkerInfo { - using (packageManager.SourceRepository.StartOperation( - RepositoryOperationNames.Install, - packageName, - version == null ? null : version.ToString())) + type = WalkerType.install, + ignoreDependencies = config.IgnoreDependencies, + allowPrereleaseVersions = config.Prerelease + }; + + SetPathResolver(packageManager, availablePackage, availablePackage); + DoOperation(installWalker, packageName, version, availablePackage, packageManager, packageInstalls, continueAction); + } + + OptimizedZipPackage.NuGetScratchFileSystem.DeleteDirectory(null, true); + return packageInstalls; + } + + /// + /// Gets package installation directory. + /// + /// Generally 'InstallLocation' tag determines where specific package gets installed - it also determines where + /// dependent packages will be installed as well. + /// + /// 'AddonsInstallFolder' determines into which folder all application addons (=dependencies) + /// will be installed (from 'InstallLocation' folder). + /// + /// Additionally '{selector}_InstallFolder' can enforce specific package location. + /// where selection can be just 'package id' or text + asterisk for multiple package id selection. + /// You can use '*plugin' to select 'firstplugin' & 'secondplugin' packages. + /// + /// Main package to be installed (which has dependencies) + /// Child package (who's install directory is determined by mainpackage) + /// sub package id as a string if subpackage is not given + public static string GetInstallDirectory(IPackage mainPackage, IPackage subpackage, string subpackageId = null) + { + // Figure out installation directory. + string targetDir = subpackage?.GetInstallLocation(); + + if (subpackage != null) + { + subpackageId = subpackage.Id; + } + + if (subpackage == null && mainPackage.Id == subpackageId) + { + subpackage = mainPackage; + } + + if (string.IsNullOrEmpty(targetDir) && mainPackage != subpackage) + { + targetDir = mainPackage.GetInstallLocation(); + + string addonsDirectory = mainPackage.GetKey($"{subpackageId}_InstallFolder"); + const string installFolderSuffix = "_InstallFolder"; + + if (string.IsNullOrEmpty(addonsDirectory)) + { + var keyValuePairs = mainPackage.GetKeyValuePairs(x => x.EndsWith(installFolderSuffix)); + + foreach (var kvpair in keyValuePairs) { - packageManager.InstallPackage(availablePackage, ignoreDependencies: config.IgnoreDependencies, allowPrereleaseVersions: config.Prerelease); - //packageManager.InstallPackage(packageName, version, configuration.IgnoreDependencies, configuration.Prerelease); - remove_nuget_cache_for_package(availablePackage); + string key = kvpair.Key; + key = key.Substring(0, key.Length - installFolderSuffix.Length); + bool takeEntry = false; + + if (!key.Contains("*")) + { + takeEntry = key == subpackageId; + } + else + { + takeEntry = Regex.IsMatch(subpackageId, "^" + Regex.Escape(key).Replace("\\*", ".*") + "$"); + } + + if(takeEntry) + { + addonsDirectory = kvpair.Value; + break; + } } } - catch (Exception ex) + + if (string.IsNullOrEmpty(addonsDirectory)) + { + addonsDirectory = mainPackage.GetKey("AddonsInstallFolder"); + } + + if (!string.IsNullOrEmpty(addonsDirectory)) { - var message = ex.Message; - var webException = ex as System.Net.WebException; - if (webException != null) + targetDir = Path.Combine(targetDir, addonsDirectory); + } + } + + if (string.IsNullOrEmpty(targetDir)) + { + targetDir = InstallContext.Instance.PackagesLocation; + } + else + { + // properties use "$propertykey$", use '%' to avoid conflicts + targetDir = Regex.Replace(targetDir, "%(.*?)%", (m) => + { + Environment.SpecialFolder e; + string propKey = m.Groups[1].Value; + + // Using "%ProgramFiles%\yourcompany" can set install directory to program files + if (Enum.TryParse(propKey, out e)) { - var response = webException.Response as HttpWebResponse; - if (response != null && !string.IsNullOrWhiteSpace(response.StatusDescription)) message += " {0}".format_with(response.StatusDescription); + return Environment.GetFolderPath(e); } - var logMessage = "{0} not installed. An error occurred during installation:{1} {2}".format_with(packageName, Environment.NewLine, message); - this.Log().Error(ChocolateyLoggers.Important, logMessage); - var errorResult = packageInstalls.GetOrAdd(packageName, new PackageResult(packageName, version.to_string(), null)); - errorResult.Messages.Add(new ResultMessage(ResultType.Error, logMessage)); - if (errorResult.ExitCode == 0) errorResult.ExitCode = 1; - if (continueAction != null) continueAction.Invoke(errorResult); + // Using "%RootLocation%\plugins" can set install directory to plugins folder. + var prop = typeof(InstallContext).GetProperty(propKey, System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public); + if (prop != null && prop.PropertyType == typeof(string)) + { + return (string)prop.GetValue(InstallContext.Instance); + } + + return m.Value; + }); + } + + return targetDir; + } + + /// + /// Set package path resolver - to either one folder when we know which package we will install and into which folder, + /// or resolving to multiple paths, when there are multiple packages in questions and they are installed into different folders. + /// + /// mainPackage which controls subpackages + /// currently installed main package + /// update package + void SetPathResolver(PackageManagerEx packageManager, IPackage mainPackage, IPackage updateSubPackage, IPackage mainInstalledPackage = null) + { + bool setMultiPathResolver = updateSubPackage != null && + mainInstalledPackage != null && + mainInstalledPackage.Id == updateSubPackage.Id; + + var packageRepo = (ChocolateyLocalPackageRepository)packageManager.LocalRepository; + + packageRepo.PackageIdToFilesystem.Clear(); + + string mainPackageDir; + bool mainPackageIsRegistryPackage; + + if (mainInstalledPackage != null && mainInstalledPackage is RegistryPackage regp) + { + // Installation can be seen from registry only + mainPackageDir = Path.GetDirectoryName(regp.GetPackageLocation()); + mainPackageIsRegistryPackage = true; + } + else + { + // Normal installation + mainPackageDir = GetInstallDirectory(mainPackage, updateSubPackage ?? mainPackage); + mainPackageIsRegistryPackage = false; + } + + if (!setMultiPathResolver) + { + packageManager.FileSystem.Root = mainPackageDir; + packageRepo.GetPackageInstallPath = null; + } + else + { + // Multiple packages, each is installed into it's own independent folder. + packageManager.FileSystem.Root = mainPackageDir; + + IPackage trueMainPackage = mainPackage; + if (mainPackageIsRegistryPackage) + { + // We need to get same Tags as in meta-data, registry does not keep this information currently. + trueMainPackage = packageManager.LocalRepository.FindPackagesById(mainPackage.Id).FirstOrDefault(); } + trueMainPackage ??= mainPackage; + + packageRepo.GetPackageInstallPath = (packageId) => + { + if (packageId == mainPackage.Id) + { + return mainPackageDir; + } + + return GetInstallDirectory(trueMainPackage, null, packageId); + }; } + } - OptimizedZipPackage.NuGetScratchFileSystem.DeleteDirectory(null, true); - return packageInstalls; + /// + /// Performs specific nuget operation (install / uninstall / update) + /// + /// available package + /// operation to perform + /// action executed before main nuget package operation + /// action executed after main nuget package operation + void DoOperation( + WalkerInfo walkerInfo, string packageName, SemanticVersion packageVersion, IPackage package, + PackageManagerEx packageManager, ConcurrentDictionary packageResults, + Action continueAction, + Action beforeOp = null, + Action afterOp = null + ) + { + string packageVersionStr = packageVersion?.ToString(); + + try + { + using (packageManager.SourceRepository.StartOperation(walkerInfo.type.ToString(), packageName, packageVersionStr) ) + { + if (beforeOp != null) + { + beforeOp(); + } + + if (walkerInfo.type == WalkerType.uninstall) + { + // package could be Registy package, need to find right one which is installed + package = packageManager.FindLocalPackage(package.Id.to_lower(), packageVersion); + } + + var walker = packageManager.GetWalker(walkerInfo); + var operations = walker.ResolveOperations(package); + + if (operations.Any()) + { + foreach (PackageOperation operation in operations) + { + SetPathResolver(packageManager, package, operation.Package); + packageManager.Execute(operation); + } + } + else + { + if (walkerInfo.type != WalkerType.uninstall) + { + packageManager.Logger.Log(MessageLevel.Verbose, $"'{package.GetFullName()}' already installed."); + } + } + + SetPathResolver(packageManager, package, null); + + if (afterOp != null) + { + afterOp(); + } + + remove_nuget_cache_for_package(package); + } + } + catch (Exception ex) + { + var message = ex.Message; + var webException = ex as WebException; + if (webException != null) + { + var response = webException.Response as HttpWebResponse; + if (response != null && !string.IsNullOrWhiteSpace(response.StatusDescription)) message += " {0}".format_with(response.StatusDescription); + } + + string logMessage; + string reportPackageName; + PackageResult reportPackageResult; + + switch (walkerInfo.type) + { + default: + case WalkerType.install: + logMessage = "not installed. An error occurred during installation"; + reportPackageName = packageName.to_lower(); + reportPackageResult = new PackageResult(packageName, packageVersionStr.to_string(), null); + break; + case WalkerType.update: + logMessage = "not upgraded. An error occurred during installation"; + reportPackageName = packageName.to_lower(); + reportPackageResult = new PackageResult(packageName, packageVersionStr.to_string(), null); + break; + case WalkerType.uninstall: logMessage = "not uninstalled. An error occurred during uninstall"; + reportPackageName = packageName.to_lower() + "." + packageVersion.to_string(); + reportPackageResult = new PackageResult(package, _fileSystem.combine_paths(ApplicationParameters.PackagesLocation, package.Id)); + break; + } + + logMessage = $"{packageName} {logMessage}:\n {message}"; + logMessage = InstallContext.NormalizeMessage(logMessage); + this.Log().Error(ChocolateyLoggers.Important, logMessage); + + var result = packageResults.GetOrAdd(reportPackageName, reportPackageResult); + result.Messages.Add(new ResultMessage(ResultType.Error, logMessage)); + + if (result.ExitCode == 0) result.ExitCode = 1; + if (continueAction != null) continueAction.Invoke(result); + } } public virtual void remove_rollback_directory_if_exists(string packageName) @@ -708,7 +936,7 @@ public virtual ConcurrentDictionary upgrade_run(Chocolate // reset config each time through config = originalConfig.deep_copy(); - IPackage installedPackage = packageManager.FindLocalPackage(packageName); + IPackage installedPackage = packageManager.FindAnyLocalPackage(packageName); if (installedPackage == null) { @@ -902,64 +1130,49 @@ public virtual ConcurrentDictionary upgrade_run(Chocolate if (performAction) { - if (installedPackage is RegistryPackage regp) + WalkerInfo walker = new WalkerInfo() + { + ignoreDependencies = config.IgnoreDependencies, + allowPrereleaseVersions = config.Prerelease, + updateDependencies = !config.IgnoreDependencies, + }; + + if (config.Force && (installedPackage.Version == availablePackage.Version)) { - packageManager.FileSystem.Root = Path.GetDirectoryName(regp.GetPackageLocation()); + walker.type = WalkerType.install; + } + else + { + walker.type = WalkerType.update; } - try + Action beforeOp = () => { - using (packageManager.SourceRepository.StartOperation( - RepositoryOperationNames.Update, - packageName, - version == null ? null : version.ToString())) + if (beforeUpgradeAction != null) { - if (beforeUpgradeAction != null) - { - var currentPackageResult = new PackageResult(installedPackage, get_install_directory(config, installedPackage)); - beforeUpgradeAction(currentPackageResult); - } - - remove_rollback_directory_if_exists(packageName); - ensure_package_files_have_compatible_attributes(config, installedPackage, pkgInfo); - rename_legacy_package_version(config, installedPackage, pkgInfo); - backup_existing_version(config, installedPackage, pkgInfo); - remove_shim_directors(config, installedPackage, pkgInfo); - if (config.Force && (installedPackage.Version == availablePackage.Version)) - { - FaultTolerance.try_catch_with_logging_exception( - () => - { - _fileSystem.delete_directory_if_exists(_fileSystem.combine_paths(ApplicationParameters.PackagesLocation, installedPackage.Id), recursive: true); - remove_cache_for_package(config, installedPackage); - }, - "Error during force upgrade"); - packageManager.InstallPackage(availablePackage, config.IgnoreDependencies, config.Prerelease); - } - else - { - packageManager.UpdatePackage(availablePackage, updateDependencies: !config.IgnoreDependencies, allowPrereleaseVersions: config.Prerelease); - } - remove_nuget_cache_for_package(availablePackage); + var currentPackageResult = new PackageResult(installedPackage, get_install_directory(config, installedPackage)); + beforeUpgradeAction(currentPackageResult); } - } - catch (Exception ex) - { - var message = ex.Message; - var webException = ex as System.Net.WebException; - if (webException != null) + + remove_rollback_directory_if_exists(packageName); + ensure_package_files_have_compatible_attributes(config, installedPackage, pkgInfo); + rename_legacy_package_version(config, installedPackage, pkgInfo); + backup_existing_version(config, installedPackage, pkgInfo); + remove_shim_directors(config, installedPackage, pkgInfo); + if (config.Force && (installedPackage.Version == availablePackage.Version)) { - var response = webException.Response as HttpWebResponse; - if (response != null && !string.IsNullOrWhiteSpace(response.StatusDescription)) message += " {0}".format_with(response.StatusDescription); + FaultTolerance.try_catch_with_logging_exception( + () => + { + _fileSystem.delete_directory_if_exists(_fileSystem.combine_paths(ApplicationParameters.PackagesLocation, installedPackage.Id), recursive: true); + remove_cache_for_package(config, installedPackage); + }, + "Error during force upgrade"); } + }; - var logMessage = "{0} not upgraded. An error occurred during installation:{1} {2}".format_with(packageName, Environment.NewLine, message); - logMessage = InstallContext.NormalizeMessage(logMessage); - this.Log().Error(ChocolateyLoggers.Important, logMessage); - packageResult.Messages.Add(new ResultMessage(ResultType.Error, logMessage)); - if (packageResult.ExitCode == 0) packageResult.ExitCode = 1; - if (continueAction != null) continueAction.Invoke(packageResult); - } + SetPathResolver(packageManager, availablePackage, availablePackage, installedPackage); + DoOperation(walker, packageName, version, availablePackage, packageManager, packageInstalls, continueAction, beforeOp); } } } @@ -984,7 +1197,7 @@ public virtual ConcurrentDictionary get_outdated(Chocolat // reset config each time through config = originalConfig.deep_copy(); - var installedPackage = packageManager.FindLocalPackage(packageName); + var installedPackage = packageManager.FindAnyLocalPackage(packageName); var pkgInfo = _packageInfoService.get_package_information(installedPackage); bool isPinned = pkgInfo.IsPinned; @@ -1384,7 +1597,7 @@ public virtual ConcurrentDictionary uninstall_run(Chocola } // is this the latest version, have you passed --sxs, or is this a side-by-side install? This is the only way you get through to the continue action. - var latestVersion = packageManager.FindLocalPackage(e.Package.Id); + var latestVersion = packageManager.FindAnyLocalPackage(e.Package.Id); var pkgInfo = _packageInfoService.get_package_information(e.Package); if (latestVersion.Version == pkg.Version || config.AllowMultipleVersions || (pkgInfo != null && pkgInfo.IsSideBySide)) { @@ -1548,50 +1761,48 @@ public virtual ConcurrentDictionary uninstall_run(Chocola if (performAction) { - if (packageVersion is RegistryPackage regp) - { - string packagesLocation = Path.GetDirectoryName(regp.GetPackageLocation()); - packageManager.FileSystem.Root = packagesLocation; - } - - try + Action beforeOp = () => { - using (packageManager.SourceRepository.StartOperation( - RepositoryOperationNames.Install, - packageVersion.Id, packageVersion.Version.to_string()) - ) + if (beforeUninstallAction != null) { - if (beforeUninstallAction != null) - { - // guessing this is not added so that it doesn't fail the action if an error is recorded? - //var currentPackageResult = packageUninstalls.GetOrAdd(packageName, new PackageResult(packageVersion, get_install_directory(config, packageVersion))); - var currentPackageResult = new PackageResult(packageVersion, get_install_directory(config, packageVersion)); - beforeUninstallAction(currentPackageResult); - } - ensure_package_files_have_compatible_attributes(config, packageVersion, pkgInfo); - rename_legacy_package_version(config, packageVersion, pkgInfo); - remove_rollback_directory_if_exists(packageName); - backup_existing_version(config, packageVersion, pkgInfo); - packageManager.UninstallPackage(packageVersion.Id.to_lower(), forceRemove: config.Force, removeDependencies: config.ForceDependencies, version: packageVersion.Version); - ensure_nupkg_is_removed(packageVersion, pkgInfo); - remove_installation_files(packageVersion, pkgInfo); - remove_cache_for_package(config, packageVersion); + // guessing this is not added so that it doesn't fail the action if an error is recorded? + //var currentPackageResult = packageUninstalls.GetOrAdd(packageName, new PackageResult(packageVersion, get_install_directory(config, packageVersion))); + var currentPackageResult = new PackageResult(packageVersion, get_install_directory(config, packageVersion)); + beforeUninstallAction(currentPackageResult); } - } - catch (Exception ex) + ensure_package_files_have_compatible_attributes(config, packageVersion, pkgInfo); + rename_legacy_package_version(config, packageVersion, pkgInfo); + remove_rollback_directory_if_exists(packageName); + backup_existing_version(config, packageVersion, pkgInfo); + }; + + Action afterOp = () => + { + ensure_nupkg_is_removed(packageVersion, pkgInfo); + remove_installation_files(packageVersion, pkgInfo); + }; + + var walker = new WalkerInfo + { + type = WalkerType.uninstall, + forceRemove = config.Force, + removeDependencies = config.ForceDependencies + }; + + Action continueUninstallAction = (r) => { - var logMessage = "{0} not uninstalled. An error occurred during uninstall:{1} {2}".format_with(packageName, Environment.NewLine, ex.Message); - logMessage = InstallContext.NormalizeMessage(logMessage); - this.Log().Error(ChocolateyLoggers.Important, logMessage); - var result = packageUninstalls.GetOrAdd(packageVersion.Id.to_lower() + "." + packageVersion.Version.to_string(), new PackageResult(packageVersion, _fileSystem.combine_paths(ApplicationParameters.PackagesLocation, packageVersion.Id))); - result.Messages.Add(new ResultMessage(ResultType.Error, logMessage)); - if (result.ExitCode == 0) result.ExitCode = 1; if (config.Features.StopOnFirstPackageFailure) { throw new ApplicationException("Stopping further execution as {0} has failed uninstallation".format_with(packageVersion.Id.to_lower())); } + }; + + SetPathResolver(packageManager, packageVersion, packageVersion, packageVersion); + DoOperation(walker, packageName, packageVersion.Version, packageVersion, packageManager, packageUninstalls, // do not call continueAction - will result in multiple passes - } + continueUninstallAction, + beforeOp, afterOp); + } else {