Skip to content

Commit

Permalink
Run commands using their real paths
Browse files Browse the repository at this point in the history
Fixes pypa#1164
  • Loading branch information
dechamps committed Dec 25, 2023
1 parent 1a3e6ad commit dfe9027
Showing 1 changed file with 51 additions and 12 deletions.
63 changes: 51 additions & 12 deletions src/pipx/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,15 @@ def rmdir(path: Path, safe_rm: bool = True) -> None:
# move it to be deleted later if it still exists
if path.is_dir():
if safe_rm:
logger.warning(f"Failed to delete {path}. Will move it to a temp folder to delete later.")
logger.warning(
f"Failed to delete {path}. Will move it to a temp folder to delete later."
)

path.rename(_get_trash_file(path))
else:
logger.warning(f"Failed to delete {path}. You may need to delete it manually.")
logger.warning(
f"Failed to delete {path}. You may need to delete it manually."
)


def mkdir(path: Path) -> None:
Expand Down Expand Up @@ -171,6 +175,17 @@ def run_subprocess(
logger.info(f"running {log_cmd_str}")
# windows cannot take Path objects, only strings
cmd_str_list = [str(c) for c in cmd]
# Make sure to call the binary using its real path. This matters especially
# on Windows when using the packaged app (Microsoft Store) version of
# Python, which uses path redirection for sandboxing. If the path to the
# executable is redirected, the executable can get confused as to which
# directory it's being run from, leading to problems.
# See https://github.com/pypa/pipx/issues/1164
# Conversely, if the binary is a symlink, then we should NOT use the real
# path, as Python expects to receive the symlink in argv[0] so that it can
# locate the venv.
if not os.path.islink(cmd_str_list[0]):
cmd_str_list[0] = os.path.realpath(cmd_str_list[0])
completed_process = subprocess.run(
cmd_str_list,
env=env,
Expand All @@ -190,14 +205,18 @@ def run_subprocess(
return completed_process


def subprocess_post_check(completed_process: "subprocess.CompletedProcess[str]", raise_error: bool = True) -> None:
def subprocess_post_check(
completed_process: "subprocess.CompletedProcess[str]", raise_error: bool = True
) -> None:
if completed_process.returncode:
if completed_process.stdout is not None:
print(completed_process.stdout, file=sys.stdout, end="")
if completed_process.stderr is not None:
print(completed_process.stderr, file=sys.stderr, end="")
if raise_error:
raise PipxError(f"{' '.join([str(x) for x in completed_process.args])!r} failed")
raise PipxError(
f"{' '.join([str(x) for x in completed_process.args])!r} failed"
)
else:
logger.info(f"{' '.join(completed_process.args)!r} failed")

Expand Down Expand Up @@ -290,12 +309,16 @@ def analyze_pip_output(pip_stdout: str, pip_stderr: str) -> None:
failed_to_build_str = "\n ".join(failed_build_stdout)
plural_str = "s" if len(failed_build_stdout) > 1 else ""
print("", file=sys.stderr)
logger.error(f"pip failed to build package{plural_str}:\n {failed_to_build_str}")
logger.error(
f"pip failed to build package{plural_str}:\n {failed_to_build_str}"
)
elif failed_build_stderr:
failed_to_build_str = "\n ".join(failed_build_stderr)
plural_str = "s" if len(failed_build_stderr) > 1 else ""
print("", file=sys.stderr)
logger.error(f"pip seemed to fail to build package{plural_str}:\n {failed_to_build_str}")
logger.error(
f"pip seemed to fail to build package{plural_str}:\n {failed_to_build_str}"
)
elif last_collecting_dep is not None:
print("", file=sys.stderr)
logger.error(f"pip seemed to fail to build package:\n {last_collecting_dep}")
Expand All @@ -307,9 +330,13 @@ def analyze_pip_output(pip_stdout: str, pip_stderr: str) -> None:

print_categories = [x.category for x in relevant_searches]
relevants_saved_filtered = relevants_saved.copy()
while (len(print_categories) > 1) and (len(relevants_saved_filtered) > max_relevant_errors):
while (len(print_categories) > 1) and (
len(relevants_saved_filtered) > max_relevant_errors
):
print_categories.pop(-1)
relevants_saved_filtered = [x for x in relevants_saved if x[1] in print_categories]
relevants_saved_filtered = [
x for x in relevants_saved if x[1] in print_categories
]

for relevant_saved in relevants_saved_filtered:
print(f" {relevant_saved[0]}", file=sys.stderr)
Expand All @@ -323,7 +350,9 @@ def subprocess_post_check_handle_pip_error(
# Save STDOUT and STDERR to file in pipx/logs/
if pipx.constants.pipx_log_file is None:
raise PipxError("Pipx internal error: No log_file present.")
pip_error_file = pipx.constants.pipx_log_file.parent / (pipx.constants.pipx_log_file.stem + "_pip_errors.log")
pip_error_file = pipx.constants.pipx_log_file.parent / (
pipx.constants.pipx_log_file.stem + "_pip_errors.log"
)
with pip_error_file.open("w", encoding="utf-8") as pip_error_fh:
print("PIP STDOUT", file=pip_error_fh)
print("----------", file=pip_error_fh)
Expand All @@ -334,7 +363,10 @@ def subprocess_post_check_handle_pip_error(
if completed_process.stderr is not None:
print(completed_process.stderr, file=pip_error_fh, end="")

logger.error("Fatal error from pip prevented installation. Full pip output in file:\n" f" {pip_error_file}")
logger.error(
"Fatal error from pip prevented installation. Full pip output in file:\n"
f" {pip_error_file}"
)

analyze_pip_output(completed_process.stdout, completed_process.stderr)

Expand All @@ -356,7 +388,12 @@ def exec_app(

if extra_python_paths is not None:
env["PYTHONPATH"] = os.path.pathsep.join(
extra_python_paths + (os.getenv("PYTHONPATH", "").split(os.path.pathsep) if os.getenv("PYTHONPATH") else [])
extra_python_paths
+ (
os.getenv("PYTHONPATH", "").split(os.path.pathsep)
if os.getenv("PYTHONPATH")
else []
)
)

# make sure we show cursor again before handing over control
Expand Down Expand Up @@ -387,7 +424,9 @@ def full_package_description(package_name: str, package_spec: str) -> str:
return f"{package_name} from spec {package_spec!r}"


def pipx_wrap(text: str, subsequent_indent: str = "", keep_newlines: bool = False) -> str:
def pipx_wrap(
text: str, subsequent_indent: str = "", keep_newlines: bool = False
) -> str:
"""Dedent, strip, wrap to shell width. Don't break on hyphens, only spaces"""
minimum_width = 40
width = max(shutil.get_terminal_size((80, 40)).columns, minimum_width) - 2
Expand Down

0 comments on commit dfe9027

Please sign in to comment.