1#
2# Copyright 2013, 2018 Free Software Foundation, Inc.
3#
4# This file is part of GNU Radio
5#
6# GNU Radio is free software; you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation; either version 3, or (at your option)
9# any later version.
10#
11# GNU Radio is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with GNU Radio; see the file COPYING.  If not, write to
18# the Free Software Foundation, Inc., 51 Franklin Street,
19# Boston, MA 02110-1301, USA.
20#
21""" Edit CMakeLists.txt files """
22
23from __future__ import print_function
24from __future__ import absolute_import
25from __future__ import unicode_literals
26
27import re
28import logging
29
30logger = logging.getLogger(__name__)
31
32
33class CMakeFileEditor(object):
34    """A tool for editing CMakeLists.txt files. """
35    def __init__(self, filename, separator='\n    ', indent='    '):
36        self.filename = filename
37        with open(filename, 'r') as f:
38            self.cfile = f.read()
39        self.separator = separator
40        self.indent = indent
41
42    def append_value(self, entry, value, to_ignore_start='', to_ignore_end=''):
43        """ Add a value to an entry. """
44        regexp = re.compile(r'({}\({}[^()]*?)\s*?(\s?{})\)'.format(entry, to_ignore_start, to_ignore_end),
45                            re.MULTILINE)
46        substi = r'\1' + self.separator + value + r'\2)'
47        (self.cfile, nsubs) = regexp.subn(substi, self.cfile, count=1)
48        return nsubs
49
50    def remove_value(self, entry, value, to_ignore_start='', to_ignore_end=''):
51        """
52        Remove a value from an entry.
53        Example: You want to remove file.cc from this list() entry:
54        list(SOURCES
55            file.cc
56            other_file.cc
57        )
58
59        Then run:
60        >>> C.remove_value('list', 'file.cc', 'SOURCES')
61
62        Returns the number of occurrences of entry in the current file
63        that were removed.
64        """
65        # In the case of the example above, these are cases we need to catch:
66        # - list(file.cc ...
67        #   entry is right after the value parentheses, no whitespace. Can only happen
68        #   when to_ignore_start is empty.
69        # - list(... file.cc)
70        #   Other entries come first, then entry is preceded by whitespace.
71        # - list(SOURCES ... file.cc) # whitespace!
72        #   When to_ignore_start is not empty, entry must always be preceded by whitespace.
73        if len(to_ignore_start) == 0:
74            regexp = r'^\s*({entry}\((?:[^()]*?\s+|)){value}\s*([^()]*{to_ignore_end}\s*\)){to_ignore_start}'
75        else:
76            regexp = r'^\s*({entry}\(\s*{to_ignore_start}[^()]*?\s+){value}\s*([^()]*{to_ignore_end}\s*\))'
77        regexp = regexp.format(
78                entry=entry,
79                to_ignore_start=to_ignore_start,
80                value=value,
81                to_ignore_end=to_ignore_end,
82        )
83        regexp = re.compile(regexp, re.MULTILINE)
84        (self.cfile, nsubs) = re.subn(regexp, r'\1\2', self.cfile, count=1)
85        return nsubs
86
87    def delete_entry(self, entry, value_pattern=''):
88        """Remove an entry from the current buffer."""
89        regexp = r'{}\s*\([^()]*{}[^()]*\)[^\n]*\n'.format(entry, value_pattern)
90        regexp = re.compile(regexp, re.MULTILINE)
91        (self.cfile, nsubs) = re.subn(regexp, '', self.cfile, count=1)
92        return nsubs
93
94    def write(self):
95        """ Write the changes back to the file. """
96        with open(self.filename, 'w') as f:
97            f.write(self.cfile)
98
99    def remove_double_newlines(self):
100        """Simply clear double newlines from the file buffer."""
101        self.cfile = re.compile('\n\n\n+', re.MULTILINE).sub('\n\n', self.cfile)
102
103    def find_filenames_match(self, regex):
104        """ Find the filenames that match a certain regex
105        on lines that aren't comments """
106        filenames = []
107        reg = re.compile(regex)
108        fname_re = re.compile(r'[a-zA-Z]\w+\.\w{1,5}$')
109        for line in self.cfile.splitlines():
110            if len(line.strip()) == 0 or line.strip()[0] == '#':
111                continue
112            for word in re.split('[ /)(\t\n\r\f\v]', line):
113                if fname_re.match(word) and reg.search(word):
114                    filenames.append(word)
115        return filenames
116
117    def disable_file(self, fname):
118        """ Comment out a file.
119        Example:
120        add_library(
121            file1.cc
122        )
123
124        Here, file1.cc becomes #file1.cc with disable_file('file1.cc').
125        """
126        starts_line = False
127        for line in self.cfile.splitlines():
128            if len(line.strip()) == 0 or line.strip()[0] == '#':
129                continue
130            if re.search(r'\b'+fname+r'\b', line):
131                if re.match(fname, line.lstrip()):
132                    starts_line = True
133                break
134        comment_out_re = r'#\1' + '\n' + self.indent
135        if not starts_line:
136            comment_out_re = r'\n' + self.indent + comment_out_re
137        (self.cfile, nsubs) = re.subn(r'(\b'+fname+r'\b)\s*', comment_out_re, self.cfile)
138        if nsubs == 0:
139            logger.warning("Warning: A replacement failed when commenting out {}. Check the CMakeFile.txt manually.".format(fname))
140        elif nsubs > 1:
141            logger.warning("Warning: Replaced {} {} times (instead of once). Check the CMakeFile.txt manually.".format(fname, nsubs))
142
143    def comment_out_lines(self, pattern, comment_str='#'):
144        """ Comments out all lines that match with pattern """
145        for line in self.cfile.splitlines():
146            if re.search(pattern, line):
147                self.cfile = self.cfile.replace(line, comment_str+line)
148
149    def check_for_glob(self, globstr):
150        """ Returns true if a glob as in globstr is found in the cmake file """
151        glob_re = r'GLOB\s[a-z_]+\s"{}"'.format(globstr.replace('*', r'\*'))
152        return re.search(glob_re, self.cfile, flags=re.MULTILINE|re.IGNORECASE) is not None
153