upgrade-mods: Update slapt-mod modules

New features which should be implemented in Porteus; suggestions are welcome. All questions or problems with testing releases (alpha, beta, or rc) should go in their relevant thread here, rather than the Bug Reports section.
pterid
Contributor
Contributor
Posts: 105
Joined: 01 Feb 2025, 20:13
Distribution: Porteus 5.01 Xfce on ext4 USB

slapt-mod-upgrade: Update modules

Post#31 by pterid » 15 Mar 2026, 00:06

Ed_P wrote:
14 Mar 2026, 19:16
Normally the word "upgrade" implies "replacement" of the original version and in the Porteus world a new replacement module, or new version of Porteus. :D The very enhanced slapt-mod-upgrade script doesn't replace or upgrade the existing Porteus' slapt-mod module, or app, it's an enhanced version of the existing version. So maybe naming it slapt-mod-supreme or slapt-mod-enhanced or slapt-mod-II would be a less confusing name for new, or senior, users. :) :pardon:
It replaces modules with upgraded versions of those modules. I do think upgrade is probably the right word for that? Anyway I do not much care what it is called, only that it works well and provides a function that some people want to use!

rych
Warlord
Warlord
Posts: 868
Joined: 04 Jan 2014, 04:27
Distribution: Porteus 5.0 x64 OpenBox
Location: NZ
Contact:

slapt-mod-upgrade: Update modules

Post#32 by rych » 15 Mar 2026, 02:45

pterid wrote:
14 Mar 2026, 12:56
whether it is good practice in Porteus to mess around with activated modules is another matter! It seems dodgy, so it might be best for slapt-mod-upgrade if we build in some checks for activation, and at least warn before renaming or deleting an activated module.
Good news for us, as confirmed by a thorough AI search: It's OKAY to simply delete/rename the old/previous xzm while it's activated: the current mount is unaffected, and perfect for after reboot.

Read the whole story below:
AI Search wrote: ---

## What Happens When You Delete an Activated .xzm Module in Porteus?

This is a genuinely nuanced question that operates at the intersection of Linux inode semantics, the loop device subsystem, squashfs read-on-demand behavior, and Porteus's AUFS/overlay layer. Let me take it apart carefully.

---

### First: How Module Activation Actually Works

When you activate a .xzm module in Porteus, the system first creates a directory at `/mnt/live/memory/images/` (named after the module), then loop-mounts the .xzm file into it with `mount -o loop module.xzm directory`, and finally inserts that directory as a read-only AUFS branch into root `/`:

```
mount -n -o remount,add:1:$dir=rr aufs /
```

So the full stack is: **the .xzm file on disk → `/dev/loopN` → squashfs mount → AUFS branch → your live `/`**. This chain is important for understanding every question you asked.

When you activate a .xzm module its files don't get loaded into RAM wholesale — instead the module is mounted just like an ISO, keeping in RAM only the metadata list of files available. Any time a given file is actually required, it gets loaded into RAM on demand (decompressed from the squashfs blocks as needed). The loop device keeps a file descriptor open to the backing .xzm file on disk and serves block reads through it.

---

### What Actually Happens on `rm` (Kernel-Level Mechanics)

According to the Linux `unlink()` man page: if the name was the last link to a file but any processes still have the file open, the file will remain in existence until the last file descriptor referring to it is closed.

More precisely, deleting a file via `unlink()` just lowers the link count and removes the directory entry. Closing open file descriptors is what lowers the reference count. The kernel keeps both a link count and a reference count for each active inode; an inode is only marked as unused and the filesystem told to free its space when *both* counts go to zero.

So when you `rm` the .xzm file:

1. **The filename disappears immediately** from the directory. It will no longer show up in `ls`.
2. **The inode's link count drops to 0** — but the loop device driver's open file descriptor holds the reference count above zero.
3. **The inode and all its data blocks remain fully intact** on disk. The kernel simply marks them as "orphaned" — no directory entry points to them, but they are alive.
4. The data persists until the last open handle closes. This is why disk space usage doesn't decrease immediately after deleting large files — the blocks are still occupied.
5. **The loop device `/dev/loopN` continues functioning normally**, squashfs keeps serving files on demand from the loop device, and all your activated module's files remain visible through AUFS as if nothing happened.

**In short: yes, the currently running session is completely unaffected.** The module keeps working. Applications launched from it keep working. Nothing breaks.

---

### Is There a "Lock" Protecting the File?

Not in the traditional advisory-lock sense. There is no `flock()` or `LOCK_EX`. Instead, it is the **kernel reference-counting mechanism** that protects it. The loop device holds an open file descriptor — and by the fundamental POSIX/Linux contract, if one or more processes have the file open when the last link is removed, the link shall be removed before `unlink()` returns, but the removal of the file contents shall be postponed until all references to the file are closed.

So it's not a lock per se — it's an automatic, guaranteed kernel contract. The file *cannot* be physically destroyed while anything holds it open.

---

### Is the Data "Backed by RAM" in the Meantime?

**Partially, and on-demand.** Squashfs does not pre-load the whole file into RAM. It reads and decompresses blocks as they are needed. The Linux page cache (part of RAM) may hold *recently accessed* squashfs blocks — but the authoritative backing store remains the inode's disk blocks. The loop device reads from them transparently when a needed block isn't in the page cache.

**Important exception — `copy2ram` mode:** Porteus's `copy2ram` boot option copies all .xzm modules to RAM without extracting them. Then it works the same way, but loading from the modules in RAM rather than from the storage unit. When `copy2ram` is used, modules are placed in `/mnt/live/memory/copy2ram` on tmpfs. In this case, the backing store is **RAM/tmpfs**, not your USB/disk at all. Deleting the .xzm from disk has no effect whatsoever on the running session — it was already fully in RAM.

For modules activated **during the live session** (not at boot), Porteus mounts them from wherever they physically reside on your storage. The loop device's backing file is the on-disk .xzm — so the analysis above applies directly.

---

### Does It Persist Until Restart?

**Yes.** The orphaned inode (deleted-but-open .xzm) survives with full functionality until one of the following:

- **Clean shutdown/reboot**: The shutdown sequence unmounts aufs first, then the squashfs mount, then releases the loop device, which closes the file descriptor. With the last reference gone, the kernel reclaims the inode and data blocks. Clean. Done.
- **Crash/hard power-off**: The inode may not be properly freed before power dies (see below).

---

### The "Garbage" Question — Orphan Inodes

This is where it gets more nuanced. While the file is deleted-but-open, journaling filesystems track the inode in an **orphan list** in the journal. This is by design.

After a system reboot, messages like `Clearing orphaned inode 408612` may appear in logs — this is the filesystem cleaning up inodes that had their link count reach zero but were not yet freed when the system shut down or crashed.

**On clean shutdown:** The loop device releases cleanly, the inode is freed properly at runtime, and the orphan list is never written to disk in a dirty state. No cleanup needed.

**On crash (hard power-off/kernel panic):** If the system dies while the inode is in the orphan state, the journaling filesystem records this. On remount, you may see kernel messages like `EXT4-fs: orphan cleanup` followed by `N orphan inodes deleted` — this is the filesystem's automatic journal recovery doing exactly what it is supposed to do.

Clearing an orphan inode or recovering the journal on boot means the shutdown was not clean and the journal doesn't match what is actually on disk — but this is handled automatically by fsck/journal recovery, not a sign of lasting damage.

**However — important caveat for Porteus users on FAT32/exFAT USB drives:**

FAT and exFAT do not have journal-based orphan tracking. If the .xzm's backing partition is FAT32 (very common for Porteus USB sticks), the kernel still holds the inode alive in-memory and continues operating correctly. But if a crash occurs before the loop device releases the file descriptor, FAT has no orphan list to consult on next boot — the behavior is essentially filesystem-specific. In practice, since squashfs is read-only (squashfs is always read-only), there are no pending writes to the .xzm data blocks, so data corruption of the .xzm file itself won't happen. The FAT directory entry was removed by `rm`. The data blocks may end up as leaked clusters (similar to what `chkdsk` or `fsck.fat` calls "lost chains"), though in practice they're often silently reclaimed when the OS releases them on clean shutdown.

For **Linux-formatted partitions (ext4)**, it's a non-issue: the orphan list cleanly handles it.

---

### Summary Table

| Scenario | What Happens |
|---|---|
| `rm module.xzm` while activated | Filename gone, inode/data survives via loop fd |
| Running session | **Fully intact.** Module works normally |
| Disk space freed? | **No** — not until loop device releases the fd |
| Module visible in filesystem? | **No** — but AUFS branch still active |
| On clean reboot | Loop device closes, inode freed, blocks reclaimed — **zero garbage** |
| On crash (ext4) | Orphan inode recorded in journal; cleaned automatically on next mount |
| On crash (FAT32) | Possible leaked clusters; `fsck.fat` can clean them; read-only squashfs means no data corruption |
| Module loads at next boot? | **No** — the file is gone; won't auto-activate |

---

### One Nuance Specific to Porteus

If the module is active, you can view the files in read-only format at `/mnt/live/memory/images/module-name.xzm/` — that directory remains intact and operational even after you delete the .xzm backing file, for exactly the reasons above.

Also worth knowing: modules activated during the live session are relocated to `/mnt/live/memory/modules/` in RAM before activation, and disappear from their original location in some Porteus configurations — so in those cases the backing store may already be in tmpfs anyway, making the whole question moot from a disk-persistence standpoint.

---

**Bottom line**: Deleting an activated .xzm is safe for the current session — it keeps working transparently thanks to Unix inode semantics. No garbage accumulates on a clean reboot. A crash could leave orphan inode artifacts, but ext4 handles those automatically, and since squashfs is read-only there's no data corruption risk. The only lasting consequence is that the module won't be there the next time you boot.

rych
Warlord
Warlord
Posts: 868
Joined: 04 Jan 2014, 04:27
Distribution: Porteus 5.0 x64 OpenBox
Location: NZ
Contact:

slapt-mod-upgrade: Update modules

Post#33 by rych » 15 Mar 2026, 02:49

pterid wrote:
15 Mar 2026, 00:06
upgrade is probably the right word for that?
A more correct word here would be update :) When I had to replace (update) the old tires on my car, I decided to invest into a higher class, wider tires: I upgraded the tires :)

rych
Warlord
Warlord
Posts: 868
Joined: 04 Jan 2014, 04:27
Distribution: Porteus 5.0 x64 OpenBox
Location: NZ
Contact:

slapt-mod-upgrade: Update modules

Post#34 by rych » 15 Mar 2026, 03:13

pterid wrote:
14 Mar 2026, 15:05
get the new module
your great script (thank you for doing this project!) is a single file affair, is it not? IMHO, it doesn't need to be packaged into an xzm, and then activated and so on.

Could we just get it once, put it in our home directory and whatever, and then let it self-update by running, for example:

Code: Select all

slapt-mod-update -u
or

Code: Select all

slapt-mod-update --self
It's a bit tricky to update in-place though. We can rename the old script, even while it's running, from within itself: slapt-mod-update-->slapt-mod-update.old. But when can we rename it to the original name: slapt-mod-update.new-->slapt-mod-update?! I'm going to have to learn it myself and implement it in all my github-hosted scripts. Nowadays, we shouldn't expect the user to have the mental real-state to keep going to some github or forum to look for the new version of each App.

For now, let's read the AI research, (I don't yet fully understand it myself): https://claude.ai/share/35a8b1d7-0c1a-4 ... 2be3a034d9
Claude suggests the code below, full story at the above link if you can see it?

Code: Select all

# 1. Download/write the new version to a temp file
TMP=$(mktemp /tmp/slapt-mod-update.XXXXX)
curl -o "$TMP" "$UPDATE_URL"
chmod +x "$TMP"

# 2. Atomically replace the name on disk
mv "$TMP" "$(readlink -f "$0")"

# 3. exec into the new version (replaces this process)
# But first Remove --self from the args array before re-launching -- added to avoid endless loop :)
exec "$0" "${@/--self/}"
exec "$(readlink -f "$0")" "$@"
ACTUALLY, I now better like its
Option C — Version-gate it (most elegant):
bash# The new script only self-updates if remote version > current version
# After the mv+exec, the new version IS current, so the version
# check fails ("already up to date") and it falls through to normal execution
Option C is arguably the cleanest because it requires zero special flag handling — the version comparison is the natural termination condition. This is exactly how the gist pattern worked: it compares `VERSION‘with‘VERSION` with `
VERSION‘with‘NEW_VER`, and only proceeds if newer. After `exec`, the running version *is* the new version, so the check quietly exits the update branch.
No need for the new --self or -u flags to self-update I guess. Your script is already designed to do a lot of downloading and checking, and take a lot of time, so let it always check for its own version from your github first, and replace itself in-place when a newer version is released? Cool, isn't it? (Always double-check Claude's code, by using Claude :)

Code: Select all

#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────
#  slapt-mod-update — self-update header
#  Place this at the very top, before any other logic.
# ─────────────────────────────────────────────────────────────

SELF_VERSION="1.0.0"
SELF_URL="https://raw.githubusercontent.com/youruser/yourrepo/main/slapt-mod-update"
SELF_PATH="$(readlink -f "$0")"

_self_update() {
    local remote_ver tmp

    # Fetch just the version line — cheap, no full download yet
    remote_ver=$(curl -fsSL "$SELF_URL" 2>/dev/null \
                 | grep -m1 '^SELF_VERSION=' \
                 | cut -d'"' -f2)

    # If we couldn't reach GitHub, just continue with current version
    [[ -z "$remote_ver" ]] && return 0

    # Compare: if remote == current, nothing to do
    [[ "$remote_ver" == "$SELF_VERSION" ]] && return 0

    # Remote is different (newer) — download full new script
    tmp=$(mktemp /tmp/slapt-mod-update.XXXXX) || return 1
    if curl -fsSL "$SELF_URL" -o "$tmp"; then
        chmod +x "$tmp"
        mv "$tmp" "$SELF_PATH"          # atomic rename — safe while running
        echo "[slapt-mod-update] Updated $SELF_VERSION → $remote_ver — restarting..."
        exec "$SELF_PATH" "$@"          # re-exec with ORIGINAL args, no extra flags
                                        # new version's SELF_VERSION now matches remote
                                        # so the check above exits cleanly next time
    else
        rm -f "$tmp"
        echo "[slapt-mod-update] Update download failed, continuing with $SELF_VERSION" >&2
    fi
}

_self_update "$@"

# ─────────────────────────────────────────────────────────────
#  Rest of slapt-mod-update begins here
# ─────────────────────────────────────────────────────────────
Last edited by rych on 15 Mar 2026, 03:44, edited 1 time in total.

User avatar
Ed_P
Contributor
Contributor
Posts: 9285
Joined: 06 Feb 2013, 22:12
Distribution: Cinnamon 5.01 ISO
Location: Western NY, USA

slapt-mod-upgrade: Update modules

Post#35 by Ed_P » 15 Mar 2026, 03:43

pterid wrote:
15 Mar 2026, 00:06
Anyway I do not much care what it is called, only that it works well and provides a function that some people want to use!
Google wrote:up·grade
verb
/ˈəpˌɡrād,ˌəpˈɡrād/
raise (something) to a higher standard, in particular improve (equipment or machinery) by adding or replacing components.
I installed the script, ran it with the -u option, ran slapt-mod and it ran same as before. Nothing changed or enhanced or updated.

I don't wish to imply your script isn't good or useful. It's impressive and the time and effort you've put into it is incredible. Thank you for both. But the name is misleading. imo
rych wrote:
15 Mar 2026, 02:49
When I had to replace (update) the old tires on my car, I decided to invest into a higher class, wider tires: I upgraded the tires
Yes, you upgraded by replacing. slapt-mod-upgrade doesn't replace anything of the existing slapt-mod module in Porteus. Unless I'm doing something wrong. :pardon:

rych
Warlord
Warlord
Posts: 868
Joined: 04 Jan 2014, 04:27
Distribution: Porteus 5.0 x64 OpenBox
Location: NZ
Contact:

slapt-mod-upgrade: Update modules

Post#36 by rych » 15 Mar 2026, 03:49

Ed_P, we're not ready for you here yet, be patient :) pterid may roll out a new name and logic that satisfy both of us I'm sure in due time :)

User avatar
ncmprhnsbl
DEV Team
DEV Team
Posts: 4561
Joined: 20 Mar 2012, 03:42
Distribution: v5.1-alpha*-64bit
Location: australia
Contact:

slapt-mod-upgrade: Update modules

Post#37 by ncmprhnsbl » 15 Mar 2026, 04:37

Ed_P wrote:
14 Mar 2026, 19:16
Normally the word "upgrade" implies "replacement" of the original version and in the Porteus world a new replacement module, or new version of Porteus. :D The very enhanced slapt-mod-upgrade script doesn't replace or upgrade the existing Porteus' slapt-mod module, or app, it's an enhanced version of the existing version. So maybe naming it slapt-mod-supreme or slapt-mod-enhanced or slapt-mod-II would be a less confusing name for new, or senior, users.
hmm, are you confused because the thing is confusing or is the thing confusing because you're confused ?
to (attempt) to clarify:
slapt-mod-upgrade does not "upgrade slapt-mod" (to what??)
slapt-mod-upgrade is not an "enhanced version of" slapt-mod.
slapt-mod-upgrade -h explains very clearly what it does.
Forum Rules : https://forum.porteus.org/viewtopic.php?f=35&t=44

pterid
Contributor
Contributor
Posts: 105
Joined: 01 Feb 2025, 20:13
Distribution: Porteus 5.01 Xfce on ext4 USB

slapt-mod-upgrade: Update modules

Post#38 by pterid » 15 Mar 2026, 09:11

rych wrote:
15 Mar 2026, 02:45
Good news for us, as confirmed by a thorough AI search: It's OKAY to simply delete/rename the old/previous xzm while it's activated: the current mount is unaffected, and perfect for after reboot.

Read the whole story below:
I actually consulted a chatbot already while formulating my reply to you about it. It produced similar output to the long text you have posted, which I hope you will forgive me for not reading.

But my search did not end there. I went into the Porteus utils and confirmed what they do, and then went fishing for original sources about loop mounts (that's how I found the Stack Overflow post I linked to). I can also recommend manpages mount(8) and loop(4). Then I briefly tried it on my system, and saw the aufs branch indeed still available to me.

I don't believe there is such a thing as a "thorough AI search", as when a topic doesn't feature strongly in their training, LLMs just lie confidently, and the more you follow up, the more they lie. They're very well trained on Linux generally, but less so on Porteus in my experience (despite their best efforts to ddos this forum with constant re-scraping).

On the question of moving or deleting activated Porteus modules: whether or not it's possible without causing disk issues (and it seems it is), I would rather defer to the long term Porteus maintainers (ncm, Blaze et al) about whether they feel it's an appropriate thing to do. From limited testing, I predict problems with the lsmodules app not noticing an "orphan" activation without an accompanying file of the same name.

Self-updating of this script would be a neat idea. But that requires a stable hosting platform, and I'm not sure if I will stay on github for long. I have a Bitbucket account, and am looking to see if I can get an account on a more foss-focused platform. I also have a vain fantasy that after a lot of work to harmonise it with other tools, creation of GUI etc, this thing might get folded into Porteus proper.

I won't paste bot code into any project I write for pleasure (though I will happily use it as a starting point for working through something myself..) Feel free to fork it and have fun yourself though. You can call me what you want, but my actual motivation for coding anything here is to taste the logic problems of creating things myself. This is not a company, we don't have a deadline.

User avatar
Ed_P
Contributor
Contributor
Posts: 9285
Joined: 06 Feb 2013, 22:12
Distribution: Cinnamon 5.01 ISO
Location: Western NY, USA

slapt-mod-upgrade: Update modules

Post#39 by Ed_P » 15 Mar 2026, 21:07

Code: Select all

# slapt-mod-upgrade
# A script to upgrade Porteus modules using the slapt-mod tool.
# lcpterid, 2026
Ok, getting a better understanding of this script's function. slapt-mod-upgrade is not an update of slapt-mod, as I read it's name, it's an updater of slapt-mod created modules. Ok, cool. :happy62: :good:

slapt-mods-updater
slapt-mod_mods-updater
slapt-mod-xzm-updater

less confusing names? more specific names? :pardon:

Thanks for the insight. :beer:

pterid
Contributor
Contributor
Posts: 105
Joined: 01 Feb 2025, 20:13
Distribution: Porteus 5.01 Xfce on ext4 USB

slapt-mod-upgrade: Update modules

Post#40 by pterid » 15 Mar 2026, 21:45

Ed_P wrote:
15 Mar 2026, 21:07
slapt-mods-updater
slapt-mod_mods-updater
slapt-mod-xzm-updater

less confusing names? more specific names? :pardon:
Thanks, and happy to use any of these! My favourite of the three would be slapt-mods-updater (or even just mods-updater).

pterid
Contributor
Contributor
Posts: 105
Joined: 01 Feb 2025, 20:13
Distribution: Porteus 5.01 Xfce on ext4 USB

upgrade-mods: Update slapt-mod modules

Post#41 by pterid » 06 Apr 2026, 13:21

New version 0.0.3 :)

https://github.com/lcpterid/slapt-mod-u ... tag/v0.0.3

Changes wrt v0.0.2:
  • rename to upgrade-mods (sadly I can't now change the name of this thread or the text of the first post; it's too old!)
  • new options -u/U to (not) update package lists, without asking
  • new options -m/M to (not) move new modules to existing folders, without asking
  • new options -x/X to exclude files/folders containing a certain string
  • new option -r to search subfolders recursively
  • make .bak files from old modules on move (previous .bak of same name will be overwritten unless user gives cautious option -c)
  • less output by default, -v option for verbose output
  • check if old module is activated before moving it; refuse if it is activated or otherwise mounted
  • improved handling of bad paths and empty folders
Full source:

Code: Select all

#!/usr/bin/env bash

# upgrade-mods
# A script to upgrade Porteus modules using the slapt-mod tool.
# lcpterid, 2026
# with some code from slapt-mod by babam and ncmprhnsbl

. /usr/share/porteus/porteus-functions
get_colors
lightblue () { echo -e ${txtbld}$(tput setaf 12) "$1" $rst; }

declare -a module_folders

prog_name="$(basename "$0")"
nu='[0-9]'
major_minor_eregex="^$nu+\.$nu+$"
major_minor_patch_eregex="^$nu+\.$nu+\.$nu+$"
# Valid package name: Need at least 4 non-dash regions separated by dashes
# i.e. basename-version-arch-build, where basename may contain dashes
slackware_package_eregex="[^-]+(-[^-]+){3}"
pkg_list_raw=''
pkg_list=''
echo2 () {
	echo "$1" >&2
}

get_help () {
	echo2 "$prog_name: Attempt to upgrade Porteus modules made with slapt-mod."
	echo2
	echo2 "Usage:"
	echo2 "$prog_name [options]                         Update all modules in all known module directories."
	echo2 "$prog_name [options] /path/to/directory      Update all modules in the given directory."
	echo2 "$prog_name [options] /path/to/file.xzm       Update the given module only."
	echo2
	echo2 "Options:"
	echo2 "-v          Verbose output, useful for debugging"
	echo2 "-u          Always update package list"
	echo2 "-U          Do not update package list"
	echo2 "-r          Explore folders recursively"
	echo2 "-m          Always move updated packages to original folder, renaming old module to .bak"
	echo2 "-M          Do not move updated packages to original folder"
	echo2 "-c          When moving, be cautious: do not move if .bak file would be overwritten"
	echo2 "-x word     Exclude modules whose name contains the given word"
	echo2 "-X word     Exclude folders whose name contains the given word"
}

# Must be root:
swtch_rt () {
if [ `whoami` != "root" ]; then
	red "Please enter root user password"
	su -c "sh $0 $*"
	yellow "For ease of use, su to root user first."
	exit
fi
}

error_quit () {
	[ "$1" -ne 0 ] && red "$0: error: $2"
	exit "$1"
}

offer_update () {
	lightblue "Would you like to update the package lists? (y/n)"
	read -r tmp_answer
	if [ "${tmp_answer:0:1}" = "y" ] || [ "${tmp_answer:0:1}" = "Y" ]; then
		slapt-mod -u
	fi
	unset tmp_answer
}

handle_update () {
	[ "$update_list" = "yes" ] && slapt-mod -u
	[ "$update_list" = "ask" ] && offer_update
}

slapt_getlist () {
	pkg_list_raw="$(slapt-get --available)" || error_quit 1 "Could not run slapt-get"
	pkg_list="$(echo "$pkg_list_raw" | sed 's/ .*$//')"
}

add_folder () {
	# add folder if it exists and is not in module_folders array
	if [ -d "$1" ] && ! echo "${module_folders[@]}" | grep -Fw "$1" >/dev/null; then
		if [ -z "$exclude_folders" ] || ! [[ "$(basename "$1")" =~ "$exclude_folders" ]]; then
			module_folders+=("$1")
		fi
	fi
}

# Get module directories
populate_folders () {
	# default Porteus folders
	for i in modules optional; do
		add_folder "$PORTDIR/$i"
	done

	# extra module directories defined in cheat code
	cheatcodes_file="/etc/bootcmd.cfg" 
	if [ -f "$cheatcodes_file" ] && grep '^extramod=' "$cheatcodes_file" >/dev/null; then
		extramod_folders="$(grep '^extramod=' "$cheatcodes_file" | sed 's/^extramod=//')"
		IFS=";"
		for emf in $extramod_folders; do
			add_folder "$emf"
		done
		unset IFS
	fi

	# custom module directories defined in lsmodules app
	if [ -f "$HOME/.config/lsmodules" ]; then
		while read -r custom_dir; do
			add_folder "$custom_dir"
		done < "$HOME/.config/lsmodules" 
	fi
}


# Input to following functions is a full package name
# You could reconstruct the whole package name via:
# "$(get_base_name "$pkg")-$(get_version "$pkg")-$(get_arch "$pkg")-$(get_build "$pkg")""

get_base_name () {
	# Python equivalent: pkgname.split('-')[:-4]
	printf "%s" "$1" | sed 's/ .*$//' | sed -E 's/-[^-]+-[^-]+-[^-]+$//'
}

get_version () {
	# Python equivalent: pkgname.split('-')[-3]
	printf "%s" "$1" | sed 's/ .*$//' | sed -E 's/-[^-]+-[^-]+$//' | sed -E 's/^.*-([^-]+)$/\1/'
}

get_arch () {
	# Python equivalent: pkgname.split('-')[-2]
	printf "%s" "$1" | sed 's/ .*$//' | sed -E 's/-[^-]+$//' | sed -E 's/^.*-([^-]+)$/\1/'
}

get_build () {
	# Python equivalent: pkgname.split('-')[-1]
	printf "%s" "$1" | sed 's/ .*$//' | sed -E 's/^.*-([^-]+)$/\1/'
}


valid_packages_in_module () {
	# Only consider packages whose names meet the basic Slackware regex
	package_path='/var/lib/pkgtools/packages' 
	lsxzm "$1" |
		grep "^${package_path}/." |
		sed "s|^${package_path}/||" |
		grep -E "$slackware_package_eregex"
}

same_base_name_as () {
	while read -r fullpkg; do
		[ "$(get_base_name "$fullpkg")" = "$1" ] && echo "$fullpkg"
	done
}

newer_than () {
	local reference_version="$1"
	while read -r fullpkg; do
		local version="$(get_version "$fullpkg")"
		local major_regex='s/^([0-9]+)\..*$/\1/'
		local minor_regex='s/^[0-9]+\.([0-9]+)(\..*)?$/\1/'
		local patch_regex='s/^.*\.([0-9]+)$/\1/'
		local major="$(echo "$version" | sed -E "$major_regex")"
		local minor="$(echo "$version" | sed -E "$minor_regex")"
		local patch="$(echo "$version" | sed -E "$patch_regex")"
		local reference_major="$(echo "$reference_version" | sed -E "$major_regex")"
		local reference_minor="$(echo "$reference_version" | sed -E "$minor_regex")"
		local reference_patch="$(echo "$reference_version" | sed -E "$patch_regex")"
		if [[ "$reference_version" =~ $major_minor_patch_eregex ]] && [[ "$version" =~ $major_minor_patch_eregex ]]; then
			if [ "$major" -gt "$reference_major" ]; then
				echo "$fullpkg"
			elif [ "$major" -eq "$reference_major" ] &&
				[ "$minor" -gt "$reference_minor" ]; then
				echo "$fullpkg"
			elif [ "$major" -eq "$reference_major" ] &&
				[ "$minor" -eq "$reference_minor" ] && 
				[ "$patch" -gt "$reference_patch" ]; then
				echo "$fullpkg"
			else
				:
			fi
		elif [[ "$reference_version" =~ $major_minor_eregex ]] && [[ "$version" =~ $major_minor_eregex ]]; then
			if [ "$major" -gt "$reference_major" ]; then
				echo "$fullpkg"
			elif [ "$major" -eq "$reference_major" ] &&
				[ "$minor" -gt "$reference_minor" ]; then
				echo "$fullpkg"
			else
				:
			fi
		else
			# Just find the numbers and compare them
			local reference_version_stripped="$(echo "$reference_version" | tr -dC '0-9')"
			local version_stripped="$(echo "$version" | tr -dC '0-9')"
			[ "$version_stripped" -gt "$reference_version_stripped" ] && echo "$fullpkg"
		fi
	done
}

is_valid_module () {
	[ -f "$1" ] || return 1

	local filetype="$(file "$1")"
	grep "quashfs" < <(echo "$filetype") >/dev/null || {
		[ "$verbosity" = "loud" ] && red "$1 does not appear to be a squashfs module"
		return 1
		}


	local mod_path="$(realpath "$1")"
	local mod_name="$(basename "$mod_path")"
	local mod_stem="${mod_name%\.xzm}"
	if ! grep "/var/lib/pkgtools/packages/$mod_stem" < <(lsxzm "$mod_path") >/dev/null; then
		[ "$verbosity" = "loud" ] && red "didn't find package file /var/lib/pkgtools/packages/$mod_stem in lsxzm"
		return 1
	else
		return 0
	fi
}

is_untampered () {

	local check_mod_path="$1"

	# a. Get date of module formation from unsquashfs -stat:
	# b. Get date from -ll var/lib/pkgtools/packages/(name of file): unsquashfs -stat ed-1.17-x86_64-3.xzm | grep time | rev | cut -d ' ' -f 1-4 | rev
	# c. Should be the same time (within 15 mins).

	pkgfile_date="$(unsquashfs -ll "$check_mod_path" "var/lib/pkgtools/packages/$mod_stem" | tail -1 | sed 's/  */ /g' | cut -d ' ' -f 4-5)"

	xzm_date="$(unsquashfs -stat "$check_mod_path" | grep time | rev | sed 's/  */ /g' | cut -d ' ' -f 1-4 | rev)"

	pkgfile_epoch=$(date -d "$pkgfile_date" '+%s')
	xzm_epoch=$(date -d "$xzm_date" '+%s')

	pkg_to_xzm_time="$((xzm_epoch - pkgfile_epoch))"

	if [ "$pkg_to_xzm_time" -gt 900 ]; then
		return 1
	else
		return 0
	fi
}

do_move () {
	local old_path
	local tmp_path
	local new_path
	old_path="$1"
	tmp_path="$2"
	new_path="$3"
	if [ "$overwrite_bak_files" = "no" ] && [ -f "${old_path}.bak" ]; then
		yellow "Did not move $tmp_path because a .bak file of the old module exists, and would be overwritten."
	elif [ -n "$(mount | grep -F "$old_path on " )" ]; then
		yellow "Did not move $tmp_path because the existing module is activated or mounted. Deactivate or unmount the existing module and move the new one manually."
	else
		mv "$old_path" "$old_path.bak"
		mv "$tmp_path" "$new_path"
	fi
}

offer_move () {
	local old_mod_path="$(realpath "$1")"
	local tmp_mod_path="$(realpath "$2")"
	local new_mod_path="$(realpath "$3")"
	local ui_deletion_state

	lightblue "Would you like to move the new module from /tmp to the folder $(dirname "$new_mod_path")?"
	if [ -f "$old_mod_path" ]; then
		lightblue "NOTE: The old module will be renamed with extension .bak."
	fi
	if [ "$overwrite_bak_files" = "yes" ] && [ -f "${old_mod_path}.bak" ]; then
		yellow "WARNING: An existing file $(basename "$old_mod_path").bak will be overwritten."
	fi
	lightblue "Please type an UPPERCASE Y to proceed."
	read -r tmp_answer
	if [ "${tmp_answer:0:1}" = "Y" ]; then
		do_move "$old_mod_path" "$tmp_mod_path" "$new_mod_path"
	fi
}

handle_move () {
	local old_mod_path="$(realpath "$1")"
	local new_mod_path="$(realpath "$2")"
	local new_mod_name="$(basename "$new_mod_path")"

	[ -f "/tmp/$new_mod_name" ] || return 1

	if [ "$move_files" = "yes" ]; then
		do_move "$old_mod_path" "/tmp/$new_mod_name" "$new_mod_path"
	elif [ "$move_files" = "ask" ]; then
		offer_move "$old_mod_path" "/tmp/$new_mod_name" "$new_mod_path"
	fi
}

upgrade_module () {
	local mod_path="$(realpath "$1")"
	local mod_name="$(basename "$mod_path")"
	[ "$verbosity" = "loud" ] && cyan "Module: $mod_path"
	is_valid_module "$mod_path" || return 1
	local filetype="$(file "$1")"
	local compressopt
	if [ -n "$(grep "zstd" < <(echo "$filetype"))" ]; then
	       compressopt="-n"
	else
	       compressopt="-N"
	fi

	local found_newer="no"
	local can_rebuild="yes"
	local -a new_packages
	local -a all_packages
	local first_missing_package
	local mod_pkg_name
	local mod_pkg_version
	local mod_pkg_list_raw="$(valid_packages_in_module "$mod_path")"
	if [ "$(echo "$mod_pkg_list_raw" | wc -l)" -ne "$(lsxzm "$1" | grep '^/var/lib/pkgtools/packages/.' | wc -l)" ]; then
		[ "$verbosity" = "loud" ] && red "Found package with a non-standard name - can't update this module."
		return 1
	fi

	# Hoist the original package name to the top, so the new module will be named after it
	mod_pkg_name=$(get_base_name "${mod_name%.xzm}")
	local mod_pkg_list="$(echo "$mod_pkg_list_raw" | grep "^$mod_pkg_name"; echo "$mod_pkg_list_raw" | grep -v "^$mod_pkg_name")"

	if [ "$(echo "$mod_pkg_list_raw" | wc -l)" -ne "$(echo "$mod_pkg_list" | wc -l)" ]; then
		[ "$verbosity" = "loud" ] && red "The module seems to be named after a package that is not present inside it. Can't update this module."
		return 1
	fi


	while read -r mod_pkg; do

		mod_pkg_name=$(get_base_name "$mod_pkg")
		mod_pkg_version=$(get_version "$mod_pkg")

		# replacements are packages with the same base name, but not full name
		# and whose version number appears newer.
		# In case of a clash, pick the later one alphabetically
		replacements="$(echo "$pkg_list" |
			grep -v "$mod_pkg" |
			grep "^$mod_pkg_name" |
			same_base_name_as "$mod_pkg_name" |
			newer_than "$(get_version "$mod_pkg_version")" |
			sort -n |
			tail -1)"
		# We also need to know whether the very same version is still present
		local samepkg="$(echo "$pkg_list" | grep "$mod_pkg")"
		if [ -n "$replacements" ]; then
		       found_newer="yes"
		       new_packages+=("$replacements")
		       all_packages+=("$replacements")
		elif [ -n "$samepkg" ]; then
		       all_packages+=("$samepkg")
	       else
		       first_missing_package="$mod_pkg"
		       can_rebuild="no"
		fi

	done < <(echo "$mod_pkg_list")

	if [ "$can_rebuild" = "no" ]; then
		[ "$verbosity" = "loud" ] && red "Couldn't find all packages needed to rebuild module. First missing package was $first_missing_package."
		return 1
	elif ! is_untampered "$mod_path"; then
		[ "$verbosity" = "loud" ] && yellow "NB: this module may have been edited after creation with slapt-mod. If so, be careful not to lose your changes."
	fi


	if [ "$found_newer" = "yes" ]; then
		echo "Existing packages in $mod_name: $mod_pkg_list" | tr '\n' ' ' | fold -w "$(tput cols)" -s
		echo
		green "Found possible newer packages: ${new_packages[*]}" | fold -w "$(tput cols)" -s
		lightblue "Do you want to attempt the upgrade? (y/n)"
		read -r tmp_answer
		if [ "${tmp_answer:0:1}" = "y" ] || [ "${tmp_answer:0:1}" = "Y" ]; then
			slapt-mod "$compressopt" "${all_packages[@]}"
			local new_mod_name="${all_packages[0]}.xzm"
			local new_mod_path="$(dirname "$mod_path")/$new_mod_name"
			[ -f "/tmp/$new_mod_name" ] && handle_move "$mod_path" "$new_mod_path"
		fi
		unset tmp_answer
	else
		[ "$verbosity" = "loud" ] && yellow "Didn't find any updates."
	fi

}

upgrade_folder () {
	local modules_raw
	local -a modules_in_folder
	if [ "$recursive" = "yes" ]; then
		modules_raw="$(find "$1" -type f -name "*.xzm")"
	else
		modules_raw="$(find "$1" -type f -name "*.xzm" -maxdepth 1)"
	fi

	if [ -n "$modules_raw" ]; then
		while read -r xzm_path; do
			if [ -z "$exclude_files" ] || ! [[ "$(basename "$xzm_path")" =~ "$exclude_files" ]]; then
				modules_in_folder+=("$xzm_path")
			fi
		done < <(echo "$modules_raw")
	fi

	if [ "${#modules_in_folder[*]}" -eq 0 ]; then
		[ "$verbosity" = "loud" ] && red "No modules found in folder $1"
	else
		for mif in "${modules_in_folder[@]}"; do
			upgrade_module "$mif"
		done
	fi
}

swtch_rt "$@"

# Options: default values
update_list="ask"
move_files="ask"
overwrite_bak_files="yes"
recursive="no"
exclude_files=""
exclude_folders=""
verbosity="quiet"


# Parse long help option
if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
	get_help
	exit
fi
# Parse short options
while getopts ":hvuUmMcrx:X:" thisopt; do
	case "$thisopt" in
		h)
			get_help
			exit
			;;
		v)
			verbosity="loud"
			;;
		u)
			update_list="yes"
			;;
		U)
			update_list="no"
			;;
		m)
			move_files="yes"
			;;
		M)
			move_files="no"
			;;
		c)
			overwrite_bak_files="no"
			;;
		r)
			recursive="yes"
			;;
		x)
			exclude_files="$OPTARG"
			;;
		X)
			exclude_folders="$OPTARG"
			;;
		:)
			error_quit 4 "used the -x or -X option without specifying a word to exclude in file/folder names"
			;;
		?)
			error_quit 1 "invalid option -$OPTARG"
			;;
	esac
done

shift $(( OPTIND - 1 ))

if [ "$#" -eq 0 ]; then
	handle_update
	slapt_getlist
	populate_folders
	for mf in "${module_folders[@]}"; do
		[ "$verbosity" = "loud" ] && lightblue "Starting module directory: $mf"
		upgrade_folder "$mf"
	done
else
	if [ -f "$1" ]; then
		handle_update
		slapt_getlist
		upgrade_module "$1"

	elif [ -d "$1" ]; then
		handle_update
		slapt_getlist
		upgrade_folder "$1"

	else
		error_quit 2 "could not find file or directory: $1"
	fi
fi

Post Reply