#!/usr/bin/env python3 import argparse import subprocess import os import shutil import sys import sysconfig VERSION = "1.0.7" HOOK_FILENAME = "hook-ffmpeg.py" # This hook is used by PyInstaller. It tells PyInstaller to treat "ffmpeg" as a hidden import. # (PyInstaller sometimes misses modules that are single-file or not recognized as packages.) HOOK_CONTENT = "hiddenimports = ['ffmpeg']\n" # Global flag to know if we created the hook file (so we can delete it later) created_hook = False def create_ffmpeg_hook(): """Create a hook file for ffmpeg in the current directory if it doesn't already exist.""" global created_hook if not os.path.exists(HOOK_FILENAME): with open(HOOK_FILENAME, "w", encoding="utf-8") as hook_file: hook_file.write(HOOK_CONTENT) print(f"Created hook file: {HOOK_FILENAME}") created_hook = True def get_extra_pyinstaller_args(): """ Returns a list of extra arguments for PyInstaller: - Forces inclusion of the ffmpeg module. - Adds our additional hooks directory. - Adds the Python shared library using the correct INSTSONAME. """ extra_args = [ "--hidden-import", "ffmpeg", "--collect-submodules", "ffmpeg", "--additional-hooks-dir", "." ] instsoname = sysconfig.get_config_var("INSTSONAME") # e.g., libpython3.11.so.1.0 if instsoname: libdir = sysconfig.get_config_var("LIBDIR") libpython_path = os.path.join(libdir, instsoname) if libdir else None if not libpython_path or not os.path.exists(libpython_path): fallback = os.path.join("/usr/lib/x86_64-linux-gnu", instsoname) if os.path.exists(fallback): libpython_path = fallback if libpython_path and os.path.exists(libpython_path): extra_args.extend(["--add-binary", f"{libpython_path}:."]) else: print("Warning: Could not locate the Python shared library.") return extra_args def build_binary(): """ Builds a one-file binary from screenrecord.py using PyInstaller. Attempts: 1. A locally installed PyInstaller. 2. pipx if available. 3. Installing PyInstaller in a virtual environment (with --break-system-packages). Returns True on success, False otherwise. """ create_ffmpeg_hook() pyinstaller_args = ["--onefile"] + get_extra_pyinstaller_args() + ["screenrecord.py"] # Try using a locally installed PyInstaller. try: import PyInstaller.__main__ print("Using locally installed PyInstaller.") PyInstaller.__main__.run(pyinstaller_args) return True except ImportError: pass # Try using pipx. if shutil.which("pipx"): print("PyInstaller not found locally. Using pipx to run PyInstaller.") try: cmd = ["pipx", "run", "pyinstaller"] + pyinstaller_args subprocess.check_call(cmd) return True except subprocess.CalledProcessError: print("Error: 'pipx run pyinstaller' failed.") return False # If pipx isn't available, check if we're in a virtual environment. if os.environ.get("VIRTUAL_ENV") is not None: print("In a virtual environment. Installing PyInstaller using pip with --break-system-packages...") try: subprocess.check_call( [sys.executable, "-m", "pip", "install", "--break-system-packages", "pyinstaller"] ) except subprocess.CalledProcessError: print("Error: Failed to install PyInstaller in the virtual environment.") return False try: import PyInstaller.__main__ PyInstaller.__main__.run(pyinstaller_args) return True except Exception as e: print("Error: PyInstaller run failed after installation in venv:", e) return False print("Error: PyInstaller is not installed and neither pipx nor a virtual environment is available.") print("Please install pipx or run this installer in a virtual environment, then try again.") return False def cleanup(): """Remove temporary build files and our hook file if we created it.""" dirs_to_remove = ["build"] files_to_remove = [HOOK_FILENAME, "screenrecord.spec"] for d in dirs_to_remove: if os.path.isdir(d): try: shutil.rmtree(d) print(f"Removed directory: {d}") except Exception as e: print(f"Warning: Could not remove directory {d}: {e}") for f in files_to_remove: if os.path.exists(f): try: os.remove(f) print(f"Removed file: {f}") except Exception as e: print(f"Warning: Could not remove file {f}: {e}") def install(): print("Building binary with PyInstaller...") if not build_binary(): sys.exit(1) # Determine the binary name; PyInstaller typically creates "screenrecord" in the dist folder. binary_name = "screenrecord" dist_path = os.path.join("dist", binary_name) if not os.path.exists(dist_path): # As a fallback, check for a .py extension. dist_path = os.path.join("dist", "screenrecord.py") if not os.path.exists(dist_path): print("Error: Binary not found in the 'dist' folder after build.") sys.exit(1) print(f"Built binary located at: {dist_path}") confirm = input("Do you want to move the binary to ~/.local/bin for global access? [y/N]: ") if confirm.lower() in ["y", "yes"]: local_bin = os.path.expanduser("~/.local/bin") os.makedirs(local_bin, exist_ok=True) destination = os.path.join(local_bin, binary_name) try: shutil.move(dist_path, destination) print(f"Installation successful! Binary moved to {destination}") except Exception as e: print(f"Error moving binary to {destination}: {e}") sys.exit(1) else: print("Installation complete. The binary remains in the 'dist' folder.") # Cleanup temporary files created by PyInstaller and this installer. cleanup() def uninstall(): local_bin = os.path.expanduser("~/.local/bin") binary_path = os.path.join(local_bin, "screenrecord") if os.path.exists(binary_path): try: os.remove(binary_path) print(f"Uninstalled: Removed {binary_path}") except Exception as e: print(f"Error removing {binary_path}: {e}") sys.exit(1) else: print("Uninstall: 'screenrecord' binary not found in ~/.local/bin.") def main(): parser = argparse.ArgumentParser(description="Installer for screenrecord.py") parser.add_argument("command", choices=["install", "uninstall", "version", "help"], help="Command: install, uninstall, version, or help") args = parser.parse_args() if args.command == "install": install() elif args.command == "uninstall": uninstall() elif args.command == "version": print(f"screenrecord installer version {VERSION}") elif args.command == "help": parser.print_help() if __name__ == "__main__": main()