]> git.tdb.fi Git - ext/sigc++-2.0.git/blob - untracked/docs/doc_install.py
Import libsigc++ 2.10.8 sources
[ext/sigc++-2.0.git] / untracked / docs / doc_install.py
1 #!/usr/bin/env python3
2
3 # doc_install.py [OPTION]... [-T] SOURCE DEST
4 # doc_install.py [OPTION]... SOURCE... DIRECTORY
5 # doc_install.py [OPTION]... -t DIRECTORY SOURCE...
6
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.
10
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
21
22 import os
23 import sys
24 import re
25 import glob
26
27 # Globals
28 g_verbose = False
29 tags_dict = {}
30 subst_dict = {}
31 perm_mode = 0o644
32 g_book_base = None
33 html_doxygen_count = 0
34
35 message_prefix = os.path.basename(__file__) + ':'
36
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")
43   ''', re.VERBOSE)
44 html_split2_pattern = re.compile(rb'''
45   \b((?:href|src)=")([^"]+") # (href="|src=")(BASEPUB RELPATH")
46   ''', re.VERBOSE)
47
48 devhelp_start_pattern = re.compile(rb'\s*(?:<[?!][^<]+)*<book\s')
49 devhelp_subst_pattern = re.compile(rb'(<book\s+[^<>]*?\bbase=")[^"]*(?=")')
50
51 def notice(*msg):
52   if g_verbose:
53     print(message_prefix, ''.join(msg))
54
55 def error(*msg):
56   print(message_prefix, 'Error:', ''.join(msg), file=sys.stderr)
57   raise RuntimeError(''.join(msg))
58
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]
64   return group2
65
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):]
72   return None
73
74 def install_file(in_name, out_name):
75   '''
76   Copy file to destination while translating references on the fly.
77   '''
78   global html_doxygen_count
79
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.
85     buf = in_file.read()
86
87   if (tags_dict or subst_dict) and html_start_pattern.match(buf):
88     # Probably an html file. Modify it, if appropriate.
89     #
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
98     number_of_subs = 0
99     change = 'no'
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])
105         split_buf[i+1] = b''
106         split_buf[i+2] = b''
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)
112
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])
119         if basepath:
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)')
128
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, ')')
135   else:
136     # A file that shall not be modified.
137     notice('Copying ', in_basename)
138
139   with open(out_name, mode='wb') as out_file:
140     # Write the whole buffer into the target file.
141     out_file.write(buf)
142
143   os.chmod(out_name, perm_mode)
144
145 def split_key_value(mapping):
146   '''
147   Split TAGFILE@BASEPATH or s@BASEPUB@BASEPATH argument into key/value pair
148   '''
149   (name, path) = mapping.split('@', 1)
150   if name != 's': # Doxygen 1.8.15 and earlier
151     if not name:
152       error('Invalid base path mapping: ', mapping)
153     if path != None:
154       return (name, path, False)
155     notice('Not changing base path for tag file ', name);
156
157   else: # name=='s', Doxygen 1.8.16 and later
158     (name, path) = path.split('@', 1)
159     if not name:
160       error('Invalid base path mapping: ', mapping)
161     if path != None:
162       return (name, path, True)
163     notice('Not changing base path for ', name);
164
165   return (None, None, None)
166
167 def string_to_bytes(s):
168   if isinstance(s, str):
169     return s.encode('utf-8')
170   return s # E.g. None
171
172 def make_dicts(tags):
173   global tags_dict, subst_dict
174
175   tags_dict = {}
176   subst_dict = {}
177   if not tags:
178     return
179
180   for tag in tags:
181     (name, path, subst) = split_key_value(tag)
182     if subst == None:
183       continue
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('/'):
190       path += '/'
191     if subst:
192       notice('Using base path ', path, ' for ', name)
193       subst_dict[string_to_bytes(name)] = string_to_bytes(path)
194     else:
195       notice('Using base path ', path, ' for tag file ', name)
196       tags_dict[string_to_bytes(name)] = string_to_bytes(path)
197
198 def doc_install_funcargs(sources=[], target=None, book_base=None, tags=[],
199   mode=0o644, target_is_dir=True, expand_glob=False, verbose=False):
200   '''
201   Copy source files to target files or target directory.
202   '''
203   global g_verbose, perm_mode, g_book_base
204
205   g_verbose = verbose
206   perm_mode = mode
207   make_dicts(tags)
208   g_book_base = string_to_bytes(book_base)
209
210   if not target:
211     error('Target file or directory required.')
212   if book_base:
213     notice('Using base path ', book_base, ' for Devhelp book')
214
215   if not target_is_dir:
216     if expand_glob:
217       error('Filename globbing requires target directory.')
218     if len(sources) != 1:
219       error('Only one source file allowed when target is a filename.')
220
221     install_file(sources[0], target)
222     return 0
223
224   if expand_glob:
225     expanded_sources = []
226     for source in sources:
227       expanded_sources += glob.glob(source)
228     sources = expanded_sources
229
230   basename_set = set()
231   for source in sources:
232     basename = os.path.basename(source)
233
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)
241   return 0
242
243 def doc_install_cmdargs(args=None):
244   '''
245   Parse command line parameters, or a sequence of strings equal to
246   command line parameters. Then copy source files to target file or
247   target directory.
248   '''
249   import argparse
250
251   parser = argparse.ArgumentParser(
252     formatter_class=argparse.RawTextHelpFormatter,
253     prog=os.path.basename(__file__),
254     usage='''
255       %(prog)s [OPTION]... [-T] SOURCE DEST
256       %(prog)s [OPTION]... SOURCE... DIRECTORY
257       %(prog)s [OPTION]... -t DIRECTORY SOURCE...''',
258     description='''
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.'''
262   )
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)'''
268   )
269   parser.add_argument('-m', '--mode', dest='mode', metavar='MODE', default='0o644',
270     help='override file permission MODE (octal)')
271
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')
277
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='+',
283     help='''SOURCE DEST
284 SOURCE... DIRECTORY
285 SOURCE...'''
286   )
287   parsed_args = parser.parse_args(args)
288
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]
294   else:
295     target = parsed_args.target_dir
296     if not target:
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]
301     else:
302       sources = parsed_args.source_dest
303
304   return doc_install_funcargs(
305     sources=sources,
306     target=target,
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
313   )
314
315 # ----- Main -----
316 if __name__ == '__main__':
317   sys.exit(doc_install_cmdargs())