From c6ce91f31ba29681f161f368de541e33e7a836a9 Mon Sep 17 00:00:00 2001 From: "R. Larocque" Date: Tue, 5 May 2026 09:14:30 -0400 Subject: [PATCH 01/10] Please enter the commit message for your changes. Lines starting with '' will be ignored, and an empty message aborts the commit. On branch dev Your branch is up to date with 'origin/dev'. Changes to be committed: new file: build-musl.sh new file: build.sh --- build-musl.sh | 39 +++++++++++++++++++++++++++++++++++++++ build.sh | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100755 build-musl.sh create mode 100755 build.sh diff --git a/build-musl.sh b/build-musl.sh new file mode 100755 index 0000000..fe45824 --- /dev/null +++ b/build-musl.sh @@ -0,0 +1,39 @@ +#!/bin/bash +set -e + +echo "Building on Alpine (musl) using Docker..." + +docker run --rm \ + -v "$(pwd):/build" \ + -w /build \ + alpine:latest \ + sh -c ' + apk add --no-cache python3 pyqt6 pyqt6-qt6 python3-pip gcc musl-dev linux-headers + + pip install --break-system-packages nuitka pyinstaller psutil pyudev packaging platformdirs + + pyinstaller --onefile \ + --collect-all PyQt6 \ + --add-binary "/usr/bin/dd:bin" \ + --add-binary "/usr/bin/pkexec:bin" \ + --add-binary "/usr/bin/sudo:bin" \ + --add-binary "/usr/bin/lsblk:bin" \ + --add-binary "/usr/bin/mount:bin" \ + --add-binary "/usr/bin/umount:bin" \ + --add-binary "/usr/sbin/blkid:bin" \ + --add-binary "/usr/sbin/badblocks:bin" \ + --add-binary "/usr/sbin/mkfs.ntfs:bin" \ + --add-binary "/usr/sbin/mkfs.vfat:bin" \ + --add-binary "/usr/sbin/mkfs.exfat:bin" \ + --add-binary "/usr/sbin/mkfs.ext4:bin" \ + --add-binary "/usr/sbin/mkfs.btrfs:bin" \ + --add-data "src/lufus/gui/languages:/lufus/gui/languages" \ + --add-data "src/lufus/gui/themes:/lufus/gui/themes" \ + --add-data "src/lufus/gui/assets:/lufus/gui/assets" \ + --add-data "src/lufus/writing/grub.cfg:/lufus/writing" \ + --add-data "src/lufus/writing/uefi-ntfs.img:/lufus/writing" \ + --name lufus src/lufus/__main__.py + ' + +echo "Done! Output: dist/lufus (musl build)" +echo "Size: $(ls -lh dist/lufus | awk '{print $5}')" diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..e92610e --- /dev/null +++ b/build.sh @@ -0,0 +1,35 @@ +#!/bin/bash +set -e + +echo "Creating venv..." +python3 -m venv /tmp/lufus-build +source /tmp/lufus-build/bin/activate + +echo "Installing dependencies..." +pip install nuitka pyinstaller pyqt6 psutil pyudev packaging platformdirs + +echo "Building binary..." +pyinstaller --onefile \ + --collect-all PyQt6 \ + --add-binary "/usr/bin/dd:bin" \ + --add-binary "/usr/bin/pkexec:bin" \ + --add-binary "/usr/bin/sudo:bin" \ + --add-binary "/usr/bin/lsblk:bin" \ + --add-binary "/usr/bin/mount:bin" \ + --add-binary "/usr/bin/umount:bin" \ + --add-binary "/usr/sbin/blkid:bin" \ + --add-binary "/usr/sbin/badblocks:bin" \ + --add-binary "/usr/sbin/mkfs.ntfs:bin" \ + --add-binary "/usr/sbin/mkfs.vfat:bin" \ + --add-binary "/usr/sbin/mkfs.exfat:bin" \ + --add-binary "/usr/sbin/mkfs.ext4:bin" \ + --add-binary "/usr/sbin/mkfs.btrfs:bin" \ + --add-data "src/lufus/gui/languages:lufus/gui/languages" \ + --add-data "src/lufus/gui/themes:lufus/gui/themes" \ + --add-data "src/lufus/gui/assets:lufus/gui/assets" \ + --add-data "src/lufus/writing/grub.cfg:lufus/writing" \ + --add-data "src/lufus/writing/uefi-ntfs.img:lufus/writing" \ + --name lufus src/lufus/__main__.py + +echo "Done! Output: dist/lufus" +echo "Size: $(ls -lh dist/lufus | awk '{print $5}')" From 2d9cae9a53ffaff553be71b4c49a1d467c5e9f41 Mon Sep 17 00:00:00 2001 From: Radiump123 Date: Tue, 5 May 2026 19:22:53 -0400 Subject: [PATCH 02/10] =?UTF-8?q?=20Veuillez=20saisir=20le=20message=20de?= =?UTF-8?q?=20validation=20pour=20vos=20modifications.=20Les=20lignes=20?= =?UTF-8?q?=20commen=C3=A7ant=20par=20''=20seront=20ignor=C3=A9es,=20et=20?= =?UTF-8?q?un=20message=20vide=20abandonne=20la=20validation.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sur la branche dev Votre branche est à jour avec 'origin/dev'. Modifications qui seront validées : supprimé : build-musl.sh supprimé : build.sh --- build-musl.sh | 39 --------------------------------------- build.sh | 35 ----------------------------------- 2 files changed, 74 deletions(-) delete mode 100755 build-musl.sh delete mode 100755 build.sh diff --git a/build-musl.sh b/build-musl.sh deleted file mode 100755 index fe45824..0000000 --- a/build-musl.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash -set -e - -echo "Building on Alpine (musl) using Docker..." - -docker run --rm \ - -v "$(pwd):/build" \ - -w /build \ - alpine:latest \ - sh -c ' - apk add --no-cache python3 pyqt6 pyqt6-qt6 python3-pip gcc musl-dev linux-headers - - pip install --break-system-packages nuitka pyinstaller psutil pyudev packaging platformdirs - - pyinstaller --onefile \ - --collect-all PyQt6 \ - --add-binary "/usr/bin/dd:bin" \ - --add-binary "/usr/bin/pkexec:bin" \ - --add-binary "/usr/bin/sudo:bin" \ - --add-binary "/usr/bin/lsblk:bin" \ - --add-binary "/usr/bin/mount:bin" \ - --add-binary "/usr/bin/umount:bin" \ - --add-binary "/usr/sbin/blkid:bin" \ - --add-binary "/usr/sbin/badblocks:bin" \ - --add-binary "/usr/sbin/mkfs.ntfs:bin" \ - --add-binary "/usr/sbin/mkfs.vfat:bin" \ - --add-binary "/usr/sbin/mkfs.exfat:bin" \ - --add-binary "/usr/sbin/mkfs.ext4:bin" \ - --add-binary "/usr/sbin/mkfs.btrfs:bin" \ - --add-data "src/lufus/gui/languages:/lufus/gui/languages" \ - --add-data "src/lufus/gui/themes:/lufus/gui/themes" \ - --add-data "src/lufus/gui/assets:/lufus/gui/assets" \ - --add-data "src/lufus/writing/grub.cfg:/lufus/writing" \ - --add-data "src/lufus/writing/uefi-ntfs.img:/lufus/writing" \ - --name lufus src/lufus/__main__.py - ' - -echo "Done! Output: dist/lufus (musl build)" -echo "Size: $(ls -lh dist/lufus | awk '{print $5}')" diff --git a/build.sh b/build.sh deleted file mode 100755 index e92610e..0000000 --- a/build.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/bin/bash -set -e - -echo "Creating venv..." -python3 -m venv /tmp/lufus-build -source /tmp/lufus-build/bin/activate - -echo "Installing dependencies..." -pip install nuitka pyinstaller pyqt6 psutil pyudev packaging platformdirs - -echo "Building binary..." -pyinstaller --onefile \ - --collect-all PyQt6 \ - --add-binary "/usr/bin/dd:bin" \ - --add-binary "/usr/bin/pkexec:bin" \ - --add-binary "/usr/bin/sudo:bin" \ - --add-binary "/usr/bin/lsblk:bin" \ - --add-binary "/usr/bin/mount:bin" \ - --add-binary "/usr/bin/umount:bin" \ - --add-binary "/usr/sbin/blkid:bin" \ - --add-binary "/usr/sbin/badblocks:bin" \ - --add-binary "/usr/sbin/mkfs.ntfs:bin" \ - --add-binary "/usr/sbin/mkfs.vfat:bin" \ - --add-binary "/usr/sbin/mkfs.exfat:bin" \ - --add-binary "/usr/sbin/mkfs.ext4:bin" \ - --add-binary "/usr/sbin/mkfs.btrfs:bin" \ - --add-data "src/lufus/gui/languages:lufus/gui/languages" \ - --add-data "src/lufus/gui/themes:lufus/gui/themes" \ - --add-data "src/lufus/gui/assets:lufus/gui/assets" \ - --add-data "src/lufus/writing/grub.cfg:lufus/writing" \ - --add-data "src/lufus/writing/uefi-ntfs.img:lufus/writing" \ - --name lufus src/lufus/__main__.py - -echo "Done! Output: dist/lufus" -echo "Size: $(ls -lh dist/lufus | awk '{print $5}')" From 14896ebd494d07cf52771e846888faff376b1d98 Mon Sep 17 00:00:00 2001 From: Radiump123 Date: Tue, 5 May 2026 19:47:27 -0400 Subject: [PATCH 03/10] =?UTF-8?q?DO=20NOT=20PUSH=20TO=20HOG185'S=20LUFUS!?= =?UTF-8?q?=20=20Veuillez=20saisir=20le=20message=20de=20validation=20pour?= =?UTF-8?q?=20vos=20modifications.=20Les=20lignes=20=20commen=C3=A7ant=20p?= =?UTF-8?q?ar=20''=20seront=20ignor=C3=A9es,=20et=20un=20message=20vide=20?= =?UTF-8?q?abandonne=20la=20validation.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sur la branche dev Votre branche est à jour avec 'origin/dev'. Modifications qui seront validées : nouveau fichier : deps.txt --- deps.txt | 83 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 deps.txt diff --git a/deps.txt b/deps.txt new file mode 100644 index 0000000..59ac1da --- /dev/null +++ b/deps.txt @@ -0,0 +1,83 @@ +# Lufus External Dependencies (Non-Python) + +## System Privileges +- pkexec (polkit) +- sudo +- runuser (util-linux) + +## Flashing & Imaging +- dd (coreutils) +- sync (coreutils) +- cp (coreutils) +- rm (coreutils) +- mkdir (coreutils) + +## Partitioning & Disk Info +- lsblk (util-linux) +- parted +- sfdisk (util-linux) +- partprobe (parted) +- udevadm (systemd/udev) +- wipefs (util-linux) +- blockdev (util-linux) +- badblocks (e2fsprogs) + +## Filesystem Tools +- mkfs.vfat, fatlabel (dosfstools) +- mkfs.exfat (exfatprogs or exfat-utils) +- mkfs.ntfs / mkntfs, ntfslabel (ntfs-3g) +- mkfs.ext4, e2label (e2fsprogs) +- mkudffs, udflabel (udftools) + +## Mounting & ISO Handling +- mount (util-linux) +- umount (util-linux) +- 7z (p7zip-full) - Used for Windows ISO detection +- isoinfo (genisoimage) - Fallback for Windows ISO detection + +## Process & System Management +- lsof +- fuser (psmisc) +- pgrep (procps) +- which (debianutils or similar) +- stty (coreutils) +- xdg-user-dir (xdg-user-dirs) +- xdg-open (xdg-utils) + +## Windows Image Operations (WIM) +- wimlib-imagex (wimlib / wimtools) +- wimmountrw (wimlib) +- wimunmount (wimlib) + +## Bootloader Installation +- grub-install (grub2 / grub-common) + +## GUI & System Libraries (PyQt6 Requirements) +- libgl1 +- libx11-6 +- libxcb1 +- libxrender1 +- fontconfig +- libfreetype6 +- libxext6 +- libxrandr2 +- libxcursor1 +- libxi6 +- libxfixes3 +- libxcomposite1 +- libxdamage1 + +## Package Managers (Used for auto-installing missing tools) +- apt-get (Debian/Ubuntu) +- dnf (Fedora/RHEL) +- pacman (Arch) +- zypper (openSUSE) + +## Python Environment +- python3 +- python3-pip +- python3-venv + +## Other (from requirements-system.txt) +- wget +- file From 2227a623b19de24ef838d7681aa8751d325f9231 Mon Sep 17 00:00:00 2001 From: Radiump123 Date: Tue, 5 May 2026 20:05:17 -0400 Subject: [PATCH 04/10] =?UTF-8?q?=20Veuillez=20saisir=20le=20message=20de?= =?UTF-8?q?=20validation=20pour=20vos=20modifications.=20Les=20lignes=20?= =?UTF-8?q?=20commen=C3=A7ant=20par=20''=20seront=20ignor=C3=A9es,=20et=20?= =?UTF-8?q?un=20message=20vide=20abandonne=20la=20validation.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sur la branche dev Votre branche est en avance sur 'origin/dev' de 1 commit. (utilisez "git push" pour publier vos commits locaux) Modifications qui seront validées : modifié : src/lufus/writing/install_ventoy.py modifié : src/lufus/writing/windows/flash.py modifié : src/lufus/writing/windows/tweaks.py --- src/lufus/writing/install_ventoy.py | 2 +- src/lufus/writing/windows/flash.py | 38 ++++++++++++++++++++--------- src/lufus/writing/windows/tweaks.py | 9 ++++--- 3 files changed, 32 insertions(+), 17 deletions(-) diff --git a/src/lufus/writing/install_ventoy.py b/src/lufus/writing/install_ventoy.py index 1171d8a..16cb6de 100644 --- a/src/lufus/writing/install_ventoy.py +++ b/src/lufus/writing/install_ventoy.py @@ -162,7 +162,7 @@ def install_grub(target_device: str) -> bool: # Refresh kernel table subprocess.run(["partprobe", target_device], check=False) subprocess.run(["udevadm", "settle"], check=False) - subprocess.run(["sync"], check=True) + os.sync() # Wait for device nodes to be created by udev efi_part = f"{target_device}{sep}2" diff --git a/src/lufus/writing/windows/flash.py b/src/lufus/writing/windows/flash.py index a68895f..690915f 100644 --- a/src/lufus/writing/windows/flash.py +++ b/src/lufus/writing/windows/flash.py @@ -71,12 +71,12 @@ def _fix_efi_bootloader(efi_mount): log.info("EFI bootloader fix: BOOTX64.EFI not found, will attempt to create at %s", boot_dir) bootx64 = os.path.join(boot_dir, "BOOTX64.EFI") - run_cmd(["sudo", "mkdir", "-p", boot_dir]) + os.makedirs(boot_dir, exist_ok=True) log.info("EFI bootloader fix: created directory %s", boot_dir) src = _find_path_case_insensitive(efi_mount, "EFI", "Microsoft", "Boot", "bootmgfw.efi") if src: - run_cmd(["sudo", "cp", src, bootx64]) + shutil.copy2(src, bootx64) log.info("EFI bootloader fix: copied %s -> %s", src, bootx64) return @@ -155,7 +155,7 @@ def _copy_file(src: str, dst: str) -> str: def _find_ntfs_tool(status_cb=None) -> str | None: """Find mkfs.ntfs/mkntfs, installing ntfs-3g if needed. Returns command name or None.""" for candidate in ["mkfs.ntfs", "mkntfs"]: - if subprocess.run(["which", candidate], capture_output=True).returncode == 0: + if shutil.which(candidate): return candidate if status_cb: @@ -167,13 +167,13 @@ def _find_ntfs_tool(status_cb=None) -> str | None: ["zypper", "install", "-y", "ntfs-3g"], ] for pm_cmd in pkg_managers: - if subprocess.run(["which", pm_cmd[0]], capture_output=True).returncode == 0: + if shutil.which(pm_cmd[0]): run_cmd(["sudo"] + pm_cmd) break # stop after the first working package manager # Re-check after installation attempt for candidate in ["mkfs.ntfs", "mkntfs"]: - if subprocess.run(["which", candidate], capture_output=True).returncode == 0: + if shutil.which(candidate): return candidate # installation succeeded return None @@ -181,7 +181,7 @@ def _find_ntfs_tool(status_cb=None) -> str | None: def _ensure_wimlib(status_cb=None) -> None: """Install wimlib-imagex if not present. Raises FileNotFoundError if it can't be found after install.""" - if subprocess.run(["which", "wimlib-imagex"], capture_output=True).returncode == 0: + if shutil.which("wimlib-imagex"): return if status_cb: status_cb("wimlib-imagex not found, attempting to install...") @@ -192,10 +192,10 @@ def _ensure_wimlib(status_cb=None) -> None: ["zypper", "install", "-y", "wimtools"], ] for pm_cmd in pkg_managers: - if subprocess.run(["which", pm_cmd[0]], capture_output=True).returncode == 0: + if shutil.which(pm_cmd[0]): run_cmd(["sudo"] + pm_cmd) break - if subprocess.run(["which", "wimlib-imagex"], capture_output=True).returncode != 0: + if not shutil.which("wimlib-imagex"): raise FileNotFoundError( "wimlib-imagex not found. Install manually: sudo pacman -S wimlib / sudo apt install wimtools" ) @@ -271,20 +271,34 @@ def _copy_efi_boot_files(iso_mount, mount_efi, _status): if efi_src: efi_items = os.listdir(efi_src) _status(f"Found EFI/ with {len(efi_items)} items: {efi_items}") - run_cmd(["sudo", "cp", "-r"] + [os.path.join(efi_src, i) for i in efi_items] + [mount_efi]) + for item in efi_items: + src_path = os.path.join(efi_src, item) + dst_path = os.path.join(mount_efi, "EFI", item) + if os.path.isdir(src_path): + shutil.copytree(src_path, dst_path, dirs_exist_ok=True) + else: + os.makedirs(os.path.dirname(dst_path), exist_ok=True) + shutil.copy2(src_path, dst_path) _status("Copied EFI/ tree to EFI partition") else: _status("WARNING: No EFI directory found in ISO - drive may not be UEFI bootable") boot_src = _find_path_case_insensitive(iso_mount, "boot") if boot_src: - run_cmd(["sudo", "cp", "-r"] + [os.path.join(boot_src, i) for i in os.listdir(boot_src)] + [mount_efi]) + for item in os.listdir(boot_src): + src_path = os.path.join(boot_src, item) + dst_path = os.path.join(mount_efi, "boot", item) + if os.path.isdir(src_path): + shutil.copytree(src_path, dst_path, dirs_exist_ok=True) + else: + os.makedirs(os.path.dirname(dst_path), exist_ok=True) + shutil.copy2(src_path, dst_path) _status("Copied boot/ tree to EFI partition") for fname in ["bootmgr", "bootmgr.efi"]: src = _find_path_case_insensitive(iso_mount, fname) if src: - run_cmd(["sudo", "cp", src, f"{mount_efi}/{fname}"]) + shutil.copy2(src, os.path.join(mount_efi, fname)) _status(f"Copied {fname} to EFI partition root") _fix_efi_bootloader(mount_efi) @@ -412,7 +426,7 @@ def _status(msg): # Step 7: Sync _status("Syncing all writes to disk...") - run_cmd(["sudo", "sync"]) + os.sync() _emit(97) _status("Sync complete") diff --git a/src/lufus/writing/windows/tweaks.py b/src/lufus/writing/windows/tweaks.py index 095e78a..9eb3a62 100644 --- a/src/lufus/writing/windows/tweaks.py +++ b/src/lufus/writing/windows/tweaks.py @@ -9,6 +9,7 @@ import re import subprocess import os +import shutil from lufus.utils import get_mount_and_drive from lufus import state from lufus.lufus_logging import get_logger @@ -54,7 +55,7 @@ def win_hardware_bypass(): cmd_string = "\n".join(commands) + "\n" log.info("win_hardware_bypass: injecting registry keys into boot.wim at %s...", mount) try: - subprocess.run(["mkdir", "/media/tempwinmnt"], check=True) + os.makedirs("/media/tempwinmnt", exist_ok=True) subprocess.run(["wimmountrw", f"{mount}/sources/boot.wim", "2", "/media/tempwinmnt"], check=True) subprocess.run( ["chntpw", "e", "/media/tempwinmnt/Windows/System32/config/SYSTEM"], @@ -64,7 +65,7 @@ def win_hardware_bypass(): check=True, ) subprocess.run(["wimunmount", "/media/tempwinmnt", "--commit"], check=True) - subprocess.run(["rm", "-rf", "/media/tempwinmnt"], check=True) + shutil.rmtree("/media/tempwinmnt", ignore_errors=True) log.info("win_hardware_bypass: registry keys injected successfully.") except subprocess.CalledProcessError as e: log.error("win_hardware_bypass: CalledProcessError: %s", e.stderr) @@ -79,7 +80,7 @@ def win_local_acc(): cmd_string = "\n".join(commands) + "\n" log.info("win_local_acc: bypassing online account requirement at %s...", mount) try: - subprocess.run(["mkdir", "/media/tempwinmnt"], check=True) + os.makedirs("/media/tempwinmnt", exist_ok=True) subprocess.run(["wimmountrw", f"{mount}/sources/boot.wim", "2", "/media/tempwinmnt"], check=True) subprocess.run( ["chntpw", "e", "/media/tempwinmnt/Windows/System32/config/SOFTWARE"], @@ -89,7 +90,7 @@ def win_local_acc(): check=True, ) subprocess.run(["wimunmount", "/media/tempwinmnt", "--commit"], check=True) - subprocess.run(["rm", "-rf", "/media/tempwinmnt"], check=True) + shutil.rmtree("/media/tempwinmnt", ignore_errors=True) log.info("win_local_acc: online account bypass applied successfully.") except subprocess.CalledProcessError as e: log.error("win_local_acc: CalledProcessError: %s", e.stderr) From 5af1dce371c2c0359ffdfb31ae252d66f2496cc7 Mon Sep 17 00:00:00 2001 From: Radiump123 Date: Tue, 5 May 2026 21:23:55 -0400 Subject: [PATCH 05/10] Update tweaks.py --- src/lufus/writing/windows/tweaks.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lufus/writing/windows/tweaks.py b/src/lufus/writing/windows/tweaks.py index 9eb3a62..fe3daac 100644 --- a/src/lufus/writing/windows/tweaks.py +++ b/src/lufus/writing/windows/tweaks.py @@ -1,3 +1,4 @@ +# Not tested, at least by me, I don't remember when that was added, and never used it nor really know what it's supposed to do. """Windows installation customization functions. These modify Windows installation media (boot.wim, autounattend.xml) From 00ba3450b2e7f239745982df514bccaf65b017a9 Mon Sep 17 00:00:00 2001 From: Radiump123 Date: Fri, 8 May 2026 16:48:42 -0400 Subject: [PATCH 06/10] Rename deps.txt to deps.md --- deps.txt => deps.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename deps.txt => deps.md (100%) diff --git a/deps.txt b/deps.md similarity index 100% rename from deps.txt rename to deps.md From 00b2c94cc2604c3dd0f0f87c0d3766d429aaec73 Mon Sep 17 00:00:00 2001 From: Radiump123 Date: Sat, 9 May 2026 14:36:40 -0400 Subject: [PATCH 07/10] Fixed issue #183. To make @hog185 and @splayer happy, I'll just go and explain how this works :D So, I made a small worker that starts before PKEXEC, logs XDG_DOWNLOAD_DIR (or ~/ if XDG_DOWNLOAD_DIR doesn't exist) to a variable, passes it to the app running as root before exiting in nanoseconds. The file manager from the GUI automatically takes the content of the variable as default directory. Made sure utils.py doesn't erase the variable. I also made tests (including edge cases), ruff-formatted and tested. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sur la branche dev Votre branche est à jour avec 'origin/dev'. Modifications qui seront validées : modifié : src/lufus/gui/gui.py modifié : src/lufus/gui/start_gui.py nouveau fichier : src/lufus/user_paths.py modifié : src/lufus/utils.py nouveau fichier : tests/test_user_paths.py --- src/lufus/gui/gui.py | 8 +++++- src/lufus/gui/start_gui.py | 10 +++++++ src/lufus/user_paths.py | 51 ++++++++++++++++++++++++++++++++ src/lufus/utils.py | 4 ++- tests/test_user_paths.py | 59 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 src/lufus/user_paths.py create mode 100644 tests/test_user_paths.py diff --git a/src/lufus/gui/gui.py b/src/lufus/gui/gui.py index 4f384e9..9b96439 100644 --- a/src/lufus/gui/gui.py +++ b/src/lufus/gui/gui.py @@ -1099,10 +1099,16 @@ def dropEvent(self, event): def browse_file(self): # open file dialog to select image :D + from lufus.user_paths import get_best_starting_dir + # ^ Uses the XDG_DOWNALOD_DIR that was detected and shoved into a variable + + starting_dir = get_best_starting_dir() + self.log_message(f"Opening file browser at: {starting_dir}") + file_name, _ = QFileDialog.getOpenFileName( self, self._T.get("dlg_select_image_title", "Select Image"), - "", + starting_dir, self._T.get( "dlg_select_image_filter", "Disk Images (*.iso *.dmg *.img *.bin *.raw);;All Files (*)", diff --git a/src/lufus/gui/start_gui.py b/src/lufus/gui/start_gui.py index 0d2bdeb..2a9542c 100644 --- a/src/lufus/gui/start_gui.py +++ b/src/lufus/gui/start_gui.py @@ -51,6 +51,16 @@ def _show_root_warning() -> None: def launch_gui_with_usb_data() -> None: elevation_attempted = False if os.geteuid() != 0: + # Capture user context (XDG_DOWNLOAD_DIR path) before pkexec elevation + from lufus.user_paths import get_best_starting_dir, ENV_DOWNLOAD_DIR + + try: + detected_path = get_best_starting_dir() + os.environ[ENV_DOWNLOAD_DIR] = detected_path + log.info("Captured user starting directory: %s", detected_path) + except Exception as e: + log.warning("Could not capture user starting directory: %s", e) + _load_initial_theme() elevation_attempted = True elevate_privileges() diff --git a/src/lufus/user_paths.py b/src/lufus/user_paths.py new file mode 100644 index 0000000..785cd83 --- /dev/null +++ b/src/lufus/user_paths.py @@ -0,0 +1,51 @@ +""" +Utility for detecting user's XDG_DOWNLOAD_DIR across root elevation +Falls back to ~/ if no XDG_DOWNLOAD_DIR is found +Made it so it runs as fast as possible before exiting to free like 2-3 KB RAM if you are using an OptiPlex GX270 or smth +""" + +import os +from pathlib import Path +from platformdirs import user_downloads_dir, user_documents_dir +from lufus.lufus_logging import get_logger + +log = get_logger(__name__) + +# Environment variable used to tunnel the path through the root/pkexec barrier +ENV_DOWNLOAD_DIR = "LUFUS_DOWNLOAD_DIR" + + +def get_best_starting_dir() -> str: + """ + Identify the best default dir for the file browser. + + This function works in two steps: + 1: Pre-elevation: Detects the real user's XDG_DOWNLOAD_DIR. If nothing is fonud, it falls back to ~/. + 2: Post-elevation: Gets the path from the tunneled environment variable. + + Returns: + str: An absolute path to a valid directory. + """ + # 1. High Priority: Check if we've already tunneled the path via environment + tunneled = os.environ.get(ENV_DOWNLOAD_DIR) + if tunneled: + if os.path.isdir(tunneled): + log.debug("Using tunneled user path: %s", tunneled) + return tunneled + log.warning("Tunneled path %s is no longer a valid directory.", tunneled) + + # 2. Medium Priority: Use platformdirs to find XDG standard paths + # This works best when called before elevation (geteuid != 0) + try: + # Standard XDG Downloads + dl_path = user_downloads_dir() + if dl_path and os.path.isdir(dl_path): + log.debug("Detected XDG Downloads directory: %s", dl_path) + return str(dl_path) + except Exception as e: + log.error("Failed to resolve XDG Downloads directory: %s", e) + + # 3. Low Priority: Home directory fallback + home = str(Path.home()) + log.debug("Falling back to home directory: %s", home) + return home diff --git a/src/lufus/utils.py b/src/lufus/utils.py index 4065ff4..2226948 100644 --- a/src/lufus/utils.py +++ b/src/lufus/utils.py @@ -27,6 +27,7 @@ def elevate_privileges() -> None: ) # Preserve DISPLAY and XAUTHORITY for GUI apps under pkexec/sudo + # Now also takes the detected XDG_DOWNLOAD_DIR of /src/lufus/user_paths.py to put it into LUFUS_DOWNLOAD_DIR env_vars = [ "DISPLAY", "XAUTHORITY", @@ -34,6 +35,7 @@ def elevate_privileges() -> None: "WAYLAND_DISPLAY", "PYTHONPATH", "LUFUS_THEME", + "LUFUS_DOWNLOAD_DIR", ] cmd = ["pkexec", "env"] @@ -47,7 +49,7 @@ def elevate_privileges() -> None: subprocess.run(cmd, check=True) sys.exit(0) except subprocess.CalledProcessError: - # User likely cancelled or pkexec failed + # User likely cancelled or pkexec failed/isn't installed pass except Exception as e: print(f"Elevation failed: {e}") diff --git a/tests/test_user_paths.py b/tests/test_user_paths.py new file mode 100644 index 0000000..99abf53 --- /dev/null +++ b/tests/test_user_paths.py @@ -0,0 +1,59 @@ +import os +import unittest +from unittest.mock import patch, MagicMock +from lufus.user_paths import get_best_starting_dir, ENV_DOWNLOAD_DIR + + +class TestUserPaths(unittest.TestCase): + def setUp(self): + # Clear the var before each test + if ENV_DOWNLOAD_DIR in os.environ: + del os.environ[ENV_DOWNLOAD_DIR] + + def test_get_best_starting_dir_with_env(self): + """Test that it prioritizes the environment variable if it points to a valid dir.""" + test_path = "/tmp/lufus_test_dir" + os.makedirs(test_path, exist_ok=True) + try: + os.environ[ENV_DOWNLOAD_DIR] = test_path + self.assertEqual(get_best_starting_dir(), test_path) + finally: + if os.path.exists(test_path): + os.rmdir(test_path) + + @patch("lufus.user_paths.user_downloads_dir") + @patch("os.path.isdir") + def test_get_best_starting_dir_with_xdg(self, mock_isdir, mock_downloads): + """Test that it uses XDG_DOWNLOAD_DIR if env is not set""" + mock_downloads.return_value = "/home/user/Downloads" + mock_isdir.side_effect = lambda p: p == "/home/user/Downloads" + + self.assertEqual(get_best_starting_dir(), "/home/user/Downloads") + + @patch("lufus.user_paths.user_downloads_dir") + @patch("os.path.isdir") + def test_get_best_starting_dir_with_custom_name(self, mock_isdir, mock_downloads): + """Test that it handles edge cases/translated names (like ~/Potato or ~/Téléchargements) (~/Potato is a great Downloads folder name btw)""" + custom_path = "/home/user/Téléchargements" + mock_downloads.return_value = custom_path + mock_isdir.side_effect = lambda p: p == custom_path + + self.assertEqual(get_best_starting_dir(), custom_path) + + @patch("lufus.user_paths.user_downloads_dir") + @patch("pathlib.Path.home") + @patch("os.path.isdir") + def test_get_best_starting_dir_fallback_to_home(self, mock_isdir, mock_home, mock_downloads): + """Test fallback to Home if Downloads is not found.""" + mock_downloads.return_value = "/home/user/Downloads" + mock_home.return_value = MagicMock() + mock_home.return_value.__str__.return_value = "/home/user" + + # Downloads does not exist + mock_isdir.return_value = False + + self.assertEqual(get_best_starting_dir(), "/home/user") + + +if __name__ == "__main__": + unittest.main() From dbc3469e1b0cb3adcf0575bc56a1ce9490e5d95a Mon Sep 17 00:00:00 2001 From: Radiump123 Date: Sat, 9 May 2026 14:49:15 -0400 Subject: [PATCH 08/10] =?UTF-8?q?Fixed=20the=20failed=20test.=20P.S.=20Fro?= =?UTF-8?q?m=20the=20provious=20commit=20and=20this=20one,=20NO=20AI=20USE?= =?UTF-8?q?D!=20=20Sur=20la=20branche=20dev=20=20Votre=20branche=20est=20?= =?UTF-8?q?=C3=A0=20jour=20avec=20'origin/dev'.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Modifications qui seront validées : modifié : .github/workflows/ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f7188a9..065d258 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,7 +34,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install psutil pytest + pip install psutil platformdirs pytest - name: Install package (no-deps to avoid GUI reqs) run: pip install -e . --no-deps From 325962a7156060a12f95604fbb6fc02ba96be544 Mon Sep 17 00:00:00 2001 From: Radiump123 Date: Sat, 9 May 2026 14:53:01 -0400 Subject: [PATCH 09/10] Removed deps.md, forgot this artifact when I made the PR. --- deps.md | 83 --------------------------------------------------------- 1 file changed, 83 deletions(-) delete mode 100644 deps.md diff --git a/deps.md b/deps.md deleted file mode 100644 index 59ac1da..0000000 --- a/deps.md +++ /dev/null @@ -1,83 +0,0 @@ -# Lufus External Dependencies (Non-Python) - -## System Privileges -- pkexec (polkit) -- sudo -- runuser (util-linux) - -## Flashing & Imaging -- dd (coreutils) -- sync (coreutils) -- cp (coreutils) -- rm (coreutils) -- mkdir (coreutils) - -## Partitioning & Disk Info -- lsblk (util-linux) -- parted -- sfdisk (util-linux) -- partprobe (parted) -- udevadm (systemd/udev) -- wipefs (util-linux) -- blockdev (util-linux) -- badblocks (e2fsprogs) - -## Filesystem Tools -- mkfs.vfat, fatlabel (dosfstools) -- mkfs.exfat (exfatprogs or exfat-utils) -- mkfs.ntfs / mkntfs, ntfslabel (ntfs-3g) -- mkfs.ext4, e2label (e2fsprogs) -- mkudffs, udflabel (udftools) - -## Mounting & ISO Handling -- mount (util-linux) -- umount (util-linux) -- 7z (p7zip-full) - Used for Windows ISO detection -- isoinfo (genisoimage) - Fallback for Windows ISO detection - -## Process & System Management -- lsof -- fuser (psmisc) -- pgrep (procps) -- which (debianutils or similar) -- stty (coreutils) -- xdg-user-dir (xdg-user-dirs) -- xdg-open (xdg-utils) - -## Windows Image Operations (WIM) -- wimlib-imagex (wimlib / wimtools) -- wimmountrw (wimlib) -- wimunmount (wimlib) - -## Bootloader Installation -- grub-install (grub2 / grub-common) - -## GUI & System Libraries (PyQt6 Requirements) -- libgl1 -- libx11-6 -- libxcb1 -- libxrender1 -- fontconfig -- libfreetype6 -- libxext6 -- libxrandr2 -- libxcursor1 -- libxi6 -- libxfixes3 -- libxcomposite1 -- libxdamage1 - -## Package Managers (Used for auto-installing missing tools) -- apt-get (Debian/Ubuntu) -- dnf (Fedora/RHEL) -- pacman (Arch) -- zypper (openSUSE) - -## Python Environment -- python3 -- python3-pip -- python3-venv - -## Other (from requirements-system.txt) -- wget -- file From 6ab0c2baf27ef38f7fc353eae04c01ed15321033 Mon Sep 17 00:00:00 2001 From: Radiump123 Date: Sat, 9 May 2026 15:55:02 -0400 Subject: [PATCH 10/10] =?UTF-8?q?Fallbacked=20some=20untested=20changes.?= =?UTF-8?q?=20=20Veuillez=20saisir=20le=20message=20de=20validation=20pour?= =?UTF-8?q?=20vos=20modifications.=20Les=20lignes=20=20commen=C3=A7ant=20p?= =?UTF-8?q?ar=20''=20seront=20ignor=C3=A9es,=20et=20un=20message=20vide=20?= =?UTF-8?q?abandonne=20la=20validation.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sur la branche dev Votre branche est à jour avec 'origin/dev'. Modifications qui seront validées : modifié : install_ventoy.py modifié : windows/flash.py modifié : windows/tweaks.py --- src/lufus/writing/install_ventoy.py | 2 +- src/lufus/writing/windows/flash.py | 38 +++++++++-------------------- src/lufus/writing/windows/tweaks.py | 10 +++----- 3 files changed, 17 insertions(+), 33 deletions(-) diff --git a/src/lufus/writing/install_ventoy.py b/src/lufus/writing/install_ventoy.py index 16cb6de..1171d8a 100644 --- a/src/lufus/writing/install_ventoy.py +++ b/src/lufus/writing/install_ventoy.py @@ -162,7 +162,7 @@ def install_grub(target_device: str) -> bool: # Refresh kernel table subprocess.run(["partprobe", target_device], check=False) subprocess.run(["udevadm", "settle"], check=False) - os.sync() + subprocess.run(["sync"], check=True) # Wait for device nodes to be created by udev efi_part = f"{target_device}{sep}2" diff --git a/src/lufus/writing/windows/flash.py b/src/lufus/writing/windows/flash.py index 690915f..a68895f 100644 --- a/src/lufus/writing/windows/flash.py +++ b/src/lufus/writing/windows/flash.py @@ -71,12 +71,12 @@ def _fix_efi_bootloader(efi_mount): log.info("EFI bootloader fix: BOOTX64.EFI not found, will attempt to create at %s", boot_dir) bootx64 = os.path.join(boot_dir, "BOOTX64.EFI") - os.makedirs(boot_dir, exist_ok=True) + run_cmd(["sudo", "mkdir", "-p", boot_dir]) log.info("EFI bootloader fix: created directory %s", boot_dir) src = _find_path_case_insensitive(efi_mount, "EFI", "Microsoft", "Boot", "bootmgfw.efi") if src: - shutil.copy2(src, bootx64) + run_cmd(["sudo", "cp", src, bootx64]) log.info("EFI bootloader fix: copied %s -> %s", src, bootx64) return @@ -155,7 +155,7 @@ def _copy_file(src: str, dst: str) -> str: def _find_ntfs_tool(status_cb=None) -> str | None: """Find mkfs.ntfs/mkntfs, installing ntfs-3g if needed. Returns command name or None.""" for candidate in ["mkfs.ntfs", "mkntfs"]: - if shutil.which(candidate): + if subprocess.run(["which", candidate], capture_output=True).returncode == 0: return candidate if status_cb: @@ -167,13 +167,13 @@ def _find_ntfs_tool(status_cb=None) -> str | None: ["zypper", "install", "-y", "ntfs-3g"], ] for pm_cmd in pkg_managers: - if shutil.which(pm_cmd[0]): + if subprocess.run(["which", pm_cmd[0]], capture_output=True).returncode == 0: run_cmd(["sudo"] + pm_cmd) break # stop after the first working package manager # Re-check after installation attempt for candidate in ["mkfs.ntfs", "mkntfs"]: - if shutil.which(candidate): + if subprocess.run(["which", candidate], capture_output=True).returncode == 0: return candidate # installation succeeded return None @@ -181,7 +181,7 @@ def _find_ntfs_tool(status_cb=None) -> str | None: def _ensure_wimlib(status_cb=None) -> None: """Install wimlib-imagex if not present. Raises FileNotFoundError if it can't be found after install.""" - if shutil.which("wimlib-imagex"): + if subprocess.run(["which", "wimlib-imagex"], capture_output=True).returncode == 0: return if status_cb: status_cb("wimlib-imagex not found, attempting to install...") @@ -192,10 +192,10 @@ def _ensure_wimlib(status_cb=None) -> None: ["zypper", "install", "-y", "wimtools"], ] for pm_cmd in pkg_managers: - if shutil.which(pm_cmd[0]): + if subprocess.run(["which", pm_cmd[0]], capture_output=True).returncode == 0: run_cmd(["sudo"] + pm_cmd) break - if not shutil.which("wimlib-imagex"): + if subprocess.run(["which", "wimlib-imagex"], capture_output=True).returncode != 0: raise FileNotFoundError( "wimlib-imagex not found. Install manually: sudo pacman -S wimlib / sudo apt install wimtools" ) @@ -271,34 +271,20 @@ def _copy_efi_boot_files(iso_mount, mount_efi, _status): if efi_src: efi_items = os.listdir(efi_src) _status(f"Found EFI/ with {len(efi_items)} items: {efi_items}") - for item in efi_items: - src_path = os.path.join(efi_src, item) - dst_path = os.path.join(mount_efi, "EFI", item) - if os.path.isdir(src_path): - shutil.copytree(src_path, dst_path, dirs_exist_ok=True) - else: - os.makedirs(os.path.dirname(dst_path), exist_ok=True) - shutil.copy2(src_path, dst_path) + run_cmd(["sudo", "cp", "-r"] + [os.path.join(efi_src, i) for i in efi_items] + [mount_efi]) _status("Copied EFI/ tree to EFI partition") else: _status("WARNING: No EFI directory found in ISO - drive may not be UEFI bootable") boot_src = _find_path_case_insensitive(iso_mount, "boot") if boot_src: - for item in os.listdir(boot_src): - src_path = os.path.join(boot_src, item) - dst_path = os.path.join(mount_efi, "boot", item) - if os.path.isdir(src_path): - shutil.copytree(src_path, dst_path, dirs_exist_ok=True) - else: - os.makedirs(os.path.dirname(dst_path), exist_ok=True) - shutil.copy2(src_path, dst_path) + run_cmd(["sudo", "cp", "-r"] + [os.path.join(boot_src, i) for i in os.listdir(boot_src)] + [mount_efi]) _status("Copied boot/ tree to EFI partition") for fname in ["bootmgr", "bootmgr.efi"]: src = _find_path_case_insensitive(iso_mount, fname) if src: - shutil.copy2(src, os.path.join(mount_efi, fname)) + run_cmd(["sudo", "cp", src, f"{mount_efi}/{fname}"]) _status(f"Copied {fname} to EFI partition root") _fix_efi_bootloader(mount_efi) @@ -426,7 +412,7 @@ def _status(msg): # Step 7: Sync _status("Syncing all writes to disk...") - os.sync() + run_cmd(["sudo", "sync"]) _emit(97) _status("Sync complete") diff --git a/src/lufus/writing/windows/tweaks.py b/src/lufus/writing/windows/tweaks.py index fe3daac..095e78a 100644 --- a/src/lufus/writing/windows/tweaks.py +++ b/src/lufus/writing/windows/tweaks.py @@ -1,4 +1,3 @@ -# Not tested, at least by me, I don't remember when that was added, and never used it nor really know what it's supposed to do. """Windows installation customization functions. These modify Windows installation media (boot.wim, autounattend.xml) @@ -10,7 +9,6 @@ import re import subprocess import os -import shutil from lufus.utils import get_mount_and_drive from lufus import state from lufus.lufus_logging import get_logger @@ -56,7 +54,7 @@ def win_hardware_bypass(): cmd_string = "\n".join(commands) + "\n" log.info("win_hardware_bypass: injecting registry keys into boot.wim at %s...", mount) try: - os.makedirs("/media/tempwinmnt", exist_ok=True) + subprocess.run(["mkdir", "/media/tempwinmnt"], check=True) subprocess.run(["wimmountrw", f"{mount}/sources/boot.wim", "2", "/media/tempwinmnt"], check=True) subprocess.run( ["chntpw", "e", "/media/tempwinmnt/Windows/System32/config/SYSTEM"], @@ -66,7 +64,7 @@ def win_hardware_bypass(): check=True, ) subprocess.run(["wimunmount", "/media/tempwinmnt", "--commit"], check=True) - shutil.rmtree("/media/tempwinmnt", ignore_errors=True) + subprocess.run(["rm", "-rf", "/media/tempwinmnt"], check=True) log.info("win_hardware_bypass: registry keys injected successfully.") except subprocess.CalledProcessError as e: log.error("win_hardware_bypass: CalledProcessError: %s", e.stderr) @@ -81,7 +79,7 @@ def win_local_acc(): cmd_string = "\n".join(commands) + "\n" log.info("win_local_acc: bypassing online account requirement at %s...", mount) try: - os.makedirs("/media/tempwinmnt", exist_ok=True) + subprocess.run(["mkdir", "/media/tempwinmnt"], check=True) subprocess.run(["wimmountrw", f"{mount}/sources/boot.wim", "2", "/media/tempwinmnt"], check=True) subprocess.run( ["chntpw", "e", "/media/tempwinmnt/Windows/System32/config/SOFTWARE"], @@ -91,7 +89,7 @@ def win_local_acc(): check=True, ) subprocess.run(["wimunmount", "/media/tempwinmnt", "--commit"], check=True) - shutil.rmtree("/media/tempwinmnt", ignore_errors=True) + subprocess.run(["rm", "-rf", "/media/tempwinmnt"], check=True) log.info("win_local_acc: online account bypass applied successfully.") except subprocess.CalledProcessError as e: log.error("win_local_acc: CalledProcessError: %s", e.stderr)