File:  [ELWIX - Embedded LightWeight unIX -] / embedaddon / rsync / packaging / pkglib.py
Revision 1.1.1.1 (vendor branch): download - view: text, annotated - select for diffs - revision graph
Wed Mar 17 00:32:36 2021 UTC (3 years, 3 months ago) by misho
Branches: rsync, MAIN
CVS tags: v3_2_3, HEAD
rsync 3.2.3

    1: import os, sys, re, subprocess
    2: 
    3: # This python3 library provides a few helpful routines that are
    4: # used by the latest packaging scripts.
    5: 
    6: default_encoding = 'utf-8'
    7: 
    8: # Output the msg args to stderr.  Accepts all the args that print() accepts.
    9: def warn(*msg):
   10:     print(*msg, file=sys.stderr)
   11: 
   12: 
   13: # Output the msg args to stderr and die with a non-zero return-code.
   14: # Accepts all the args that print() accepts.
   15: def die(*msg):
   16:     warn(*msg)
   17:     sys.exit(1)
   18: 
   19: 
   20: # Set this to an encoding name or set it to None to avoid the default encoding idiom.
   21: def set_default_encoding(enc):
   22:     default_encoding = enc
   23: 
   24: 
   25: # Set shell=True if the cmd is a string; sets a default encoding unless raw=True was specified.
   26: def _tweak_opts(cmd, opts, **maybe_set):
   27:     # This sets any maybe_set value that isn't already set AND creates a copy of opts for us.
   28:     opts = {**maybe_set, **opts}
   29: 
   30:     if type(cmd) == str:
   31:         opts = {'shell': True, **opts}
   32: 
   33:     want_raw = opts.pop('raw', False)
   34:     if default_encoding and not want_raw:
   35:         opts = {'encoding': default_encoding, **opts}
   36: 
   37:     capture = opts.pop('capture', None)
   38:     if capture:
   39:         if capture == 'stdout':
   40:             opts = {'stdout': subprocess.PIPE, **opts}
   41:         elif capture == 'stderr':
   42:             opts = {'stderr': subprocess.PIPE, **opts}
   43:         elif capture == 'output':
   44:             opts = {'stdout': subprocess.PIPE, 'stderr': subprocess.PIPE, **opts}
   45:         elif capture == 'combined':
   46:             opts = {'stdout': subprocess.PIPE, 'stderr': subprocess.STDOUT, **opts}
   47: 
   48:     discard = opts.pop('discard', None)
   49:     if discard:
   50:         # We DO want to override any already set stdout|stderr values (unlike above).
   51:         if discard == 'stdout' or discard == 'output':
   52:             opts['stdout'] = subprocess.DEVNULL
   53:         if discard == 'stderr' or discard == 'output':
   54:             opts['stderr'] = subprocess.DEVNULL
   55: 
   56:     return opts
   57: 
   58: 
   59: # This does a normal subprocess.run() with some auto-args added to make life easier.
   60: def cmd_run(cmd, **opts):
   61:     return subprocess.run(cmd, **_tweak_opts(cmd, opts))
   62: 
   63: 
   64: # Like cmd_run() with a default check=True specified.
   65: def cmd_chk(cmd, **opts):
   66:     return subprocess.run(cmd, **_tweak_opts(cmd, opts, check=True))
   67: 
   68: 
   69: # Capture stdout in a string and return the (output, return_code) tuple.
   70: # Use capture='combined' opt to get both stdout and stderr together.
   71: def cmd_txt_status(cmd, **opts):
   72:     input = opts.pop('input', None)
   73:     if input is not None:
   74:         opts['stdin'] = subprocess.PIPE
   75:     proc = subprocess.Popen(cmd, **_tweak_opts(cmd, opts, capture='stdout'))
   76:     out = proc.communicate(input=input)[0]
   77:     return (out, proc.returncode)
   78: 
   79: 
   80: # Like cmd_txt_status() but just return the output.
   81: def cmd_txt(cmd, **opts):
   82:     return cmd_txt_status(cmd, **opts)[0]
   83: 
   84: 
   85: # Capture stdout in a string and return the output if the command has a 0 return code.
   86: # Otherwise it throws an exception that indicates the return code and the output.
   87: def cmd_txt_chk(cmd, **opts):
   88:     out, rc = cmd_txt_status(cmd, **opts)
   89:     if rc != 0:
   90:         cmd_err = f'Command "{cmd}" returned non-zero exit status "{rc}" and output:\n{out}'
   91:         raise Exception(cmd_err)
   92:     return out
   93: 
   94: 
   95: # Starts a piped-output command of stdout (by default) and leaves it up to you to read
   96: # the output and call communicate() on the returned object.
   97: def cmd_pipe(cmd, **opts):
   98:     return subprocess.Popen(cmd, **_tweak_opts(cmd, opts, capture='stdout'))
   99: 
  100: 
  101: # Runs a "git status" command and dies if the checkout is not clean (the
  102: # arg fatal_unless_clean can be used to make that non-fatal.  Returns a
  103: # tuple of the current branch, the is_clean flag, and the status text.
  104: def check_git_status(fatal_unless_clean=True, subdir='.'):
  105:     status_txt = cmd_txt_chk(f"cd '{subdir}' && git status")
  106:     is_clean = re.search(r'\nnothing to commit.+working (directory|tree) clean', status_txt) != None
  107: 
  108:     if not is_clean and fatal_unless_clean:
  109:         if subdir == '.':
  110:             subdir = ''
  111:         else:
  112:             subdir = f" *{subdir}*"
  113:         die(f"The{subdir} checkout is not clean:\n" + status_txt)
  114: 
  115:     m = re.match(r'^(?:# )?On branch (.+)\n', status_txt)
  116:     cur_branch = m[1] if m else None
  117: 
  118:     return (cur_branch, is_clean, status_txt)
  119: 
  120: 
  121: # Calls check_git_status() on the current git checkout and (optionally) a subdir path's
  122: # checkout. Use fatal_unless_clean to indicate if an unclean checkout is fatal or not.
  123: # The master_branch arg indicates what branch we want both checkouts to be using, and
  124: # if the branch is wrong the user is given the option of either switching to the right
  125: # branch or aborting.
  126: def check_git_state(master_branch, fatal_unless_clean=True, check_extra_dir=None):
  127:     cur_branch = check_git_status(fatal_unless_clean)[0]
  128:     branch = re.sub(r'^patch/([^/]+)/[^/]+$', r'\1', cur_branch) # change patch/BRANCH/PATCH_NAME into BRANCH
  129:     if branch != master_branch:
  130:         print(f"The checkout is not on the {master_branch} branch.")
  131:         if master_branch != 'master':
  132:             sys.exit(1)
  133:         ans = input(f"Do you want me to continue with --branch={branch}? [n] ")
  134:         if not ans or not re.match(r'^y', ans, flags=re.I):
  135:             sys.exit(1)
  136:         master_branch = branch
  137: 
  138:     if check_extra_dir and os.path.isdir(os.path.join(check_extra_dir, '.git')):
  139:         branch = check_git_status(fatal_unless_clean, check_extra_dir)[0]
  140:         if branch != master_branch:
  141:             print(f"The *{check_extra_dir}* checkout is on branch {branch}, not branch {master_branch}.")
  142:             ans = input(f"Do you want to change it to branch {master_branch}? [n] ")
  143:             if not ans or not re.match(r'^y', ans, flags=re.I):
  144:                 sys.exit(1)
  145:             subdir.check_call(f"cd {check_extra_dir} && git checkout '{master_branch}'", shell=True)
  146: 
  147:     return (cur_branch, master_branch)
  148: 
  149: 
  150: # Return the git hash of the most recent commit.
  151: def latest_git_hash(branch):
  152:     out = cmd_txt_chk(['git', 'log', '-1', '--no-color', branch])
  153:     m = re.search(r'^commit (\S+)', out, flags=re.M)
  154:     if not m:
  155:         die(f"Unable to determine commit hash for master branch: {branch}")
  156:     return m[1]
  157: 
  158: 
  159: # Return a set of all branch names that have the format "patch/BASE_BRANCH/NAME"
  160: # for the given base_branch string.  Just the NAME portion is put into the set.
  161: def get_patch_branches(base_branch):
  162:     branches = set()
  163:     proc = cmd_pipe('git branch -l'.split())
  164:     for line in proc.stdout:
  165:         m = re.search(r' patch/([^/]+)/(.+)', line)
  166:         if m and m[1] == base_branch:
  167:             branches.add(m[2])
  168:     proc.communicate()
  169:     return branches
  170: 
  171: 
  172: def mandate_gensend_hook():
  173:     hook = '.git/hooks/pre-push'
  174:     if not os.path.exists(hook):
  175:         print('Creating hook file:', hook)
  176:         cmd_chk(['./rsync', '-a', 'packaging/pre-push', hook])
  177:     else:
  178:         out, rc = cmd_txt_status(['fgrep', 'make gensend', hook], discard='output')
  179:         if rc:
  180:             die('Please add a "make gensend" into your', hook, 'script.')
  181: 
  182: 
  183: # Snag the GENFILES values out of the Makefile.in file and return them as a list.
  184: def get_gen_files(want_dir_plus_list=False):
  185:     cont_re = re.compile(r'\\\n')
  186: 
  187:     gen_files = [ ]
  188: 
  189:     auto_dir = os.path.join('auto-build-save', cmd_txt('git rev-parse --abbrev-ref HEAD').strip().replace('/', '%'))
  190: 
  191:     with open('Makefile.in', 'r', encoding='utf-8') as fh:
  192:         for line in fh:
  193:             if not gen_files:
  194:                 chk = re.sub(r'^GENFILES=', '', line)
  195:                 if line == chk:
  196:                     continue
  197:                 line = chk
  198:             m = re.search(r'\\$', line)
  199:             line = re.sub(r'^\s+|\s*\\\n?$|\s+$', '', line)
  200:             gen_files += line.split()
  201:             if not m:
  202:                 break
  203: 
  204:     if want_dir_plus_list:
  205:         return (auto_dir, gen_files)
  206: 
  207:     return [ os.path.join(auto_dir, fn) for fn in gen_files ]
  208: 
  209: 
  210: def get_rsync_version():
  211:     with open('version.h', 'r', encoding='utf-8') as fh:
  212:         txt = fh.read()
  213:     m = re.match(r'^#define\s+RSYNC_VERSION\s+"(\d.+?)"', txt)
  214:     if m:
  215:         return m[1]
  216:     die("Unable to find RSYNC_VERSION define in version.h")
  217: 
  218: 
  219: def get_NEWS_version_info():
  220:     rel_re = re.compile(r'^\| \S{2} \w{3} \d{4}\s+\|\s+(?P<ver>\d+\.\d+\.\d+)\s+\|\s+(?P<pdate>\d{2} \w{3} \d{4})?\s+\|\s+(?P<pver>\d+)\s+\|')
  221:     last_version = last_protocol_version = None
  222:     pdate = { }
  223: 
  224:     with open('NEWS.md', 'r', encoding='utf-8') as fh:
  225:         for line in fh:
  226:             if not last_version: # Find the first non-dev|pre version with a release date.
  227:                 m = re.search(r'rsync (\d+\.\d+\.\d+) .*\d\d\d\d', line)
  228:                 if m:
  229:                     last_version = m[1]
  230:             m = rel_re.match(line)
  231:             if m:
  232:                 if m['pdate']:
  233:                     pdate[m['ver']] = m['pdate']
  234:                 if m['ver'] == last_version:
  235:                     last_protocol_version = m['pver']
  236: 
  237:     if not last_protocol_version:
  238:         die(f"Unable to determine protocol_version for {last_version}.")
  239: 
  240:     return last_version, last_protocol_version, pdate
  241: 
  242: 
  243: def get_protocol_versions():
  244:     protocol_version = subprotocol_version = None
  245: 
  246:     with open('rsync.h', 'r', encoding='utf-8') as fh:
  247:         for line in fh:
  248:             m = re.match(r'^#define\s+PROTOCOL_VERSION\s+(\d+)', line)
  249:             if m:
  250:                 protocol_version = m[1]
  251:                 continue
  252:             m = re.match(r'^#define\s+SUBPROTOCOL_VERSION\s+(\d+)', line)
  253:             if m:
  254:                 subprotocol_version = m[1]
  255:                 break
  256: 
  257:     if not protocol_version:
  258:         die("Unable to determine the current PROTOCOL_VERSION.")
  259: 
  260:     if not subprotocol_version:
  261:         die("Unable to determine the current SUBPROTOCOL_VERSION.")
  262: 
  263:     return protocol_version, subprotocol_version
  264: 
  265: # vim: sw=4 et
  266: 

FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>