#!/usr/bin/env python """Makes a python installation relocatable""" # Copied from https://github.com/pypa/virtualenv/blob/legacy/virtualenv.py # Newer virtualenv versions don't provide this functionality anymore import os import sys import logging from os.path import join logging.basicConfig(stream=sys.stdout, level=logging.INFO) logger = logging.getLogger() VERSION = "{}.{}".format(*sys.version_info) PY_VERSION = "python{}.{}".format(*sys.version_info) IS_PYPY = hasattr(sys, "pypy_version_info") IS_WIN = sys.platform == "win32" IS_CYGWIN = sys.platform == "cygwin" IS_DARWIN = sys.platform == "darwin" ABI_FLAGS = getattr(sys, "abiflags", "") # Relocating the environment: def make_environment_relocatable(home_dir): """ Makes the already-existing environment use relative paths, and takes out the #!-based environment selection in scripts. """ home_dir, lib_dir, inc_dir, bin_dir = path_locations(home_dir) activate_this = os.path.join(bin_dir, "activate_this.py") if not os.path.exists(activate_this): logger.fatal( "The environment doesn't have a file %s -- please re-run virtualenv " "on this environment to update it", activate_this, ) fixup_scripts(home_dir, bin_dir) fixup_pth_and_egg_link(home_dir) # FIXME: need to fix up distutils.cfg OK_ABS_SCRIPTS = [ "python", PY_VERSION, "activate", "activate.bat", "activate_this.py", "activate.fish", "activate.csh", "activate.xsh", ] def mkdir(at_path): if not os.path.exists(at_path): logger.info("Creating %s", at_path) os.makedirs(at_path) else: logger.info("Directory %s already exists", at_path) def fixup_scripts(_, bin_dir): if IS_WIN: new_shebang_args = ("{} /c".format(os.path.normcase(os.environ.get("COMSPEC", "cmd.exe"))), "", ".exe") else: new_shebang_args = ("/usr/bin/env", VERSION, "") # This is what we expect at the top of scripts: shebang = "#!{}".format( os.path.normcase(os.path.join(os.path.abspath(bin_dir), "python{}".format(new_shebang_args[2]))) ) # This is what we'll put: new_shebang = "#!{} python{}{}".format(*new_shebang_args) for filename in os.listdir(bin_dir): filename = os.path.join(bin_dir, filename) if not os.path.isfile(filename): # ignore child directories, e.g. .svn ones. continue with open(filename, "rb") as f: try: lines = f.read().decode("utf-8").splitlines() except UnicodeDecodeError: # This is probably a binary program instead # of a script, so just ignore it. continue if not lines: logger.warning("Script %s is an empty file", filename) continue old_shebang = lines[0].strip() old_shebang = old_shebang[0:2] + os.path.normcase(old_shebang[2:]) if not old_shebang.startswith(shebang): if os.path.basename(filename) in OK_ABS_SCRIPTS: logger.debug("Cannot make script %s relative", filename) elif lines[0].strip() == new_shebang: logger.info("Script %s has already been made relative", filename) else: logger.warning( "Script %s cannot be made relative (it's not a normal script that starts with %s)", filename, shebang, ) continue logger.info("Making script %s relative", filename) #logger.notify("Making script %s relative", filename) script = relative_script([new_shebang] + lines[1:]) with open(filename, "wb") as f: f.write("\n".join(script).encode("utf-8")) def relative_script(lines): """Return a script that'll work in a relocatable environment.""" activate = ( "import os; " "activate_this=os.path.join(os.path.dirname(os.path.realpath(__file__)), 'activate_this.py'); " "exec(compile(open(activate_this).read(), activate_this, 'exec'), { '__file__': activate_this}); " "del os, activate_this" ) # Find the last future statement in the script. If we insert the activation # line before a future statement, Python will raise a SyntaxError. activate_at = None for idx, line in reversed(list(enumerate(lines))): if line.split()[:3] == ["from", "__future__", "import"]: activate_at = idx + 1 break if activate_at is None: # Activate after the shebang. activate_at = 1 return lines[:activate_at] + ["", activate, ""] + lines[activate_at:] def fixup_pth_and_egg_link(home_dir, sys_path=None): """Makes .pth and .egg-link files use relative paths""" home_dir = os.path.normcase(os.path.abspath(home_dir)) if sys_path is None: sys_path = sys.path for a_path in sys_path: if not a_path: a_path = "." if not os.path.isdir(a_path): continue a_path = os.path.normcase(os.path.abspath(a_path)) if not a_path.startswith(home_dir): logger.debug("Skipping system (non-environment) directory %s", a_path) continue for filename in os.listdir(a_path): filename = os.path.join(a_path, filename) if filename.endswith(".pth"): if not os.access(filename, os.W_OK): logger.warning("Cannot write .pth file %s, skipping", filename) else: fixup_pth_file(filename) if filename.endswith(".egg-link"): if not os.access(filename, os.W_OK): logger.warning("Cannot write .egg-link file %s, skipping", filename) else: fixup_egg_link(filename) def fixup_pth_file(filename): lines = [] with open(filename) as f: prev_lines = f.readlines() for line in prev_lines: line = line.strip() if not line or line.startswith("#") or line.startswith("import ") or os.path.abspath(line) != line: lines.append(line) else: new_value = make_relative_path(filename, line) if line != new_value: logger.debug("Rewriting path {} as {} (in {})".format(line, new_value, filename)) lines.append(new_value) if lines == prev_lines: logger.info("No changes to .pth file %s", filename) return logger.notify("Making paths in .pth file %s relative", filename) with open(filename, "w") as f: f.write("\n".join(lines) + "\n") def fixup_egg_link(filename): with open(filename) as f: link = f.readline().strip() if os.path.abspath(link) != link: logger.debug("Link in %s already relative", filename) return new_link = make_relative_path(filename, link) logger.notify("Rewriting link {} in {} as {}".format(link, filename, new_link)) with open(filename, "w") as f: f.write(new_link) def make_relative_path(source, dest, dest_is_directory=True): """ Make a filename relative, where the filename is dest, and it is being referred to from the filename source. >>> make_relative_path('/usr/share/something/a-file.pth', ... '/usr/share/another-place/src/Directory') '../another-place/src/Directory' >>> make_relative_path('/usr/share/something/a-file.pth', ... '/home/user/src/Directory') '../../../home/user/src/Directory' >>> make_relative_path('/usr/share/a-file.pth', '/usr/share/') './' """ source = os.path.dirname(source) if not dest_is_directory: dest_filename = os.path.basename(dest) dest = os.path.dirname(dest) else: dest_filename = None dest = os.path.normpath(os.path.abspath(dest)) source = os.path.normpath(os.path.abspath(source)) dest_parts = dest.strip(os.path.sep).split(os.path.sep) source_parts = source.strip(os.path.sep).split(os.path.sep) while dest_parts and source_parts and dest_parts[0] == source_parts[0]: dest_parts.pop(0) source_parts.pop(0) full_parts = [".."] * len(source_parts) + dest_parts if not dest_is_directory and dest_filename is not None: full_parts.append(dest_filename) if not full_parts: # Special case for the current directory (otherwise it'd be '') return "./" return os.path.sep.join(full_parts) def path_locations(home_dir, dry_run=False): """Return the path locations for the environment (where libraries are, where scripts go, etc)""" home_dir = os.path.abspath(home_dir) lib_dir, inc_dir, bin_dir = None, None, None # XXX: We'd use distutils.sysconfig.get_python_inc/lib but its # prefix arg is broken: http://bugs.python.org/issue3386 if IS_WIN: # Windows has lots of problems with executables with spaces in # the name; this function will remove them (using the ~1 # format): if not dry_run: mkdir(home_dir) if " " in home_dir: import ctypes get_short_path_name = ctypes.windll.kernel32.GetShortPathNameW size = max(len(home_dir) + 1, 256) buf = ctypes.create_unicode_buffer(size) try: # noinspection PyUnresolvedReferences u = unicode except NameError: u = str ret = get_short_path_name(u(home_dir), buf, size) if not ret: print('Error: the path "{}" has a space in it'.format(home_dir)) print("We could not determine the short pathname for it.") print("Exiting.") sys.exit(3) home_dir = str(buf.value) lib_dir = join(home_dir, "Lib") inc_dir = join(home_dir, "Include") bin_dir = join(home_dir, "Scripts") if IS_PYPY: lib_dir = home_dir inc_dir = join(home_dir, "include") bin_dir = join(home_dir, "bin") elif not IS_WIN: lib_dir = join(home_dir, "lib", PY_VERSION) inc_dir = join(home_dir, "include", PY_VERSION + ABI_FLAGS) bin_dir = join(home_dir, "bin") return home_dir, lib_dir, inc_dir, bin_dir def main(): args = sys.argv[1:] if not args: print("You must provide a DEST_DIR") sys.exit(2) if len(args) > 1: print("There must be only one argument: DEST_DIR (you gave {})".format(" ".join(args))) sys.exit(2) home_dir = args[0] if os.path.exists(home_dir) and os.path.isfile(home_dir): logger.fatal("ERROR: File already exists and is not a directory.") logger.fatal("Please provide a different path or delete the file.") sys.exit(3) if os.pathsep in home_dir: logger.fatal("ERROR: target path contains the operating system path separator '{}'".format(os.pathsep)) logger.fatal("This is not allowed as would make the activation scripts unusable.".format(os.pathsep)) sys.exit(3) if os.environ.get("WORKING_ENV"): logger.fatal("ERROR: you cannot run virtualenv while in a working env") logger.fatal("Please deactivate your working env, then re-run this script") sys.exit(3) make_environment_relocatable(home_dir) if __name__ == "__main__": main()