Skip to content

Commit 08ca8fa

Browse files
committed
Merge pull request #865 from qwcode/no_reuse_build
deal with pre-existing build dirs
2 parents acc9172 + dc70d49 commit 08ca8fa

File tree

8 files changed

+142
-35
lines changed

8 files changed

+142
-35
lines changed

pip/commands/install.py

+31-22
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,12 @@ def __init__(self, *args, **kw):
144144
default=False,
145145
help="Include pre-release and development versions. By default, pip only finds stable versions.")
146146

147+
cmd_opts.add_option(
148+
'--no-clean',
149+
action='store_true',
150+
default=False,
151+
help="Don't delete build directories after installs or errors.")
152+
147153
index_opts = cmdoptions.make_option_group(cmdoptions.index_group, self.parser)
148154

149155
self.parser.insert_option_group(0, index_opts)
@@ -230,28 +236,31 @@ def run(self, options, args):
230236

231237
raise InstallationError('--user --editable not supported with setuptools, use distribute')
232238

233-
if not options.no_download:
234-
requirement_set.prepare_files(finder, force_root_egg_info=self.bundle, bundle=self.bundle)
235-
else:
236-
requirement_set.locate_files()
237-
238-
if not options.no_install and not self.bundle:
239-
requirement_set.install(install_options, global_options, root=options.root_path)
240-
installed = ' '.join([req.name for req in
241-
requirement_set.successfully_installed])
242-
if installed:
243-
logger.notify('Successfully installed %s' % installed)
244-
elif not self.bundle:
245-
downloaded = ' '.join([req.name for req in
246-
requirement_set.successfully_downloaded])
247-
if downloaded:
248-
logger.notify('Successfully downloaded %s' % downloaded)
249-
elif self.bundle:
250-
requirement_set.create_bundle(self.bundle_filename)
251-
logger.notify('Created bundle in %s' % self.bundle_filename)
252-
# Clean up
253-
if not options.no_install or options.download_dir:
254-
requirement_set.cleanup_files(bundle=self.bundle)
239+
try:
240+
if not options.no_download:
241+
requirement_set.prepare_files(finder, force_root_egg_info=self.bundle, bundle=self.bundle)
242+
else:
243+
requirement_set.locate_files()
244+
245+
if not options.no_install and not self.bundle:
246+
requirement_set.install(install_options, global_options, root=options.root_path)
247+
installed = ' '.join([req.name for req in
248+
requirement_set.successfully_installed])
249+
if installed:
250+
logger.notify('Successfully installed %s' % installed)
251+
elif not self.bundle:
252+
downloaded = ' '.join([req.name for req in
253+
requirement_set.successfully_downloaded])
254+
if downloaded:
255+
logger.notify('Successfully downloaded %s' % downloaded)
256+
elif self.bundle:
257+
requirement_set.create_bundle(self.bundle_filename)
258+
logger.notify('Created bundle in %s' % self.bundle_filename)
259+
finally:
260+
# Clean up
261+
if (not options.no_clean) and ((not options.no_install) or options.download_dir):
262+
requirement_set.cleanup_files(bundle=self.bundle)
263+
255264
if options.target_dir:
256265
if not os.path.exists(options.target_dir):
257266
os.makedirs(options.target_dir)

pip/exceptions.py

+5
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,8 @@ class BadCommand(PipError):
2929

3030
class CommandError(PipError):
3131
"""Raised when there is an error in command-line arguments"""
32+
33+
34+
class PreviousBuildDirError(PipError):
35+
"""Raised when there's a previous conflicting build directory"""
36+

pip/req.py

+17-4
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@
66
import sys
77
import shutil
88
import tempfile
9+
import textwrap
910
import zipfile
1011

1112
from distutils.util import change_root
1213
from pip.locations import bin_py, running_under_virtualenv
1314
from pip.exceptions import (InstallationError, UninstallationError,
1415
BestVersionAlreadyInstalled,
15-
DistributionNotFound, CommandError)
16+
DistributionNotFound, PreviousBuildDirError)
1617
from pip.vcs import vcs
1718
from pip.log import logger
1819
from pip.util import (display_path, rmtree, ask, ask_path_exists, backup_dir,
@@ -1045,11 +1046,23 @@ def prepare_files(self, finder, force_root_egg_info=False, bundle=False):
10451046

10461047
# NB: This call can result in the creation of a temporary build directory
10471048
location = req_to_install.build_location(self.build_dir, not self.is_download)
1048-
1049-
## FIXME: is the existance of the checkout good enough to use it? I don't think so.
10501049
unpack = True
10511050
url = None
1052-
if not os.path.exists(os.path.join(location, 'setup.py')):
1051+
1052+
# If a checkout exists, it's unwise to keep going.
1053+
# Version inconsistencies are logged later, but do not fail the installation.
1054+
if os.path.exists(os.path.join(location, 'setup.py')):
1055+
msg = textwrap.dedent("""
1056+
pip can't install requirement '%s' due to a pre-existing build directory.
1057+
location: %s
1058+
This is likely due to a previous installation that failed.
1059+
pip is being responsible and not assuming it can delete this.
1060+
Please delete it and try again.
1061+
""" % (req_to_install, location))
1062+
e = PreviousBuildDirError(msg)
1063+
logger.fatal(e)
1064+
raise e
1065+
else:
10531066
## FIXME: this won't upgrade when there's an existing package unpacked in `location`
10541067
if req_to_install.url is None:
10551068
if not_found:

tests/packages/README.txt

+4
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ install). If any earlier step would fail (i.e. egg-info-generation), the
1313
already-installed version would never be uninstalled, so uninstall-rollback
1414
would not come into play.
1515

16+
brokenegginfo-0.1.tar.gz
17+
------------------------
18+
crafted to fail on egg_info
19+
1620
BrokenEmitsUTF8
1721
---------------
1822
for generating unicode error in py3.x
701 Bytes
Binary file not shown.

tests/test_cleanup.py

+38-7
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,33 @@
11
import os
22
import textwrap
33
from os.path import abspath, exists, join
4-
from tests.test_pip import (here, reset_env, run_pip, write_file, mkdir)
4+
from tests.test_pip import (here, reset_env, run_pip,
5+
write_file, mkdir, path_to_url)
56
from tests.local_repos import local_checkout
67
from tests.path import Path
78

9+
find_links = path_to_url(os.path.join(here, 'packages'))
810

9-
def test_cleanup_after_install_from_pypi():
11+
def test_cleanup_after_install():
1012
"""
11-
Test clean up after installing a package from PyPI.
12-
13+
Test clean up after installing a package.
1314
"""
1415
env = reset_env()
15-
run_pip('install', 'INITools==0.2', expect_error=True)
16-
build = env.scratch_path/"build"
17-
src = env.scratch_path/"src"
16+
run_pip('install', '--no-index', '--find-links=%s' % find_links, 'simple')
17+
build = env.venv_path/"build"
18+
src = env.venv_path/"src"
1819
assert not exists(build), "build/ dir still exists: %s" % build
1920
assert not exists(src), "unexpected src/ dir exists: %s" % src
2021
env.assert_no_temp()
2122

23+
def test_no_clean_option_blocks_cleaning():
24+
"""
25+
Test --no-clean option blocks cleaning.
26+
"""
27+
env = reset_env()
28+
result = run_pip('install', '--no-clean', '--no-index', '--find-links=%s' % find_links, 'simple')
29+
build = env.venv_path/'build'/'simple'
30+
assert exists(build), "build/simple should still exist %s" % str(result)
2231

2332
def test_cleanup_after_install_editable_from_hg():
2433
"""
@@ -135,3 +144,25 @@ def test_download_should_not_delete_existing_build_dir():
135144
assert os.path.exists(env.venv_path/'build'), "build/ should be left if it exists before pip run"
136145
assert content == 'I am not empty!', "it should not affect build/ and its content"
137146
assert ['somefile.txt'] == os.listdir(env.venv_path/'build')
147+
148+
def test_cleanup_after_install_exception():
149+
"""
150+
Test clean up after a 'setup.py install' exception.
151+
"""
152+
env = reset_env()
153+
#broken==0.2broken fails during install; see packages readme file
154+
result = run_pip('install', '-f', find_links, '--no-index', 'broken==0.2broken', expect_error=True)
155+
build = env.venv_path/'build'
156+
assert not exists(build), "build/ dir still exists: %s" % result.stdout
157+
env.assert_no_temp()
158+
159+
def test_cleanup_after_egg_info_exception():
160+
"""
161+
Test clean up after a 'setup.py egg_info' exception.
162+
"""
163+
env = reset_env()
164+
#brokenegginfo fails during egg_info; see packages readme file
165+
result = run_pip('install', '-f', find_links, '--no-index', 'brokenegginfo==0.1', expect_error=True)
166+
build = env.venv_path/'build'
167+
assert not exists(build), "build/ dir still exists: %s" % result.stdout
168+
env.assert_no_temp()

tests/test_req.py

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import os
2+
import tempfile
3+
import shutil
4+
5+
from pip.exceptions import PreviousBuildDirError
6+
from pip.index import PackageFinder
7+
from pip.req import InstallRequirement, RequirementSet
8+
from tests.test_pip import here, path_to_url, assert_raises_regexp
9+
10+
find_links = path_to_url(os.path.join(here, 'packages'))
11+
12+
class TestRequirementSet(object):
13+
"""RequirementSet tests"""
14+
15+
def setup(self):
16+
self.tempdir = tempfile.mkdtemp()
17+
18+
def teardown(self):
19+
shutil.rmtree(self.tempdir, ignore_errors=True)
20+
21+
def basic_reqset(self):
22+
return RequirementSet(
23+
build_dir=os.path.join(self.tempdir, 'build'),
24+
src_dir=os.path.join(self.tempdir, 'src'),
25+
download_dir=None,
26+
download_cache=os.path.join(self.tempdir, 'download_cache'),
27+
)
28+
29+
def test_no_reuse_existing_build_dir(self):
30+
"""Test prepare_files raise exception with previous build dir"""
31+
32+
build_dir = os.path.join(self.tempdir, 'build', 'simple')
33+
os.makedirs(build_dir)
34+
open(os.path.join(build_dir, "setup.py"), 'w')
35+
reqset = self.basic_reqset()
36+
req = InstallRequirement.from_line('simple')
37+
reqset.add_requirement(req)
38+
finder = PackageFinder([find_links], [])
39+
assert_raises_regexp(
40+
PreviousBuildDirError,
41+
"pip can't install[\s\S]*%s[\s\S]*%s" % (req, build_dir),
42+
reqset.prepare_files,
43+
finder
44+
)
45+

tests/test_user_site.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -206,8 +206,8 @@ def test_install_user_in_global_virtualenv_with_conflict_fails(self):
206206
result2 = run_pip('install', '--user', 'INITools==0.1', expect_error=True)
207207
resultp = env.run('python', '-c', "import pkg_resources; print(pkg_resources.get_distribution('initools').location)")
208208
dist_location = resultp.stdout.strip()
209-
assert result2.stdout.startswith("Will not install to the user site because it will lack sys.path precedence to %s in %s"
210-
%('INITools', dist_location)), result2.stdout
209+
assert "Will not install to the user site because it will lack sys.path precedence to %s in %s" \
210+
% ('INITools', dist_location) in result2.stdout, result2.stdout
211211

212212

213213
def test_uninstall_from_usersite(self):

0 commit comments

Comments
 (0)