Remove the bundle functionality from pip

This commit is contained in:
Donald Stufft 2014-05-08 12:12:34 -04:00
parent 228da66ccc
commit f79ca70c66
16 changed files with 25 additions and 469 deletions

View File

@ -3,6 +3,9 @@
* **BACKWARD INCOMPATIBLE** Dropped support for Python 3.1.
* **BACKWARD INCOMPATIBLE** Removed the bundle support which was deprecated in
1.4. (:pull:`1806`)
* Removed the deprecated support for dependency links and the
``--process-dependency-links`` flag that turned them on. For alternatives to
dependency links please see http://www.pip-installer.org/en/latest/dependency_links.html

View File

@ -3,7 +3,6 @@ Package containing all pip commands
"""
from pip.commands.bundle import BundleCommand
from pip.commands.completion import CompletionCommand
from pip.commands.freeze import FreezeCommand
from pip.commands.help import HelpCommand
@ -18,7 +17,6 @@ from pip.commands.wheel import WheelCommand
commands = {
BundleCommand.name: BundleCommand,
CompletionCommand.name: CompletionCommand,
FreezeCommand.name: FreezeCommand,
HelpCommand.name: HelpCommand,
@ -43,7 +41,6 @@ commands_order = [
WheelCommand,
ZipCommand,
UnzipCommand,
BundleCommand,
HelpCommand,
]

View File

@ -1,50 +0,0 @@
from pip.locations import build_prefix, src_prefix
from pip.util import display_path, backup_dir
from pip.log import logger
from pip.exceptions import InstallationError
from pip.commands.install import InstallCommand
class BundleCommand(InstallCommand):
"""Create pybundles (archives containing multiple packages)."""
name = 'bundle'
usage = """
%prog [options] <bundle name>.pybundle <package>..."""
summary = 'DEPRECATED. Create pybundles.'
bundle = True
def __init__(self, *args, **kw):
super(BundleCommand, self).__init__(*args, **kw)
# bundle uses different default source and build dirs
build_opt = self.parser.get_option("--build")
build_opt.default = backup_dir(build_prefix, '-bundle')
src_opt = self.parser.get_option("--src")
src_opt.default = backup_dir(src_prefix, '-bundle')
self.parser.set_defaults(**{
src_opt.dest: src_opt.default,
build_opt.dest: build_opt.default,
})
def run(self, options, args):
logger.deprecated(
'1.6',
"DEPRECATION: 'pip bundle' and support for installing from "
"*.pybundle files is deprecated. "
"See https://github.com/pypa/pip/pull/1046"
)
if not args:
raise InstallationError('You must give a bundle filename')
# We have to get everything when creating a bundle:
options.ignore_installed = True
logger.notify(
'Putting temporary build files in %s and source/develop files in '
'%s' % (
display_path(options.build_dir),
display_path(options.src_dir)
)
)
self.bundle_filename = args.pop(0)
requirement_set = super(BundleCommand, self).run(options, args)
return requirement_set

View File

@ -36,7 +36,6 @@ class InstallCommand(Command):
%prog [options] <archive url/path> ..."""
summary = 'Install packages.'
bundle = False
def __init__(self, *args, **kw):
super(InstallCommand, self).__init__(*args, **kw)
@ -313,15 +312,11 @@ class InstallCommand(Command):
try:
if not options.no_download:
requirement_set.prepare_files(
finder,
force_root_egg_info=self.bundle,
bundle=self.bundle,
)
requirement_set.prepare_files(finder)
else:
requirement_set.locate_files()
if not options.no_install and not self.bundle:
if not options.no_install:
requirement_set.install(
install_options,
global_options,
@ -331,15 +326,12 @@ class InstallCommand(Command):
requirement_set.successfully_installed])
if installed:
logger.notify('Successfully installed %s' % installed)
elif not self.bundle:
else:
downloaded = ' '.join([
req.name for req in requirement_set.successfully_downloaded
])
if downloaded:
logger.notify('Successfully downloaded %s' % downloaded)
elif self.bundle:
requirement_set.create_bundle(self.bundle_filename)
logger.notify('Created bundle in %s' % self.bundle_filename)
except PreviousBuildDirError:
options.no_clean = True
raise
@ -347,7 +339,7 @@ class InstallCommand(Command):
# Clean up
if ((not options.no_clean)
and ((not options.no_install) or options.download_dir)):
requirement_set.cleanup_files(bundle=self.bundle)
requirement_set.cleanup_files()
if options.target_dir:
if not os.path.exists(options.target_dir):

View File

@ -304,7 +304,7 @@ def path_to_url(path):
def is_archive_file(name):
"""Return True if `name` is a considered as an archive file."""
archives = (
'.zip', '.tar.gz', '.tar.bz2', '.tgz', '.tar', '.pybundle', '.whl'
'.zip', '.tar.gz', '.tar.bz2', '.tgz', '.tar', '.whl'
)
ext = splitext(name)[1].lower()
if ext in archives:

View File

@ -36,7 +36,7 @@ class InstallRequirement(object):
def __init__(self, req, comes_from, source_dir=None, editable=False,
url=None, as_egg=False, update=True, prereleases=None,
editable_options=None, from_bundle=False, pycompile=True):
editable_options=None, pycompile=True):
self.extras = ()
if isinstance(req, string_types):
req = pkg_resources.Requirement.parse(req)
@ -60,7 +60,6 @@ class InstallRequirement(object):
# conflicts with another installed distribution:
self.conflicts_with = None
self._temp_build_dir = None
self._is_bundle = None
# True if the editable should be updated:
self.update = update
# Set to True after successful installation
@ -69,7 +68,6 @@ class InstallRequirement(object):
self.uninstalled = None
self.use_user_site = False
self.target_dir = None
self.from_bundle = from_bundle
self.pycompile = pycompile
@ -276,7 +274,7 @@ class InstallRequirement(object):
return setup_py
def run_egg_info(self, force_root_egg_info=False):
def run_egg_info(self):
assert self.source_dir
if self.name:
logger.notify(
@ -310,7 +308,7 @@ class InstallRequirement(object):
# We can't put the .egg-info files at the root, because then the
# source code will be mistaken for an installed egg, causing
# problems
if self.editable or force_root_egg_info:
if self.editable:
egg_base_option = []
else:
egg_info_dir = os.path.join(self.source_dir, 'pip-egg-info')
@ -816,7 +814,7 @@ exec(compile(
def remove_temporary_source(self):
"""Remove the source files from this requirement, if they are marked
for deletion"""
if self.is_bundle or os.path.exists(self.delete_marker_filename):
if os.path.exists(self.delete_marker_filename):
logger.info('Removing source in %s' % self.source_dir)
if self.source_dir:
rmtree(self.source_dir)
@ -912,82 +910,6 @@ exec(compile(
def is_wheel(self):
return self.url and '.whl' in self.url
@property
def is_bundle(self):
if self._is_bundle is not None:
return self._is_bundle
base = self._temp_build_dir
if not base:
# FIXME: this doesn't seem right:
return False
self._is_bundle = (
os.path.exists(os.path.join(base, 'pip-manifest.txt'))
or os.path.exists(os.path.join(base, 'pyinstall-manifest.txt'))
)
return self._is_bundle
def bundle_requirements(self):
for dest_dir in self._bundle_editable_dirs:
package = os.path.basename(dest_dir)
# FIXME: svnism:
for vcs_backend in vcs.backends:
url = rev = None
vcs_bundle_file = os.path.join(
dest_dir, vcs_backend.bundle_file)
if os.path.exists(vcs_bundle_file):
vc_type = vcs_backend.name
fp = open(vcs_bundle_file)
content = fp.read()
fp.close()
url, rev = vcs_backend().parse_vcs_bundle_file(content)
break
if url:
url = '%s+%s@%s' % (vc_type, url, rev)
else:
url = None
yield InstallRequirement(
package, self, editable=True, url=url,
update=False, source_dir=dest_dir, from_bundle=True)
for dest_dir in self._bundle_build_dirs:
package = os.path.basename(dest_dir)
yield InstallRequirement(
package,
self,
source_dir=dest_dir,
from_bundle=True,
)
def move_bundle_files(self, dest_build_dir, dest_src_dir):
base = self._temp_build_dir
assert base
src_dir = os.path.join(base, 'src')
build_dir = os.path.join(base, 'build')
bundle_build_dirs = []
bundle_editable_dirs = []
for source_dir, dest_dir, dir_collection in [
(src_dir, dest_src_dir, bundle_editable_dirs),
(build_dir, dest_build_dir, bundle_build_dirs)]:
if os.path.exists(source_dir):
for dirname in os.listdir(source_dir):
dest = os.path.join(dest_dir, dirname)
dir_collection.append(dest)
if os.path.exists(dest):
logger.warn(
'The directory %s (containing package %s) already '
'exists; cannot move source from bundle %s' %
(dest, dirname, self)
)
continue
if not os.path.exists(dest_dir):
logger.info('Creating directory %s' % dest_dir)
os.makedirs(dest_dir)
shutil.move(os.path.join(source_dir, dirname), dest)
if not os.listdir(source_dir):
os.rmdir(source_dir)
self._temp_build_dir = None
self._bundle_build_dirs = bundle_build_dirs
self._bundle_editable_dirs = bundle_editable_dirs
def move_wheel_files(self, wheeldir, root=None):
move_wheel_files(
self.name, self.req, wheeldir,

View File

@ -1,6 +1,5 @@
import os
import shutil
import zipfile
from pip._vendor import pkg_resources
from pip.backwardcompat import HTTPError
@ -206,7 +205,7 @@ class RequirementSet(object):
(req_to_install, req_to_install.source_dir)
)
def prepare_files(self, finder, force_root_egg_info=False, bundle=False):
def prepare_files(self, finder):
"""
Prepare process. Create temp directories, download and/or unpack files.
"""
@ -280,7 +279,6 @@ class RequirementSet(object):
# ################################ #
try:
is_bundle = False
is_wheel = False
if req_to_install.editable:
if req_to_install.source_dir is None:
@ -311,14 +309,10 @@ class RequirementSet(object):
unpack = True
url = None
# In the case where the req comes from a bundle, we should
# assume a build dir exists and move on
if req_to_install.from_bundle:
pass
# If a checkout exists, it's unwise to keep going. version
# inconsistencies are logged later, but do not fail the
# installation.
elif os.path.exists(os.path.join(location, 'setup.py')):
if os.path.exists(os.path.join(location, 'setup.py')):
raise PreviousBuildDirError(
"pip can't proceed with requirements '%s' due to a"
" pre-existing build directory (%s). This is "
@ -372,17 +366,8 @@ class RequirementSet(object):
else:
unpack = False
if unpack:
is_bundle = req_to_install.is_bundle
is_wheel = url and url.filename.endswith(wheel_ext)
if is_bundle:
req_to_install.move_bundle_files(
self.build_dir,
self.src_dir,
)
for subreq in req_to_install.bundle_requirements():
reqs.append(subreq)
self.add_requirement(subreq)
elif self.is_download:
if self.is_download:
req_to_install.source_dir = location
if not is_wheel:
# FIXME:https://github.com/pypa/pip/issues/1112
@ -395,19 +380,7 @@ class RequirementSet(object):
else:
req_to_install.source_dir = location
req_to_install.run_egg_info()
if force_root_egg_info:
# We need to run this to make sure that the
# .egg-info/ directory is created for packing
# in the bundle
req_to_install.run_egg_info(
force_root_egg_info=True,
)
req_to_install.assert_source_matches_version()
# @@ sketchy way of identifying packages not
# grabbed from an index
if bundle and req_to_install.url:
self.copy_to_build_dir(req_to_install)
install = False
# req_to_install.req is only avail after unpack for URL
# pkgs repeat check_if_exists to uninstall-on-upgrade
# (#14)
@ -454,7 +427,7 @@ class RequirementSet(object):
self.add_requirement(subreq)
# sdists
elif not is_bundle:
else:
if (req_to_install.extras):
logger.notify(
"Installing extra requirements: %r" %
@ -486,24 +459,17 @@ class RequirementSet(object):
self.add_requirement(req_to_install)
# cleanup tmp src
if not is_bundle:
if (
self.is_download or
req_to_install._temp_build_dir is not None
):
self.reqs_to_cleanup.append(req_to_install)
if (self.is_download or
req_to_install._temp_build_dir is not None):
self.reqs_to_cleanup.append(req_to_install)
if install:
self.successfully_downloaded.append(req_to_install)
if (bundle
and (
req_to_install.url
and req_to_install.url.startswith('file:///')
)):
self.copy_to_build_dir(req_to_install)
finally:
logger.indent -= 2
def cleanup_files(self, bundle=False):
def cleanup_files(self):
"""Clean up files, remove builds."""
logger.notify('Cleaning up...')
logger.indent += 2
@ -514,11 +480,6 @@ class RequirementSet(object):
if self._pip_has_created_build_dir():
remove_dir.append(self.build_dir)
# The source dir of a bundle can always be removed.
# FIXME: not if it pre-existed the bundle!
if bundle:
remove_dir.append(self.src_dir)
for dir in remove_dir:
if os.path.exists(dir):
logger.info('Removing temporary dir %s...' % dir)
@ -658,83 +619,3 @@ class RequirementSet(object):
finally:
logger.indent -= 2
self.successfully_installed = to_install
def create_bundle(self, bundle_filename):
# FIXME: can't decide which is better; zip is easier to read
# random files from, but tar.bz2 is smaller and not as lame a
# format.
# FIXME: this file should really include a manifest of the
# packages, maybe some other metadata files. It would make
# it easier to detect as well.
zip = zipfile.ZipFile(bundle_filename, 'w', zipfile.ZIP_DEFLATED)
vcs_dirs = []
for dir, basename in (self.build_dir, 'build'), (self.src_dir, 'src'):
dir = os.path.normcase(os.path.abspath(dir))
for dirpath, dirnames, filenames in os.walk(dir):
for backend in vcs.backends:
vcs_backend = backend()
vcs_url = vcs_rev = None
if vcs_backend.dirname in dirnames:
for vcs_dir in vcs_dirs:
if dirpath.startswith(vcs_dir):
# vcs bundle file already in parent directory
break
else:
vcs_url, vcs_rev = vcs_backend.get_info(
os.path.join(dir, dirpath))
vcs_dirs.append(dirpath)
vcs_bundle_file = vcs_backend.bundle_file
vcs_guide = vcs_backend.guide % {'url': vcs_url,
'rev': vcs_rev}
dirnames.remove(vcs_backend.dirname)
break
if 'pip-egg-info' in dirnames:
dirnames.remove('pip-egg-info')
for dirname in dirnames:
dirname = os.path.join(dirpath, dirname)
name = self._clean_zip_name(dirname, dir)
zip.writestr(basename + '/' + name + '/', '')
for filename in filenames:
if filename == PIP_DELETE_MARKER_FILENAME:
continue
filename = os.path.join(dirpath, filename)
name = self._clean_zip_name(filename, dir)
zip.write(filename, basename + '/' + name)
if vcs_url:
name = os.path.join(dirpath, vcs_bundle_file)
name = self._clean_zip_name(name, dir)
zip.writestr(basename + '/' + name, vcs_guide)
zip.writestr('pip-manifest.txt', self.bundle_requirements())
zip.close()
BUNDLE_HEADER = '''\
# This is a pip bundle file, that contains many source packages
# that can be installed as a group. You can install this like:
# pip this_file.zip
# The rest of the file contains a list of all the packages included:
'''
def bundle_requirements(self):
parts = [self.BUNDLE_HEADER]
for req in [req for req in self.requirements.values()
if not req.comes_from]:
parts.append('%s==%s\n' % (req.name, req.installed_version))
parts.append(
'# These packages were installed to satisfy the above '
'requirements:\n'
)
for req in [req for req in self.requirements.values()
if req.comes_from]:
parts.append('%s==%s\n' % (req.name, req.installed_version))
# FIXME: should we do something with self.unnamed_requirements?
return ''.join(parts)
def _clean_zip_name(self, name, prefix):
assert name.startswith(prefix + os.path.sep), (
"name %r doesn't start with prefix %r" % (name, prefix)
)
name = name[len(prefix) + 1:]
name = name.replace(os.path.sep, '/')
return name

View File

@ -631,13 +631,12 @@ def unpack_file(filename, location, content_type, link):
filename = os.path.realpath(filename)
if (content_type == 'application/zip'
or filename.endswith('.zip')
or filename.endswith('.pybundle')
or filename.endswith('.whl')
or zipfile.is_zipfile(filename)):
unzip_file(
filename,
location,
flatten=not filename.endswith(('.pybundle', '.whl'))
flatten=not filename.endswith('.whl')
)
elif (content_type == 'application/x-gzip'
or tarfile.is_tarfile(filename)

View File

@ -153,14 +153,6 @@ class VersionControl(object):
"""
return (self.normalize_url(url1) == self.normalize_url(url2))
def parse_vcs_bundle_file(self, content):
"""
Takes the contents of the bundled text file that explains how to revert
the stripped off version control data of the given package and returns
the URL and revision of it.
"""
raise NotImplementedError
def obtain(self, dest):
"""
Called when installing or updating an editable package, takes the

View File

@ -12,13 +12,10 @@ class Bazaar(VersionControl):
name = 'bzr'
dirname = '.bzr'
repo_name = 'branch'
bundle_file = 'bzr-branch.txt'
schemes = (
'bzr', 'bzr+http', 'bzr+https', 'bzr+ssh', 'bzr+sftp', 'bzr+ftp',
'bzr+lp',
)
guide = ('# This was a Bazaar branch; to make it a branch again run:\n'
'bzr branch -r %(rev)s %(url)s .\n')
def __init__(self, url=None, *args, **kwargs):
super(Bazaar, self).__init__(url, *args, **kwargs)
@ -28,19 +25,6 @@ class Bazaar(VersionControl):
urlparse.uses_fragment.extend(['lp'])
urlparse.non_hierarchical.extend(['lp'])
def parse_vcs_bundle_file(self, content):
url = rev = None
for line in content.splitlines():
if not line.strip() or line.strip().startswith('#'):
continue
match = re.search(r'^bzr\s*branch\s*-r\s*(\d*)', line)
if match:
rev = match.group(1).strip()
url = line[match.end():].strip().split(None, 1)[0]
if url and rev:
return url, rev
return None, None
def export(self, location):
"""
Export the Bazaar repository at the url to the destination location

View File

@ -1,11 +1,12 @@
import tempfile
import re
import os.path
from pip.util import call_subprocess
from pip.util import display_path, rmtree
from pip.vcs import vcs, VersionControl
from pip.log import logger
from pip.backwardcompat import url2pathname, urlparse
urlsplit = urlparse.urlsplit
urlunsplit = urlparse.urlunsplit
@ -17,10 +18,6 @@ class Git(VersionControl):
schemes = (
'git', 'git+http', 'git+https', 'git+ssh', 'git+git', 'git+file',
)
bundle_file = 'git-clone.txt'
guide = ('# This was a Git repo; to make it a repo again run:\n'
'git init\ngit remote add origin %(url)s -f\ngit '
'checkout %(rev)s\n')
def __init__(self, url=None, *args, **kwargs):
@ -42,24 +39,6 @@ class Git(VersionControl):
super(Git, self).__init__(url, *args, **kwargs)
def parse_vcs_bundle_file(self, content):
url = rev = None
for line in content.splitlines():
if not line.strip() or line.strip().startswith('#'):
continue
url_match = re.search(
r'git\s*remote\s*add\s*origin(.*)\s*-f',
line,
)
if url_match:
url = url_match.group(1).strip()
rev_match = re.search(r'^git\s*checkout\s*-q\s*(.*)\s*', line)
if rev_match:
rev = rev_match.group(1).strip()
if url and rev:
return url, rev
return None, None
def export(self, location):
"""Export the Git repository at the url to the destination location"""
temp_dir = tempfile.mkdtemp('-export', 'pip-')

View File

@ -15,24 +15,6 @@ class Mercurial(VersionControl):
dirname = '.hg'
repo_name = 'clone'
schemes = ('hg', 'hg+http', 'hg+https', 'hg+ssh', 'hg+static-http')
bundle_file = 'hg-clone.txt'
guide = ('# This was a Mercurial repo; to make it a repo again run:\n'
'hg init\nhg pull %(url)s\nhg update -r %(rev)s\n')
def parse_vcs_bundle_file(self, content):
url = rev = None
for line in content.splitlines():
if not line.strip() or line.strip().startswith('#'):
continue
url_match = re.search(r'hg\s*pull\s*(.*)\s*', line)
if url_match:
url = url_match.group(1).strip()
rev_match = re.search(r'^hg\s*update\s*-r\s*(.*)\s*', line)
if rev_match:
rev = rev_match.group(1).strip()
if url and rev:
return url, rev
return None, None
def export(self, location):
"""Export the Hg repository at the url to the destination location"""

View File

@ -18,9 +18,6 @@ class Subversion(VersionControl):
dirname = '.svn'
repo_name = 'checkout'
schemes = ('svn', 'svn+ssh', 'svn+http', 'svn+https', 'svn+svn')
bundle_file = 'svn-checkout.txt'
guide = ('# This was an svn checkout; to make it a checkout again run:\n'
'svn checkout --force -r %(rev)s %(url)s .\n')
def get_info(self, location):
"""Returns (url, revision), where both are strings"""
@ -49,18 +46,6 @@ class Subversion(VersionControl):
return url, None
return url, match.group(1)
def parse_vcs_bundle_file(self, content):
for line in content.splitlines():
if not line.strip() or line.strip().startswith('#'):
continue
match = re.search(r'^-r\s*([^ ])?', line)
if not match:
return None, None
rev = match.group(1)
rest = line[match.end():].strip().split(None, 1)[0]
return rest, rev
return None, None
def export(self, location):
"""Export the svn repository at the url to the destination location"""
url, rev = self.get_url_rev()

View File

@ -1,100 +0,0 @@
import zipfile
import textwrap
from os.path import exists, join
from pip.download import path_to_url
from tests.lib.local_repos import local_checkout
def test_create_bundle(script, tmpdir, data):
"""
Test making a bundle. We'll grab one package from the filesystem
(the FSPkg dummy package), one from vcs (initools) and one from an
index (pip itself).
"""
fspkg = path_to_url(data.packages / 'FSPkg')
script.pip('install', '-e', fspkg)
pkg_lines = textwrap.dedent(
'''
-e %s
-e %s#egg=initools-dev
pip
''' %
(
fspkg,
local_checkout(
'svn+http://svn.colorstudy.com/INITools/trunk',
tmpdir.join("cache"),
),
),
)
script.scratch_path.join("bundle-req.txt").write(pkg_lines)
# Create a bundle in env.scratch_path/ test.pybundle
result = script.pip(
'bundle', '--no-use-wheel', '-r',
script.scratch_path / 'bundle-req.txt',
script.scratch_path / 'test.pybundle',
)
bundle = result.files_after.get(join('scratch', 'test.pybundle'), None)
assert bundle is not None
files = zipfile.ZipFile(bundle.full).namelist()
assert 'src/FSPkg/' in files
assert 'src/initools/' in files
assert 'build/pip/' in files
def test_cleanup_after_create_bundle(script, tmpdir, data):
"""
Test clean up after making a bundle. Make sure (build|src)-bundle/ dirs are
removed but not src/.
"""
# Install an editable to create a src/ dir.
args = ['install']
args.extend([
'-e',
'%s#egg=pip-test-package' %
local_checkout(
'git+http://github.com/pypa/pip-test-package.git',
tmpdir.join("cache"),
),
])
script.pip(*args)
build = script.venv_path / "build"
src = script.venv_path / "src"
assert not exists(build), "build/ dir still exists: %s" % build
assert exists(src), "expected src/ dir doesn't exist: %s" % src
# Make the bundle.
fspkg = path_to_url(data.packages / 'FSPkg')
pkg_lines = textwrap.dedent(
'''
-e %s
-e %s#egg=initools-dev
pip
''' %
(
fspkg,
local_checkout(
'svn+http://svn.colorstudy.com/INITools/trunk',
tmpdir.join("cache"),
),
),
)
script.scratch_path.join("bundle-req.txt").write(pkg_lines)
script.pip(
'bundle', '--no-use-wheel', '-r', 'bundle-req.txt', 'test.pybundle',
)
build_bundle = script.scratch_path / "build-bundle"
src_bundle = script.scratch_path / "src-bundle"
assert not exists(build_bundle), (
"build-bundle/ dir still exists: %s" % build_bundle
)
assert not exists(src_bundle), "src-bundle/ dir still exists: %s" % (
src_bundle
)
script.assert_no_temp()
# Make sure previously created src/ from editable still exists
assert exists(src), "expected src dir doesn't exist: %s" % src

View File

@ -1,10 +0,0 @@
def test_install_pybundle(script, data):
"""
Test intalling a *.pybundle file
"""
result = script.pip_install_local(
data.packages.join("simplebundle.pybundle"),
expect_temp=True,
)
result.assert_installed('simple', editable=False)
result.assert_installed('simple2', editable=False)