diff --git a/src/mcedit2/util/directories.py b/src/mcedit2/util/directories.py deleted file mode 100644 index 6984b09..0000000 --- a/src/mcedit2/util/directories.py +++ /dev/null @@ -1,41 +0,0 @@ -import os -import sys - - -def getUserFilesDirectory(): - if sys.platform == "win32": - import win32api - # On Windows, sys.executable is codepage-encoded. - # It cannot represent all possible filenames, so get the exe filename - # using this wide-character API, which returns a `unicode` - exe = win32api.GetModuleFileNameW(None) - else: - # On OS X, the FS encoding is always UTF-8 - # OS X filenames are defined to be UTF-8 encoded. - # On Linux, the FS encoding is given by the current locale - # Linux filenames are defined to be bytestrings. - exe = sys.executable.decode(sys.getfilesystemencoding()) - - assert os.path.exists(exe), "%r does not exist" % exe - if hasattr(sys, 'frozen'): - folder = os.path.dirname(exe) - else: - if exe.endswith("python") or exe.endswith("python.exe"): - script = sys.argv[0] - # assert the source checkout is not in a non-representable path... - assert os.path.exists(script), "Source checkout path cannot be represented with 'mbcs' encoding. Put the source checkout somewhere else." - folder = os.path.dirname(os.path.dirname(os.path.dirname(script))) # from src/mcedit, ../../ - else: - folder = os.path.dirname(exe) - - dataDir = os.path.join(folder, "MCEdit User Data") - - if not os.path.exists(dataDir): - os.makedirs(dataDir) - return dataDir - -def getUserSchematicsDirectory(): - return os.path.join(getUserFilesDirectory(), "schematics") - -def getUserPluginsDirectory(): - return os.path.join(getUserFilesDirectory(), "plugins") diff --git a/src/mcedit2/util/directories/__init__.py b/src/mcedit2/util/directories/__init__.py new file mode 100644 index 0000000..e99ba66 --- /dev/null +++ b/src/mcedit2/util/directories/__init__.py @@ -0,0 +1,24 @@ +""" + directories + + Get platform specific user data folders. +""" +from __future__ import absolute_import, division, print_function, unicode_literals +import logging +import os +import sys +log = logging.getLogger(__name__) + +if sys.platform == "win32": + from .win32 import getUserFilesDirectory +elif sys.platform == "darwin": + from .mac import getUserFilesDirectory +else: + from .posix import getUserFilesDirectory + + +def getUserSchematicsDirectory(): + return os.path.join(getUserFilesDirectory(), "schematics") + +def getUserPluginsDirectory(): + return os.path.join(getUserFilesDirectory(), "plugins") diff --git a/src/mcedit2/util/directories/mac.py b/src/mcedit2/util/directories/mac.py new file mode 100644 index 0000000..b4c08cb --- /dev/null +++ b/src/mcedit2/util/directories/mac.py @@ -0,0 +1,28 @@ +""" + mac +""" +from __future__ import absolute_import, division, print_function, unicode_literals +import logging +import os +import sys + +log = logging.getLogger(__name__) + +# no build scripts for OS X yet. assuming py2app or py2installer will be used. +# +# store user files in source checkout folder + +def getUserFilesDirectory(): + + # TODO if sys.getattr('frozen', False): + # TODO os.getenv('RESOURCEPATH') or sys.getattr('_MEIPASS'), etc etc... + + # On OS X, the FS encoding is always UTF-8 + # OS X filenames are defined to be UTF-8 encoded. + # We internally handle filenames as unicode. + + script = sys.argv[0].decode(sys.getfilesystemencoding()) + + folder = os.path.dirname(os.path.dirname(os.path.dirname(script))) # main script is src/mcedit/main.py, so, ../../ + dataDir = os.path.join(folder, "MCEdit User Data") + return dataDir diff --git a/src/mcedit2/util/directories/posix.py b/src/mcedit2/util/directories/posix.py new file mode 100644 index 0000000..23879ea --- /dev/null +++ b/src/mcedit2/util/directories/posix.py @@ -0,0 +1,35 @@ +""" + posix +""" +from __future__ import absolute_import, division, print_function, unicode_literals +import logging +import os +import sys + +log = logging.getLogger(__name__) + +# no build scripts for linux yet. no idea if frozen apps will be built for linux. +# assume always running from source checkout and put user files in source checkout. + +# xxx for .whl or distro-specific distrib, put user files in ~/.mcedit2 + +def getUserFilesDirectory(): + # On Linux, the FS encoding is given by the current locale + # Linux filenames are defined to be bytestrings. + + # TODO: if not os.path.exists(os.path.join(mumble_mumble, ".git")): + + # We handle filenames internally as 'unicode', so decode 'sys.argv[0]' + # If a linux filename cannot be decoded with the current locale, ignore it. + # If this filename is the script filename, you lose. + try: + # assert the source checkout is not in a non-representable path... + script = sys.argv[0].decode(sys.getfilesystemencoding()) + except UnicodeDecodeError: + print("Script filename %r cannot be decoded with the current locale %s! Please use sensible filenames." % ( + sys.argv[0], sys.getfilesystemencoding())) + raise + + folder = os.path.dirname(os.path.dirname(os.path.dirname(script))) # main script is src/mcedit/main.py, so, ../../ + dataDir = os.path.join(folder, "MCEdit User Data") + return dataDir diff --git a/src/mcedit2/util/directories/win32.py b/src/mcedit2/util/directories/win32.py new file mode 100644 index 0000000..d2df56f --- /dev/null +++ b/src/mcedit2/util/directories/win32.py @@ -0,0 +1,57 @@ +import os +import sys + +# when running from source, put the user files in the source checkout +# when running from a built app ("frozen"), decide from install mode + +# TODO how to do install mode? +# 1. two different downloads +# - .exe installer for system-wide/per-user install +# - .zip for self-contained/portable install +# 2. one download. prompt on first app startup for install mode. create INSTALL_MODE file in +# exe folder. check contents of file for install mode and prompt if file is missing. +# 3. like MCEdit1. option in app to change install mode and move user folder into app or docs folder +# check for presence of user folder in app folder on startup to decide install mode. +# 4. ALWAYS self-contained/portable install. same as MultiMC. +# +# ... also, get auto updater working. manually updating a self-contained install may be weird. + +# I think 4 is the best. +from mcedit2.util import resources + +PORTABLE_INSTALL = True + +_userFilesDirectory = None +def getUserFilesDirectory(): + global _userFilesDirectory + if _userFilesDirectory is not None: + return _userFilesDirectory + + if hasattr(sys, 'frozen'): + # On Windows, filenames are UTF-16 encoded. + # Filenames are defined as UTF-16. + # However, sys.executable is codepage-encoded. Codepages cannot represent all possible + # filenames, so we must get the exe filename using this wide-character API. + # Wide-character APIs in pywin32 always return a `unicode`. + # + # Filenames must be passed to Python's filesystem functions as `unicode` to call the + # wide-character forms of the underlying Win32 APIs. + # + # We take care not to store filenames as `bytes`, as this causes the filesystem functions + # to use the legacy codepage APIs. + + import win32api + exe = win32api.GetModuleFileNameW(None) + assert os.path.exists(exe), "MCEdit executable %r does not exist! Something is very wrong." % exe + folder = os.path.dirname(exe) + + else: + folder = os.path.dirname(resources.getSrcFolder()) + + dataDir = os.path.join(folder, "MCEdit User Data") + + if not os.path.exists(dataDir): + os.makedirs(dataDir) + + _userFilesDirectory = dataDir + return dataDir diff --git a/src/mcedit2/util/resources.py b/src/mcedit2/util/resources.py index 18fd05c..4ed51f0 100644 --- a/src/mcedit2/util/resources.py +++ b/src/mcedit2/util/resources.py @@ -8,15 +8,50 @@ import sys log = logging.getLogger(__name__) +def getSrcFolder(): + """ + Find the 'src/' folder of a source checkout. + + ...should maybe assert that '.git' exists? + :return: unicode + """ + import mcedit2 + mod = os.path.realpath(mcedit2.__file__) # src/mcedit2/__init__.py + + # On Windows, sys.argv[0] is always codepage-encoded, as is the __file__ attribute of any module. + # in fact, the entire module import subsystem of python2.7 is either restricted to codepages + # or to ASCII (haven't found out which) as `import` seems to break with non-ascii paths. + + # On OS X, it is always UTF-8 encoded and filenames are *always* UTF-8 encoded. + + # On Linux, it is locale-encoded and filenames are defined as bytestrings, so it is possible + # to have a filename that cannot be interpreted as unicode. If the user writes a filename + # that is not locale-encoded, he loses. + + mod = mod.decode(sys.getfilesystemencoding()) + + # Assert the source checkout is not in a non-representable path... + assert os.path.exists(mod), ("Source checkout path cannot be represented as unicode. " + "Put the source checkout somewhere else.") + return os.path.dirname((os.path.dirname(mod))) def resourcePath(filename): + """ + Return the absolute path of a filename included as a resource. + + "Resource" is not well-defined. When packaged with PyInstaller, the filename is found + relative to the app's folder (_MEIPASS). When running from source, it is relative to the + 'src' folder. I'd imagine that when installed as a .whl (on linux) we need to use + pkg_resources to get filenames. + + :param filename: + :return: + """ filename = filename.replace('/', os.path.sep) basedir = getattr(sys, "_MEIPASS", None) # if pyinstaller'd if basedir is None: - import mcedit2 - mod = mcedit2.__file__ - basedir = os.path.dirname(mod) + "/.." - basedir = os.path.normpath(basedir) + # should work across platforms + basedir = getSrcFolder() path = os.path.join(basedir, filename) if not os.path.exists(path): raise RuntimeError("Could not get resource path for %s\n(Tried %s which does not exist)" % (filename, path))