#!/usr/bin/python3.9 # # CDDL HEADER START # # The contents of this file are subject to the terms of the # Common Development and Distribution License (the "License"). # You may not use this file except in compliance with the License. # # You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE # or http://www.illumos.org/license/CDDL. # See the License for the specific language governing permissions # and limitations under the License. # # When distributing Covered Code, include this CDDL HEADER in each # file and include the License file at usr/src/OPENSOLARIS.LICENSE. # If applicable, add the following below this CDDL HEADER, with the # fields enclosed by brackets "[]" replaced with your own identifying # information: Portions Copyright [yyyy] [name of copyright owner] # # CDDL HEADER END # # Copyright (c) 2011, 2012, Oracle and/or its affiliates. All rights reserved. # # # userland-mangler - a file mangling utility # # A simple program to mangle files to conform to Solaris WOS or Consoldation # requirements. # import os import sys import re import subprocess import shutil import stat import pkg.fmri import pkg.manifest import pkg.actions import pkg.elf as elf attribute_oracle_table_header = """ .\\\" Oracle has added the ARC stability level to this manual page""" attribute_table_header = """ .SH ATTRIBUTES See .BR attributes (5) for descriptions of the following attributes: .sp .TS box; cbp-1 | cbp-1 l | l . ATTRIBUTE TYPE ATTRIBUTE VALUE """ attribute_table_availability = """ = Availability %s""" attribute_table_stability = """ = Stability %s""" attribute_table_footer = """ .TE .PP """ def attributes_section_text(availability, stability, modified_date): result = '' # is there anything to do? if availability is not None or stability is not None: result = attribute_oracle_table_header if modified_date is not None: result += ("\n.\\\" on %s" % modified_date) result += attribute_table_header if availability is not None: result += (attribute_table_availability % availability) if stability is not None: result += (attribute_table_stability % stability.capitalize()) result += attribute_table_footer return result notes_oracle_comment = """ .\\\" Oracle has added source availability information to this manual page""" notes_header = """ .SH NOTES """ notes_community = """ Further information about this software can be found on the open source community website at %s. """ notes_source = """ This software was built from source available at https://openindiana.org/. The original community source was downloaded from %s """ def notes_section_text(header_seen, community, source, modified_date): result = '' # is there anything to do? if community is not None or source is not None: if header_seen == False: result += notes_header result += notes_oracle_comment if modified_date is not None: result += ("\n.\\\" on %s" % modified_date) if source is not None: result += (notes_source % source) if community is not None: result += (notes_community % community) return result so_re = re.compile('^\.so.+$', re.MULTILINE) section_re = re.compile('\.SH "?([^"]+).*$', re.IGNORECASE) TH_re = re.compile('\.TH\s+(?:"[^"]+"|\S+)\s+(\S+)', re.IGNORECASE) # # mangler.man.stability = (mangler.man.stability) # mangler.man.modified_date = (mangler.man.modified-date) # mangler.man.availability = (pkg.fmri) # mangler.man.source-url = (pkg.source-url) # mangler.man.upstream-url = (pkg.upstream-url) # def mangle_manpage(manifest, action, text): # manpages must have a taxonomy defined stability = action.attrs.pop('mangler.man.stability', None) if stability is None: sys.stderr.write("ERROR: manpage action missing mangler.man.stability: %s" % action) sys.exit(1) # manpages may have a 'modified date' modified_date = action.attrs.pop('mangler.man.modified-date', None) # Rewrite the section in the .TH line to match the section in which # we're delivering it. rewrite_sect = action.attrs.pop('mangler.man.rewrite-section', 'true') attributes_written = False notes_seen = False if 'pkg.fmri' in manifest.attributes: fmri = pkg.fmri.PkgFmri(manifest.attributes['pkg.fmri']) availability = fmri.pkg_name community = None if 'info.upstream-url' in manifest.attributes: community = manifest.attributes['info.upstream-url'] source = None if 'info.source-url' in manifest.attributes: source = manifest.attributes['info.source-url'] elif 'info.repository-url' in manifest.attributes: source = manifest.attributes['info.repository-url'] # skip reference only pages if so_re.match(text) is not None: return text # tell man that we want tables (and eqn) result = "'\\\" te\n" # write the orginal data for line in text.split('\n'): match = section_re.match(line) if match is not None: section = match.group(1) if section in ['SEE ALSO', 'NOTES']: if attributes_written == False: result += attributes_section_text( availability, stability, modified_date) attributes_written = True if section == 'NOTES': notes_seen = True match = TH_re.match(line) if match and rewrite_sect.lower() == "true": # Use the section defined by the filename, rather than # the directory in which it sits. sect = os.path.splitext(action.attrs["path"])[1][1:] line = line[:match.span(1)[0]] + sect + \ line[match.span(1)[1]:] result += ("%s\n" % line) if attributes_written == False: result += attributes_section_text(availability, stability, modified_date) result += notes_section_text(notes_seen, community, source, modified_date) return result # # mangler.elf.strip_runpath = (true|false) # def mangle_elf(manifest, action, src, dest): strip_elf_runpath = action.attrs.pop('mangler.elf.strip_runpath', 'true') if strip_elf_runpath == 'false': return # # Strip any runtime linker default search path elements from the file # and replace relative paths with absolute paths # ELFEDIT = '/usr/bin/elfedit' # runtime linker default search path elements + /64 link rtld_default_dirs = [ '/lib', '/usr/lib', '/lib/64', '/usr/lib/64', '/lib/amd64', '/usr/lib/amd64', '/lib/sparcv9', '/usr/lib/sparcv9' ] runpath_re = re.compile('.+\s(RPATH|RUNPATH)\s+\S+\s+(\S+)') # Retreive the search path from the object file. Use elfedit(1) because pkg.elf only # retrieves the RUNPATH. Note that dyn:rpath and dyn:runpath return both values. # Both RPATH and RUNPATH are expected to be the same, but in an overabundand of caution, # process each element found separately. result = subprocess.Popen([ELFEDIT, '-re', 'dyn:runpath', src ], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) result.wait() if result.returncode != 0: # no RUNPATH or RPATH to potentially strip return for line in result.stdout: result = runpath_re.match(line) if result != None: element = result.group(1) original_dirs = result.group(2).split(":") keep_dirs = [] matched_dirs = [] for dir in original_dirs: if dir not in rtld_default_dirs: if dir.startswith('$ORIGIN'): path = action.attrs['path'] dirname = os.path.dirname(path) if dirname[0] != '/': dirname = '/' + dirname corrected_dir = dir.replace('$ORIGIN', dirname) corrected_dir = os.path.realpath(corrected_dir) matched_dirs.append(dir) keep_dirs.append(corrected_dir) else: keep_dirs.append(dir) else: matched_dirs.append(dir) if len(matched_dirs) != 0: # Emit an "Error" message in case someone wants to look at the build log # and fix the component build so that this is a NOP. print("Stripping %s from %s in %s" % (":".join(matched_dirs), element, src), file=sys.stderr) # Make sure that there is a destdir to copy the file into for mangling. destdir = os.path.dirname(dest) if not os.path.exists(destdir): os.makedirs(destdir) # Create a copy to mangle # Earlier the code would check that the destination file does not exist # yet, however internal library versioning can be different while the # filename remains the same. # When publishing from a non-clean prototype directory older libraries # which may be ABI incompatible would then be republished in the new # package instead of the new version. shutil.copy2(src, dest) # Make sure we do have write permission before we try to modify the file os.chmod(dest, os.stat(dest).st_mode | stat.S_IWUSR) # Mangle the copy by deleting the tag if there is nothing left to keep # or replacing the value if there is something left. elfcmd = "dyn:delete %s" % element.lower() if len(keep_dirs) > 0: elfcmd = "dyn:%s '%s'" % (element.lower(), ":".join(keep_dirs)) subprocess.call([ELFEDIT, '-e', elfcmd, dest]) # # mangler.script.file-magic = # def mangle_script(manifest, action, text): return text # # mangler.strip_cddl = false # def mangle_cddl(manifest, action, text): strip_cddl = action.attrs.pop('mangler.strip_cddl', 'false') if strip_cddl == 'false': return text cddl_re = re.compile('^[^\n]*CDDL HEADER START.+CDDL HEADER END[^\n]*$', re.MULTILINE|re.DOTALL) return cddl_re.sub('', text) def do_ctfconvert(converter, file): args = [converter, '-i', '-m', '-k', file] print(*args, file=sys.stderr) subprocess.call(args) def mangle_path(manifest, action, src, dest, ctfconvert): if elf.is_elf_object(src): if ctfconvert is not None: do_ctfconvert(ctfconvert, src) mangle_elf(manifest, action, src, dest) else: # a 'text' document (script, man page, config file, ... # We treat all documents as latin-1 text to avoid # reencoding them and loosing data ifp = open(src, 'r', encoding='latin-1') text = ifp.read() ifp.close() # remove the CDDL from files result = mangle_cddl(manifest, action, text) if 'facet.doc.man' in action.attrs: result = mangle_manpage(manifest, action, result) elif 'mode' in action.attrs and int(action.attrs['mode'], 8) & 0o111 != 0: result = mangle_script(manifest, action, result) if text != result: destdir = os.path.dirname(dest) if not os.path.exists(destdir): os.makedirs(destdir) with open(dest, 'w', encoding='latin-1') as ofp: ofp.write(result) # # mangler.bypass = (true|false) # def mangle_paths(manifest, search_paths, destination, ctfconvert): for action in manifest.gen_actions_by_type("file"): bypass = action.attrs.pop('mangler.bypass', 'false').lower() if bypass == 'true': continue path = None if 'path' in action.attrs: path = action.attrs['path'] if action.hash and action.hash != 'NOHASH': path = action.hash if not path: continue if not os.path.exists(destination): os.makedirs(destination) dest = os.path.join(destination, path) for directory in search_paths: if directory != destination: src = os.path.join(directory, path) if os.path.isfile(src): mangle_path(manifest, action, src, dest, ctfconvert) break def load_manifest(manifest_file): manifest = pkg.manifest.Manifest() manifest.set_content(pathname=manifest_file) return manifest def usage(): print("Usage: %s [-m|--manifest (file)] [-d|--search-directory (dir)] [-D|--destination (dir)] " % (sys.argv[0].split('/')[-1])) sys.exit(1) def main(): import getopt sys.stdout.flush() search_paths = [] destination = None manifests = [] ctfconvert = None try: opts, args = getopt.getopt(sys.argv[1:], "c:D:d:m:", ["ctf=", "destination=", "search-directory=", "manifest="]) except getopt.GetoptError as err: print(str(err)) usage() for opt, arg in opts: if opt in [ "-D", "--destination" ]: destination = arg elif opt in [ "-d", "--search-directory" ]: search_paths.append(arg) elif opt in [ "-m", "--manifest" ]: try: manifest = load_manifest(arg) except IOError as err: print("oops, %s: %s" % (arg, str(err))) usage() else: manifests.append(manifest) elif opt in [ "-c", "--ctf" ]: ctfconvert = arg else: usage() if destination == None: usage() for manifest in manifests: mangle_paths(manifest, search_paths, destination, ctfconvert) print(manifest) sys.exit(0) if __name__ == "__main__": main()