Source code for abimap.symver

from __future__ import print_function

import argparse
import logging
import os
import re
import shutil
import sys
from itertools import chain

from ._version import __version__

VERBOSITY_MAP = {"debug": logging.DEBUG,
                 "info": logging.INFO,
                 "warning": logging.WARNING,
                 "error": logging.ERROR,
                 "quiet": logging.CRITICAL}


###############################################################################
# Classes
###############################################################################

[docs]class Single_Logger(object): """ A singleton logger for the module This class is a singleton logger factory. It takes advantage of the uniqueness of class attributes to hold a unique instance of the logger for the module. It logs to the default log output, and prints WARNING and ERROR messages to stderr. It allows the caller to provide a file to receive the log (the messages will be logged by all handlers: to stderr if WARNING or ERROR, to default log, and to the provided file) Attributes: __instance: Holds the unique instance given by the factory when called. """ __instance = None
[docs] @classmethod def getLogger(cls, name, filename=None): """ Get the unique instance of the logger :param name: The name of the module (usually just __name__) :returns: An instance of logging.Logger """ if Single_Logger.__instance is None: # Get logger logger = logging.getLogger(name) # Setup a handler to print warnings and above to stderr console_handler = logging.StreamHandler() console_handler.setLevel(logging.WARNING) console_format = "[%(levelname)s] %(message)s" console_formatter = logging.Formatter(console_format) console_handler.setFormatter(console_formatter) logger.addHandler(console_handler) Single_Logger.__instance = logger if filename: # If a new logfile is added, a handler is added file_handler = logging.FileHandler(filename) file_format = "[%(levelname)s] (%(asctime)s) in"\ " %(filename)s, line %(lineno)d:"\ " %(message)s" file_formatter = logging.Formatter(file_format) file_handler.setFormatter(file_formatter) Single_Logger.__instance.addHandler(file_handler) return Single_Logger.__instance
[docs]class ParserError(Exception): """ Exception type raised by the map parser Used mostly to keep track where an error was found in the given file Attributes: filename: The name (path) of the file being parsed context: The line where the error was detected line: The index of the line where the error was detected column: The index of the column where the error was detected message: The error message """ def __str__(self): content = ("In file {0.filename}, line {1}, column {0.column}: " "{0.message}\n" "{0.context}" "{2:>{0.column}}").format(self, self.line + 1, '^') return content def __init__(self, filename, context, line, column, message): """ The constructor :param filename: The name (path) of the file being parsed :param context: The line where the error was detected :param line: The index of the line where the error was detected :param column: The index of the column where the error was detected :param message: The error message """ self.filename = filename self.context = context self.line = line self.column = column self.message = message
[docs]class Map(object): """ A linker map (version script) representation This class is an internal representation of a version script. It is intended to be initialized by calling the method ``read()`` and passing the path to a version script file. The parser will parse the file and check the file syntax, creating a list of releases (instances of the ``Release`` class), which is stored in ``releases``. Attributes: init: Indicates if the object was initialized by calling ``read()`` logger: The logger object; can be specified in the constructor filename: Holds the name (path) of the file read lines: A list containing the lines of the file """ # To make printable def __str__(self): """ Print the map in a usable form for the linker :returns: A string containing the whole map file as it would be written in a file """ content = "".join((str(release) + "\n" for release in self.releases if release)) return content # Constructor def __init__(self, filename=None, logger=None): """ The constructor. :param filename: The name of the file to be read. If provided the ``read()`` method is called using this name. :param logger: A logger object. If not provided, the module based logger will be used """ # The state self.init = False self.releases = [] # Logging self.logger = Single_Logger.getLogger(__name__) # From the raw file self.filename = '' self.lines = [] if filename: self.read(filename)
[docs] def parse(self, lines): """ A simple version script parser. This is the main initializator of the ``releases`` list. This simple parser receives the lines of a given version script, check its syntax, and construct the list of releases. Some semantic aspects are checked, like the existence of the ``*`` wildcard in global scope and the existence of duplicated release names. It works by running a finite state machine: The parser states. Can be: 0. name: The parser is searching for a release name or ``EOF`` 1. opening: The parser is searching for the release opening ``{`` 2. element: The parser is searching for an identifier name or ``}`` 3. element_closer: The parser is searching for ``:`` or ``;`` 4. previous: The parser is searching for previous release name 5. previous_closer: The parser is searching for ``;`` :param lines: The lines of a version script file """ state = 0 # The list of releases parsed releases = [] last = (0, 0) for index, line in enumerate(lines): column = 0 while column < len(line): try: # Remove whitespaces or comments m = re.match(r'\s+|\s*#.*$', line[column:]) if m: column += m.end() last = (index, column) continue # Searching for a release name if state == 0: self.logger.debug(">>Name") m = re.match(r'\w+', line[column:]) if m is None: raise ParserError(self.filename, lines[last[0]], last[0], last[1], "Invalid Release identifier") else: # New release found name = m.group(0) # Check if a release with this name is present has_duplicate = [release for release in releases if release.name == name] column += m.end() r = Release() r.name = m.group(0) releases.append(r) last = (index, column) if has_duplicate: msg = "Duplicated Release identifier \'{}\'"\ .format(name) # This is non-critical, only warning self.logger.warn(ParserError(self.filename, lines[index], index, column, msg)) # Search for the special release marker comment m = re.match(r'\s*#.\s*released.*$', line[column:], re.IGNORECASE) if m: column += m.end() r.released = True last = (index, column) # Advance to the next state state += 1 continue # Searching for the '{' elif state == 1: self.logger.debug(">>Opening") found = line.find('{', column) if found < 0: raise ParserError(self.filename, lines[last[0]], last[0], last[1], "Missing \'{\'") else: column += (found + 1) v = None last = (index, column) state += 1 continue elif state == 2: self.logger.debug(">>Element") found = line.find('}', column) if found >= 0: self.logger.debug(">>Closer, jump to Previous") column += (found + 1) last = (index, column) state = 4 continue m = re.match(r'\w+|\*', line[column:]) if m is None: raise ParserError(self.filename, lines[last[0]], last[0], last[1], "Invalid identifier") else: # In this case the position before the # identifier is stored last = (index, m.start()) column += m.end() identifier = m.group(0) state += 1 continue elif state == 3: self.logger.debug(">>Element closer") found = line.find(';', column) if found < 0: # It was not Symbol. Maybe a new visibility. found = line.find(':', column) if found != column: msg = "Missing \';\' or \':\' after"" \'{0}\'"\ .format(identifier) # In this case the current position is used raise ParserError(self.filename, lines[index], index, column, msg) else: # New visibility found if identifier in r.symbols: v = r.symbols[identifier] else: v = [] r.symbols[identifier] = v column += (found + 1) last = (index, column) state = 2 continue elif found == column: if v is None: # There was no open visibility scope v = [] r.symbols['global'] = v msg = "Missing visibility scope before"\ " \'{0}\'. Symbols considered in"\ " 'global:\'".format(identifier) # Non-critical, only warning self.logger.warn(ParserError(self.filename, lines[last[0]], last[0], last[1], msg)) else: # Symbol found v.append(identifier) column += (found + 1) last = (index, column) # Move back the state to find elements state = 2 continue else: msg = "Missing \';\' or \':\' after"" \'{0}\'"\ .format(identifier) # In this case the current position is used raise ParserError(self.filename, lines[index], index, column, msg) elif state == 4: self.logger.debug(">>Previous") found = line.find(";", column) if found == column: self.logger.debug(">>Empty previous") column += (found + 1) last = (index, column) # Move back the state to find other releases state = 0 continue m = re.match(r'\w+', line[column:]) if m is None: raise ParserError(self.filename, lines[last[0]], last[0], last[1], "Invalid identifier") else: # Found previous release identifier column += m.end() identifier = m.group(0) last = (index, column) state += 1 continue elif state == 5: self.logger.debug(">>Previous closer") found = line.find(";", column) if found < 0: raise ParserError(self.filename, lines[last[0]], last[0], last[1], "Missing \';\'") elif found == column: # Found previous closer column += (found + 1) r.previous = identifier last = (index, column) # Move back the state to find other releases state = 0 continue else: raise ParserError(self.filename, lines[index], index, column, "Unexpected character") except ParserError as e: # Any exception raised is considered an error self.logger.error(e) raise e # Store the parsed releases self.releases = releases
[docs] def read(self, filename): """ Read a linker map file (version script) and store the obtained releases Obtain the lines of the file and calls ``parse()`` to parse the file :param filename: The path to the file to be read :raises ParserError: Raised when a syntax error is found in the file """ with open(filename, "r") as f: self.lines = f.readlines() self.filename = filename self.parse(self.lines) # Check the map read self.check()
[docs] def all_global_symbols(self): """ Returns all global symbols from all releases contained in the Map object :returns: A set containing all global symbols in all releases """ if not self.init: msg = "Map not checked, run check()" self.logger.error(msg) raise Exception(msg) symbols = [] for release in self.releases: if 'global' in release.symbols: symbols.extend(release.symbols['global']) return set(symbols)
[docs] def duplicates(self): """ Find and return a list of duplicated symbols for each release If no duplicates are found, return an empty list :returns: A list of tuples [(release, [(scope, [duplicates])])] """ duplicates = [] for release in self.releases: rel_dup = release.duplicates() if rel_dup: duplicates.append((release.name, rel_dup)) return duplicates
[docs] def dependencies(self): """ Construct the dependencies lists Contruct a list of dependency lists. Each dependency list contain the names of the releases in a dependency path. The heads of the dependencies lists are the releases not refered as a previous release in any release. :returns: A list containing the dependencies lists """ def get_dependency(releases, head): found = [release for release in releases if release.name == head] if not found: msg = "Release \'{0}\' not found".format(head) self.logger.error(msg) raise Exception(msg) if len(found) > 1: msg = "defined more than 1 release \'{0}\'".format(head) self.logger.error(msg) raise Exception(msg) return found[0].previous solved = set() deps = [] for release in self.releases: # If the dependencies of the current release were resolved, skip if release.name in solved: continue else: current = [release.name] dep = release.previous # Construct the current release dependency list while dep: # If the found dependency was already in the list if dep in current: msg = ("Circular dependency detected!\n" " {0}".format("->".join(chain(current, [dep])))) self.logger.error(msg) raise Exception(msg) # Append the dependency to the current list current.append(dep) # Remove the releases that are not heads from the list if dep in solved: deps = [i for i in deps if i[0] != dep] else: solved.add(dep) dep = get_dependency(self.releases, dep) solved.add(release.name) deps.append(current) return deps
[docs] def check(self): """ Check the map structure. Reports errors found in the structure of the map in form of warnings. """ if not self.releases: msg = "Empty map" self.logger.error(msg) raise Exception(msg) have_wildcard = [] seems_base = [] # Find duplicated symbols d = self.duplicates() if d: for release, duplicates in d: self.logger.warn("Duplicates found in release \'%s\':", release) for scope, symbols in duplicates: self.logger.warn(" %s:", scope) self.logger.warn("\n".join( (" " * 8 + symbol for symbol in symbols))) # Check '*' wildcard usage for release in self.releases: for scope, symbols in release.symbols.items(): if scope == 'local': if symbols: if "*" in symbols: self.logger.info("%s contains the local \'*\'" " wildcard", release.name) if release.previous: # Predecessor version and local: *; are present self.logger.warn("%s should not contain the" " local wildcard because it" " is not the base version" " (it refers to version %s" " as its predecessor)", release.name, release.previous) else: # Release seems to be base: empty predecessor msg = "{} seems to be the base version"\ .format(release.name) self.logger.info(msg) seems_base.append(release.name) # Append to the list of releases which contain the # wildcard '*' have_wildcard.append((release.name, scope)) elif scope == 'global': if symbols: if "*" in symbols: # Release contains '*' wildcard in global scope self.logger.warn("%s contains the \'*\' wildcard" " in global scope. It is probably" " exporting symbols" " it should not.", release.name) have_wildcard.append((release.name, scope)) else: # Release contains unknown visibility scopes (not global or # local) self.logger.warn("%s contains unknown scope named %s" " (different from \'global\' and" " \'local\')", release.name, scope) if have_wildcard: if len(have_wildcard) > 1: # The '*' wildcard was found in more than one place self.logger.warn("The \'*\' wildcard was found in more than" " one place:") for name, scope in have_wildcard: self.logger.warn(" %s: in \'%s\'", name, scope) else: self.logger.warn("The \'*\' wildcard was not found") if seems_base: if len(seems_base) > 1: # There is more than one release without predecessor and # containing '*' wildcard in local scope self.logger.warn("More than one release seem to be the base" " version (contain the local wildcard and" " do not have a predecessor version):") for name in seems_base: self.logger.warn(" %s", name) else: self.logger.warn("No base version release found") dependencies = self.dependencies() self.logger.info("Found dependencies:") for release in dependencies: content = "".join(chain(" " * 4, (dep + "->" for dep in release))) self.logger.info(content) # After calling a check, the map is considered initialized self.init = True
[docs] def guess_latest_release(self): """ Try to guess the latest release It uses the information found in the releases present in the version script read. It tries to find the latest release using heuristics. :returns: A list [release, prefix, suffix, version[CUR, AGE, REV]] """ if not self.init: msg = "Map not checked, run check()" self.logger.error(msg) raise Exception(msg) deps = self.dependencies() heads = (dep[0] for dep in deps) latest = [None, None, '_0_0_0', None] for release in heads: info = get_info_from_release_string(release) # This check is necessary because the suffix can be missing if info[2]: if info[2] > latest[2]: latest = info return latest
[docs] def guess_name(self, new_release, abi_break=False, guess=False): """ Use the given information to guess the name for the new release The two parts necessary to make the release name: - The new prefix: Usually the library name (e.g. LIBX) - The new suffix: The version information (e.g. _1_2_3) If the new release is not provided, try a guess strategy: If the new prefix is not provided: 1. Try to find a common prefix between release names 2. Try to find latest release If the new suffix is not provided: 1. Try to find latest release version and bump :param new_release: String, the name of the new release. If this is :param abi_break: Boolean, indicates if the ABI was broken :param guess: Boolean, indicates if should try to guess :returns: The guessed release name (new prefix + new suffix) """ new_prefix = None new_suffix = None if new_release: new_prefix = new_release[1] new_suffix = new_release[2] # If the two required parts were given, just combine and return if new_prefix: if new_suffix: self.logger.debug("[guess]: Two parts found, using them") return new_prefix.upper() + new_suffix if guess: if not new_prefix: self.logger.debug("[guess]: Trying to find common prefix") # Find a common prefix between all releases names = [release.name for release in self.releases] if names: s1 = min(names) s2 = max(names) for i, c in enumerate(s1): if c != s2[i]: break if s1[i] != s2[i]: new_prefix = s1[:i] else: new_prefix = s1 # If a common prefix was found, use it if new_prefix: self.logger.debug("[guess]: Common prefix found") # Search and remove any version info found as prefix m = re.search(r'_+[0-9]+|_+$', new_prefix) if m: new_prefix = new_prefix[:m.start()] else: self.logger.debug("[guess]: Using prefix from latest") # Try to use the latest_release prefix head = self.guess_latest_release() new_prefix = head[1] # At this point, new_prefix can still be None if not new_suffix: self.logger.debug("[guess]: Guessing new suffix") self.logger.debug("[guess]: find latest release") # Guess the latest release head = self.guess_latest_release() if head[3]: self.logger.debug("[guess]: Got suffix from latest") prev_ver = head[3] # Bump the previous release version self.logger.debug("[guess]: Bumping release") new_ver = bump_version(prev_ver, abi_break) new_suffix = "".join(("_" + str(i) for i in new_ver if i is not None)) if not new_prefix or not new_suffix: # ERROR: could not guess the name msg = "Insufficient information to guess the new release"\ " name. Releases found do not have version"\ " information or a valid library name. Please"\ " provide the complete name of the release." self.logger.error(msg) raise Exception(msg) # Return the combination of the prefix and version return new_prefix.upper() + new_suffix
[docs] def sort_releases_nice(self, top_release): """ Sort the releases contained in a map file putting the dependencies of ``top_release`` first. This changes the order of the list in ``releases``. :param top_release: The release whose dependencies should be prioritized """ if not self.init: msg = "Map not checked, run check()" self.logger.error(msg) raise Exception(msg) self.releases.sort(key=lambda release: release.name, reverse=True) dependencies = self.dependencies() top_dependency = next((dependency for dependency in dependencies if dependency[0] == top_release)) new_list = [] index = 0 while self.releases: release = self.releases.pop() if release.name in top_dependency: new_list.insert(index, release) index += 1 else: new_list.append(release) self.releases = new_list
[docs]class Release(object): """ A internal representation of a release version and its symbols A release is usually identified by the library name (suffix) and the release version (suffix). A release contains symbols, grouped by their visibility scope (global or local). In this class the symbols of a release are stored in a list of dictionaries mapping a visibility scope name (e.g. \"global\") to a list of the contained symbols: :: ([{"global": [symbols]}, {"local": [local_symbols]}]) Attributes: name: The release name previous: The previous release to which this release is dependent symbols: The symbols contained in the release, grouped by the visibility scope. """ def __init__(self): self.name = '' self.previous = '' self.released = False self.symbols = dict() def __str__(self): released = "" vs = [] visibilities = sorted(self.symbols.keys()) if self.released: released = " # Released" for v in visibilities: symbols = sorted(self.symbols[v]) vs.extend([" " * 4, v, ":\n", "".join((" " * 8 + symbol + ";\n" for symbol in symbols))]) content = "".join(chain(self.name, released, "\n", "{\n", vs, "} ", self.previous, ";\n")) return content
[docs] def duplicates(self): duplicates = [] for scope, symbols in (self.symbols.items()): seen = set() release_dups = set() if symbols: for symbol in symbols: if symbol not in seen: seen.add(symbol) else: release_dups.add(symbol) if release_dups: duplicates.append((scope, list(release_dups))) return duplicates
############################################################################### # Utility functions ###############################################################################
[docs]def get_version_from_string(version_string): """ Get the version numbers from a string :param version_string: A string composed by numbers separated by non \ alphanumeric characters (e.g. 0_1_2 or 0.1.2) :returns: A list of the numbers in the string """ # Get logger logger = Single_Logger.getLogger(__name__) m = re.findall(r'[0-9]+', version_string) if m: if len(m) < 2: logger.warn("Provide at least a major and a minor" " version digit (eg. '1.2.3' or '1_2')") if len(m) > 3: logger.warn("Version has too many parts; provide 3 or less" " ( e.g. '0.1.2')") else: msg = "Could not get version parts. Provide digits separated"\ " by non-alphanumeric characters. (e.g. 0_1_2 or 0.1.2)" logger.error(msg) raise Exception(msg) version = [int(i) for i in m] return version
[docs]def get_info_from_release_string(release): """ Get the information from a release name The given string is split in a prefix (usually the name of the lib) and a suffix (the version part, e.g. '_1_4_7'). A list with the version info converted to ints is also contained in the returned list. :param release: A string in format 'LIBX_1_0_0' or similar :returns: A list in format [release, prefix, suffix, [CUR, AGE, REV]] """ # Get logger logger = Single_Logger.getLogger(__name__) version = [None, None, None] ver_suffix = None prefix = None tail = None if not release: logger.warn("No release provided") return None # Remove eventual white spaces release = release.lstrip() # Search for the first ocurrence of a version like sequence m = re.search(r'_+[0-9]+', release) if m: # If found, remove the version like sequence to get the prefix prefix = release[:m.start()] tail = release[m.start():] else: # Check if the prefix contain at least a letter m = re.findall(r'[a-zA-Z]+', release) if m: prefix = release else: # If not, reject the prefix logger.warn("Release provided is not well formed" " (a well formed release contain the library" " identifier and the version information)." " Suggested: something like LIBNAME_1_2_3") return None if tail: # Search and get the version information version = get_version_from_string(tail) ver_suffix = "".join(["_" + str(i) for i in version if i is not None]) if prefix: # The prefix can have trailing '_' prefix = prefix.rstrip("_") # Fix common unsupported characters in prefix prefix = prefix.replace("-", "_") # Return the information got return [release.upper(), prefix.upper(), ver_suffix, version]
# TODO: Make bump strategy customizable
[docs]def bump_version(version, abi_break): """ Bump a version depending if the ABI was broken or not If the ABI was broken, CUR is bumped; AGE and REV are set to zero. Otherwise, CUR is kept, AGE is bumped, and REV is set to zero. This also works with versions without the REV component (e.g. [1, 4, None]) :param version: A list in format [CUR, AGE, REV] :param abi_break: A boolean indication if the ABI was broken :returns: A list in format [CUR, AGE, REV] """ new_version = [] if abi_break: if version[0] is not None: new_version.append(version[0] + 1) new_version.extend([0] * len(version[1:])) else: if version[0] is not None: new_version.append(version[0]) if version[1] is not None: new_version.append(version[1] + 1) new_version.extend([0] * len(version[2:])) return new_version
[docs]def clean_symbols(symbols): """ Receives a list of lines read from the input and returns a list of words :param symbols: A list of lines containing symbols :returns: A list of the obtained symbols """ # Get logger logger = Single_Logger.getLogger(__name__) # Split the lines into potential symbols and remove invalid characters clean = [] if symbols: no_invalid = chain(*(re.split(r'\W+', i) for i in symbols)) clean.extend((i for i in no_invalid if i)) # Report duplicated symbols if clean: previous = None duplicates = set() for i in clean: if not previous: previous = i else: if previous == i: duplicates.add(previous) previous = i if duplicates: dup_list = "".join((" " * 4 + dup + "\n" for dup in sorted(duplicates))) logger.warn("Duplicated symbols provided:\n%s", dup_list) return clean
[docs]def check_files(out_arg, out_name, in_arg, in_name, dry): """ Check if output and input are the same file. Create a backup if so. :param out_arg: The name of the option used to receive output file name :param out_name: The received string as output file path :param in_arg: The name of the option used to receive input file name :param in_name: The received string as input file path """ # Get logger logger = Single_Logger.getLogger(__name__) # Check if the first file exists if os.path.isfile(out_name): # Check if given input file is the same as output if os.path.isfile(in_name): if os.path.samefile(out_name, in_name): logger.warn("Given paths in \'%s\' and \'%s\' are the same.", str(out_arg), str(in_arg)) # Avoid changing the files if this is a dry run if dry: return logger.warn("Moving \'%s\' to \'%s.old\'.", str(in_name), str(in_name)) try: # If it is the case, copy to another file to # preserve the content shutil.copy2(str(in_name), str(in_name) + ".old") except Exception as e: logger.error("Could no copy \'%s\' to \'%s.old\'." " Aborting.", str(in_name), str(in_name)) raise e
[docs]def get_info_from_args(args): """ Get the release information from the provided arguments It is possible to set the new release name to be used through the command line arguments. :param args: Arguments given in command line parsed by argparse """ # Get logger logger = Single_Logger.getLogger(__name__) release_info = None if args.release: # Parse the release name string to get info release_info = get_info_from_release_string(args.release) if args.name: m = re.search(r'\w+', args.name) if m: release_info[1] = m.group() if args.version: version = get_version_from_string(args.version) new_suffix = "".join(("_" + str(i) for i in version)) release_info[2] = new_suffix release_info[3] = version if release_info: if release_info[1] and release_info[2]: release_info[0] = release_info[1] + release_info[2] elif args.name and args.version: # Parse the given version string to get the version information version = get_version_from_string(args.version) # Create a release string rel_string = "_".join([args.name] + [str(i) for i in version]) # Parse the release string release_info = get_info_from_release_string(rel_string) else: if not args.guess or args.func == new: msg = "It is necessary to provide either release name or"\ " name and version" logger.error(msg) raise Exception(msg) return release_info
############################################################################### # INTERFACE ###############################################################################
[docs]def update(args): """ Given the new list of symbols, update the map The new map will be generated by the following rules: - If new symbols are added, a new release is created containing the new symbols. This is a compatible update. - If a previous existing symbol is removed, then all releases are unified in a new release. This is an incompatible change, the SONAME of the library should be bumped The symbols provided are considered all the exported symbols in the new version. Such set of symbols is compared to the previous existing symbols. If symbols are added, but nothing removed, it is a compatible change. Otherwise, it is an incompatible change and the SONAME of the library should be bumped. If --add is provided, the symbols provided are considered new symbols to be added. This is a compatible change. If --remove is provided, the symbols provided are considered the symbols to be removed. This is an incompatible change and the SONAME of the library should be bumped. :param args: Arguments given in command line parsed by argparse """ # Get logger logger = Single_Logger.getLogger(__name__, filename=args.logfile) logger.info("Command: update") logger.debug("Arguments provided: ") logger.debug(str(args)) # Set the verbosity if provided if args.verbosity: logger.setLevel(VERBOSITY_MAP[args.verbosity]) # If output would be overwritten, print a warning if args.out: if os.path.isfile(args.out): logger.warn("Overwriting existing file \'%s\'", args.out) # If both output and input files were given, check if are the same if args.out and args.input: check_files('--out', args.out, '--in', args.input, args.dry) # If output is given, check with the file to be updated if args.out and args.file: check_files('--out', args.out, 'file', args.file, args.dry) # Get the release information provided in the arguments release_info = get_info_from_args(args) # Read the current map file cur_map = Map(filename=args.file, logger=logger) # Get all global symbols (it is a set) all_symbols = cur_map.all_global_symbols() # Generate the list of the new symbols new_symbols = [] lines = None if args.input: with open(args.input, "r") as symbols_fp: lines = symbols_fp.readlines() else: # Read from stdin lines = sys.stdin.readlines() for line in lines: new_symbols.extend(line.split()) # Clean the input removing invalid symbols new_symbols = clean_symbols(new_symbols) # All symbols read new_set = set(new_symbols) added_set = set() removed_set = set() # If the list of symbols are being added if args.add: # Check the symbols and print a warning if already present for symbol in new_set: if symbol in all_symbols: logger.warn("The symbol \'%s\' is already" " present in a previous version. Keep the" " previous implementation to not break ABI.", symbol) added_set.update(new_set) # If the list of symbols are being removed elif args.remove: # Remove the symbols to be removed for symbol in new_set: if symbol in all_symbols: removed_set.add(symbol) else: logger.warn("Requested to remove \'%s\', but not found.", symbol) # If the list of all symbols are being compared (the default option) else: for symbol in new_set: if symbol not in all_symbols: added_set.add(symbol) for symbol in all_symbols: if symbol not in new_set: removed_set.add(symbol) # Make lists from the sets added = list(added_set) removed = list(removed_set) # Print the modifications if added: added.sort() msg = "".join(chain("Added:\n", (" " + symbol + "\n" for symbol in added))) print(msg) if removed: removed.sort() msg = "".join(chain("Removed:\n", (" " + symbol + "\n" for symbol in removed))) print(msg) # Guess the latest release latest = cur_map.guess_latest_release() if not added and not removed: print("No symbols added or removed. Nothing done.") return r = None if added: if release_info: for to_up in cur_map.releases: if to_up and to_up.name == release_info[0]: # If the release to be modified is released if to_up.released: msg = "Released releases cannot be modified. Abort." logger.error(msg) raise Exception(msg) r = to_up if not r: r = Release() # Guess the name for the new release r.name = cur_map.guess_name(release_info, guess=args.guess) r.name.upper() r.symbols['global'] = [] if not removed: # Add the name for the previous release r.previous = latest[0] # Put the release on the map cur_map.releases.append(r) # If this is the final change to the release, mark as released if args.final: r.released = True # Add the symbols added to global scope r.symbols['global'].extend(added) if removed: if not args.allow_abi_break: msg = "ABI break detected: symbols would be removed" logger.error(msg) raise Exception(msg) logger.warn("ABI break detected: symbols were removed.") print("Merging all symbols in a single new release") new_map = Map() r = Release() # Guess the name of the new release r.name = cur_map.guess_name(release_info, abi_break=True, guess=args.guess) r.name.upper() # Add the symbols added to global scope all_symbols.update(added_set) # Remove the '*' wildcard, if present if '*' in all_symbols: logger.warn("Wildcard \'*\' found in global. Removed to avoid" " exporting unexpected symbols.") all_symbols.remove('*') # Remove the symbols to be removed and convert to a list all_symbols_list = [symbol for symbol in all_symbols if symbol not in removed_set] # Update the global symbols r.symbols.update({'global': all_symbols_list}) # Add the wildcard to the local symbols r.symbols.update({'local': ['*']}) # If this is the final change to the release, mark as released if args.final: r.released = True # Put the release on the map new_map.releases.append(r) # Substitute the map cur_map = new_map # Do a structural check cur_map.check() # Sort the releases putting the new release and dependencies first cur_map.sort_releases_nice(r.name) if args.dry: print("This is a dry run, the files were not modified.") return try: if args.out: f = open(args.out, "w") else: f = sys.stdout # Set the name of the application in the output name_version = None if args.program: name_version = "{0}-{1}".format(args.program, __version__) else: name_version = "abimap-{0}".format(__version__) f.write("# This map file was updated with" " {0}\n\n".format(name_version)) f.write(str(cur_map)) finally: if args.out: f.close()
[docs]def new(args): """ \'new\' subcommand Create a new version script file containing the provided symbols. :param args: Arguments given in command line parsed by argparse """ # Get logger logger = Single_Logger.getLogger(__name__, filename=args.logfile) logger.info("Command: new") logger.debug("Arguments provided: ") logger.debug(str(args)) # Set the verbosity if provided if args.verbosity: logger.setLevel(VERBOSITY_MAP[args.verbosity]) # If output would be overwritten, print a warning if args.out: if os.path.isfile(args.out): logger.warn("Overwriting existing file \'%s\'.", args.out) # If both output and input files were given, check if are the same if args.out and args.input: check_files('--out', args.out, '--in', args.input, args.dry) # Get the release information provided in the arguments release_info = get_info_from_args(args) # In the new command, there is no way to guess the name, since there are # not previous information. So the exception have to be raised early to # avoid collecting the new symbols and then find out we do not have a name if not release_info: msg = "Please provide the release name." logger.error(msg) raise Exception(msg) logger.debug("Release info in args:") logger.debug(str(release_info)) # Generate the list of the new symbols new_symbols = [] lines = None if args.input: with open(args.input, "r") as symbols_fp: lines = symbols_fp.readlines() else: # Read from stdin lines = sys.stdin.readlines() for line in lines: new_symbols.extend(line.split()) # Clean the input removing invalid symbols new_symbols = clean_symbols(new_symbols) new_symbols_set = set(new_symbols) if new_symbols: new_map = Map() r = Release() name = new_map.guess_name(release_info) debug_msg = "Generated name: \'{}\'".format(name) logger.debug(debug_msg) # Set the name of the new release r.name = name.upper() # Add the symbols to global scope r.symbols['global'] = list(new_symbols_set) # Add the wildcard to the local symbols r.symbols['local'] = ['*'] if args.final: r.released = True # Put the release on the map new_map.releases.append(r) # Do a structural check new_map.check() # Sort the releases putting the new release and dependencies first new_map.sort_releases_nice(r.name) if args.dry: print("This is a dry run, the files were not modified.") return try: if args.out: f = open(args.out, "w") else: f = sys.stdout # Set the name of the application in the output name_version = None if args.program: name_version = "{0}-{1}".format(args.program, __version__) else: name_version = "abimap-{0}".format(__version__) f.write("# This map file was created with" " {0}\n\n".format(name_version)) f.write(str(new_map)) finally: if args.out: f.close() else: logger.warn("No valid symbols provided. Nothing done.")
[docs]def check(args): """ \'check\' subcommand Check the content of a symbol version script :param args: Arguments given in command line parsed by argparse """ # Get logger logger = Single_Logger.getLogger(__name__, filename=args.logfile) logger.info("Command: check") logger.debug("Arguments provided: ") logger.debug(str(args)) # Set the verbosity if provided if args.verbosity: logger.setLevel(VERBOSITY_MAP[args.verbosity]) # Read the map file abimap = Map(filename=args.file, logger=logger) # Check the map file abimap.check()
[docs]def version(args): """ \'version\' subcommand Prints and returns the program name and version. :param args: Arguments given in command line parsed by argparse :returns: A string containing the program name and version """ if args.program: name_version = "{0}-{1}".format(args.program, __version__) else: name_version = "abimap-{0}".format(__version__) print(name_version) return name_version
[docs]def get_arg_parser(): """ Get a parser for the command line arguments The parser is capable of checking requirements for the arguments and possible incompatible arguments. :returns: A parser for command line arguments. (argparse.ArgumentParser) """ # Common file arguments file_args = argparse.ArgumentParser(add_help=False) file_args.add_argument('-o', '--out', help='Output file (defaults to stdout)') file_args.add_argument('-i', '--in', help='Read from this file instead of stdio', dest='input') file_args.add_argument('-d', '--dry', help='Do everything, but do not modify the files', action='store_true') # Common verbosity arguments verb_args = argparse.ArgumentParser(add_help=False) group_verb = verb_args.add_mutually_exclusive_group() group_verb.add_argument('--verbosity', help='Set the program verbosity', choices=['quiet', 'error', 'warning', 'info', 'debug'], default='warning') group_verb.add_argument('--quiet', help='Makes the program quiet', dest='verbosity', action='store_const', const='quiet') group_verb.add_argument('--debug', help='Makes the program print debug info', dest='verbosity', action='store_const', const='debug') verb_args.add_argument('-l', '--logfile', help='Log to this file') # Common release name arguments name_args = argparse.ArgumentParser(add_help=False) name_args.add_argument("-n", "--name", help="The name of the library (e.g. libx)") name_args.add_argument("-v", "--version", help="The release version (e.g. 1_0_0 or 1.0.0)") name_args.add_argument("-r", "--release", help="The full name of the release to be used" " (e.g. LIBX_1_0_0)") name_args.add_argument("--no_guess", help="Disable next release name guessing", action="store_false", dest="guess") # Main arguments parser parser = argparse.ArgumentParser(description="Helper tools for linker" " version script maintenance", epilog="Call a subcommand passing \'-h\'" " to see its specific options") # Subcommands parser subparsers = parser.add_subparsers(title="Subcommands", help="These subcommands have their own" " set of options", dest="subcommand") subparsers.required = True # Update subcommand parser parser_up = subparsers.add_parser("update", help="Update the map file", parents=[file_args, verb_args, name_args], epilog="A list of symbols is expected as" " the input.\nIf a file is provided with" " \'-i\', the symbols are read" " from the given file. Otherwise the" " symbols are read from stdin.") parser_up.add_argument("--allow-abi-break", help="Allow removing symbols, and to break ABI", action='store_true') parser_up.add_argument("-f", "--final", help="Mark the modified release as final," " preventing later changes.", action='store_true') group = parser_up.add_mutually_exclusive_group() group.add_argument("-a", "--add", help="Adds the symbols to the map file.", action='store_true') group.add_argument("--remove", help="Remove the symbols from the map" " file. This breaks the ABI.", action="store_true") parser_up.add_argument('file', help='The map file being updated') parser_up.set_defaults(func=update) # New subcommand parser parser_new = subparsers.add_parser("new", help="Create a new map file", parents=[file_args, verb_args, name_args], epilog="A list of symbols is expected" " as the input.\nIf a file is provided" " with \'-i\', the symbols are read" " from the given file. Otherwise the" " symbols are read from stdin.") parser_new.add_argument("-f", "--final", help="Mark the new release as final," " preventing later changes.", action='store_true') parser_new.set_defaults(func=new) # Check subcommand parser parser_check = subparsers.add_parser("check", help="Check the map file", parents=[verb_args]) parser_check.add_argument("file", help="The map file to be checked") parser_check.set_defaults(func=check) # Version subcommand parser parser_version = subparsers.add_parser("version", help="Print version") parser_version.set_defaults(func=version) return parser