File: frigcal-products/unzipped/fixfrozenpaths.py

"""
================================================================================
fixfrozenpaths.py:
  setup path/cwd context for all frozen scripts (part of the frigcal system)

For frozen apps/exes, fix module+resource visibility.  This configures
sys.path and the CWD as needed for the freeze tool used, to grant importers
access to these items.  This module is imported by most scripts frozen in
the frigcal package; it updates the global sys.path and CWD in-place.

In the frozen package, all command-line scripts are frozen executables without 
a ".py" extension, but otherwise work the same, and require no separate Python
install.  In the Mac app, running the app itself launches the frigcal GUI; 
on Windows an Linux, the launcher-GUI executable must be started directly.

The logic here was originally hashed out in PyEdit's file of the same name;
see that system's more-complex version for additional background details.

FORMER CAVEAT: an os.chdir(exedir) for PyInstaller apps was used to make the
empty __file__ dir portably map to the install dir in sys.argv[0] (instead of the
_MEIPASS temp unzip folder), but precluded using any relative paths in command
lines.  This isn't an issue in PyEdit or PyMailGUI (they have no or rare args),
but utilities in both frigcal and mergeall process file paths.  Omitting the
chdir almost works, but the launcher then couldn't find its icons, help, and
script if run from elsewhere via a command line (instead of clicked directly).

   FIXED: the chdir is no longer run; instead, clients call fetchMyInstallDir()
   to select the install path explicitly per deployment mode -- from __file __
   for source and Mac apps, and sys.argv[0] for PyInstaller exes.  The cwd is
   unchanged, so relative paths work in all frozen executables' command lines.

   Exception: both frigcal-launcher.pyw and frigcal.py manually os.chdir() to
   the install dir returned from fetchMyInstallDir() early, so they can access 
   their data relative to "." easily; neither has any command-line arguments.
   Ditto for searchcals.py and makenewcaledar.py: both chdir to the install
   dir to make relative calendar paths map to it, and have no pathname args. 
================================================================================
"""

import sys, os
RunningOnMacOS   = sys.platform.startswith('darwin')
RunningOnWindows = sys.platform.startswith('win')
RunningOnLinux   = sys.platform.startswith('linux')      # Android too?, but moot

DEBUG = False
if DEBUG:
    print('file', __file__)           # this in _MEI* (parent's = 'frigcal.py')
    print('argv', sys.argv[0])        # 'frigcal.exe' (+path if not in cwd)
    print('exec', sys.executable)     # 'C:\...\frigcal.exe' (abs of argv[0])
    print('path', sys.path)           # just 2 _MEI* temp unzip folder paths
    print('cwd ', os.getcwd())        # 'C:\...' frigcal.exe dir (iff clicked!)



#===============================================================================
# Set global import-path context to find imported modules.
#===============================================================================

    
if hasattr(sys, 'frozen') and sys.frozen == 'macosx_app':
    #
    # 1) Mac py2app app-bundle folder distribution (now PyInstaller --onedir).
    # Frozen importer's bootloader is in Frigcal.app's Content/MacOS dir.
    # Add '.' for importing config module in app's Content/Resources dir.
    # dirname(__file__) and cwd work for icons, UserGuide.html, scripts,
    # and imports: code is source files, run by the app's bundled Python.
    # Ok to use cwd here: py2app cds to the data dir by default anyhow. 
    #
    sys.path.append(os.getcwd())    # for configs .py

elif hasattr(sys, 'frozen') and (RunningOnWindows or RunningOnLinux):
    #
    # 2) Windows and Linux PyInstaller single-file executable distribution
    # Use exe's path (not temp _MEI*) for config module and all data items.
    # The config module cannot be in PyInstaller's auto-unzip Temp dir.
    # DROPPED os.chdir(): this made empty __file__ dir map to install dir,
    # but precluded any relative paths in cmdline args to frozen exes; see
    # fetchMyInstallDir() below for the later explicit-install-dir scheme.
    # [3.0] sys.argv[0] should be sys.executable to allow for symlinks: rare.
    #
    exepath = sys.argv[0]
    exedir  = os.path.dirname(os.path.abspath(exepath))
    sys.path.append(exedir)         # for configs .py import
    ##os.chdir(exedir)              # NOT for extras => now call fetchMyInstallDir!
    
else:
    #
    # 3) Portable Source code distribution - no tweaking required
    # The src dir is on sys.path: no action required for imports.
    # Data will be located by dirname(__file__) and/or cwd.
    #
    # THIS CASE ALSO INCUDES MACOS PYINSTALLER APP (see ahead)
    # THIS CASE ALSO INCLUDES ANDROID (see ahead)
    #
    pass



#===============================================================================
# Return path where program resource files reside (some are user changeable).
#===============================================================================


def fetchMyInstallDir(__file__):     # not global __file__
    """
    --------------------------------------------------------
    call this to fetch folder where extra items reside;
    use to access installed icons, help, readme, scripts;
    replaces former os.chdir() which precluded rel paths;
    the return value is always an absolute pathname;
    
    pass importer's __file__ to __file__ arg: for frozen
    Mac apps, this module's dir(__file__) is in a zipfile,
    and differs from the importer's dir(__file__); they're
    the same for source code, and unused for Win/Lin exes;
    --------------------------------------------------------
    """
    
   # [3.0] all standalones now built with PyInstaller (no py2app);
   # macOS app use PI's --onedir option which makes it still unique,
   # but macOS tools are raw executables as on Windows and Linux;
   # see special-case code in the tools; too onvoluted to fix here

    if hasattr(sys, 'frozen') and (RunningOnWindows or RunningOnLinux):
        #
        # PyInstaller executable: from sys.argv[0] = exe's dir;
        # __file__ dir is empty and cwd may be any user folder
        # if the importer is run from a command line elsewhere;
        # [3.0] sys.argv[0] s/b sys.executable for rare symlinks.
        #
        exepath = sys.argv[0]
        exedir  = os.path.dirname(os.path.abspath(exepath))
        return exedir

    else:
        #
        # Mac app bundle or source-code: from __file__ as usual;
        # cwd is anywhere: return importing file's install folder;
        # this mod's __file__ is *not* ok to use here for frozen
        # Mac apps: it's in a zipfile, not importer's Resources/;
        #
        # THIS CASE ALSO INCUDES MACOS PYINSTALLER APP
        # [3.0] Subtlety: in the macoOS PyInstaller --onedir app, this
        # works WITHOUT using ../Resources, even though the exe is in
        # Contents/MacOS.  It relies on symlinks auto-created by PI in
        # Contents/Frameworks that point to each item in /Resources, and
        # __file__ is really Contents/Frameworks, not /MacOS.  Else, the 
        # default Calendars/ folder wouldn't be in Resources - but is.
        # This differs from the PyEdit --onedir PI macOS app, which runs
        # the exe in /MacOS and has no symlinks to /Resources items in
        # /Frameworks.  The working theory is that this reflects build 
        # diffs: Frigcal uses --add-data but PyEdit does not.  The
        # structural permutations in PyInstaller products are legion!
        # __file__ is _MEI* unzip folder for --onefile, but not --onedir
        # where __file__ is .app/Contents/Frameworks, sys.exe is /MacOS.
        #
        # THIS CASE ALSO INCLUDES ANDROID
        # [3.0 Subtlety: in Android source-code runs for shortcuts and
        # explorer opens that export the file via SAF, this works WITHOUT 
        # specializing for __file__ being set to a temp file in /data.
        # This is because Frigcal calls this func from frigcal.py which 
        # is imported from CWD by frigcal-main.py, thereby setting __file__
        # to the special CWD dir for Android.  __file__ is the temp file
        # in /data for the top-level frigcal-main.py, but CWD in the 
        # imported frigcal.py.  This differs from PyEdit, where ffp.py
        # is imported by the top-level script in /data.  Convoluted, that. 
        #
        srcpath = __file__
        srcdir  = os.path.dirname(os.path.abspath(srcpath))
        return srcdir



[Home page] Books Code Blog Python Author Train Find ©M.Lutz