Commit a0dce4cc authored by Julien Muchembled's avatar Julien Muchembled Committed by Xavier Thompson

[fix] Write .installed.cfg only once, in safe way and only if there's any change.

Also, updating a part does not put it anymore at the end of the list of
installed parts, that was making .installed.cfg too big.
parent 26382551
...@@ -29,6 +29,11 @@ try: ...@@ -29,6 +29,11 @@ try:
except ImportError: except ImportError:
from UserDict import DictMixin from UserDict import DictMixin
try:
from cStringIO import StringIO
except ImportError:
from io import StringIO
try: try:
from urllib.parse import urljoin from urllib.parse import urljoin
except ImportError: # BBB Py2 except ImportError: # BBB Py2
...@@ -38,6 +43,7 @@ import zc.buildout.configparser ...@@ -38,6 +43,7 @@ import zc.buildout.configparser
import copy import copy
import datetime import datetime
import distutils.errors import distutils.errors
import errno
import glob import glob
import importlib import importlib
import inspect import inspect
...@@ -264,6 +270,12 @@ def _print_annotate(data, verbose, chosen_sections, basedir): ...@@ -264,6 +270,12 @@ def _print_annotate(data, verbose, chosen_sections, basedir):
sectionkey = data[section][key] sectionkey = data[section][key]
sectionkey.printAll(key, basedir, verbose) sectionkey.printAll(key, basedir, verbose)
def _remove_ignore_missing(path):
try:
os.remove(path)
except OSError as e:
if e.errno != errno.ENOENT:
raise
def _unannotate_section(section): def _unannotate_section(section):
for key in section: for key in section:
...@@ -330,6 +342,8 @@ class Buildout(DictMixin): ...@@ -330,6 +342,8 @@ class Buildout(DictMixin):
COMMANDS = set() COMMANDS = set()
installed_part_options = None
def __init__(self, config_file, cloptions, def __init__(self, config_file, cloptions,
use_user_defaults=True, use_user_defaults=True,
command=None, args=()): command=None, args=()):
...@@ -752,6 +766,19 @@ class Buildout(DictMixin): ...@@ -752,6 +766,19 @@ class Buildout(DictMixin):
@command @command
def install(self, install_args): def install(self, install_args):
try:
self._install_parts(install_args)
finally:
if self.installed_part_options is not None:
try:
self._save_installed_options()
finally:
del self.installed_part_options
if self.show_picked_versions or self.update_versions_file:
self._print_picked_versions()
self._unload_extensions()
def _install_parts(self, install_args):
__doing__ = 'Installing.' __doing__ = 'Installing.'
self._load_extensions() self._load_extensions()
...@@ -765,8 +792,8 @@ class Buildout(DictMixin): ...@@ -765,8 +792,8 @@ class Buildout(DictMixin):
self._maybe_upgrade() self._maybe_upgrade()
# load installed data # load installed data
(installed_part_options, installed_exists installed_part_options = self._read_installed_part_options()
)= self._read_installed_part_options() installed_parts = installed_part_options['buildout']['parts'].split()
# Remove old develop eggs # Remove old develop eggs
self._uninstall( self._uninstall(
...@@ -779,21 +806,15 @@ class Buildout(DictMixin): ...@@ -779,21 +806,15 @@ class Buildout(DictMixin):
installed_part_options['buildout']['installed_develop_eggs' installed_part_options['buildout']['installed_develop_eggs'
] = installed_develop_eggs ] = installed_develop_eggs
if installed_exists: # From now, the caller will update the .installed.cfg at return.
self._update_installed( self.installed_part_options = installed_part_options
installed_develop_eggs=installed_develop_eggs)
# get configured and installed part lists
conf_parts = self['buildout']['parts']
conf_parts = conf_parts and conf_parts.split() or []
installed_parts = installed_part_options['buildout']['parts']
installed_parts = installed_parts and installed_parts.split() or []
install_parts = self['buildout']['parts']
if install_args: if install_args:
install_parts = install_args install_parts = install_args
uninstall_missing = False uninstall_missing = False
else: else:
install_parts = conf_parts install_parts = install_parts.split()
uninstall_missing = True uninstall_missing = True
# load and initialize recipes # load and initialize recipes
...@@ -854,9 +875,8 @@ class Buildout(DictMixin): ...@@ -854,9 +875,8 @@ class Buildout(DictMixin):
self._uninstall_part(part, installed_part_options) self._uninstall_part(part, installed_part_options)
installed_parts = [p for p in installed_parts if p != part] installed_parts = [p for p in installed_parts if p != part]
installed_part_options['buildout']['parts'] = (
if installed_exists: ' '.join(installed_parts))
self._update_installed(parts=' '.join(installed_parts))
# Check for unused buildout options: # Check for unused buildout options:
_check_for_unused_options_in_section(self, 'buildout') _check_for_unused_options_in_section(self, 'buildout')
...@@ -867,11 +887,10 @@ class Buildout(DictMixin): ...@@ -867,11 +887,10 @@ class Buildout(DictMixin):
saved_options = self[part].copy() saved_options = self[part].copy()
recipe = self[part].recipe recipe = self[part].recipe
if part in installed_parts: # update if part in installed_parts: # update
need_to_save_installed = False
__doing__ = 'Updating %s.', part __doing__ = 'Updating %s.', part
self._logger.info(*__doing__) self._logger.info(*__doing__)
old_options = installed_part_options[part] old_options = installed_part_options[part]
old_installed_files = old_options['__buildout_installed__'] installed_files = old_options['__buildout_installed__']
try: try:
update = recipe.update update = recipe.update
...@@ -883,34 +902,21 @@ class Buildout(DictMixin): ...@@ -883,34 +902,21 @@ class Buildout(DictMixin):
part) part)
try: try:
installed_files = self[part]._call(update) updated_files = self[part]._call(update)
except: except Exception:
installed_parts.remove(part) installed_parts.remove(part)
self._uninstall(old_installed_files) self._uninstall(installed_files)
if installed_exists: installed_part_options['buildout']['parts'] = (
self._update_installed( ' '.join(installed_parts))
parts=' '.join(installed_parts))
raise raise
old_installed_files = old_installed_files.split('\n') if updated_files:
if installed_files is None: installed_files = set(installed_files.split('\n'))
installed_files = old_installed_files (installed_files.add if isinstance(updated_files, str) else
else: installed_files.update)(updated_files)
if isinstance(installed_files, str): installed_files = '\n'.join(sorted(installed_files))
installed_files = [installed_files]
else:
installed_files = list(installed_files)
need_to_save_installed = [
p for p in installed_files
if p not in old_installed_files]
if need_to_save_installed:
installed_files = (old_installed_files
+ need_to_save_installed)
else: # install else: # install
need_to_save_installed = True
__doing__ = 'Installing %s.', part __doing__ = 'Installing %s.', part
self._logger.info(*__doing__) self._logger.info(*__doing__)
installed_files = self[part]._call(recipe.install) installed_files = self[part]._call(recipe.install)
...@@ -919,47 +925,19 @@ class Buildout(DictMixin): ...@@ -919,47 +925,19 @@ class Buildout(DictMixin):
"The %s install returned None. A path or " "The %s install returned None. A path or "
"iterable os paths should be returned.", "iterable os paths should be returned.",
part) part)
installed_files = () installed_files = ""
elif isinstance(installed_files, str): elif not isinstance(installed_files, str):
installed_files = [installed_files] installed_files = '\n'.join(installed_files)
else:
installed_files = list(installed_files)
installed_part_options[part] = saved_options saved_options['__buildout_installed__'] = installed_files
saved_options['__buildout_installed__'
] = '\n'.join(installed_files)
saved_options['__buildout_signature__'] = signature saved_options['__buildout_signature__'] = signature
installed_part_options[part] = saved_options
installed_parts = [p for p in installed_parts if p != part] if part not in installed_parts:
installed_parts.append(part) installed_parts.append(part)
_check_for_unused_options_in_section(self, part)
if need_to_save_installed:
installed_part_options['buildout']['parts'] = ( installed_part_options['buildout']['parts'] = (
' '.join(installed_parts)) ' '.join(installed_parts))
self._save_installed_options(installed_part_options) _check_for_unused_options_in_section(self, part)
installed_exists = True
else:
assert installed_exists
self._update_installed(parts=' '.join(installed_parts))
if installed_develop_eggs:
if not installed_exists:
self._save_installed_options(installed_part_options)
elif (not installed_parts) and installed_exists:
os.remove(self['buildout']['installed'])
if self.show_picked_versions or self.update_versions_file:
self._print_picked_versions()
self._unload_extensions()
def _update_installed(self, **buildout_options):
installed = self['buildout']['installed']
f = open(installed, 'a')
f.write('\n[buildout]\n')
for option, value in list(buildout_options.items()):
_save_option(option, value, f)
f.close()
def _uninstall_part(self, part, installed_part_options): def _uninstall_part(self, part, installed_part_options):
# uninstall part # uninstall part
...@@ -1076,11 +1054,9 @@ class Buildout(DictMixin): ...@@ -1076,11 +1054,9 @@ class Buildout(DictMixin):
options[option] = value options[option] = value
result[section] = self.Options(self, section, options) result[section] = self.Options(self, section, options)
return result, True return result
else: else:
return ({'buildout': self.Options(self, 'buildout', {'parts': ''})}, return {'buildout': self.Options(self, 'buildout', {'parts': ''})}
False,
)
def _uninstall(self, installed): def _uninstall(self, installed):
for f in installed.split('\n'): for f in installed.split('\n'):
...@@ -1121,16 +1097,39 @@ class Buildout(DictMixin): ...@@ -1121,16 +1097,39 @@ class Buildout(DictMixin):
return ' '.join(installed) return ' '.join(installed)
def _save_installed_options(self, installed_options): def _save_installed_options(self):
installed = self['buildout']['installed'] installed_path = self['buildout']['installed']
if not installed: if not installed_path:
return return
f = open(installed, 'w') installed_part_options = self.installed_part_options
_save_options('buildout', installed_options['buildout'], f) buildout = installed_part_options['buildout']
for part in installed_options['buildout']['parts'].split(): installed_parts = buildout['parts']
print_(file=f) if installed_parts or buildout['installed_develop_eggs']:
_save_options(part, installed_options[part], f) new = StringIO()
f.close() _save_options('buildout', buildout, new)
for part in installed_parts.split():
new.write('\n')
_save_options(part, installed_part_options[part], new)
new = new.getvalue()
try:
with open(installed_path) as f:
save = f.read(1+len(new)) != new
except IOError as e:
if e.errno != errno.ENOENT:
raise
save = True
if save:
installed_tmp = installed_path + ".tmp"
try:
with open(installed_tmp, "w") as f:
f.write(new)
f.flush()
os.fsync(f.fileno())
os.rename(installed_tmp, installed_path)
finally:
_remove_ignore_missing(installed_tmp)
else:
_remove_ignore_missing(installed_path)
def _error(self, message, *args): def _error(self, message, *args):
raise zc.buildout.UserError(message % args) raise zc.buildout.UserError(message % args)
...@@ -1442,10 +1441,6 @@ class Buildout(DictMixin): ...@@ -1442,10 +1441,6 @@ class Buildout(DictMixin):
self[name] # Add to parts self[name] # Add to parts
def parse(self, data): def parse(self, data):
try:
from cStringIO import StringIO
except ImportError:
from io import StringIO
import textwrap import textwrap
sections = zc.buildout.configparser.parse( sections = zc.buildout.configparser.parse(
......
...@@ -2804,10 +2804,10 @@ provide alternate locations, and even names for these directories:: ...@@ -2804,10 +2804,10 @@ provide alternate locations, and even names for these directories::
Creating directory '/sample-alt/work'. Creating directory '/sample-alt/work'.
Creating directory '/sample-alt/developbasket'. Creating directory '/sample-alt/developbasket'.
Develop: '/sample-buildout/recipes' Develop: '/sample-buildout/recipes'
Uninstalling d4.
Uninstalling d3.
Uninstalling d2. Uninstalling d2.
Uninstalling debug. Uninstalling debug.
Uninstalling d4.
Uninstalling d3.
>>> ls(alt) >>> ls(alt)
d basket d basket
......
...@@ -1679,7 +1679,7 @@ some evil recipes that exit uncleanly: ...@@ -1679,7 +1679,7 @@ some evil recipes that exit uncleanly:
>>> mkdir('recipes') >>> mkdir('recipes')
>>> write('recipes', 'recipes.py', >>> write('recipes', 'recipes.py',
... ''' ... '''
... import os ... import sys
... ...
... class Clean: ... class Clean:
... def __init__(*_): pass ... def __init__(*_): pass
...@@ -1687,10 +1687,10 @@ some evil recipes that exit uncleanly: ...@@ -1687,10 +1687,10 @@ some evil recipes that exit uncleanly:
... def update(_): pass ... def update(_): pass
... ...
... class EvilInstall(Clean): ... class EvilInstall(Clean):
... def install(_): os._exit(1) ... def install(_): sys.exit(1)
... ...
... class EvilUpdate(Clean): ... class EvilUpdate(Clean):
... def update(_): os._exit(1) ... def update(_): sys.exit(1)
... ''') ... ''')
>>> write('recipes', 'setup.py', >>> write('recipes', 'setup.py',
...@@ -1784,10 +1784,10 @@ Now let's look at 3 cases: ...@@ -1784,10 +1784,10 @@ Now let's look at 3 cases:
>>> print_(system(buildout+' buildout:parts='), end='') >>> print_(system(buildout+' buildout:parts='), end='')
Develop: '/sample-buildout/recipes' Develop: '/sample-buildout/recipes'
Uninstalling p2.
Uninstalling p1.
Uninstalling p4. Uninstalling p4.
Uninstalling p3. Uninstalling p3.
Uninstalling p2.
Uninstalling p1.
3. We exit while installing or updating after uninstalling: 3. We exit while installing or updating after uninstalling:
......
...@@ -434,8 +434,8 @@ Create a clean buildout.cfg w/o the checkenv recipe, and delete the recipe: ...@@ -434,8 +434,8 @@ Create a clean buildout.cfg w/o the checkenv recipe, and delete the recipe:
... """ % dict(server=link_server)) ... """ % dict(server=link_server))
>>> print_(system(buildout), end='') # doctest: +ELLIPSIS >>> print_(system(buildout), end='') # doctest: +ELLIPSIS
Develop: '/sample-buildout/recipes' Develop: '/sample-buildout/recipes'
Uninstalling checkenv.
Uninstalling extdemo. Uninstalling extdemo.
Uninstalling checkenv.
Installing extdemo... Installing extdemo...
>>> rmdir(sample_buildout, 'recipes') >>> rmdir(sample_buildout, 'recipes')
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment