| 1 | # -*- coding: utf-8 -*- |
|---|
| 2 | # |
|---|
| 3 | # Copyright (C) 2016 Emmanuel Saint-James <esj@rezo.net> |
|---|
| 4 | # |
|---|
| 5 | |
|---|
| 6 | import os |
|---|
| 7 | import re |
|---|
| 8 | from collections import OrderedDict |
|---|
| 9 | from subprocess import Popen |
|---|
| 10 | |
|---|
| 11 | from trac.core import TracError |
|---|
| 12 | from trac.util.text import to_unicode |
|---|
| 13 | |
|---|
| 14 | |
|---|
| 15 | def post_doxyfile(req, doxygen, doxygen_args, doxyfile, input, base_path, |
|---|
| 16 | log): |
|---|
| 17 | """ |
|---|
| 18 | Build a Doxyfile from POST data and execute Doxygen, |
|---|
| 19 | whose path and arguments are issued from trac.ini. |
|---|
| 20 | If exec succeed, redirect to the main page of the generated documentation. |
|---|
| 21 | """ |
|---|
| 22 | path_trac = req.args.get('OUTPUT_DIRECTORY') |
|---|
| 23 | if path_trac and path_trac[-1] != '/': |
|---|
| 24 | path_trac += '/' |
|---|
| 25 | if not doxyfile: |
|---|
| 26 | doxyfile = os.path.join(path_trac, 'Doxyfile') |
|---|
| 27 | if not os.path.isdir(path_trac): |
|---|
| 28 | try: |
|---|
| 29 | os.mkdir(path_trac) |
|---|
| 30 | except (IOError, OSError): |
|---|
| 31 | raise TracError("Can't create directory: %s" % path_trac) |
|---|
| 32 | |
|---|
| 33 | if not os.path.isdir(path_trac) or not os.access(path_trac, os.W_OK): |
|---|
| 34 | return {'msg': 'Error:' + path_trac + ' not W_OK', 'trace': ''} |
|---|
| 35 | else: |
|---|
| 36 | log.debug('calling ' + doxygen + ' ' + doxygen_args) |
|---|
| 37 | env = apply_doxyfile(req.args, doxygen, doxygen_args, doxyfile, input, |
|---|
| 38 | path_trac) |
|---|
| 39 | log.debug('write %d options in %s', env['options'], doxyfile) |
|---|
| 40 | if env['msg'] != '': |
|---|
| 41 | return env |
|---|
| 42 | else: |
|---|
| 43 | log.debug(env['trace']) |
|---|
| 44 | doc = path_trac[len(base_path):].strip('/') |
|---|
| 45 | if not doc: |
|---|
| 46 | url = req.href.doxygen('/') |
|---|
| 47 | else: |
|---|
| 48 | url = req.href.doxygen('/', doc=doc) |
|---|
| 49 | req.redirect(url) |
|---|
| 50 | |
|---|
| 51 | |
|---|
| 52 | def init_doxyfile(env, doxygen, doxyfile, input, base_path, default_doc, log): |
|---|
| 53 | """ |
|---|
| 54 | Build the standard Doxyfile, and merge it with the current if any. |
|---|
| 55 | Return a dict with the list of Doxygen options with their values, |
|---|
| 56 | along with an error message if any, |
|---|
| 57 | and the trace for warnings and the like. |
|---|
| 58 | """ |
|---|
| 59 | path_trac = os.path.join(base_path, default_doc) |
|---|
| 60 | if not env and not os.path.isdir(path_trac) or not os.access(path_trac, |
|---|
| 61 | os.W_OK): |
|---|
| 62 | env = {'msg': 'Error: ' + path_trac + ' not W_OK', 'trace': ''} |
|---|
| 63 | path_trac = '/tmp' |
|---|
| 64 | elif not env: |
|---|
| 65 | env = {'msg': '', 'trace': ''} |
|---|
| 66 | if not doxyfile: |
|---|
| 67 | doxyfile = os.path.join(path_trac, 'Doxyfile') |
|---|
| 68 | else: |
|---|
| 69 | doxyfile = '' |
|---|
| 70 | # Read old choices if they exists |
|---|
| 71 | if os.path.exists(doxyfile): |
|---|
| 72 | old = analyze_doxyfile(base_path, default_doc, input, doxyfile, {}, |
|---|
| 73 | log) |
|---|
| 74 | else: |
|---|
| 75 | old = {} |
|---|
| 76 | # Generate the std Doxyfile |
|---|
| 77 | # (newer after a doxygen command update, who knows) |
|---|
| 78 | fi = os.path.join(path_trac, 'doxygen.tmp') |
|---|
| 79 | fo = os.path.join(path_trac, 'doxygen.out') |
|---|
| 80 | o = open(fo, 'w') |
|---|
| 81 | fr = os.path.join(path_trac, 'doxygen.err') |
|---|
| 82 | e = open(fr, 'w') |
|---|
| 83 | if o and e: |
|---|
| 84 | p = Popen([doxygen, '-g', fi], bufsize=-1, stdout=o, stderr=e) |
|---|
| 85 | p.communicate() |
|---|
| 86 | n = p.returncode |
|---|
| 87 | else: |
|---|
| 88 | n = -1 |
|---|
| 89 | if not os.path.exists(fi) or n != 0: |
|---|
| 90 | env['fieldsets'] = {} |
|---|
| 91 | env['msg'] += (" Doxygen -g Error %s\n" % n) |
|---|
| 92 | env['trace'] = file(fr).read() |
|---|
| 93 | else: |
|---|
| 94 | # Read std Doxyfile and report old choices in it |
|---|
| 95 | # split in fieldsets |
|---|
| 96 | fieldsets = OrderedDict() |
|---|
| 97 | sets = OrderedDict() |
|---|
| 98 | prev = first = '' |
|---|
| 99 | inputs = analyze_doxyfile(base_path, default_doc, input, fi, old, log) |
|---|
| 100 | for k, s in inputs.items(): |
|---|
| 101 | if s['explain']: |
|---|
| 102 | if prev and sets: |
|---|
| 103 | log.debug("fieldset %s first '%s'", prev, first) |
|---|
| 104 | fieldsets[prev] = display_doxyfile(prev, first, sets) |
|---|
| 105 | sets = OrderedDict() |
|---|
| 106 | prev = s['explain'] |
|---|
| 107 | first = s['value'].strip() |
|---|
| 108 | sets[k] = s |
|---|
| 109 | if prev and sets: |
|---|
| 110 | fieldsets[prev] = display_doxyfile(prev, first, sets) |
|---|
| 111 | env['fieldsets'] = fieldsets |
|---|
| 112 | |
|---|
| 113 | # try, don't cry |
|---|
| 114 | try: |
|---|
| 115 | os.unlink(fi) |
|---|
| 116 | os.unlink(fr) |
|---|
| 117 | os.unlink(fo) |
|---|
| 118 | except (IOError, OSError): |
|---|
| 119 | log.debug("forget temporary files") |
|---|
| 120 | |
|---|
| 121 | return env |
|---|
| 122 | |
|---|
| 123 | |
|---|
| 124 | def apply_doxyfile(req_args, doxygen, doxygen_args, doxyfile, input, |
|---|
| 125 | path_trac): |
|---|
| 126 | """ |
|---|
| 127 | Save the POST data in the Doxyfile, and execute doxygen from the INPUT |
|---|
| 128 | dir, since the EXCLUDE option is computed relative to the execution path. |
|---|
| 129 | Return a dict with an empty "msg" field if ok, an error message otherwise. |
|---|
| 130 | Warning: Doxygen does not check if the Dot utility exit on error, |
|---|
| 131 | so an empty message doest not always mean the execution is ok. |
|---|
| 132 | The "trace" field mention the possible Dot errors. |
|---|
| 133 | It also contains the Doxygen warnings. |
|---|
| 134 | """ |
|---|
| 135 | |
|---|
| 136 | f = open(doxyfile, 'w') |
|---|
| 137 | i = 0 |
|---|
| 138 | for k in req_args: |
|---|
| 139 | if not re.match(r'''^[A-Z]''', k): |
|---|
| 140 | continue |
|---|
| 141 | if req_args.get(k): |
|---|
| 142 | s = req_args.get(k) |
|---|
| 143 | else: |
|---|
| 144 | s = '' |
|---|
| 145 | o = "#\n" + k + '=' + s + "\n" |
|---|
| 146 | f.write(o.encode('utf8')) |
|---|
| 147 | i += 1 |
|---|
| 148 | |
|---|
| 149 | f.close() |
|---|
| 150 | fo = os.path.join(path_trac, 'doxygen.out') |
|---|
| 151 | o = open(fo, 'w') |
|---|
| 152 | fr = os.path.join(path_trac, 'doxygen.err') |
|---|
| 153 | e = open(fr, 'w') |
|---|
| 154 | if doxygen_args: |
|---|
| 155 | arg = doxygen_args |
|---|
| 156 | else: |
|---|
| 157 | arg = doxyfile |
|---|
| 158 | |
|---|
| 159 | dir_ = req_args.get('INPUT') if req_args.get('INPUT') else input |
|---|
| 160 | if not os.path.isdir(dir_) or not os.access(dir_, os.R_OK): |
|---|
| 161 | return {'msg': 'Error: ' + dir_ + ' not R_OK', 'trace': '', |
|---|
| 162 | 'options': i} |
|---|
| 163 | p = Popen([doxygen, arg], bufsize=-1, stdout=o, stderr=e, |
|---|
| 164 | cwd=dir_ if dir_ else None) |
|---|
| 165 | p.communicate() |
|---|
| 166 | n = p.returncode |
|---|
| 167 | o.close() |
|---|
| 168 | e.close() |
|---|
| 169 | if n == 0: |
|---|
| 170 | Popen(['chmod', '-R', 'g+w', path_trac]) |
|---|
| 171 | msg = "" |
|---|
| 172 | trace = file(fo).read() + file(fr).read() |
|---|
| 173 | else: |
|---|
| 174 | msg = ("Doxygen Error %s\n" % n) |
|---|
| 175 | trace = file(fr).read() |
|---|
| 176 | os.unlink(fo) |
|---|
| 177 | os.unlink(fr) |
|---|
| 178 | return {'msg': msg, 'trace': trace, 'options': i} |
|---|
| 179 | |
|---|
| 180 | |
|---|
| 181 | def analyze_doxyfile(base_path, default_doc, input, path, old, log): |
|---|
| 182 | """ |
|---|
| 183 | Read a Doxyfile and build a Web form allowing to change its default |
|---|
| 184 | values. Text blocs between '#----' introduce new section. |
|---|
| 185 | Other text blocs are treated as the "label" tag of the "input" tag |
|---|
| 186 | described by the line matching NAME=VALUE that follows. |
|---|
| 187 | Values different form default value for a field have a different CSS |
|---|
| 188 | class. For some NAMES, value cannot be choosen freely. |
|---|
| 189 | """ |
|---|
| 190 | |
|---|
| 191 | try: |
|---|
| 192 | content = file(path).read() |
|---|
| 193 | except (IOError, OSError) as e: |
|---|
| 194 | raise TracError("Can't read doxygen content: %s" % e) |
|---|
| 195 | |
|---|
| 196 | content = to_unicode(content, 'utf-8') |
|---|
| 197 | # Initial text is about file, not form |
|---|
| 198 | c = re.match(r'''^.*?(#-----.*)''', content, re.S) |
|---|
| 199 | if c: |
|---|
| 200 | log.debug('taking "%s" last characters out of %s in %s', |
|---|
| 201 | len(c.group(1)), len(content), path) |
|---|
| 202 | content = c.group(1) |
|---|
| 203 | |
|---|
| 204 | m = re.compile( |
|---|
| 205 | r'\s*(#-+\s+#(.*?)\s+#-+)?(.*?)$\s*([A-Z][A-Z0-9_-]+)\s*=([^#]*?)$', |
|---|
| 206 | re.S | re.M) |
|---|
| 207 | s = m.findall(content) |
|---|
| 208 | log.debug('Found "%s" options in Doxyfile', len(s)) |
|---|
| 209 | options = OrderedDict() |
|---|
| 210 | for o in s: |
|---|
| 211 | u, explain, label, id_, value = o |
|---|
| 212 | atclass = default = '' |
|---|
| 213 | if id_ in old and value != old[id_]['value']: |
|---|
| 214 | value = old[id_]['value'] |
|---|
| 215 | atclass = 'changed' |
|---|
| 216 | # required for plugin to work |
|---|
| 217 | if id_ == 'SERVER_BASED_SEARCH' or id_ == 'EXTERNAL_SEARCH': |
|---|
| 218 | value = 'YES' |
|---|
| 219 | atclass = 'changed' |
|---|
| 220 | if id_ == 'OUTPUT_DIRECTORY' and base_path: |
|---|
| 221 | default = base_path + ('' if base_path[-1] == '/' else '/') |
|---|
| 222 | value = value[len(default):] |
|---|
| 223 | if value: |
|---|
| 224 | atclass = 'changed' |
|---|
| 225 | else: |
|---|
| 226 | value = default_doc |
|---|
| 227 | elif id_ == 'INPUT' and input: |
|---|
| 228 | default = input + ('' if input[-1] == '/' else '/') |
|---|
| 229 | value = value[len(default):] |
|---|
| 230 | if value: |
|---|
| 231 | atclass = 'changed' |
|---|
| 232 | elif id_ == 'STRIP_FROM_PATH' and input and not value: |
|---|
| 233 | value = input |
|---|
| 234 | atclass = 'changed' |
|---|
| 235 | elif id_ == 'PROJECT_NAME' and re.match('\s*"My Project"', value): |
|---|
| 236 | value = os.path.basename(input) |
|---|
| 237 | if not value: |
|---|
| 238 | value = os.path.basename(base_path) |
|---|
| 239 | atclass = 'changed' |
|---|
| 240 | |
|---|
| 241 | # prepare longer input tag for long default value |
|---|
| 242 | l = 20 if len(value) < 20 else len(value) + 3 |
|---|
| 243 | options[id_] = { |
|---|
| 244 | 'explain': explain, |
|---|
| 245 | 'label': label, |
|---|
| 246 | 'value': value, |
|---|
| 247 | 'default': default, |
|---|
| 248 | 'size': l, |
|---|
| 249 | 'atclass': atclass |
|---|
| 250 | } |
|---|
| 251 | return options |
|---|
| 252 | |
|---|
| 253 | |
|---|
| 254 | def display_doxyfile(prev, first, sets): |
|---|
| 255 | """ |
|---|
| 256 | Prepare the fieldset corresponding to a section in the Doxyfile. |
|---|
| 257 | If the name of the section matches "output", |
|---|
| 258 | and the first option in it is set to 'NO', |
|---|
| 259 | the fieldset will not be displayed. |
|---|
| 260 | """ |
|---|
| 261 | if re.match(r'''.*output$''', prev) and first == 'NO': |
|---|
| 262 | display = 'none' |
|---|
| 263 | else: |
|---|
| 264 | display = 'block' |
|---|
| 265 | return {'display': display, 'opt': sets} |
|---|