Alexander Pyhalov
2018-12-05 db2ae300899858fdda044a86f0b84920de6b391e
commit | author | age
5cc93f 1 #!/usr/bin/python2.7
4158c0 2 #
NJ 3 # CDDL HEADER START
4 #
5 # The contents of this file are subject to the terms of the
6 # Common Development and Distribution License (the "License").
7 # You may not use this file except in compliance with the License.
8 #
9 # You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
f1351e 10 # or http://www.illumos.org/license/CDDL.
4158c0 11 # See the License for the specific language governing permissions
NJ 12 # and limitations under the License.
13 #
14 # When distributing Covered Code, include this CDDL HEADER in each
15 # file and include the License file at usr/src/OPENSOLARIS.LICENSE.
16 # If applicable, add the following below this CDDL HEADER, with the
17 # fields enclosed by brackets "[]" replaced with your own identifying
18 # information: Portions Copyright [yyyy] [name of copyright owner]
19 #
20 # CDDL HEADER END
21 #
90e68e 22 # Copyright (c) 2011, 2012, Oracle and/or its affiliates. All rights reserved.
4158c0 23 #
NJ 24 #
25 # userland-mangler - a file mangling utility
26 #
27 #  A simple program to mangle files to conform to Solaris WOS or Consoldation
28 #  requirements.
29 #
30
31 import os
32 import sys
33 import re
df9898 34 import subprocess
NJ 35 import shutil
36
4158c0 37
NJ 38 import pkg.fmri
39 import pkg.manifest
40 import pkg.actions
41 import pkg.elf as elf
90e68e 42
MS 43 attribute_oracle_table_header = """
44 .\\\" Oracle has added the ARC stability level to this manual page"""
4158c0 45
NJ 46 attribute_table_header = """
47 .SH ATTRIBUTES
48 See
49 .BR attributes (5)
50 for descriptions of the following attributes:
51 .sp
52 .TS
53 box;
54 cbp-1 | cbp-1
55 l | l .
56 ATTRIBUTE TYPE    ATTRIBUTE VALUE """
57
58 attribute_table_availability = """
59 =
60 Availability    %s"""
61
62 attribute_table_stability = """
63 =
64 Stability    %s"""
65
66 attribute_table_footer = """
404117 67 .TE
4158c0 68 .PP
NJ 69 """
90e68e 70 def attributes_section_text(availability, stability, modified_date):
2720d6 71     result = ''
NJ 72
4158c0 73     # is there anything to do?
2720d6 74     if availability is not None or stability is not None:
90e68e 75         result = attribute_oracle_table_header
MS 76         if modified_date is not None:
77             result += ("\n.\\\" on %s" % modified_date)
78         result += attribute_table_header
4158c0 79
2720d6 80         if availability is not None:
NJ 81             result += (attribute_table_availability % availability)
82         if stability is not None:
83             result += (attribute_table_stability % stability.capitalize())
84         result += attribute_table_footer
4158c0 85
2720d6 86     return result
90e68e 87
MS 88 notes_oracle_comment = """
89 .\\\" Oracle has added source availability information to this manual page"""
4158c0 90
NJ 91 notes_header = """
92 .SH NOTES
93 """
94
95 notes_community = """
96 Further information about this software can be found on the open source community website at %s.
97 """
98 notes_source = """
f1351e 99 This software was built from source available at https://openindiana.org/.  The original community source was downloaded from  %s
4158c0 100 """
NJ 101
90e68e 102 def notes_section_text(header_seen, community, source, modified_date):
2720d6 103     result = ''
NJ 104
4158c0 105     # is there anything to do?
2720d6 106     if community is not None or source is not None:
NJ 107         if header_seen == False:
108             result += notes_header
90e68e 109         result += notes_oracle_comment
MS 110         if modified_date is not None:
111             result += ("\n.\\\" on %s" % modified_date)
2720d6 112         if source is not None:
NJ 113             result += (notes_source % source)
114         if community is not None:
115             result += (notes_community % community)
4158c0 116
2720d6 117     return result
4158c0 118
2720d6 119 so_re = re.compile('^\.so.+$', re.MULTILINE)
4158c0 120 section_re = re.compile('\.SH "?([^"]+).*$', re.IGNORECASE)
fbf173 121 TH_re = re.compile('\.TH\s+(?:"[^"]+"|\S+)\s+(\S+)', re.IGNORECASE)
4158c0 122 #
NJ 123 # mangler.man.stability = (mangler.man.stability)
90e68e 124 # mangler.man.modified_date = (mangler.man.modified-date)
4158c0 125 # mangler.man.availability = (pkg.fmri)
1de4f0 126 # mangler.man.source-url = (pkg.source-url)
MS 127 # mangler.man.upstream-url = (pkg.upstream-url)
4158c0 128 #
2720d6 129 def mangle_manpage(manifest, action, text):
4158c0 130     # manpages must have a taxonomy defined
NJ 131     stability = action.attrs.pop('mangler.man.stability', None)
132     if stability is None:
133         sys.stderr.write("ERROR: manpage action missing mangler.man.stability: %s" % action)
134         sys.exit(1)
90e68e 135
MS 136     # manpages may have a 'modified date'
137     modified_date = action.attrs.pop('mangler.man.modified-date', None)
fbf173 138
AP 139     # Rewrite the section in the .TH line to match the section in which
140     # we're delivering it.
141     rewrite_sect = action.attrs.pop('mangler.man.rewrite-section', 'true')
4158c0 142
NJ 143     attributes_written = False
144     notes_seen = False
145
146     if 'pkg.fmri' in manifest.attributes:
147         fmri = pkg.fmri.PkgFmri(manifest.attributes['pkg.fmri'])
148         availability = fmri.pkg_name
149
dc18f2 150     community = None
1de4f0 151     if 'info.upstream-url' in manifest.attributes:
MS 152         community = manifest.attributes['info.upstream-url']
4158c0 153
dc18f2 154     source = None
1de4f0 155     if 'info.source-url' in manifest.attributes:
MS 156         source = manifest.attributes['info.source-url']
157     elif 'info.repository-url' in manifest.attributes:
158         source = manifest.attributes['info.repository-url']
4158c0 159
NJ 160     # skip reference only pages
2720d6 161     if so_re.match(text) is not None:
NJ 162         return text
4158c0 163
NJ 164     # tell man that we want tables (and eqn)
2720d6 165     result = "'\\\" te\n"
4158c0 166
NJ 167     # write the orginal data
2720d6 168     for line in text.split('\n'):
4158c0 169         match = section_re.match(line)
NJ 170         if match is not None:
171             section = match.group(1)
172             if section in ['SEE ALSO', 'NOTES']:
173                 if attributes_written == False:
2720d6 174                     result += attributes_section_text(
4158c0 175                                  availability,
90e68e 176                                  stability,
MS 177                                  modified_date)
4158c0 178                     attributes_written = True
NJ 179                 if section == 'NOTES':
180                     notes_seen = True
fbf173 181             match = TH_re.match(line)
AP 182             if match and rewrite_sect.lower() == "true":
183                 # Use the section defined by the filename, rather than
184                 # the directory in which it sits.
185                 sect = os.path.splitext(action.attrs["path"])[1][1:]
186                 line = line[:match.span(1)[0]] + sect + \
187                     line[match.span(1)[1]:]
188
2720d6 189         result += ("%s\n" % line)
4158c0 190
NJ 191     if attributes_written == False:
90e68e 192         result += attributes_section_text(availability, stability,
MS 193             modified_date)
4158c0 194
90e68e 195     result += notes_section_text(notes_seen, community, source,
MS 196         modified_date)
4158c0 197
2720d6 198     return result
4158c0 199
NJ 200 #
df9898 201 # mangler.elf.strip_runpath = (true|false)
4158c0 202 #
NJ 203 def mangle_elf(manifest, action, src, dest):
df9898 204     strip_elf_runpath = action.attrs.pop('mangler.elf.strip_runpath', 'true')
NJ 205     if strip_elf_runpath is 'false':
206         return
207
208     #
209     # Strip any runtime linker default search path elements from the file
db2ae3 210     # and replace relative paths with absolute paths
df9898 211     #
NJ 212     ELFEDIT = '/usr/bin/elfedit'
213
214     # runtime linker default search path elements + /64 link
215     rtld_default_dirs = [ '/lib', '/usr/lib',
216                   '/lib/64', '/usr/lib/64',
217                   '/lib/amd64', '/usr/lib/amd64',
218                   '/lib/sparcv9', '/usr/lib/sparcv9' ]
219
220     runpath_re = re.compile('.+\s(RPATH|RUNPATH)\s+\S+\s+(\S+)')
221
222     # Retreive the search path from the object file.  Use elfedit(1) because pkg.elf only
223     # retrieves the RUNPATH.  Note that dyn:rpath and dyn:runpath return both values.
224     # Both RPATH and RUNPATH are expected to be the same, but in an overabundand of caution,
225     # process each element found separately.
226     result = subprocess.Popen([ELFEDIT, '-re', 'dyn:runpath', src ],
227                   stdout=subprocess.PIPE, stderr=subprocess.PIPE)
228         result.wait()
229     if result.returncode != 0:    # no RUNPATH or RPATH to potentially strip
230         return
231
232     for line in result.stdout:
233         result = runpath_re.match(line)
234         if result != None:
235             element = result.group(1)
236             original_dirs = result.group(2).split(":")
237             keep_dirs = []
238             matched_dirs = []
239
240             for dir in original_dirs:
241                 if dir not in rtld_default_dirs:
db2ae3 242                     if dir.startswith('$ORIGIN'):
AP 243                         path = action.attrs['path']
244                         dirname = os.path.dirname(path)
245                         if dirname[0] != '/':
246                             dirname = '/' + dirname
247                         corrected_dir = dir.replace('$ORIGIN', dirname)
248                         corrected_dir = os.path.realpath(corrected_dir)
249                         matched_dirs.append(dir)
250                         keep_dirs.append(corrected_dir)
251                                     else:
252                         keep_dirs.append(dir)
df9898 253                 else:
NJ 254                     matched_dirs.append(dir)
255
256             if len(matched_dirs) != 0:
257                 # Emit an "Error" message in case someone wants to look at the build log
258                 # and fix the component build so that this is a NOP.
259                 print >>sys.stderr, "Stripping %s from %s in %s" % (":".join(matched_dirs), element, src)
260
261                 # Make sure that there is a destdir to copy the file into for mangling.
262                 destdir = os.path.dirname(dest)
263                 if not os.path.exists(destdir):
264                     os.makedirs(destdir)
265                 # Create a copy to mangle if it doesn't exist yet.
266                 if os.path.isfile(dest) == False:
267                     shutil.copy2(src, dest)
268
269                 # Mangle the copy by deleting the tag if there is nothing left to keep
270                 # or replacing the value if there is something left.
271                 elfcmd = "dyn:delete %s" % element.lower()
272                 if len(keep_dirs) > 0:
273                     elfcmd = "dyn:%s '%s'" % (element.lower(), ":".join(keep_dirs))
274                 subprocess.call([ELFEDIT, '-e', elfcmd, dest])
4158c0 275
NJ 276 #
277 # mangler.script.file-magic =
278 #
2720d6 279 def mangle_script(manifest, action, text):
NJ 280     return text
281
282 #
283 # mangler.strip_cddl = false
284 #
285 def mangle_cddl(manifest, action, text):
286     strip_cddl = action.attrs.pop('mangler.strip_cddl', 'true')
287     if strip_cddl is 'false':
288         return text
289     cddl_re = re.compile('^[^\n]*CDDL HEADER START.+CDDL HEADER END[^\n]*$',
290                  re.MULTILINE|re.DOTALL)
291     return cddl_re.sub('', text)
4158c0 292
NJ 293 def mangle_path(manifest, action, src, dest):
2720d6 294     if elf.is_elf_object(src):
NJ 295         mangle_elf(manifest, action, src, dest)
296     else:
297         # a 'text' document (script, man page, config file, ...
298         ifp = open(src, 'r')
299         text = ifp.read()
300         ifp.close()
301
302         # remove the CDDL from files
303         result = mangle_cddl(manifest, action, text)
304
305         if 'facet.doc.man' in action.attrs:
306              result = mangle_manpage(manifest, action, result)
307         elif 'mode' in action.attrs and int(action.attrs['mode'], 8) & 0111 != 0:
308             result = mangle_script(manifest, action, result)
309
310         if text != result:
311             destdir = os.path.dirname(dest)
312             if not os.path.exists(destdir):
313                 os.makedirs(destdir)
314             with open(dest, 'w') as ofp:
315                 ofp.write(result)
4158c0 316
NJ 317 #
318 # mangler.bypass = (true|false)
319 #
320 def mangle_paths(manifest, search_paths, destination):
321     for action in manifest.gen_actions_by_type("file"):
322         bypass = action.attrs.pop('mangler.bypass', 'false').lower()
323         if bypass == 'true':
324             continue
325
326         path = None
327         if 'path' in action.attrs:
328             path = action.attrs['path']
329         if action.hash and action.hash != 'NOHASH':
330             path = action.hash
331         if not path:
332             continue
333
2720d6 334         if not os.path.exists(destination):
NJ 335             os.makedirs(destination)
336
4158c0 337         dest = os.path.join(destination, path)
NJ 338         for directory in search_paths:
339             if directory != destination:
340                 src = os.path.join(directory, path)
b0fde7 341                 if os.path.isfile(src):
4158c0 342                     mangle_path(manifest, action, src, dest)
NJ 343                     break
344
345 def load_manifest(manifest_file):
346     manifest = pkg.manifest.Manifest()
347     manifest.set_content(pathname=manifest_file)
348
349     return manifest
350
351 def usage():
352     print "Usage: %s [-m|--manifest (file)] [-d|--search-directory (dir)] [-D|--destination (dir)] " % (sys.argv[0].split('/')[-1])
353     sys.exit(1)
354
355 def main():
356     import getopt
357
404117 358     # FLUSH STDOUT
4158c0 359     sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0)
NJ 360
361     search_paths = []
362     destination = None
363     manifests = []
364
365     try:
366         opts, args = getopt.getopt(sys.argv[1:], "D:d:m:",
367             ["destination=", "search-directory=", "manifest="])
368     except getopt.GetoptError, err:
369         print str(err)
370         usage()
371
372     for opt, arg in opts:
373         if opt in [ "-D", "--destination" ]:
374             destination = arg
375         elif opt in [ "-d", "--search-directory" ]:
376             search_paths.append(arg)
377         elif opt in [ "-m", "--manifest" ]:
378             try:
379                 manifest = load_manifest(arg)
380             except IOError, err:
381                 print "oops, %s: %s" % (arg, str(err))
382                 usage()
383             else:
384                 manifests.append(manifest)
385         else:
386             usage()
387
388     if destination == None:
389         usage()
390
391     for manifest in manifests:
392         mangle_paths(manifest, search_paths, destination)
393         print manifest
394
395     sys.exit(0)
396
397 if __name__ == "__main__":
398     main()