From 65fb5af9835f222660714ae4070ffd2987609a4f Mon Sep 17 00:00:00 2001 From: Jason Ross Date: Mon, 25 Aug 2025 20:31:02 -0500 Subject: [PATCH 1/8] fixing some issues and creating spec files --- .../android-file-handler-linux.spec | 47 +++++++++++++++++++ .../android-file-handler-windows.spec | 23 +++++---- src/main.py | 7 +-- 3 files changed, 61 insertions(+), 16 deletions(-) create mode 100644 scripts/spec_scripts/android-file-handler-linux.spec rename android-file-handler-windows.spec => scripts/spec_scripts/android-file-handler-windows.spec (71%) diff --git a/scripts/spec_scripts/android-file-handler-linux.spec b/scripts/spec_scripts/android-file-handler-linux.spec new file mode 100644 index 0000000..50671f7 --- /dev/null +++ b/scripts/spec_scripts/android-file-handler-linux.spec @@ -0,0 +1,47 @@ +# -*- mode: python ; coding: utf-8 -*- +block_cipher = None +a = Analysis( + ['src/main.py'], + pathex=['src'], + binaries=[], + datas=[('src/gui', 'gui')], + hiddenimports=[ + 'gui', + 'gui.file_browser', + 'gui.license_agreement', + 'gui.main_window', + 'gui.progress_handler', + 'adb_manager' + ], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False, +) +pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + [], + name='android-file-handler', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=False, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + icon='icon_media/robot_files_256.png', +) \ No newline at end of file diff --git a/android-file-handler-windows.spec b/scripts/spec_scripts/android-file-handler-windows.spec similarity index 71% rename from android-file-handler-windows.spec rename to scripts/spec_scripts/android-file-handler-windows.spec index 46f1749..1303195 100644 --- a/android-file-handler-windows.spec +++ b/scripts/spec_scripts/android-file-handler-windows.spec @@ -1,15 +1,18 @@ # -*- mode: python ; coding: utf-8 -*- - - block_cipher = None - - a = Analysis( ['src/main.py'], - pathex=[], + pathex=['src'], binaries=[], - datas=[], - hiddenimports=[], + datas=[('src/gui', 'gui')], + hiddenimports=[ + 'gui', + 'gui.file_browser', + 'gui.license_agreement', + 'gui.main_window', + 'gui.progress_handler' + 'adb_manager' + ], hookspath=[], hooksconfig={}, runtime_hooks=[], @@ -20,7 +23,6 @@ a = Analysis( noarchive=False, ) pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) - exe = EXE( pyz, a.scripts, @@ -35,10 +37,11 @@ exe = EXE( upx=True, upx_exclude=[], runtime_tmpdir=None, - console=True, + console=False, disable_windowed_traceback=False, argv_emulation=False, target_arch=None, codesign_identity=None, entitlements_file=None, -) + icon='icon_media/robot_files_256.ico', +) \ No newline at end of file diff --git a/src/main.py b/src/main.py index 60d80bf..a9186e4 100644 --- a/src/main.py +++ b/src/main.py @@ -4,12 +4,7 @@ Android File Handler - Main Entry Point Simple entry point to launch the Android file transfer application. """ -try: - # Try relative import first (when used as module) - from .gui.main_window import main -except ImportError: - # Fall back to direct import (when run directly) - from gui.main_window import main +from gui.main_window import main if __name__ == "__main__": -- 2.47.3 From bbcdbdbede512201efa2135bfdd388cbbf52d221 Mon Sep 17 00:00:00 2001 From: Jason Ross Date: Tue, 26 Aug 2025 14:37:54 -0500 Subject: [PATCH 2/8] updated workflow to use spec scripts --- .github/workflows/simple-release.yml | 134 ++------------------------- 1 file changed, 7 insertions(+), 127 deletions(-) diff --git a/.github/workflows/simple-release.yml b/.github/workflows/simple-release.yml index b46473f..c9f0d1c 100644 --- a/.github/workflows/simple-release.yml +++ b/.github/workflows/simple-release.yml @@ -53,7 +53,8 @@ jobs: - name: Build Windows executable run: | - poetry run pyinstaller --onefile --windowed --icon=icon_media/robot_files_256.ico --name android-file-handler src/main.py + # Use the Windows spec file so packaging is consistent and reproducible + poetry run pyinstaller scripts/spec_scripts/android-file-handler-windows.spec - name: Upload Windows artifact uses: actions/upload-artifact@v4 @@ -101,7 +102,8 @@ jobs: export PATH="$HOME/.local/bin:$PATH" poetry env use python3.12 || true poetry install - poetry run pyinstaller --onefile --icon=icon_media/robot_files_256.png --name android-file-handler src/main.py + # Use the Linux spec file for reproducible builds + poetry run pyinstaller scripts/spec_scripts/android-file-handler-linux.spec - name: Package .deb (fpm) run: | @@ -159,7 +161,8 @@ jobs: run: | export PATH="$HOME/.local/bin:/usr/local/bin:$PATH" poetry install - poetry run pyinstaller --onefile --icon=icon_media/robot_files_256.png --name android-file-handler src/main.py + # Use the Linux spec file for reproducible builds on Arch + poetry run pyinstaller scripts/spec_scripts/android-file-handler-linux.spec - name: Package pacman (fpm) run: | @@ -216,127 +219,4 @@ jobs: draft: false prerelease: false env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - # build-rocky: - # runs-on: ubuntu-latest - # container: - # image: rockylinux:9 - # steps: - # - name: Robust dnf setup and install build deps (retry-clean-refresh) - # run: | - # set -euo pipefail - # retry() { - # local max_tries=5 - # local sleep_base=3 - # local n=1 - # until "$@"; do - # if [ $n -ge $max_tries ]; then - # echo "Command failed after $n attempts: $*" - # return 1 - # fi - # echo "Command failed: $*. Retrying ($n/$max_tries) after $((sleep_base * n))s..." - # sleep $((sleep_base * n)) - # n=$((n + 1)) - # done - # return 0 - # } - - # # Ensure plugin and enable CRB - # retry dnf -y install dnf-plugins-core - # retry dnf config-manager --set-enabled crb || true - - # # Clean caches and refresh metadata - # retry dnf -y clean all - # rm -rf /var/cache/dnf || true - # retry dnf -y makecache --refresh - - # # Install build-time deps including ruby so gem stdlib is present - # retry dnf -y install -y \ - # git gcc make openssl-devel bzip2-devel libffi-devel zlib-devel xz-devel \ - # readline-devel sqlite-devel tk-devel gdbm-devel libuuid-devel patch \ - # ruby ruby-devel rubygems rpm-build tar ca-certificates wget - - # - name: Clean up pyenv - # run: | - # echo "Removing existing pyenv installations" - # rm -rf "${GITHUB_WORKSPACE}/.pyenv" - # rm -rf "${HOME}/.pyenv" - - # - name: Tarball-based pyenv + python-build (non-interactive) - # env: - # PYENV_ROOT: ${{ github.workspace }}/.pyenv # Set this BEFORE running installer - # run: | - # set -euo pipefail - - # echo "PYENV_ROOT is set to: $PYENV_ROOT" - # mkdir -p "$PYENV_ROOT" - - # echo "Downloading pyenv installer and running it" - # wget -qO- https://pyenv.run | bash - - # # Initialize pyenv environment for this job - # export PATH="$PYENV_ROOT/bin:$PATH" - # eval "$($PYENV_ROOT/bin/pyenv init -)" - - # # Persist environment variables for subsequent steps - # echo "PYENV_ROOT=$PYENV_ROOT" >> $GITHUB_ENV - # echo "PATH=$PYENV_ROOT/shims:$PYENV_ROOT/bin:$PATH" >> $GITHUB_ENV - - # echo "pyenv version:" - # "$PYENV_ROOT/bin/pyenv" --version || true - - # - name: Build Python with pyenv (3.12.11) and set global - # env: - # PYENV_ROOT: ${{ github.workspace }}/.pyenv - # run: | - # set -euo pipefail - # export PYENV_ROOT="${PYENV_ROOT}" - # export PATH="$PYENV_ROOT/bin:$PATH" - # eval "$($PYENV_ROOT/bin/pyenv init -)" - # # Install python (idempotent) - # $PYENV_ROOT/bin/pyenv install -s 3.12.11 - # $PYENV_ROOT/bin/pyenv global 3.12.11 - # python --version - # pip --version - - # - name: Install Poetry - # uses: snok/install-poetry@v1 - # with: - # version: latest - # virtualenvs-create: true - # virtualenvs-in-project: true - - # - name: Configure Poetry - # run: | - # echo 'export PATH="$HOME/.local/bin:$PATH"' >> $GITHUB_ENV - # export PATH="$HOME/.local/bin:$PATH" - # poetry config virtualenvs.create true - # poetry config virtualenvs.in-project true - - # - name: Install dependencies & build executable - # run: | - # set -euo pipefail - # export PATH="$HOME/.local/bin:$PYENV_ROOT/shims:$PYENV_ROOT/bin:$PATH" - # poetry env use 3.12.11 || true - # poetry install - # poetry run pyinstaller --onefile --icon=icon_media/robot_files_256.png --name android-file-handler src/main.py - - # - name: Install fpm and package .rpm - # run: | - # set -euo pipefail - # # install pinned fpm and ensure binary is in system bindir - # gem install --no-document -v "${FPM_VERSION}" fpm --bindir /usr/local/bin - # echo "/usr/local/bin" >> $GITHUB_PATH - - # # Build rpm with explicit filename - # VERSION="$(poetry version -s)" - # mkdir -p dist - # chmod +x dist/android-file-handler - # fpm -s dir -t rpm -n android-file-handler -v "$VERSION" --architecture x86_64 --prefix /usr/local/bin -p "dist/android-file-handler-${VERSION}-1.x86_64.rpm" dist/android-file-handler - - # - name: Upload Rocky RPM - # uses: actions/upload-artifact@v4 - # with: - # name: rocky-package - # path: dist/*.rpm + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file -- 2.47.3 From d654453a2f749fb20c2b371a19bb0e006c793483 Mon Sep 17 00:00:00 2001 From: Jason Ross Date: Tue, 26 Aug 2025 14:56:12 -0500 Subject: [PATCH 3/8] add linux local build script --- scripts/build_exe.sh | 4 ---- 1 file changed, 4 deletions(-) delete mode 100755 scripts/build_exe.sh diff --git a/scripts/build_exe.sh b/scripts/build_exe.sh deleted file mode 100755 index 01a4c29..0000000 --- a/scripts/build_exe.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -poetry lock -poetry install -poetry run pyinstaller --onefile src/main.py --distpath dist --name android-file-handler-windows \ No newline at end of file -- 2.47.3 From d8b6bafc38ed012b9293479119850ad4f8c5eddf Mon Sep 17 00:00:00 2001 From: Jason Ross Date: Tue, 26 Aug 2025 14:56:35 -0500 Subject: [PATCH 4/8] add linux local build script --- scripts/build_package_linux.sh | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100755 scripts/build_package_linux.sh diff --git a/scripts/build_package_linux.sh b/scripts/build_package_linux.sh new file mode 100755 index 0000000..7f3a985 --- /dev/null +++ b/scripts/build_package_linux.sh @@ -0,0 +1,22 @@ +#!/bin/bash +set -euo pipefail + +poetry lock +poetry install + +# Build using the Linux spec so the build is reproducible and uses the project spec +poetry run pyinstaller scripts/spec_scripts/android-file-handler-linux.spec --distpath dist + +# Package into a Debian .deb using fpm +export PATH="$HOME/.local/bin:/usr/local/bin:$PATH" +VERSION="$(poetry version -s)" +mkdir -p dist +chmod +x dist/android-file-handler +# explicit filename into dist/ (Debian .deb) +# change directory into dist and package the binary named 'android-file-handler' +# this ensures the package installs /usr/local/bin/android-file-handler (no extra dist/ prefix) + +# Debian +fpm -s dir -t deb -n android-file-handler -v "$VERSION" \ +--architecture amd64 --prefix /usr/local/bin --deb-user root --deb-group root \ +-p "dist/android-file-handler_${VERSION}_amd64.deb" -C dist android-file-handler \ No newline at end of file -- 2.47.3 From a6c7b3dd26c49fb2ef4582a7bd499cbf350104ce Mon Sep 17 00:00:00 2001 From: Jason Ross Date: Tue, 26 Aug 2025 16:16:42 -0500 Subject: [PATCH 5/8] updated logic to put license agreeement and platform-tools in user writable folders --- poetry.lock | 10 +- pyproject.toml | 3 +- scripts/build_package_linux.sh | 2 +- .../android-file-handler-linux.spec | 4 +- src/adb_manager.py | 323 +++++++++++------- src/gui/license_agreement.py | 69 ++-- 6 files changed, 257 insertions(+), 154 deletions(-) diff --git a/poetry.lock b/poetry.lock index 37769b6..d365585 100644 --- a/poetry.lock +++ b/poetry.lock @@ -533,14 +533,14 @@ files = [ [[package]] name = "platformdirs" -version = "4.3.8" +version = "4.4.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.9" -groups = ["dev"] +groups = ["main", "dev"] files = [ - {file = "platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"}, - {file = "platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc"}, + {file = "platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85"}, + {file = "platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf"}, ] [package.extras] @@ -909,4 +909,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [metadata] lock-version = "2.1" python-versions = "<3.13,>=3.12" -content-hash = "dfb6152cb0fb34dcda59387fbaef4f445b0f38437bd7bdfe25b38b62b14d527e" +content-hash = "2e5b4bc609ab9efe7fe2f83c7c475b1189133759463452d0ed664870cea7ed08" diff --git a/pyproject.toml b/pyproject.toml index 413aaea..325dbff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,11 +8,12 @@ authors = [ license = { text = "MIT" } readme = "README.md" requires-python = "<3.13,>=3.12" -dependencies = ["requests>=2.32.4,<3.0.0"] +dependencies = ["requests>=2.32.4,<3.0.0", "platformdirs>4.0.0,<5.0.0"] [tool.poetry.dependencies] python = "<3.13,>=3.12" requests = ">=2.32.4,<3.0.0" +platformdirs = ">4.0.0,<5.0.0" [build-system] requires = ["poetry-core>=2.0.0,<3.0.0"] diff --git a/scripts/build_package_linux.sh b/scripts/build_package_linux.sh index 7f3a985..b4443dc 100755 --- a/scripts/build_package_linux.sh +++ b/scripts/build_package_linux.sh @@ -5,7 +5,7 @@ poetry lock poetry install # Build using the Linux spec so the build is reproducible and uses the project spec -poetry run pyinstaller scripts/spec_scripts/android-file-handler-linux.spec --distpath dist +poetry run pyinstaller /home/jasonross/workspace/android_file_handler_adb/scripts/spec_scripts/android-file-handler-linux.spec --distpath dist # Package into a Debian .deb using fpm export PATH="$HOME/.local/bin:/usr/local/bin:$PATH" diff --git a/scripts/spec_scripts/android-file-handler-linux.spec b/scripts/spec_scripts/android-file-handler-linux.spec index 50671f7..a5819d3 100644 --- a/scripts/spec_scripts/android-file-handler-linux.spec +++ b/scripts/spec_scripts/android-file-handler-linux.spec @@ -1,10 +1,10 @@ # -*- mode: python ; coding: utf-8 -*- block_cipher = None a = Analysis( - ['src/main.py'], + ['../../src/main.py'], pathex=['src'], binaries=[], - datas=[('src/gui', 'gui')], + datas=[('../../src/gui', 'gui')], hiddenimports=[ 'gui', 'gui.file_browser', diff --git a/src/adb_manager.py b/src/adb_manager.py index 76c61ad..42615c1 100644 --- a/src/adb_manager.py +++ b/src/adb_manager.py @@ -14,6 +14,9 @@ import glob import time import re from typing import Optional, Tuple, Callable +import tempfile +from typing import Optional +import hashlib def get_executable_directory() -> str: """Get the directory containing the executable or script.""" @@ -23,6 +26,140 @@ def get_executable_directory() -> str: else: # Running as script - use the script's directory return os.path.dirname(os.path.abspath(__file__)) + +def ensure_platform_tools_in_user_dir(version_tag: Optional[str] = "latest") -> str: + """Ensure platform-tools installed in a per-user data dir and return adb path. + + Behavior: + - Uses platformdirs.user_data_dir('android-file-handler') if available, + else falls back to ~/.local/share/android-file-handler (POSIX) or + %LOCALAPPDATA% on Windows via expanduser. + - Installs into /platform-tools// and creates/update + a symlink /platform-tools/current -> . + - Downloads into a temp dir and moves atomically to avoid partial installs. + - Sets executable permissions on adb binary. + - Returns absolute path to adb binary (no PATH modification required). + """ + try: + from platformdirs import user_data_dir # type: ignore + except Exception: + user_data_dir = None + + # Determine base data dir + if user_data_dir: + data_root = os.path.join(user_data_dir("android-file-handler"), "platform-tools") + else: + # Fallback: use home-based location + home = os.path.expanduser("~") + data_root = os.path.join(home, ".local", "share", "android-file-handler", "platform-tools") + + os.makedirs(data_root, exist_ok=True) + + target_version = version_tag or "latest" + target_dir = os.path.join(data_root, target_version) + current_link = os.path.join(data_root, "current") + + # If current symlink exists and points to a valid adb, return it + if os.path.islink(current_link): + try: + resolved = os.path.realpath(current_link) + adb_name = "adb.exe" if sys.platform.startswith("win") else "adb" + candidate = os.path.join(resolved, adb_name) + if os.path.isfile(candidate): + return candidate + except Exception: + pass + + # If requested version already installed, point current there + if os.path.isdir(target_dir) and os.path.isfile(os.path.join(target_dir, "adb" if not sys.platform.startswith("win") else "adb.exe")): + # update symlink atomically + if os.path.islink(current_link) or os.path.exists(current_link): + try: + os.remove(current_link) + except Exception: + pass + try: + os.symlink(target_dir, current_link) + except Exception: + # best-effort, ignore if unable to create symlink + pass + return os.path.join(target_dir, "adb.exe" if sys.platform.startswith("win") else "adb") + + # Download into temp location and extract + tmp_dir = tempfile.mkdtemp(prefix="platform-tools-") + try: + # choose URL + if sys.platform.startswith("linux"): + url = ADB_LINUX_ZIP_URL + elif sys.platform.startswith("win"): + url = ADB_WIN_ZIP_URL + else: + raise RuntimeError("Unsupported platform for platform-tools download") + + # download in streaming fashion to avoid memory pressure + resp = requests.get(url, stream=True, timeout=30) + resp.raise_for_status() + + zip_path = os.path.join(tmp_dir, "platform-tools.zip") + with open(zip_path, "wb") as fh: + for chunk in resp.iter_content(chunk_size=8192): + if chunk: + fh.write(chunk) + + # extract + with zipfile.ZipFile(zip_path, "r") as zf: + zf.extractall(tmp_dir) + + # the zip contains a top-level platform-tools directory; move that into target_dir + extracted_dir = os.path.join(tmp_dir, "platform-tools") + if not os.path.isdir(extracted_dir): + # try to find a platform-tools directory inside temp + for entry in os.listdir(tmp_dir): + candidate = os.path.join(tmp_dir, entry) + if os.path.isdir(candidate) and entry.lower().startswith("platform-tools"): + extracted_dir = candidate + break + + if not os.path.isdir(extracted_dir): + raise RuntimeError("Platform-tools not found in archive") + + # Atomic install: move extracted_dir -> target_dir (remove existing backup first) + if os.path.isdir(target_dir): + backup = f"{target_dir}.bak" + shutil.rmtree(backup, ignore_errors=True) + shutil.move(target_dir, backup) + shutil.move(extracted_dir, target_dir) + + # Ensure adb executable perms on POSIX + adb_name = "adb.exe" if sys.platform.startswith("win") else "adb" + adb_path = os.path.join(target_dir, adb_name) + if os.path.isfile(adb_path) and os.name == "posix": + os.chmod(adb_path, 0o755) + + # Atomically update 'current' symlink + tmp_link = f"{current_link}.tmp" + try: + if os.path.exists(tmp_link): + os.remove(tmp_link) + os.symlink(target_dir, tmp_link) + os.replace(tmp_link, current_link) + except OSError: + # fallback: remove and recreate + try: + if os.path.exists(current_link): + os.remove(current_link) + os.symlink(target_dir, current_link) + except Exception: + pass + + return adb_path + finally: + # Clean temp dir + try: + if os.path.exists(tmp_dir): + shutil.rmtree(tmp_dir) + except Exception: + pass def get_platform_tools_directory() -> str: """Get platform-tools directory.""" @@ -60,7 +197,6 @@ ADB_WIN_ZIP_URL = ( ADB_LINUX_ZIP_URL = ( "https://dl.google.com/android/repository/platform-tools-latest-linux.zip" ) -LOCAL_ADB_FOLDER = get_platform_tools_directory() OS_TYPE = sys.platform # Determine ADB binary name based on platform @@ -71,7 +207,28 @@ elif OS_TYPE.startswith("win"): else: ADB_BINARY_NAME = "adb" -ADB_BINARY_PATH = os.path.join(LOCAL_ADB_FOLDER, ADB_BINARY_NAME) +# ADB binary path is resolved at runtime via get_adb_binary_path() to avoid +# duplicate logic and to centralize platform-tools installation behavior. + + +def get_adb_binary_path() -> str: + """Return the path to the adb binary, installing platform-tools if needed. + + This central helper ensures a consistent location across the codebase. + """ + adb_name = "adb.exe" if sys.platform.startswith("win") else "adb" + try: + # If platform-tools are installed in the user data dir, prefer that + adb_path = ensure_platform_tools_in_user_dir() + if adb_path and os.path.isfile(adb_path): + return adb_path + except Exception: + pass + + # Fallback: look for an executable next to the project or installed path + local_folder = get_platform_tools_directory() + candidate = os.path.join(local_folder, adb_name) + return candidate class ADBManager: @@ -103,60 +260,39 @@ class ADBManager: def check_local_disk_space(self) -> bool: """Check if there's enough disk space for ADB download.""" try: - free_space = shutil.disk_usage(LOCAL_ADB_FOLDER)[2] + free_space = shutil.disk_usage(get_platform_tools_directory())[2] if free_space < 50 * 1024 * 1024: # 50MB minimum raise Exception("Insufficient disk space") return True except OSError: # Create directory if it doesn't exist - os.makedirs(LOCAL_ADB_FOLDER, exist_ok=True) + os.makedirs(get_platform_tools_directory(), exist_ok=True) return True def download_and_extract_adb(self) -> bool: """Download and extract ADB tools if not present.""" - if os.path.isfile(ADB_BINARY_PATH): - return True - + # Use the centralized installer which will return the adb path (and + # perform a download if needed). If it returns a valid path, report + # success; otherwise return False. try: - # Determine the correct URL based on platform - if OS_TYPE.startswith("linux"): - adb_zip_url = ADB_LINUX_ZIP_URL - elif OS_TYPE.startswith("win"): - adb_zip_url = ADB_WIN_ZIP_URL - else: - print("Unsupported platform") - return False - - if not self.check_local_disk_space(): - return False - - self._update_status("Downloading platform-tools (ADB)...") - - response = requests.get(adb_zip_url, stream=True, timeout=30) - response.raise_for_status() - - binary_archive = zipfile.ZipFile(io.BytesIO(response.content)) - - # Extract to parent directory since the zip contains platform-tools/ folder - extract_to = os.path.dirname(LOCAL_ADB_FOLDER) - os.makedirs(extract_to, exist_ok=True) - binary_archive.extractall(extract_to) - - if OS_TYPE.startswith("linux"): - if os.path.exists(ADB_BINARY_PATH): - os.chmod(ADB_BINARY_PATH, 0o755) - else: - raise Exception("ADB binary not found after extraction") - - self._update_status("Downloaded and extracted platform-tools.") - return True + adb_path = ensure_platform_tools_in_user_dir() + if adb_path and os.path.isfile(adb_path): + # Ensure executable permissions on POSIX + if os.name == "posix": + try: + os.chmod(adb_path, 0o755) + except Exception: + pass + self._update_status("ADB available at: " + adb_path) + return True + return False except Exception as e: - print(f"Failed to download ADB: {e}") + self._update_status(f"Failed to ensure platform-tools: {e}") return False def run_adb_command(self, args: list, capture_output: bool = True): """Run an ADB command and return output.""" - cmd = [ADB_BINARY_PATH] + args + cmd = [get_adb_binary_path()] + args try: if capture_output: p = subprocess.run(cmd, capture_output=True, text=True, timeout=15) @@ -223,35 +359,29 @@ class ADBManager: def pull_folder(self, remote_path: str, local_path: str) -> bool: """Pull files from Android device to local machine.""" - # Normalize paths for better compatibility + # Normalize paths and prepare local_path = os.path.normpath(local_path) remote_path = remote_path.strip() - # Ensure local directory exists try: os.makedirs(local_path, exist_ok=True) except Exception as e: self._update_status(f"Failed to create local directory: {e}") return False - # For Windows root drives, ensure proper formatting + # Warn if writing to root drive on Windows if os.name == "nt": - # Check if this is a root drive (like C:\, D:\, etc.) normalized_path = os.path.abspath(local_path) drive_root = os.path.splitdrive(normalized_path)[0] + os.sep if normalized_path == drive_root: - # Root drive path like C:\ - this might cause issues with ADB self._update_status( "Warning: Transferring to root drive. Consider using a subfolder." ) - cmd = [ADB_BINARY_PATH, "pull", remote_path, local_path] - - # Debug output for troubleshooting + cmd = [get_adb_binary_path(), "pull", remote_path, local_path] self._update_status(f"Command: adb pull '{remote_path}' '{local_path}'") try: - # Start with initial progress self._update_progress(0) self._update_status("Starting transfer...") @@ -276,45 +406,29 @@ class ADBManager: pct = self.parse_progress(line) if pct is not None: - # Use explicit progress when available self._update_progress(pct) last_progress = pct last_update_time = current_time else: - # Improved progress estimation for large transfers elapsed_time = current_time - start_time time_since_last_update = current_time - last_update_time - # Calculate progress based on multiple factors should_update = False new_progress = last_progress - # Time-based progress (update every 2 seconds) if time_since_last_update >= 2.0 and last_progress < 95: - # Estimate progress based on activity and time if line_count > 100: - # For large transfers, use a logarithmic approach - activity_factor = min( - line_count / 1000, 50 - ) # Max 50% from activity - time_factor = min( - elapsed_time / 60, 40 - ) # Max 40% from time (assumes 1-2 min transfers) + activity_factor = min(line_count / 1000, 50) + time_factor = min(elapsed_time / 60, 40) new_progress = min(activity_factor + time_factor, 95) else: - # For smaller transfers, use the original approach new_progress = min(last_progress + 10, 95) - should_update = True - - # Line-based progress (for very active transfers) elif line_count % 50 == 0 and last_progress < 90: - # More conservative line-based updates increment = max(1, min(5, 90 // (line_count // 50 + 1))) new_progress = min(last_progress + increment, 90) should_update = True - # Update progress if needed if should_update and new_progress > last_progress: self._update_progress(int(new_progress)) last_progress = new_progress @@ -329,19 +443,17 @@ class ADBManager: self.current_process = None return True else: - # Capture error output for better debugging error_msg = f"Transfer failed with code {proc.returncode}" if hasattr(proc, "stderr") and proc.stderr: try: stderr_output = proc.stderr.read() if stderr_output: error_msg += f". Error: {stderr_output}" - except: + except Exception: pass self._update_status(error_msg) self.current_process = None return False - except Exception as e: self._update_status(f"Transfer error: {e}") self.current_process = None @@ -349,33 +461,25 @@ class ADBManager: def push_folder(self, local_path: str, remote_path: str) -> bool: """Push files from local machine to Android device.""" - # Normalize paths for better compatibility local_path = os.path.normpath(local_path) remote_path = remote_path.strip() - # Validate local path exists if not os.path.exists(local_path): self._update_status(f"Local path does not exist: {local_path}") return False - # For Windows root drives, ensure proper formatting if os.name == "nt": - # Check if this is a root drive (like C:\, D:\, etc.) normalized_path = os.path.abspath(local_path) drive_root = os.path.splitdrive(normalized_path)[0] + os.sep if normalized_path == drive_root: - # Root drive path like C:\ - this might cause issues with ADB self._update_status( "Warning: Pushing from root drive. Consider using a subfolder." ) - cmd = [ADB_BINARY_PATH, "push", local_path, remote_path] - - # Debug output for troubleshooting + cmd = [get_adb_binary_path(), "push", local_path, remote_path] self._update_status(f"Command: adb push '{local_path}' '{remote_path}'") try: - # Start with initial progress self._update_progress(0) self._update_status("Starting transfer...") @@ -403,46 +507,13 @@ class ADBManager: pct = self.parse_progress(line) if pct is not None: - # Use explicit progress when available self._update_progress(pct) last_progress = pct last_update_time = current_time else: - # Improved progress estimation for large transfers elapsed_time = current_time - start_time - time_since_last_update = current_time - last_update_time - - # Calculate progress based on multiple factors - should_update = False - new_progress = last_progress - - # Time-based progress (update every 2 seconds) - if time_since_last_update >= 2.0 and last_progress < 95: - # Estimate progress based on activity and time - if line_count > 100: - # For large transfers, use a logarithmic approach - activity_factor = min( - line_count / 1000, 50 - ) # Max 50% from activity - time_factor = min( - elapsed_time / 60, 40 - ) # Max 40% from time (assumes 1-2 min transfers) - new_progress = min(activity_factor + time_factor, 95) - else: - # For smaller transfers, use the original approach - new_progress = min(last_progress + 10, 95) - - should_update = True - - # Line-based progress (for very active transfers) - elif line_count % 50 == 0 and last_progress < 90: - # More conservative line-based updates - increment = max(1, min(5, 90 // (line_count // 50 + 1))) - new_progress = min(last_progress + increment, 90) - should_update = True - - # Update progress if needed - if should_update and new_progress > last_progress: + if elapsed_time >= 1.0 and last_progress < 90: + new_progress = min(last_progress + 20, 90) self._update_progress(int(new_progress)) last_progress = new_progress last_update_time = current_time @@ -456,14 +527,13 @@ class ADBManager: self.current_process = None return True else: - # Capture error output for better debugging error_msg = f"Push failed with code {proc.returncode}" if hasattr(proc, "stderr") and proc.stderr: try: stderr_output = proc.stderr.read() if stderr_output: error_msg += f". Error: {stderr_output}" - except: + except Exception: pass self._update_status(error_msg) self.current_process = None @@ -495,7 +565,7 @@ class ADBManager: "Warning: Transferring to root drive. Consider using a subfolder." ) - cmd = [ADB_BINARY_PATH, "pull", remote_file_path, local_file_path] + cmd = [get_adb_binary_path(), "pull", remote_file_path, local_file_path] # Debug output for troubleshooting self._update_status(f"Command: adb pull '{remote_file_path}' '{local_file_path}'") @@ -589,7 +659,7 @@ class ADBManager: "Warning: Pushing from root drive. Consider using a subfolder." ) - cmd = [ADB_BINARY_PATH, "push", local_file_path, remote_file_path] + cmd = [get_adb_binary_path(), "push", local_file_path, remote_file_path] # Debug output for troubleshooting self._update_status(f"Command: adb push '{local_file_path}' '{remote_file_path}'") @@ -681,6 +751,8 @@ class ADBManager: return False return False + # (Method intentionally removed - use top-level ensure_platform_tools_in_user_dir) + class LinuxMTPManager: """Manages MTP operations on Linux systems.""" @@ -808,14 +880,13 @@ class LinuxMTPManager: # Helper functions for standalone usage -def get_adb_binary_path() -> str: - """Get the path to the ADB binary.""" - return ADB_BINARY_PATH - - def is_adb_available() -> bool: - """Check if ADB binary is available.""" - return os.path.isfile(ADB_BINARY_PATH) + """Check if ADB binary is available using the centralized resolver.""" + try: + adb_path = get_adb_binary_path() + return os.path.isfile(adb_path) + except Exception: + return False def get_platform_type() -> str: diff --git a/src/gui/license_agreement.py b/src/gui/license_agreement.py index 06227a0..98390fd 100644 --- a/src/gui/license_agreement.py +++ b/src/gui/license_agreement.py @@ -7,42 +7,73 @@ import os import sys import tkinter as tk from tkinter import messagebox, scrolledtext +import tempfile +import stat -def get_license_file_path(): - """Get the path to the license agreement file.""" - # Get the directory where the application is running from - if getattr(sys, 'frozen', False): - # Running as executable - app_dir = os.path.dirname(sys.executable) +def get_license_file_path() -> str: + """Return the per-user license-agreed file path. + + Uses platform-appropriate user config directory when possible: + - On Linux/macOS this will typically use ~/.config or ~/.android-file-handler fallback. + - On Windows it prefers %APPDATA% (via platformdirs) and falls back to ~. + """ + try: + from platformdirs import user_config_dir + except Exception: + user_config_dir = None + + if getattr(sys, "frozen", False): + # Running as executable: prefer platform dirs + if user_config_dir: + config_dir = os.path.join(user_config_dir("android-file-handler"), "") + else: + config_dir = os.path.expanduser("~/.android-file-handler") + os.makedirs(config_dir, exist_ok=True) + return os.path.join(config_dir, "license_agreed.ini") else: - # Running as script - use directory containing the main script + # Dev mode: store beside project tree app_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - - return os.path.join(app_dir, "license_agreed.ini") + return os.path.join(app_dir, "dev-mode_license_agreed.ini") -def check_license_agreement(): +def check_license_agreement() -> bool: """Check if user has already agreed to the license.""" license_file = get_license_file_path() try: if os.path.exists(license_file): - with open(license_file, 'r') as f: - content = f.read().strip() - return content == "1" + with open(license_file, "r") as f: + return f.read().strip() == "1" return False except Exception: return False -def save_license_agreement(): - """Save that the user has agreed to the license.""" +def save_license_agreement() -> bool: + """Persist that the user agreed to the license. + + Writes atomically and sets secure file permissions on POSIX. + """ license_file = get_license_file_path() try: - # Create directory if it doesn't exist - os.makedirs(os.path.dirname(license_file), exist_ok=True) - with open(license_file, 'w') as f: - f.write("1") + parent_dir = os.path.dirname(license_file) + os.makedirs(parent_dir, exist_ok=True) + + # Atomic write to temporary file then rename + fd, tmp_path = tempfile.mkstemp(dir=parent_dir) + try: + with os.fdopen(fd, "w") as fh: + fh.write("1") + # Set owner-only permissions on POSIX + if os.name == "posix": + os.chmod(tmp_path, 0o600) + os.replace(tmp_path, license_file) + finally: + if os.path.exists(tmp_path): + try: + os.remove(tmp_path) + except Exception: + pass return True except Exception: return False -- 2.47.3 From ae22b497857f02f532cfc74eb4d38afe3d72bf26 Mon Sep 17 00:00:00 2001 From: Jason Ross Date: Tue, 26 Aug 2025 17:06:33 -0500 Subject: [PATCH 6/8] updating Windows install experience --- scripts/build_package_linux.sh | 37 +++++++++++++- scripts/linux_postinst.sh | 26 ++++++++++ .../android-file-handler-linux.spec | 1 - .../android-file-handler-windows.spec | 2 +- scripts/windows/first_run_install.ps1 | 51 +++++++++++++++++++ src/gui/license_agreement.py | 43 +++++++++++++++- src/main.py | 2 + 7 files changed, 156 insertions(+), 6 deletions(-) create mode 100644 scripts/linux_postinst.sh create mode 100644 scripts/windows/first_run_install.ps1 diff --git a/scripts/build_package_linux.sh b/scripts/build_package_linux.sh index b4443dc..40a5c67 100755 --- a/scripts/build_package_linux.sh +++ b/scripts/build_package_linux.sh @@ -17,6 +17,39 @@ chmod +x dist/android-file-handler # this ensures the package installs /usr/local/bin/android-file-handler (no extra dist/ prefix) # Debian +# Prepare packaging layout in a separate directory to avoid clobbering build artifacts +PKG_DIR="pkg_dist" +rm -rf "$PKG_DIR" +mkdir -p "$PKG_DIR/usr/local/bin" \ + "$PKG_DIR/usr/share/applications" \ + "$PKG_DIR/usr/share/icons/hicolor/256x256/apps" + +# Copy built binary +cp dist/android-file-handler "$PKG_DIR/usr/local/bin/android-file-handler" +chmod 0755 "$PKG_DIR/usr/local/bin/android-file-handler" + +# Create .desktop file +cat > "$PKG_DIR/usr/share/applications/android-file-handler.desktop" <<'EOF' +[Desktop Entry] +Type=Application +Name=Android File Handler +Comment=Manage Android device files via ADB +Exec=/usr/local/bin/android-file-handler %U +Icon=android-file-handler +Terminal=false +Categories=Utility;Development; +StartupNotify=true +EOF +chmod 644 "$PKG_DIR/usr/share/applications/android-file-handler.desktop" + +# Copy icon if present in repo (fallback optional) +if [ -f "assets/icons/android-file-handler-256.png" ]; then + cp assets/icons/android-file-handler-256.png "$PKG_DIR/usr/share/icons/hicolor/256x256/apps/android-file-handler.png" + chmod 644 "$PKG_DIR/usr/share/icons/hicolor/256x256/apps/android-file-handler.png" +fi + +# Build the Debian package and include postinst script fpm -s dir -t deb -n android-file-handler -v "$VERSION" \ ---architecture amd64 --prefix /usr/local/bin --deb-user root --deb-group root \ --p "dist/android-file-handler_${VERSION}_amd64.deb" -C dist android-file-handler \ No newline at end of file + --architecture amd64 --prefix /usr/local/bin --deb-user root --deb-group root \ + --after-install scripts/debian/postinst.sh \ + -p "dist/android-file-handler_${VERSION}_amd64.deb" -C "$PKG_DIR" usr/local/bin/android-file-handler usr/share/applications/android-file-handler.desktop usr/share/icons/hicolor/256x256/apps/android-file-handler.png \ No newline at end of file diff --git a/scripts/linux_postinst.sh b/scripts/linux_postinst.sh new file mode 100644 index 0000000..19020a5 --- /dev/null +++ b/scripts/linux_postinst.sh @@ -0,0 +1,26 @@ +#!/bin/sh +set -e + +# Post-installation script for android-file-handler +# - update desktop database if available +# - update icon cache if available +# - ensure binary is executable + +# Update desktop database if the utility exists +if command -v update-desktop-database >/dev/null 2>&1; then + update-desktop-database /usr/share/applications || true +fi + +# Update GTK icon cache for hicolor theme if utility exists +if command -v gtk-update-icon-cache >/dev/null 2>&1; then + if [ -d /usr/share/icons/hicolor ]; then + gtk-update-icon-cache -t -f /usr/share/icons/hicolor || true + fi +fi + +# Ensure installed binary is executable +if [ -f /usr/local/bin/android-file-handler ]; then + chmod 0755 /usr/local/bin/android-file-handler || true +fi + +exit 0 diff --git a/scripts/spec_scripts/android-file-handler-linux.spec b/scripts/spec_scripts/android-file-handler-linux.spec index a5819d3..73f84c8 100644 --- a/scripts/spec_scripts/android-file-handler-linux.spec +++ b/scripts/spec_scripts/android-file-handler-linux.spec @@ -43,5 +43,4 @@ exe = EXE( target_arch=None, codesign_identity=None, entitlements_file=None, - icon='icon_media/robot_files_256.png', ) \ No newline at end of file diff --git a/scripts/spec_scripts/android-file-handler-windows.spec b/scripts/spec_scripts/android-file-handler-windows.spec index 1303195..f5bfa10 100644 --- a/scripts/spec_scripts/android-file-handler-windows.spec +++ b/scripts/spec_scripts/android-file-handler-windows.spec @@ -4,7 +4,7 @@ a = Analysis( ['src/main.py'], pathex=['src'], binaries=[], - datas=[('src/gui', 'gui')], + datas=[('src/gui', 'gui'), ('../scripts/windows/first_run_install.ps1', 'scripts/windows')], hiddenimports=[ 'gui', 'gui.file_browser', diff --git a/scripts/windows/first_run_install.ps1 b/scripts/windows/first_run_install.ps1 new file mode 100644 index 0000000..55c4c64 --- /dev/null +++ b/scripts/windows/first_run_install.ps1 @@ -0,0 +1,51 @@ +param( + [string]$ExePath = "$PSScriptRoot\..\..\dist\android-file-handler.exe", + [string]$IconPath = "$PSScriptRoot\..\..\assets\icons\android-file-handler.ico", + [string]$AppName = "Android File Handler" +) + +function Ensure-Elevated { + if (-not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) { + # Relaunch the script with elevation + $psi = New-Object System.Diagnostics.ProcessStartInfo + $psi.FileName = "powershell.exe" + $psi.Arguments = "-ExecutionPolicy Bypass -File `"$PSCommandPath`"" + $psi.Verb = "runas" + try { + [System.Diagnostics.Process]::Start($psi) | Out-Null + Exit 0 + } catch { + Write-Error "Elevation required to install to Program Files." + Exit 1 + } + } +} + +Ensure-Elevated + +$destDir = Join-Path ${env:ProgramFiles} $AppName +if (-not (Test-Path $destDir)) { New-Item -ItemType Directory -Path $destDir | Out-Null } + +$resolvedExe = Resolve-Path -Path $ExePath -ErrorAction SilentlyContinue +if (-not $resolvedExe) { + Write-Error "Application executable not found at $ExePath" + Exit 1 +} + +Copy-Item -Path $resolvedExe -Destination (Join-Path $destDir (Split-Path $resolvedExe -Leaf)) -Force + +# Create Start Menu shortcut +$programs = Join-Path $env:APPDATA 'Microsoft\Windows\Start Menu\Programs' +$appFolder = Join-Path $programs $AppName +if (-not (Test-Path $appFolder)) { New-Item -ItemType Directory -Path $appFolder | Out-Null } + +$shortcutPath = Join-Path $appFolder "$AppName.lnk" +$wsh = New-Object -ComObject WScript.Shell +$sc = $wsh.CreateShortcut($shortcutPath) +$sc.TargetPath = (Join-Path $destDir (Split-Path $resolvedExe -Leaf)) +$sc.WorkingDirectory = $destDir +if (Test-Path $IconPath) { $sc.IconLocation = Resolve-Path $IconPath } +$sc.Save() + +Write-Output "Installed $AppName to $destDir and created Start Menu shortcut." +Exit 0 diff --git a/src/gui/license_agreement.py b/src/gui/license_agreement.py index 98390fd..78f28ad 100644 --- a/src/gui/license_agreement.py +++ b/src/gui/license_agreement.py @@ -9,6 +9,9 @@ import tkinter as tk from tkinter import messagebox, scrolledtext import tempfile import stat +import subprocess +import sys +import os def get_license_file_path() -> str: @@ -104,12 +107,48 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.""" +def resource_path(relative_path: str) -> str: + """Return absolute path to resource for dev and frozen runs.""" + try: + if getattr(sys, "frozen", False): + base = getattr(sys, "_MEIPASS", os.path.dirname(sys.executable)) + else: + base = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + return os.path.normpath(os.path.join(base, relative_path)) + except Exception: + return os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), relative_path)) + + +def run_windows_first_run_if_needed() -> None: + """If running on Windows and license not agreed, launch first-run installer script. + + Uses `resource_path` to locate the bundled PowerShell script in both dev and frozen modes. + """ + try: + if not sys.platform.startswith("win"): + return + + if check_license_agreement(): + return + + script_rel = os.path.join("scripts", "windows", "first_run_install.ps1") + script_path = resource_path(script_rel) + if not os.path.exists(script_path): + return + try: + subprocess.Popen(["powershell.exe", "-ExecutionPolicy", "Bypass", "-File", script_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + except Exception: + pass + except Exception: + pass + + class LicenseAgreementFrame(tk.Frame): """License agreement UI frame that can be embedded in the main window.""" - + def __init__(self, parent, on_agree_callback): """Initialize the license agreement frame. - + Args: parent: Parent widget on_agree_callback: Function to call when user agrees to license diff --git a/src/main.py b/src/main.py index a9186e4..88a95c7 100644 --- a/src/main.py +++ b/src/main.py @@ -5,7 +5,9 @@ Simple entry point to launch the Android file transfer application. """ from gui.main_window import main +from gui.license_agreement import run_windows_first_run_if_needed if __name__ == "__main__": + run_windows_first_run_if_needed() main() -- 2.47.3 From 10c76489996c23c50925d54676bd996c8f7b003e Mon Sep 17 00:00:00 2001 From: Jason Ross Date: Tue, 26 Aug 2025 17:07:29 -0500 Subject: [PATCH 7/8] documentation update - clarify which is for developers and add instructions for Arch users --- README.md | 2 +- WIKI.md | 89 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 WIKI.md diff --git a/README.md b/README.md index 2ae387a..fef39e0 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ A simple Windows GUI tool to pull folders from an Android device using ADB (Andr - [Poetry](https://python-poetry.org/) for dependency management - (Optional) pyenv for Python version managment -## Installation +## Installation (Developers) 1. Clone this repository: diff --git a/WIKI.md b/WIKI.md new file mode 100644 index 0000000..1072b13 --- /dev/null +++ b/WIKI.md @@ -0,0 +1,89 @@ +# Arch Linux installation instructions + +## Arch Linux installation + +These instructions describe how Arch users can install the Linux build produced by the project's `simple-release.yml` workflow. The workflow produces an Arch package named similar to: + +``` +android-file-handler--1-x86_64.pkg.tar.zst +``` + +Find that file on the project's GitHub Releases page for the matching version and download it. + +### System install (recommended) + +Use pacman to install the downloaded package so package scripts run and files are placed in standard system locations: + +```bash +# from the directory with the downloaded package +sudo pacman -U ./android-file-handler-1.2.3-1-x86_64.pkg.tar.zst +``` + +Replace the filename with the actual release artifact you downloaded. After installation you should have: + +- Binary: e.g. `/usr/local/bin/android-file-handler` +- Desktop entry: `/usr/share/applications/android-file-handler.desktop` +- Icons: under `/usr/share/icons/hicolor/...` + +Launch the app from your application menu (search "Android File Handler") or from a terminal: + +```bash +android-file-handler +``` + +### Non-root / manual install + +If you cannot or do not want to install the package system-wide, you can extract and install files into your home directory manually: + +1. Extract the package to a temporary folder: + +```bash +mkdir -p /tmp/afh_pkg +tar -I zstd -xvf android-file-handler-1.2.3-1-x86_64.pkg.tar.zst -C /tmp/afh_pkg +``` + +2. Copy the executable and desktop/icon files to your local directories: + +```bash +mkdir -p ~/.local/bin ~/.local/share/applications ~/.local/share/icons/hicolor/256x256/apps +cp /tmp/afh_pkg/usr/local/bin/android-file-handler ~/.local/bin/ +chmod +x ~/.local/bin/android-file-handler + +# Copy desktop entry and icon (paths inside the package may vary) +cp /tmp/afh_pkg/usr/share/applications/android-file-handler.desktop ~/.local/share/applications/ +cp /tmp/afh_pkg/usr/share/icons/hicolor/256x256/apps/android-file-handler.png ~/.local/share/icons/hicolor/256x256/apps/ +``` + +3. (Optional) Update desktop and icon caches so the menu picks up the new entry immediately: + +```bash +update-desktop-database ~/.local/share/applications || true +gtk-update-icon-cache -t -f ~/.local/share/icons/hicolor || true +``` + +Now run the app from the menu or with `~/.local/bin/android-file-handler`. + +### Verify package contents + +To inspect the package contents without installing: + +```bash +# list files in the package +pacman -Qlp android-file-handler-1.2.3-1-x86_64.pkg.tar.zst +``` + +To query package info (after installation): + +```bash +pacman -Qi android-file-handler +``` + +### Troubleshooting + +- If the app does not appear in your menu, try logging out/in or run the cache update commands above. +- If icons are missing, ensure the `icons/hicolor/*/apps/android-file-handler.png` entries were copied to the correct theme directories. +- If you manually copied files, ensure `~/.local/bin` is on your PATH; add it to `~/.profile` if necessary: + +```bash +export PATH="$HOME/.local/bin:$PATH" +``` -- 2.47.3 From 95cf26b5e18888841cd29e26dd13c6c116f4b51e Mon Sep 17 00:00:00 2001 From: Jason Ross Date: Tue, 26 Aug 2025 18:34:16 -0500 Subject: [PATCH 8/8] committing working build progress --- .gitignore | 2 + WIKI.md | 2 +- scripts/build_package_linux.sh | 38 ++++++++++++++----- scripts/linux_postinst.sh | 0 .../android-file-handler-linux.spec | 5 ++- 5 files changed, 36 insertions(+), 11 deletions(-) mode change 100644 => 100755 scripts/linux_postinst.sh diff --git a/.gitignore b/.gitignore index 80f6699..84cba60 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,8 @@ build .actrc bin/ test-act.yml +pkg_dist + # Python cache files __pycache__/ *.py[cod] diff --git a/WIKI.md b/WIKI.md index 1072b13..bb17354 100644 --- a/WIKI.md +++ b/WIKI.md @@ -4,7 +4,7 @@ These instructions describe how Arch users can install the Linux build produced by the project's `simple-release.yml` workflow. The workflow produces an Arch package named similar to: -``` +```bash android-file-handler--1-x86_64.pkg.tar.zst ``` diff --git a/scripts/build_package_linux.sh b/scripts/build_package_linux.sh index 40a5c67..a8bd54c 100755 --- a/scripts/build_package_linux.sh +++ b/scripts/build_package_linux.sh @@ -28,7 +28,21 @@ mkdir -p "$PKG_DIR/usr/local/bin" \ cp dist/android-file-handler "$PKG_DIR/usr/local/bin/android-file-handler" chmod 0755 "$PKG_DIR/usr/local/bin/android-file-handler" -# Create .desktop file +# ...existing code... +# Copy icon if present in repo (fallback optional) — do this before creating the .desktop +ICON_SRC="icon_media/robot_files_256.png" +ICON_DST="$PKG_DIR/usr/share/icons/hicolor/256x256/apps/android-file-handler.png" +ICON_INCLUDED=false +if [ -f "$ICON_SRC" ]; then + mkdir -p "$(dirname "$ICON_DST")" + cp "$ICON_SRC" "$ICON_DST" + chmod 644 "$ICON_DST" + ICON_INCLUDED=true +else + echo "Warning: icon not found at $ICON_SRC; packaging without icon" +fi + +# Create .desktop file (icon name matches installed icon) cat > "$PKG_DIR/usr/share/applications/android-file-handler.desktop" <<'EOF' [Desktop Entry] Type=Application @@ -42,14 +56,20 @@ StartupNotify=true EOF chmod 644 "$PKG_DIR/usr/share/applications/android-file-handler.desktop" -# Copy icon if present in repo (fallback optional) -if [ -f "assets/icons/android-file-handler-256.png" ]; then - cp assets/icons/android-file-handler-256.png "$PKG_DIR/usr/share/icons/hicolor/256x256/apps/android-file-handler.png" - chmod 644 "$PKG_DIR/usr/share/icons/hicolor/256x256/apps/android-file-handler.png" +# Build the Debian package and include postinst script +PKG_ITEMS=( "usr/local/bin/android-file-handler" "usr/share/applications/android-file-handler.desktop" ) +if [ "$ICON_INCLUDED" = true ]; then + PKG_ITEMS+=( "usr/share/icons/hicolor/256x256/apps/android-file-handler.png" ) fi -# Build the Debian package and include postinst script +# Debug listing +echo "Packaging the following items (relative to $PKG_DIR):" +printf ' - %s\n' "${PKG_ITEMS[@]}" +for p in "${PKG_ITEMS[@]}"; do ls -la "$PKG_DIR/$p" || echo " (missing) $PKG_DIR/$p"; done + fpm -s dir -t deb -n android-file-handler -v "$VERSION" \ - --architecture amd64 --prefix /usr/local/bin --deb-user root --deb-group root \ - --after-install scripts/debian/postinst.sh \ - -p "dist/android-file-handler_${VERSION}_amd64.deb" -C "$PKG_DIR" usr/local/bin/android-file-handler usr/share/applications/android-file-handler.desktop usr/share/icons/hicolor/256x256/apps/android-file-handler.png \ No newline at end of file + --architecture amd64 --deb-user root --deb-group root \ + --after-install scripts/linux_postinst.sh \ + -p "dist/android-file-handler_${VERSION}_amd64.deb" -C "$PKG_DIR" "${PKG_ITEMS[@]}" + +echo "Package created at dist/android-file-handler_${VERSION}_amd64.deb" \ No newline at end of file diff --git a/scripts/linux_postinst.sh b/scripts/linux_postinst.sh old mode 100644 new mode 100755 diff --git a/scripts/spec_scripts/android-file-handler-linux.spec b/scripts/spec_scripts/android-file-handler-linux.spec index 73f84c8..91d6397 100644 --- a/scripts/spec_scripts/android-file-handler-linux.spec +++ b/scripts/spec_scripts/android-file-handler-linux.spec @@ -4,7 +4,10 @@ a = Analysis( ['../../src/main.py'], pathex=['src'], binaries=[], - datas=[('../../src/gui', 'gui')], + datas=[ + ('../../src/gui', 'gui'), + ('../../scripts/linux_postinst.sh', 'scripts'), + ], hiddenimports=[ 'gui', 'gui.file_browser', -- 2.47.3