From bbb77343a96302afbd78d589fa322cd56b98f1e8 Mon Sep 17 00:00:00 2001 From: Chris Johns Date: Fri, 14 Apr 2023 13:40:43 +1000 Subject: Add version and git support for apps to use --- git.py | 226 +++++++++++++++++++++++++++++++++++++++++++++++++++ version.py | 270 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 496 insertions(+) create mode 100644 git.py create mode 100644 version.py diff --git a/git.py b/git.py new file mode 100644 index 0000000..1c90f0d --- /dev/null +++ b/git.py @@ -0,0 +1,226 @@ +# +# RTEMS Tools Project (http://www.rtems.org/) +# Copyright 2010-2016, 2023 Chris Johns (chrisj@rtems.org) +# All rights reserved. +# +# This file is part of the RTEMS Tools package in 'rtems-tools'. +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +# +# Provide some basic access to the git command. +# + +from __future__ import print_function + +import os +import os.path + +class repo: + """An object to manage a git repo.""" + + def _git_exit_code(self, ec): + if ec: + raise self.ctx.fatal('git command failed (%s): %d' % + (self.git, ec)) + + def _run(self, args, check=False): + import waflib + if os.path.exists(self.path): + cwd = self.path + else: + cwd = None + cmd = [self.git] + args + exit_code = 0 + try: + output = self.ctx.cmd_and_log(cmd, + cwd=cwd, + output=waflib.Context.STDOUT, + quiet=waflib.Context.BOTH) + except waflib.Errors.WafError as e: + exit_code = e.returncode + output = e.stderr + if check: + self._git_exit_code(exit_code) + return exit_code, output + + def __init__(self, ctx, path): + self.ctx = ctx + self.path = path + self.git = 'git' + + def git_version(self): + ec, output = self._run(['--version'], True) + gvs = output.split() + if len(gvs) < 3: + raise self.ctx.fatal('invalid version string from git: %s' % + (output)) + vs = gvs[2].split('.') + if len(vs) not in [3, 4]: + raise self.ctx.fatal('invalid version number from git: %s' % + (gvs[2])) + return tuple(map(int, vs)) + + def clone(self, url, path): + ec, output = self._run(['clone', url, path], check=True) + + def fetch(self): + ec, output = self._run(['fetch'], check=True) + + def merge(self): + ec, output = self._run(['merge'], check=True) + + def pull(self): + ec, output = self._run(['pull'], check=True) + + def reset(self, args): + if type(args) == str: + args = [args] + ec, output = self._run(['reset'] + args, check=True) + + def branch(self): + ec, output = self._run(['branch']) + if ec == 0: + for b in output.split(os.linesep): + if b[0] == '*': + return b[2:] + return None + + def checkout(self, branch='master'): + ec, output = self._run(['checkout', branch], check=True) + + def submodule(self, module): + ec, output = self._run(['submodule', 'update', '--init', module], + check=True) + + def submodule_foreach(self, args=[]): + if type(args) == str: + args = [args.split(args)] + ec, output = self._run( + ['submodule', 'foreach', '--recursive', self.git] + args, + check=True) + + def submodules(self): + smodules = {} + ec, output = self._run(['submodule'], check=True) + if ec == 0: + for l in output.split(os.linesep): + ms = l.split() + if len(ms) == 3: + smodules[ms[1]] = (ms[0], ms[2][1:-1]) + return smodules + + def clean(self, args=[]): + if type(args) == str: + args = [args] + ec, output = self._run(['clean'] + args, check=True) + + def status(self, submodules_always_clean=False): + _status = {} + if os.path.exists(self.path): + if submodules_always_clean: + submodules = self.submodules() + else: + submodules = {} + ec, output = self._run(['status']) + if ec == 0: + state = 'none' + for l in output.split(os.linesep): + if l.startswith('# '): + l = l[2:] + if l.startswith('On branch '): + _status['branch'] = l[len('On branch '):] + elif l.startswith('Changes to be committed:'): + state = 'staged' + elif l.startswith('Changes not staged for commit:'): + state = 'unstaged' + elif l.startswith('Untracked files:'): + state = 'untracked' + elif l.startswith('HEAD detached'): + state = 'detached' + elif state != 'none' and len(l.strip()) != 0: + if l[0].isspace(): + l = l.strip() + if l[0] != '(': + if ':' in l: + l = l.split(':')[1] + if len(l.strip()) > 0: + l = l.strip() + ls = l.split() + if state != 'unstaged' or ls[ + 0] not in submodules: + if state not in _status: + _status[state] = [l] + else: + _status[state] += [l] + return _status + + def dirty(self): + _status = self.status() + _status.pop('untracked', None) + _status.pop('detached', None) + return not (len(_status) == 1 and 'branch' in _status) + + def valid(self): + if os.path.exists(self.path): + ec, output = self._run(['status']) + return ec == 0 + return False + + def remotes(self): + _remotes = {} + ec, output = self._run(['config', '--list']) + if ec == 0: + for l in output.split(os.linesep): + if l.startswith('remote'): + ls = l.split('=') + if len(ls) >= 2: + rs = ls[0].split('.') + if len(rs) == 3: + r_name = rs[1] + r_type = rs[2] + if r_name not in _remotes: + _remotes[r_name] = {} + if r_type not in _remotes[r_name]: + _remotes[r_name][r_type] = [] + _remotes[r_name][r_type] = '='.join(ls[1:]) + return _remotes + + def email(self): + _email = None + _name = None + ec, output = self._run(['config', '--list']) + if ec == 0: + for l in output.split(os.linesep): + if l.startswith('user.email'): + ls = l.split('=') + if len(ls) >= 2: + _email = ls[1] + elif l.startswith('user.name'): + ls = l.split('=') + if len(ls) >= 2: + _name = ls[1] + if _email is not None: + if _name is not None: + _email = '%s <%s>' % (_name, _email) + return _email + return None + + def head(self): + hash = '' + ec, output = self._run(['log', '-n', '1']) + if ec == 0: + l1 = output.split(os.linesep)[0] + if l1.startswith('commit '): + hash = l1[len('commit '):] + return hash diff --git a/version.py b/version.py new file mode 100644 index 0000000..87e18c5 --- /dev/null +++ b/version.py @@ -0,0 +1,270 @@ +# +# RTEMS Tools Project (http://www.rtems.org/) +# Copyright 2010-2018,2023 Chris Johns (chrisj@rtems.org) +# All rights reserved. +# +# This file is part of the RTEMS Tools package in 'rtems-tools'. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# + +# +# Releasing RTEMS Tools +# --------------------- +# +# Format: +# +# The format is INI. The file requires a `[version`] section and a `revision` +# option: +# +# [version] +# revision = +# +# The `` has the `version` and `revision` delimited by a +# single `.`. An example file is: +# +# [version] +# revision = 5.0.not_released +# +# where the `version` is `5` and the revision is `0` and the package is not +# released. The label `not_released` is reversed to mean the package is not +# released. A revision string can contain extra characters after the +# `revision` number for example `5.0-rc1` or is deploying a package +# `5.0-nasa-cfs` +# +# Packages can optionally add specialised sections to a version configuration +# files. These can be accessed via the: +# +# load_release_settings: Return the items in a section +# load_release_setting: Return an item from a section +# +# User deployment: +# +# Create a git archive and then add a suitable VERSION file to the top +# directory of the package. The package assumes your python executable is +# location in `bin` directory which is one below the top of the package's +# install prefix. +# +# RTEMS Release: +# +# Set the values in the `rtems-version.ini` file. This is a shared file so +# packages and encouraged to add specific settings to other configuration +# files. +# +# Notes: +# +# This module uses os.apth for paths and assumes all paths are in the host +# format. +# + +from __future__ import print_function + +import itertools +import os +import sys + +try: + import configparser +except ImportError: + import ConfigParser as configparser + +from . import git +from . import rtems + +# +# Default to an internal string. +# +_version = 'undefined' +_revision = 'not_released' +_version_str = '%s.%s' % (_version, _revision) +_released = False +_git = False +_is_loaded = False + + +def _top(ctx): + top = ctx.path + if top == None: + cts.fatal('no top path found') + return str(top) + +def _load_released_version_config(ctx): + '''Local worker to load a configuration file.''' + top = _top(ctx) + for ver in [os.path.join(top, 'VERSION')]: + if os.path.exists(os.path.join(ver)): + v = configparser.SafeConfigParser() + try: + v.read(os.path.host(ver)) + except Exception as e: + raise ctx.fatal('invalid version config format: %s: %s' % + (ver, e)) + return ver, v + return None, None + + +def _load_released_version(ctx): + '''Load the release data if present. If not found the package is not + released. + + A release can be made by adding a file called `VERSION` to the top level + directory of a package. This is useful for user deploying a package and + making custom releases. + ''' + global _version + global _revision + global _released + global _version_str + global _is_loaded + + if not _is_loaded: + vc, v = _load_released_version_config(ctx) + if v is not None: + try: + ver_str = v.get('version', 'revision') + except Exception as e: + raise ctx.fatal('invalid version file: %s: %s' % (vc, e)) + ver_split = ver_str.split('.') + if len(ver_split) < 2: + raise ctx.fatal('invalid version release value: %s: %s' % + (vc, ver_str)) + ver = ver_split[0] + rev = '.'.join(ver_split[1:]) + try: + _version = int(ver) + except: + raise ctx.fatal('invalid version config value: %s: %s' % + (vc, ver)) + try: + _revision = int(''.join( + itertools.takewhile(str.isdigit, str(rev)))) + except Exception as e: + raise ctx.fatal('Invalid revision config value: %s: %s: %s' % + (vc, rev, e)) + if not 'not_released' in ver: + _released = True + _version_str = ver_str + _is_loaded = True + return _released + + +def _load_git_version(ctx): + global _version + global _revision + global _git + global _version_str + repo = git.repo(ctx, _top(ctx)) + if repo.valid(): + head = repo.head() + if repo.dirty(): + modified = 'modified' + revision_sep = '-' + sep = ' ' + else: + modified = '' + revision_sep = '' + sep = '' + _revision = '%s%s%s' % (head[0:12], revision_sep, modified) + _version_str += ' (%s%s%s)' % (head[0:12], sep, modified) + _git = True + return _git + + +def load_release_settings(ctx, section, error=False): + vc, v = _load_released_version_config(ctx) + items = [] + if v is not None: + try: + items = v.items(section) + except Exception as e: + if not isinstance(error, bool): + error(e) + elif error: + raise ctx.fatal('Invalid config section: %s: %s: %s' % + (vc, section, e)) + return items + + +def load_release_setting(ctx, section, option, raw=False, error=False): + vc, v = _load_released_version_config() + value = None + if v is not None: + try: + value = v.get(section, option, raw=raw) + except Exception as e: + if not isinstance(error, bool): + error(e) + elif error: + raise ctx.fatal('Invalid config section: %s: %s: %s.%s' % + (vc, section, option, e)) + return value + + +def load_rtems_version_header(ctx, rtems_version, arch_bsp, incpaths): + global _version + global _revision + global _version_str + for inc in incpaths: + header = os.path.join(inc, 'rtems/score/cpuopts.h') + if os.path.exists(header): + try: + with open(header, 'r') as h: + text = h.readlines() + except: + ctx.fatal('cannot read: ' + header) + for l in text: + ls = l.split() + if len(ls) == 3: + if ls[1] == '__RTEMS_MAJOR__': + _version = int(ls[2]) + elif ls[1] == '__RTEMS_REVISION__': + _revision = int(ls[2]) + elif ls[1] == 'RTEMS_VERSION': + _version_str = ls[2][1:-1] + _is_loaded = True + break + +def released(ctx): + return _load_released_version(ctx) + + +def version_control(ctx): + return _load_git_version(ctx) + + +def string(ctx): + _load_released_version(ctx) + _load_git_version(ctx) + return _version_str + + +def version(ctx): + _load_released_version(ctx) + _load_git_version(ctx) + return _version + + +def revision(ctx): + _load_released_version(ctx) + _load_git_version(ctx) + return _revision -- cgit v1.2.3