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