From 8dd8ff3c4c4dd4ae5ec3a4862826d7015cf16b9a Mon Sep 17 00:00:00 2001 From: AshokThangavel Date: Mon, 12 Jan 2026 15:55:45 +0530 Subject: [PATCH 1/2] feat: add -clean flag and CleanOnUninstall config for source directory removal - Implemented '-c' / '-clean' flag for the uninstall command. - Added 'CleanOnUninstall' global configuration in UniversalSettings. - Added recursive support for cleaning dependency directories. - Included verbose logging to show specific paths being deleted. --- preload/cls/IPM/Installer.cls | 1 + src/cls/IPM/Lifecycle/Base.cls | 20 +++++++ src/cls/IPM/Main.cls | 7 +++ src/cls/IPM/Repo/UniversalSettings.cls | 10 +++- tests/unit_tests/Test/PM/Unit/CLI.cls | 76 +++++++++++++++++++++++++- 5 files changed, 111 insertions(+), 3 deletions(-) diff --git a/preload/cls/IPM/Installer.cls b/preload/cls/IPM/Installer.cls index 3d3427191..c55925e6e 100644 --- a/preload/cls/IPM/Installer.cls +++ b/preload/cls/IPM/Installer.cls @@ -112,6 +112,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 e43a847c1..9d0ff18cf 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 0cbe438dd..8a2824416 100644 --- a/src/cls/IPM/Main.cls +++ b/src/cls/IPM/Main.cls @@ -480,6 +480,7 @@ reinstall -env /path/to/env1.json;/path/to/env2.json example-package + @@ -2455,6 +2456,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 ce178229f..19cf18760 100644 --- a/src/cls/IPM/Repo/UniversalSettings.cls +++ b/src/cls/IPM/Repo/UniversalSettings.cls @@ -41,7 +41,10 @@ Parameter UseStandalonePip = "UseStandalonePip"; /// Default value is 0, where 1.0.0-anystring is considered a pre-release of 1.0.0, hence 1.0.0-anystring < 1.0.0 Parameter SemVerPostRelease = "SemVerPostRelease"; -Parameter CONFIGURABLE = "trackingId,analytics,ColorScheme,TerminalPrompt,PublishTimeout,PipCaller,UseStandalonePip,SemVerPostRelease,DefaultLogEntryLimit"; +/// 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,CleanOnUninstall"; /// Returns configArray, that includes all configurable settings ClassMethod GetAll(Output configArray) As %Status @@ -181,4 +184,9 @@ ClassMethod SetAnalyticsAvailable( return ..SetValue(..#analytics, +val, overwrite) } +ClassMethod GetCleanOnUninstall() As %String [ CodeMode = expression ] +{ +..GetValue(..#CleanOnUninstall) +} + } diff --git a/tests/unit_tests/Test/PM/Unit/CLI.cls b/tests/unit_tests/Test/PM/Unit/CLI.cls index a25bcd032..28bd9934b 100644 --- a/tests/unit_tests/Test/PM/Unit/CLI.cls +++ b/tests/unit_tests/Test/PM/Unit/CLI.cls @@ -219,10 +219,11 @@ Method CompareModifiers( } } -Method RunCommand(pCommand As %String) +Method RunCommand(pCommand As %String) As %Status { - do ##class(%IPM.Main).Shell(pCommand) + set status = ##class(%IPM.Main).Shell(pCommand) do $$$LogMessage("Run command: "_pCommand) + return status } Method AssertNoException(pCommand As %String) @@ -313,4 +314,75 @@ Method TestListPython() As %Status quit sc } +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") +} + } From d9f3df867711385ba085e824cf78bdd2694bfc3c Mon Sep 17 00:00:00 2001 From: AshokThangavel Date: Mon, 12 Jan 2026 16:04:11 +0530 Subject: [PATCH 2/2] docs: Add entry in changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6374d038..bf628f6c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - #961: Adding creation of a lock file for a module by using the `-create-lockfile` flag on install. - #959: In ORAS repos, external name can now be used interchangeably with (default) name for `install` and `update`, i.e. a module published with its (default) name can be installed using its external name. - #951: The `unpublish` command will skip user confirmation prompt if the `-force` flag is provided. +- #1034: Add -clean flag to remove source directory while uninstalling ### Changed - #316: All parameters, except developer mode, included with a `load`, `install` or `update` command will be propagated to dependencies