diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f72ed274..1bb44cd80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - #992: Implement automatic history purge logic - #973: Enables CORS and JWT configuration for WebApplications in module.xml +- #1034: Add -clean flag to remove source directory while uninstalling ### Fixed - #1001: The `unmap` and `enable` commands will now only activate CPF merge once after all namespaces have been configured instead after every namespace diff --git a/preload/cls/IPM/Installer.cls b/preload/cls/IPM/Installer.cls index 26970544b..25cfb3070 100644 --- a/preload/cls/IPM/Installer.cls +++ b/preload/cls/IPM/Installer.cls @@ -113,6 +113,7 @@ ClassMethod ZPMInit( $$$QuitOnError(##class(%IPM.Repo.UniversalSettings).SetValue("UseStandalonePip", "", 0)) $$$QuitOnError(##class(%IPM.Repo.UniversalSettings).SetValue("SemVerPostRelease", 0, 0)) $$$QuitOnError(##class(%IPM.Repo.UniversalSettings).SetValue("DefaultLogEntryLimit",20, 0)) + $$$QuitOnError(##class(%IPM.Repo.UniversalSettings).SetValue("CleanOnUninstall",0, 0)) quit $$$OK } diff --git a/src/cls/IPM/Lifecycle/Base.cls b/src/cls/IPM/Lifecycle/Base.cls index 8fef53d56..f5aa38b0e 100644 --- a/src/cls/IPM/Lifecycle/Base.cls +++ b/src/cls/IPM/Lifecycle/Base.cls @@ -492,6 +492,26 @@ Method %Clean(ByRef pParams) As %Status quit } } + // Remove the source directory while uninstalling the module. + if $data(pParams("Clean","clean")) ||(##class(%IPM.Repo.UniversalSettings).GetCleanOnUninstall()) { + set verbose = $get(pParams("Clean","Verbose")) + set moduleRootDir = ..Module.Root + set moduleName = ..Module.DisplayName + write !,"["_$namespace_"|"_moduleName_"]",$char(9),"Cleanup: Removing source directory" + if ##class(%File).DirectoryExists(moduleRootDir) { + if verbose { + write !,"["_$namespace_"|"_moduleName_"]",$char(9),"Found source directory: "_moduleRootDir + } + set tSC = ##class(%IPM.Utils.File).RemoveDirectoryTree(moduleRootDir) + if verbose { + write !,"["_$namespace_"|"_moduleName_"]",$char(9),"Source directory successfully deleted" + } + if $$$ISERR(tSC) { + write !,"["_$namespace_"|"_moduleName_"]",$char(9),"Source directory delete issue: "_$system.Status.GetErrorText(tSC) + $$$ThrowOnError(tSC) + } + } + } } catch e { set tSC = e.AsStatus() } diff --git a/src/cls/IPM/Main.cls b/src/cls/IPM/Main.cls index 64f7e2819..e09b46328 100644 --- a/src/cls/IPM/Main.cls +++ b/src/cls/IPM/Main.cls @@ -481,6 +481,7 @@ reinstall -env /path/to/env1.json;/path/to/env2.json example-package + @@ -2553,6 +2554,12 @@ ClassMethod Uninstall(ByRef pCommandInfo) [ Internal ] $$$ThrowOnError(..CheckModuleNamespace()) } set tRecurse = $$$HasModifier(pCommandInfo,"recurse") // Recursively uninstall unneeded dependencies + if $data(pCommandInfo("modifiers","clean")) { + set tParams("Clean","clean") = pCommandInfo("modifiers","clean") + } + if $get(pCommandInfo("data","Verbose")) { + set tParams("Clean","Verbose") = 1 + } $$$ThrowOnError(##class(%IPM.Storage.Module).Uninstall(tModuleName,tForce,tRecurse,.tParams)) } } diff --git a/src/cls/IPM/Repo/UniversalSettings.cls b/src/cls/IPM/Repo/UniversalSettings.cls index e1ff1f3ed..364afe4ff 100644 --- a/src/cls/IPM/Repo/UniversalSettings.cls +++ b/src/cls/IPM/Repo/UniversalSettings.cls @@ -45,7 +45,10 @@ Parameter SemVerPostRelease = "SemVerPostRelease"; /// to retain IPM history records before they are eligible for cleanup. Parameter HistoryRetain = "history_retain"; -Parameter CONFIGURABLE = "trackingId,analytics,ColorScheme,TerminalPrompt,PublishTimeout,PipCaller,UseStandalonePip,SemVerPostRelease,DefaultLogEntryLimit,HistoryRetain"; +/// Determines if the source folder should be physically deleted when a module is uninstalled. +Parameter CleanOnUninstall = "CleanOnUninstall"; + +Parameter CONFIGURABLE = "trackingId,analytics,ColorScheme,TerminalPrompt,PublishTimeout,PipCaller,UseStandalonePip,SemVerPostRelease,DefaultLogEntryLimit,HistoryRetain,CleanOnUninstall"; /// Returns configArray, that includes all configurable settings ClassMethod GetAll(Output configArray) As %Status @@ -185,9 +188,4 @@ ClassMethod SetAnalyticsAvailable( return ..SetValue(..#analytics, +val, overwrite) } -ClassMethod GetHistoryRetain() As %Integer -{ - return ..GetValue(..#HistoryRetain) -} - } diff --git a/tests/unit_tests/Test/PM/Unit/CLI.cls b/tests/unit_tests/Test/PM/Unit/CLI.cls index 8e9f2329a..bd78479de 100644 --- a/tests/unit_tests/Test/PM/Unit/CLI.cls +++ b/tests/unit_tests/Test/PM/Unit/CLI.cls @@ -227,6 +227,7 @@ Method RunCommand(pCommand As %String) As %Status return status } +/// This method returns the %Status for checking the success or failure of executed commands. Method AssertNoException(pCommand As %String) { do ##class(%IPM.Main).ShellInternal(pCommand,.tException) @@ -352,4 +353,75 @@ Method TestUninstallWithoutModuleName() do $$$AssertNotTrue(exists, "Module removed successfully.") } +Method TestUninstallWithCleanModifier() +{ + set module = "objectscript-math" + do $$$LogMessage("Testing: Uninstall with -clean flag to remove source directory.") + + //TestRepository method removed the registry configuration. So reconfigure while testing + do ##class(%IPM.Main).GetVersion("zpm",.out) + if $get(out)="" { + do $$$LogMessage("No registry configured. So started configuring https://pm.community.intersystems.com") + set status = ..RunCommand("repo -remote -n registry -url https://pm.community.intersystems.com/ -user """" -pass """"") + do $$$AssertStatusOK(status, "Registory configured successfully") + } + + // 1. Setup: Ensure module is installed + if '##class(%IPM.Storage.Module).NameExists(module) { + do $$$LogMessage("Module "_module_" not found. Installing...") + set status = ..RunCommand("install "_module) + do $$$AssertStatusOK(status, "Setup: Module installed successfully.") + } + + // 2. Capture the root directory path + set moduleObj = ##class(%IPM.Storage.Module).NameOpen(module) + set moduleRootDir = moduleObj.Root + do $$$AssertNotEquals(moduleRootDir, "", "Verified module root directory path: "_moduleRootDir) + + // 3. Test Standard Uninstall (Should NOT delete directory) + do $$$LogMessage("Step 1: Uninstalling "_module_" without -clean flag.") + set status = ..RunCommand("uninstall "_module) + do $$$AssertStatusOK(status, "Standard uninstall completed.") + + set dirExists = ##class(%File).DirectoryExists(moduleRootDir) + do $$$AssertTrue(dirExists, "Verification: Directory still exists after standard uninstall (as expected).") + + // 4. Re-install for Clean Test + do $$$LogMessage("Step 2: Re-installing for -clean flag test.") + set status = ..RunCommand("install "_module) + do $$$AssertStatusOK(status, "Module re-installed successfully.") + + // 5. Test Uninstall with -c (Should delete directory) + do $$$LogMessage("Step 3: Uninstalling "_module_" with -clean flag.") + set status = ..RunCommand("uninstall "_module_" -c") + do $$$AssertStatusOK(status, "Clean uninstall completed.") + + set dirDeleted = '##class(%File).DirectoryExists(moduleRootDir) + do $$$AssertTrue(dirDeleted, "Verification: Source directory was physically deleted from the file system.") + + do $$$LogMessage("Repeating this test for the UnvierslSettings 'CleanOnUninsall'") + // ZPMInit method configuratio from installer + /// bydefualt keep this flag as 0 + set status = ##class(%IPM.Repo.UniversalSettings).SetValue("CleanOnUninstall",0, 0) + do $$$AssertStatusOK(status, "initial setup for CleanOnUninstall done.") + + do $$$LogMessage("Step 4: Re-installing for CleanOnUninstall settings test.") + set status = ..RunCommand("install "_module) + do $$$AssertStatusOK(status, "Module re-installed successfully.") + + do $$$LogMessage("Step 5: Updating the CleanOnUninstall settings to 1.") + set status = ##class(%IPM.Repo.UniversalSettings).UpdateOne("CleanOnUninstall",1) + do $$$AssertStatusOK(status, "Updated the CleanOnUninstall to 1.") + + do $$$LogMessage("Step 6: Uninstall the module without -clean flag") + set status = ..RunCommand("uninstall "_module) + do $$$AssertStatusOK(status, "Clean uninstall completed.") + + set dirDeleted = '##class(%File).DirectoryExists(moduleRootDir) + do $$$AssertTrue(dirDeleted, "Verification: Source directory was physically deleted from the file system.") + + do ##class(%IPM.Repo.UniversalSettings).ResetToDefault("CleanOnUninstall") + do $$$LogMessage("CleanOnUninstall configuration restored") +} + }