3 # doc_install.py [OPTION]... [-T] SOURCE DEST
4 # doc_install.py [OPTION]... SOURCE... DIRECTORY
5 # doc_install.py [OPTION]... -t DIRECTORY SOURCE...
7 # Copy SOURCE to DEST or multiple SOURCE files to the existing DIRECTORY,
8 # while setting permission modes. For HTML files, translate references to
9 # external documentation.
11 # Mandatory arguments to long options are mandatory for short options, too.
12 # --book-base=BASEPATH use reference BASEPATH for Devhelp book
13 # -l, --tag-base=TAGFILE\@BASEPATH use BASEPATH for references from TAGFILE (Doxygen <= 1.8.15)
14 # -l, --tag-base=s\@BASEPUB\@BASEPATH substitute BASEPATH for BASEPUB (Doxygen >= 1.8.16)
15 # -m, --mode=MODE override file permission MODE (octal)
16 # -t, --target-directory=DIRECTORY copy all SOURCE arguments into DIRECTORY
17 # -T, --no-target-directory treat DEST as normal file
18 # --glob expand SOURCE as filename glob pattern
19 # -v, --verbose enable informational messages
20 # -h, --help display help and exit
33 html_doxygen_count = 0
35 message_prefix = os.path.basename(__file__) + ':'
37 # The installed files are read and written in binary mode.
38 # All regular expressions and replacement strings must be bytes objects.
39 html_start_pattern = re.compile(rb'\s*(?:<[?!][^<]+)*<html[>\s]')
40 html_split1_pattern = re.compile(rb'''
41 \bdoxygen="([^:"]+):([^"]*)" # doxygen="(TAGFILE):(BASEPATH)"
42 \s+((?:href|src)=")\2([^"]*") # (href="|src=")BASEPATH(RELPATH")
44 html_split2_pattern = re.compile(rb'''
45 \b((?:href|src)=")([^"]+") # (href="|src=")(BASEPUB RELPATH")
48 devhelp_start_pattern = re.compile(rb'\s*(?:<[?!][^<]+)*<book\s')
49 devhelp_subst_pattern = re.compile(rb'(<book\s+[^<>]*?\bbase=")[^"]*(?=")')
53 print(message_prefix, ''.join(msg))
56 print(message_prefix, 'Error:', ''.join(msg), file=sys.stderr)
57 raise RuntimeError(''.join(msg))
59 def html_split1_func(group1, group2):
60 global html_doxygen_count
61 if group1 in tags_dict:
62 html_doxygen_count += 1
63 return tags_dict[group1]
66 def html_split2_func(group2):
67 for key in subst_dict:
68 # Don't use regular expressions here. key may contain characters
69 # that are special in regular expressions.
70 if group2.startswith(key):
71 return subst_dict[key] + group2[len(key):]
74 def install_file(in_name, out_name):
76 Copy file to destination while translating references on the fly.
78 global html_doxygen_count
80 # Some installed files are binary (e.g. .png).
81 # Read and write all files in binary mode, thus avoiding decoding/encoding errors.
82 in_basename = os.path.basename(in_name)
83 with open(in_name, mode='rb') as in_file:
84 # Read the whole file into a string buffer.
87 if (tags_dict or subst_dict) and html_start_pattern.match(buf):
88 # Probably an html file. Modify it, if appropriate.
90 # It would be possible to modify with a call to Pattern.sub() or Pattern.subn()
91 # and let a function calculate the replacement string. Example:
92 # (buf, number_of_subs) = html_split2_pattern.subn(html_subst2_func, buf)
93 # A previous Perl script does just that. However, calling a function from
94 # sub() or subn() is a slow operation. Installing doc files for a typical
95 # module such as glibmm or gtkmm takes about 8 times as long as with the
96 # present split+join solution. (Measured with python 3.9.5)
97 html_doxygen_count = 0
100 if tags_dict and b'doxygen="' in buf:
101 # Doxygen 1.8.15 and earlier stores the tag file name and BASEPATH in the html files.
102 split_buf = html_split1_pattern.split(buf)
103 for i in range(0, len(split_buf)-4, 5):
104 basepath = html_split1_func(split_buf[i+1], split_buf[i+2])
107 split_buf[i+3] += basepath
108 number_of_subs = len(split_buf) // 5
109 if number_of_subs > 0:
110 buf = b''.join(split_buf)
111 change = 'rewrote ' + str(html_doxygen_count) + ' of ' + str(number_of_subs)
113 if number_of_subs == 0 and subst_dict:
114 # Doxygen 1.8.16 and later does not store the tag file name and BASEPATH in the html files.
115 # The previous html_split1_pattern.split() won't find anything to substitute.
116 split_buf = html_split2_pattern.split(buf)
117 for i in range(2, len(split_buf), 3):
118 basepath = html_split2_func(split_buf[i])
120 split_buf[i] = basepath
121 html_doxygen_count += 1
122 number_of_subs = len(split_buf) // 3
123 if html_doxygen_count > 0:
124 buf = b''.join(split_buf)
125 if number_of_subs > 0:
126 change = 'rewrote ' + str(html_doxygen_count)
127 notice('Translating ', in_basename, ' (', change, ' references)')
129 elif g_book_base and devhelp_start_pattern.match(buf):
130 # Probably a devhelp file.
131 # Substitute new value for attribute "base" of element <book>.
132 (buf, number_of_subs) = devhelp_subst_pattern.subn(rb'\1' + g_book_base, buf, 1)
133 change = 'rewrote base path' if number_of_subs else 'base path not set'
134 notice('Translating ', in_basename, ' (', change, ')')
136 # A file that shall not be modified.
137 notice('Copying ', in_basename)
139 with open(out_name, mode='wb') as out_file:
140 # Write the whole buffer into the target file.
143 os.chmod(out_name, perm_mode)
145 def split_key_value(mapping):
147 Split TAGFILE@BASEPATH or s@BASEPUB@BASEPATH argument into key/value pair
149 (name, path) = mapping.split('@', 1)
150 if name != 's': # Doxygen 1.8.15 and earlier
152 error('Invalid base path mapping: ', mapping)
154 return (name, path, False)
155 notice('Not changing base path for tag file ', name);
157 else: # name=='s', Doxygen 1.8.16 and later
158 (name, path) = path.split('@', 1)
160 error('Invalid base path mapping: ', mapping)
162 return (name, path, True)
163 notice('Not changing base path for ', name);
165 return (None, None, None)
167 def string_to_bytes(s):
168 if isinstance(s, str):
169 return s.encode('utf-8')
172 def make_dicts(tags):
173 global tags_dict, subst_dict
181 (name, path, subst) = split_key_value(tag)
184 # Translate a local absolute path to URI.
185 path = path.replace('\\', '/').replace(' ', '%20')
186 if path.startswith('/'):
187 path = 'file://' + path
188 path = re.sub(r'^([A-Za-z]:/)', r'file:///\1', path, count=1) # Windows: C:/path
189 if not path.endswith('/'):
192 notice('Using base path ', path, ' for ', name)
193 subst_dict[string_to_bytes(name)] = string_to_bytes(path)
195 notice('Using base path ', path, ' for tag file ', name)
196 tags_dict[string_to_bytes(name)] = string_to_bytes(path)
198 def doc_install_funcargs(sources=[], target=None, book_base=None, tags=[],
199 mode=0o644, target_is_dir=True, expand_glob=False, verbose=False):
201 Copy source files to target files or target directory.
203 global g_verbose, perm_mode, g_book_base
208 g_book_base = string_to_bytes(book_base)
211 error('Target file or directory required.')
213 notice('Using base path ', book_base, ' for Devhelp book')
215 if not target_is_dir:
217 error('Filename globbing requires target directory.')
218 if len(sources) != 1:
219 error('Only one source file allowed when target is a filename.')
221 install_file(sources[0], target)
225 expanded_sources = []
226 for source in sources:
227 expanded_sources += glob.glob(source)
228 sources = expanded_sources
231 for source in sources:
232 basename = os.path.basename(source)
234 # If there are multiple files with the same base name in the list, only
235 # the first one will be installed. This behavior makes it very easy to
236 # implement a VPATH search for each individual file.
237 if basename not in basename_set:
238 basename_set.add(basename)
239 out_name = os.path.join(target, basename)
240 install_file(source, out_name)
243 def doc_install_cmdargs(args=None):
245 Parse command line parameters, or a sequence of strings equal to
246 command line parameters. Then copy source files to target file or
251 parser = argparse.ArgumentParser(
252 formatter_class=argparse.RawTextHelpFormatter,
253 prog=os.path.basename(__file__),
255 %(prog)s [OPTION]... [-T] SOURCE DEST
256 %(prog)s [OPTION]... SOURCE... DIRECTORY
257 %(prog)s [OPTION]... -t DIRECTORY SOURCE...''',
259 Copy SOURCE to DEST or multiple SOURCE files to the existing DIRECTORY,
260 while setting permission modes. For HTML files, translate references to
261 external documentation.'''
263 parser.add_argument('--book-base', dest='book_base', metavar='BASEPATH',
264 help='use reference BASEPATH for Devhelp book')
265 parser.add_argument('-l', '--tag-base', action='append', dest='tags', metavar='SUBST',
266 help='''TAGFILE@BASEPATH use BASEPATH for references from TAGFILE (Doxygen <= 1.8.15)
267 s@BASEPUB@BASEPATH substitute BASEPATH for BASEPUB (Doxygen >= 1.8.16)'''
269 parser.add_argument('-m', '--mode', dest='mode', metavar='MODE', default='0o644',
270 help='override file permission MODE (octal)')
272 group = parser.add_mutually_exclusive_group()
273 group.add_argument('-t', '--target-directory', dest='target_dir', metavar='DIRECTORY',
274 help='copy all SOURCE arguments into DIRECTORY')
275 group.add_argument('-T', '--no-target-directory', action='store_false', dest='target_is_dir',
276 help='treat DEST as normal file')
278 parser.add_argument('--glob', action='store_true', dest='expand_glob',
279 help='expand SOURCE as filename glob pattern')
280 parser.add_argument('-v', '--verbose', action='store_true', dest='verbose',
281 help='enable informational messages')
282 parser.add_argument('source_dest', nargs='+',
287 parsed_args = parser.parse_args(args)
289 if not parsed_args.target_is_dir:
290 if len(parsed_args.source_dest) != 2:
291 error('Source and destination filenames expected.')
292 sources = [parsed_args.source_dest[0]]
293 target = parsed_args.source_dest[1]
295 target = parsed_args.target_dir
297 if len(parsed_args.source_dest) < 2:
298 error('At least one source file and destination directory expected.')
299 target = parsed_args.source_dest[-1]
300 sources = parsed_args.source_dest[0:-1]
302 sources = parsed_args.source_dest
304 return doc_install_funcargs(
307 book_base=parsed_args.book_base,
308 tags=parsed_args.tags,
309 mode=int(parsed_args.mode, base=8),
310 target_is_dir=parsed_args.target_is_dir,
311 expand_glob=parsed_args.expand_glob,
312 verbose=parsed_args.verbose
316 if __name__ == '__main__':
317 sys.exit(doc_install_cmdargs())