public inbox for buildroot@busybox.net
 help / color / mirror / Atom feed
* [Buildroot] [PATCH] package/pkg-generic: add support for uninstalling packages
@ 2026-03-17  7:49 Shubham Chakraborty
  2026-03-17  7:58 ` Thomas Petazzoni via buildroot
  0 siblings, 1 reply; 6+ messages in thread
From: Shubham Chakraborty @ 2026-03-17  7:49 UTC (permalink / raw)
  To: buildroot, Thomas Petazzoni; +Cc: Shubham Chakraborty

Buildroot currently does not track which files are installed by each
package in output/staging, output/target, output/host, and
output/images directories. This makes it impossible to cleanly remove
a package without performing a full rebuild.

This commit adds:

1. A new Python script (support/scripts/pkg-uninstall.py) that reads
   manifest files (.files-list*.txt) tracking package file installations
   and removes all files belonging to a specified package from a given
   directory.

   The script handles:
   - Regular files and symlinks (both to files and directories)
   - Broken symlinks
   - Empty directory cleanup (deepest first)
   - Safe removal that prevents deletion outside the base directory
   - Graceful handling of missing manifests

2. A new 'make <pkg>-uninstall' target in pkg-generic.mk that:
   - Displays an "Uninstalling" message
   - Removes the package's install stamp files
   - Calls pkg-uninstall.py for each output directory:
     * TARGET_DIR (target rootfs)
     * STAGING_DIR (staging for libraries/headers)
     * HOST_DIR (host tools)
     * BINARIES_DIR (images like kernel/bootloader)
   - Updates size tracking baselines for each directory
   - Cleans up the manifest files after uninstallation

Usage:
  make <package-name>-uninstall

This enables selective package removal without requiring a complete
rebuild of the entire system.

Signed-off-by: Shubham Chakraborty <chakrabortyshubham66@gmail.com>
---
 package/pkg-generic.mk           | 26 +++++++++++-
 support/scripts/pkg-uninstall.py | 71 ++++++++++++++++++++++++++++++++
 2 files changed, 96 insertions(+), 1 deletion(-)
 create mode 100755 support/scripts/pkg-uninstall.py

diff --git a/package/pkg-generic.mk b/package/pkg-generic.mk
index dd440e4062..db058b86a3 100644
--- a/package/pkg-generic.mk
+++ b/package/pkg-generic.mk
@@ -1043,6 +1043,29 @@ $(1)-all-legal-info:	$$(foreach p,$$($(2)_FINAL_ALL_DEPENDENCIES),$$(p)-all-lega
 
 $(1)-dirclean:		$$($(2)_TARGET_DIRCLEAN)
 
+$(1)-uninstall: PKG=$(2)
+$(1)-uninstall:
+	@$$(call MESSAGE,"Uninstalling")
+	# Remove stamps first (fault-tolerant ordering)
+	rm -f $$($(2)_TARGET_INSTALL)
+	rm -f $$($(2)_TARGET_INSTALL_STAGING)
+	rm -f $$($(2)_TARGET_INSTALL_TARGET)
+	rm -f $$($(2)_TARGET_INSTALL_IMAGES)
+	rm -f $$($(2)_TARGET_INSTALL_HOST)
+	# Uninstall files from all locations
+	$$(Q)support/scripts/pkg-uninstall.py -p $$($(2)_NAME) -d $$(TARGET_DIR) -l $$($(2)_DIR)/.files-list.txt
+	$$(Q)support/scripts/pkg-uninstall.py -p $$($(2)_NAME) -d $$(STAGING_DIR) -l $$($(2)_DIR)/.files-list-staging.txt
+	$$(Q)support/scripts/pkg-uninstall.py -p $$($(2)_NAME) -d $$(HOST_DIR) -l $$($(2)_DIR)/.files-list-host.txt
+	$$(Q)support/scripts/pkg-uninstall.py -p $$($(2)_NAME) -d $$(BINARIES_DIR) -l $$($(2)_DIR)/.files-list-images.txt
+	# Update baselines to reflect post-uninstall state
+	@$$(call pkg_size_before,$$(TARGET_DIR))
+	@$$(call pkg_size_before,$$(STAGING_DIR),-staging)
+	@$$(call pkg_size_before,$$(HOST_DIR),-host)
+	@$$(call pkg_size_before,$$(BINARIES_DIR),-images)
+	# Clean up manifests
+	rm -f $$($(2)_DIR)/.files-list.txt $$($(2)_DIR)/.files-list-staging.txt $$($(2)_DIR)/.files-list-host.txt $$($(2)_DIR)/.files-list-images.txt
+
+
 $(1)-clean-for-reinstall:
 ifneq ($$($(2)_OVERRIDE_SRCDIR),)
 			rm -f $$($(2)_TARGET_RSYNC)
@@ -1304,7 +1327,8 @@ DL_TOOLS_DEPENDENCIES += $$(call extractor-system-dependency,$$($(2)_SOURCE))
 	$(1)-show-depends \
 	$(1)-show-info \
 	$(1)-show-version \
-	$(1)-source
+	$(1)-source \
+	$(1)-uninstall
 
 ifneq ($$($(2)_SOURCE),)
 ifeq ($$($(2)_SITE),)
diff --git a/support/scripts/pkg-uninstall.py b/support/scripts/pkg-uninstall.py
new file mode 100755
index 0000000000..3ef3f7bd7c
--- /dev/null
+++ b/support/scripts/pkg-uninstall.py
@@ -0,0 +1,71 @@
+#!/usr/bin/env python3
+
+import sys
+import os
+import argparse
+
+def main():
+    parser = argparse.ArgumentParser(description="Cleanly uninstall a package's files")
+    parser.add_argument("-p", "--package", required=True, help="Package name")
+    parser.add_argument("-d", "--dir", required=True, help="Base directory (e.g. output/target)")
+    parser.add_argument("-l", "--list", required=True, help="Path to .files-list.txt")
+    
+    args = parser.parse_args()
+    
+    if not os.path.exists(args.list):
+        # We don't error out because a package might not install files in all locations
+        # (e.g. no staging or no images), but we print a notice for debugging.
+        print(f"Notice: Manifest not found, skipping: {args.list}")
+        return
+        
+    with open(args.list, "r") as f:
+        lines = f.readlines()
+        
+    directories = set()
+    
+    # Remove files
+    for line in lines:
+        line = line.strip()
+        if not line:
+            continue
+            
+        parts = line.split(",", 1)
+        if len(parts) != 2:
+            continue
+            
+        pkg, rel_path = parts
+        if pkg != args.package:
+            continue
+            
+        if rel_path.startswith("./"):
+            rel_path = rel_path[2:]
+            
+        full_path = os.path.join(args.dir, rel_path)
+        
+        if os.path.lexists(full_path) and (not os.path.isdir(full_path) or os.path.islink(full_path)):
+            try:
+                os.remove(full_path)
+                print(f"Removed: {full_path}")
+                directories.add(os.path.dirname(full_path))
+            except OSError as e:
+                print(f"Failed to remove {full_path}: {e}", file=sys.stderr)
+        elif not os.path.lexists(full_path):
+            # If it doesn't exist, we still might want to clean up its parent dir
+            directories.add(os.path.dirname(full_path))
+                
+    # Remove empty directories, deeper first
+    sorted_dirs = sorted(list(directories), key=len, reverse=True)
+    for d in sorted_dirs:
+        # Don't try to remove the base dir or outside it
+        if not d or d == args.dir or not os.path.abspath(d).startswith(os.path.abspath(args.dir)):
+            continue
+        try:
+            # Atomic attempt to remove directory if empty
+            os.rmdir(d)
+            print(f"Removed empty dir: {d}")
+        except OSError:
+            # Directory not empty or other error, safe to ignore
+            pass
+
+if __name__ == "__main__":
+    main()
-- 
2.53.0

_______________________________________________
buildroot mailing list
buildroot@buildroot.org
https://lists.buildroot.org/mailman/listinfo/buildroot

^ permalink raw reply related	[flat|nested] 6+ messages in thread
[parent not found: <mailman.9.1773748801.2109898.buildroot@buildroot.org>]

end of thread, other threads:[~2026-03-18  7:45 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-03-17  7:49 [Buildroot] [PATCH] package/pkg-generic: add support for uninstalling packages Shubham Chakraborty
2026-03-17  7:58 ` Thomas Petazzoni via buildroot
2026-03-17  8:11   ` Shubham Chakraborty
2026-03-17  9:08     ` Thomas Petazzoni via buildroot
     [not found] <mailman.9.1773748801.2109898.buildroot@buildroot.org>
2026-03-18  4:43 ` Andreas Ziegler
2026-03-18  7:44   ` Shubham Chakraborty

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox