pip uninstall fails on symlinks to directories #40

Closed
vbabiy opened this Issue Mar 15, 2011 · 14 comments

Comments

Projects
None yet
1 participant
Contributor

vbabiy commented Mar 15, 2011

If uninstalling a python package, which installs symbolic links to directories somewhere, pip uninstall does not remove the symbolic link itself, but instead leaves the symbolic link intact and instead removes the whole directory, the link pointed too.


Contributor

vbabiy commented Mar 15, 2011

Do you have an example package that installs symbolic links? I would like to
try and reproduce this bug?


Original Comment By: Kelsey Hightower
Contributor

vbabiy commented Mar 15, 2011

I came across this issue while developing synaptiks. It is a python
application for KDE, and it has a standard KDE handbook in docbook format. The
KDE help viewer renders these handbooks to HTML for display, and this HTML
needs some common files like CSS stylesheets and the KDE logo as image file.

These common files are symlinked into the directory of the specific handbook,
which results in the following directory structure:

 # common files for handbooks

/usr/share/doc/kde/html/en/common

# the directory for the handbook of synaptiks

/usr/share/doc/kde/html/en/synaptiks/

# symlink to these common files from a specific application handbook

/usr/share/doc/kde/html/en/synaptiks/common ->

/usr/share/doc/kde/html/en/common

I've not invented this system, and I'm in no position to change it, so I have
to install this symlink. When executing "pip uninstall synaptiks" the "common"
symlink remains, but the whole "common" directory is deleted.

My guess is, that pip applies os.path.normpath() before removing installed
files, which resolves the symlink to its destination, which seems not the
right thing to do in this situation.


Original Comment By: Sebastian Wiesner
Contributor

vbabiy commented Mar 15, 2011

Ok, it looks like pip moves the 'files to be removed' into a temp location,
and then calls os.removedirs() on the old paths. This maybe whats deleting
the directory the symlink points too. Pip preserves the original symlink, but
blows it away during the os.removedirs() step.

I am going to work up a patch and test package to see if my hypothesis
checks out.


Original Comment By: Kelsey Hightower
Contributor

vbabiy commented Mar 15, 2011

Also what version of pip are you running?


Original Comment By: Kelsey Hightower
Contributor

vbabiy commented Mar 15, 2011

0.8.2


Original Comment By: Sebastian Wiesner
Contributor

vbabiy commented Mar 15, 2011

Looks like you where pretty much right. It seems pip is calling
os.path.normcase(os.path.realpath(path)) on each path that is added to the
list of paths to be remove.

Per the docs os.path.realpath():

"Return the canonical path of the specified filename, eliminating any symbolic
links encountered in the path (if they are supported by the operating system"

Not sure how to work around this. Maybe pip needs "yet another flag" to
disable this behavior.


Original Comment By: Kelsey Hightower
Contributor

vbabiy commented Mar 15, 2011

I'm using pip 0.8.2.


Original Comment By: Sebastian Wiesner
Contributor

vbabiy commented Mar 15, 2011

I have added a --disable-followlinks flag to the uninstall command in my fork
of pip. Do you mind testing it to see if it resolves your issue?

You can clone from here:

hg clone https://khightower@bitbucket.org/khightower/pip -b pip-
uninstall-fails-on-symlinks-to-directories

Example output: I have created a symbolic link under the nose installation
directory which points to /testdir. I also added an entry to installed-
files.txt

Running pip uninstall
# /opt/OpenPython-2.7.1/bin/pip install nose

# cd /opt/OpenPython-2.7.1/lib/python2.7/site-packages/nose

# ln -s /testdir testdir

# echo "../nose/testdir" >> /opt/OpenPython-2.7.1/lib/python2.7/site-

packages/nose-1.0.0-py2.7.egg-info/installed-files.txt

# /opt/OpenPython-2.7.1/bin/pip uninstall nose

Uninstalling nose:

  /opt/OpenPython-2.7.1/bin/nosetests

  /opt/OpenPython-2.7.1/bin/nosetests-2.7

  /opt/OpenPython-2.7.1/lib/python2.7/site-packages/nose

  /opt/OpenPython-2.7.1/lib/python2.7/site-packages/nose-1.0.0-py2.7.egg-

info

  /opt/OpenPython-2.7.1/man/man1/nosetests.1

  /testdir

Proceed (y/n)?

Notice how '/testdir' shows up in the list of directories to be removed.
Answering 'y' causes /testdir to be deleted as expected.

Running pip uninstall using --disable-followlinks
# /opt/OpenPython-2.7.1/bin/pip install nose

# cd /opt/OpenPython-2.7.1/lib/python2.7/site-packages/nose

# ln -s /testdir testdir

# echo "../nose/testdir" >> /opt/OpenPython-2.7.1/lib/python2.7/site-

packages/nose-1.0.0-py2.7.egg-info/installed-files.txt

# /opt/OpenPython-2.7.1/bin/pip uninstall nose --disable-followlinks


Uninstalling nose:

  /opt/OpenPython-2.7.1/bin/nosetests

  /opt/OpenPython-2.7.1/bin/nosetests-2.7

  /opt/OpenPython-2.7.1/lib/python2.7/site-packages/nose

  /opt/OpenPython-2.7.1/lib/python2.7/site-packages/nose-1.0.0-py2.7.egg-

info

  /opt/OpenPython-2.7.1/man/man1/nosetests.1

Proceed (y/n)? y

  Successfully uninstalled nose

Notice how '/testdir' does not show up in the list of directories to be
removed. Answering 'y' causes /testdir not to be deleted.


Original Comment By: Kelsey Hightower
Contributor

vbabiy commented Mar 15, 2011

Does pip uninstall --disable-followlinks remove the symlink itself? Or does
it leave the link and the directory pointed to untouched?

And to be honest, I dislike this solution. The target users of my application
do not really care what files the program actually installs, and of what type
these files are. I can't seriously expect, that they inspect the package
contents, and give the required --disable-followlinks flag. And if they
forget it, pip uninstall will happily remove the common documentation files
and thus break the handbooks of all other KDE applications. Of course, I
could provide uninstallations instructions in the documentation, but we all
know, that users tend to ignore or misread documentation.

Imho, pip uninstall should just "work", and do the right thing depending on
the type of the installed file, without having the user to choose the "right"
behaviour with a command line flag.


Original Comment By: Sebastian Wiesner
Contributor

vbabiy commented Mar 15, 2011

The proposed solution only removes the symlink. I agree with that pip should
just do the right thing. Maybe what I am proposing can become the default,
unless there is a good reason pip other wise. Maybe one of the core devs can
way in.


Original Comment By: Kelsey Hightower
Contributor

vbabiy commented Mar 15, 2011

I agree with Sebastian that we should fix this without the need for an
additional flag. I can't think off the top of my head of any reason why the
call to realpath needs to be in there. The one case I'd like to see tested
before we make this change is the case of a virtualenv that is activated via a
symlink-containing path, and then uninstalling something from inside that
virtualenv.

Thanks Sebastian for the report, and Kelsey for your work on a patch!


Original Comment By: Carl Meyer
Contributor

vbabiy commented Mar 15, 2011

I have rerun the tests again; this time inside a virtualenv. Looks like pip
does the "right thing".

Running with pip uninstall nose
# /root/symlinktestenv/bin/pip uninstall nose

Uninstalling nose:

  /root/symlinktestenv/bin/nosetests

  /root/symlinktestenv/bin/nosetests-2.7

  /root/symlinktestenv/lib/python2.7/site-packages/nose

  /root/symlinktestenv/lib/python2.7/site-packages/nose-1.0.0-py2.7.egg-

info

  /root/symlinktestenv/man/man1/nosetests.1

Proceed (y/n)? y

  Not removing or modifying (outside of prefix):

  /testdir

  Successfully uninstalled nose

Notice the line: Not removing or modifying (outside of prefix):

Running with pip uninstall nose --disable-followlinks
Uninstalling nose:

  /root/symlinktestenv/bin/nosetests

  /root/symlinktestenv/bin/nosetests-2.7

  /root/symlinktestenv/lib/python2.7/site-packages/nose

  /root/symlinktestenv/lib/python2.7/site-packages/nose-1.0.0-py2.7.egg-

info

  /root/symlinktestenv/man/man1/nosetests.1

Proceed (y/n)? y

  Not removing or modifying (outside of prefix):

  /root/symlinktestenv/lib/python2.7/site-packages/nose/testdir

  Successfully uninstalled nose

Notice now that the full path to the symlink is expanded. Looks like when
running in a virtualenv, path are treated differently. Going to do a little
more research.


Original Comment By: Kelsey Hightower
Contributor

vbabiy commented Mar 15, 2011

Looks like I was getting tripped up on the "is_local" check pip does during
the uninstall process. My original patch did not cover the following piece of
code:

[ 1][1]

[ 2][2]

[ 3][3]

[ 4][4]

[ 5][5]

[ 6][6]

[ 7][7]

[ 8][8]

[ 9][9]

[10][10]



def is_local(path):

    """

    Return True if path is within sys.prefix, if we're running in a

virtualenv.

    If we're not in a virtualenv, all paths are considered "local."


    """

    if not running_under_virtualenv():

        return True

    return normalize_path(path).startswith(normalize_path(sys.prefix))

My new patch removes the call to os.path.realpath when normalizing a path; in
turn removing the need for the --disable-followlinks flag.

After Running the tests

Pip only removes the symlink leaving the content it points to untouched. I get
the same results whether or not I am inside a virtualenv.

Link to changeset

Changes can be found in fork under the uninstall-fails-on-symlinks-to-
directories branch:

https://bitbucket.org/khightower/pip/changeset/31571bc08c73


Original Comment By: Kelsey Hightower
Contributor

vbabiy commented Mar 15, 2011

/uninstall-fails-on-symlinks-to-directories.patch


Original Comment By: Kelsey Hightower

takluyver added a commit to takluyver/pip that referenced this issue Mar 17, 2015

@qwcode qwcode closed this in #2552 Mar 22, 2015

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment