1"""SCons.SConsign
2
3Writing and reading information to the .sconsign file or files.
4
5"""
6
7#
8# Copyright (c) 2001 - 2014 The SCons Foundation
9#
10# Permission is hereby granted, free of charge, to any person obtaining
11# a copy of this software and associated documentation files (the
12# "Software"), to deal in the Software without restriction, including
13# without limitation the rights to use, copy, modify, merge, publish,
14# distribute, sublicense, and/or sell copies of the Software, and to
15# permit persons to whom the Software is furnished to do so, subject to
16# the following conditions:
17#
18# The above copyright notice and this permission notice shall be included
19# in all copies or substantial portions of the Software.
20#
21# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
22# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
23# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
24# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
25# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
26# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
27# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
28#
29
30__revision__ = "src/engine/SCons/SConsign.py  2014/07/05 09:42:21 garyo"
31
32import SCons.compat
33
34import os
35# compat layer imports "cPickle" for us if it's available.
36import pickle
37
38import SCons.dblite
39import SCons.Warnings
40
41def corrupt_dblite_warning(filename):
42    SCons.Warnings.warn(SCons.Warnings.CorruptSConsignWarning,
43                        "Ignoring corrupt .sconsign file: %s"%filename)
44
45SCons.dblite.ignore_corrupt_dbfiles = 1
46SCons.dblite.corruption_warning = corrupt_dblite_warning
47
48#XXX Get rid of the global array so this becomes re-entrant.
49sig_files = []
50
51# Info for the database SConsign implementation (now the default):
52# "DataBase" is a dictionary that maps top-level SConstruct directories
53# to open database handles.
54# "DB_Module" is the Python database module to create the handles.
55# "DB_Name" is the base name of the database file (minus any
56# extension the underlying DB module will add).
57DataBase = {}
58DB_Module = SCons.dblite
59# Nuitka: Avoid collisions with newer Scons using newer pickle protocols
60DB_Name = ".sconsign2"
61DB_sync_list = []
62
63def Get_DataBase(dir):
64    global DataBase, DB_Module, DB_Name
65    top = dir.fs.Top
66    if not os.path.isabs(DB_Name) and top.repositories:
67        mode = "c"
68        for d in [top] + top.repositories:
69            if dir.is_under(d):
70                try:
71                    return DataBase[d], mode
72                except KeyError:
73                    path = d.entry_abspath(DB_Name)
74                    try: db = DataBase[d] = DB_Module.open(path, mode)
75                    except (IOError, OSError): pass
76                    else:
77                        if mode != "r":
78                            DB_sync_list.append(db)
79                        return db, mode
80            mode = "r"
81    try:
82        return DataBase[top], "c"
83    except KeyError:
84        db = DataBase[top] = DB_Module.open(DB_Name, "c")
85        DB_sync_list.append(db)
86        return db, "c"
87    except TypeError:
88        print "DataBase =", DataBase
89        raise
90
91def Reset():
92    """Reset global state.  Used by unit tests that end up using
93    SConsign multiple times to get a clean slate for each test."""
94    global sig_files, DB_sync_list
95    sig_files = []
96    DB_sync_list = []
97
98normcase = os.path.normcase
99
100def write():
101    global sig_files
102    for sig_file in sig_files:
103        sig_file.write(sync=0)
104    for db in DB_sync_list:
105        try:
106            syncmethod = db.sync
107        except AttributeError:
108            pass # Not all dbm modules have sync() methods.
109        else:
110            syncmethod()
111        try:
112            closemethod = db.close
113        except AttributeError:
114            pass # Not all dbm modules have close() methods.
115        else:
116            closemethod()
117
118class SConsignEntry(object):
119    """
120    Wrapper class for the generic entry in a .sconsign file.
121    The Node subclass populates it with attributes as it pleases.
122
123    XXX As coded below, we do expect a '.binfo' attribute to be added,
124    but we'll probably generalize this in the next refactorings.
125    """
126    current_version_id = 1
127    def __init__(self):
128        # Create an object attribute from the class attribute so it ends up
129        # in the pickled data in the .sconsign file.
130        _version_id = self.current_version_id
131    def convert_to_sconsign(self):
132        self.binfo.convert_to_sconsign()
133    def convert_from_sconsign(self, dir, name):
134        self.binfo.convert_from_sconsign(dir, name)
135
136class Base(object):
137    """
138    This is the controlling class for the signatures for the collection of
139    entries associated with a specific directory.  The actual directory
140    association will be maintained by a subclass that is specific to
141    the underlying storage method.  This class provides a common set of
142    methods for fetching and storing the individual bits of information
143    that make up signature entry.
144    """
145    def __init__(self):
146        self.entries = {}
147        self.dirty = False
148        self.to_be_merged = {}
149
150    def get_entry(self, filename):
151        """
152        Fetch the specified entry attribute.
153        """
154        return self.entries[filename]
155
156    def set_entry(self, filename, obj):
157        """
158        Set the entry.
159        """
160        self.entries[filename] = obj
161        self.dirty = True
162
163    def do_not_set_entry(self, filename, obj):
164        pass
165
166    def store_info(self, filename, node):
167        entry = node.get_stored_info()
168        entry.binfo.merge(node.get_binfo())
169        self.to_be_merged[filename] = node
170        self.dirty = True
171
172    def do_not_store_info(self, filename, node):
173        pass
174
175    def merge(self):
176        for key, node in self.to_be_merged.items():
177            entry = node.get_stored_info()
178            try:
179                ninfo = entry.ninfo
180            except AttributeError:
181                # This happens with SConf Nodes, because the configuration
182                # subsystem takes direct control over how the build decision
183                # is made and its information stored.
184                pass
185            else:
186                ninfo.merge(node.get_ninfo())
187            self.entries[key] = entry
188        self.to_be_merged = {}
189
190class DB(Base):
191    """
192    A Base subclass that reads and writes signature information
193    from a global .sconsign.db* file--the actual file suffix is
194    determined by the database module.
195    """
196    def __init__(self, dir):
197        Base.__init__(self)
198
199        self.dir = dir
200
201        db, mode = Get_DataBase(dir)
202
203        # Read using the path relative to the top of the Repository
204        # (self.dir.tpath) from which we're fetching the signature
205        # information.
206        path = normcase(dir.tpath)
207        try:
208            rawentries = db[path]
209        except KeyError:
210            pass
211        else:
212            try:
213                self.entries = pickle.loads(rawentries)
214                if not isinstance(self.entries, dict):
215                    self.entries = {}
216                    raise TypeError
217            except KeyboardInterrupt:
218                raise
219            except Exception, e:
220                SCons.Warnings.warn(SCons.Warnings.CorruptSConsignWarning,
221                                    "Ignoring corrupt sconsign entry : %s (%s)\n"%(self.dir.tpath, e))
222            for key, entry in self.entries.items():
223                entry.convert_from_sconsign(dir, key)
224
225        if mode == "r":
226            # This directory is actually under a repository, which means
227            # likely they're reaching in directly for a dependency on
228            # a file there.  Don't actually set any entry info, so we
229            # won't try to write to that .sconsign.dblite file.
230            self.set_entry = self.do_not_set_entry
231            self.store_info = self.do_not_store_info
232
233        global sig_files
234        sig_files.append(self)
235
236    def write(self, sync=1):
237        if not self.dirty:
238            return
239
240        self.merge()
241
242        db, mode = Get_DataBase(self.dir)
243
244        # Write using the path relative to the top of the SConstruct
245        # directory (self.dir.path), not relative to the top of
246        # the Repository; we only write to our own .sconsign file,
247        # not to .sconsign files in Repositories.
248        path = normcase(self.dir.path)
249        for key, entry in self.entries.items():
250            entry.convert_to_sconsign()
251        db[path] = pickle.dumps(self.entries, 1)
252
253        if sync:
254            try:
255                syncmethod = db.sync
256            except AttributeError:
257                # Not all anydbm modules have sync() methods.
258                pass
259            else:
260                syncmethod()
261
262class Dir(Base):
263    def __init__(self, fp=None, dir=None):
264        """
265        fp - file pointer to read entries from
266        """
267        Base.__init__(self)
268
269        if not fp:
270            return
271
272        self.entries = pickle.load(fp)
273        if not isinstance(self.entries, dict):
274            self.entries = {}
275            raise TypeError
276
277        if dir:
278            for key, entry in self.entries.items():
279                entry.convert_from_sconsign(dir, key)
280
281class DirFile(Dir):
282    """
283    Encapsulates reading and writing a per-directory .sconsign file.
284    """
285    def __init__(self, dir):
286        """
287        dir - the directory for the file
288        """
289
290        self.dir = dir
291        self.sconsign = os.path.join(dir.path, '.sconsign')
292
293        try:
294            fp = open(self.sconsign, 'rb')
295        except IOError:
296            fp = None
297
298        try:
299            Dir.__init__(self, fp, dir)
300        except KeyboardInterrupt:
301            raise
302        except:
303            SCons.Warnings.warn(SCons.Warnings.CorruptSConsignWarning,
304                                "Ignoring corrupt .sconsign file: %s"%self.sconsign)
305
306        global sig_files
307        sig_files.append(self)
308
309    def write(self, sync=1):
310        """
311        Write the .sconsign file to disk.
312
313        Try to write to a temporary file first, and rename it if we
314        succeed.  If we can't write to the temporary file, it's
315        probably because the directory isn't writable (and if so,
316        how did we build anything in this directory, anyway?), so
317        try to write directly to the .sconsign file as a backup.
318        If we can't rename, try to copy the temporary contents back
319        to the .sconsign file.  Either way, always try to remove
320        the temporary file at the end.
321        """
322        if not self.dirty:
323            return
324
325        self.merge()
326
327        temp = os.path.join(self.dir.path, '.scons%d' % os.getpid())
328        try:
329            file = open(temp, 'wb')
330            fname = temp
331        except IOError:
332            try:
333                file = open(self.sconsign, 'wb')
334                fname = self.sconsign
335            except IOError:
336                return
337        for key, entry in self.entries.items():
338            entry.convert_to_sconsign()
339        pickle.dump(self.entries, file, 1)
340        file.close()
341        if fname != self.sconsign:
342            try:
343                mode = os.stat(self.sconsign)[0]
344                os.chmod(self.sconsign, 0666)
345                os.unlink(self.sconsign)
346            except (IOError, OSError):
347                # Try to carry on in the face of either OSError
348                # (things like permission issues) or IOError (disk
349                # or network issues).  If there's a really dangerous
350                # issue, it should get re-raised by the calls below.
351                pass
352            try:
353                os.rename(fname, self.sconsign)
354            except OSError:
355                # An OSError failure to rename may indicate something
356                # like the directory has no write permission, but
357                # the .sconsign file itself might still be writable,
358                # so try writing on top of it directly.  An IOError
359                # here, or in any of the following calls, would get
360                # raised, indicating something like a potentially
361                # serious disk or network issue.
362                open(self.sconsign, 'wb').write(open(fname, 'rb').read())
363                os.chmod(self.sconsign, mode)
364        try:
365            os.unlink(temp)
366        except (IOError, OSError):
367            pass
368
369ForDirectory = DB
370
371def File(name, dbm_module=None):
372    """
373    Arrange for all signatures to be stored in a global .sconsign.db*
374    file.
375    """
376    global ForDirectory, DB_Name, DB_Module
377    if name is None:
378        ForDirectory = DirFile
379        DB_Module = None
380    else:
381        ForDirectory = DB
382        DB_Name = name
383        if not dbm_module is None:
384            DB_Module = dbm_module
385
386# Local Variables:
387# tab-width:4
388# indent-tabs-mode:nil
389# End:
390# vim: set expandtab tabstop=4 shiftwidth=4:
391